summaryrefslogtreecommitdiffstats
path: root/src/tools/emcoui/src/compositeApps
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/emcoui/src/compositeApps')
-rw-r--r--src/tools/emcoui/src/compositeApps/CompositeApp.jsx30
-rw-r--r--src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx143
-rw-r--r--src/tools/emcoui/src/compositeApps/CompositeApps.jsx88
-rw-r--r--src/tools/emcoui/src/compositeApps/apps/Apps.jsx11
-rw-r--r--src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx23
-rw-r--r--src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx7
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx149
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx129
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx83
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx524
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx441
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx410
-rw-r--r--src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx55
-rw-r--r--src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx9
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 :&nbsp;</label>
- <label style={{ fontWeight: "bold" }}>
- {intent["provider-name"]}, &nbsp;
- </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 :&nbsp;</label>
+ <label style={{ fontWeight: "bold" }}>
+ {intent["provider-name"]}
+ </label>
+ <label>, &nbsp; Cluster :&nbsp;</label>
+ <label style={{ fontWeight: "bold" }}>
+ {intent["cluster-name"]}
+ </label>
+ {intent["cluster-label-name"] && (
+ <>
+ <label>, &nbsp; 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>