diff options
Diffstat (limited to 'src/tools/emcoui/src')
41 files changed, 4341 insertions, 1782 deletions
diff --git a/src/tools/emcoui/src/App.js b/src/tools/emcoui/src/App.js index 2613ecfd..3a2c5ffc 100644 --- a/src/tools/emcoui/src/App.js +++ b/src/tools/emcoui/src/App.js @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import { BrowserRouter as Router, @@ -53,7 +53,7 @@ function App() { <Redirect exact from={`${match.path}`} - to={`${match.path}/composite-apps`} + to={`${match.path}/services`} /> <Route path={`${match.path}`} diff --git a/src/tools/emcoui/src/admin/AdminNavigator.js b/src/tools/emcoui/src/admin/AdminNavigator.js index be07cba0..9cee73b1 100644 --- a/src/tools/emcoui/src/admin/AdminNavigator.js +++ b/src/tools/emcoui/src/admin/AdminNavigator.js @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import PropTypes from "prop-types"; import clsx from "clsx"; @@ -37,15 +37,15 @@ const categories = [ url: "/projects", }, { - id: "Clusters", - icon: <DnsRoundedIcon />, - url: "/clusters", - }, - { id: "Controllers", icon: <SettingsIcon />, url: "/controllers", }, + { + id: "Clusters", + icon: <DnsRoundedIcon />, + url: "/clusters", + }, ], }, ]; diff --git a/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx b/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx index 150a1912..57ee7557 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx @@ -11,147 +11,157 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField } from '@material-ui/core'; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import { TextField } from "@material-ui/core"; import * as Yup from "yup"; -import { Formik } from 'formik'; +import { Formik } from "formik"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - }) +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), +}); const ClusterProviderForm = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create" - const title = item ? "Edit Cluster Provider" : "Register Cluster Provider" - const handleClose = () => { - onClose(); - }; - let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" } + const { onClose, item, open, onSubmit } = props; + const buttonLabel = item ? "OK" : "Create"; + const title = item ? "Edit Cluster Provider" : "Register Cluster Provider"; + const handleClose = () => { + onClose(); + }; + let initialValues = item + ? { name: item.metadata.name, description: item.metadata.description } + : { name: "", description: "" }; - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - onSubmit(values); - }} - validationSchema={schema} - > - {props => { - const { - values, - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = props; - return ( - <form noValidate onSubmit={handleSubmit}> - <DialogContent dividers> - <TextField - style={{ width: "100%", marginBottom: "10px" }} - id="name" - label="Provider name" - type="text" - value={values.name} - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - value={values.description} - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + return ( + <Dialog + maxWidth={"xs"} + onClose={handleClose} + aria-labelledby="customized-dialog-title" + open={open} + disableBackdropClick + > + <DialogTitle id="simple-dialog-title">{title}</DialogTitle> + <Formik + initialValues={initialValues} + onSubmit={async (values) => { + onSubmit(values); + }} + validationSchema={schema} + > + {(props) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = props; + return ( + <form noValidate onSubmit={handleSubmit}> + <DialogContent dividers> + <TextField + style={{ width: "100%", marginBottom: "10px" }} + id="name" + label="Provider name" + type="text" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + helperText={errors.name && touched.name && "Name is required"} + required + error={errors.name && touched.name} + /> + <TextField + style={{ width: "100%", marginBottom: "25px" }} + name="description" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="secondary"> + Cancel + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + {buttonLabel} + </Button> + </DialogActions> + </form> + ); + }} + </Formik> + </Dialog> + ); }; ClusterProviderForm.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, - item: PropTypes.object + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + item: PropTypes.object, }; export default ClusterProviderForm; diff --git a/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx b/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx index 20317695..192992bc 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Accordion from "@material-ui/core/Accordion"; @@ -22,11 +22,13 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import apiService from "../../services/apiService"; import { Button } from "@material-ui/core"; import DeleteIcon from "@material-ui/icons/Delete"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import ClusterForm from "./clusters/ClusterForm"; import ClustersTable from "./clusters/ClusterTable"; import DeleteDialog from "../../common/Dialogue"; -import ClusterProviderForm from "../clusterProvider/ClusterProviderForm"; +import Notification from "../../common/Notification"; + +//import ClusterProviderForm from "../clusterProvider/ClusterProviderForm"; const useStyles = makeStyles((theme) => ({ root: { @@ -47,8 +49,9 @@ export default function ControlledAccordions({ data, setData, ...props }) { const [expanded, setExpanded] = useState(false); const [open, setOpen] = React.useState(false); const [formOpen, setFormOpen] = useState(false); - const [openProviderForm, setOpenProviderForm] = useState(false); + // const [openProviderForm, setOpenProviderForm] = useState(false); const [selectedRowIndex, setSelectedRowIndex] = useState(0); + const [notificationDetails, setNotificationDetails] = useState({}); const handleAccordianOpen = (providerRow) => (event, isExpanded) => { if (!isExpanded) { setExpanded(isExpanded ? providerRow : false); @@ -141,7 +144,7 @@ export default function ControlledAccordions({ data, setData, ...props }) { setSelectedRowIndex(index); setOpen(true); }; - const handleSubmit = (values) => { + const handleSubmit = (values, setSubmitting) => { let metadata = {}; if (values.userData) { metadata = JSON.parse(values.userData); @@ -150,7 +153,6 @@ export default function ControlledAccordions({ data, setData, ...props }) { metadata.description = values.description; const formData = new FormData(); formData.append("file", values.file); - // `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}` formData.append("metadata", `{"metadata":${JSON.stringify(metadata)}}`); formData.append("providerName", data[selectedRowIndex].metadata.name); apiService @@ -161,12 +163,24 @@ export default function ControlledAccordions({ data, setData, ...props }) { ? (data[selectedRowIndex].clusters = [res]) : data[selectedRowIndex].clusters.push(res); setData([...data]); + setFormOpen(false); + setNotificationDetails({ + show: true, + message: `${values.name} cluster added`, + severity: "success", + }); }) .catch((err) => { - console.log("error adding cluster : ", err); - }) - .finally(() => { - setFormOpen(false); + debugger; + if (err.response.status === 403) { + setNotificationDetails({ + show: true, + message: `${err.response.data}`, + severity: "error", + }); + setSubmitting(false); + } + console.log("error adding cluster : " + err); }); }; const handleFormClose = () => { @@ -198,35 +212,36 @@ export default function ControlledAccordions({ data, setData, ...props }) { setOpen(false); setSelectedRowIndex(0); }; - const handleEdit = (index) => { - setSelectedRowIndex(index); - setOpenProviderForm(true); - }; - const handleCloseProviderForm = () => { - setOpenProviderForm(false); - }; - const handleSubmitProviderForm = (values) => { - let request = { - payload: { metatada: values }, - providerName: data[selectedRowIndex].metadata.name, - }; - apiService - .updateClusterProvider(request) - .then((res) => { - setData((data) => { - data[selectedRowIndex].metadata = res.metadata; - return data; - }); - }) - .catch((err) => { - console.log("error updating cluster provider. " + err); - }) - .finally(() => { - setOpenProviderForm(false); - }); - }; + // const handleEdit = (index) => { + // setSelectedRowIndex(index); + // setOpenProviderForm(true); + // }; + // const handleCloseProviderForm = () => { + // setOpenProviderForm(false); + // }; + // const handleSubmitProviderForm = (values) => { + // let request = { + // payload: { metatada: values }, + // providerName: data[selectedRowIndex].metadata.name, + // }; + // apiService + // .updateClusterProvider(request) + // .then((res) => { + // setData((data) => { + // data[selectedRowIndex].metadata = res.metadata; + // return data; + // }); + // }) + // .catch((err) => { + // console.log("error updating cluster provider. " + err); + // }) + // .finally(() => { + // setOpenProviderForm(false); + // }); + // }; return ( <> + <Notification notificationDetails={notificationDetails} /> {data && data.length > 0 && ( <div className={classes.root}> <ClusterForm @@ -234,12 +249,12 @@ export default function ControlledAccordions({ data, setData, ...props }) { onClose={handleFormClose} onSubmit={handleSubmit} /> - <ClusterProviderForm + {/* <ClusterProviderForm open={openProviderForm} onClose={handleCloseProviderForm} onSubmit={handleSubmitProviderForm} item={data[selectedRowIndex]} - /> + /> */} <DeleteDialog open={open} onClose={handleClose} @@ -288,6 +303,8 @@ export default function ControlledAccordions({ data, setData, ...props }) { > Delete Provider </Button> + {/* + //edit cluster provider is not supported by the api yet <Button variant="outlined" size="small" @@ -299,7 +316,7 @@ export default function ControlledAccordions({ data, setData, ...props }) { }} > Edit Provider - </Button> + </Button> */} </div> <AccordionDetails> {item.clusters && ( diff --git a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx index 6d9fc83b..6c49cb85 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import PropTypes from "prop-types"; import { withStyles } from "@material-ui/core/styles"; @@ -113,8 +113,8 @@ const ClusterForm = (props) => { <DialogTitle id="simple-dialog-title">{title}</DialogTitle> <Formik initialValues={initialValues} - onSubmit={async (values) => { - onSubmit(values); + onSubmit={(values, actions) => { + onSubmit(values, actions.setSubmitting); }} validationSchema={schema} > diff --git a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx index 1066d472..26bc1ca9 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx @@ -11,309 +11,528 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import AddIconOutline from '@material-ui/icons/AddCircleOutline'; -import AddIcon from '@material-ui/icons/Add'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import IconButton from '@material-ui/core/IconButton'; -import EditIcon from '@material-ui/icons/Edit'; -import Chip from '@material-ui/core/Chip'; -import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet'; -import DeleteIcon from '@material-ui/icons/Delete'; -import { makeStyles, TextField, Button } from '@material-ui/core'; +// ======================================================================== +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import AddIconOutline from "@material-ui/icons/AddCircleOutline"; +import AddIcon from "@material-ui/icons/Add"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import IconButton from "@material-ui/core/IconButton"; +// import EditIcon from "@material-ui/icons/Edit"; +import Chip from "@material-ui/core/Chip"; +import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { makeStyles, TextField, Button } from "@material-ui/core"; import NetworkForm from "../networks/NetworkForm"; import apiService from "../../../services/apiService"; import DeleteDialog from "../../../common/Dialogue"; -import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; -import CheckIcon from '@material-ui/icons/CheckCircleOutlineOutlined'; -import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; +import CancelOutlinedIcon from "@material-ui/icons/CancelOutlined"; +import CheckIcon from "@material-ui/icons/CheckCircleOutlineOutlined"; +import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined"; import NetworkDetailsDialog from "../../../common/DetailsDialog"; -import DoneOutlineIcon from '@material-ui/icons/DoneOutline'; +import DoneOutlineIcon from "@material-ui/icons/DoneOutline"; import ClusterForm from "../clusters/ClusterForm"; +import Notification from "../../../common/Notification"; const useStyles = makeStyles((theme) => ({ - root: { - width: '100%', - }, - heading: { - fontSize: theme.typography.pxToRem(15), - flexBasis: '33.33%', - flexShrink: 0, - }, - secondaryHeading: { - fontSize: theme.typography.pxToRem(15), - color: theme.palette.text.secondary, - }, + root: { + width: "100%", + }, + heading: { + fontSize: theme.typography.pxToRem(15), + flexBasis: "33.33%", + flexShrink: 0, + }, + secondaryHeading: { + fontSize: theme.typography.pxToRem(15), + color: theme.palette.text.secondary, + }, })); const ClusterTable = ({ clustersData, ...props }) => { - const classes = useStyles(); - const [formOpen, setformOpen] = useState(false); - const [networkDetailsOpen, setNetworkDetailsOpen] = useState(false); - const [network, setNetwork] = useState({}); - const [activeRowIndex, setActiveRowIndex] = useState(0); - const [activeNetwork, setActiveNetwork] = useState({}); - const [open, setOpen] = useState(false); - const [openDeleteNetwork, setOpenDeleteNetwork] = useState(false); - const [showAddLabel, setShowAddLabel] = useState(false); - const [labelInput, setLabelInput] = React.useState(""); - const [clusterFormOpen, setClusterFormOpen] = useState(false); - const handleFormClose = () => { + const classes = useStyles(); + const [formOpen, setformOpen] = useState(false); + const [networkDetailsOpen, setNetworkDetailsOpen] = useState(false); + const [network, setNetwork] = useState({}); + const [activeRowIndex, setActiveRowIndex] = useState(0); + const [activeNetwork, setActiveNetwork] = useState({}); + const [open, setOpen] = useState(false); + const [openDeleteNetwork, setOpenDeleteNetwork] = useState(false); + const [showAddLabel, setShowAddLabel] = useState(false); + const [labelInput, setLabelInput] = useState(""); + // const [clusterFormOpen, setClusterFormOpen] = useState(false); + const [notificationDetails, setNotificationDetails] = useState({}); + const handleFormClose = () => { + setformOpen(false); + }; + const handleSubmit = (data) => { + let networkSpec = JSON.parse(data.spec); + let payload = { + metadata: { name: data.name, description: data.description }, + spec: networkSpec, + }; + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + networkType: data.type, + payload: payload, + }; + apiService + .addNetwork(request) + .then((res) => { + let networkType = + data.type === "networks" ? "networks" : "providerNetworks"; + !clustersData[activeRowIndex][networkType] || + clustersData[activeRowIndex][networkType] === null + ? (clustersData[activeRowIndex][networkType] = [res]) + : clustersData[activeRowIndex][networkType].push(res); + }) + .catch((err) => { + console.log("error adding cluster network : ", err); + }) + .finally(() => { + setActiveRowIndex(0); setformOpen(false); - } - const handleSubmit = (data) => { - let networkSpec = JSON.parse(data.spec); - let payload = { metadata: { name: data.name, description: data.description }, spec: networkSpec }; - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, networkType: data.type, payload: payload }; - apiService.addNetwork(request).then(res => { - let networkType = (data.type === "networks" ? "networks" : "providerNetworks"); - (!clustersData[activeRowIndex][networkType] || clustersData[activeRowIndex][networkType] === null) ? (clustersData[activeRowIndex][networkType] = [res]) : clustersData[activeRowIndex][networkType].push(res); - }).catch(err => { - console.log("error adding cluster network : ", err) - }).finally(() => { - setActiveRowIndex(0); - setformOpen(false); + }); + }; + const handleAddNetwork = (index) => { + setActiveRowIndex(index); + setformOpen(true); + }; + const handleDeleteLabel = (index, label, labelIndex) => { + let request = { + providerName: props.providerName, + clusterName: clustersData[index].metadata.name, + labelName: label, + }; + apiService + .deleteClusterLabel(request) + .then((res) => { + console.log("label deleted"); + clustersData[index].labels.splice(labelIndex, 1); + props.onUpdateCluster(props.parentIndex, clustersData); + }) + .catch((err) => { + console.log("error deleting label : ", err); + }); + }; + const handleClose = (el) => { + if (el.target.innerText === "Delete") { + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + }; + apiService + .deleteCluster(request) + .then(() => { + console.log("cluster deleted"); + props.onDeleteCluster(props.parentIndex, activeRowIndex); + }) + .catch((err) => { + console.log("Error deleting cluster : ", +err); + setNotificationDetails({ + show: true, + message: "Unable to remove cluster", + severity: "error", + }); }); } - const handleAddNetwork = (index) => { - setActiveRowIndex(index); - setformOpen(true); - } - const handleDeleteLabel = (index, label, labelIndex) => { - let request = { providerName: props.providerName, clusterName: clustersData[index].metadata.name, labelName: label } - apiService.deleteClusterLabel(request).then(res => { - console.log("label deleted"); - clustersData[index].labels.splice(labelIndex, 1); - props.onUpdateCluster(props.parentIndex, clustersData); - }).catch(err => { console.log("error deleting label : ", err) }) - } - const handleClose = el => { - if (el.target.innerText === "Delete") { - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name }; - apiService.deleteCluster(request).then(() => { - console.log("cluster deleted"); - props.onDeleteCluster(props.parentIndex, activeRowIndex); - }).catch(err => { - console.log("Error deleting cluster : ", err) - }) - } - setOpen(false); - setActiveRowIndex(0); - }; + setOpen(false); + setActiveRowIndex(0); + }; - const handleCloseDeleteNetwork = (el) => { - if (el.target.innerText === "Delete") { - let networkName = clustersData[activeRowIndex][activeNetwork.networkType][activeNetwork.networkIndex].metadata.name; - let networkType = (activeNetwork.networkType === "providerNetworks" ? "provider-networks" : "networks"); - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, networkType: networkType, networkName: networkName }; - apiService.deleteClusterNetwork(request).then(() => { - console.log("cluster network deleted"); - clustersData[activeRowIndex][activeNetwork.networkType].splice(activeNetwork.networkIndex, 1); - }).catch(err => { - console.log("Error deleting cluster network : ", err) - }).finally(() => { setActiveRowIndex(0); setActiveNetwork({}); }) - } - setOpenDeleteNetwork(false); - } - const handleDeleteCluster = (index) => { - setActiveRowIndex(index); - setOpen(true); - } - const handleAddLabel = (index) => { - if (labelInput !== "") { - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, payload: { "label-name": labelInput } }; - apiService.addClusterLabel(request) - .then(res => { - (!clustersData[index].labels || clustersData[index].labels === null) ? (clustersData[index].labels = [res]) : clustersData[index].labels.push(res); - }) - .catch(err => { console.log("error adding label", err) }) - .finally(() => { - setShowAddLabel(!showAddLabel); - }) - } + const handleCloseDeleteNetwork = (el) => { + if (el.target.innerText === "Delete") { + let networkName = + clustersData[activeRowIndex][activeNetwork.networkType][ + activeNetwork.networkIndex + ].metadata.name; + let networkType = + activeNetwork.networkType === "providerNetworks" + ? "provider-networks" + : "networks"; + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + networkType: networkType, + networkName: networkName, + }; + apiService + .deleteClusterNetwork(request) + .then(() => { + console.log("cluster network deleted"); + clustersData[activeRowIndex][activeNetwork.networkType].splice( + activeNetwork.networkIndex, + 1 + ); + }) + .catch((err) => { + console.log("Error deleting cluster network : ", err); + }) + .finally(() => { + setActiveRowIndex(0); + setActiveNetwork({}); + }); } - - const handleToggleAddLabel = (index) => { - setShowAddLabel(showAddLabel === index ? false : index); - setActiveRowIndex(index); - setLabelInput(''); + setOpenDeleteNetwork(false); + }; + const handleDeleteCluster = (index) => { + setActiveRowIndex(index); + setOpen(true); + }; + const handleAddLabel = (index) => { + if (labelInput !== "") { + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + payload: { "label-name": labelInput }, + }; + apiService + .addClusterLabel(request) + .then((res) => { + !clustersData[index].labels || clustersData[index].labels === null + ? (clustersData[index].labels = [res]) + : clustersData[index].labels.push(res); + }) + .catch((err) => { + console.log("error adding label", err); + }) + .finally(() => { + setShowAddLabel(!showAddLabel); + }); } - const handleLabelInputChange = (event) => { - setLabelInput(event.target.value); - }; + }; - const handleNetworkDetailOpen = (network) => { - setNetwork(network); - setNetworkDetailsOpen(true); - } - const handleDeleteNetwork = (index, networkIndex, networkType, networkName) => { - setActiveNetwork({ networkIndex: networkIndex, networkType: networkType, name: networkName }); - setActiveRowIndex(index); - setOpenDeleteNetwork(true); - } - const applyNetworkConfig = (clusterName) => { - let request = { providerName: props.providerName, clusterName: clusterName } - apiService.applyNetworkConfig(request) - .then(res => { - console.log("Network config applied"); - }) - .catch(err => { - console.log("Error applying network config : ", err); - if (err.response) - console.log("Network config applied" + err.response.data); - else - console.log("Network config applied" + err); - }); - } - const handleClusterFormClose = () => { - setClusterFormOpen(false); - } - const handleClusterSubmit = (values) => { - const formData = new FormData(); - if (values.file) - formData.append('file', values.file); - formData.append("metadata", `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}`); - formData.append("providerName", props.providerName); - apiService.updateCluster(formData) - .then(res => { - clustersData[activeRowIndex].metadata = res.metadata; - props.onUpdateCluster(props.parentIndex, clustersData); - }) - .catch(err => { console.log("error updating cluster : ", err) }) - .finally(() => { handleClusterFormClose() }); + const handleToggleAddLabel = (index) => { + setShowAddLabel(showAddLabel === index ? false : index); + setActiveRowIndex(index); + setLabelInput(""); + }; + const handleLabelInputChange = (event) => { + setLabelInput(event.target.value); + }; - } - const handleEditCluster = (index) => { - setActiveRowIndex(index); - setClusterFormOpen(true); - } - return ( + const handleNetworkDetailOpen = (network) => { + setNetwork(network); + setNetworkDetailsOpen(true); + }; + const handleDeleteNetwork = ( + index, + networkIndex, + networkType, + networkName + ) => { + setActiveNetwork({ + networkIndex: networkIndex, + networkType: networkType, + name: networkName, + }); + setActiveRowIndex(index); + setOpenDeleteNetwork(true); + }; + const applyNetworkConfig = (clusterName) => { + let request = { + providerName: props.providerName, + clusterName: clusterName, + }; + apiService + .applyNetworkConfig(request) + .then((res) => { + setNotificationDetails({ + show: true, + message: "Network configuration applied", + severity: "success", + }); + console.log("Network config applied"); + }) + .catch((err) => { + setNotificationDetails({ + show: true, + message: "Error applying network configuration", + severity: "error", + }); + console.log("Error applying network config : ", err); + if (err.response) + console.log("Network config applied" + err.response.data); + else console.log("Network config applied" + err); + }); + }; + // const handleClusterFormClose = () => { + // setClusterFormOpen(false); + // }; + // const handleClusterSubmit = (values) => { + // const formData = new FormData(); + // if (values.file) formData.append("file", values.file); + // formData.append( + // "metadata", + // `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}` + // ); + // formData.append("providerName", props.providerName); + // apiService + // .updateCluster(formData) + // .then((res) => { + // clustersData[activeRowIndex].metadata = res.metadata; + // props.onUpdateCluster(props.parentIndex, clustersData); + // }) + // .catch((err) => { + // console.log("error updating cluster : ", err); + // }) + // .finally(() => { + // handleClusterFormClose(); + // }); + // }; + //disabling as edit is not supported yet by the api yet + // const handleEditCluster = (index) => { + // setActiveRowIndex(index); + // setClusterFormOpen(true); + // }; + return ( + <> + <Notification notificationDetails={notificationDetails} /> + {clustersData && clustersData.length > 0 && ( <> - {clustersData && (clustersData.length > 0) && - (<> - <ClusterForm item={clustersData[activeRowIndex]} open={clusterFormOpen} onClose={handleClusterFormClose} onSubmit={handleClusterSubmit} /> - <NetworkDetailsDialog onClose={setNetworkDetailsOpen} open={networkDetailsOpen} item={network} type="Network" /> - <NetworkForm onClose={handleFormClose} onSubmit={handleSubmit} open={formOpen} /> - <DeleteDialog open={open} onClose={handleClose} title={"Delete Cluster"} - content={`Are you sure you want to delete "${clustersData[activeRowIndex] ? clustersData[activeRowIndex].metadata.name : ""}" ?`} /> - <DeleteDialog open={openDeleteNetwork} onClose={handleCloseDeleteNetwork} title={"Delete Network"} content={`Are you sure you want to delete "${activeNetwork.name}" ?`} /> - <TableContainer > - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell style={{ width: "10%" }}>Name</TableCell> - <TableCell style={{ width: "15%" }}>Description</TableCell> - <TableCell style={{ width: "20%" }}>Networks </TableCell> - <TableCell style={{ width: "35%" }}>Labels </TableCell> - <TableCell style={{ width: "20%" }}>Actions</TableCell> - </TableRow> - </TableHead> - <TableBody> - {clustersData.map((row, index) => ( - <TableRow key={row.metadata.name + "" + index}> - <TableCell >{row.metadata.name}</TableCell> - <TableCell >{row.metadata.description}</TableCell> - <TableCell> - <div> - {row.providerNetworks && (row.providerNetworks.length > 0) && row.providerNetworks.map((providerNetwork, providerNetworkIndex) => - (<Chip - key={providerNetwork.metadata.name + "" + providerNetworkIndex} - size="small" - icon={<InfoOutlinedIcon onClick={() => { handleNetworkDetailOpen(providerNetwork) }} style={{ cursor: "pointer" }} />} - onDelete={(e) => { handleDeleteNetwork(index, providerNetworkIndex, "providerNetworks", providerNetwork.metadata.name) }} - label={providerNetwork.metadata.name} - style={{ marginRight: "10px", marginBottom: "5px" }} - />) - )} + {/* <ClusterForm + item={clustersData[activeRowIndex]} + open={clusterFormOpen} + onClose={handleClusterFormClose} + onSubmit={handleClusterSubmit} + /> */} + <NetworkDetailsDialog + onClose={setNetworkDetailsOpen} + open={networkDetailsOpen} + item={network} + type="Network" + /> + <NetworkForm + onClose={handleFormClose} + onSubmit={handleSubmit} + open={formOpen} + /> + <DeleteDialog + open={open} + onClose={handleClose} + title={"Delete Cluster"} + content={`Are you sure you want to delete "${ + clustersData[activeRowIndex] + ? clustersData[activeRowIndex].metadata.name + : "" + }" ?`} + /> + <DeleteDialog + open={openDeleteNetwork} + onClose={handleCloseDeleteNetwork} + title={"Delete Network"} + content={`Are you sure you want to delete "${activeNetwork.name}" ?`} + /> + <TableContainer> + <Table className={classes.table}> + <TableHead> + <TableRow> + <TableCell style={{ width: "10%" }}>Name</TableCell> + <TableCell style={{ width: "15%" }}>Description</TableCell> + <TableCell style={{ width: "20%" }}>Networks </TableCell> + <TableCell style={{ width: "35%" }}>Labels </TableCell> + <TableCell style={{ width: "20%" }}>Actions</TableCell> + </TableRow> + </TableHead> + <TableBody> + {clustersData.map((row, index) => ( + <TableRow key={row.metadata.name + "" + index}> + <TableCell>{row.metadata.name}</TableCell> + <TableCell>{row.metadata.description}</TableCell> + <TableCell> + <div> + {row.providerNetworks && + row.providerNetworks.length > 0 && + row.providerNetworks.map( + (providerNetwork, providerNetworkIndex) => ( + <Chip + key={ + providerNetwork.metadata.name + + "" + + providerNetworkIndex + } + size="small" + icon={ + <InfoOutlinedIcon + onClick={() => { + handleNetworkDetailOpen(providerNetwork); + }} + style={{ cursor: "pointer" }} + /> + } + onDelete={(e) => { + handleDeleteNetwork( + index, + providerNetworkIndex, + "providerNetworks", + providerNetwork.metadata.name + ); + }} + label={providerNetwork.metadata.name} + style={{ + marginRight: "10px", + marginBottom: "5px", + }} + /> + ) + )} - {row.networks && (row.networks.length > 0) && row.networks.map((network, networkIndex) => - (<Chip - key={network.metadata.name + "" + networkIndex} - size="small" - icon={<InfoOutlinedIcon onClick={() => { handleNetworkDetailOpen(network) }} style={{ cursor: "pointer" }} />} - onDelete={(e) => { handleDeleteNetwork(index, networkIndex, "networks", network.metadata.name) }} - label={network.metadata.name} - style={{ marginRight: "10px", marginBottom: "5px" }} - color="secondary" - />) - )} - </div> - </TableCell> - <TableCell> - {row.labels && (row.labels.length > 0) && row.labels.map((label, labelIndex) => - (<Chip - key={label["label-name"] + "" + labelIndex} - size="small" - icon={<SettingsEthernetIcon />} - label={label["label-name"]} - onDelete={(e) => { handleDeleteLabel(index, label["label-name"], labelIndex) }} - color="primary" - style={{ marginRight: "10px" }} - />) - )} - {(showAddLabel === index) && - <TextField - style={{ height: "24px" }} - size="small" - value={labelInput} - onChange={handleLabelInputChange} - id="outlined-basic" label="Add label" variant="outlined" /> - } - {(showAddLabel === index) && - <IconButton color="primary" onClick={() => { handleAddLabel(index) }}> - <CheckIcon /> - </IconButton> - } - <IconButton color="primary" onClick={() => { handleToggleAddLabel(index) }}> - {!(showAddLabel === index) && <AddIconOutline />} - {(showAddLabel === index) && <CancelOutlinedIcon color="secondary" />} - </IconButton> - </TableCell> - <TableCell> - <Button - variant="outlined" - startIcon={<AddIcon />} - size="small" - color="primary" - title="Add Network" - onClick={() => { handleAddNetwork(index) }}> - Network - </Button> - <IconButton - style={{ color: "green" }} - onClick={() => { applyNetworkConfig(row.metadata.name) }} - title="Apply Network Configuration"> - <DoneOutlineIcon /> - </IconButton> - <IconButton - title="Edit" - onClick={() => { handleEditCluster(index) }} - color="primary"> - <EditIcon /> - </IconButton> - <IconButton - title="Delete" - color="secondary" - onClick={() => { handleDeleteCluster(index) }}> - <DeleteIcon /> - </IconButton> - </TableCell> - </TableRow>))} - </TableBody> - </Table> - </TableContainer> - </>)} - {(!clustersData || (clustersData.length === 0)) && (<span>No Clusters</span>)} - </>) -} + {row.networks && + row.networks.length > 0 && + row.networks.map((network, networkIndex) => ( + <Chip + key={network.metadata.name + "" + networkIndex} + size="small" + icon={ + <InfoOutlinedIcon + onClick={() => { + handleNetworkDetailOpen(network); + }} + style={{ cursor: "pointer" }} + /> + } + onDelete={(e) => { + handleDeleteNetwork( + index, + networkIndex, + "networks", + network.metadata.name + ); + }} + label={network.metadata.name} + style={{ + marginRight: "10px", + marginBottom: "5px", + }} + color="secondary" + /> + ))} + </div> + </TableCell> + <TableCell> + {row.labels && + row.labels.length > 0 && + row.labels.map((label, labelIndex) => ( + <Chip + key={label["label-name"] + "" + labelIndex} + size="small" + icon={<SettingsEthernetIcon />} + label={label["label-name"]} + onDelete={(e) => { + handleDeleteLabel( + index, + label["label-name"], + labelIndex + ); + }} + color="primary" + style={{ marginRight: "10px" }} + /> + ))} + {showAddLabel === index && ( + <TextField + style={{ height: "24px" }} + size="small" + value={labelInput} + onChange={handleLabelInputChange} + id="outlined-basic" + label="Add label" + variant="outlined" + /> + )} + {showAddLabel === index && ( + <IconButton + color="primary" + onClick={() => { + handleAddLabel(index); + }} + > + <CheckIcon /> + </IconButton> + )} + <IconButton + color="primary" + onClick={() => { + handleToggleAddLabel(index); + }} + > + {!(showAddLabel === index) && <AddIconOutline />} + {showAddLabel === index && ( + <CancelOutlinedIcon color="secondary" /> + )} + </IconButton> + </TableCell> + <TableCell> + <Button + variant="outlined" + startIcon={<AddIcon />} + size="small" + color="primary" + title="Add Network" + onClick={() => { + handleAddNetwork(index); + }} + > + Network + </Button> + <IconButton + color="primary" + disabled={ + !( + (row.networks && row.networks.length > 0) || + (row.providerNetworks && + row.providerNetworks.length > 0) + ) + } + onClick={() => { + applyNetworkConfig(row.metadata.name); + }} + title="Apply Network Configuration" + > + <DoneOutlineIcon /> + </IconButton> + {/* + //disabling as edit is not supported yet by the api yet + <IconButton + title="Edit" + onClick={() => { handleEditCluster(index) }} + color="primary"> + <EditIcon /> + </IconButton> */} + <IconButton + title="Delete" + color="secondary" + disabled={ + (row.networks && row.networks.length > 0) || + (row.providerNetworks && + row.providerNetworks.length > 0) || + (row.labels && row.labels.length > 0) + } + onClick={() => { + handleDeleteCluster(index); + }} + > + <DeleteIcon /> + </IconButton> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </> + )} + {(!clustersData || clustersData.length === 0) && <span>No Clusters</span>} + </> + ); +}; ClusterTable.propTypes = { - clusters: PropTypes.arrayOf(PropTypes.object) + clusters: PropTypes.arrayOf(PropTypes.object), }; export default ClusterTable; diff --git a/src/tools/emcoui/src/admin/controllers/Controllers.jsx b/src/tools/emcoui/src/admin/controllers/Controllers.jsx index 4a8a502c..4316f6ea 100644 --- a/src/tools/emcoui/src/admin/controllers/Controllers.jsx +++ b/src/tools/emcoui/src/admin/controllers/Controllers.jsx @@ -29,7 +29,8 @@ function Controllers() { apiService .getControllers() .then((res) => { - setControllersData(res); + if (res && res.length > 0) setControllersData(res); + else setControllersData([]); }) .catch((err) => { console.log("error getting controllers : " + err); @@ -53,9 +54,7 @@ function Controllers() { .addController(request) .then((res) => { setControllersData((controllersData) => { - if (controllersData && controllersData.length > 0) - return [...controllersData, res]; - else return [res]; + return [...controllersData, res]; }); }) .catch((err) => { diff --git a/src/tools/emcoui/src/admin/projects/ProjectForm.jsx b/src/tools/emcoui/src/admin/projects/ProjectForm.jsx index 751de5d0..4ea87b2c 100644 --- a/src/tools/emcoui/src/admin/projects/ProjectForm.jsx +++ b/src/tools/emcoui/src/admin/projects/ProjectForm.jsx @@ -11,146 +11,157 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField } from '@material-ui/core'; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import { TextField } from "@material-ui/core"; import * as Yup from "yup"; -import { Formik } from 'formik'; +import { Formik } from "formik"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - }) +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), +}); const ProjectFormFunc = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create" - const title = item ? "Edit Project" : "Create Project" - const handleClose = () => { - onClose(); - }; - let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" } + const { onClose, item, open, onSubmit } = props; + const buttonLabel = item ? "OK" : "Create"; + const title = item ? "Edit Project" : "Create Project"; + const handleClose = () => { + onClose(); + }; + let initialValues = item + ? { name: item.metadata.name, description: item.metadata.description } + : { name: "", description: "" }; - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - onSubmit(values); - }} - validationSchema={schema} - > - {props => { - const { - values, - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = props; - return ( - <form noValidate onSubmit={handleSubmit}> - <DialogContent dividers> - <TextField - style={{ width: "100%", marginBottom: "10px" }} - id="name" - label="Project name" - type="text" - value={values.name} - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - value={values.description} - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + return ( + <Dialog + maxWidth={"xs"} + onClose={handleClose} + aria-labelledby="customized-dialog-title" + open={open} + disableBackdropClick + > + <DialogTitle id="simple-dialog-title">{title}</DialogTitle> + <Formik + initialValues={initialValues} + onSubmit={async (values) => { + onSubmit(values); + }} + validationSchema={schema} + > + {(props) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = props; + return ( + <form noValidate onSubmit={handleSubmit}> + <DialogContent dividers> + <TextField + style={{ width: "100%", marginBottom: "10px" }} + id="name" + label="Project name" + type="text" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + helperText={errors.name && touched.name && "Name is required"} + required + disabled={item} + error={errors.name && touched.name} + /> + <TextField + style={{ width: "100%", marginBottom: "25px" }} + name="description" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="secondary"> + Cancel + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + {buttonLabel} + </Button> + </DialogActions> + </form> + ); + }} + </Formik> + </Dialog> + ); }; ProjectFormFunc.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, }; export default ProjectFormFunc; diff --git a/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx b/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx index d96d44fa..fb03155d 100644 --- a/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx +++ b/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import { withStyles, makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -24,118 +24,142 @@ import Paper from "@material-ui/core/Paper"; import { Link } from "react-router-dom"; import IconButton from "@material-ui/core/IconButton"; import EditIcon from "@material-ui/icons/Edit"; -import DeleteDialog from "../../common/Dialogue" +import DeleteDialog from "../../common/Dialogue"; import DeleteIcon from "@material-ui/icons/Delete"; import ProjectForm from "./ProjectForm"; import apiService from "../../services/apiService"; const StyledTableCell = withStyles((theme) => ({ - body: { - fontSize: 14, - }, + body: { + fontSize: 14, + }, }))(TableCell); const StyledTableRow = withStyles((theme) => ({ - root: { - "&:nth-of-type(odd)": { - backgroundColor: theme.palette.action.hover, - }, + root: { + "&:nth-of-type(odd)": { + backgroundColor: theme.palette.action.hover, }, + }, }))(TableRow); const useStyles = makeStyles({ - table: { - minWidth: 350, - }, - cell: { - color: "grey", - }, + table: { + minWidth: 350, + }, + cell: { + color: "grey", + }, }); export default function ProjectsTable(props) { - const classes = useStyles(); - const [open, setOpen] = React.useState(false); - const [openForm, setOpenForm] = React.useState(false); - const [index, setIndex] = React.useState(0); + const classes = useStyles(); + const [open, setOpen] = React.useState(false); + const [openForm, setOpenForm] = React.useState(false); + const [index, setIndex] = React.useState(0); - let handleEdit = index => { - setIndex(index); - setOpenForm(true); - } - const handleClose = el => { - if (el.target.innerText === "Delete") { - apiService.deleteProject(props.data[index].metadata.name).then(() => { - console.log("project deleted"); - props.data.splice(index, 1); - props.setProjectsData([...props.data]); - }).catch(err => { - console.log("Error deleting project : ", err) - }) - } - setOpen(false); - setIndex(0); - }; - const handleFormClose = () => { - setIndex(0); - setOpenForm(false); - }; - const handleDelete = (index) => { - setIndex(index); - setOpen(true); - } - const handleSubmit = (data) => { - let payload = { "metadata": data } - apiService.updateProject(payload).then(res => { - props.data[index] = res; - props.setProjectsData([...props.data]); - }).catch(err => { - console.log("Error updating project : ", err); + let handleEdit = (index) => { + setIndex(index); + setOpenForm(true); + }; + const handleClose = (el) => { + if (el.target.innerText === "Delete") { + apiService + .deleteProject(props.data[index].metadata.name) + .then(() => { + console.log("project deleted"); + props.data.splice(index, 1); + props.setProjectsData([...props.data]); }) - setOpenForm(false); - }; - - return ( - <React.Fragment> - {(props.data && props.data.length > 0) && - <> - <ProjectForm open={openForm} onClose={handleFormClose} item={props.data[index]} onSubmit={handleSubmit} /> - <DeleteDialog open={open} onClose={handleClose} title={"Delete Project"} - content={`Are you sure you want to delete "${props.data[index] ? props.data[index].metadata.name : ""}" ?`} /> - <TableContainer component={Paper}> - <Table className={classes.table} size="small"> - <TableHead> - <TableRow> - <StyledTableCell>Name</StyledTableCell> - <StyledTableCell>Description</StyledTableCell> - <StyledTableCell>Actions</StyledTableCell> - </TableRow> - </TableHead> - <TableBody> - {props.data.map((row, index) => ( - <StyledTableRow key={row.metadata.name + "" + index}> - <StyledTableCell> - {" "} - <Link to={`/app/projects/${row.metadata.name}`}>{row.metadata.name}</Link> - </StyledTableCell> - <StyledTableCell className={classes.cell}> - {row.metadata.description} - </StyledTableCell> - <StyledTableCell className={classes.cell}> - <IconButton onClick={(e) => handleEdit(index)} title="Edit" > - <EditIcon color="primary" /> - </IconButton> - <IconButton onClick={(e) => handleDelete(index)} title="Delete" > - <DeleteIcon color="secondary" /> - </IconButton> - </StyledTableCell> - </StyledTableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> - } + .catch((err) => { + console.log("Error deleting project : ", err); + }); + } + setOpen(false); + setIndex(0); + }; + const handleFormClose = () => { + setIndex(0); + setOpenForm(false); + }; + const handleDelete = (index) => { + setIndex(index); + setOpen(true); + }; + const handleSubmit = (data) => { + let payload = { metadata: data }; + apiService + .updateProject(payload) + .then((res) => { + props.data[index] = res; + props.setProjectsData([...props.data]); + }) + .catch((err) => { + console.log("Error updating project : ", err); + }); + setOpenForm(false); + }; - </React.Fragment> - ); + return ( + <React.Fragment> + {props.data && props.data.length > 0 && ( + <> + <ProjectForm + open={openForm} + onClose={handleFormClose} + item={props.data[index]} + onSubmit={handleSubmit} + /> + <DeleteDialog + open={open} + onClose={handleClose} + title={"Delete Project"} + content={`Are you sure you want to delete "${ + props.data[index] ? props.data[index].metadata.name : "" + }" ?`} + /> + <TableContainer component={Paper}> + <Table className={classes.table} size="small"> + <TableHead> + <TableRow> + <StyledTableCell>Name</StyledTableCell> + <StyledTableCell>Description</StyledTableCell> + <StyledTableCell>Actions</StyledTableCell> + </TableRow> + </TableHead> + <TableBody> + {props.data.map((row, index) => ( + <StyledTableRow key={row.metadata.name + "" + index}> + <StyledTableCell> + {" "} + <Link to={`/app/projects/${row.metadata.name}`}> + {row.metadata.name} + </Link> + </StyledTableCell> + <StyledTableCell className={classes.cell}> + {row.metadata.description} + </StyledTableCell> + <StyledTableCell className={classes.cell}> + <IconButton + onClick={(e) => handleEdit(index)} + title="Edit" + > + <EditIcon color="primary" /> + </IconButton> + <IconButton + onClick={(e) => handleDelete(index)} + title="Delete" + > + <DeleteIcon color="secondary" /> + </IconButton> + </StyledTableCell> + </StyledTableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </> + )} + </React.Fragment> + ); } diff --git a/src/tools/emcoui/src/appbase/AppBase.js b/src/tools/emcoui/src/appbase/AppBase.js index 5dd3b53b..76dc4d8e 100644 --- a/src/tools/emcoui/src/appbase/AppBase.js +++ b/src/tools/emcoui/src/appbase/AppBase.js @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import PropTypes from "prop-types"; import { ThemeProvider, withStyles } from "@material-ui/core/styles"; @@ -25,6 +25,7 @@ import theme from "../theme/Theme"; import apiService from "../services/apiService"; import DeploymentIntentGroups from "../deploymentIntentGroups/DeploymentIntentGroups"; import { Switch, Route, Link } from "react-router-dom"; +// import Dashboard from "../dashboard/DashboardView"; const drawerWidth = 256; const styles = { @@ -45,11 +46,10 @@ const styles = { }, main: { flex: 1, - padding: theme.spacing(6, 4), + padding: theme.spacing(3, 4, 6, 4), background: "#eaeff1", }, footer: { - // padding: theme.spacing(2), background: "#eaeff1", }, }; @@ -63,17 +63,6 @@ class AppBase extends React.Component { }; } - componentDidMount() { - apiService - .getCompositeApps({ projectName: this.state.projectName }) - .then((response) => { - this.setState({ data: response }); - }) - .catch((err) => { - console.log("Unable to get composite apps"); - }) - .finally(); - } setMobileOpen = (mobileOpen) => { this.setState({ mobileOpen }); }; @@ -114,15 +103,15 @@ class AppBase extends React.Component { path={`${this.props.match.url}/404`} component={() => <div>Page Not found</div>} /> - <Route - exact - path={`${this.props.match.url}/composite-apps`} - > + {/* <Route exact path={`${this.props.match.url}/dashboard`}> + <Dashboard projectName={this.state.projectName} /> + </Route> */} + <Route exact path={`${this.props.match.url}/services`}> <CompositeApps projectName={this.state.projectName} /> </Route> <Route exact - path={`${this.props.match.url}/composite-apps/:appname/:version`} + path={`${this.props.match.url}/services/:appname/:version`} > <CompositeApp projectName={this.state.projectName} /> </Route> diff --git a/src/tools/emcoui/src/appbase/Content.js b/src/tools/emcoui/src/appbase/Content.js index 2c907acb..eff9a561 100644 --- a/src/tools/emcoui/src/appbase/Content.js +++ b/src/tools/emcoui/src/appbase/Content.js @@ -95,7 +95,7 @@ function Content(props) { </AppBar> <div className={classes.contentWrapper}> <Typography color="textSecondary" align="center"> - No composite apps for this project yet + No services for this project yet </Typography> </div> </Paper> diff --git a/src/tools/emcoui/src/appbase/Header.js b/src/tools/emcoui/src/appbase/Header.js index 0222151f..19f148a4 100644 --- a/src/tools/emcoui/src/appbase/Header.js +++ b/src/tools/emcoui/src/appbase/Header.js @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import PropTypes from "prop-types"; import AppBar from "@material-ui/core/AppBar"; @@ -57,18 +57,18 @@ function Header(props) { let headerName = ""; let getHeaderName = () => { - if (location.pathname === `${props.match.url}/composite-apps`) { - headerName = "Composite Apps"; + if (location.pathname === `${props.match.url}/dashboard`) { + headerName = "Dashboard"; + } else if (location.pathname === `${props.match.url}/services`) { + headerName = "Services"; } else if ( location.pathname === `${props.match.url}/deployment-intent-group` ) { headerName = "Deployment Intent Groups"; - } else if (location.pathname.includes("composite-apps")) { + } else if (location.pathname.includes("services")) { headerName = - "Composite Apps / " + - location.pathname - .slice(location.pathname.indexOf("composite-apps")) - .slice(15); + "services / " + + location.pathname.slice(location.pathname.indexOf("services")).slice(9); } else if (location.pathname === `${props.match.url}/projects`) { headerName = "Projects"; } else if (location.pathname === `${props.match.url}/clusters`) { diff --git a/src/tools/emcoui/src/appbase/Navigator.js b/src/tools/emcoui/src/appbase/Navigator.js index 2df2c009..e8f16367 100644 --- a/src/tools/emcoui/src/appbase/Navigator.js +++ b/src/tools/emcoui/src/appbase/Navigator.js @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import PropTypes from "prop-types"; import clsx from "clsx"; @@ -23,7 +23,7 @@ import ListItem from "@material-ui/core/ListItem"; import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemText from "@material-ui/core/ListItemText"; import HomeIcon from "@material-ui/icons/Home"; -import AppsIcon from "@material-ui/icons/Apps"; +import DeviceHubIcon from "@material-ui/icons/DeviceHub"; import DnsRoundedIcon from "@material-ui/icons/DnsRounded"; import { withRouter, Link } from "react-router-dom"; @@ -32,9 +32,9 @@ const categories = [ id: "1", children: [ { - id: "Composite Apps", - icon: <AppsIcon />, - url: "/composite-apps", + id: "Services", + icon: <DeviceHubIcon />, + url: "/services", }, { id: "Deployment Intent Groups", @@ -85,8 +85,8 @@ const styles = (theme) => ({ marginTop: theme.spacing(2), }, version: { - fontSize: "15px" - } + fontSize: "15px", + }, }); function Navigator(props) { @@ -99,11 +99,20 @@ function Navigator(props) { setActiveTab(location.pathname); } return ( - <Drawer PaperProps={props.PaperProps} variant={props.variant} open={props.open} onClose={props.onClose}> + <Drawer + PaperProps={props.PaperProps} + variant={props.variant} + open={props.open} + onClose={props.onClose} + > <List disablePadding> - <Link style={{ textDecoration: "none" }} to='/'> + <Link style={{ textDecoration: "none" }} to="/"> <ListItem - className={clsx(classes.firebase, classes.item, classes.itemCategory)} + className={clsx( + classes.firebase, + classes.item, + classes.itemCategory + )} > <ListItemText classes={{ @@ -111,14 +120,29 @@ function Navigator(props) { }} > ONAP4K8s - </ListItemText> - <span - className={clsx(classes.version)} - >{process.env.REACT_APP_VERSION}</span> + </ListItemText> + <span className={clsx(classes.version)}> + {process.env.REACT_APP_VERSION} + </span> </ListItem> </Link> - <ListItem className={clsx(classes.item, classes.itemCategory)}> + {/* <Link + style={{ textDecoration: "none" }} + to={{ + pathname: `${props.match.url}/dashboard`, + activeItem: "childId", + }} + key={"childId"} + > */} + <ListItem + button + className={clsx( + classes.item, + classes.itemCategory, + activeItem.includes("dashboard") && classes.itemActiveItem + )} + > <ListItemIcon className={classes.itemIcon}> <HomeIcon /> </ListItemIcon> @@ -130,10 +154,18 @@ function Navigator(props) { Dashboard </ListItemText> </ListItem> + {/* </Link> */} {categories.map(({ id, children }) => ( <React.Fragment key={id}> {children.map(({ id: childId, icon, url }) => ( - <Link style={{ textDecoration: "none" }} to={{ pathname: `${props.match.url}${url}`, activeItem: childId }} key={childId}> + <Link + style={{ textDecoration: "none" }} + to={{ + pathname: `${props.match.url}${url}`, + activeItem: childId, + }} + key={childId} + > <ListItem button className={clsx( diff --git a/src/tools/emcoui/src/assets/icons/empty.svg b/src/tools/emcoui/src/assets/icons/empty.svg new file mode 100644 index 00000000..f4e020ed --- /dev/null +++ b/src/tools/emcoui/src/assets/icons/empty.svg @@ -0,0 +1 @@ +<svg enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/><path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/></svg>
\ No newline at end of file diff --git a/src/tools/emcoui/src/common/ExpandableCard.jsx b/src/tools/emcoui/src/common/ExpandableCard.jsx new file mode 100644 index 00000000..1d2ea9e6 --- /dev/null +++ b/src/tools/emcoui/src/common/ExpandableCard.jsx @@ -0,0 +1,94 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== + +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import clsx from "clsx"; +import Card from "@material-ui/core/Card"; +import CardHeader from "@material-ui/core/CardHeader"; +import CardContent from "@material-ui/core/CardContent"; +import Collapse from "@material-ui/core/Collapse"; +import IconButton from "@material-ui/core/IconButton"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import StorageIcon from "@material-ui/icons/Storage"; +import ErrorIcon from "@material-ui/icons/Error"; + +const useStyles = makeStyles((theme) => ({ + root: { + width: "100%", + }, + expand: { + transform: "rotate(0deg)", + marginLeft: "auto", + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest, + }), + }, + expandOpen: { + transform: "rotate(180deg)", + }, +})); +const ExpandableCard = (props) => { + const classes = useStyles(); + const [expanded, setExpanded] = useState(false); + + const handleExpandClick = () => { + if (!expanded) { + setExpanded(!expanded); + } else { + setExpanded(!expanded); + } + }; + + return ( + <> + <Card className={classes.root}> + <CardHeader + onClick={handleExpandClick} + avatar={ + <> + <StorageIcon fontSize="large" /> + </> + } + action={ + <> + {props.error && ( + <ErrorIcon color="error" style={{ verticalAlign: "middle" }} /> + )} + <IconButton + className={clsx(classes.expand, { + [classes.expandOpen]: expanded, + })} + onClick={handleExpandClick} + aria-expanded={expanded} + > + <ExpandMoreIcon /> + </IconButton> + </> + } + title={props.title} + subheader={props.description} + /> + <Collapse in={expanded} timeout="auto" unmountOnExit> + <CardContent>{props.content}</CardContent> + </Collapse> + </Card> + </> + ); +}; + +ExpandableCard.propTypes = {}; + +export default ExpandableCard; diff --git a/src/tools/emcoui/src/common/FileUpload.jsx b/src/tools/emcoui/src/common/FileUpload.jsx index 847951e9..97d34bc2 100644 --- a/src/tools/emcoui/src/common/FileUpload.jsx +++ b/src/tools/emcoui/src/common/FileUpload.jsx @@ -11,58 +11,64 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; import FileCopyIcon from "@material-ui/icons/FileCopy"; import CloudUploadIcon from "@material-ui/icons/CloudUpload"; -import './fileUpload.css' +import "./fileUpload.css"; const FileUpload = (props) => { - return ( - <> - <div className="file-upload"> - <div - className="file-upload-wrap" - style={{ - border: props.file && props.file.name && "2px dashed rgba(0, 131, 143, 1)" - }} - > - <input - required - className="file-upload-input" - type="file" - accept={props.accept ? props.accept : "*"} - name="file" - onBlur={props.handleBlur ? props.handleBlur : null} - onChange={(event) => { - props.setFieldValue("file", event.currentTarget.files[0]); - }} - /> + return ( + <> + <div className="file-upload"> + <div + className="file-upload-wrap" + style={{ + border: + props.file && + props.file.name && + "2px dashed rgba(0, 131, 143, 1)", + }} + > + <input + required + className="file-upload-input" + type="file" + accept={props.accept ? props.accept : "*"} + name="file" + onBlur={props.handleBlur ? props.handleBlur : null} + onChange={(event) => { + props.setFieldValue(props.name, event.currentTarget.files[0]); + }} + /> - <div className="file-upload-text"> - {(props.file && props.file.name) ? (<> - <span> - <FileCopyIcon color="primary" /> - </span> - <span style={{ fontWeight: 600 }}>{props.file.name}</span> - </>) : (<> - <span> - <CloudUploadIcon /> - </span> - <span> - Drag And Drop or Click To Upload - </span> - </>)} - </div> - </div> - </div> - </>); + <div className="file-upload-text"> + {props.file && props.file.name ? ( + <> + <span> + <FileCopyIcon color="primary" /> + </span> + <span style={{ fontWeight: 600 }}>{props.file.name}</span> + </> + ) : ( + <> + <span> + <CloudUploadIcon /> + </span> + <span>Drag And Drop or Click To Upload</span> + </> + )} + </div> + </div> + </div> + </> + ); }; FileUpload.propTypes = { - handleBlur: PropTypes.func, - setFieldValue: PropTypes.func.isRequired, + handleBlur: PropTypes.func, + setFieldValue: PropTypes.func.isRequired, }; export default FileUpload; diff --git a/src/tools/emcoui/src/common/Form.jsx b/src/tools/emcoui/src/common/Form.jsx index e9fe3a2d..6e8eee2e 100644 --- a/src/tools/emcoui/src/common/Form.jsx +++ b/src/tools/emcoui/src/common/Form.jsx @@ -11,144 +11,155 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField } from '@material-ui/core'; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import { TextField } from "@material-ui/core"; import * as Yup from "yup"; -import { Formik } from 'formik'; +import { Formik } from "formik"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - }) +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), +}); const CreateForm = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create" - const title = item ? "Edit" : "Create" - const handleClose = () => { - onClose(); - }; - let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" } + const { onClose, item, open, onSubmit } = props; + const buttonLabel = item ? "OK" : "Create"; + const title = item ? "Edit" : "Create"; + const handleClose = () => { + onClose(); + }; + let initialValues = item + ? { name: item.metadata.name, description: item.metadata.description } + : { name: "", description: "" }; - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - onSubmit(values); - }} - validationSchema={schema} - > - {props => { - const { - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = props; - return ( - <form noValidate onSubmit={handleSubmit}> - <DialogContent dividers> - <TextField - style={{ width: "100%", marginBottom: "10px" }} - id="name" - label="Name" - name="name" - type="text" - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + return ( + <Dialog + maxWidth={"xs"} + onClose={handleClose} + aria-labelledby="customized-dialog-title" + open={open} + disableBackdropClick + > + <DialogTitle id="simple-dialog-title">{title}</DialogTitle> + <Formik + initialValues={initialValues} + onSubmit={async (values) => { + onSubmit(values); + }} + validationSchema={schema} + > + {(props) => { + const { + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + submitCount, + } = props; + return ( + <form noValidate onSubmit={handleSubmit}> + <DialogContent dividers> + <TextField + style={{ width: "100%", marginBottom: "10px" }} + id="name" + label="Name" + name="name" + type="text" + onChange={handleChange} + onBlur={handleBlur} + helperText={errors.name && touched.name && "Name is required"} + required + error={errors.name && touched.name} + /> + <TextField + style={{ width: "100%", marginBottom: "25px" }} + name="description" + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="secondary"> + Cancel + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting || submitCount > 0} + > + {buttonLabel} + </Button> + </DialogActions> + </form> + ); + }} + </Formik> + </Dialog> + ); }; CreateForm.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, }; export default CreateForm; diff --git a/src/tools/emcoui/src/compositeApps/CompositeApp.jsx b/src/tools/emcoui/src/compositeApps/CompositeApp.jsx index 34d07fbc..8b2c2b10 100644 --- a/src/tools/emcoui/src/compositeApps/CompositeApp.jsx +++ b/src/tools/emcoui/src/compositeApps/CompositeApp.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import Tab from "@material-ui/core/Tab"; import Tabs from "@material-ui/core/Tabs"; @@ -19,15 +19,15 @@ import Paper from "@material-ui/core/Paper"; import { withStyles } from "@material-ui/core/styles"; import Box from "@material-ui/core/Box"; import PropTypes from "prop-types"; -import Apps from "../compositeApps/apps/Apps"; -import CompositeProfiles from "../compositeApps/compositeProfiles/CompositeProfiles"; -import Intents from "../compositeApps/intents/GenericPlacementIntents"; import BackIcon from "@material-ui/icons/ArrowBack"; import { withRouter } from "react-router-dom"; import { IconButton } from "@material-ui/core"; import apiService from "../services/apiService"; import Spinner from "../common/Spinner"; -import NetworkIntent from "../networkIntents/NetworkIntents"; +import Apps from "../compositeApps/apps/Apps"; +import CompositeProfiles from "../compositeApps/compositeProfiles/CompositeProfiles"; +// import Intents from "../compositeApps/intents/GenericPlacementIntents"; +// import NetworkIntent from "../networkIntents/NetworkIntents"; const lightColor = "rgba(255, 255, 255, 0.7)"; @@ -134,8 +134,6 @@ class CompositeApp extends React.Component { > <Tab label="Apps" /> <Tab label="Composite Profiles" /> - <Tab label="Generic Placement Intents" /> - <Tab label="Network Controller Intents" /> </Tabs> {this.state.isLoading && <Spinner />} @@ -158,22 +156,6 @@ class CompositeApp extends React.Component { appsData={this.state.appsData} /> </TabPanel> - <TabPanel value={this.state.activeTab} index={2}> - <Intents - projectName={this.props.projectName} - compositeAppName={this.state.compositeAppName} - compositeAppVersion={this.state.compositeAppVersion} - appsData={this.state.appsData} - /> - </TabPanel> - <TabPanel value={this.state.activeTab} index={3}> - <NetworkIntent - projectName={this.props.projectName} - compositeAppName={this.state.compositeAppName} - compositeAppVersion={this.state.compositeAppVersion} - appsData={this.state.appsData} - /> - </TabPanel> </> )} </Paper> @@ -181,7 +163,5 @@ class CompositeApp extends React.Component { ); } } - CompositeApp.propTypes = {}; - export default withStyles(styles)(withRouter(CompositeApp)); diff --git a/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx b/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx index 220d7df8..926ab545 100644 --- a/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx +++ b/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { withStyles, makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -21,13 +21,15 @@ import TableContainer from "@material-ui/core/TableContainer"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; -import { Link } from "react-router-dom"; +// import { Link } from "react-router-dom"; import IconButton from "@material-ui/core/IconButton"; import EditIcon from "@material-ui/icons/Edit"; -import DeleteIcon from '@material-ui/icons/Delete'; +import DeleteIcon from "@material-ui/icons/Delete"; import CreateCompositeAppForm from "./dialogs/CompositeAppForm"; import apiService from "../services/apiService"; import DeleteDialog from "../common/Dialogue"; +import { Link, withRouter } from "react-router-dom"; +import Notification from "../common/Notification"; const StyledTableCell = withStyles((theme) => ({ body: { @@ -52,50 +54,80 @@ const useStyles = makeStyles({ }, }); -export default function CustomizedTables({ data, ...props }) { +function CustomizedTables({ data, ...props }) { const classes = useStyles(); const [openForm, setOpenForm] = useState(false); const [activeRowIndex, setActiveRowIndex] = useState(0); const [row, setRow] = useState({}); const [open, setOpen] = useState(false); + const [notificationDetails, setNotificationDetails] = useState({}); let onEditCompositeApp = (row, index) => { setActiveRowIndex(index); setRow(row); setOpenForm(true); - } + // props.history.push(`services/${row.metadata.name}/${row.spec.version}`); + }; const handleCloseForm = (fields) => { if (fields) { - let request = { payload: { name: fields.name, description: fields.description, spec: { version: fields.version } }, projectName: props.projectName, compositeAppVersion: row.spec.version }; - apiService.updateCompositeApp(request).then(res => { - let updatedData = data.slice(); - updatedData.splice(activeRowIndex, 1); - updatedData.push(res); - props.handleUpdateState(updatedData); - }).catch(err => { - console.log("error creating composite app : ", err) - }).finally(() => { - setOpenForm(false); - }); - } - else { + let request = { + payload: { + name: fields.name, + description: fields.description, + spec: { version: fields.version }, + }, + projectName: props.projectName, + compositeAppVersion: row.spec.version, + }; + apiService + .updateCompositeApp(request) + .then((res) => { + let updatedData = data.slice(); + updatedData.splice(activeRowIndex, 1); + updatedData.push(res); + props.handleUpdateState(updatedData); + }) + .catch((err) => { + console.log("error creating composite app : ", err); + }) + .finally(() => { + setOpenForm(false); + }); + } else { setOpenForm(false); } }; const handleDeleteCompositeApp = (index) => { setActiveRowIndex(index); setOpen(true); - } - const handleClose = el => { + }; + const handleClose = (el) => { if (el.target.innerText === "Delete") { - let request = { projectName: props.projectName, compositeAppName: data[activeRowIndex].metadata.name, compositeAppVersion: data[activeRowIndex].spec.version }; - apiService.deleteCompositeApp(request).then(() => { - console.log("cluster deleted"); - data.splice(activeRowIndex, 1); - let updatedData = data.slice(); - props.handleUpdateState(updatedData); - }).catch(err => { - console.log("Error deleting cluster : ", err) - }) + let request = { + projectName: props.projectName, + compositeAppName: data[activeRowIndex].metadata.name, + compositeAppVersion: data[activeRowIndex].spec.version, + }; + apiService + .deleteCompositeApp(request) + .then(() => { + console.log("cluster deleted"); + data.splice(activeRowIndex, 1); + let updatedData = data.slice(); + props.handleUpdateState(updatedData); + }) + .catch((err) => { + console.log("Error deleting cluster : ", err); + let message = "Error deleting service"; + if (err.response.data.includes("Non emtpy DIG in service")) { + message = + "Error deleting service : please delete deployment intent group first"; + } + setNotificationDetails({ + show: true, + message: message, + severity: "error", + }); + }); } setOpen(false); setActiveRowIndex(0); @@ -103,11 +135,22 @@ export default function CustomizedTables({ data, ...props }) { return ( <> - {data && (data.length > 0) && - (<> - <CreateCompositeAppForm open={openForm} handleClose={handleCloseForm} item={row} /> - <DeleteDialog open={open} onClose={handleClose} title={"Delete Cluster"} - content={`Are you sure you want to delete "${data[activeRowIndex] ? data[activeRowIndex].metadata.name : ""}" ?`} /> + <Notification notificationDetails={notificationDetails} /> + {data && data.length > 0 && ( + <> + <CreateCompositeAppForm + open={openForm} + handleClose={handleCloseForm} + item={row} + /> + <DeleteDialog + open={open} + onClose={handleClose} + title={"Delete Service"} + content={`Are you sure you want to delete "${ + data[activeRowIndex] ? data[activeRowIndex].metadata.name : "" + }" ?`} + /> <TableContainer component={Paper}> <Table className={classes.table} size="small"> <TableHead> @@ -122,8 +165,12 @@ export default function CustomizedTables({ data, ...props }) { {data.map((row, index) => ( <StyledTableRow key={row.metadata.name}> <StyledTableCell> - {" "} - <Link to={`composite-apps/${row.metadata.name}/${row.spec.version}`}>{row.metadata.name}</Link> + <Link + to={`services/${row.metadata.name}/${row.spec.version}`} + > + {row.metadata.name} + </Link> + {/* {row.metadata.name} */} </StyledTableCell> <StyledTableCell className={classes.cell}> {row.metadata.description} @@ -132,10 +179,18 @@ export default function CustomizedTables({ data, ...props }) { {row.spec.version} </StyledTableCell> <StyledTableCell className={classes.cell}> - <IconButton onClick={(e) => onEditCompositeApp(row, index)} title="Edit"> + {/* <IconButton + onClick={(e) => onEditCompositeApp(row, index)} + title="Edit" + > <EditIcon color="primary" /> - </IconButton> - <IconButton color="secondary" onClick={() => { handleDeleteCompositeApp(index) }}> + </IconButton> */} + <IconButton + color="secondary" + onClick={() => { + handleDeleteCompositeApp(index); + }} + > <DeleteIcon /> </IconButton> </StyledTableCell> @@ -144,7 +199,11 @@ export default function CustomizedTables({ data, ...props }) { </TableBody> </Table> </TableContainer> - </>)} - {(!data || (data.length === 0)) && (<span>No Clusters</span>)} - </>) + </> + )} + {(!data || data.length === 0) && <span>No Composite Apps</span>} + </> + ); } + +export default withRouter(CustomizedTables); diff --git a/src/tools/emcoui/src/compositeApps/CompositeApps.jsx b/src/tools/emcoui/src/compositeApps/CompositeApps.jsx index e7901ff9..5c540039 100644 --- a/src/tools/emcoui/src/compositeApps/CompositeApps.jsx +++ b/src/tools/emcoui/src/compositeApps/CompositeApps.jsx @@ -11,14 +11,15 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React from "react"; import CompositeAppTable from "./CompositeAppTable"; -import { withStyles, Button, Grid } from "@material-ui/core"; +import { withStyles, Button, Grid, Typography } from "@material-ui/core"; import CreateCompositeAppForm from "./dialogs/CompositeAppForm"; import AddIcon from "@material-ui/icons/Add"; import apiService from "../services/apiService"; import Spinner from "../common/Spinner"; +import { ReactComponent as EmptyIcon } from "../assets/icons/empty.svg"; const styles = { root: { display: "flex", @@ -57,22 +58,43 @@ class CompositeApps extends React.Component { handleClose = (fields) => { if (fields) { - let request = { - payload: { - metadata: { name: fields.name, description: fields.description }, - spec: { version: fields.version }, - }, - projectName: this.props.projectName, + const formData = new FormData(); + let appsData = []; + fields.apps.forEach((app) => { + //add files for each app + formData.append(`${app.appName}_file`, app.file); + formData.append(`${app.appName}_profile`, app.profilePackageFile); + appsData.push({ + metadata: { + name: app.appName, + description: app.description ? app.description : "na", + filename: app.file.name, + }, + profileMetadata: { + name: `${app.appName}_profile`, + filename: app.profilePackageFile.name, + }, + clusters: app.clusters, + }); + }); + + let servicePayload = { + name: fields.name, + description: fields.description, + spec: { projectName: this.props.projectName, appsData }, }; + formData.append("servicePayload", JSON.stringify(servicePayload)); + let request = { projectName: this.props.projectName, payload: formData }; apiService - .createCompositeApp(request) + .addService(request) .then((res) => { + console.log("create service response : " + res); if (this.state.data && this.state.data.length > 0) this.setState({ data: [...this.state.data, res] }); else this.setState({ data: [res] }); }) .catch((err) => { - console.log("error creating composite app : ", err); + console.log("error adding app : ", err); }); } this.setState({ open: false }); @@ -88,32 +110,48 @@ class CompositeApps extends React.Component { {this.state.isLoading && <Spinner />} {!this.state.isLoading && ( <> - <Button - variant="outlined" - color="primary" - startIcon={<AddIcon />} - onClick={this.handleCreateCompositeApp} - > - Create Composite App - </Button> <CreateCompositeAppForm open={this.state.open} handleClose={this.handleClose} /> <Grid container spacing={2} alignItems="center"> - <Grid item xs style={{ marginTop: "20px" }}> - {this.state.data && this.state.data.length > 0 && ( + <Grid item xs={12}> + <Button + variant="outlined" + color="primary" + startIcon={<AddIcon />} + onClick={this.handleCreateCompositeApp} + > + Add service + </Button> + </Grid> + {this.state.data && this.state.data.length > 0 && ( + <Grid item xs={12}> <CompositeAppTable data={this.state.data} projectName={this.props.projectName} handleUpdateState={this.handleUpdateState} /> - )} - {(!this.state.data || this.state.data.length === 0) && ( - <span>No Composite Apps</span> - )} - </Grid> + </Grid> + )} </Grid> + {(!this.state.data || this.state.data.length === 0) && ( + <Grid + container + spacing={2} + direction="column" + alignItems="center" + > + <Grid style={{ marginTop: "60px" }} item xs={6}> + <EmptyIcon style={{ height: "100px", width: "100px" }} /> + </Grid> + <Grid item xs={12}> + <Typography variant="h6"> + No service found, start by adding a service + </Typography> + </Grid> + </Grid> + )} </> )} </> diff --git a/src/tools/emcoui/src/compositeApps/apps/Apps.jsx b/src/tools/emcoui/src/compositeApps/apps/Apps.jsx index 14be60c2..0e3638b3 100644 --- a/src/tools/emcoui/src/compositeApps/apps/Apps.jsx +++ b/src/tools/emcoui/src/compositeApps/apps/Apps.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Card from "@material-ui/core/Card"; @@ -19,7 +19,7 @@ import CardContent from "@material-ui/core/CardContent"; import IconButton from "@material-ui/core/IconButton"; import Typography from "@material-ui/core/Typography"; import DeleteIcon from "@material-ui/icons/Delete"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import { Grid, Button, Tooltip } from "@material-ui/core"; import AddIcon from "@material-ui/icons/Add"; import apiService from "../../services/apiService"; @@ -144,7 +144,7 @@ const Apps = ({ data, onStateChange, ...props }) => { }; return ( <> - <Button + {/* <Button variant="outlined" color="primary" startIcon={<AddIcon />} @@ -152,7 +152,7 @@ const Apps = ({ data, onStateChange, ...props }) => { size="small" > Add App - </Button> + </Button> */} <AppForm open={formOpen} onClose={handleFormClose} @@ -200,6 +200,7 @@ const Apps = ({ data, onStateChange, ...props }) => { </Typography> </CardContent> <div className={classes.controls}> + {/* //edit app api is not implemented yet <IconButton onClick={handleEditApp.bind(this, value)} color="primary" @@ -212,7 +213,7 @@ const Apps = ({ data, onStateChange, ...props }) => { onClick={() => handleDeleteApp(index)} > <DeleteIcon /> - </IconButton> + </IconButton> */} </div> </div> </Card> diff --git a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx index 58161e8e..d565eba7 100644 --- a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx +++ b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import clsx from "clsx"; @@ -35,7 +35,7 @@ import TableCell from "@material-ui/core/TableCell"; import Paper from "@material-ui/core/Paper"; import TableBody from "@material-ui/core/TableBody"; import apiService from "../../services/apiService"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import DeleteIcon from "@material-ui/icons/Delete"; import ProfileForm from "./ProfileForm"; import DeleteDialog from "../../common/Dialogue"; @@ -196,7 +196,7 @@ export default function RecipeReviewCard(props) { /> <Collapse in={expanded} timeout="auto" unmountOnExit> <CardContent> - <Button + {/* <Button disabled={!(props.appsData && props.appsData.length > 0)} variant="outlined" size="small" @@ -208,11 +208,12 @@ export default function RecipeReviewCard(props) { }} > Add Profile - </Button> - <Button + </Button> */} + {/* <Button variant="outlined" size="small" color="secondary" + disabled={data && data.length > 0} style={{ float: "right" }} startIcon={<DeleteIcon />} onClick={() => { @@ -220,7 +221,7 @@ export default function RecipeReviewCard(props) { }} > Delete Composite Profile - </Button> + </Button> */} {data && data.length > 0 && ( <> <DeleteDialog @@ -238,7 +239,7 @@ export default function RecipeReviewCard(props) { <StyledTableCell>Name</StyledTableCell> <StyledTableCell>Description</StyledTableCell> <StyledTableCell>App</StyledTableCell> - <StyledTableCell>Actions</StyledTableCell> + {/* <StyledTableCell>Actions</StyledTableCell> */} </TableRow> </TableHead> <TableBody> @@ -253,7 +254,9 @@ export default function RecipeReviewCard(props) { <StyledTableCell> {profile.spec["app-name"]} </StyledTableCell> - <StyledTableCell> + {/* <StyledTableCell> + + //edit profile api is not implemented yet <IconButton onClick={(e) => handleEdit(index)} title="Edit" @@ -266,7 +269,7 @@ export default function RecipeReviewCard(props) { > <DeleteIcon color="secondary" /> </IconButton> - </StyledTableCell> + </StyledTableCell> */} </StyledTableRow> ))} </TableBody> @@ -275,7 +278,7 @@ export default function RecipeReviewCard(props) { </> )} {!(props.appsData && props.appsData.length > 0) && ( - <div>No apps found for adding profile</div> + <div>No app found for adding profile</div> )} </CardContent> </Collapse> diff --git a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx index 25ecaaee..26629e03 100644 --- a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx +++ b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState, useEffect } from "react"; import Card from "./CompositeProfileCard"; import { Button, Grid } from "@material-ui/core"; @@ -82,7 +82,6 @@ const CompositeProfiles = (props) => { compositeAppVersion: props.compositeAppVersion, compositeProfileName: data[index].metadata.name, }; - console.log(request); apiService .deleteCompositeProfile(request) .then(() => { @@ -115,7 +114,7 @@ const CompositeProfiles = (props) => { }"`} /> - <Button + {/* <Button disabled={isLoading} variant="outlined" color="primary" @@ -123,7 +122,7 @@ const CompositeProfiles = (props) => { onClick={handleAddCompositeProfile} > Add Composite Profile - </Button> + </Button> */} <Form onClose={handleCloseForm} open={openForm} onSubmit={handleSubmit} /> <Grid container diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx new file mode 100644 index 00000000..12dd7dd8 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx @@ -0,0 +1,149 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import React from "react"; +import ExpandableCard from "../../common/ExpandableCard"; +import { Grid, Paper, TextField } from "@material-ui/core"; +import FileUpload from "../../common/FileUpload"; + +function AppDetailsForm({ formikProps, ...props }) { + return ( + <> + <Paper + style={{ + width: "100%", + padding: "20px", + maxHeight: "395px", + overflowY: "auto", + scrollbarWidth: "thin", + }} + > + <Grid container spacing={3}> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].appName} + name={`apps[${props.index}].appName`} + id="app-name" + label="App name" + size="small" + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + required + helperText={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName + } + error={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName && + true + } + /> + </Grid> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].description} + name={`apps[${props.index}].description`} + id="app-description" + label="Description" + multiline + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + rowsMax={4} + /> + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + App tgz file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].file} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].file`} + accept={".tgz"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].file && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].file} + </p> + )} + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + Profile tar file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].profilePackageFile} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].profilePackageFile`} + accept={".tar.gz, .tar"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].profilePackageFile && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].profilePackageFile} + </p> + )} + </Grid> + </Grid> + </Paper> + </> + ); +} + +const AppForm = (props) => { + return ( + <ExpandableCard + error={ + props.formikProps.errors.apps && + props.formikProps.errors.apps[props.index] + } + title={props.name} + description={props.description} + content={ + <AppDetailsForm + formikProps={props.formikProps} + name={props.name} + index={props.index} + /> + } + /> + ); +}; +export default AppForm; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx new file mode 100644 index 00000000..e2272ae8 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx @@ -0,0 +1,129 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import { Grid, Paper, TextField } from "@material-ui/core"; +import FileUpload from "../../common/FileUpload"; +import React from "react"; + +function AppFormGeneral({ formikProps, ...props }) { + return ( + <> + <Paper + style={{ + width: "100%", + padding: "20px", + maxHeight: "395px", + overflowY: "auto", + scrollbarWidth: "thin", + }} + > + <Grid container spacing={3}> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].appName} + name={`apps[${props.index}].appName`} + id="app-name" + label="App name" + size="small" + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + required + helperText={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName + } + error={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName && + true + } + /> + </Grid> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].description} + name={`apps[${props.index}].description`} + id="app-description" + label="Description" + multiline + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + rowsMax={4} + /> + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + App tgz file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].file} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].file`} + accept={".tgz"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].file && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].file} + </p> + )} + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + Profile tar file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].profilePackageFile} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].profilePackageFile`} + accept={".tar.gz, .tar"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].profilePackageFile && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].profilePackageFile} + </p> + )} + </Grid> + </Grid> + </Paper> + </> + ); +} + +export default AppFormGeneral; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx new file mode 100644 index 00000000..c52c2b42 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx @@ -0,0 +1,83 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { Grid, Paper, Typography } from "@material-ui/core"; +import EnhancedTable from "./SortableTable"; + +function AppFormPlacement({ + formikProps, + index, + clusterProviders, + handleRowSelect, + ...props +}) { + return ( + <> + <Typography variant="subtitle1" style={{ float: "left" }}> + Select Clusters + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> *</span> + </Typography> + {formikProps.errors.apps && + formikProps.errors.apps[index] && + formikProps.errors.apps[index].clusters && ( + <span + style={{ + color: "#f44336", + marginRight: "35px", + float: "right", + }} + > + {typeof formikProps.errors.apps[index].clusters === "string" && + formikProps.errors.apps[index].clusters} + </span> + )} + <Grid + container + spacing={3} + style={{ + height: "400px", + overflowY: "auto", + width: "100%", + scrollbarWidth: "thin", + }} + > + {clusterProviders && + clusterProviders.length > 0 && + clusterProviders.map((clusterProvider) => ( + <Grid key={clusterProvider.name} item xs={12}> + <Paper> + <EnhancedTable + key={clusterProvider.name} + tableName={clusterProvider.name} + clusters={clusterProvider.clusters} + formikValues={formikProps.values.apps[index].clusters} + onRowSelect={handleRowSelect} + /> + </Paper> + </Grid> + ))} + </Grid> + </> + ); +} + +AppFormPlacement.propTypes = { + formikProps: PropTypes.object, + index: PropTypes.number, + handleRowSelect: PropTypes.func, +}; + +export default AppFormPlacement; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx new file mode 100644 index 00000000..055dc3e6 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx @@ -0,0 +1,524 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; +import Typography from "@material-ui/core/Typography"; +import { Grid, IconButton } from "@material-ui/core"; +import { TextField, Select, MenuItem, InputLabel } from "@material-ui/core"; +import AddIcon from "@material-ui/icons/Add"; +import CardContent from "@material-ui/core/CardContent"; +import Card from "@material-ui/core/Card"; +import apiService from "../../services/apiService"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { Formik } from "formik"; +import Notification from "../../common/Notification"; + +function NetworkForm({ formikProps, ...props }) { + const [clusters, setClusters] = useState(props.clusters); + const [notificationDetails, setNotificationDetails] = useState({}); + const useStyles = makeStyles({ + root: { + minWidth: 275, + }, + title: { + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, + }); + + const handleAddNetworkInterface = (providerIndex, clusterIndex, values) => { + let updatedFields = []; + if ( + values.apps[props.index].clusters[providerIndex].selectedClusters[ + clusterIndex + ].interfaces + ) { + updatedFields = [ + ...values.apps[props.index].clusters[providerIndex].selectedClusters[ + clusterIndex + ].interfaces, + { + networkName: "", + ip: "", + subnet: "", + }, + ]; + } else { + updatedFields = [ + { + networkName: "", + ip: "", + subnet: "", + }, + ]; + } + + let request = { + providerName: values.apps[props.index].clusters[providerIndex].provider, + clusterName: + values.apps[props.index].clusters[providerIndex].selectedClusters[ + clusterIndex + ].name, + }; + apiService + .getClusterProviderNetworks(request) + .then((networks) => { + let networkData = []; + if (networks && networks.length > 0) { + networks.forEach((network) => { + networkData.push({ + name: network.metadata.name, + subnets: network.spec.ipv4Subnets, + }); + }); + } + + apiService + .getClusterNetworks(request) + .then((clusterNetworks) => { + if (clusterNetworks && clusterNetworks.length > 0) { + clusterNetworks.forEach((clusterNetwork) => { + networkData.push({ + name: clusterNetwork.metadata.name, + subnets: clusterNetwork.spec.ipv4Subnets, + }); + }); + } + //add interface entry onyl of there is atlease one available network + if (networkData.length > 0) { + setClusters((clusters) => { + clusters[providerIndex].selectedClusters[ + clusterIndex + ].interfaces = updatedFields; + clusters[providerIndex].selectedClusters[ + clusterIndex + ].networks = networkData; + clusters[providerIndex].selectedClusters[ + clusterIndex + ].availableNetworks = getAvailableNetworks( + clusters[providerIndex].selectedClusters[clusterIndex] + ); + return clusters; + }); + formikProps.setFieldValue( + `apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces`, + updatedFields + ); + } else { + setNotificationDetails({ + show: true, + message: `No network available for this cluster`, + severity: "warning", + }); + } + }) + .catch((err) => { + console.log("error getting cluster networks : ", err); + }); + }) + .catch((err) => { + console.log("error getting cluster provider networks : ", err); + }) + .finally(() => { + return updatedFields; + }); + }; + + const handleSelectNetowrk = ( + e, + providerIndex, + clusterIndex, + interfaceIndex + ) => { + setClusters((clusters) => { + clusters[providerIndex].selectedClusters[clusterIndex].interfaces[ + interfaceIndex + ] = { + networkName: e.target.value, + ip: "", + subnet: "", + }; + clusters[providerIndex].selectedClusters[ + clusterIndex + ].availableNetworks = getAvailableNetworks( + clusters[providerIndex].selectedClusters[clusterIndex], + "handleAddNetworkInterface" + ); + return clusters; + }); + formikProps.handleChange(e); + }; + const handleRemoveNetwork = (providerIndex, clusterIndex, interfaceIndex) => { + setClusters((clusters) => { + clusters[providerIndex].selectedClusters[clusterIndex].interfaces.splice( + interfaceIndex, + 1 + ); + clusters[providerIndex].selectedClusters[ + clusterIndex + ].availableNetworks = getAvailableNetworks( + clusters[providerIndex].selectedClusters[clusterIndex] + ); + return clusters; + }); + formikProps.setFieldValue( + `apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces`, + clusters[providerIndex].selectedClusters[clusterIndex].interfaces + ); + }; + const getAvailableNetworks = (cluster) => { + let availableNetworks = []; + cluster.networks.forEach((network) => { + let match = false; + cluster.interfaces.forEach((networkInterface) => { + if (network.name === networkInterface.networkName) { + match = true; + return; + } + }); + if (!match) availableNetworks.push(network); + }); + return availableNetworks; + }; + + const classes = useStyles(); + return ( + <> + <Notification notificationDetails={notificationDetails} /> + <Grid + key="networkForm" + container + spacing={3} + style={{ + height: "400px", + overflowY: "auto", + width: "100%", + scrollbarWidth: "thin", + }} + > + {(!clusters || clusters.length < 1) && ( + <Grid item xs={12}> + <Typography variant="h6">No clusters selected</Typography> + </Grid> + )} + {clusters && + clusters.map((cluster, providerIndex) => ( + <Grid key={cluster.provider + providerIndex} item xs={12}> + <Card className={classes.root}> + <CardContent> + <Grid container spacing={2}> + <Grid item xs={12}> + <Typography + className={classes.title} + color="textSecondary" + gutterBottom + > + {cluster.provider} + </Typography> + </Grid> + {cluster.selectedClusters.map( + (selectedCluster, clusterIndex) => ( + <React.Fragment key={selectedCluster.name}> + <Grid item xs={12}> + <Typography>{selectedCluster.name}</Typography> + </Grid> + <Formik> + {() => { + const { + values, + errors, + handleChange, + handleBlur, + } = formikProps; + return ( + <> + {selectedCluster.interfaces && + selectedCluster.interfaces.length > 0 + ? selectedCluster.interfaces.map( + (networkInterface, interfaceIndex) => ( + <Grid + spacing={1} + container + item + key={interfaceIndex} + xs={12} + > + <Grid item xs={4}> + <InputLabel id="network-select-label"> + Network + </InputLabel> + <Select + fullWidth + labelId="network-select-label" + id="network-select" + name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].networkName`} + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .networkName + } + onChange={(e) => { + handleSelectNetowrk( + e, + providerIndex, + clusterIndex, + interfaceIndex + ); + }} + > + {values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .networkName && ( + <MenuItem + key={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + } + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + } + > + { + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + } + </MenuItem> + )} + {selectedCluster.availableNetworks && + selectedCluster.availableNetworks.map( + (network) => ( + <MenuItem + key={network.name} + value={network.name} + > + {network.name} + </MenuItem> + ) + )} + </Select> + </Grid> + + <Grid item xs={4}> + <InputLabel id="subnet-select-label"> + Subnet + </InputLabel> + <Select + fullWidth + labelId="subnet-select-label" + id="subnet-select-label" + name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].subnet`} + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .subnet + } + onChange={handleChange} + > + {values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .networkName === "" + ? null + : selectedCluster.networks + .filter( + (network) => + network.name === + values.apps[ + props.index + ].clusters[ + providerIndex + ].selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + )[0] + .subnets.map((subnet) => ( + <MenuItem + key={subnet.name} + value={subnet.name} + > + {subnet.name}( + {subnet.subnet}) + </MenuItem> + ))} + </Select> + </Grid> + <Grid item xs={3}> + <TextField + width={"65%"} + name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].ip`} + onBlur={handleBlur} + id="ip" + label="IP Address" + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .ip + } + onChange={handleChange} + helperText={ + (errors.apps && + errors.apps[props.index] && + errors.apps[props.index] + .clusters && + errors.apps[props.index] + .clusters[clusterIndex] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .ip) || + "blank for auto assign" + } + error={ + errors.apps && + errors.apps[props.index] && + errors.apps[props.index] + .clusters && + errors.apps[props.index] + .clusters[clusterIndex] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .ip && + true + } + /> + </Grid> + <Grid item xs={1}> + <IconButton + color="secondary" + onClick={() => { + handleRemoveNetwork( + providerIndex, + clusterIndex, + interfaceIndex + ); + }} + > + <DeleteIcon fontSize="small" /> + </IconButton> + </Grid> + </Grid> + ) + ) + : null} + <Grid + key={selectedCluster.name + "addButton"} + item + xs={12} + > + <Button + variant="outlined" + size="small" + fullWidth + color="primary" + disabled={ + selectedCluster.interfaces && + selectedCluster.interfaces.length > 0 && + selectedCluster.networks.length === + selectedCluster.interfaces.length + } + onClick={() => { + handleAddNetworkInterface( + providerIndex, + clusterIndex, + values + ); + }} + startIcon={<AddIcon />} + > + Add Network Interface + </Button> + </Grid> + </> + ); + }} + </Formik> + </React.Fragment> + ) + )} + </Grid> + </CardContent> + </Card> + </Grid> + ))} + </Grid> + </> + ); +} + +export default NetworkForm; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx index 29e17cd7..751ea8eb 100644 --- a/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx +++ b/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx @@ -11,185 +11,298 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React from "react"; -import { withStyles } from "@material-ui/core/styles"; +// ======================================================================== +import React, { useState } from "react"; import Button from "@material-ui/core/Button"; import Dialog from "@material-ui/core/Dialog"; -import MuiDialogTitle from "@material-ui/core/DialogTitle"; -import MuiDialogContent from "@material-ui/core/DialogContent"; -import MuiDialogActions from "@material-ui/core/DialogActions"; +import AppBar from "@material-ui/core/AppBar"; +import Toolbar from "@material-ui/core/Toolbar"; +import IconButton from "@material-ui/core/IconButton"; import Typography from "@material-ui/core/Typography"; -import { TextField } from '@material-ui/core'; -const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), +import CloseIcon from "@material-ui/icons/Close"; +import Slide from "@material-ui/core/Slide"; +import { Grid } from "@material-ui/core"; +import { TextField } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import AddIcon from "@material-ui/icons/Add"; +import NewAppForm from "../../common/Form"; +import AppForm from "./AppForm"; +import { Formik, FieldArray } from "formik"; +import * as Yup from "yup"; + +const Transition = React.forwardRef(function Transition(props, ref) { + return <Slide direction="up" ref={ref} {...props} />; +}); + +const useStyles = makeStyles((theme) => ({ + tableRoot: { + width: "100%", }, - closeButton: { + paper: { + width: "100%", + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 550, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, position: "absolute", - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], + top: 20, + width: 1, }, -}); - - -const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, + demo: { + backgroundColor: theme.palette.background.paper, }, -}))(MuiDialogContent); - -const DialogActions = withStyles((theme) => ({ root: { - margin: 0, - padding: theme.spacing(1), + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: "flex", + height: 424, }, -}))(MuiDialogActions); - + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + }, +})); -class CreateCompositeAppForm extends React.Component { - constructor(props) { - super(props) - this.state = { - fields: { name: "", version: "", description: "" }, - errors: {} - } - this.handleChange = this.handleChange.bind(this); - this.submituserRegistrationForm = this.submituserRegistrationForm.bind(this); - } +const PROFILE_SUPPORTED_FORMATS = [ + ".tgz", + ".tar.gz", + ".tar", + "application/x-tar", + "application/x-tgz", + "application/x-compressed", + "application/x-gzip", + "application/x-compressed-tar", + "application/gzip", +]; +const APP_PACKAGE_SUPPORTED_FORMATS = [ + ".tgz", + ".tar.gz", + ".tar", + "application/x-tar", + "application/x-tgz", + "application/x-compressed", + "application/x-gzip", + "application/x-compressed-tar", +]; +const serviceBasicValidationSchema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), + apps: Yup.array() + .of( + Yup.object({ + appName: Yup.string().required("App name is required"), + file: Yup.mixed() + .required("An app package file is required") + .test( + "fileFormat", + "Unsupported file format", + (value) => + value && APP_PACKAGE_SUPPORTED_FORMATS.includes(value.type) + ), + profilePackageFile: Yup.mixed() + .required("A profile package file is required") + .test( + "fileFormat", + "Unsupported file format", + (value) => value && PROFILE_SUPPORTED_FORMATS.includes(value.type) + ), + }) + ) + .required("At least one app is required"), +}); - componentDidMount = () => { - if (this.props.item) { - this.title = "Edit Composite App"; - this.buttonLabel = "Update"; - this.isEdit = true; - } - else { - this.title = "New Composite App"; - this.buttonLabel = "Create"; - this.isEdit = false; - } +const CreateCompositeAppForm = ({ open, handleClose }) => { + const classes = useStyles(); + const [openForm, setOpenForm] = useState(false); + const handleCloseForm = () => { + setOpenForm(false); }; - - componentDidUpdate = (prevProps, prevState) => { - if (this.props.item && ((prevProps.item !== this.props.item))) { - this.setState({ fields: { ...this.props.item.metadata, version: this.props.item.spec.version } }); - } - } - - resetFields = () => { - if (!this.isEdit) { - this.setState({ - fields: { name: "", version: "", description: "" }, - errors: {} - }); - } - else { - this.setState({ fields: { ...this.props.item.metadata, version: this.props.item.spec.version } }); - } - } - - handleClose = () => { - this.resetFields(); - this.props.handleClose(); + const handleAddApp = () => { + setOpenForm(true); }; - - submituserRegistrationForm(e) { - e.preventDefault(); - if (this.validateForm()) { - this.resetFields(); - this.props.handleClose(this.state.fields); - } - } - - validateForm() { - let fields = this.state.fields; - let errors = {}; - let formIsValid = true; - - if (!fields["name"]) { - formIsValid = false; - errors["name"] = "*Please enter your username."; - } - - if (typeof fields["name"] !== "string") { - if (!fields["name"].match(/^[a-zA-Z ]*$/)) { - formIsValid = false; - errors["name"] = "*Please enter alphabet characters only."; - } - } - this.setState({ - errors: errors - }); - return formIsValid; - } - - handleChange = (e) => { - this.setState({ fields: { ...this.state.fields, [e.target.name]: e.target.value } }); - } - - render = () => { - const { classes } = this.props; - return ( - <> + let initialValues = { name: "", description: "", apps: [] }; + return ( + <> + {open && ( <Dialog - maxWidth={"xs"} - onClose={this.handleClose} - aria-labelledby="customized-dialog-title" - open={this.props.open} - disableBackdropClick + open={open} + onClose={() => { + handleClose(); + }} + fullScreen + TransitionComponent={Transition} > - <MuiDialogTitle disableTypography className={classes.root} > - <Typography variant="h6">{this.title}</Typography> - </MuiDialogTitle> + <Formik + initialValues={initialValues} + onSubmit={(values, { setSubmitting }) => { + setSubmitting(false); + handleClose(values); + }} + validationSchema={serviceBasicValidationSchema} + > + {(props) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = props; + return ( + <> + <form noValidate onSubmit={handleSubmit}> + <AppBar className={classes.appBar}> + <Toolbar> + <IconButton + edge="start" + color="inherit" + onClick={() => { + handleClose(); + }} + aria-label="close" + > + <CloseIcon /> + </IconButton> + <Typography variant="h6" className={classes.title}> + Add Service + </Typography> + <Button + type="submit" + autoFocus + variant="contained" + disabled={isSubmitting} + > + SUBMIT + </Button> + </Toolbar> + </AppBar> + <div style={{ padding: "12px" }}> + <Grid + container + direction="row" + justify="center" + alignItems="center" + style={{ marginTop: "40px" }} + spacing={3} + > + <Grid item xs={6}> + <Grid container spacing={3}> + {errors.apps && + touched.apps && + typeof errors.apps !== "object" && ( + <Grid item xs={12} sm={12}> + <Typography>{errors.apps}</Typography> + </Grid> + )} + + <Grid item xs={12} sm={6}> + <TextField + fullWidth + name="name" + id="input-name" + label="Name" + variant="outlined" + size="small" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + required + helperText={ + errors.name && + touched.name && + "Name is required" + } + error={errors.name && touched.name} + /> + </Grid> + <Grid item xs={12} sm={6}> + <TextField + fullWidth + name="description" + id="input-description" + label="Description" + variant="outlined" + size="small" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + /> + </Grid> - <form onSubmit={this.submituserRegistrationForm}> - <DialogContent dividers> - <TextField - style={{ width: "40%", marginBottom: "10px" }} - name="name" - value={this.state.fields.name} - id="input-name" - label="Name" - helperText="Name should be unique" - onChange={this.handleChange} - required - /> - <TextField - style={{ width: "40%", marginBottom: "20px", float: "right" }} - name="version" - value={this.state.fields.version} - onChange={this.handleChange} - id="input-version" - label="Version" - required - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - value={this.state.fields.description} - onChange={this.handleChange} - id="input-description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={this.handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary"> - {this.buttonLabel} - </Button> - </DialogActions> - </form> + <FieldArray + name="apps" + render={(arrayHelpers) => ( + <> + <NewAppForm + open={openForm} + onClose={handleCloseForm} + onSubmit={(values) => { + arrayHelpers.push({ + appName: values.name, + description: values.description, + }); + setOpenForm(false); + }} + /> + {values.apps && + values.apps.length > 0 && + values.apps.map((app, index) => ( + <Grid key={index} item sm={12} xs={12}> + <AppForm + formikProps={props} + name={app.appName} + description={app.description} + index={index} + initialValues={values} + /> + </Grid> + ))} + </> + )} + /> + <Grid item xs={12}> + <Button + variant="outlined" + size="small" + fullWidth + color="primary" + onClick={() => { + handleAddApp(); + }} + startIcon={<AddIcon />} + > + Add App + </Button> + </Grid> + </Grid> + </Grid> + </Grid> + </div> + </form> + </> + ); + }} + </Formik> </Dialog> - </> - ); - } -} -export default withStyles(styles)(CreateCompositeAppForm) + )} + </> + ); +}; +export default CreateCompositeAppForm; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx b/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx new file mode 100644 index 00000000..f1a6ac2d --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx @@ -0,0 +1,410 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import { makeStyles } from "@material-ui/core/styles"; +import React, { useEffect, useState } from "react"; +import Checkbox from "@material-ui/core/Checkbox"; +import PropTypes from "prop-types"; +import Typography from "@material-ui/core/Typography"; +import Toolbar from "@material-ui/core/Toolbar"; + +import clsx from "clsx"; +import TablePagination from "@material-ui/core/TablePagination"; +import { lighten } from "@material-ui/core/styles"; + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === "desc" + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +const headCells = [ + { + id: "name", + numeric: false, + sortable: true, + disablePadding: true, + label: "Cluster", + }, + { + id: "description", + numeric: true, + sortable: false, + disablePadding: false, + label: "Description", + }, +]; + +function EnhancedTableHead(props) { + const { + classes, + onSelectAllClick, + order, + orderBy, + numSelected, + rowCount, + onRequestSort, + } = props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + return ( + <TableHead> + <TableRow> + <TableCell padding="checkbox"> + <Checkbox + indeterminate={numSelected > 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + /> + </TableCell> + {headCells.map((headCell) => + headCell.sortable ? ( + <TableCell + style={{ fontWeight: "520" }} + key={headCell.id} + align={headCell.numeric ? "right" : "left"} + padding={headCell.disablePadding ? "none" : "default"} + sortDirection={orderBy === headCell.id ? order : false} + > + <TableSortLabel + active={orderBy === headCell.id} + direction={orderBy === headCell.id ? order : "asc"} + onClick={createSortHandler(headCell.id)} + > + {headCell.label} + {orderBy === headCell.id ? ( + <span className={classes.visuallyHidden}> + {order === "desc" + ? "sorted descending" + : "sorted ascending"} + </span> + ) : null} + </TableSortLabel> + </TableCell> + ) : ( + <TableCell + key={headCell.id} + style={{ fontWeight: "520" }} + padding={headCell.disablePadding ? "none" : "default"} + align={headCell.numeric ? "right" : "left"} + > + {headCell.label} + </TableCell> + ) + )} + </TableRow> + </TableHead> + ); +} + +EnhancedTableHead.propTypes = { + classes: PropTypes.object.isRequired, + numSelected: PropTypes.number.isRequired, + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.oneOf(["asc", "desc"]).isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, +}; + +const useToolbarStyles = makeStyles((theme) => ({ + root: { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(1), + }, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.primary.main, + backgroundColor: lighten(theme.palette.primary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.primary.dark, + }, + title: { + flex: "1 1 100%", + }, +})); + +const EnhancedTableToolbar = (props) => { + const classes = useToolbarStyles(); + const { numSelected } = props; + + return ( + <Toolbar + className={clsx(classes.root, { + [classes.highlight]: numSelected > 0, + })} + > + <Typography + className={classes.title} + variant="h6" + id="tableTitle" + component="div" + > + {props.tableName} + </Typography> + + <Typography + className={classes.title} + style={{ textAlign: "right" }} + color="inherit" + variant="subtitle1" + component="div" + > + {numSelected} selected + </Typography> + </Toolbar> + ); +}; + +EnhancedTableToolbar.propTypes = { + numSelected: PropTypes.number.isRequired, +}; + +const useStyles = makeStyles((theme) => ({ + tableRoot: { + width: "100%", + }, + paper: { + width: "100%", + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 550, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, + demo: { + backgroundColor: theme.palette.background.paper, + }, + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: "flex", + height: 424, + }, + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + }, +})); + +function EnhancedTable({ + clusters, + formikValues, + tableName, + onRowSelect, + ...props +}) { + const classes = useStyles(); + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState("name"); + const [selected, setSelected] = useState([]); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [rows, setRows] = useState([]); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + useEffect(() => { + if (formikValues) { + let formikClusterData = formikValues.filter( + (cluster) => cluster.provider === tableName + ); + if (formikClusterData && formikClusterData.length > 0) { + let data = []; + formikClusterData[0].selectedClusters.forEach((selectedCluster) => { + data.push(selectedCluster.name); + }); + setSelected(data); + } + } + setRows(clusters); + }, []); + + useEffect(() => { + onRowSelect(tableName, selected); + }, [selected]); + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = rows.map((n) => n.name); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + setSelected(newSelected); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const isSelected = (name) => selected.indexOf(name) !== -1; + + const emptyRows = + rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); + + return ( + <div className={classes.tableRoot}> + <EnhancedTableToolbar + tableName={tableName} + numSelected={selected.length} + /> + <TableContainer> + <Table + className={classes.table} + aria-labelledby="tableTitle" + size={"small"} + aria-label="enhanced table" + > + <EnhancedTableHead + classes={classes} + numSelected={selected.length} + order={order} + orderBy={orderBy} + onSelectAllClick={handleSelectAllClick} + onRequestSort={handleRequestSort} + rowCount={rows.length} + /> + <TableBody> + {stableSort(rows, getComparator(order, orderBy)) + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => { + const isItemSelected = isSelected(row.name); + const labelId = `enhanced-table-checkbox-${index}`; + + return ( + <TableRow + hover + onClick={(event) => handleClick(event, row.name)} + role="checkbox" + aria-checked={isItemSelected} + tabIndex={-1} + key={row.name} + selected={isItemSelected} + > + <TableCell padding="checkbox"> + <Checkbox + checked={isItemSelected} + inputProps={{ "aria-labelledby": labelId }} + /> + </TableCell> + <TableCell + component="th" + id={labelId} + scope="row" + padding="none" + > + {row.name} + </TableCell> + <TableCell align="right">{row.description}</TableCell> + </TableRow> + ); + })} + {emptyRows > 0 && ( + <TableRow style={{ height: 33 * emptyRows }}> + <TableCell colSpan={6} /> + </TableRow> + )} + </TableBody> + </Table> + </TableContainer> + <TablePagination + rowsPerPageOptions={[5, 10, 25]} + component="div" + count={rows.length} + rowsPerPage={rowsPerPage} + page={page} + onChangePage={handleChangePage} + onChangeRowsPerPage={handleChangeRowsPerPage} + /> + </div> + ); +} + +export default EnhancedTable; diff --git a/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx b/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx index 6dfb8279..09f70c99 100644 --- a/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx +++ b/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { TableContainer, @@ -24,7 +24,7 @@ import { } from "@material-ui/core"; import Paper from "@material-ui/core/Paper"; import TableBody from "@material-ui/core/TableBody"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import DeleteIcon from "@material-ui/icons/Delete"; import PropTypes from "prop-types"; import apiService from "../../services/apiService"; @@ -106,31 +106,42 @@ const AppPlacementIntentTable = ({ data, setData, ...props }) => { <StyledTableCell>{entry.name}</StyledTableCell> <StyledTableCell>{entry.description}</StyledTableCell> <StyledTableCell> - {entry.allOf.map((intent, index) => ( - <Paper - key={index} - style={{ width: "max-content" }} - variant="outlined" - > - <label>Cluster Provider : </label> - <label style={{ fontWeight: "bold" }}> - {intent["provider-name"]}, - </label> - <label>Labels : </label> - <Chip - style={{ marginRight: "10px" }} - size="small" - label={intent["cluster-label-name"]} - color="primary" + {entry.allOf && + entry.allOf.map((intent, index) => ( + <Paper + key={index} + style={{ width: "max-content" }} variant="outlined" - /> - </Paper> - ))} + > + <label>Cluster Provider : </label> + <label style={{ fontWeight: "bold" }}> + {intent["provider-name"]} + </label> + <label>, Cluster : </label> + <label style={{ fontWeight: "bold" }}> + {intent["cluster-name"]} + </label> + {intent["cluster-label-name"] && ( + <> + <label>, Labels : </label> + <Chip + style={{ marginRight: "10px" }} + size="small" + label={intent["cluster-label-name"]} + color="primary" + variant="outlined" + /> + </> + )} + </Paper> + ))} </StyledTableCell> <StyledTableCell> + {/* + //edit app placement api has not been implemented yet <IconButton onClick={(e) => handleEdit(index)} title="Edit"> <EditIcon color="primary" /> - </IconButton> + </IconButton> */} <IconButton onClick={(e) => handleDelete(index)} title="Delete" diff --git a/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx b/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx index bb43972c..9946c92d 100644 --- a/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx +++ b/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import clsx from "clsx"; @@ -168,6 +168,10 @@ const GenericPlacementIntentCard = (props) => { variant="outlined" size="small" color="secondary" + disabled={ + appPlacementIntentData.applications && + appPlacementIntentData.applications.length > 0 + } style={{ float: "right" }} startIcon={<DeleteIcon />} onClick={() => { @@ -190,6 +194,9 @@ const GenericPlacementIntentCard = (props) => { } /> )} + {!(props.appsData && props.appsData.length > 0) && ( + <div>No app found for adding app placement intent</div> + )} </CardContent> </Collapse> </Card> diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx index f0cf1e1d..ee1fec74 100644 --- a/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx +++ b/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx @@ -11,245 +11,112 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField, InputLabel, NativeSelect, FormControl, FormHelperText } from '@material-ui/core'; -import * as Yup from "yup"; -import { Formik } from 'formik'; +// ======================================================================== +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import Stepper from "./Stepper"; import apiService from "../services/apiService"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - version: Yup.string().required(), - compositeProfile: Yup.string().required(), - overrideValues: Yup.array().of(Yup.object()).typeError("Invalid override values, expected array"), - }) - const DIGform = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create"; - const title = item ? "Edit Deployment Intent Group" : "Create Deployment Intent Group"; - const [selectedAppIndex, setSelectedAppIndex] = useState(0); - const handleClose = () => { - onClose(); - }; - useEffect(() => { - props.data.compositeApps.forEach(compositeApp => { - let request = { projectName: props.projectName, compositeAppName: compositeApp.metadata.name, compositeAppVersion: compositeApp.spec.version } - apiService.getCompositeProfiles(request).then(res => { - compositeApp.profiles = res; - }).catch(error => { - console.log("error getting cluster providers : ", error) - }).finally(() => { - }) + const { onClose, item, open, onSubmit } = props; + const title = item + ? "Edit Deployment Intent Group" + : "Create Deployment Intent Group"; + const handleClose = () => { + onClose(); + }; + useEffect(() => { + props.data.compositeApps.forEach((compositeApp) => { + let request = { + projectName: props.projectName, + compositeAppName: compositeApp.metadata.name, + compositeAppVersion: compositeApp.spec.version, + }; + apiService + .getCompositeProfiles(request) + .then((res) => { + compositeApp.profiles = res; }) - }, [props.data.compositeApps, props.projectName]); - let initialValues = item ? - { name: item.metadata.name, description: item.metadata.description, overrideValues: JSON.stringify(item.spec["override-values"]), compositeApp: item.compositeAppName, compositeProfile: item.spec.profile, version: item.spec.version } : - { name: "", description: "", overrideValues: undefined, compositeApp: props.data.compositeApps[0].metadata.name, compositeProfile: "", version: "" } - - const handleSetCompositeApp = (val) => { - props.data.compositeApps.forEach((ca, index) => { - if (ca.metadata.name === val) - setSelectedAppIndex(index); - }); - } - - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - values.compositeAppVersion = props.data.compositeApps[selectedAppIndex].spec.version; - onSubmit(values); - }} - validationSchema={schema} - > - {formicProps => { - const { - values, - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = formicProps; - return ( - <form noValidate onSubmit={handleSubmit} onChange={handleChange}> - <DialogContent dividers> - <div style={{ width: "45%", float: "left" }}> - <InputLabel shrink htmlFor="compositeApp-label-placeholder"> - Composite App - </InputLabel> - <NativeSelect - name="compositeApp" - onChange={(e) => { handleChange(e); handleSetCompositeApp(e.target.value) }} - onBlur={handleBlur} - disabled={item ? true : false} - inputProps={{ - name: 'compositeApp', - id: 'compositeApps-label-placeholder', - }} - > - {item && (<option >{values.compositeApp}</option>)} - {props.data && props.data.compositeApps.map(compositeApp => - (<option value={compositeApp.metadata.name} key={compositeApp.metadata.name} >{compositeApp.metadata.name}</option>) - )} - </NativeSelect> - </div> - - <FormControl style={{ width: "45%", float: "right" }} required error={errors.compositeProfile && touched.compositeProfile}> - <InputLabel htmlFor="compositeProfile-label-placeholder"> - Composite Profile - </InputLabel> - <NativeSelect - name="compositeProfile" - onChange={handleChange} - onBlur={handleBlur} - disabled={item ? true : false} - required - inputProps={{ - name: 'compositeProfile', - id: 'compositeProfile-label-placeholder', - }} - > - <option value="" /> - {props.data.compositeApps[selectedAppIndex].profiles && props.data.compositeApps[selectedAppIndex].profiles.map(compositeProfile => - (<option value={compositeProfile.metadata.name} key={compositeProfile.metadata.name} >{compositeProfile.metadata.name}</option>) - )} - </NativeSelect> - {errors.compositeProfile && touched.compositeProfile && <FormHelperText>Required</FormHelperText>} - </FormControl> - <TextField - style={{ width: "45%", float: "left", marginTop: "10px" }} - id="name" - label="Name" - type="text" - value={values.name} - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "45%", float: "right", marginTop: "10px" }} - id="version" - label="Version" - type="text" - name="version" - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.version && touched.version && ( - "Version is required" - ))} - required - error={errors.version && touched.version} - /> - <TextField - style={{ width: "100%", marginTop: "20px" }} - id="overrideValues" - label="Override Values" - type="text" - value={values.overrideValues} - onChange={handleChange} - onBlur={handleBlur} - required - multiline - rows={4} - variant="outlined" - error={errors.overrideValues && touched.overrideValues} - helperText={(errors.overrideValues && touched.overrideValues && ( - (errors["overrideValues"]) - ))} - /> - <TextField - style={{ width: "100%", marginBottom: "25px", marginTop: "10px" }} - name="description" - value={values.description} - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + .catch((error) => { + console.log("error getting cluster providers : ", error); + }) + .finally(() => {}); + }); + }, [props.data.compositeApps, props.projectName]); + return ( + <Dialog + maxWidth={"md"} + fullWidth={true} + onClose={handleClose} + open={open} + disableBackdropClick + > + <DialogTitle id="customized-dialog-title" onClose={handleClose}> + {title} + </DialogTitle> + <DialogContent dividers> + <Stepper + data={props.data} + projectName={props.projectName} + onSubmit={onSubmit} + /> + </DialogContent> + </Dialog> + ); }; DIGform.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, }; export default DIGform; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx index 5710b52b..3e22dc4c 100644 --- a/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx +++ b/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { withStyles, makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -22,14 +22,14 @@ import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import IconButton from "@material-ui/core/IconButton"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import DeleteDialog from "../common/Dialogue"; -import AddIcon from "@material-ui/icons/Add"; +// import AddIcon from "@material-ui/icons/Add"; import DeleteIcon from "@material-ui/icons/Delete"; import GetAppIcon from "@material-ui/icons/GetApp"; import apiService from "../services/apiService"; -import { Button } from "@material-ui/core"; -import IntentsForm from "./IntentsForm"; +// import { Button } from "@material-ui/core"; +// import IntentsForm from "./IntentsForm"; import Notification from "../common/Notification"; const StyledTableCell = withStyles((theme) => ({ @@ -58,20 +58,15 @@ const useStyles = makeStyles({ export default function DIGtable({ data, setData, ...props }) { const classes = useStyles(); const [open, setOpen] = useState(false); - // const [openForm, setOpenForm] = useState(false); const [index, setIndex] = useState(0); - const [openIntentsForm, setOpenIntentsForm] = useState(false); + // const [openIntentsForm, setOpenIntentsForm] = useState(false); const [notificationDetails, setNotificationDetails] = useState({}); - let handleEdit = (index) => { - // setIndex(index); - // setOpenForm(true); - }; const handleClose = (el) => { if (el.target.innerText === "Delete") { let request = { projectName: props.projectName, - compositeAppName: data[index].compositeAppName, - compositeAppVersion: data[index].compositeAppVersion, + compositeAppName: data[index].metadata.compositeAppName, + compositeAppVersion: data[index].metadata.compositeAppVersion, deploymentIntentGroupName: data[index].metadata.name, }; apiService @@ -92,50 +87,46 @@ export default function DIGtable({ data, setData, ...props }) { setIndex(index); setOpen(true); }; - const handleAddIntent = (index) => { - setIndex(index); - setOpenIntentsForm(true); - }; - const handleCloseIntentsForm = () => { - setOpenIntentsForm(false); - }; - const handleSubmitIntentForm = (values) => { - setOpenIntentsForm(false); - let request = { - projectName: props.projectName, - compositeAppName: values.compositeAppName, - compositeAppVersion: values.compositeAppVersion, - deploymentIntentGroupName: values.deploymentIntentGroupName, - payload: { - metadata: { name: values.name, description: values.description }, - spec: { - intent: { - genericPlacementIntent: values.genericPlacementIntent, - }, - }, - }, - }; - if (values.networkControllerIntent && values.networkControllerIntent !== "") - request.payload.spec.intent.ovnaction = values.networkControllerIntent; - apiService - .addIntentsToDeploymentIntentGroup(request) - .then((res) => { - if (data[index].intent) { - data[index].intent.push(res.spec.intent); - } else { - data[index].intent = [res.spec.intent]; - } - setData([...data]); - }) - .catch((err) => { - console.log("error adding intent to deployment intent group"); - }); - }; + // const handleCloseIntentsForm = () => { + // setOpenIntentsForm(false); + // }; + // const handleSubmitIntentForm = (values) => { + // setOpenIntentsForm(false); + // let request = { + // projectName: props.projectName, + // compositeAppName: values.compositeAppName, + // compositeAppVersion: values.compositeAppVersion, + // deploymentIntentGroupName: values.deploymentIntentGroupName, + // payload: { + // metadata: { name: values.name, description: values.description }, + // spec: { + // intent: { + // genericPlacementIntent: values.genericPlacementIntent, + // }, + // }, + // }, + // }; + // if (values.networkControllerIntent && values.networkControllerIntent !== "") + // request.payload.spec.intent.ovnaction = values.networkControllerIntent; + // apiService + // .addIntentsToDeploymentIntentGroup(request) + // .then((res) => { + // if (data[index].intent) { + // data[index].intent.push(res.spec.intent); + // } else { + // data[index].intent = [res.spec.intent]; + // } + // setData([...data]); + // }) + // .catch((err) => { + // console.log("error adding intent to deployment intent group"); + // }); + // }; const handleInstantiate = (index) => { let request = { projectName: props.projectName, - compositeAppName: data[index].compositeAppName, - compositeAppVersion: data[index].compositeAppVersion, + compositeAppName: data[index].metadata.compositeAppName, + compositeAppVersion: data[index].metadata.compositeAppVersion, deploymentIntentGroupName: data[index].metadata.name, }; apiService @@ -184,13 +175,6 @@ export default function DIGtable({ data, setData, ...props }) { <Notification notificationDetails={notificationDetails} /> {data && data.length > 0 && ( <> - <IntentsForm - projectName={props.projectName} - open={openIntentsForm} - onClose={handleCloseIntentsForm} - onSubmit={handleSubmitIntentForm} - data={data[index]} - /> <DeleteDialog open={open} onClose={handleClose} @@ -207,7 +191,7 @@ export default function DIGtable({ data, setData, ...props }) { <StyledTableCell>Version</StyledTableCell> <StyledTableCell>Profile</StyledTableCell> <StyledTableCell>Composite App</StyledTableCell> - <StyledTableCell>Intents</StyledTableCell> + {/* <StyledTableCell>Intents</StyledTableCell> */} <StyledTableCell>Description</StyledTableCell> <StyledTableCell style={{ width: "15%" }}> Actions @@ -225,54 +209,39 @@ export default function DIGtable({ data, setData, ...props }) { {row.spec.profile} </StyledTableCell> <StyledTableCell className={classes.cell}> - {row.compositeAppName} + {row.metadata.compositeAppName} </StyledTableCell> - { + {/* { <StyledTableCell className={classes.cell}> - {row.intent - ? row.intent.map((intentEntry) => { - return Object.keys(intentEntry) - .map(function (k) { - return intentEntry[k]; - }) - .join(" | "); - }) - : ""} + {Object.keys(row.spec.deployedIntents[0]).map(function ( + key, + index + ) { + if ( + index === 0 || + row.spec.deployedIntents[0][key] === "" + ) + return row.spec.deployedIntents[0][key]; + else return ", " + row.spec.deployedIntents[0][key]; + })} </StyledTableCell> - } + } */} <StyledTableCell className={classes.cell}> {row.metadata.description} </StyledTableCell> <StyledTableCell className={classes.cell}> - <Button - variant="outlined" - color="primary" - size="small" - onClick={() => { - handleAddIntent(index); - }} - startIcon={<AddIcon />} - > - Intents - </Button> <IconButton - disabled={!(row.intent && row.intent.length > 0)} + color={"primary"} + // disabled={ + // !( + // row.spec.deployedIntents && + // row.spec.deployedIntents.length > 0 + // ) + // } title="Instantiate" onClick={(e) => handleInstantiate(index)} > - <GetAppIcon - color={ - !(row.intent && row.intent.length > 0) - ? "" - : "primary" - } - /> - </IconButton> - <IconButton - onClick={(e) => handleEdit(index)} - title="Edit" - > - <EditIcon color="primary" /> + <GetAppIcon /> </IconButton> <IconButton onClick={(e) => handleDelete(index)} diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx index c1f73bb7..132b9fc3 100644 --- a/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx +++ b/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx @@ -11,14 +11,15 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useEffect, useState } from "react"; import DIGtable from "./DIGtable"; -import { withStyles, Button, Grid } from "@material-ui/core"; +import { withStyles, Button, Grid, Typography } from "@material-ui/core"; import AddIcon from "@material-ui/icons/Add"; import apiService from "../services/apiService"; import Spinner from "../common/Spinner"; import DIGform from "./DIGform"; +import { ReactComponent as EmptyIcon } from "../assets/icons/empty.svg"; const styles = { root: { @@ -44,84 +45,59 @@ const DeploymentIntentGroups = (props) => { setOpen(true); }; const handleSubmit = (inputFields) => { - let payload = { - metadata: { - name: inputFields.name, - description: inputFields.description, - }, - spec: { - profile: inputFields.compositeProfile, - version: inputFields.version, - }, - projectName: props.projectName, - compositeAppName: inputFields.compositeApp, - compositeAppVersion: inputFields.compositeAppVersion, - }; - if (inputFields.overrideValues && inputFields.overrideValues !== "") { - payload.spec["override-values"] = JSON.parse(inputFields.overrideValues); + try { + let payload = { + spec: { + projectName: props.projectName, + appsData: inputFields.intents.apps, + }, + }; + if (inputFields.overrideValues && inputFields.overrideValues !== "") { + payload.spec["override-values"] = JSON.parse( + inputFields.overrideValues + ); + } + payload = { ...payload, ...inputFields.general }; + apiService + .createDeploymentIntentGroup(payload) + .then((response) => { + response.metadata.compositeAppName = inputFields.general.compositeApp; + response.metadata.compositeAppVersion = + inputFields.general.compositeAppVersion; + data && data.length > 0 + ? setData([...data, response]) + : setData([response]); + }) + .catch((error) => { + console.log("error creating DIG : ", error); + }) + .finally(() => { + setIsloading(false); + setOpen(false); + }); + } catch (error) { + console.error(error); } - apiService - .createDeploymentIntentGroup(payload) - .then((response) => { - response.compositeAppName = inputFields.compositeApp; - response.compositeAppVersion = inputFields.compositeAppVersion; - data && data.length > 0 - ? setData([...data, response]) - : setData([response]); - }) - .catch((error) => { - console.log("error creating DIG : ", error); - }) - .finally(() => { - setIsloading(false); - setOpen(false); - }); }; useEffect(() => { + let getDigs = () => { + apiService + .getDeploymentIntentGroups({ projectName: props.projectName }) + .then((res) => { + setData(res); + }) + .catch((err) => { + console.log("error getting deplotment intent groups : " + err); + }) + .finally(() => setIsloading(false)); + }; + apiService .getCompositeApps({ projectName: props.projectName }) .then((response) => { - const getDigIntents = (input) => { - let request = { - projectName: props.projectName, - compositeAppName: input.compositeAppName, - compositeAppVersion: input.compositeAppVersion, - deploymentIntentGroupName: input.metadata.name, - }; - apiService - .getDeploymentIntentGroupIntents(request) - .then((res) => { - input.intent = res.intent; - }) - .catch((err) => {}) - .finally(() => { - setData((data) => [...data, input]); - }); - }; - response.forEach((compositeApp) => { - let request = { - projectName: props.projectName, - compositeAppName: compositeApp.metadata.name, - compositeAppVersion: compositeApp.spec.version, - }; - apiService - .getDeploymentIntentGroups(request) - .then((digResponse) => { - digResponse.forEach((res) => { - res.compositeAppName = compositeApp.metadata.name; - res.compositeAppVersion = compositeApp.spec.version; - getDigIntents(res); - }); - }) - .catch((error) => { - console.log("unable to get deployment intent groups", error); - }) - .finally(() => { - setCompositeApps(response); - setIsloading(false); - }); - }); + setCompositeApps(response); + getDigs(); }) .catch((err) => { console.log("Unable to get composite apps : ", err); @@ -131,16 +107,8 @@ const DeploymentIntentGroups = (props) => { return ( <> {isLoading && <Spinner />} - {!isLoading && compositeApps && compositeApps.length > 0 && ( + {!isLoading && compositeApps && ( <> - <Button - variant="outlined" - color="primary" - startIcon={<AddIcon />} - onClick={onCreateDIG} - > - Create Deployment Intent Group - </Button> <DIGform projectName={props.projectName} open={open} @@ -148,15 +116,41 @@ const DeploymentIntentGroups = (props) => { onSubmit={handleSubmit} data={{ compositeApps: compositeApps }} /> - <Grid container spacing={2} alignItems="center"> - <Grid item xs style={{ marginTop: "20px" }}> - <DIGtable - data={data} - setData={setData} - projectName={props.projectName} - /> - </Grid> + <Grid item xs={12}> + <Button + variant="outlined" + color="primary" + startIcon={<AddIcon />} + onClick={onCreateDIG} + > + Create Deployment Intent Group + </Button> </Grid> + + {data && data.length > 0 && ( + <Grid container spacing={2} alignItems="center"> + <Grid item xs style={{ marginTop: "20px" }}> + <DIGtable + data={data} + setData={setData} + projectName={props.projectName} + /> + </Grid> + </Grid> + )} + + {(data === null || (data && data.length < 1)) && ( + <Grid container spacing={2} direction="column" alignItems="center"> + <Grid style={{ marginTop: "60px" }} item xs={6}> + <EmptyIcon style={{ height: "100px", width: "100px" }} /> + </Grid> + <Grid item xs={12}> + <Typography variant="h6"> + No deployment group found, start by adding a deployment group + </Typography> + </Grid> + </Grid> + )} </> )} </> diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx new file mode 100644 index 00000000..e0662455 --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx @@ -0,0 +1,201 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import { makeStyles } from "@material-ui/core/styles"; +import PropTypes from "prop-types"; +import Tabs from "@material-ui/core/Tabs"; +import Tab from "@material-ui/core/Tab"; +import Box from "@material-ui/core/Box"; +import React, { useState } from "react"; +import Typography from "@material-ui/core/Typography"; +import { Formik } from "formik"; +import ExpandableCard from "../common/ExpandableCard"; +import AppPlacementForm from "../compositeApps/dialogs/AppFormPlacement"; +import NetworkForm from "../compositeApps/dialogs/AppNetworkForm"; + +const useStyles = makeStyles((theme) => ({ + tableRoot: { + width: "100%", + }, + paper: { + width: "100%", + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 550, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, + demo: { + backgroundColor: theme.palette.background.paper, + }, + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: "flex", + height: 424, + }, + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + }, +})); +function TabPanel(props) { + const { children, value, index, ...other } = props; + return ( + <div + role="tabpanel" + hidden={value !== index} + id={`vertical-tabpanel-${index}`} + aria-labelledby={`vertical-tab-${index}`} + {...other} + > + {value === index && <Box style={{ padding: "0 24px" }}>{children}</Box>} + </div> + ); +} + +function AppDetailsForm({ formikProps, ...props }) { + const classes = useStyles(); + const [value, setValue] = useState(0); + const handleChange = (event, newValue) => { + setValue(newValue); + }; + const handleRowSelect = (clusterProvider, selectedClusters) => { + if ( + !formikProps.values.apps[props.index].clusters || + formikProps.values.apps[props.index].clusters === undefined + ) { + if (selectedClusters.length > 0) { + let selectedClusterData = []; + selectedClusters.forEach((selectedCluster) => { + selectedClusterData.push({ name: selectedCluster, interfaces: [] }); + }); + formikProps.setFieldValue(`apps[${props.index}].clusters`, [ + { + provider: clusterProvider, + selectedClusters: selectedClusterData, + }, + ]); + } + } else { + let selectedClusterData = []; + //filter out the value of cluster provider so that it can be completely replaced by the new values + let updatedClusterValues = formikProps.values.apps[ + props.index + ].clusters.filter((cluster) => cluster.provider !== clusterProvider); + selectedClusters.forEach((selectedCluster) => { + selectedClusterData.push({ name: selectedCluster, interfaces: [] }); + }); + if (selectedClusters.length > 0) + updatedClusterValues.push({ + provider: clusterProvider, + selectedClusters: selectedClusterData, + }); + formikProps.setFieldValue( + `apps[${props.index}].clusters`, + updatedClusterValues + ); + } + }; + return ( + <div className={classes.root}> + <Formik> + {() => { + return ( + <> + <Tabs + orientation="vertical" + variant="scrollable" + value={value} + onChange={handleChange} + aria-label="Vertical tabs example" + className={classes.tabs} + > + <Tab label="Placement" {...a11yProps(1)} /> + <Tab label="Network" {...a11yProps(2)} /> + </Tabs> + <TabPanel style={{ width: "85%" }} value={value} index={0}> + <AppPlacementForm + formikProps={formikProps} + index={props.index} + clusterProviders={props.clusterProviders} + handleRowSelect={handleRowSelect} + /> + </TabPanel> + <TabPanel style={{ width: "85%" }} value={value} index={1}> + <Typography variant="subtitle1">Select Network</Typography> + <NetworkForm + clusters={formikProps.values.apps[props.index].clusters} + formikProps={formikProps} + index={props.index} + /> + </TabPanel> + </> + ); + }} + </Formik> + </div> + ); +} + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.any.isRequired, + value: PropTypes.any.isRequired, +}; + +function a11yProps(index) { + return { + id: `vertical-tab-${index}`, + "aria-controls": `vertical-tabpanel-${index}`, + }; +} + +const AppForm2 = (props) => { + return ( + <ExpandableCard + error={ + props.formikProps.errors.apps && + props.formikProps.errors.apps[props.index] + } + title={props.name} + description={props.description} + content={ + <AppDetailsForm + formikProps={props.formikProps} + name={props.name} + index={props.index} + clusterProviders={props.clusterProviders} + /> + } + /> + ); +}; +export default AppForm2; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx new file mode 100644 index 00000000..5b5c4191 --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx @@ -0,0 +1,265 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import React, { useEffect, useState } from "react"; +import { Formik } from "formik"; +import * as Yup from "yup"; + +import { + Button, + DialogActions, + FormControl, + FormHelperText, + Grid, + InputLabel, + MenuItem, + Select, + TextField, +} from "@material-ui/core"; + +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), + version: Yup.string() + .matches(/^[A-Za-z0-9\\s]+$/, "Special characters and space not allowed") + .required("Version is required"), + compositeProfile: Yup.string().required(), + overrideValues: Yup.array() + .of(Yup.object()) + .typeError("Invalid override values, expected array"), +}); + +function DigFormGeneral(props) { + const { item, onSubmit } = props; + const [selectedAppIndex, setSelectedAppIndex] = useState(0); //let the first composite app as default selection + useEffect(() => { + if (item) { + props.data.compositeApps.forEach((ca, index) => { + if (ca.metadata.name === item.compositeApp) { + setSelectedAppIndex(index); + } + }); + } + }, []); + + let initialValues = item + ? { + ...item, + } + : { + name: "", + description: "", + overrideValues: undefined, + compositeApp: props.data.compositeApps[selectedAppIndex].metadata.name, + compositeProfile: "", + version: "", + }; + + const handleSetCompositeApp = (val) => { + props.data.compositeApps.forEach((ca, index) => { + if (ca.metadata.name === val) setSelectedAppIndex(index); + }); + }; + return ( + <Formik + initialValues={initialValues} + onSubmit={(values) => { + values.compositeAppVersion = + props.data.compositeApps[selectedAppIndex].spec.version; + onSubmit(values); + }} + validationSchema={schema} + > + {(formicProps) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = formicProps; + return ( + <form noValidate onSubmit={handleSubmit} onChange={handleChange}> + <Grid container spacing={4} justify="center"> + <Grid container item xs={12} spacing={8}> + <Grid item xs={12} md={6}> + <TextField + fullWidth + id="name" + label="Name" + type="text" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + helperText={ + errors.name && touched.name && "Name is required" + } + required + error={errors.name && touched.name} + /> + </Grid> + <Grid item xs={12} md={6}> + <TextField + fullWidth + id="version" + label="Version" + type="text" + name="version" + value={values.version} + onChange={handleChange} + onBlur={handleBlur} + helperText={ + errors.version && touched.version && errors["version"] + } + required + error={errors.version && touched.version} + /> + </Grid> + </Grid> + + <Grid item container xs={12} spacing={8}> + <Grid item xs={12} md={6}> + <InputLabel shrink htmlFor="compositeApp-label-placeholder"> + Composite App + </InputLabel> + <Select + fullWidth + name="compositeApp" + value={values.compositeApp} + onChange={(e) => { + handleChange(e); + handleSetCompositeApp(e.target.value); + }} + onBlur={handleBlur} + inputProps={{ + name: "compositeApp", + id: "compositeApps-label-placeholder", + }} + > + {props.data && + props.data.compositeApps.map((compositeApp) => ( + <MenuItem + value={compositeApp.metadata.name} + key={compositeApp.metadata.name} + > + {compositeApp.metadata.name} + </MenuItem> + ))} + </Select> + </Grid> + <Grid item xs={12} md={6}> + <FormControl + fullWidth + required + error={errors.compositeProfile && touched.compositeProfile} + > + <InputLabel htmlFor="compositeProfile-label-placeholder"> + Composite Profile + </InputLabel> + <Select + name="compositeProfile" + onChange={handleChange} + onBlur={handleBlur} + required + value={values.compositeProfile} + inputProps={{ + name: "compositeProfile", + id: "compositeProfile-label-placeholder", + }} + > + {props.data.compositeApps[selectedAppIndex].profiles && + props.data.compositeApps[selectedAppIndex].profiles.map( + (compositeProfile) => ( + <MenuItem + value={compositeProfile.metadata.name} + key={compositeProfile.metadata.name} + > + {compositeProfile.metadata.name} + </MenuItem> + ) + )} + </Select> + {errors.compositeProfile && touched.compositeProfile && ( + <FormHelperText>Required</FormHelperText> + )} + </FormControl> + </Grid> + </Grid> + + <Grid item container xs={12} spacing={8}> + <Grid item xs={12} md={6}> + <TextField + fullWidth + name="description" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </Grid> + <Grid item xs={12} md={6}> + <TextField + fullWidth + id="overrideValues" + label="Override Values" + type="text" + value={values.overrideValues} + onChange={handleChange} + onBlur={handleBlur} + multiline + rows={4} + variant="outlined" + error={errors.overrideValues && touched.overrideValues} + helperText={ + errors.overrideValues && + touched.overrideValues && + errors["overrideValues"] + } + /> + </Grid> + </Grid> + <Grid item xs={12}> + <DialogActions> + <Button + autoFocus + disabled + onClick={props.onClickBack} + color="secondary" + > + Back + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + Next + </Button> + </DialogActions> + </Grid> + </Grid> + </form> + ); + }} + </Formik> + ); +} + +export default DigFormGeneral; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx new file mode 100644 index 00000000..580044ac --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx @@ -0,0 +1,160 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import React, { useEffect, useState } from "react"; +import { Formik } from "formik"; +import * as Yup from "yup"; +import AppForm from "./DigFormApp"; +import apiService from "../services/apiService"; + +import { Button, DialogActions, Grid } from "@material-ui/core"; + +DigFormIntents.propTypes = {}; +const schema = Yup.object({ + apps: Yup.array() + .of( + Yup.object({ + clusters: Yup.array() + .of( + Yup.object({ + provider: Yup.string(), + selectedClusters: Yup.array().of( + Yup.object({ + name: Yup.string(), + interfaces: Yup.array().of( + Yup.object({ + networkName: Yup.string().required(), + subnet: Yup.string().required(), + ip: Yup.string().matches( + /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, + "invalid ip address" + ), + }) + ), + }) + ), + }) + ) + .required("Select at least one cluster"), + }) + ) + .required("At least one app is required"), +}); + +function DigFormIntents(props) { + const { onSubmit, appsData } = props; + const [isLoading, setIsloading] = useState(true); + const [clusterProviders, setClusterProviders] = useState([]); + let initialValues = { apps: appsData }; + useEffect(() => { + let clusterProviderData = []; + apiService + .getClusterProviders() + .then((res) => { + res.forEach((clusterProvider, providerIndex) => { + clusterProviderData.push({ + name: clusterProvider.metadata.name, + clusters: [], + }); + apiService + .getClusters(clusterProvider.metadata.name) + .then((clusters) => { + clusters.forEach((cluster) => { + clusterProviderData[providerIndex].clusters.push({ + name: cluster.metadata.name, + description: cluster.metadata.description, + }); + }); + if (providerIndex + 1 === res.length) { + setClusterProviders(clusterProviderData); + setIsloading(false); + } + }) + .catch((err) => { + console.log( + `error getting clusters for ${clusterProvider.metadata.name} : ` + + err + ); + }); + }); + }) + .catch((err) => { + console.log("error getting cluster providers : " + err); + }); + }, []); + useEffect(() => {}, []); + + return ( + <Formik + initialValues={initialValues} + onSubmit={(values) => { + values.compositeAppVersion = onSubmit(values); + }} + validationSchema={schema} + > + {(formikProps) => { + const { + values, + isSubmitting, + handleChange, + handleSubmit, + } = formikProps; + return ( + !isLoading && ( + <form noValidate onSubmit={handleSubmit} onChange={handleChange}> + <Grid container spacing={4} justify="center"> + {initialValues.apps && + initialValues.apps.length > 0 && + initialValues.apps.map((app, index) => ( + <Grid key={index} item sm={12} xs={12}> + <AppForm + clusterProviders={clusterProviders} + formikProps={formikProps} + name={app.metadata.name} + description={app.metadata.description} + index={index} + initialValues={values} + /> + </Grid> + ))} + + <Grid item xs={12}> + <DialogActions> + <Button + autoFocus + onClick={props.onClickBack} + color="secondary" + > + Back + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + Submit + </Button> + </DialogActions> + </Grid> + </Grid> + </form> + ) + ); + }} + </Formik> + ); +} + +export default DigFormIntents; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx b/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx new file mode 100644 index 00000000..746f49e3 --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx @@ -0,0 +1,118 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Stepper from "@material-ui/core/Stepper"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import DigFormGeneral from "./DigFormGeneral"; +import DigFormIntents from "./DigFormIntents"; +import apiService from "../services/apiService"; + +const useStyles = makeStyles((theme) => ({ + root: { + width: "100%", + }, + backButton: { + marginRight: theme.spacing(1), + }, + instructions: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + }, +})); + +function getSteps() { + return ["General", "Intents"]; +} + +export default function HorizontalStepper(props) { + const classes = useStyles(); + const [activeStep, setActiveStep] = useState(0); + const [generalData, setGeneralData] = useState(null); + const [intentsData, setIntentsData] = useState(null); + const [appsData, setAppsData] = useState([]); + + const steps = getSteps(); + + function getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return ( + <DigFormGeneral + data={props.data} + onSubmit={handleGeneralFormSubmit} + item={generalData} + /> + ); + case 1: + return ( + <DigFormIntents + appsData={appsData} + onSubmit={handleIntentsFormSubmit} + onClickBack={handleBack} + item={intentsData} + /> + ); + default: + return "Unknown stepIndex"; + } + } + + const handleNext = () => { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + }; + + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + const handleGeneralFormSubmit = (values) => { + setGeneralData(values); + let request = { + projectName: props.projectName, + compositeAppName: values.compositeApp, + compositeAppVersion: values.compositeAppVersion, + }; + apiService + .getApps(request) + .then((res) => { + setAppsData(res); + handleNext((prevActiveStep) => prevActiveStep + 1); + }) + .catch((err) => { + console.log("Error getting apps : " + err); + }); + }; + + const handleIntentsFormSubmit = (values) => { + setIntentsData(values); + let digPayload = { general: generalData, intents: values }; + props.onSubmit(digPayload); + }; + return ( + <div className={classes.root}> + <Stepper activeStep={activeStep} alternativeLabel> + {steps.map((label) => ( + <Step key={label}> + <StepLabel>{label}</StepLabel> + </Step> + ))} + </Stepper> + <div> + <div>{getStepContent(activeStep)}</div> + </div> + </div> + ); +} diff --git a/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx b/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx index b641737b..ed93bd0f 100644 --- a/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx +++ b/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import clsx from "clsx"; @@ -54,7 +54,7 @@ const NetworkIntentCard = (props) => { const [expanded, setExpanded] = useState(false); const [workloadData, setWorkloadData] = useState([]); const handleExpandClick = () => { - if (!expanded && workloadData.length < 1) { + if (!expanded && workloadData && workloadData.length < 1) { let request = { projectName: props.projectName, compositeAppName: props.compositeAppName, @@ -189,6 +189,7 @@ const NetworkIntentCard = (props) => { variant="outlined" size="small" color="secondary" + disabled={workloadData && workloadData.length > 0} style={{ float: "right" }} startIcon={<DeleteIcon />} onClick={props.onDeleteNetworkControllerIntent.bind( @@ -210,6 +211,9 @@ const NetworkIntentCard = (props) => { } /> )} + {!(props.appsData && props.appsData.length > 0) && ( + <div>No app found for adding workload intent</div> + )} </CardContent> </Collapse> </Card> diff --git a/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx b/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx index 5e196ed9..540de8a0 100644 --- a/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx +++ b/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx @@ -11,207 +11,268 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== -import React, { useState } from 'react'; -import { TableContainer, Table, TableRow, TableHead, withStyles, Chip, TableCell } from '@material-ui/core'; +// ======================================================================== +import React, { useState } from "react"; +import { + TableContainer, + Table, + TableRow, + TableHead, + withStyles, + Chip, + TableCell, +} from "@material-ui/core"; import Paper from "@material-ui/core/Paper"; import TableBody from "@material-ui/core/TableBody"; import EditIcon from "@material-ui/icons/Edit"; import DeleteIcon from "@material-ui/icons/Delete"; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; import apiService from "../services/apiService"; import DeleteDialog from "../common/Dialogue"; -import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; -import IconButton from '@material-ui/core/IconButton'; -import AddIconOutline from '@material-ui/icons/AddCircleOutline'; +import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined"; +import IconButton from "@material-ui/core/IconButton"; +import AddIconOutline from "@material-ui/icons/AddCircleOutline"; import Form from "./InterfaceForm"; import InterfaceDetailsDialog from "../common/DetailsDialog"; - const StyledTableCell = withStyles((theme) => ({ - body: { - fontSize: 14, - }, + body: { + fontSize: 14, + }, }))(TableCell); const StyledTableRow = withStyles((theme) => ({ - root: { - "&:nth-of-type(odd)": { - backgroundColor: theme.palette.action.hover, - }, + root: { + "&:nth-of-type(odd)": { + backgroundColor: theme.palette.action.hover, }, + }, }))(TableRow); const WokloadIntentTable = ({ data, setData, ...props }) => { - const [formOpen, setFormOpen] = useState(false); - const [index, setIndex] = useState(0); - const [openDialog, setOpenDialog] = useState(false); - const [openInterfaceDetails, setOpenInterfaceDetails] = useState(false); - const [selectedInterface, setSelectedInterface] = useState({}); - const [openInterfaceDialog, setOpenInterfaceDialog] = useState(false); - const handleDelete = (index) => { - setIndex(index); - setOpenDialog(true); - } - const handleEdit = () => { + const [formOpen, setFormOpen] = useState(false); + const [index, setIndex] = useState(0); + const [openDialog, setOpenDialog] = useState(false); + const [openInterfaceDetails, setOpenInterfaceDetails] = useState(false); + const [selectedInterface, setSelectedInterface] = useState({}); + const [openInterfaceDialog, setOpenInterfaceDialog] = useState(false); + const handleDelete = (index) => { + setIndex(index); + setOpenDialog(true); + }; + const handleEdit = () => {}; + const handleInterfaceDetailOpen = (entry) => { + setSelectedInterface(entry); + setOpenInterfaceDetails(true); + }; + const handleDeleteInterface = (index, entry) => { + setIndex(index); + setSelectedInterface(entry); + setOpenInterfaceDialog(true); + }; + const handleAddInterface = (index) => { + setIndex(index); + setFormOpen(true); + }; + const handleCloseForm = () => { + setFormOpen(false); + }; + const handleCloseInterfaceDialog = (el) => { + if (el.target.innerText === "Delete") { + let request = { + projectName: props.projectName, + compositeAppName: props.compositeAppName, + compositeAppVersion: props.compositeAppVersion, + networkControllerIntentName: props.networkControllerIntentName, + workloadIntentName: data[index].metadata.name, + interfaceName: selectedInterface.metadata.name, + }; + apiService + .deleteInterface(request) + .then(() => { + console.log("Interface deleted"); + let updatedInterfaceData = data[index].interfaces.filter(function ( + obj + ) { + return obj.metadata.name !== selectedInterface.metadata.name; + }); + data[index].interfaces = updatedInterfaceData; + setData([...data]); + }) + .catch((err) => { + console.log("Error deleting interface : ", err); + }) + .finally(() => { + setIndex(0); + setSelectedInterface({}); + }); } - const handleInterfaceDetailOpen = (entry) => { - setSelectedInterface(entry); - setOpenInterfaceDetails(true); + setOpenInterfaceDialog(false); + }; + const handleCloseDialog = (el) => { + if (el.target.innerText === "Delete") { + let request = { + projectName: props.projectName, + compositeAppName: props.compositeAppName, + compositeAppVersion: props.compositeAppVersion, + networkControllerIntentName: props.networkControllerIntentName, + workloadIntentName: data[index].metadata.name, + }; + apiService + .deleteWorkloadIntent(request) + .then(() => { + console.log("workload intent deleted"); + data.splice(index, 1); + setData([...data]); + }) + .catch((err) => { + console.log("Error deleting workload intent : ", err); + }) + .finally(() => { + setIndex(0); + }); } - - const handleDeleteInterface = (index, entry) => { - setIndex(index); - setSelectedInterface(entry); - setOpenInterfaceDialog(true); - } - const handleAddInterface = (index) => { - setIndex(index); - setFormOpen(true); - } - const handleCloseForm = () => { - setFormOpen(false); - } - const handleCloseInterfaceDialog = (el) => { - if (el.target.innerText === "Delete") { - let request = { - projectName: props.projectName, - compositeAppName: props.compositeAppName, - compositeAppVersion: props.compositeAppVersion, - networkControllerIntentName: props.networkControllerIntentName, - workloadIntentName: data[index].metadata.name, - interfaceName: selectedInterface.metadata.name - } - apiService.deleteInterface(request).then(() => { - console.log("Interface deleted"); - let updatedInterfaceData = data[index].interfaces.filter(function (obj) { - return obj.metadata.name !== selectedInterface.metadata.name; - }); - data[index].interfaces = updatedInterfaceData; - setData([...data]); - }).catch(err => { - console.log("Error deleting interface : ", err) - }).finally(() => { - setIndex(0); - setSelectedInterface({}); - }) - } - setOpenInterfaceDialog(false); - } - const handleCloseDialog = (el) => { - if (el.target.innerText === "Delete") { - let request = { - projectName: props.projectName, - compositeAppName: props.compositeAppName, - compositeAppVersion: props.compositeAppVersion, - networkControllerIntentName: props.networkControllerIntentName, - workloadIntentName: data[index].metadata.name - } - apiService.deleteWorkloadIntent(request).then(() => { - console.log("workload intent deleted"); - data.splice(index, 1); - setData([...data]); - }).catch(err => { - console.log("Error deleting workload intent : ", err) - }).finally(() => { - setIndex(0); - }) + setOpenDialog(false); + }; + const handleSubmit = (values) => { + let spec = values.spec ? JSON.parse(values.spec) : ""; + let request = { + payload: { + metadata: { name: values.name, description: values.description }, + spec: spec, + }, + projectName: props.projectName, + compositeAppName: props.compositeAppName, + compositeAppVersion: props.compositeAppVersion, + networkControllerIntentName: props.networkControllerIntentName, + workloadIntentName: data[index].metadata.name, + }; + apiService + .addInterface(request) + .then((res) => { + if (data[index].interfaces && data[index].interfaces.length > 0) { + data[index].interfaces.push(res); + } else { + data[index].interfaces = [res]; } - setOpenDialog(false); - } - const handleSubmit = (values) => { - let spec = values.spec ? JSON.parse(values.spec) : ""; - let request = { - payload: { metadata: { name: values.name, description: values.description }, spec: spec }, - projectName: props.projectName, - compositeAppName: props.compositeAppName, - compositeAppVersion: props.compositeAppVersion, - networkControllerIntentName: props.networkControllerIntentName, - workloadIntentName: data[index].metadata.name - }; - apiService.addInterface(request) - .then(res => { - if (data[index].interfaces && data[index].interfaces.length > 0) { - data[index].interfaces.push(res); - } - else { - data[index].interfaces = [res]; - } - setData([...data]); - }) - .catch(err => { - console.log("error creating composite profile : ", err); - }) - .finally(() => { setFormOpen(false); }) - } - return ( - <> - <InterfaceDetailsDialog open={openInterfaceDetails} onClose={setOpenInterfaceDetails} item={selectedInterface} type="Interface" /> - <Form open={formOpen} onClose={handleCloseForm} onSubmit={handleSubmit} /> - <DeleteDialog open={openDialog} onClose={handleCloseDialog} title={"Delete Profile"} - content={`Are you sure you want to delete "${data && data[index] ? data[index].metadata.name : ""}"`} /> - <DeleteDialog open={openInterfaceDialog} onClose={handleCloseInterfaceDialog} title={"Delete Interface"} - content={`Are you sure you want to delete "${selectedInterface.metadata ? selectedInterface.metadata.name : ""}"`} /> - <TableContainer component={Paper}> - <Table> - <TableHead> - <TableRow> - <StyledTableCell>Name</StyledTableCell> - <StyledTableCell>Description</StyledTableCell> - <StyledTableCell>App</StyledTableCell> - <StyledTableCell>Workload Resource</StyledTableCell> - <StyledTableCell style={{ width: "27%" }}>Interfaces</StyledTableCell> - <StyledTableCell>Actions</StyledTableCell> - </TableRow> - </TableHead> - <TableBody> - {data.map((entry, index) => - <StyledTableRow key={entry.metadata.name + index}> - <StyledTableCell> - {entry.metadata.name} - </StyledTableCell> - <StyledTableCell > - {entry.metadata.description} - </StyledTableCell> - <StyledTableCell > - {entry.spec["application-name"]} - </StyledTableCell> - <StyledTableCell> - {entry.spec["workload-resource"]} - </StyledTableCell> - <StyledTableCell> - {entry.interfaces && (entry.interfaces.length > 0) && entry.interfaces.map((interfaceEntry, interfacekIndex) => - (<Chip - key={interfaceEntry.metadata.name + "" + interfacekIndex} - size="small" - icon={<InfoOutlinedIcon onClick={() => { handleInterfaceDetailOpen(interfaceEntry) }} style={{ cursor: "pointer" }} />} - onDelete={(e) => { handleDeleteInterface(index, interfaceEntry) }} - label={interfaceEntry.spec.ipAddress} - style={{ marginRight: "10px", marginBottom: "5px" }} - />) - )} - <IconButton color="primary" onClick={() => { handleAddInterface(index) }}> - <AddIconOutline /> - </IconButton> - </StyledTableCell> - <StyledTableCell > - <IconButton onClick={(e) => handleEdit(index)} title="Edit" > - <EditIcon color="primary" /> - </IconButton> - <IconButton onClick={(e) => handleDelete(index)} title="Delete" > - <DeleteIcon color="secondary" /> - </IconButton> - </StyledTableCell> - </StyledTableRow> - )} - </TableBody> - </Table> - </TableContainer></> - ); + setData([...data]); + }) + .catch((err) => { + console.log("error creating composite profile : ", err); + }) + .finally(() => { + setFormOpen(false); + }); + }; + return ( + <> + <InterfaceDetailsDialog + open={openInterfaceDetails} + onClose={setOpenInterfaceDetails} + item={selectedInterface} + type="Interface" + /> + <Form open={formOpen} onClose={handleCloseForm} onSubmit={handleSubmit} /> + <DeleteDialog + open={openDialog} + onClose={handleCloseDialog} + title={"Delete Profile"} + content={`Are you sure you want to delete "${ + data && data[index] ? data[index].metadata.name : "" + }"`} + /> + <DeleteDialog + open={openInterfaceDialog} + onClose={handleCloseInterfaceDialog} + title={"Delete Interface"} + content={`Are you sure you want to delete "${ + selectedInterface.metadata ? selectedInterface.metadata.name : "" + }"`} + /> + <TableContainer component={Paper}> + <Table> + <TableHead> + <TableRow> + <StyledTableCell>Name</StyledTableCell> + <StyledTableCell>Description</StyledTableCell> + <StyledTableCell>App</StyledTableCell> + <StyledTableCell>Workload Resource</StyledTableCell> + <StyledTableCell style={{ width: "27%" }}> + Interfaces + </StyledTableCell> + <StyledTableCell>Actions</StyledTableCell> + </TableRow> + </TableHead> + <TableBody> + {data.map((entry, index) => ( + <StyledTableRow key={entry.metadata.name + index}> + <StyledTableCell>{entry.metadata.name}</StyledTableCell> + <StyledTableCell>{entry.metadata.description}</StyledTableCell> + <StyledTableCell> + {entry.spec["application-name"]} + </StyledTableCell> + <StyledTableCell> + {entry.spec["workload-resource"]} + </StyledTableCell> + <StyledTableCell> + {entry.interfaces && + entry.interfaces.length > 0 && + entry.interfaces.map((interfaceEntry, interfacekIndex) => ( + <Chip + key={ + interfaceEntry.metadata.name + "" + interfacekIndex + } + size="small" + icon={ + <InfoOutlinedIcon + onClick={() => { + handleInterfaceDetailOpen(interfaceEntry); + }} + style={{ cursor: "pointer" }} + /> + } + onDelete={(e) => { + handleDeleteInterface(index, interfaceEntry); + }} + label={interfaceEntry.spec.ipAddress} + style={{ marginRight: "10px", marginBottom: "5px" }} + /> + ))} + <IconButton + color="primary" + onClick={() => { + handleAddInterface(index); + }} + > + <AddIconOutline /> + </IconButton> + </StyledTableCell> + <StyledTableCell> + {/* + //edit workload intent api has not been added yet + <IconButton onClick={(e) => handleEdit(index)} title="Edit" > + <EditIcon color="primary" /> + </IconButton> */} + <IconButton + color="secondary" + disabled={entry.interfaces && entry.interfaces.length > 0} + onClick={(e) => handleDelete(index)} + title="Delete" + > + <DeleteIcon /> + </IconButton> + </StyledTableCell> + </StyledTableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </> + ); }; WokloadIntentTable.propTypes = { - data: PropTypes.arrayOf(PropTypes.object).isRequired, - setData: PropTypes.func.isRequired + data: PropTypes.arrayOf(PropTypes.object).isRequired, + setData: PropTypes.func.isRequired, }; export default WokloadIntentTable; diff --git a/src/tools/emcoui/src/services/apiService.js b/src/tools/emcoui/src/services/apiService.js index 4bff9305..0c830768 100644 --- a/src/tools/emcoui/src/services/apiService.js +++ b/src/tools/emcoui/src/services/apiService.js @@ -11,10 +11,9 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// ======================================================================== +// ======================================================================== import axios from "axios"; axios.defaults.baseURL = process.env.REACT_APP_BACKEND || ""; - //orchestrator //projects const createProject = (request) => { @@ -45,6 +44,14 @@ const getCompositeApps = (request) => { return res.data; }); }; +const addService = ({ projectName, ...request }) => { + return axios + .post(`/middleend/projects/${projectName}/composite-apps`, request.payload) + .then((res) => { + return res.data; + }); +}; + const createCompositeApp = ({ projectName, ...request }) => { return axios .post(`/v2/projects/${projectName}/composite-apps`, request.payload) @@ -65,7 +72,7 @@ const updateCompositeApp = (request) => { const deleteCompositeApp = (request) => { return axios .delete( - `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}` + `/middleend/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}` ) .then((res) => { return res.data; @@ -302,15 +309,10 @@ const deleteInterface = (request) => { }; //deployment intent group -const createDeploymentIntentGroup = ({ - projectName, - compositeAppName, - compositeAppVersion, - ...request -}) => { +const createDeploymentIntentGroup = (request) => { return axios .post( - `/v2/projects/${projectName}/composite-apps/${compositeAppName}/${compositeAppVersion}/deployment-intent-groups`, + `/middleend/projects/${request.spec.projectName}/composite-apps/${request.compositeApp}/${request.compositeAppVersion}/deployment-intent-groups`, { ...request } ) .then((res) => { @@ -329,9 +331,7 @@ const addIntentsToDeploymentIntentGroup = (request) => { }; const getDeploymentIntentGroups = (request) => { return axios - .get( - `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups` - ) + .get(`/middleend/projects/${request.projectName}/deployment-intent-groups`) .then((res) => { return res.data; }); @@ -349,7 +349,7 @@ const editDeploymentIntentGroup = (request) => { const deleteDeploymentIntentGroup = (request) => { return axios .delete( - `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups/${request.deploymentIntentGroupName}` + `/middleend/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups/${request.deploymentIntentGroupName}` ) .then((res) => { return res.data; @@ -421,7 +421,7 @@ const updateClusterProvider = (request) => { const addCluster = (request) => { return axios .post( - `/v2/cluster-providers/${request.get("providerName")}/clusters`, + `/middleend/clusterproviders/${request.get("providerName")}/clusters`, request ) .then((res) => { @@ -577,6 +577,7 @@ const vimService = { getCompositeApps, getProfiles, createCompositeApp, + addService, updateCompositeApp, deleteCompositeApp, getApps, |