diff options
Diffstat (limited to 'src/tools/emcoui/src/compositeApps')
14 files changed, 1804 insertions, 298 deletions
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> |