diff options
author | brunomilitzer <bruno.militzer@est.tech> | 2021-05-18 12:50:32 +0100 |
---|---|---|
committer | brunomilitzer <bruno.militzer@est.tech> | 2021-05-27 09:43:12 +0100 |
commit | 87111eea8588fb30936a8f876f5f3feed61e7b8a (patch) | |
tree | 1fe70089a51b4065f9db26e643982418d3c23756 /gui-clamp/ui-react | |
parent | e6a9251b46ce602ac0dccfe5890d886c7e445ed7 (diff) |
React Front-End UI
This commit is to move the React front end files from the Clamp Policy Repo to the Onap Gui Policy Repo.
Also created the added Maven featue to compile the React project via Node and NPM, and copy the production files to the target directory.
Fixed in gitignore that was ignoring the contents inside the logs/ directory.
Reformated JS spacing files from 4 spaces to 2 spaces.
Fixed Broken JEST test
Applied Jim's Code Review as well updated Node Version from 12.18 to 14.17 accross the modules. Unfortunately cannot apply NPM version since it broke npm install.
Fixed ONAP Job Builder Linting Error
Applied Jim's Second Code Review
Issue-ID: POLICY-3218
Signed-off-by: brunomilitzer <bruno.militzer@est.tech>
Change-Id: I01f95c350d27d72f941c835592fd596472601d6e
Diffstat (limited to 'gui-clamp/ui-react')
74 files changed, 11364 insertions, 0 deletions
diff --git a/gui-clamp/ui-react/package.json b/gui-clamp/ui-react/package.json new file mode 100644 index 0000000..8d11044 --- /dev/null +++ b/gui-clamp/ui-react/package.json @@ -0,0 +1,98 @@ +{ + "name": "onap-policy-clamp-ui", + "version": "${project.version}", + "description": "ONAP Policy Clamp UI", + "author": "ONAP Policy Team", + "license": "Apache-2.0", + "publishConfig": { + "registry": "${npm.publish.url}" + }, + "main": "index.js", + "proxy": "https://localhost:8443", + "scripts": { + "start": "HTTPS=true react-scripts start", + "build": "react-scripts build", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "eject": "react-scripts eject" + }, + "files": [ + "src/*.js", + "src/*.png", + "src/api", + "src/components", + "src/theme" + ], + "dependencies": { + "@json-editor/json-editor": "2.5.2", + "@fortawesome/fontawesome-free": "5.15.2", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-scripts": "4.0.3", + "react-bootstrap": "1.5.2", + "bootstrap-css-only": "4.3.1", + "styled-components": "5.2.1", + "react-router-dom": "5.2.0", + "@material-ui/core": "4.11.3", + "@material-ui/icons": "4.11.2", + "material-table": "1.68.1", + "react-select": "4.2.1", + "react-uuid": "1.0.2" + }, + "devDependencies": { + "jest": "26.6.0", + "babel-jest": "26.6.0", + "@babel/preset-env": "7.13.10", + "@babel/preset-react": "7.12.13", + "@babel/plugin-proposal-class-properties": "7.13.0", + "enzyme": "3.11.0", + "enzyme-adapter-react-17-updated": "1.0.2", + "enzyme-to-json": "3.6.1", + "jest-fetch-mock": "3.0.3" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "jest": { + "verbose": true, + "coverageDirectory": "${project.build.directory}/${ui.react.src}/coverage", + "collectCoverageFrom": [ + "**/*.{js,jsx}" + ], + "rootDir": "${project.build.directory}/${ui.react.src}", + "coverageReporters": [ + "lcov" + ], + "moduleNameMapper": { + "\\.(css|png)$": "identity-obj-proxy" + }, + "setupFiles": [ + "./src/setupTests.js" + ], + "snapshotSerializers": [ + "enzyme-to-json/serializer" + ] + }, + "babel": { + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + [ + "@babel/plugin-proposal-class-properties", + { + "loose": true + } + ], + [ + "@babel/plugin-transform-runtime" + ] + ] + } +} + diff --git a/gui-clamp/ui-react/public/index.html b/gui-clamp/ui-react/public/index.html new file mode 100644 index 0000000..bab3e7a --- /dev/null +++ b/gui-clamp/ui-react/public/index.html @@ -0,0 +1,40 @@ +<!-- + ============LICENSE_START======================================================= + ONAP CLAMP + ================================================================================ + Copyright (C) 2019 AT&T Intellectual Property. 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. + ============LICENSE_END============================================ + =================================================================== + + --> + +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no"> +<meta name="theme-color" content="#000000"> +<link rel="manifest" href="manifest.json"> +<link rel="shortcut icon" href="onap.ico"> + +<title>Clamp Designer UI</title> +</head> +<body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> +</body> +</html> diff --git a/gui-clamp/ui-react/public/manifest.json b/gui-clamp/ui-react/public/manifest.json new file mode 100644 index 0000000..8210c4e --- /dev/null +++ b/gui-clamp/ui-react/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Clamp Designer UI", + "name": "Clamp Designer UI", + "icons": [ + { + "src": "onap.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/gui-clamp/ui-react/public/onap.ico b/gui-clamp/ui-react/public/onap.ico Binary files differnew file mode 100644 index 0000000..85e168a --- /dev/null +++ b/gui-clamp/ui-react/public/onap.ico diff --git a/gui-clamp/ui-react/src/LoopUI.js b/gui-clamp/ui-react/src/LoopUI.js new file mode 100644 index 0000000..d998762 --- /dev/null +++ b/gui-clamp/ui-react/src/LoopUI.js @@ -0,0 +1,420 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react'; +import styled from 'styled-components'; +import MenuBar from './components/menu/MenuBar'; +import Navbar from 'react-bootstrap/Navbar'; +import logo from './logo.png'; +import { GlobalClampStyle } from './theme/globalStyle.js'; +import OnapConstants from './utils/OnapConstants'; + +import SvgGenerator from './components/loop_viewer/svg/SvgGenerator'; +import LoopLogs from './components/loop_viewer/logs/LoopLogs'; +import LoopStatus from './components/loop_viewer/status/LoopStatus'; +import UserService from './api/UserService'; +import LoopCache from './api/LoopCache'; +import LoopActionService from './api/LoopActionService'; + +import { Route } from 'react-router-dom' +import CreateLoopModal from './components/dialogs/Loop/CreateLoopModal'; +import OpenLoopModal from './components/dialogs/Loop/OpenLoopModal'; +import ModifyLoopModal from './components/dialogs/Loop/ModifyLoopModal'; +import PolicyModal from './components/dialogs/Policy/PolicyModal'; +import ViewAllPolicies from './components/dialogs/Policy/ViewAllPolicies'; +import LoopPropertiesModal from './components/dialogs/Loop/LoopPropertiesModal'; +import UserInfoModal from './components/dialogs/UserInfoModal'; +import LoopService from './api/LoopService'; +import ViewLoopTemplatesModal from './components/dialogs/Tosca/ViewLoopTemplatesModal'; +import ManageDictionaries from './components/dialogs/ManageDictionaries/ManageDictionaries'; +import PerformAction from './components/dialogs/PerformActions'; +import RefreshStatus from './components/dialogs/RefreshStatus'; +import DeployLoopModal from './components/dialogs/Loop/DeployLoopModal'; +import Alert from 'react-bootstrap/Alert'; +import Spinner from 'react-bootstrap/Spinner'; + +import { Link } from 'react-router-dom'; + +const StyledMainDiv = styled.div` + background-color: ${ props => props.theme.backgroundColor }; +` + +const StyledSpinnerDiv = styled.div` + justify-content: center !important; + display: flex !important; +`; + +const ProjectNameStyled = styled.a` + vertical-align: middle; + padding-left: 30px; + font-size: 36px; + font-weight: bold; +` + +const StyledRouterLink = styled(Link)` + color: ${ props => props.theme.menuFontColor }; + background-color: ${ props => props.theme.backgroundColor }; +` + +const StyledLoginInfo = styled.a` + color: ${ props => props.theme.menuFontColor }; + background-color: ${ props => props.theme.backgroundColor }; +` + +const LoopViewDivStyled = styled.div` + height: 100%; + overflow: hidden; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 10px; + color: ${ props => props.theme.loopViewerFontColor }; + background-color: ${ props => props.theme.loopViewerBackgroundColor }; + border: 1px solid transparent; + border-color: ${ props => props.theme.loopViewerHeaderBackgroundColor }; +` + +const LoopViewHeaderDivStyled = styled.div` + background-color: ${ props => props.theme.loopViewerHeaderBackgroundColor }; + padding: 10px 10px; + color: ${ props => props.theme.loopViewerHeaderFontColor }; +` + +const LoopViewBodyDivStyled = styled.div` + background-color: ${ props => (props.theme.loopViewerBackgroundColor) }; + padding: 10px 10px; + color: ${ props => (props.theme.loopViewerHeaderFontColor) }; + height: 95%; +` + +export default class LoopUI extends React.Component { + + state = { + userName: null, + loopName: OnapConstants.defaultLoopName, + loopCache: new LoopCache({}), + showSucAlert: false, + showFailAlert: false, + busyLoadingCount: 0 + }; + + constructor() { + super(); + this.getUser = this.getUser.bind(this); + this.updateLoopCache = this.updateLoopCache.bind(this); + this.loadLoop = this.loadLoop.bind(this); + this.closeLoop = this.closeLoop.bind(this); + this.showSucAlert = this.showSucAlert.bind(this); + this.showFailAlert = this.showFailAlert.bind(this); + this.disableAlert = this.disableAlert.bind(this); + this.setBusyLoading = this.setBusyLoading.bind(this); + this.clearBusyLoading = this.clearBusyLoading.bind(this); + this.isBusyLoading = this.isBusyLoading.bind(this); + this.renderGlobalStyle = this.renderGlobalStyle.bind(this); + this.renderSvg = this.renderSvg.bind(this); + } + + componentWillMount() { + this.getUser(); + } + + getUser() { + UserService.login().then(user => { + this.setState({ userName: user }) + }); + } + + renderMenuNavBar() { + return ( + <MenuBar loopName={ this.state.loopName }/> + ); + } + + renderUserLoggedNavBar() { + return ( + <Navbar.Text> + <StyledLoginInfo>Signed in as: </StyledLoginInfo> + <StyledRouterLink to="/userInfo">{ this.state.userName }</StyledRouterLink> + </Navbar.Text> + ); + } + + renderLogoNavBar() { + return ( + <Navbar.Brand> + <img height="50px" width="234px" src={ logo } alt=""/> + <ProjectNameStyled>CLAMP</ProjectNameStyled> + </Navbar.Brand> + ); + } + + renderAlertBar() { + return ( + <div> + <Alert variant="success" show={ this.state.showSucAlert } onClose={ this.disableAlert } dismissible> + { this.state.showMessage } + </Alert> + <Alert variant="danger" show={ this.state.showFailAlert } onClose={ this.disableAlert } dismissible> + { this.state.showMessage } + </Alert> + </div> + ); + } + + renderNavBar() { + return ( + <Navbar> + { this.renderLogoNavBar() } + <Navbar.Toggle aria-controls="responsive-navbar-nav"/> + { this.renderMenuNavBar() } + { this.renderUserLoggedNavBar() } + </Navbar> + ); + } + + renderLoopViewHeader() { + return ( + <LoopViewHeaderDivStyled> + Loop Viewer - { this.state.loopName } - ({ this.state.loopCache.getTemplateName() }) + </LoopViewHeaderDivStyled> + ); + } + + renderSvg() { + return ( + <SvgGenerator loopCache={ this.state.loopCache } clickable={ true } generatedFrom={ SvgGenerator.GENERATED_FROM_INSTANCE } isBusyLoading={ this.isBusyLoading }/> + ) + } + + renderLoopViewBody() { + return ( + <LoopViewBodyDivStyled> + { this.renderSvg() } + <LoopStatus loopCache={ this.state.loopCache }/> + <LoopLogs loopCache={ this.state.loopCache }/> + </LoopViewBodyDivStyled> + ); + } + + getLoopCache() { + return this.state.loopCache; + + } + + renderLoopViewer() { + return ( + <LoopViewDivStyled> + { this.renderLoopViewHeader() } + { this.renderLoopViewBody() } + </LoopViewDivStyled> + ); + } + + updateLoopCache(loopJson) { + + // If call with an empty object for loopJson, this is a reset to empty + // from someplace like PerformActions for the case where we are "deleting" + // a Control Loop model. Set the loopName to the default. + + if (loopJson === null) { + this.setState({ loopName: OnapConstants.defaultLoopName }); + this.setState({ loopCache: new LoopCache({}) }); + } else { + this.setState({ loopCache: new LoopCache(loopJson) }); + this.setState({ loopName: this.state.loopCache.getLoopName() }); + } + console.info(this.state.loopName + " loop loaded successfully"); + } + + showSucAlert(message) { + this.setState({ showSucAlert: true, showMessage: message }); + } + + showFailAlert(message) { + this.setState({ showFailAlert: true, showMessage: message }); + } + + disableAlert() { + this.setState({ showSucAlert: false, showFailAlert: false }); + } + + loadLoop(loopName) { + this.setBusyLoading(); + LoopService.getLoop(loopName).then(loop => { + console.debug("Updating loopCache"); + LoopActionService.refreshStatus(loopName).then(data => { + this.updateLoopCache(data); + this.clearBusyLoading(); + this.props.history.push('/'); + }) + .catch(error => { + this.updateLoopCache(loop); + this.clearBusyLoading(); + this.props.history.push('/'); + }); + }); + } + + setBusyLoading() { + this.setState((state, props) => ({ busyLoadingCount: ++state.busyLoadingCount })); + } + + clearBusyLoading() { + this.setState((state, props) => ({ busyLoadingCount: --state.busyLoadingCount })); + } + + isBusyLoading() { + if (this.state.busyLoadingCount === 0) { + return false; + } else { + return true; + } + } + + closeLoop() { + this.setState({ loopCache: new LoopCache({}), loopName: OnapConstants.defaultLoopName }); + this.props.history.push('/'); + } + + renderRoutes() { + return ( + <React.Fragment> + <Route path="/viewLoopTemplatesModal" render={ (routeProps) => (<ViewLoopTemplatesModal { ...routeProps } />) }/> + <Route path="/manageDictionaries" render={ (routeProps) => (<ManageDictionaries { ...routeProps } />) }/> + <Route path="/viewAllPolicies" render={ (routeProps) => (<ViewAllPolicies { ...routeProps } />) }/> + <Route path="/policyModal/:policyInstanceType/:policyName" render={ (routeProps) => (<PolicyModal { ...routeProps } + loopCache={ this.getLoopCache() } + loadLoopFunction={ this.loadLoop }/>) } + /> + <Route path="/createLoop" render={ (routeProps) => (<CreateLoopModal { ...routeProps } + loadLoopFunction={ this.loadLoop }/>) } + /> + <Route path="/openLoop" render={ (routeProps) => (<OpenLoopModal { ...routeProps } + loadLoopFunction={ this.loadLoop }/>) } + /> + <Route path="/loopProperties" render={ (routeProps) => (<LoopPropertiesModal { ...routeProps } + loopCache={ this.getLoopCache() } + loadLoopFunction={ this.loadLoop }/>) } + /> + <Route path="/modifyLoop" render={ (routeProps) => (<ModifyLoopModal { ...routeProps } + loopCache={ this.getLoopCache() } + loadLoopFunction={ this.loadLoop }/>) } + /> + + <Route path="/userInfo" render={ (routeProps) => (<UserInfoModal { ...routeProps } />) }/> + <Route path="/closeLoop" render={ this.closeLoop }/> + + <Route path="/submit" render={ (routeProps) => (<PerformAction { ...routeProps } + loopAction="submit" + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert } + setBusyLoading={ this.setBusyLoading } + clearBusyLoading={ this.clearBusyLoading }/>) } + /> + <Route path="/stop" render={ (routeProps) => (<PerformAction { ...routeProps } + loopAction="stop" + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert } + setBusyLoading={ this.setBusyLoading } + clearBusyLoading={ this.clearBusyLoading }/>) } + /> + <Route path="/restart" render={ (routeProps) => (<PerformAction { ...routeProps } + loopAction="restart" + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert } + setBusyLoading={ this.setBusyLoading } + clearBusyLoading={ this.clearBusyLoading }/>) } + /> + <Route path="/delete" render={ (routeProps) => (<PerformAction { ...routeProps } + loopAction="delete" + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert } + setBusyLoading={ this.setBusyLoading } + clearBusyLoading={ this.clearBusyLoading }/>) } + /> + <Route path="/undeploy" render={ (routeProps) => (<PerformAction { ...routeProps } + loopAction="undeploy" + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert } + setBusyLoading={ this.setBusyLoading } + clearBusyLoading={ this.clearBusyLoading }/>) } + /> + <Route path="/deploy" render={ (routeProps) => (<DeployLoopModal { ...routeProps } + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert }/>) } + /> + <Route path="/refreshStatus" render={ (routeProps) => (<RefreshStatus { ...routeProps } + loopCache={ this.getLoopCache() } + updateLoopFunction={ this.updateLoopCache } + showSucAlert={ this.showSucAlert } + showFailAlert={ this.showFailAlert }/>) } + /> + </React.Fragment> + ); + } + + renderGlobalStyle() { + return ( + <GlobalClampStyle/> + ); + }; + + + renderSpinner() { + if (this.isBusyLoading()) { + return ( + <StyledSpinnerDiv> + <Spinner animation="border" role="status"> + <span className="sr-only">Loading...</span> + </Spinner> + </StyledSpinnerDiv> + ); + } else { + return (<div></div>); + } + } + + render() { + return ( + <StyledMainDiv id="main_div"> + { this.renderGlobalStyle() } + { this.renderRoutes() } + { this.renderSpinner() } + { this.renderAlertBar() } + { this.renderNavBar() } + { this.renderLoopViewer() } + </StyledMainDiv> + ); + } +} diff --git a/gui-clamp/ui-react/src/LoopUI.test.js b/gui-clamp/ui-react/src/LoopUI.test.js new file mode 100644 index 0000000..47ade44 --- /dev/null +++ b/gui-clamp/ui-react/src/LoopUI.test.js @@ -0,0 +1,173 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import LoopUI from './LoopUI'; +import OnapConstants from './utils/OnapConstants'; + +import LoopCache from './api/LoopCache'; +import LoopActionService from './api/LoopActionService'; +import LoopService from './api/LoopService'; + +describe('Verify LoopUI', () => { + beforeEach(() => { + fetch.resetMocks(); + fetch.mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + text: () => "testUser" + + }); + }); + }) + + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "components": { + "POLICY": { + "componentState": { + "stateName": "UNKNOWN", + "description": "The policies defined have NOT yet been created on the policy engine" + } + }, + "DCAE": { + "componentState": { + "stateName": "BLUEPRINT_DEPLOYED", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop" + } + } + } + }); + + it('Test the render method', async () => { + const flushPromises = () => new Promise(setImmediate); + + const component = shallow(<LoopUI/>) + component.setState({ + loopName: "testLoopName", + showSucAlert: false, + showFailAlert: false + }); + await flushPromises(); + expect(component).toMatchSnapshot(); + }); + + test('Test closeLoop method', () => { + const historyMock = { push: jest.fn() }; + const component = shallow(<LoopUI history={ historyMock }/>) + const instance = component.instance(); + instance.closeLoop(); + + expect(component.state('loopName')).toEqual(OnapConstants.defaultLoopName); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }) + + test('Test loadLoop method refresh suc', async () => { + const historyMock = { push: jest.fn() }; + LoopService.getLoop = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + text: () => { + } + }); + }); + + LoopActionService.refreshStatus = jest.fn().mockImplementation(() => { + return Promise.resolve({ name: "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca" }); + }); + + const flushPromises = () => new Promise(setImmediate); + const component = shallow(<LoopUI history={ historyMock }/>) + const instance = component.instance(); + instance.loadLoop("LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca"); + + await flushPromises(); + + const resLoopCache = instance.getLoopCache(); + + expect(resLoopCache.getComponentStates()).toBeUndefined(); + expect(component.state('loopName')).toEqual("LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca"); + }) + + test('Test loadLoop method refresh fail', async () => { + const historyMock = { push: jest.fn() }; + LoopService.getLoop = jest.fn().mockImplementation(() => { + return Promise.resolve({ + name: "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "components": { + "POLICY": { + "componentState": { + "stateName": "UNKNOWN", + "description": "The policies defined have NOT yet been created on the policy engine" + } + }, + "DCAE": { + "componentState": { + "stateName": "BLUEPRINT_DEPLOYED", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop" + } + } + } + }); + }); + + LoopActionService.refreshStatus = jest.fn().mockImplementation(() => { + return Promise.reject({ error: "whatever" }); + }); + + const flushPromises = () => new Promise(setImmediate); + const component = shallow(<LoopUI history={ historyMock }/>) + const instance = component.instance(); + instance.loadLoop("LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca"); + + await flushPromises(); + + const resLoopCache = instance.getLoopCache(); + + expect(resLoopCache).toEqual(loopCache); + expect(component.state('loopName')).toEqual("LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca"); + }) + + test('Test alert methods', () => { + const component = shallow(<LoopUI/>) + expect(component.state('showSucAlert')).toEqual(false); + + const instance = component.instance(); + instance.showSucAlert("testAlert"); + expect(component.state('showSucAlert')).toEqual(true); + expect(component.state('showFailAlert')).toEqual(false); + expect(component.state('showMessage')).toEqual("testAlert"); + + instance.disableAlert(); + + expect(component.state('showSucAlert')).toEqual(false); + expect(component.state('showFailAlert')).toEqual(false); + + instance.showFailAlert("testAlert2"); + expect(component.state('showSucAlert')).toEqual(false); + expect(component.state('showFailAlert')).toEqual(true); + expect(component.state('showMessage')).toEqual("testAlert2"); + }) +}); diff --git a/gui-clamp/ui-react/src/NotFound.js b/gui-clamp/ui-react/src/NotFound.js new file mode 100644 index 0000000..fb6d10a --- /dev/null +++ b/gui-clamp/ui-react/src/NotFound.js @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react' + + +export default class NotFound extends React.Component { + render() { + return ( + <div id='main'> + <div class="divRow"><b>Page Not Found!</b></div> + <div class="divRow">Please cick <a href="/">here</a> to go back to the main page.</div> + </div> + + ); + } +} diff --git a/gui-clamp/ui-react/src/NotFound.test.js b/gui-clamp/ui-react/src/NotFound.test.js new file mode 100644 index 0000000..3b4dcb0 --- /dev/null +++ b/gui-clamp/ui-react/src/NotFound.test.js @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import NotFound from './NotFound'; + +describe('Verify OnapClamp', () => { + + it('Test the render method', () => { + + const component = shallow(<NotFound/>) + + expect(component).toMatchSnapshot(); + }); + +}); diff --git a/gui-clamp/ui-react/src/OnapClamp.js b/gui-clamp/ui-react/src/OnapClamp.js new file mode 100644 index 0000000..b8615ac --- /dev/null +++ b/gui-clamp/ui-react/src/OnapClamp.js @@ -0,0 +1,39 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react'; +import LoopUI from './LoopUI' +import { ThemeProvider } from 'styled-components'; +import { DefaultClampTheme } from './theme/globalStyle.js'; + +export default class OnapClamp extends LoopUI { + + render() { + console.info("Onap Clamp UI starting"); + return ( + <ThemeProvider theme={ DefaultClampTheme }> + { super.render() } + </ThemeProvider>); + } +} + diff --git a/gui-clamp/ui-react/src/OnapClamp.test.js b/gui-clamp/ui-react/src/OnapClamp.test.js new file mode 100644 index 0000000..c711a95 --- /dev/null +++ b/gui-clamp/ui-react/src/OnapClamp.test.js @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import OnapClamp from './OnapClamp'; + +describe('Verify OnapClamp', () => { + + it('Test the render method', () => { + + const component = shallow(<OnapClamp/>) + + expect(component).toMatchSnapshot(); + }); + +}); diff --git a/gui-clamp/ui-react/src/__snapshots__/LoopUI.test.js.snap b/gui-clamp/ui-react/src/__snapshots__/LoopUI.test.js.snap new file mode 100644 index 0000000..322c931 --- /dev/null +++ b/gui-clamp/ui-react/src/__snapshots__/LoopUI.test.js.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify LoopUI Test the render method 1`] = ` +<styled.div + id="main_div" +> + <Memo(l) /> + <Route + path="/viewLoopTemplatesModal" + render={[Function]} + /> + <Route + path="/manageDictionaries" + render={[Function]} + /> + <Route + path="/viewAllPolicies" + render={[Function]} + /> + <Route + path="/policyModal/:policyInstanceType/:policyName" + render={[Function]} + /> + <Route + path="/createLoop" + render={[Function]} + /> + <Route + path="/openLoop" + render={[Function]} + /> + <Route + path="/loopProperties" + render={[Function]} + /> + <Route + path="/modifyLoop" + render={[Function]} + /> + <Route + path="/userInfo" + render={[Function]} + /> + <Route + path="/closeLoop" + render={[Function]} + /> + <Route + path="/submit" + render={[Function]} + /> + <Route + path="/stop" + render={[Function]} + /> + <Route + path="/restart" + render={[Function]} + /> + <Route + path="/delete" + render={[Function]} + /> + <Route + path="/undeploy" + render={[Function]} + /> + <Route + path="/deploy" + render={[Function]} + /> + <Route + path="/refreshStatus" + render={[Function]} + /> + <div /> + <div> + <Alert + closeLabel="Close alert" + dismissible={true} + onClose={[Function]} + show={false} + transition={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "appear": false, + "in": false, + "mountOnEnter": false, + "timeout": 300, + "unmountOnExit": false, + }, + "render": [Function], + } + } + variant="success" + /> + <Alert + closeLabel="Close alert" + dismissible={true} + onClose={[Function]} + show={false} + transition={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "appear": false, + "in": false, + "mountOnEnter": false, + "timeout": 300, + "unmountOnExit": false, + }, + "render": [Function], + } + } + variant="danger" + /> + </div> + <Navbar + collapseOnSelect={false} + expand={true} + variant="light" + > + <NavbarBrand> + <img + alt="" + height="50px" + src={null} + width="234px" + /> + <styled.a> + CLAMP + </styled.a> + </NavbarBrand> + <NavbarToggle + aria-controls="responsive-navbar-nav" + label="Toggle navigation" + /> + <MenuBar + loopName="testLoopName" + /> + <NavbarText> + <styled.a> + Signed in as: + </styled.a> + <Styled(Link) + to="/userInfo" + > + testUser + </Styled(Link)> + </NavbarText> + </Navbar> + <styled.div> + <styled.div> + Loop Viewer - + testLoopName + - ( + ) + </styled.div> + <styled.div> + <withRouter(SvgGenerator) + clickable={true} + generatedFrom="INSTANCE" + isBusyLoading={[Function]} + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + <LoopStatus + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + <LoopLogs + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + </styled.div> + </styled.div> +</styled.div> +`; diff --git a/gui-clamp/ui-react/src/__snapshots__/NotFound.test.js.snap b/gui-clamp/ui-react/src/__snapshots__/NotFound.test.js.snap new file mode 100644 index 0000000..86bcfd1 --- /dev/null +++ b/gui-clamp/ui-react/src/__snapshots__/NotFound.test.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify OnapClamp Test the render method 1`] = ` +<div + id="main" +> + <div + class="divRow" + > + <b> + Page Not Found! + </b> + </div> + <div + class="divRow" + > + Please cick + <a + href="/" + > + here + </a> + to go back to the main page. + </div> +</div> +`; diff --git a/gui-clamp/ui-react/src/__snapshots__/OnapClamp.test.js.snap b/gui-clamp/ui-react/src/__snapshots__/OnapClamp.test.js.snap new file mode 100644 index 0000000..353bc11 --- /dev/null +++ b/gui-clamp/ui-react/src/__snapshots__/OnapClamp.test.js.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify OnapClamp Test the render method 1`] = ` +<Component + theme={ + Object { + "backgroundColor": "#eeeeee", + "fontDanger": "#eb238e", + "fontDark": "#888888", + "fontFamily": "Arial, Sans-serif", + "fontHighlight": "#ffff00", + "fontLight": "#ffffff", + "fontNormal": "black", + "fontSize": "16px", + "fontWarning": "#eb238e", + "loopLogsHeaderBackgroundColor": "white", + "loopLogsHeaderFontColor": "black", + "loopViewerBackgroundColor": "white", + "loopViewerFontColor": "yellow", + "loopViewerHeaderBackgroundColor": "#337ab7", + "loopViewerHeaderFontColor": "white", + "menuBackgroundColor": "white", + "menuFontColor": "black", + "menuHighlightedBackgroundColor": "#337ab7", + "menuHighlightedFontColor": "white", + "policyEditorBackgroundColor": "white", + "policyEditorFontSize": "13px", + "toscaTextareaBackgroundColor": "white", + "toscaTextareaFontSize": "13px", + } + } +> + <styled.div + id="main_div" + > + <Memo(l) /> + <Route + path="/viewLoopTemplatesModal" + render={[Function]} + /> + <Route + path="/manageDictionaries" + render={[Function]} + /> + <Route + path="/viewAllPolicies" + render={[Function]} + /> + <Route + path="/policyModal/:policyInstanceType/:policyName" + render={[Function]} + /> + <Route + path="/createLoop" + render={[Function]} + /> + <Route + path="/openLoop" + render={[Function]} + /> + <Route + path="/loopProperties" + render={[Function]} + /> + <Route + path="/modifyLoop" + render={[Function]} + /> + <Route + path="/userInfo" + render={[Function]} + /> + <Route + path="/closeLoop" + render={[Function]} + /> + <Route + path="/submit" + render={[Function]} + /> + <Route + path="/stop" + render={[Function]} + /> + <Route + path="/restart" + render={[Function]} + /> + <Route + path="/delete" + render={[Function]} + /> + <Route + path="/undeploy" + render={[Function]} + /> + <Route + path="/deploy" + render={[Function]} + /> + <Route + path="/refreshStatus" + render={[Function]} + /> + <div /> + <div> + <Alert + closeLabel="Close alert" + dismissible={true} + onClose={[Function]} + show={false} + transition={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "appear": false, + "in": false, + "mountOnEnter": false, + "timeout": 300, + "unmountOnExit": false, + }, + "render": [Function], + } + } + variant="success" + /> + <Alert + closeLabel="Close alert" + dismissible={true} + onClose={[Function]} + show={false} + transition={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "appear": false, + "in": false, + "mountOnEnter": false, + "timeout": 300, + "unmountOnExit": false, + }, + "render": [Function], + } + } + variant="danger" + /> + </div> + <Navbar + collapseOnSelect={false} + expand={true} + variant="light" + > + <NavbarBrand> + <img + alt="" + height="50px" + src={null} + width="234px" + /> + <styled.a> + CLAMP + </styled.a> + </NavbarBrand> + <NavbarToggle + aria-controls="responsive-navbar-nav" + label="Toggle navigation" + /> + <MenuBar + loopName="Empty (NO loop loaded yet)" + /> + <NavbarText> + <styled.a> + Signed in as: + </styled.a> + <Styled(Link) + to="/userInfo" + /> + </NavbarText> + </Navbar> + <styled.div> + <styled.div> + Loop Viewer - + Empty (NO loop loaded yet) + - ( + ) + </styled.div> + <styled.div> + <withRouter(SvgGenerator) + clickable={true} + generatedFrom="INSTANCE" + isBusyLoading={[Function]} + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + <LoopStatus + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + <LoopLogs + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + </styled.div> + </styled.div> + </styled.div> +</Component> +`; diff --git a/gui-clamp/ui-react/src/api/LoopActionService.js b/gui-clamp/ui-react/src/api/LoopActionService.js new file mode 100644 index 0000000..f3695c2 --- /dev/null +++ b/gui-clamp/ui-react/src/api/LoopActionService.js @@ -0,0 +1,74 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class LoopActionService { + + static performAction(cl_name, uiAction) { + console.info("LoopActionService perform action: " + uiAction + " closedloopName=" + cl_name); + const svcAction = uiAction.toLowerCase(); + return fetch(window.location.pathname + "restservices/clds/v2/loop/" + svcAction + "/" + cl_name, { + method: 'PUT', + credentials: 'same-origin' + }) + .then(function (response) { + if (response.ok) { + return response.json(); + } else { + return Promise.reject("Perform action failed with code:" + response.status); + } + }) + .then(function (data) { + console.info("Action Successful: " + uiAction); + return data; + }) + .catch(function (error) { + console.info("Action Failure: " + uiAction); + return Promise.reject(error); + }); + } + + + static refreshStatus(cl_name) { + console.info("Refresh the status for closedloopName=" + cl_name); + + return fetch(window.location.pathname + "restservices/clds/v2/loop/getstatus/" + cl_name, { + method: 'GET', + credentials: 'same-origin' + }) + .then(function (response) { + if (response.ok) { + return response.json(); + } else { + return Promise.reject("Refresh status failed with code:" + response.status); + } + }) + .then(function (data) { + console.info("Refresh status Successful"); + return data; + }) + .catch(function (error) { + console.info("Refresh status failed:", error); + return Promise.reject(error); + }); + } +} diff --git a/gui-clamp/ui-react/src/api/LoopCache.js b/gui-clamp/ui-react/src/api/LoopCache.js new file mode 100644 index 0000000..ac2da07 --- /dev/null +++ b/gui-clamp/ui-react/src/api/LoopCache.js @@ -0,0 +1,252 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class LoopCache { + loopJsonCache; + + constructor(loopJson) { + this.loopJsonCache = loopJson; + } + + updateMicroServiceProperties(name, newMsProperties) { + for (var policy in this.loopJsonCache["microServicePolicies"]) { + if (this.loopJsonCache["microServicePolicies"][policy]["name"] === name) { + this.loopJsonCache["microServicePolicies"][policy]["configurationsJson"] = newMsProperties; + } + } + } + + updateMicroServicePdpGroup(name, pdpGroup, pdpSubgroup) { + for (var policy in this.loopJsonCache["microServicePolicies"]) { + if (this.loopJsonCache["microServicePolicies"][policy]["name"] === name) { + this.loopJsonCache["microServicePolicies"][policy]["pdpGroup"] = pdpGroup; + this.loopJsonCache["microServicePolicies"][policy]["pdpSubgroup"] = pdpSubgroup; + } + } + } + + updateGlobalProperties(newGlobalProperties) { + this.loopJsonCache["globalPropertiesJson"] = newGlobalProperties; + } + + updateOperationalPolicyProperties(name, newOpProperties) { + for (var policy in this.loopJsonCache["operationalPolicies"]) { + if (this.loopJsonCache["operationalPolicies"][policy]["name"] === name) { + this.loopJsonCache["operationalPolicies"][policy]["configurationsJson"] = newOpProperties; + } + } + } + + updateOperationalPolicyPdpGroup(name, pdpGroup, pdpSubgroup) { + for (var policy in this.loopJsonCache["operationalPolicies"]) { + if (this.loopJsonCache["operationalPolicies"][policy]["name"] === name) { + this.loopJsonCache["operationalPolicies"][policy]["pdpGroup"] = pdpGroup; + this.loopJsonCache["operationalPolicies"][policy]["pdpSubgroup"] = pdpSubgroup; + } + } + } + + getLoopName() { + return this.loopJsonCache["name"]; + } + + getOperationalPolicyJsonSchema() { + return this.loopJsonCache["operationalPolicies"]["0"]["jsonRepresentation"]; + } + + getOperationalPolicies() { + return this.loopJsonCache["operationalPolicies"]; + } + + getOperationalPoliciesNoJsonSchema() { + var operationalPolicies = JSON.parse(JSON.stringify(this.loopJsonCache["operationalPolicies"])); + delete operationalPolicies[0]["jsonRepresentation"]; + return operationalPolicies; + } + + getGlobalProperties() { + return this.loopJsonCache["globalPropertiesJson"]; + } + + getDcaeDeploymentProperties() { + return this.loopJsonCache["globalPropertiesJson"]["dcaeDeployParameters"]; + } + + getMicroServicePolicies() { + return this.loopJsonCache["microServicePolicies"]; + } + + getOperationalPolicyForName(name) { + var opProperties = this.getOperationalPolicies(); + for (var policy in opProperties) { + if (opProperties[policy]["name"] === name) { + return opProperties[policy]; + } + } + return null; + } + + getOperationalPolicyPropertiesForName(name) { + var opConfig = this.getOperationalPolicyForName(name); + if (opConfig !== null) { + return opConfig["configurationsJson"]; + } + return null; + } + + getOperationalPolicyJsonRepresentationForName(name) { + var opConfig = this.getOperationalPolicyForName(name); + if (opConfig !== null) { + return opConfig["jsonRepresentation"]; + } + return null; + } + + getOperationalPolicySupportedPdpGroup(name) { + var opConfig = this.getOperationalPolicyForName(name); + if (opConfig !== null) { + if (opConfig["policyModel"]["policyPdpGroup"] !== undefined && opConfig["policyModel"]["policyPdpGroup"]["supportedPdpGroups"] !== undefined) { + return opConfig["policyModel"]["policyPdpGroup"]["supportedPdpGroups"]; + } + } + return []; + } + + getOperationalPolicyPdpGroup(name) { + var opConfig = this.getOperationalPolicyForName(name); + if (opConfig !== null) { + return opConfig["pdpGroup"]; + } + return null; + } + + getOperationalPolicyPdpSubgroup(name) { + var opConfig = this.getOperationalPolicyForName(name); + if (opConfig !== null) { + return opConfig["pdpSubgroup"]; + } + return null; + } + + getMicroServiceSupportedPdpGroup(name) { + var microService = this.getMicroServiceForName(name); + if (microService !== null) { + if (microService["policyModel"]["policyPdpGroup"] !== undefined && microService["policyModel"]["policyPdpGroup"]["supportedPdpGroups"] !== undefined) { + return microService["policyModel"]["policyPdpGroup"]["supportedPdpGroups"]; + } + } + return []; + } + + getMicroServicePdpGroup(name) { + var microService = this.getMicroServiceForName(name); + if (microService !== null) { + return microService["pdpGroup"]; + } + return null; + } + + getMicroServicePdpSubgroup(name) { + var microService = this.getMicroServiceForName(name); + if (microService !== null) { + return microService["pdpSubgroup"]; + } + return null; + } + + getMicroServiceForName(name) { + var msProperties = this.getMicroServicePolicies(); + for (var policy in msProperties) { + if (msProperties[policy]["name"] === name) { + return msProperties[policy]; + } + } + return null; + } + + getMicroServicePropertiesForName(name) { + var msConfig = this.getMicroServiceForName(name); + if (msConfig !== null) { + return msConfig["configurationsJson"]; + } + return null; + } + + getMicroServiceJsonRepresentationForName(name) { + var msConfig = this.getMicroServiceForName(name); + if (msConfig !== null) { + return msConfig["jsonRepresentation"]; + } + return null; + } + + getResourceDetailsVfProperty() { + return this.loopJsonCache["modelService"]["resourceDetails"]["VF"]; + } + + getResourceDetailsVfModuleProperty() { + return this.loopJsonCache["modelService"]["resourceDetails"]["VFModule"]; + } + + getLoopLogsArray() { + return this.loopJsonCache.loopLogs; + } + + getComputedState() { + return this.loopJsonCache.lastComputedState; + } + + getComponentStates() { + return this.loopJsonCache.components; + } + + getTemplateName() { + if (this.getLoopTemplate() !== undefined) { + return this.getLoopTemplate().name; + } + return null; + } + + getLoopTemplate() { + return this.loopJsonCache["loopTemplate"]; + } + + isOpenLoopTemplate() { + var loopTemplate = this.getLoopTemplate(); + if (loopTemplate != null && loopTemplate["allowedLoopType"] === "OPEN") { + return true; + } + return false; + } + + getAllLoopElementModels() { + var loopTemplate = this.getLoopTemplate(); + var loopElementModels = []; + if (loopTemplate != null) { + for (var element of loopTemplate['loopElementModelsUsed']) { + loopElementModels.push(element['loopElementModel']) + } + } + return loopElementModels; + } +} diff --git a/gui-clamp/ui-react/src/api/LoopCache.test.js b/gui-clamp/ui-react/src/api/LoopCache.test.js new file mode 100644 index 0000000..76f819c --- /dev/null +++ b/gui-clamp/ui-react/src/api/LoopCache.test.js @@ -0,0 +1,305 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import LoopCache from '../api/LoopCache'; + +const json = require('./LoopCacheMockFile.json'); + +describe('Verify LoopCache functions', () => { + const loopCache = new LoopCache(json); + it('getLoopName', () => { + expect(loopCache.getLoopName()).toBe("LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca"); + }); + + it('getOperationalPolicies', () => { + const opPolicy = [{ + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", + "configurationsJson": { + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }, + "pdpGroup": "pdpGroupTest", + "pdpSubgroup": "pdpSubgroupTest", + "jsonRepresentation": { + "schema": {} + } + }]; + expect(loopCache.getOperationalPolicies()).toStrictEqual(opPolicy); + }); + + it('getOperationalPoliciesNoJsonSchema', () => { + const opPolicy = [{ + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", + "configurationsJson": { + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }, + "pdpGroup": "pdpGroupTest", + "pdpSubgroup": "pdpSubgroupTest", + }]; + expect(loopCache.getOperationalPoliciesNoJsonSchema()).toStrictEqual(opPolicy); + }); + + it('getOperationalPolicyJsonSchema', () => { + const jsonSchema = { + "schema": {} + }; + + expect(loopCache.getOperationalPolicyJsonSchema()).toStrictEqual(jsonSchema); + }); + it('getGlobalProperties', () => { + const globelProp = { + "dcaeDeployParameters": { + "location_id": "", + "service_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + }; + expect(loopCache.getGlobalProperties()).toStrictEqual(globelProp); + }); + + it('getDcaeDeploymentProperties', () => { + const deploymentProp = { + "location_id": "", + "service_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + }; + expect(loopCache.getDcaeDeploymentProperties()).toStrictEqual(deploymentProp); + }); + + it('getMicroServiceForName', () => { + const msJson = { + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "configurationsJson": {"domain": "measurementsForVfScaling"}, + "shared": false, + "pdpGroup": "pdpGroupTest", + "pdpSubgroup": "pdpSubgroupTest", + "policyModel": {"policyPdpGroup": {"supportedPdpGroups": "supportedPdpGroupsTest"}}, + "jsonRepresentation": {"schema": {}} + }; + expect(loopCache.getMicroServiceForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msJson); + expect(loopCache.getMicroServiceForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca_2")).toBeNull(); + }); + + it('getMicroServicePropertiesForName', () => { + const msProp = {"domain": "measurementsForVfScaling"}; + expect(loopCache.getMicroServicePropertiesForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msProp); + expect(loopCache.getMicroServicePropertiesForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca_2")).toBeNull(); + }); + + it('getMicroServiceJsonRepresentationForName', () => { + const msJsonRepresentation = {"schema": {}}; + expect(loopCache.getMicroServiceJsonRepresentationForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msJsonRepresentation); + }); + + it('getResourceDetailsVfProperty', () => { + const resourceVF = { + "vLoadBalancerMS 0": { + "resourceVendor": "Test", + "resourceVendorModelNumber": "", + "name": "vLoadBalancerMS", + "description": "vLBMS", + "invariantUUID": "1a31b9f2-e50d-43b7-89b3-a040250cf506", + "subcategory": "Load Balancer", + "category": "Application L4+", + "type": "VF", + "UUID": "b4c4f3d7-929e-4b6d-a1cd-57e952ddc3e6", + "version": "1.0", + "resourceVendorRelease": "1.0", + "customizationUUID": "465246dc-7748-45f4-a013-308d92922552" + } + }; + expect(loopCache.getResourceDetailsVfProperty()).toStrictEqual(resourceVF); + }); + + it('getResourceDetailsVfModuleProperty', () => { + const vfModule = { + "Vloadbalancerms..vpkg..module-1": { + "vfModuleModelInvariantUUID": "ca052563-eb92-4b5b-ad41-9111768ce043", + "vfModuleModelVersion": "1", + "vfModuleModelName": "Vloadbalancerms..vpkg..module-1", + "vfModuleModelUUID": "1e725ccc-b823-4f67-82b9-4f4367070dbc", + "vfModuleModelCustomizationUUID": "1bffdc31-a37d-4dee-b65c-dde623a76e52", + "min_vf_module_instances": 0, + "vf_module_label": "vpkg", + "max_vf_module_instances": 1, + "vf_module_type": "Expansion", + "isBase": false, + "initial_count": 0, + "volume_group": false + } + }; + expect(loopCache.getResourceDetailsVfModuleProperty()).toStrictEqual(vfModule); + }); + + it('getLoopLogsArray', () => { + const logs = [ + { + "id": 1, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational policies UPDATED", + "logInstant": "2019-07-08T09:44:37Z" + } + ]; + expect(loopCache.getLoopLogsArray()).toStrictEqual(logs); + }); + + it('getComponentStates', () => { + const component = { + "POLICY": { + "componentState": { + "stateName": "NOT_SENT", + "description": "The policies defined have NOT yet been created on the policy engine" + } + }, + "DCAE": { + "componentState": { + "stateName": "BLUEPRINT_DEPLOYED", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop" + } + } + }; + expect(loopCache.getComponentStates()).toStrictEqual(component); + }); + + it('getOperationalPolicyForName', () => { + const opPolicy = { + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", + "configurationsJson": { + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }, + "pdpGroup": "pdpGroupTest", + "pdpSubgroup": "pdpSubgroupTest", + "jsonRepresentation": { + "schema": {} + } + }; + expect(loopCache.getOperationalPolicyForName("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(opPolicy); + expect(loopCache.getOperationalPolicyForName("Not_Exist")).toBeNull(); + }); + + it('getOperationalPolicyPropertiesForName', () => { + const opPolicyJson = { + "operational_policy": { + "controlLoop": {}, + "policies": [] + }}; + expect(loopCache.getOperationalPolicyPropertiesForName("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(opPolicyJson); + expect(loopCache.getOperationalPolicyPropertiesForName("Not_Exist")).toBeNull(); + }); + + it('getOperationalPolicyJsonRepresentationForName', () => { + const opPolicySchema = { + "schema": {} + }; + expect(loopCache.getOperationalPolicyJsonRepresentationForName("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(opPolicySchema); + expect(loopCache.getOperationalPolicyJsonRepresentationForName("Not_Exist")).toBeNull(); + }); + + it('getOperationalPolicySupportedPdpGroup', () => { + expect(loopCache.getOperationalPolicySupportedPdpGroup("Not_Exist")).toStrictEqual([]); + }); + + it('getOperationalPolicyPdpGroup', () => { + expect(loopCache.getOperationalPolicyPdpGroup("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("pdpGroupTest"); + expect(loopCache.getOperationalPolicyPdpGroup("Not_Exist")).toBeNull(); + }); + + it('getOperationalPolicyPdpSubgroup', () => { + expect(loopCache.getOperationalPolicyPdpSubgroup("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("pdpSubgroupTest"); + expect(loopCache.getOperationalPolicyPdpSubgroup("Not_Exist")).toBeNull(); + }); + + it('getMicroServiceSupportedPdpGroup', () => { + expect(loopCache.getMicroServiceSupportedPdpGroup("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("supportedPdpGroupsTest"); + expect(loopCache.getMicroServiceSupportedPdpGroup("Not_Exist")).toStrictEqual([]); + }); + + it('getMicroServicePdpGroup', () => { + expect(loopCache.getMicroServicePdpGroup("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("pdpGroupTest"); + expect(loopCache.getMicroServicePdpGroup("Not_Exist")).toBeNull(); + }); + + it('getMicroServicePdpSubgroup', () => { + expect(loopCache.getMicroServicePdpSubgroup("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("pdpSubgroupTest"); + expect(loopCache.getMicroServicePdpSubgroup("Not_Exist")).toBeNull(); + }); + + it('getMicroServiceJsonRepresentationForName', () => { + const msPolicySchema = { + "schema": {} + }; + expect(loopCache.getMicroServiceJsonRepresentationForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msPolicySchema); + expect(loopCache.getMicroServiceJsonRepresentationForName("Not_Exist")).toBeNull(); + }); + + it('getTemplateName', () => { + expect(loopCache.getTemplateName()).toStrictEqual("loopTemplateTest"); + }); + + it('updateGlobalProperties', () => { + const newGlobalProps = { + "dcaeDeployParameters": { + "location_id": "newLocation", + "service_id": "newServiceId", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca_2" + } + }; + loopCache.updateGlobalProperties(newGlobalProps); + expect(loopCache.getGlobalProperties()).toStrictEqual(newGlobalProps); + }); + + it('updateOperationalPolicyProperties', () => { + const newOpPolicy = { + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }; + loopCache.updateOperationalPolicyProperties("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca",newOpPolicy); + expect(loopCache.getOperationalPolicyPropertiesForName("OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(newOpPolicy); + }); + + it('updateMicroServiceProperties', () => { + const newMsPolicyProperties = {"domain": "measurementsForVfScalingNew"}; + loopCache.updateMicroServiceProperties("TCA_h2NMX_v1_0_ResourceInstanceName1_tca", newMsPolicyProperties); + expect(loopCache.getMicroServicePropertiesForName("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(newMsPolicyProperties); + }); + + it('updateMicroServicePdpGroup', () => { + const newMsPolicyProperties = {"domain": "measurementsForVfScalingNew"}; + loopCache.updateMicroServicePdpGroup("TCA_h2NMX_v1_0_ResourceInstanceName1_tca", "pdpGroupTest1", "pdpSubgroupTest1"); + expect(loopCache.getMicroServicePdpGroup("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("pdpGroupTest1"); + expect(loopCache.getMicroServicePdpGroup("Not_Exist")).toBeNull(); + expect(loopCache.getMicroServicePdpSubgroup("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual("pdpSubgroupTest1"); + expect(loopCache.getMicroServicePdpSubgroup("Not_Exist")).toBeNull(); + }); + }); diff --git a/gui-clamp/ui-react/src/api/LoopCacheMockFile.json b/gui-clamp/ui-react/src/api/LoopCacheMockFile.json new file mode 100644 index 0000000..e5a6702 --- /dev/null +++ b/gui-clamp/ui-react/src/api/LoopCacheMockFile.json @@ -0,0 +1,135 @@ +{ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "dcaeBlueprintId": "typeId-3a942643-a8f7-4e54-b2c1-eea8daba2b17", + "globalPropertiesJson": { + "dcaeDeployParameters": { + "location_id": "", + "service_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + }, + "loopTemplate": { + "name": "loopTemplateTest" + }, + "modelService": { + "serviceDetails": { + "serviceType": "", + "namingPolicy": "", + "environmentContext": "General_Revenue-Bearing", + "serviceEcompNaming": "true", + "serviceRole": "", + "name": "vLoadBalancerMS", + "description": "vLBMS", + "invariantUUID": "30ec5b59-4799-48d8-ac5f-1058a6b0e48f", + "ecompGeneratedNaming": "true", + "category": "Network L4+", + "type": "Service", + "UUID": "63cac700-ab9a-4115-a74f-7eac85e3fce0", + "instantiationType": "A-la-carte" + }, + "resourceDetails": { + "CP": {}, + "VL": {}, + "VF": { + "vLoadBalancerMS 0": { + "resourceVendor": "Test", + "resourceVendorModelNumber": "", + "name": "vLoadBalancerMS", + "description": "vLBMS", + "invariantUUID": "1a31b9f2-e50d-43b7-89b3-a040250cf506", + "subcategory": "Load Balancer", + "category": "Application L4+", + "type": "VF", + "UUID": "b4c4f3d7-929e-4b6d-a1cd-57e952ddc3e6", + "version": "1.0", + "resourceVendorRelease": "1.0", + "customizationUUID": "465246dc-7748-45f4-a013-308d92922552" + } + }, + "CR": {}, + "VFC": {}, + "PNF": {}, + "Service": {}, + "CVFC": {}, + "Service Proxy": {}, + "Configuration": {}, + "AllottedResource": {}, + "VFModule": { + "Vloadbalancerms..vpkg..module-1": { + "vfModuleModelInvariantUUID": "ca052563-eb92-4b5b-ad41-9111768ce043", + "vfModuleModelVersion": "1", + "vfModuleModelName": "Vloadbalancerms..vpkg..module-1", + "vfModuleModelUUID": "1e725ccc-b823-4f67-82b9-4f4367070dbc", + "vfModuleModelCustomizationUUID": "1bffdc31-a37d-4dee-b65c-dde623a76e52", + "min_vf_module_instances": 0, + "vf_module_label": "vpkg", + "max_vf_module_instances": 1, + "vf_module_type": "Expansion", + "isBase": false, + "initial_count": 0, + "volume_group": false + } + } + } + }, + "lastComputedState": "DESIGN", + "components": { + "POLICY": { + "componentState": { + "stateName": "NOT_SENT", + "description": "The policies defined have NOT yet been created on the policy engine" + } + }, + "DCAE": { + "componentState": { + "stateName": "BLUEPRINT_DEPLOYED", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop" + } + } + }, + "operationalPolicies": [ + { + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", + "configurationsJson": { + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }, + "pdpGroup": "pdpGroupTest", + "pdpSubgroup": "pdpSubgroupTest", + "jsonRepresentation": { + "schema": {} + } + } + ], + "microServicePolicies": [ + { + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "configurationsJson": { + "domain": "measurementsForVfScaling" + }, + "shared": false, + "pdpGroup": "pdpGroupTest", + "pdpSubgroup": "pdpSubgroupTest", + "policyModel": { + "policyPdpGroup": { + "supportedPdpGroups": "supportedPdpGroupsTest" + } + }, + "jsonRepresentation": { + "schema": {} + } + } + ], + "loopLogs": [ + { + "id": 1, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational policies UPDATED", + "logInstant": "2019-07-08T09:44:37Z" + } + ] +} diff --git a/gui-clamp/ui-react/src/api/LoopService.js b/gui-clamp/ui-react/src/api/LoopService.js new file mode 100644 index 0000000..16f1bec --- /dev/null +++ b/gui-clamp/ui-react/src/api/LoopService.js @@ -0,0 +1,244 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class LoopService { + static getLoopNames() { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/getAllNames', { method: 'GET', credentials: 'same-origin' }) + .then(function (response) { + console.debug("GetLoopNames response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("GetLoopNames query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("GetLoopNames error received", error); + return {}; + }); + } + + static createLoop(loopName, templateName) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/create/' + loopName + '?templateName=' + templateName, { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("CreateLoop response received: ", response.status); + return response.json(); + }) + .catch(function (error) { + console.error("CreateLoop error received", error); + return ""; + }); + } + + static getLoop(loopName) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/' + loopName, { + method: 'GET', + headers: { + "Content-Type": "application/json" + }, + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("GetLoop response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("GetLoop query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("GetLoop error received", error); + return {}; + }); + } + + static setMicroServiceProperties(loopName, jsonData) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/updateMicroservicePolicy/' + loopName, { + method: 'POST', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("updateMicroservicePolicy response received: ", response.status); + if (response.ok) { + return response.text(); + } else { + console.error("updateMicroservicePolicy query failed"); + return ""; + } + }) + .catch(function (error) { + console.error("updateMicroservicePolicy error received", error); + return ""; + }); + } + + static setOperationalPolicyProperties(loopName, jsonData) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/updateOperationalPolicies/' + loopName, { + method: 'POST', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("updateOperationalPolicies response received: ", response.status); + if (response.ok) { + return response.text(); + } else { + console.error("updateOperationalPolicies query failed"); + return ""; + } + }) + .catch(function (error) { + console.error("updateOperationalPolicies error received", error); + return ""; + }); + } + + static updateGlobalProperties(loopName, jsonData) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/updateGlobalProperties/' + loopName, { + method: 'POST', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("updateGlobalProperties response received: ", response.status); + if (response.ok) { + return response.text(); + } else { + console.error("updateGlobalProperties query failed"); + return ""; + } + }) + .catch(function (error) { + console.error("updateGlobalProperties error received", error); + return ""; + }); + } + + static refreshOperationalPolicyJson(loopName, operationalPolicyName) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/refreshOperationalPolicyJsonSchema/' + loopName + '/' + operationalPolicyName, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("Refresh Operational Policy Json Schema response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("Refresh Operational Policy Json Schema query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("Refresh Operational Policy Json Schema error received", error); + return {}; + }); + } + + static refreshMicroServicePolicyJson(loopName, microServicePolicyName) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/refreshMicroServicePolicyJsonSchema/' + loopName + '/' + microServicePolicyName, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("Refresh Operational Policy Json Schema response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("Refresh Operational Policy Json Schema query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("Refresh Operational Policy Json Schema error received", error); + return {}; + }); + } + + static addOperationalPolicyType(loopName, policyType, policyVersion) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/addOperationaPolicy/' + loopName + '/policyModel/' + policyType + '/' + policyVersion, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("Add Operational Policy response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + return response.text(); + } + }) + .catch(function (error) { + console.error("Add Operational Policy query failed"); + throw new Error(error); + }) + } + + static removeOperationalPolicyType(loopName, policyType, policyVersion, policyName) { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/removeOperationaPolicy/' + loopName + '/policyModel/' + policyType + '/' + policyVersion + '/' + policyName, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("Remove Operational Policy response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("Remove Operational Policy query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("Remove Operational Policy error received", error); + return {}; + }); + } +} diff --git a/gui-clamp/ui-react/src/api/PoliciesListCache.js b/gui-clamp/ui-react/src/api/PoliciesListCache.js new file mode 100644 index 0000000..265dab4 --- /dev/null +++ b/gui-clamp/ui-react/src/api/PoliciesListCache.js @@ -0,0 +1,34 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class PoliciesListCache { + policiesJsonCache; + + constructor(policiesJson) { + this.policiesJsonCache = policiesJson; + } + + getAllPolicies() { + return this.policiesJsonCache["policies"]; + } +} diff --git a/gui-clamp/ui-react/src/api/PoliciesListCacheMockFile.json b/gui-clamp/ui-react/src/api/PoliciesListCacheMockFile.json new file mode 100644 index 0000000..14d5cc6 --- /dev/null +++ b/gui-clamp/ui-react/src/api/PoliciesListCacheMockFile.json @@ -0,0 +1,215 @@ +{ + "policies": [ + { + "MICROSERVICE_vLoadBalancerMS_v1_0_tcagen2_1_0_0_AV0": { + "type": "onap.policies.monitoring.tcagen2", + "type_version": "1.0.0", + "properties": { + "tca.policy": { + "domain": "measurementsForVfScaling", + "metricsPerEventName": [ + { + "policyScope": "DCAE", + "thresholds": [ + { + "version": "1.0.2", + "severity": "MAJOR", + "thresholdValue": 200, + "closedLoopEventStatus": "ONSET", + "closedLoopControlName": "LOOP_test", + "direction": "LESS_OR_EQUAL", + "fieldPath": "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta" + } + ], + "eventName": "vLoadBalancer", + "policyVersion": "v0.0.1", + "controlLoopSchemaType": "VM", + "policyName": "DCAE.Config_tca-hi-lo" + } + ] + } + }, + "name": "MICROSERVICE_vLoadBalancerMS_v1_0_tcagen2_1_0_0_AV0", + "version": "1.0.0", + "metadata": { + "policy-id": "MICROSERVICE_vLoadBalancerMS_v1_0_tcagen2_1_0_0_AV0", + "policy-version": "1.0.0" + }, + "pdpGroupInfo": { + "pdpGroup": "pdpGroup2", + "pdpSubGroup": "subGroup2" + }, + "supportedPdpGroups": [ + { + "pdpGroup2": [ + "subGroup2", + "subGroup3" + ] + } + ] + } + }, + { + "OPERATIONAL_vLoadBalancerMS_v1_0_Drools_1_0_0_7xd": { + "type": "onap.policies.controlloop.operational.common.Drools", + "type_version": "1.0.0", + "properties": { + "abatement": false, + "operations": [ + { + "failure_retries": "final_failure_retries", + "id": "test1", + "failure_timeout": "final_failure_timeout", + "failure": "final_failure", + "operation": { + "payload": { + "artifact_name": "baseconfiguration", + "artifact_version": "1.0.0", + "mode": "async", + "data": "{\"resource-assignment-properties\":{\"request-id\":\"\",\"service-instance-id\":\"\",\"hostname\":\"\",\"request-info\":{\"prop1\":\"\",\"prop2\":\"\"}}}" + }, + "target": { + "entityIds": { + "resourceID": "Vloadbalancerms..vdns..module-3", + "modelInvariantId": "4c10ba9b-f88f-415e-9de3-5d33336047fa", + "modelVersionId": "4fa73b49-8a6c-493e-816b-eb401567b720", + "modelName": "Vloadbalancerms..vdns..module-3", + "modelVersion": "1", + "modelCustomizationId": "bafcdab0-801d-4d81-9ead-f464640a38b1" + }, + "targetType": "VNF" + }, + "actor": "SDNR", + "operation": "BandwidthOnDemand" + }, + "failure_guard": "final_failure_guard", + "retries": 0, + "timeout": 0, + "failure_exception": "final_failure_exception", + "description": "test", + "success": "final_success" + } + ], + "trigger": "test1", + "timeout": 0, + "id": "LOOP_test" + }, + "name": "OPERATIONAL_vLoadBalancerMS_v1_0_Drools_1_0_0_7xd", + "version": "1.0.0", + "metadata": { + "policy-id": "OPERATIONAL_vLoadBalancerMS_v1_0_Drools_1_0_0_7xd", + "policy-version": "1.0.0" + }, + "pdpGroupInfo": { + "pdpGroup": "pdpGroup2", + "pdpSubGroup": "subGroup3" + }, + "supportedPdpGroups": [ + { + "pdpGroup2": [ + "subGroup2", + "subGroup3" + ] + } + ] + } + }, + { + "SDNC_Policy.ONAP_NF_NAMING_TIMESTAMP": { + "type": "onap.policies.Naming", + "type_version": "1.0.0", + "properties": { + "naming-models": [ + { + "naming-type": "VNF", + "naming-recipe": "AIC_CLOUD_REGION|DELIMITER|CONSTANT|DELIMITER|TIMESTAMP", + "name-operation": "to_lower_case()", + "naming-properties": [ + { + "property-name": "AIC_CLOUD_REGION" + }, + { + "property-name": "CONSTANT", + "property-value": "onap-nf" + }, + { + "property-name": "TIMESTAMP" + }, + { + "property-value": "-", + "property-name": "DELIMITER" + } + ] + }, + { + "naming-type": "VNFC", + "naming-recipe": "VNF_NAME|DELIMITER|NFC_NAMING_CODE|DELIMITER|SEQUENCE", + "name-operation": "to_lower_case()", + "naming-properties": [ + { + "property-name": "VNF_NAME" + }, + { + "property-name": "SEQUENCE", + "increment-sequence": { + "max": "zzz", + "scope": "ENTIRETY", + "start-value": "1", + "length": "3", + "increment": "1", + "sequence-type": "alpha-numeric" + } + }, + { + "property-name": "NFC_NAMING_CODE" + }, + { + "property-value": "-", + "property-name": "DELIMITER" + } + ] + }, + { + "naming-type": "VF-MODULE", + "naming-recipe": "VNF_NAME|DELIMITER|VF_MODULE_LABEL|DELIMITER|VF_MODULE_TYPE|DELIMITER|SEQUENCE", + "name-operation": "to_lower_case()", + "naming-properties": [ + { + "property-name": "VNF_NAME" + }, + { + "property-value": "-", + "property-name": "DELIMITER" + }, + { + "property-name": "VF_MODULE_LABEL" + }, + { + "property-name": "VF_MODULE_TYPE" + }, + { + "property-name": "SEQUENCE", + "increment-sequence": { + "max": "zzz", + "scope": "PRECEEDING", + "start-value": "1", + "length": "3", + "increment": "1", + "sequence-type": "alpha-numeric" + } + } + ] + } + ], + "policy-instance-name": "ONAP_NF_NAMING_TIMESTAMP" + }, + "name": "SDNC_Policy.ONAP_NF_NAMING_TIMESTAMP", + "version": "1.0.0", + "metadata": { + "policy-id": "SDNC_Policy.ONAP_NF_NAMING_TIMESTAMP", + "policy-version": "1.0.0" + } + } + } + ] +} diff --git a/gui-clamp/ui-react/src/api/PolicyService.js b/gui-clamp/ui-react/src/api/PolicyService.js new file mode 100644 index 0000000..046f789 --- /dev/null +++ b/gui-clamp/ui-react/src/api/PolicyService.js @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class PolicyService { + static getPoliciesList() { + return fetch(window.location.pathname + 'restservices/clds/v2/policies', { + method: 'GET', + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("getPoliciesList response received: ", response.status); + if (response.ok) { + console.info("getPoliciesList query successful"); + return response.json(); + } else { + return response.text().then(responseBody => { + throw new Error("HTTP " + response.status + "," + responseBody); + }) + } + }) + .catch(function (error) { + console.error("getPoliciesList error occurred ", error); + alert("getPoliciesList error occurred " + error); + return undefined; + }) + } + + static createNewPolicy(policyModelType, policyModelVersion, policyName, policyVersion, policyJson) { + return fetch(window.location.pathname + 'restservices/clds/v2/policies/' + policyModelType + '/' + + policyModelVersion + '/' + policyName + '/' + policyVersion, { + method: 'POST', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(policyJson) + }) + .then(function (response) { + console.debug("createNewPolicy response received: ", response.status); + if (response.ok) { + console.info("createNewPolicy query successful"); + return response.text(); + } else { + return response.text().then(responseBody => { + throw new Error("HTTP " + response.status + "," + responseBody); + }) + } + }) + .catch(function (error) { + console.error("createNewPolicy error occurred ", error); + alert("createNewPolicy error occurred " + error); + return undefined; + }); + } + + static deletePolicy(policyModelType, policyModelVersion, policyName, policyVersion) { + return fetch(window.location.pathname + 'restservices/clds/v2/policies/' + policyModelType + '/' + + policyModelVersion + '/' + policyName + '/' + policyVersion, { + method: 'DELETE', + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("deletePolicy response received: ", response.status); + if (response.ok) { + console.info("deletePolicy query successful"); + return response.text(); + } else { + return response.text().then(responseBody => { + throw new Error("HTTP " + response.status + "," + responseBody); + }) + } + }) + .catch(function (error) { + console.error("deletePolicy error occurred ", error); + alert("deletePolicy error occurred " + error); + return undefined; + }); + } +} diff --git a/gui-clamp/ui-react/src/api/PolicyToscaService.js b/gui-clamp/ui-react/src/api/PolicyToscaService.js new file mode 100644 index 0000000..b5282f9 --- /dev/null +++ b/gui-clamp/ui-react/src/api/PolicyToscaService.js @@ -0,0 +1,136 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class PolicyToscaService { + static getToscaPolicyModels() { + return fetch(window.location.pathname + 'restservices/clds/v2/policyToscaModels', { method: 'GET', credentials: 'same-origin' }) + .then(function (response) { + console.debug("getToscaPolicyModels response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getToscaPolicyModels query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("getToscaPolicyModels error received", error); + return {}; + }); + } + + static getToscaPolicyModelYaml(policyModelType, policyModelVersion) { + return fetch(window.location.pathname + 'restservices/clds/v2/policyToscaModels/yaml/' + policyModelType + "/" + policyModelVersion, { + method: 'GET', + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("getToscaPolicyModelYaml response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getToscaPolicyModelYaml query failed"); + return ""; + } + }) + .catch(function (error) { + console.error("getToscaPolicyModelYaml error received", error); + return ""; + }); + } + + static getToscaPolicyModel(policyModelType, policyModelVersion) { + return fetch(window.location.pathname + 'restservices/clds/v2/policyToscaModels/' + policyModelType + "/" + policyModelVersion, { + method: 'GET', + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("getToscaPolicyModel response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getToscaPolicyModel query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("getToscaPolicyModel error received", error); + return {}; + }); + } + + static createPolicyModelFromToscaModel(jsonData) { + return fetch(window.location.pathname + 'restservices/clds/v2/policyToscaModels', { + method: 'POST', + credentials: 'same-origin', + headers: { + "Content-Type": "a", + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("createPolicyModelFromToscaModel response received: ", response.status); + if (response.ok) { + var message = { + status: response.status, + message: 'Tosca Policy Model successfully uploaded' + }; + return message; + } else { + console.error("createPolicyModelFromToscaModel failed"); + return response.text(); + } + }) + .catch(function (error) { + console.error("createPolicyModelFromToscaModel error received", error); + return ""; + }); + } + + static updatePolicyModelTosca(policyModelType, policyModelVersion, jsonData) { + return fetch(window.location.pathname + 'restservices/clds/v2/policyToscaModels/' + policyModelType + '/' + policyModelVersion, { + method: 'PUT', + credentials: 'same-origin', + headers: { + "Content-Type": "a", + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("updatePolicyModelTosca response received: ", response.status); + if (response.ok) { + var message = { + status: response.status, + message: 'Tosca Policy Model successfully uploaded' + }; + return message; + } else { + console.error("updatePolicyModelTosca failed"); + return response.text(); + } + }) + .catch(function (error) { + console.error("updatePolicyModelTosca error received", error); + return ""; + }); + } +} diff --git a/gui-clamp/ui-react/src/api/TemplateService.js b/gui-clamp/ui-react/src/api/TemplateService.js new file mode 100644 index 0000000..a6e386e --- /dev/null +++ b/gui-clamp/ui-react/src/api/TemplateService.js @@ -0,0 +1,197 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class TemplateService { + + static getLoopNames() { + return fetch(window.location.pathname + 'restservices/clds/v2/loop/getAllNames', { method: 'GET', credentials: 'same-origin' }) + .then(function (response) { + console.debug("getLoopNames response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getLoopNames query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("getLoopNames error received", error); + return {}; + }); + } + + static getAllLoopTemplates() { + return fetch(window.location.pathname + 'restservices/clds/v2/templates', { method: 'GET', credentials: 'same-origin', }) + .then(function (response) { + console.debug("getAllLoopTemplates response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getAllLoopTemplates query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("getAllLoopTemplates error received", error); + return {}; + }); + } + + static getDictionary() { + return fetch(window.location.pathname + 'restservices/clds/v2/dictionary/', { method: 'GET', credentials: 'same-origin', }) + .then(function (response) { + console.debug("getDictionary response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getDictionary query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("getDictionary error received", error); + return {}; + }); + } + + static getDictionaryElements(dictionaryName) { + return fetch(window.location.pathname + 'restservices/clds/v2/dictionary/' + dictionaryName, { + method: 'GET', + headers: { + "Content-Type": "application/json", + }, + credentials: 'same-origin', + }) + .then(function (response) { + console.debug("getDictionaryElements response received: ", response.status); + if (response.ok) { + return response.json(); + } else { + console.error("getDictionaryElements query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("getDictionaryElements error received", error); + return {}; + }); + } + + static insDictionary(jsonData) { + console.log("dictionaryName is", jsonData.name) + return fetch(window.location.pathname + 'restservices/clds/v2/dictionary/', { + method: 'PUT', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("insDictionary response received: ", response.status); + if (response.ok) { + return response.status; + } else { + var errorMessage = response.status; + console.error("insDictionary query failed", response.status); + return errorMessage; + } + }) + .catch(function (error) { + console.error("insDictionary error received", error); + return ""; + }); + } + + static insDictionaryElements(jsonData) { + console.log("dictionaryName is", jsonData.name) + return fetch(window.location.pathname + 'restservices/clds/v2/dictionary/' + jsonData.name, { + method: 'PUT', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonData) + }) + .then(function (response) { + console.debug("insDictionary response received: ", response.status); + if (response.ok) { + return response.status; + } else { + var errorMessage = response.status; + console.error("insDictionary query failed", response.status); + return errorMessage; + } + }) + .catch(function (error) { + console.error("insDictionary error received", error); + return ""; + }); + } + + static deleteDictionary(dictionaryName) { + console.log("inside templaemenu service", dictionaryName) + return fetch(window.location.pathname + 'restservices/clds/v2/dictionary/' + dictionaryName, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + }, + credentials: 'same-origin', + }) + .then(function (response) { + console.debug("deleteDictionary response received: ", response.status); + if (response.ok) { + return response.status; + } else { + console.error("deleteDictionary query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("deleteDictionary error received", error); + return {}; + }); + } + + static deleteDictionaryElements(dictionaryData) { + return fetch(window.location.pathname + 'restservices/clds/v2/dictionary/' + dictionaryData.name + '/elements/' + dictionaryData.shortName, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + }, + credentials: 'same-origin', + }) + .then(function (response) { + console.debug("deleteDictionary response received: ", response.status); + if (response.ok) { + return response.status; + } else { + console.error("deleteDictionary query failed"); + return {}; + } + }) + .catch(function (error) { + console.error("deleteDictionary error received", error); + return {}; + }); + } +} diff --git a/gui-clamp/ui-react/src/api/UserService.js b/gui-clamp/ui-react/src/api/UserService.js new file mode 100644 index 0000000..bcf46ea --- /dev/null +++ b/gui-clamp/ui-react/src/api/UserService.js @@ -0,0 +1,75 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class UserService { + static notLoggedUserName = 'Anonymous'; + + static login() { + return fetch(window.location.pathname + 'restservices/clds/v1/user/getUser', { + method: 'GET', + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("getUser response received, status code:", response.status); + if (response.ok) { + return response.text(); + } else { + console.error("getUser response is nok"); + return UserService.notLoggedUserName; + } + }) + .then(function (data) { + console.info("User connected:", data) + return data; + }) + .catch(function (error) { + console.warn("getUser error received, user set to: ", UserService.notLoggedUserName); + console.error("getUser error:", error); + return UserService.notLoggedUserName; + }); + } + + static getUserInfo() { + return fetch(window.location.pathname + 'restservices/clds/v2/clampInformation', { + method: 'GET', + credentials: 'same-origin' + }) + .then(function (response) { + console.debug("getUserInfo response received, status code:", response.status); + if (response.ok) { + return response.json(); + } else { + return {} + } + }) + .then(function (data) { + console.info("User info received:", data) + return data; + }) + .catch(function (error) { + console.warn("getUserInfo error received, user set to: ", UserService.notLoggedUserName); + console.error("getUserInfo error:", error); + return {}; + }); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/CreateLoopModal.js b/gui-clamp/ui-react/src/components/dialogs/Loop/CreateLoopModal.js new file mode 100644 index 0000000..690dcbb --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/CreateLoopModal.js @@ -0,0 +1,193 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import Select from 'react-select'; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Form from 'react-bootstrap/Form'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import styled from 'styled-components'; +import LoopService from '../../../api/LoopService'; +import TemplateService from '../../../api/TemplateService'; +import LoopCache from '../../../api/LoopCache'; +import SvgGenerator from '../../loop_viewer/svg/SvgGenerator'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` + +const ErrMsgStyled = styled.div` + color: red; +` + +export default class CreateLoopModal extends React.Component { + constructor(props, context) { + super(props, context); + + this.getAllLoopTemplates = this.getAllLoopTemplates.bind(this); + this.handleCreate = this.handleCreate.bind(this); + this.handleModelName = this.handleModelName.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleDropDownListChange = this.handleDropDownListChange.bind(this); + this.renderSvg = this.renderSvg.bind(this); + this.state = { + show: true, + chosenTemplateName: '', + modelInputErrMsg: '', + modelName: '', + templateNames: [], + fakeLoopCacheWithTemplate: new LoopCache({}) + }; + } + + async componentDidMount() { + await this.getAllLoopTemplates(); + await this.getModelNames(); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + handleDropDownListChange(e) { + if (typeof e.value !== "undefined") { + this.setState({ + fakeLoopCacheWithTemplate: + new LoopCache({ + "loopTemplate": e.templateObject, + "name": "fakeLoop" + }), + chosenTemplateName: e.value + }) + } else { + this.setState({ fakeLoopCacheWithTemplate: new LoopCache({}) }) + } + } + + getAllLoopTemplates() { + TemplateService.getAllLoopTemplates().then(templatesData => { + const templateOptions = templatesData.map((templateData) => { + return { label: templateData.name, value: templateData.name, templateObject: templateData } + }); + this.setState({ + templateNames: templateOptions + }) + }); + } + + getModelNames() { + TemplateService.getLoopNames().then(loopNames => { + if (!loopNames) { + loopNames = []; + } + // Remove LOOP_ prefix + let trimmedLoopNames = loopNames.map(str => str.replace('LOOP_', '')); + this.setState({ modelNames: trimmedLoopNames }); + }); + } + + handleCreate() { + if (!this.state.modelName) { + alert("A model name is required"); + return; + } + console.debug("Create Model " + this.state.modelName + ", Template " + this.state.chosenTemplateName + " is chosen"); + this.setState({ show: false }); + LoopService.createLoop("LOOP_" + this.state.modelName, this.state.chosenTemplateName).then(text => { + console.debug("CreateLoop response received: ", text); + try { + this.props.history.push('/'); + this.props.loadLoopFunction("LOOP_" + this.state.modelName); + } catch (err) { + alert(text); + this.props.history.push('/'); + } + }) + .catch(error => { + console.debug("Create Loop failed"); + }); + } + + handleModelName(event) { + if (this.state.modelNames.includes(event.target.value)) { + this.setState({ + modelInputErrMsg: 'A model named "' + event.target.value + '" already exists. Please pick another name.', + modelName: event.target.value + }); + return; + } else { + this.setState({ + modelInputErrMsg: '', + modelName: event.target.value + }); + } + } + + renderSvg() { + return ( + <SvgGenerator loopCache={ this.state.fakeLoopCacheWithTemplate } clickable={ false } generatedFrom={ SvgGenerator.GENERATED_FROM_TEMPLATE }/> + ); + } + + render() { + return ( + <ModalStyled size="xl" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + <Modal.Title>Create Model</Modal.Title> + </Modal.Header> + <Modal.Body> + <Form.Group as={ Row } controlId="formPlaintextEmail"> + <Form.Label column sm="2">Template Name:</Form.Label> + <Col sm="10"> + <Select onChange={ this.handleDropDownListChange } options={ this.state.templateNames }/> + </Col> + </Form.Group> + <Form.Group as={ Row } style={ { alignItems: 'center' } } controlId="formSvgPreview"> + <Form.Label column sm="2">Model Preview:</Form.Label> + <Col sm="10"> + { this.renderSvg() } + </Col> + </Form.Group> + <Form.Group as={ Row } controlId="formPlaintextEmail"> + <Form.Label column sm="2">Model Name:</Form.Label> + <input sm="5" type="text" style={ { width: '50%', marginLeft: '1em' } } + value={ this.state.modelName } + onChange={ this.handleModelName } + /> + <span sm="5"/> + </Form.Group> + <Form.Group as={ Row } controlId="formPlaintextEmail"> + <Form.Label column sm="2"> </Form.Label> + <ErrMsgStyled>{ this.state.modelInputErrMsg }</ErrMsgStyled> + </Form.Group> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Cancel</Button> + <Button variant="primary" type="submit" onClick={ this.handleCreate }>Create</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/CreateLoopModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Loop/CreateLoopModal.test.js new file mode 100644 index 0000000..401bb6a --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/CreateLoopModal.test.js @@ -0,0 +1,144 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateLoopModal from './CreateLoopModal'; +import LoopService from '../../../api/LoopService'; +import TemplateService from '../../../api/TemplateService'; + +describe('Verify CreateLoopModal', () => { + + it('Test the render method', async () => { + const flushPromises = () => new Promise(setImmediate); + TemplateService.getAllLoopTemplates = jest.fn().mockImplementation(() => { + return Promise.resolve([{ "name": "template1" }, { "name": "template2" }]); + }); + TemplateService.getLoopNames = jest.fn().mockImplementation(() => { + return Promise.resolve([]); + }); + + const component = shallow(<CreateLoopModal/>); + expect(component).toMatchSnapshot(); + await flushPromises(); + component.update(); + expect(component.state('templateNames')).toStrictEqual([{ "label": "template1", "value": "template1", "templateObject": { "name": "template1" } }, { + "label": "template2", + "value": "template2", + "templateObject": { "name": "template2" } + }]); + }); + + it('handleDropdownListChange event', async () => { + const flushPromises = () => new Promise(setImmediate); + + const component = shallow(<CreateLoopModal/>); + component.find('StateManager').simulate('change', { value: 'template1', templateObject: { "name": "template1" } }); + await flushPromises(); + component.update(); + expect(component.state('chosenTemplateName')).toEqual("template1"); + expect(component.state('fakeLoopCacheWithTemplate').getLoopTemplate()['name']).toEqual("template1"); + expect(component.state('fakeLoopCacheWithTemplate').getLoopName()).toEqual("fakeLoop"); + + component.find('StateManager').simulate('change', { value: 'template2', templateObject: { "name": "template2" } }); + await flushPromises(); + component.update(); + expect(component.state('chosenTemplateName')).toEqual("template2"); + expect(component.state('fakeLoopCacheWithTemplate').getLoopTemplate()['name']).toEqual("template2"); + expect(component.state('fakeLoopCacheWithTemplate').getLoopName()).toEqual("fakeLoop"); + }); + + it('handleModelName event', async () => { + const flushPromises = () => new Promise(setImmediate); + TemplateService.getAllLoopTemplates = jest.fn().mockImplementation(() => { + return Promise.resolve([{ "name": "template1" }, { "name": "template2" }]); + }); + TemplateService.getLoopNames = jest.fn().mockImplementation(() => { + return Promise.resolve([]); + }); + const event = { target: { value: "model1" } }; + const component = shallow(<CreateLoopModal/>); + await flushPromises(); + component.find('input').simulate('change', event); + component.update(); + expect(component.state('modelName')).toEqual("model1"); + }); + + it('Test handleClose', () => { + const historyMock = { push: jest.fn() }; + const handleClose = jest.spyOn(CreateLoopModal.prototype, 'handleClose'); + const component = shallow(<CreateLoopModal history={ historyMock }/>) + + component.find('[variant="secondary"]').prop('onClick')(); + + expect(handleClose).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + + handleClose.mockClear(); + }); + + it('Test handleCreate Fail', () => { + const handleCreate = jest.spyOn(CreateLoopModal.prototype, 'handleCreate'); + const component = shallow(<CreateLoopModal/>) + + component.find('[variant="primary"]').prop('onClick')(); + + expect(handleCreate).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(true); + + handleCreate.mockClear(); + }); + + it('Test handleCreate Suc', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const loadLoopFunction = jest.fn(); + + LoopService.createLoop = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + + const handleCreate = jest.spyOn(CreateLoopModal.prototype, 'handleCreate'); + const component = shallow(<CreateLoopModal history={ historyMock } loadLoopFunction={ loadLoopFunction }/>) + component.setState({ + modelName: "modelNameTest", + chosenTemplateName: "template1" + }); + + component.find('[variant="primary"]').prop('onClick')(); + await flushPromises(); + component.update(); + + expect(handleCreate).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + + handleCreate.mockClear(); + }); + +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/DeployLoopModal.js b/gui-clamp/ui-react/src/components/dialogs/Loop/DeployLoopModal.js new file mode 100644 index 0000000..b7ab8a5 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/DeployLoopModal.js @@ -0,0 +1,183 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import LoopActionService from '../../../api/LoopActionService'; +import LoopService from '../../../api/LoopService'; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Form from 'react-bootstrap/Form'; +import Tabs from 'react-bootstrap/Tabs'; +import Tab from 'react-bootstrap/Tab'; +import styled from 'styled-components'; +import Spinner from 'react-bootstrap/Spinner' + +const StyledSpinnerDiv = styled.div` + justify-content: center !important; + display: flex !important; +`; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` +const FormStyled = styled(Form.Group)` + padding: .25rem 1.5rem; +` +export default class DeployLoopModal extends React.Component { + + constructor(props, context) { + super(props, context); + + this.handleSave = this.handleSave.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleChange = this.handleChange.bind(this); + this.refreshStatus = this.refreshStatus.bind(this); + this.renderDeployParam = this.renderDeployParam.bind(this); + this.renderSpinner = this.renderSpinner.bind(this); + + const propertiesJson = JSON.parse(JSON.stringify(this.props.loopCache.getGlobalProperties())); + this.state = { + loopCache: this.props.loopCache, + temporaryPropertiesJson: propertiesJson, + show: true, + key: this.getInitialKeyValue(propertiesJson) + }; + } + + getInitialKeyValue(temporaryPropertiesJson) { + const deployJsonList = temporaryPropertiesJson["dcaeDeployParameters"]; + let initialKey; + Object.keys(deployJsonList) + .filter((obj) => Object.keys(deployJsonList).indexOf(obj) === 0) + .map(obj => + initialKey = obj + ); + return initialKey; + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopName: newProps.loopCache.getLoopName(), + show: true + }); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + renderSpinner() { + if (this.state.deploying) { + return ( + <StyledSpinnerDiv> + <Spinner animation="border" role="status"> + <span className="sr-only">Loading...</span> + </Spinner> + </StyledSpinnerDiv> + ); + } else { + return (<div></div>); + } + } + + handleSave() { + const loopName = this.props.loopCache.getLoopName(); + // save the global propserties + this.setState({ deploying: true }); + LoopService.updateGlobalProperties(loopName, this.state.temporaryPropertiesJson).then(resp => { + LoopActionService.performAction(loopName, "deploy").then(pars => { + this.props.showSucAlert("Action deploy successfully performed"); + // refresh status and update loop logs + this.refreshStatus(loopName); + }) + .catch(error => { + this.props.showFailAlert("Action deploy failed"); + // refresh status and update loop logs + this.refreshStatus(loopName); + }); + }); + } + + refreshStatus(loopName) { + LoopActionService.refreshStatus(loopName).then(data => { + this.props.updateLoopFunction(data); + this.setState({ show: false, deploying: false }); + this.props.history.push('/'); + }) + .catch(error => { + this.props.showFailAlert("Refresh status failed"); + this.setState({ show: false, deploying: false }); + this.props.history.push('/'); + }); + } + + handleChange(event) { + let deploymentParam = this.state.temporaryPropertiesJson["dcaeDeployParameters"]; + deploymentParam[this.state.key][event.target.name] = event.target.value; + + this.setState({ temporaryPropertiesJson: { dcaeDeployParameters: deploymentParam } }); + } + + renderDeployParamTabs() { + if (typeof (this.state.temporaryPropertiesJson) === "undefined") { + return ""; + } + + const deployJsonList = this.state.temporaryPropertiesJson["dcaeDeployParameters"]; + var indents = []; + Object.keys(deployJsonList).map((item, key) => + indents.push(<Tab eventKey={ item } title={ item }> + { this.renderDeployParam(deployJsonList[item]) } + </Tab>) + ); + return indents; + } + + renderDeployParam(deployJson) { + var indents = []; + Object.keys(deployJson).map((item, key) => + indents.push(<FormStyled> + <Form.Label>{ item }</Form.Label> + <Form.Control type="text" name={ item } onChange={ this.handleChange } defaultValue={ deployJson[item] }></Form.Control> + </FormStyled>)); + return indents; + } + + render() { + return ( + <ModalStyled size="lg" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + <Modal.Title>Deployment parameters</Modal.Title> + </Modal.Header> + <Tabs id="controlled-tab-example" activeKey={ this.state.key } onSelect={ key => this.setState({ key }) }> + { this.renderDeployParamTabs() } + </Tabs> + { this.renderSpinner() } + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Cancel</Button> + <Button variant="primary" type="submit" onClick={ this.handleSave }>Deploy</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/DeployLoopModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Loop/DeployLoopModal.test.js new file mode 100644 index 0000000..0c68f23 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/DeployLoopModal.test.js @@ -0,0 +1,114 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import DeployLoopModal from './DeployLoopModal'; +import LoopCache from '../../../api/LoopCache'; +import LoopActionService from '../../../api/LoopActionService'; +import LoopService from '../../../api/LoopService'; + +describe('Verify DeployLoopModal', () => { + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "globalPropertiesJson": { + "dcaeDeployParameters": { + "testMs": { + "location_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + } + } + }); + + it('Test the render method', () => { + const component = shallow( + <DeployLoopModal loopCache={ loopCache }/> + ) + + expect(component).toMatchSnapshot(); + }); + + it('Test handleClose', () => { + const historyMock = { push: jest.fn() }; + const handleClose = jest.spyOn(DeployLoopModal.prototype, 'handleClose'); + const component = shallow(<DeployLoopModal history={ historyMock } loopCache={ loopCache }/>) + + component.find('[variant="secondary"]').prop('onClick')(); + + expect(handleClose).toHaveBeenCalledTimes(1); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test handleSave successful', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const updateLoopFunction = jest.fn(); + const showSucAlert = jest.fn(); + const showFailAlert = jest.fn(); + const handleSave = jest.spyOn(DeployLoopModal.prototype, 'handleSave'); + LoopService.updateGlobalProperties = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + text: () => "OK" + }); + }); + LoopActionService.performAction = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + LoopActionService.refreshStatus = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + + const component = shallow(<DeployLoopModal history={ historyMock } + loopCache={ loopCache } updateLoopFunction={ updateLoopFunction } showSucAlert={ showSucAlert } showFailAlert={ showFailAlert }/>) + + component.find('[variant="primary"]').prop('onClick')(); + await flushPromises(); + component.update(); + + expect(handleSave).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + handleSave.mockClear(); + }); + + it('Onchange event', () => { + const event = { target: { name: "location_id", value: "testLocation" } }; + const component = shallow(<DeployLoopModal loopCache={ loopCache }/>); + + component.find('[name="location_id"]').simulate('change', event); + component.update(); + expect(component.state('temporaryPropertiesJson').dcaeDeployParameters.testMs.location_id).toEqual("testLocation"); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/LoopPropertiesModal.js b/gui-clamp/ui-react/src/components/dialogs/Loop/LoopPropertiesModal.js new file mode 100644 index 0000000..6917713 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/LoopPropertiesModal.js @@ -0,0 +1,118 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Form from 'react-bootstrap/Form'; +import styled from 'styled-components'; +import LoopService from '../../../api/LoopService'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` +export default class LoopPropertiesModal extends React.Component { + + state = { + show: true, + loopCache: this.props.loopCache, + temporaryPropertiesJson: JSON.parse(JSON.stringify(this.props.loopCache.getGlobalProperties())) + }; + + constructor(props, context) { + super(props, context); + + this.handleClose = this.handleClose.bind(this); + this.handleSave = this.handleSave.bind(this); + this.handleChange = this.handleChange.bind(this); + + this.renderDcaeParameters = this.renderDcaeParameters.bind(this); + this.renderAllParameters = this.renderAllParameters.bind(this); + this.getDcaeParameters = this.getDcaeParameters.bind(this); + this.readOnly = props.readOnly !== undefined ? props.readOnly : false; + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopCache: newProps.loopCache, + temporaryPropertiesJson: JSON.parse(JSON.stringify(newProps.loopCache.getGlobalProperties())) + }); + } + + handleClose() { + this.props.history.push('/'); + } + + handleSave(event) { + LoopService.updateGlobalProperties(this.state.loopCache.getLoopName(), this.state.temporaryPropertiesJson).then(resp => { + this.setState({ show: false }); + this.props.history.push('/'); + this.props.loadLoopFunction(this.state.loopCache.getLoopName()); + }); + } + + handleChange(event) { + this.setState({ temporaryPropertiesJson: { [event.target.name]: JSON.parse(event.target.value) } }); + } + + renderAllParameters() { + return (<Modal.Body> + <Form> + { this.renderDcaeParameters() } + </Form> + </Modal.Body> + ); + } + + getDcaeParameters() { + if (typeof (this.state.temporaryPropertiesJson) !== "undefined") { + return JSON.stringify(this.state.temporaryPropertiesJson["dcaeDeployParameters"]); + } else { + return ""; + } + + } + + renderDcaeParameters() { + return ( + <Form.Group> + <Form.Label>Deploy Parameters</Form.Label> + <Form.Control as="textarea" rows="3" name="dcaeDeployParameters" onChange={ this.handleChange } defaultValue={ this.getDcaeParameters() }></Form.Control> + </Form.Group> + ); + } + + render() { + return ( + <ModalStyled size="lg" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + <Modal.Title>Model Properties</Modal.Title> + </Modal.Header> + { this.renderAllParameters() } + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Cancel</Button> + <Button variant="primary" type="submit" disabled={ this.readOnly } onClick={ this.handleSave }>Save Changes</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/LoopPropertiesModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Loop/LoopPropertiesModal.test.js new file mode 100644 index 0000000..a9c5903 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/LoopPropertiesModal.test.js @@ -0,0 +1,110 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import LoopPropertiesModal from './LoopPropertiesModal'; +import LoopCache from '../../../api/LoopCache'; +import LoopService from '../../../api/LoopService'; + +describe('Verify LoopPropertiesModal', () => { + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "globalPropertiesJson": { + "dcaeDeployParameters": { + "location_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + } + }); + + it('Test the render method', () => { + const component = shallow( + <LoopPropertiesModal loopCache={ loopCache }/> + ) + component.setState({ + show: true, + temporaryPropertiesJson: { + "dcaeDeployParameters": { + "location_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + } + }); + + expect(component.state('temporaryPropertiesJson')).toEqual({ + "dcaeDeployParameters": { + "location_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + }); + expect(component.state('show')).toEqual(true); + + expect(component).toMatchSnapshot(); + }); + + it('Test handleClose', () => { + const historyMock = { push: jest.fn() }; + const handleClose = jest.spyOn(LoopPropertiesModal.prototype, 'handleClose'); + const component = shallow(<LoopPropertiesModal history={ historyMock } loopCache={ loopCache }/>) + + component.find('[variant="secondary"]').prop('onClick')(); + + expect(handleClose).toHaveBeenCalledTimes(1); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test handleSave successful', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const loadLoopFunction = jest.fn(); + const handleSave = jest.spyOn(LoopPropertiesModal.prototype, 'handleSave'); + LoopService.updateGlobalProperties = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + text: () => "OK" + }); + }); + + const component = shallow(<LoopPropertiesModal history={ historyMock } + loopCache={ loopCache } loadLoopFunction={ loadLoopFunction }/>) + + component.find('[variant="primary"]').prop('onClick')(); + await flushPromises(); + component.update(); + + expect(handleSave).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Onchange event', () => { + const event = { target: { name: "dcaeDeployParameters", value: "{\"location_id\": \"testLocation\",\"policy_id\": \"TCA_h2NMX_v1_0_ResourceInstanceName1_tca\"}" } }; + const component = shallow(<LoopPropertiesModal loopCache={ loopCache }/>); + + component.find('FormControl').simulate('change', event); + component.update(); + + expect(component.state('temporaryPropertiesJson').dcaeDeployParameters.location_id).toEqual("testLocation"); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/ModifyLoopModal.js b/gui-clamp/ui-react/src/components/dialogs/Loop/ModifyLoopModal.js new file mode 100644 index 0000000..8f8b74d --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/ModifyLoopModal.js @@ -0,0 +1,269 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React, { forwardRef } from 'react' +import MaterialTable from "material-table"; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import styled from 'styled-components'; +import PolicyToscaService from '../../../api/PolicyToscaService'; +import ArrowUpward from '@material-ui/icons/ArrowUpward'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Search from '@material-ui/icons/Search'; +import LoopService from '../../../api/LoopService'; +import Tabs from 'react-bootstrap/Tabs'; +import Tab from 'react-bootstrap/Tab'; +import Alert from 'react-bootstrap/Alert'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` +const TextModal = styled.textarea` + margin-top: 20px; + white-space: pre; + background-color: ${ props => props.theme.toscaTextareaBackgroundColor }; + text-align: justify; + font-size: ${ props => props.theme.toscaTextareaFontSize }; + width: 100%; + height: 300px; +` +const cellStyle = { border: '1px solid black' }; +const headerStyle = { backgroundColor: '#ddd', border: '2px solid black' }; +const rowHeaderStyle = { backgroundColor: '#ddd', fontSize: '15pt', text: 'bold', border: '1px solid black' }; + +export default class ModifyLoopModal extends React.Component { + + state = { + show: true, + loopCache: this.props.loopCache, + content: 'Please select Tosca model to view the details', + selectedRowData: {}, + toscaPolicyModelsData: [], + selectedPolicyModelsData: [], + key: 'add', + showFailAlert: false, + toscaColumns: [ + { + title: "#", field: "index", render: rowData => rowData.tableData.id + 1, + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Model Type", field: "policyModelType", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Acronym", field: "policyAcronym", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Name", field: "policyName", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Version", field: "version", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Uploaded By", field: "updatedBy", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Uploaded Date", field: "updatedDate", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Created Date", field: "createdDate", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + } + ], + tableIcons: { + FirstPage: forwardRef((props, ref) => <FirstPage { ...props } ref={ ref }/>), + LastPage: forwardRef((props, ref) => <LastPage { ...props } ref={ ref }/>), + NextPage: forwardRef((props, ref) => <ChevronRight { ...props } ref={ ref }/>), + PreviousPage: forwardRef((props, ref) => <ChevronLeft { ...props } ref={ ref }/>), + ResetSearch: forwardRef((props, ref) => <Clear { ...props } ref={ ref }/>), + Search: forwardRef((props, ref) => <Search { ...props } ref={ ref }/>), + SortArrow: forwardRef((props, ref) => <ArrowUpward { ...props } ref={ ref }/>) + } + }; + + constructor(props, context) { + super(props, context); + this.handleClose = this.handleClose.bind(this); + this.initializeToscaPolicyModelsInfo = this.initializeToscaPolicyModelsInfo.bind(this); + this.handleYamlContent = this.handleYamlContent.bind(this); + this.getToscaPolicyModelYaml = this.getToscaPolicyModelYaml.bind(this); + this.handleAdd = this.handleAdd.bind(this); + this.handleRemove = this.handleRemove.bind(this); + this.initializeToscaPolicyModelsInfo(); + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopCache: newProps.loopCache, + temporaryPropertiesJson: JSON.parse(JSON.stringify(newProps.loopCache.getGlobalProperties())) + }); + } + + initializeToscaPolicyModelsInfo() { + var operationalPolicies = this.state.loopCache.getOperationalPolicies(); + var selectedPolicyModels = []; + for (var policy in operationalPolicies) { + var newRow = operationalPolicies[policy]["policyModel"]; + newRow["policyName"] = operationalPolicies[policy].name; + selectedPolicyModels.push(newRow); + } + + PolicyToscaService.getToscaPolicyModels().then(allToscaModels => { + this.setState({ + toscaPolicyModelsData: allToscaModels, + selectedPolicyModelsData: selectedPolicyModels + }); + }); + } + + getToscaPolicyModelYaml(policyModelType, policyModelVersion) { + if (typeof policyModelType !== "undefined") { + PolicyToscaService.getToscaPolicyModelYaml(policyModelType, policyModelVersion).then(toscaYaml => { + if (toscaYaml.length !== 0) { + this.setState({ content: toscaYaml }) + } else { + this.setState({ content: 'No Tosca model Yaml available' }) + } + }); + } else { + this.setState({ content: 'Please select Tosca model to view the details' }) + } + } + + handleYamlContent(event) { + this.setState({ content: event.target.value }); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + renderAlert() { + return ( + <div> + <Alert variant="danger" show={ this.state.showFailAlert } onClose={ this.disableAlert } dismissible> + { this.state.showMessage } + </Alert> + </div> + ); + } + + handleAdd() { + LoopService.addOperationalPolicyType(this.state.loopCache.getLoopName(), this.state.selectedRowData.policyModelType, this.state.selectedRowData.version) + .then(pars => { + this.props.loadLoopFunction(this.state.loopCache.getLoopName()); + this.handleClose(); + }) + .catch(error => { + this.setState({ showFailAlert: true, showMessage: "Adding failed with error: " + error.message }); + }); + } + + handleRemove() { + LoopService.removeOperationalPolicyType(this.state.loopCache.getLoopName(), this.state.selectedRowData.policyModelType, this.state.selectedRowData.version, this.state.selectedRowData.policyName); + this.props.loadLoopFunction(this.state.loopCache.getLoopName()); + this.handleClose(); + } + + render() { + return ( + <ModalStyled size="xl" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + <Modal.Title>Modify Loop Operational Policies</Modal.Title> + </Modal.Header> + <Tabs id="controlled-tab-example" activeKey={ this.state.key } onSelect={ key => this.setState({ key, selectedRowData: {} }) }> + <Tab eventKey="add" title="Add Operational Policies"> + <Modal.Body> + <MaterialTable + title={ "View Tosca Policy Models" } + data={ this.state.toscaPolicyModelsData } + columns={ this.state.toscaColumns } + icons={ this.state.tableIcons } + onRowClick={ (event, rowData) => { + this.getToscaPolicyModelYaml(rowData.policyModelType, rowData.version); + this.setState({ selectedRowData: rowData }) + } } + options={ { + headerStyle: rowHeaderStyle, + rowStyle: rowData => ({ + backgroundColor: (this.state.selectedRowData !== {} && this.state.selectedRowData.tableData !== undefined + && this.state.selectedRowData.tableData.id === rowData.tableData.id) ? '#EEE' : '#FFF' + }) + } } + /> + <div> + <TextModal value={ this.state.content } onChange={ this.handleYamlContent }/> + </div> + </Modal.Body> + { this.renderAlert() } + </Tab> + <Tab eventKey="remove" title="Remove Operational Policies"> + <Modal.Body> + <MaterialTable + title={ "Tosca Policy Models already added" } + data={ this.state.selectedPolicyModelsData } + columns={ this.state.toscaColumns } + icons={ this.state.tableIcons } + onRowClick={ (event, rowData) => { + this.setState({ selectedRowData: rowData }) + } } + options={ { + headerStyle: rowHeaderStyle, + rowStyle: rowData => ({ + backgroundColor: (this.state.selectedRowData !== {} && this.state.selectedRowData.tableData !== undefined + && this.state.selectedRowData.tableData.id === rowData.tableData.id) ? '#EEE' : '#FFF' + }) + } } + /> + </Modal.Body> + </Tab> + </Tabs> + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Cancel</Button> + <Button variant="primary" disabled={ (this.state.key === "remove") } type="submit" onClick={ this.handleAdd }>Add</Button> + <Button variant="primary" disabled={ (this.state.key === "add") } type="submit" onClick={ this.handleRemove }>Remove</Button> + </Modal.Footer> + + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/ModifyLoopModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Loop/ModifyLoopModal.test.js new file mode 100644 index 0000000..79267af --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/ModifyLoopModal.test.js @@ -0,0 +1,109 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { mount } from 'enzyme'; +import ModifyLoopModal from './ModifyLoopModal'; +import LoopCache from '../../../api/LoopCache'; +import LoopService from '../../../api/LoopService'; +import PolicyToscaService from '../../../api/PolicyToscaService'; + +describe('Verify ModifyLoopModal', () => { + beforeEach(() => { + PolicyToscaService.getToscaPolicyModels = jest.fn().mockImplementation(() => { + return Promise.resolve([{ + "policyModelType": "test", + "policyAcronym": "test", + "version": "1.0.0", + "updatedBy": "", + "updatedDate": "" + }]); + }); + PolicyToscaService.getToscaPolicyModelYaml = jest.fn().mockImplementation(() => { + return Promise.resolve("OK"); + }); + }) + + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "microServicePolicies": [{ + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "properties": { "domain": "measurementsForVfScaling" }, + "shared": false, + "jsonRepresentation": { "schema": {} } + }], + "globalPropertiesJson": { + "dcaeDeployParameters": { + "testMs": { + "location_id": "", + "policy_id": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + } + } + } + }); + const historyMock = { push: jest.fn() }; + const flushPromises = () => new Promise(setImmediate); + + it('Test handleClose', () => { + const handleClose = jest.spyOn(ModifyLoopModal.prototype, 'handleClose'); + const component = mount(<ModifyLoopModal history={ historyMock } loopCache={ loopCache }/>) + + component.find('[variant="secondary"]').get(0).props.onClick(); + + expect(handleClose).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test getToscaPolicyModelYaml', async () => { + const flushPromises = () => new Promise(setImmediate); + const component = mount(<ModifyLoopModal history={ historyMock } loopCache={ loopCache }/>) + component.setState({ + "selectedRowData": { "tableData": { "id": 0 } } + }); + const instance = component.instance(); + + instance.getToscaPolicyModelYaml("", "1.0.0"); + expect(component.state('content')).toEqual("Please select Tosca model to view the details"); + + instance.getToscaPolicyModelYaml("test", "1.0.0"); + await flushPromises(); + expect(component.state('content')).toEqual("OK"); + + PolicyToscaService.getToscaPolicyModelYaml = jest.fn().mockImplementation(() => { + return Promise.resolve(""); + }); + instance.getToscaPolicyModelYaml("test", "1.0.0"); + await flushPromises(); + expect(component.state('content')).toEqual("No Tosca model Yaml available"); + }); + + it('Test handleYamlContent', async () => { + const component = mount(<ModifyLoopModal loopCache={ loopCache }/>) + const instance = component.instance(); + + const event = { "target": { "value": "testValue" } } + instance.handleYamlContent(event); + expect(component.state('content')).toEqual("testValue"); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/OpenLoopModal.js b/gui-clamp/ui-react/src/components/dialogs/Loop/OpenLoopModal.js new file mode 100644 index 0000000..b6407fb --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/OpenLoopModal.js @@ -0,0 +1,139 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import Select from 'react-select'; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Form from 'react-bootstrap/Form'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import FormCheck from 'react-bootstrap/FormCheck' +import styled from 'styled-components'; +import LoopService from '../../../api/LoopService'; +import SvgGenerator from '../../loop_viewer/svg/SvgGenerator'; +import LoopCache from '../../../api/LoopCache'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` +const CheckBoxStyled = styled(FormCheck.Input)` + margin-left: 3rem; +` + +export default class OpenLoopModal extends React.Component { + constructor(props, context) { + super(props, context); + + this.getLoopNames = this.getLoopNames.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleDropDownListChange = this.handleDropDownListChange.bind(this); + this.renderSvg = this.renderSvg.bind(this); + this.showReadOnly = props.showReadOnly !== undefined ? props.showReadOnly : true; + this.state = { + show: true, + chosenLoopName: '', + loopNames: [], + loopCacheOpened: new LoopCache({}) + }; + } + + componentWillMount() { + this.getLoopNames(); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + handleDropDownListChange(e) { + LoopService.getLoop(e.value).then(loop => { + this.setState({ + chosenLoopName: e.value, + loopCacheOpened: new LoopCache(loop) + }); + }); + } + + getLoopNames() { + LoopService.getLoopNames().then(loopNames => { + if (Object.entries(loopNames).length !== 0) { + const loopOptions = loopNames.filter(loopName => loopName !== 'undefined').map((loopName) => { + return { label: loopName, value: loopName } + }); + this.setState({ loopNames: loopOptions }) + } + }); + } + + handleOpen() { + console.info("Loop " + this.state.chosenLoopName + " is chosen"); + this.handleClose(); + this.props.loadLoopFunction(this.state.chosenLoopName); + } + + renderSvg() { + return ( + <SvgGenerator loopCache={ this.state.loopCacheOpened } clickable={ false } generatedFrom={ SvgGenerator.GENERATED_FROM_INSTANCE }/> + ); + } + + render() { + return ( + <ModalStyled size="xl" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + <Modal.Title>Open Model</Modal.Title> + </Modal.Header> + <Modal.Body> + <Form.Group as={ Row } controlId="formPlaintextEmail"> + <Form.Label column sm="2">Model Name:</Form.Label> + <Col sm="10"> + <Select onChange={ this.handleDropDownListChange } + options={ this.state.loopNames }/> + </Col> + </Form.Group> + <Form.Group as={ Row } style={ { alignItems: 'center' } } controlId="formSvgPreview"> + <Form.Label column sm="2">Model Preview:</Form.Label> + <Col sm="10"> + { this.renderSvg() } + </Col> + </Form.Group> + { this.showReadOnly === true ? + <Form.Group as={ Row } controlId="formBasicCheckbox"> + <Form.Check> + <FormCheck.Label>Read Only Mode:</FormCheck.Label> + <CheckBoxStyled style={ { marginLeft: '3.5em' } } type="checkbox"/> + </Form.Check> + </Form.Group> + : null } + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Cancel</Button> + <Button variant="primary" type="submit" onClick={ this.handleOpen }>Open</Button> + </Modal.Footer> + </ModalStyled> + + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/OpenLoopModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Loop/OpenLoopModal.test.js new file mode 100644 index 0000000..6b2dc4a --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/OpenLoopModal.test.js @@ -0,0 +1,93 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import OpenLoopModal from './OpenLoopModal'; +import LoopService from '../../../api/LoopService'; + +describe('Verify OpenLoopModal', () => { + + beforeEach(() => { + fetch.resetMocks(); + fetch.mockResponse(JSON.stringify([ + "LOOP_gmtAS_v1_0_ResourceInstanceName1_tca", + "LOOP_gmtAS_v1_0_ResourceInstanceName1_tca_3", + "LOOP_gmtAS_v1_0_ResourceInstanceName2_tca_2" + ])); + }); + + it('Test the render method', () => { + + const component = shallow(<OpenLoopModal/>); + expect(component).toMatchSnapshot(); + }); + + it('Onchange event', async () => { + const flushPromises = () => new Promise(setImmediate); + LoopService.getLoop = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + const event = { value: 'LOOP_gmtAS_v1_0_ResourceInstanceName1_tca_3' }; + const component = shallow(<OpenLoopModal/>); + component.find('StateManager').simulate('change', event); + await flushPromises(); + component.update(); + expect(component.state('chosenLoopName')).toEqual("LOOP_gmtAS_v1_0_ResourceInstanceName1_tca_3"); + }); + + + it('Test handleClose', () => { + const historyMock = { push: jest.fn() }; + const handleClose = jest.spyOn(OpenLoopModal.prototype, 'handleClose'); + const component = shallow(<OpenLoopModal history={ historyMock }/>) + + component.find('[variant="secondary"]').prop('onClick')(); + + expect(handleClose).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + + handleClose.mockClear(); + }); + + it('Test handleSubmit', () => { + const historyMock = { push: jest.fn() }; + const loadLoopFunction = jest.fn(); + const handleOpen = jest.spyOn(OpenLoopModal.prototype, 'handleOpen'); + const component = shallow(<OpenLoopModal history={ historyMock } loadLoopFunction={ loadLoopFunction }/>) + + component.find('[variant="primary"]').prop('onClick')(); + + expect(handleOpen).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + + handleOpen.mockClear(); + }); + +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/CreateLoopModal.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/CreateLoopModal.test.js.snap new file mode 100644 index 0000000..1ba9dba --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/CreateLoopModal.test.js.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify CreateLoopModal Test the render method 1`] = ` +<Styled(Modal) + backdrop="static" + keyboard={false} + onHide={[Function]} + show={true} + size="xl" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + > + <ModalTitle> + Create Model + </ModalTitle> + </ModalHeader> + <ModalBody> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formPlaintextEmail" + > + <FormLabel + column={true} + sm="2" + srOnly={false} + > + Template Name: + </FormLabel> + <Col + sm="10" + > + <StateManager + defaultInputValue="" + defaultMenuIsOpen={false} + defaultValue={null} + onChange={[Function]} + options={Array []} + /> + </Col> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formSvgPreview" + style={ + Object { + "alignItems": "center", + } + } + > + <FormLabel + column={true} + sm="2" + srOnly={false} + > + Model Preview: + </FormLabel> + <Col + sm="10" + > + <withRouter(SvgGenerator) + clickable={false} + generatedFrom="TEMPLATE" + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + </Col> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formPlaintextEmail" + > + <FormLabel + column={true} + sm="2" + srOnly={false} + > + Model Name: + </FormLabel> + <input + onChange={[Function]} + sm="5" + style={ + Object { + "marginLeft": "1em", + "width": "50%", + } + } + type="text" + value="" + /> + <span + sm="5" + /> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formPlaintextEmail" + > + <FormLabel + column={true} + sm="2" + srOnly={false} + > + + </FormLabel> + <styled.div /> + </FormGroup> + </ModalBody> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="null" + variant="secondary" + > + Cancel + </Button> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="submit" + variant="primary" + > + Create + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/DeployLoopModal.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/DeployLoopModal.test.js.snap new file mode 100644 index 0000000..4779ced --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/DeployLoopModal.test.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify DeployLoopModal Test the render method 1`] = ` +<Styled(Modal) + backdrop="static" + keyboard={false} + onHide={[Function]} + show={true} + size="lg" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + > + <ModalTitle> + Deployment parameters + </ModalTitle> + </ModalHeader> + <Tabs + activeKey="testMs" + id="controlled-tab-example" + mountOnEnter={false} + onSelect={[Function]} + unmountOnExit={false} + variant="tabs" + > + <Tab + eventKey="testMs" + title="testMs" + > + <Styled(FormGroup)> + <FormLabel + column={false} + srOnly={false} + > + location_id + </FormLabel> + <FormControl + defaultValue="" + name="location_id" + onChange={[Function]} + type="text" + /> + </Styled(FormGroup)> + <Styled(FormGroup)> + <FormLabel + column={false} + srOnly={false} + > + policy_id + </FormLabel> + <FormControl + defaultValue="TCA_h2NMX_v1_0_ResourceInstanceName1_tca" + name="policy_id" + onChange={[Function]} + type="text" + /> + </Styled(FormGroup)> + </Tab> + </Tabs> + <div /> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="null" + variant="secondary" + > + Cancel + </Button> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="submit" + variant="primary" + > + Deploy + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/LoopPropertiesModal.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/LoopPropertiesModal.test.js.snap new file mode 100644 index 0000000..3baaa57 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/LoopPropertiesModal.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify LoopPropertiesModal Test the render method 1`] = ` +<Styled(Modal) + backdrop="static" + keyboard={false} + onHide={[Function]} + show={true} + size="lg" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + > + <ModalTitle> + Model Properties + </ModalTitle> + </ModalHeader> + <ModalBody> + <Form + inline={false} + > + <FormGroup> + <FormLabel + column={false} + srOnly={false} + > + Deploy Parameters + </FormLabel> + <FormControl + as="textarea" + defaultValue="{\\"location_id\\":\\"\\",\\"policy_id\\":\\"TCA_h2NMX_v1_0_ResourceInstanceName1_tca\\"}" + name="dcaeDeployParameters" + onChange={[Function]} + rows="3" + /> + </FormGroup> + </Form> + </ModalBody> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="null" + variant="secondary" + > + Cancel + </Button> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="submit" + variant="primary" + > + Save Changes + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/OpenLoopModal.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/OpenLoopModal.test.js.snap new file mode 100644 index 0000000..581fd0e --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Loop/__snapshots__/OpenLoopModal.test.js.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify OpenLoopModal Test the render method 1`] = ` +<Styled(Modal) + backdrop="static" + keyboard={false} + onHide={[Function]} + show={true} + size="xl" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + > + <ModalTitle> + Open Model + </ModalTitle> + </ModalHeader> + <ModalBody> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formPlaintextEmail" + > + <FormLabel + column={true} + sm="2" + srOnly={false} + > + Model Name: + </FormLabel> + <Col + sm="10" + > + <StateManager + defaultInputValue="" + defaultMenuIsOpen={false} + defaultValue={null} + onChange={[Function]} + options={Array []} + /> + </Col> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formSvgPreview" + style={ + Object { + "alignItems": "center", + } + } + > + <FormLabel + column={true} + sm="2" + srOnly={false} + > + Model Preview: + </FormLabel> + <Col + sm="10" + > + <withRouter(SvgGenerator) + clickable={false} + generatedFrom="INSTANCE" + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + </Col> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="formBasicCheckbox" + > + <FormCheck> + <FormCheckLabel> + Read Only Mode: + </FormCheckLabel> + <Styled(FormCheckInput) + style={ + Object { + "marginLeft": "3.5em", + } + } + type="checkbox" + /> + </FormCheck> + </FormGroup> + </ModalBody> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="null" + variant="secondary" + > + Cancel + </Button> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="submit" + variant="primary" + > + Open + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/ManageDictionaries.js b/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/ManageDictionaries.js new file mode 100644 index 0000000..d7ba8d1 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/ManageDictionaries.js @@ -0,0 +1,637 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + + +import React, { forwardRef } from 'react'; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import styled from 'styled-components'; +import TemplateMenuService from '../../../api/TemplateService'; +import CsvToJson from '../../../utils/CsvToJson'; +import MaterialTable, { MTableToolbar } from "material-table"; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import AddBox from '@material-ui/icons/AddBox'; +import ArrowUpward from '@material-ui/icons/ArrowUpward'; +import Check from '@material-ui/icons/Check'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import VerticalAlignTopIcon from '@material-ui/icons/VerticalAlignTop'; +import VerticalAlignBottomIcon from '@material-ui/icons/VerticalAlignBottom'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import DeleteOutline from '@material-ui/icons/DeleteOutline'; +import Edit from '@material-ui/icons/Edit'; +import FilterList from '@material-ui/icons/FilterList'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Remove from '@material-ui/icons/Remove'; +import Search from '@material-ui/icons/Search'; +import ViewColumn from '@material-ui/icons/ViewColumn'; + + +const ModalStyled = styled(Modal)` + @media (min-width: 1200px) { + .modal-xl { + max-width: 96%; + } + } + background-color: transparent; +` + +const MTableToolbarStyled = styled(MTableToolbar)` + display: flex; + flex-direction: row; + align-items: center; +` +const ColPullLeftStyled = styled(Col)` + display: flex; + flex-direction: row; + align-items: center; + margin-left: -40px; +` + +const cellStyle = { border: '1px solid black' }; +const headerStyle = { backgroundColor: '#ddd', border: '2px solid black' }; +const rowHeaderStyle = { backgroundColor: '#ddd', fontSize: '15pt', text: 'bold', border: '1px solid black' }; + +let dictList = []; +let subDictFlag = false; + +function SelectSubDictType(props) { + const { onChange } = props; + const selectedValues = (e) => { + let options = e.target.options; + let SelectedDictTypes = ''; + for (let dictType = 0, values = options.length; dictType < values; dictType++) { + if (options[dictType].selected) { + SelectedDictTypes = SelectedDictTypes.concat(options[dictType].value); + SelectedDictTypes = SelectedDictTypes.concat('|'); + } + } + SelectedDictTypes = SelectedDictTypes.slice(0, -1); + onChange(SelectedDictTypes); + } + // When the subDictFlag is true, we need to disable selection of element "type" + return ( + <div> + <select disabled={ subDictFlag } multiple={ true } onChange={ selectedValues }> + <option value="string">string</option> + <option value="number">number</option> + <option value="datetime">datetime</option> + <option value="map">map</option> + <option value="json">json</option> + </select> + </div> + ); +} + +function SubDict(props) { + const { onChange } = props; + const subDicts = []; + subDicts.push('none'); + if (dictList !== undefined && dictList.length > 0) { + let item; + for (item in dictList) { + if (dictList[item].secondLevelDictionary === 1) { + subDicts.push(dictList[item].name); + } + } + } + let optionItems = []; + for (let i = 0; i < subDicts.length; ++i) { + if (i === 0) { + optionItems.push(<option selected key={ subDicts[i] }>{ subDicts[i] }</option>); + } else { + optionItems.push(<option key={ subDicts[i] }>{ subDicts[i] }</option>); + } + } + + function selectedValue(e) { + onChange(e.target.value); + } + + // When the subDictFlag is true, we need to disable selection of + // the sub-dictionary flag + return ( + <select disabled={ subDictFlag } onChange={ selectedValue }> + { optionItems } + </select> + ); +} + +export default class ManageDictionaries extends React.Component { + constructor(props, context) { + super(props, context); + this.addDictionaryElementRow = this.addDictionaryElementRow.bind(this); + this.addDictionaryRow = this.addDictionaryRow.bind(this); + this.addReplaceDictionaryRequest = this.addReplaceDictionaryRequest.bind(this); + this.clickHandler = this.clickHandler.bind(this); + this.deleteDictionaryElementRow = this.deleteDictionaryElementRow.bind(this); + this.deleteDictionaryRequest = this.deleteDictionaryRequest.bind(this); + this.deleteDictionaryRow = this.deleteDictionaryRow.bind(this); + this.fileSelectedHandler = this.fileSelectedHandler.bind(this); + this.getDictionaries = this.getDictionaries.bind(this); + this.getDictionaryElements = this.getDictionaryElements.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleDictionaryRowClick = this.handleDictionaryRowClick.bind(this); + this.importCsvData = this.importCsvData.bind(this); + this.updateDictionaryElementRow = this.updateDictionaryElementRow.bind(this); + this.updateDictionaryElementsRequest = this.updateDictionaryElementsRequest.bind(this); + this.updateDictionaryRow = this.updateDictionaryRow.bind(this); + this.readOnly = props.readOnly !== undefined ? props.readOnly : false; + this.state = { + show: true, + currentSelectedDictionary: null, + exportFilename: '', + content: null, + dictionaryElements: [], + tableIcons: { + Add: forwardRef((props, ref) => <AddBox { ...props } ref={ ref }/>), + Delete: forwardRef((props, ref) => <DeleteOutline { ...props } ref={ ref }/>), + DetailPanel: forwardRef((props, ref) => <ChevronRight { ...props } ref={ ref }/>), + Edit: forwardRef((props, ref) => <Edit { ...props } ref={ ref }/>), + Check: forwardRef((props, ref) => <Check { ...props } ref={ ref }/>), + Clear: forwardRef((props, ref) => <Clear { ...props } ref={ ref }/>), + Export: forwardRef((props, ref) => <VerticalAlignBottomIcon { ...props } ref={ ref }/>), + Filter: forwardRef((props, ref) => <FilterList { ...props } ref={ ref }/>), + FirstPage: forwardRef((props, ref) => <FirstPage { ...props } ref={ ref }/>), + LastPage: forwardRef((props, ref) => <LastPage { ...props } ref={ ref }/>), + NextPage: forwardRef((props, ref) => <ChevronRight { ...props } ref={ ref }/>), + PreviousPage: forwardRef((props, ref) => <ChevronLeft { ...props } ref={ ref }/>), + ResetSearch: forwardRef((props, ref) => <Clear { ...props } ref={ ref }/>), + Search: forwardRef((props, ref) => <Search { ...props } ref={ ref }/>), + SortArrow: forwardRef((props, ref) => <ArrowUpward { ...props } ref={ ref }/>), + ThirdStateCheck: forwardRef((props, ref) => <Remove { ...props } ref={ ref }/>), + ViewColumn: forwardRef((props, ref) => <ViewColumn { ...props } ref={ ref }/>) + }, + dictColumns: [ + { + title: "Dictionary Name", field: "name", editable: 'onAdd', + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Sub Dictionary ?", field: "secondLevelDictionary", lookup: { 0: 'No', 1: 'Yes' }, + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Dictionary Type", field: "subDictionaryType", lookup: { string: 'string', number: 'number' }, + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Updated By", field: "updatedBy", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Last Updated Date", field: "updatedDate", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + } + ], + dictElementColumns: [ + { + title: "Element Short Name", field: "shortName", editable: 'onAdd', + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Element Name", field: "name", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Element Description", field: "description", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Element Type", field: "type", + editComponent: props => ( + <div> + <SelectSubDictType value={ props.value } onChange={ props.onChange }/> + </div> + ), + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Sub-Dictionary", field: "subDictionary", + editComponent: props => ( + <div> + <SubDict value={ props.value } onChange={ props.onChange }/> + </div> + ), + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Updated By", field: "updatedBy", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Updated Date", field: "updatedDate", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + } + ] + } + } + + componentDidMount() { + this.getDictionaries(); + } + + getDictionaries() { + TemplateMenuService.getDictionary().then(arrayOfdictionaries => { + this.setState({ dictionaries: arrayOfdictionaries, currentSelectedDictionary: null }) + // global variable setting used functional components in this file + dictList = arrayOfdictionaries; + }).catch(() => { + console.error('Failed to retrieve dictionaries'); + this.setState({ dictionaries: [], currentSelectedDictionary: null }) + }); + } + + getDictionaryElements(dictionaryName) { + TemplateMenuService.getDictionaryElements(dictionaryName).then(dictionaryElements => { + this.setState({ dictionaryElements: dictionaryElements.dictionaryElements }); + this.setState({ currentSelectDictionary: dictionaryName }); + }).catch(() => console.error('Failed to retrieve dictionary elements')) + } + + clickHandler(rowData) { + this.getDictionaries(); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + addReplaceDictionaryRequest(dictionaryEntry) { + TemplateMenuService.insDictionary(dictionaryEntry) + .then(resp => { + this.getDictionaries(); + }) + .catch(() => console.error('Failed to insert new dictionary elements')); + } + + updateDictionaryElementsRequest(dictElements) { + let reqData = { "name": this.state.currentSelectedDictionary, 'dictionaryElements': dictElements }; + TemplateMenuService.insDictionaryElements(reqData) + .then(resp => { + this.getDictionaryElements(this.state.currentSelectedDictionary) + }) + .catch(() => console.error('Failed to update dictionary elements')); + } + + deleteDictionaryRequest(dictionaryName) { + TemplateMenuService.deleteDictionary(dictionaryName) + .then(resp => { + this.getDictionaries(); + }) + .catch(() => console.error('Failed to delete dictionary')); + } + + deleteDictionaryElementRequest(dictionaryName, elemenetShortName) { + TemplateMenuService.deleteDictionaryElements({ 'name': dictionaryName, 'shortName': elemenetShortName }) + .then(resp => { + this.getDictionaryElements(dictionaryName); + }) + .catch(() => console.error('Failed to delete dictionary elements')); + } + + fileSelectedHandler = (event) => { + + if (event.target.files[0].type === 'text/csv' || event.target.files[0].type === 'application/vnd.ms-excel') { + if (event.target.files && event.target.files[0]) { + const reader = new FileReader(); + reader.onload = (e) => { + let errorMessages = this.importCsvData(reader.result); + if (errorMessages !== '') { + alert(errorMessages); + } + } + reader.readAsText(event.target.files[0]); + } + } else { + alert('Please upload .csv extention files only.'); + } + } + + importCsvData(rawCsvData) { + + const jsonKeyNames = ['shortName', 'name', 'description', 'type', 'subDictionary']; + const userHeaderNames = ['Element Short Name', 'Element Name', 'Element Description', 'Element Type', 'Sub-Dictionary']; + const validTypes = ['string', 'number', 'datetime', 'json', 'map']; + + let mandatory; + + if (subDictFlag) { + mandatory = [true, true, true, false, false]; + } else { + mandatory = [true, true, true, true, false]; + } + + let result = CsvToJson(rawCsvData, ',', '||||', userHeaderNames, jsonKeyNames, mandatory); + + let errorMessages = result.errorMessages; + let jsonObjArray = result.jsonObjArray; + + let validTypesErrorMesg = ''; + + for (let i = 0; i < validTypes.length; ++i) { + if (i === 0) { + validTypesErrorMesg = validTypes[i]; + } else { + validTypesErrorMesg += ',' + validTypes[i]; + } + } + + if (errorMessages !== '') { + return errorMessages; + } + + // Perform further checks on data that is now in JSON form + let subDictionaries = []; + + // NOTE: dictList is a global variable maintained faithfully + // by the getDictionaries() method outside this import + // functionality. + let item; + for (item in dictList) { + if (dictList[item].secondLevelDictionary === 1) { + subDictionaries.push(dictList[item].name); + } + } + ; + + // Check for valid Sub-Dictionary and Element Type values + subDictionaries = subDictionaries.toString(); + let row = 2; + let dictElem; + for (dictElem of jsonObjArray) { + let itemKey; + for (itemKey in dictElem) { + let value = dictElem[itemKey].trim(); + let keyIndex = jsonKeyNames.indexOf(itemKey); + if (itemKey === 'shortName' && /[^a-zA-Z0-9-_.]/.test(value)) { + errorMessages += '\n' + userHeaderNames[keyIndex] + + ' at row #' + row + + ' can only contain alphanumeric characters and periods, hyphens or underscores'; + } + if (itemKey === 'type' && validTypes.indexOf(value) < 0) { + errorMessages += '\nInvalid value of "' + value + '" for "' + userHeaderNames[keyIndex] + '" at row #' + row; + errorMessages += '\nValid types are: ' + validTypesErrorMesg; + } + if (value !== "" && itemKey === 'subDictionary' && subDictionaries.indexOf(value) < 0) { + errorMessages += '\nInvalid Sub-Dictionary value of "' + value + '" at row #' + row; + } + } + ++row; + } + if (errorMessages === '') { + // We made it through all the checks. Send it to back end + this.updateDictionaryElementsRequest(jsonObjArray); + } + + return errorMessages; + } + + addDictionaryRow(newData) { + let validData = true; + return new Promise((resolve, reject) => { + setTimeout(() => { + if (/[^a-zA-Z0-9-_.]/.test(newData.name)) { + validData = false; + alert('Please enter alphanumeric input. Only allowed special characters are:(period, hyphen, underscore)'); + reject(); + } + for (let i = 0; i < this.state.dictionaries.length; i++) { + if (this.state.dictionaries[i].name === newData.name) { + validData = false; + alert(newData.name + ' dictionary name already exists') + reject(); + } + } + if (validData) { + this.addReplaceDictionaryRequest(newData); + } + resolve(); + }, 1000); + }); + } + + + updateDictionaryRow(newData, oldData) { + let validData = true; + return new Promise((resolve, reject) => { + setTimeout(() => { + if (/[^a-zA-Z0-9-_.]/.test(newData.name)) { + validData = false; + alert('Please enter alphanumberic input. Only allowed special characters are:(period, hyphen, underscore)'); + reject(); + } + if (validData) { + this.addReplaceDictionaryRequest(newData); + } + resolve(); + }, 1000); + }); + } + + deleteDictionaryRow(oldData) { + return new Promise((resolve, reject) => { + setTimeout(() => { + this.deleteDictionaryRequest(oldData.name); + resolve(); + }, 1000); + }); + } + + addDictionaryElementRow(newData) { + return new Promise((resolve, reject) => { + setTimeout(() => { + let dictionaryElements = this.state.dictionaryElements; + let errorMessages = ''; + for (let i = 0; i < this.state.dictionaryElements.length; i++) { + if (this.state.dictionaryElements[i].shortName === newData.shortName) { + alert('Short Name "' + newData.shortName + '" already exists'); + reject(""); + } + } + // MaterialTable returns no property at all if the user has not touched a + // new column, so we want to add the property with an emptry string + // for several cases if that is the case to simplify other checks. + if (newData.description === undefined) { + newData.description = ""; + } + if (newData.subDictionary === undefined) { + newData.subDictionary = null; + } + if (newData.type === undefined) { + newData.type = ""; + } + if (!newData.shortName && /[^a-zA-Z0-9-_.]/.test(newData.shortName)) { + errorMessages += '\nShort Name is limited to alphanumeric characters and also period, hyphen, and underscore'; + } + if (!newData.shortName) { + errorMessages += '\nShort Name must be specified'; + } + if (!newData.name) { + errorMessages += '\nElement Name must be specified'; + } + if (!newData.type && !subDictFlag) { + errorMessages += '\nElement Type must be specified'; + } + if (errorMessages === '') { + dictionaryElements.push(newData); + this.updateDictionaryElementsRequest([newData]); + resolve(); + } else { + alert(errorMessages); + reject(""); + } + }, 1000); + }); + } + + updateDictionaryElementRow(newData, oldData) { + return new Promise((resolve, reject) => { + setTimeout(() => { + let dictionaryElements = this.state.dictionaryElements; + let validData = true; + if (!newData.type) { + validData = false; + alert('Element Type cannot be null'); + reject(); + } + if (validData) { + const index = dictionaryElements.indexOf(oldData); + dictionaryElements[index] = newData; + this.updateDictionaryElementsRequest([newData]); + } + resolve(); + }, 1000); + }); + } + + + deleteDictionaryElementRow(oldData) { + return new Promise((resolve) => { + setTimeout(() => { + this.deleteDictionaryElementRequest(this.state.currentSelectedDictionary, oldData.shortName); + resolve(); + }, 1000); + }); + } + + handleDictionaryRowClick(event, rowData) { + subDictFlag = rowData.secondLevelDictionary === 1 ? true : false; + this.setState({ + currentSelectedDictionary: rowData.name, + exportFilename: rowData.name + }) + this.getDictionaryElements(rowData.name); + } + + render() { + return ( + <ModalStyled size="xl" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + <Modal.Title>Manage Dictionaries</Modal.Title> + </Modal.Header> + <Modal.Body> + { this.state.currentSelectedDictionary === null ? + <MaterialTable + title={ "Dictionary List" } + data={ this.state.dictionaries } + columns={ this.state.dictColumns } + icons={ this.state.tableIcons } + onRowClick={ this.handleDictionaryRowClick } + options={ { + headerStyle: rowHeaderStyle, + } } + editable={ !this.readOnly ? + { + onRowAdd: this.addDictionaryRow, + onRowUpdate: this.updateDictionaryRow, + onRowDelete: this.deleteDictionaryRow + } : undefined } + /> : null + } + { this.state.currentSelectedDictionary !== null ? + <MaterialTable + title={ 'Dictionary Elements List for ' + (subDictFlag ? 'Sub-Dictionary "' : '"') + this.state.currentSelectedDictionary + '"' } + data={ this.state.dictionaryElements } + columns={ this.state.dictElementColumns } + icons={ this.state.tableIcons } + options={ { + exportAllData: true, + exportButton: true, + exportFileName: this.state.exportFilename, + headerStyle: { backgroundColor: 'white', fontSize: '15pt', text: 'bold', border: '1px solid black' } + } } + components={ { + Toolbar: props => ( + <Row> + <Col sm="11"> + <MTableToolbarStyled { ...props } /> + </Col> + <ColPullLeftStyled sm="1"> + <Tooltip title="Import" placement="bottom"> + <IconButton aria-label="import" disabled={ this.readOnly } onClick={ () => this.fileUpload.click() }> + <VerticalAlignTopIcon/> + </IconButton> + </Tooltip> + <input type="file" ref={ (fileUpload) => { + this.fileUpload = fileUpload; + } } + style={ { visibility: 'hidden', width: '1px' } } onChange={ this.fileSelectedHandler }/> + </ColPullLeftStyled> + </Row> + ) + } } + editable={ !this.readOnly ? + { + onRowAdd: this.addDictionaryElementRow, + onRowUpdate: this.updateDictionaryElementRow, + onRowDelete: this.deleteDictionaryElementRow + } : undefined + } + /> : null + } + { this.state.currentSelectedDictionary !== null ? <button onClick={ this.clickHandler } style={ { marginTop: '25px' } }>Go Back to Dictionaries List</button> : "" } + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Close</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/ManageDictionaries.test.js b/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/ManageDictionaries.test.js new file mode 100644 index 0000000..2552d7a --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/ManageDictionaries.test.js @@ -0,0 +1,465 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; +import { render } from 'enzyme'; +import ManageDictionaries from './ManageDictionaries'; +import TemplateMenuService from '../../../api/TemplateService' + +const TestDictionaryElements = { + name: "test", + secondLevelDictionary: 0, + subDictionaryType: "", + dictionaryElements: [ + { + shortName: "alertType", + name: "Alert Type", + description: "Type of Alert", + type: "string", + subDictionary: "", + createdDate: "2020-06-12T13:58:51.443931Z", + updatedDate: "2020-06-13T16:27:57.084870Z", + updatedBy: "admin", + createdBy: "admin" + } + ] +}; + +const TestDictionaries = + [ + { + name: "test", + secondLevelDictionary: 0, + subDictionaryType: "string", + dictionaryElements: [TestDictionaryElements], + createdDate: "2020-06-14T21:00:33.231166Z", + updatedDate: "2020-06-14T21:00:33.231166Z", + updatedBy: "admin", + createdBy: "admin" + }, + { + name: "testSub1", + secondLevelDictionary: 1, + subDictionaryType: "string", + dictionaryElements: [ + { + shortName: "subElem", + name: "Sub Element", + description: "Sub Element Description", + type: "string", + createdDate: "2020-06-14T21:04:44.402287Z", + updatedDate: "2020-06-14T21:04:44.402287Z", + updatedBy: "admin", + createdBy: "admin" + } + ], + createdDate: "2020-06-14T21:01:16.390250Z", + updatedDate: "2020-06-14T21:01:16.390250Z", + updatedBy: "admin", + createdBy: "admin" + } + ]; + + +const historyMock = { push: jest.fn() }; + +let errorMessage = ''; + +window.alert = jest.fn().mockImplementation((mesg) => { + errorMessage = mesg; + return +}); + +TemplateMenuService.getDictionary = jest.fn().mockImplementation(() => { + return Promise.resolve(TestDictionaries); +}); + +TemplateMenuService.insDictionary = jest.fn().mockImplementation(() => { + return Promise.resolve({ ok: true, status: 200 }); +}); + +TemplateMenuService.deleteDictionary = jest.fn().mockImplementation(() => { + return Promise.resolve("200"); +}); + +TemplateMenuService.getDictionaryElements = jest.fn().mockImplementation(() => { + return Promise.resolve(TestDictionaryElements); +}); + +TemplateMenuService.deleteDictionaryElements = jest.fn().mockImplementation(() => { + return Promise.resolve("200"); +}); + +TemplateMenuService.insDictionaryElements = jest.fn().mockImplementation(() => { + return Promise.resolve("200"); +}); + + +describe('Verify ManageDictionaries', () => { + + beforeEach(() => { + fetch.resetMocks(); + }); + + it('Test API Successful', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "name": "vtest", + "secondLevelDictionary": 1, + "subDictionaryType": "string", + "updatedBy": "test", + "updatedDate": "05-07-2019 19:09:42" + }); + } + }); + }); + const component = shallow(<ManageDictionaries/>); + expect(component).toMatchSnapshot(); + }); + + it('Test API Exception', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: false, + status: 500, + json: () => { + return Promise.resolve({ + "name": "vtest", + "secondLevelDictionary": 1, + "subDictionaryType": "string", + "updatedBy": "test", + "updatedDate": "05-07-2019 19:09:42" + }); + } + }); + }); + const component = shallow(<ManageDictionaries/>); + }); + + it('Test Table icons', () => { + + const component = mount(<ManageDictionaries/>); + expect(component.find('[className="MuiSelect-icon MuiTablePagination-selectIcon"]')).toBeTruthy(); + }); + + test('Test add/replace and delete dictionary requests', async () => { + + const component = shallow(<ManageDictionaries history={ historyMock }/>) + const instance = component.instance(); + + const flushPromises = () => new Promise(setImmediate); + + instance.addReplaceDictionaryRequest({ name: "newdict", secondLevelDictionary: 0, subDictionaryType: "string" }); + instance.deleteDictionaryRequest("test"); + + await flushPromises(); + + expect(component.state('currentSelectedDictionary')).toEqual(null); + expect(component.state('dictionaries')).toEqual(TestDictionaries); + }); + + test('Test update dictionary row', async () => { + + const component = shallow(<ManageDictionaries history={ historyMock }/>) + const instance = component.instance(); + const rowData = { name: "newdict", secondLevelDictionary: 0, subDictionaryType: "string" }; + + await expect(instance.updateDictionaryRow(rowData, rowData)).resolves.toEqual(undefined); + + }, 2000); + + test('Test add dictionary row', async () => { + + const addReplaceRequest = jest.spyOn(ManageDictionaries.prototype, 'addReplaceDictionaryRequest'); + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + const rowData = { name: "newdict", secondLevelDictionary: 0, subDictionaryType: "string" }; + + await instance.addDictionaryRow(rowData); + expect(addReplaceRequest).toHaveBeenCalledWith(rowData); + + }, 2000); + + test('Test add dictionary row with errors name already exists', async () => { + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + let rowData = { name: "test", secondLevelDictionary: 0, subDictionaryType: "" }; + + await expect(instance.addDictionaryRow(rowData)).rejects.toEqual(undefined); + + }, 2000); + + test('Test add dictionary row with errors illegal chars in name', async () => { + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + let rowData = { name: "test@@", secondLevelDictionary: 0, subDictionaryType: "" }; + + await expect(instance.addDictionaryRow(rowData)).rejects.toEqual(undefined); + + }, 2000); + + test('Test update dictionary row with errors illegal chars in name', async () => { + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + let rowData = { name: "test@@", secondLevelDictionary: 0, subDictionaryType: "" }; + + await expect(instance.updateDictionaryRow(rowData)).rejects.toEqual(undefined); + }); + + + test('Test add dictionary row with errors (illegal chars)', async () => { + + const addReplaceRequest = jest.spyOn(ManageDictionaries.prototype, 'addReplaceDictionaryRequest'); + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + let rowData = { name: "test@@", secondLevelDictionary: 0, subDictionaryType: "" }; + + await expect(instance.addDictionaryRow(rowData)).rejects.toEqual(undefined); + + }, 2000); + + + test('Test delete dictionary row', async () => { + + const deleteRequest = jest.spyOn(ManageDictionaries.prototype, 'deleteDictionaryRequest'); + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + const rowData = { name: "newdict", secondLevelDictionary: 0, subDictionaryType: "string" }; + + await instance.deleteDictionaryRow(rowData); + expect(deleteRequest).toHaveBeenCalledWith("newdict"); + + }, 2000); + + test('Test handle select dictionary row click', async () => { + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + const rowData = { name: "newdict", secondLevelDictionary: 0, subDictionaryType: "string" }; + + instance.handleDictionaryRowClick("event", rowData); + expect(component.state('currentSelectedDictionary')).toEqual("newdict"); + }, 2000); + + test('Test dictionary element row add, update, delete', async () => { + + const rowData = { + createdBy: "admin", + createdDate: "2020-06-15T13:59:20.467381Z", + description: "Description", + name: "Some Elem", + shortName: "someElem", + type: "string", + updatedBy: "admin", + updatedDate: "2020-06-15T13:59:20.467381Z" + }; + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + const badRowData = { + description: "Description", + name: "Some Elem", + shortName: "someElem", + type: "string" + }; + + await instance.clickHandler(); + await instance.getDictionaryElements("test"); + + await expect(instance.addDictionaryElementRow(rowData)).resolves.toEqual(undefined); + await expect(instance.updateDictionaryElementRow(rowData, rowData)).resolves.toEqual(undefined); + await expect(instance.deleteDictionaryElementRow(rowData)).resolves.toEqual(undefined); + }); + + test('Test dictionary element row add with errors', async () => { + + const badRowData = { + description: "", + name: "", + shortName: "some#Elem", + type: "" + }; + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + await expect(instance.addDictionaryElementRow(badRowData)).rejects.toEqual(""); + }); + + test('Test dictionary element update with error illegal name', async () => { + + const badRowData = { + description: "", + name: "test@@", + shortName: "some#Elem", + type: "" + }; + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + await expect(instance.updateDictionaryElementRow(badRowData)).rejects.toEqual(undefined); + }); + + test('Test dictionary element addition with duplicate name error', async () => { + + const badRowData = { + description: "description", + name: "Alert Type", + shortName: "alertType", + type: "string" + }; + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + component.setState({ currentSelectedDictionary: 'test' }); + + await instance.getDictionaryElements(); + await expect(instance.addDictionaryElementRow(badRowData)).rejects.toEqual(""); + }); + + test('Test dictionary element addition with empty name error', async () => { + + const badRowData = { + description: "description", + name: "Alert Type", + shortName: "", + type: "string" + }; + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + component.setState({ currentSelectedDictionary: 'test' }); + + await instance.getDictionaryElements(); + await expect(instance.addDictionaryElementRow(badRowData)).rejects.toEqual(""); + }); + + + it('Test Import CSV Sunny Day', async () => { + + TemplateMenuService.insDictionaryElements = jest.fn().mockImplementation(() => { + return Promise.resolve({ ok: true, status: 200 }); + }); + + let rawCsvData = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsvData += '"alertType","Alert Type","Alert Type Description","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let expectedResult = [ + { + description: "Alert Type Description", + name: "Alert Type", + shortName: "alertType", + subDictionary: "", + type: "string" + } + ]; + + const updateDictionaryElementsRequest = jest.spyOn(ManageDictionaries.prototype, 'updateDictionaryElementsRequest'); + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + await expect(instance.importCsvData(rawCsvData)).toEqual(''); + expect(updateDictionaryElementsRequest).toHaveBeenCalledWith(expectedResult); + }); + + it('Test Import CSV Mandatory Field Check Errors', () => { + + let rawCsvData = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsvData += '"","","","","","",""'; + + // The empty values for all the fields in row 1 of the rawCsvData will trigger a bunch of errors. + // Getting Enzyme to properly match them with embedded newlines turned out to be impossible + // and maybe not desirable anyway; so our test for "success" here is simply that the + // routine returns a non-empty error string. + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + expect(instance.importCsvData(rawCsvData)).not.toEqual(''); + }); + + it('Test Import CSV Errors in Row Data', async () => { + + TemplateMenuService.insDictionaryElements = jest.fn().mockImplementation(() => { + return Promise.resolve({ ok: true, status: 200 }); + }); + + let rawCsvData = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsvData += '"alert@Type","Alert Type","Alert Type Description","strin","subby","admin","2020-06-11T13:56:14.927437Z"'; + + let expectedResult = [ + { + description: "Alert Type Description", + name: "Alert Type", + shortName: "alertType", + subDictionary: "", + type: "string" + } + ]; + + const updateDictionaryElementsRequest = jest.spyOn(ManageDictionaries.prototype, 'updateDictionaryElementsRequest'); + + const component = shallow(<ManageDictionaries/>) + const instance = component.instance(); + + await expect(instance.importCsvData(rawCsvData)).not.toEqual(''); + }); + + + it('Test handleClose', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "name": "vtest", + "secondLevelDictionary": 1, + "subDictionaryType": "string", + "updatedBy": "test", + "updatedDate": "05-07-2019 19:09:42" + }); + } + }); + }); + const handleClose = jest.spyOn(ManageDictionaries.prototype, 'handleClose'); + const component = shallow(<ManageDictionaries history={ historyMock }/>) + component.find('[variant="secondary"]').prop('onClick')(); + expect(handleClose).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + handleClose.mockClear(); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/__snapshots__/ManageDictionaries.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/__snapshots__/ManageDictionaries.test.js.snap new file mode 100644 index 0000000..6b58363 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/ManageDictionaries/__snapshots__/ManageDictionaries.test.js.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify ManageDictionaries Test API Successful 1`] = ` +<Styled(Modal) + backdrop="static" + keyboard={false} + onHide={[Function]} + show={true} + size="xl" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + > + <ModalTitle> + Manage Dictionaries + </ModalTitle> + </ModalHeader> + <ModalBody> + <WithStyles(Component) + columns={ + Array [ + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "editable": "onAdd", + "field": "name", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Dictionary Name", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "secondLevelDictionary", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "lookup": Object { + "0": "No", + "1": "Yes", + }, + "title": "Sub Dictionary ?", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "subDictionaryType", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "lookup": Object { + "number": "number", + "string": "string", + }, + "title": "Dictionary Type", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "editable": "never", + "field": "updatedBy", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Updated By", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "editable": "never", + "field": "updatedDate", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Last Updated Date", + }, + ] + } + editable={ + Object { + "onRowAdd": [Function], + "onRowDelete": [Function], + "onRowUpdate": [Function], + } + } + icons={ + Object { + "Add": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Check": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Clear": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Delete": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "DetailPanel": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Edit": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Export": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Filter": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "FirstPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "LastPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "NextPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "PreviousPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "ResetSearch": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Search": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "SortArrow": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "ThirdStateCheck": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "ViewColumn": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + } + } + onRowClick={[Function]} + options={ + Object { + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "1px solid black", + "fontSize": "15pt", + "text": "bold", + }, + } + } + title="Dictionary List" + /> + </ModalBody> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="null" + variant="secondary" + > + Close + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/dialogs/PerformActions.js b/gui-clamp/ui-react/src/components/dialogs/PerformActions.js new file mode 100644 index 0000000..ba7c99d --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/PerformActions.js @@ -0,0 +1,95 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import LoopActionService from '../../api/LoopActionService'; + + +export default class PerformActions extends React.Component { + state = { + loopName: this.props.loopCache.getLoopName(), + loopAction: this.props.loopAction + }; + + constructor(props, context) { + super(props, context); + this.refreshStatus = this.refreshStatus.bind(this); + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopName: newProps.loopCache.getLoopName(), + loopAction: newProps.loopAction + }); + } + + componentDidMount() { + const action = this.state.loopAction; + const loopName = this.state.loopName; + + if (action === 'delete') { + if (window.confirm('You are about to remove Control Loop Model "' + loopName + + '". Select OK to continue with deletion or Cancel to keep the model.') === false) { + return; + } + } + + this.props.setBusyLoading(); // Alert top level to start block user clicks + + LoopActionService.performAction(loopName, action) + .then(pars => { + this.props.showSucAlert("Action " + action + " successfully performed"); + if (action === 'delete') { + this.props.updateLoopFunction(null); + this.props.history.push('/'); + } else { + // refresh status and update loop logs + this.refreshStatus(loopName); + } + }) + .catch(error => { + this.props.showFailAlert("Action " + action + " failed"); + // refresh status and update loop logs + this.refreshStatus(loopName); + }) + .finally(() => this.props.clearBusyLoading()); + } + + refreshStatus(loopName) { + + this.props.setBusyLoading(); + + LoopActionService.refreshStatus(loopName) + .then(data => { + this.props.updateLoopFunction(data); + this.props.history.push('/'); + }) + .catch(error => { + this.props.history.push('/'); + }) + .finally(() => this.props.clearBusyLoading()); + } + + render() { + return null; + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/PerformActions.test.js b/gui-clamp/ui-react/src/components/dialogs/PerformActions.test.js new file mode 100644 index 0000000..74c9145 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/PerformActions.test.js @@ -0,0 +1,95 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import PerformActions from './PerformActions'; +import LoopCache from '../../api/LoopCache'; +import LoopActionService from '../../api/LoopActionService'; + +describe('Verify PerformActions', () => { + + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca" + }); + + it('Test the render method action failed', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const updateLoopFunction = jest.fn(); + const showSucAlert = jest.fn(); + const showFailAlert = jest.fn(); + const setBusyLoading = jest.fn(); + const clearBusyLoading = jest.fn(); + + LoopActionService.refreshStatus = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + const component = shallow(<PerformActions loopCache={ loopCache } + loopAction="submit" history={ historyMock } updateLoopFunction={ updateLoopFunction } showSucAlert={ showSucAlert } showFailAlert={ showFailAlert } + setBusyLoading={ setBusyLoading } clearBusyLoading={ clearBusyLoading }/>) + await flushPromises(); + component.update(); + + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test the render method action successful', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const updateLoopFunction = jest.fn(); + const showSucAlert = jest.fn(); + const showFailAlert = jest.fn(); + const setBusyLoading = jest.fn(); + const clearBusyLoading = jest.fn(); + + LoopActionService.performAction = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + LoopActionService.refreshStatus = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + const component = shallow(<PerformActions loopCache={ loopCache } + loopAction="submit" history={ historyMock } updateLoopFunction={ updateLoopFunction } showSucAlert={ showSucAlert } showFailAlert={ showFailAlert } + setBusyLoading={ setBusyLoading } clearBusyLoading={ clearBusyLoading }/>) + await flushPromises(); + component.update(); + + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyEditor.js b/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyEditor.js new file mode 100644 index 0000000..525d66b --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyEditor.js @@ -0,0 +1,217 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import PolicyToscaService from '../../../api/PolicyToscaService'; +import { JSONEditor } from '@json-editor/json-editor/dist/nonmin/jsoneditor.js'; +import "@fortawesome/fontawesome-free/css/all.css" +import styled from 'styled-components'; +import Button from 'react-bootstrap/Button'; +import TextField from '@material-ui/core/TextField'; +import Alert from 'react-bootstrap/Alert'; +import PolicyService from '../../../api/PolicyService'; +import OnapUtils from '../../../utils/OnapUtils'; +import uuid from 'react-uuid'; + +//const JSONEditor = require("@json-editor/json-editor").JSONEditor; +const DivWhiteSpaceStyled = styled.div` + white-space: pre; +` + +const JsonEditorDiv = styled.div` + margin-top: 20px; + background-color: ${ props => props.theme.loopViewerBackgroundColor }; + text-align: justify; + font-size: ${ props => props.theme.policyEditorFontSize }; + border: 1px solid #C0C0C0; +` +const PanelDiv = styled.div` + margin-top: 20px; + text-align: justify; + font-size: ${ props => props.theme.policyEditorFontSize }; + background-color: ${ props => props.theme.loopViewerBackgroundColor }; +` + +export default class PolicyEditor extends React.Component { + + state = { + policyModelType: this.props.policyModelType, + policyModelTypeVersion: this.props.policyModelTypeVersion, + policyName: (typeof this.props.policyName !== "undefined") ? this.props.policyName : "org.onap.policy.new", + policyVersion: (typeof this.props.policyVersion !== "undefined") ? this.props.policyVersion : "0.0.1", + policyProperties: this.props.policyProperties, + showSuccessAlert: false, + showFailAlert: false, + jsonEditor: null, + jsonEditorDivId: uuid(), + } + + constructor(props, context) { + super(props, context); + this.createJsonEditor = this.createJsonEditor.bind(this); + this.getToscaModelForPolicy = this.getToscaModelForPolicy.bind(this); + this.disableAlert = this.disableAlert.bind(this); + this.handleCreateNewVersion = this.handleCreateNewVersion.bind(this); + this.handleChangePolicyName = this.handleChangePolicyName.bind(this); + this.handleChangePolicyVersion = this.handleChangePolicyVersion.bind(this); + } + + disableAlert() { + this.setState({ showSuccessAlert: false, showFailAlert: false }); + } + + customValidation(editorData) { + // method for sub-classes to override with customized validation + return []; + } + + handleCreateNewVersion() { + var editorData = this.state.jsonEditor.getValue(); + var errors = this.state.jsonEditor.validate(); + errors = errors.concat(this.customValidation(editorData)); + + if (errors.length !== 0) { + console.error("Errors detected during policy data validation ", errors); + this.setState({ + showFailAlert: true, + showMessage: 'Errors detected during policy data validation:\n' + OnapUtils.jsonEditorErrorFormatter(errors) + }); + return; + } else { + console.info("NO validation errors found in policy data"); + PolicyService.createNewPolicy(this.state.policyModelType, this.state.policyModelTypeVersion, + this.state.policyName, this.state.policyVersion, editorData).then(respPolicyCreation => { + if (typeof (respPolicyCreation) === "undefined") { + //it indicates a failure + this.setState({ + showFailAlert: true, + showMessage: 'Policy Creation Failure' + }); + } else { + this.setState({ + showSuccessAlert: true, + showMessage: 'Policy ' + this.state.policyName + '/' + this.state.policyVersion + ' created successfully' + }); + this.props.policyUpdateFunction(); + } + }) + } + } + + bumpVersion(versionToBump) { + let semVer = versionToBump.split("."); + return parseInt(semVer[0]) + 1 + "." + semVer[1] + "." + semVer[2]; + } + + getToscaModelForPolicy() { + PolicyToscaService.getToscaPolicyModel(this.state.policyModelType, this.state.policyModelTypeVersion).then(respJsonPolicyTosca => { + if (respJsonPolicyTosca !== {}) { + this.setState({ + jsonSchemaPolicyTosca: respJsonPolicyTosca, + jsonEditor: this.createJsonEditor(respJsonPolicyTosca, this.state.policyProperties), + }) + } + }); + } + + componentDidMount() { + this.getToscaModelForPolicy(); + } + + createJsonEditor(toscaModel, editorData) { + /*JSONEditor.defaults.themes.myBootstrap4 = JSONEditor.defaults.themes.bootstrap4.extend({ + getTab: function(text,tabId) { + var liel = document.createElement('li'); + liel.classList.add('nav-item'); + var ael = document.createElement("a"); + ael.classList.add("nav-link"); + ael.setAttribute("style",'padding:10px;max-width:160px;'); + ael.setAttribute("href", "#" + tabId); + ael.setAttribute('data-toggle', 'tab'); + text.setAttribute("style",'word-wrap:break-word;'); + ael.appendChild(text); + liel.appendChild(ael); + return liel; + } + });*/ + + return new JSONEditor(document.getElementById(this.state.jsonEditorDivId), + { + schema: toscaModel, + startval: editorData, + //theme: 'myBootstrap4', + theme: 'bootstrap4', + iconlib: 'fontawesome5', + object_layout: 'grid', + disable_properties: false, + disable_edit_json: false, + disable_array_reorder: true, + disable_array_delete_last_row: true, + disable_array_delete_all_rows: false, + array_controls_top: true, + keep_oneof_values: false, + collapsed: true, + show_errors: 'always', + display_required_only: false, + show_opt_in: false, + prompt_before_delete: true, + required_by_default: false + }) + } + + handleChangePolicyName(event) { + this.setState({ + policyName: event.target.value, + }); + } + + handleChangePolicyVersion(event) { + this.setState({ + policyVersion: event.target.value, + }); + } + + render() { + return ( + <PanelDiv> + <Alert variant="success" show={ this.state.showSuccessAlert } onClose={ this.disableAlert } dismissible> + <DivWhiteSpaceStyled> + { this.state.showMessage } + </DivWhiteSpaceStyled> + </Alert> + <Alert variant="danger" show={ this.state.showFailAlert } onClose={ this.disableAlert } dismissible> + <DivWhiteSpaceStyled> + { this.state.showMessage } + </DivWhiteSpaceStyled> + </Alert> + <TextField required id="policyName" label="Required" defaultValue={ this.state.policyName } + onChange={ this.handleChangePolicyName } variant="outlined" size="small"/> + <TextField required id="policyVersion" label="Required" defaultValue={ this.state.policyVersion } + onChange={ this.handleChangePolicyVersion } size="small" variant="outlined"/> + <Button variant="secondary" title="Create a new policy version from the defined parameters" + onClick={ this.handleCreateNewVersion }>Create New Version</Button> + <JsonEditorDiv id={ this.state.jsonEditorDivId } title="Policy Properties"/> + </PanelDiv> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyModal.js b/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyModal.js new file mode 100644 index 0000000..105b2c5 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyModal.js @@ -0,0 +1,364 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; +import Select from 'react-select'; +import Modal from 'react-bootstrap/Modal'; +import styled from 'styled-components'; +import LoopService from '../../../api/LoopService'; +import LoopCache from '../../../api/LoopCache'; +import { JSONEditor } from '@json-editor/json-editor/dist/jsoneditor.js'; +import "@fortawesome/fontawesome-free/css/all.css" +import Alert from 'react-bootstrap/Alert'; +import OnapConstant from '../../../utils/OnapConstants'; +import OnapUtils from '../../../utils/OnapUtils'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` + +const DivWhiteSpaceStyled = styled.div` + white-space: pre; +` + +export default class PolicyModal extends React.Component { + + state = { + show: true, + loopCache: this.props.loopCache, + jsonEditor: null, + policyName: this.props.match.params.policyName, + // This is to indicate whether it's an operational or config policy (in terms of loop instance) + policyInstanceType: this.props.match.params.policyInstanceType, + pdpGroup: null, + pdpGroupList: [], + pdpSubgroupList: [], + chosenPdpGroup: '', + chosenPdpSubgroup: '', + showSucAlert: false, + showFailAlert: false + }; + + constructor(props, context) { + super(props, context); + this.handleClose = this.handleClose.bind(this); + this.handleSave = this.handleSave.bind(this); + this.renderJsonEditor = this.renderJsonEditor.bind(this); + this.handlePdpGroupChange = this.handlePdpGroupChange.bind(this); + this.handlePdpSubgroupChange = this.handlePdpSubgroupChange.bind(this); + this.createJsonEditor = this.createJsonEditor.bind(this); + this.handleRefresh = this.handleRefresh.bind(this); + this.disableAlert = this.disableAlert.bind(this); + this.renderPdpGroupDropDown = this.renderPdpGroupDropDown.bind(this); + this.renderOpenLoopMessage = this.renderOpenLoopMessage.bind(this); + this.renderModalTitle = this.renderModalTitle.bind(this); + this.readOnly = props.readOnly !== undefined ? props.readOnly : false; + } + + handleSave() { + var editorData = this.state.jsonEditor.getValue(); + var errors = this.state.jsonEditor.validate(); + errors = errors.concat(this.customValidation(editorData, this.state.loopCache.getTemplateName())); + + if (errors.length !== 0) { + console.error("Errors detected during policy data validation ", errors); + this.setState({ + showFailAlert: true, + showMessage: 'Errors detected during policy data validation:\n' + OnapUtils.jsonEditorErrorFormatter(errors) + }); + return; + } else { + console.info("NO validation errors found in policy data"); + if (this.state.policyInstanceType === OnapConstant.microServiceType) { + this.state.loopCache.updateMicroServiceProperties(this.state.policyName, editorData); + this.state.loopCache.updateMicroServicePdpGroup(this.state.policyName, this.state.chosenPdpGroup, this.state.chosenPdpSubgroup); + LoopService.setMicroServiceProperties(this.state.loopCache.getLoopName(), this.state.loopCache.getMicroServiceForName(this.state.policyName)).then(resp => { + this.setState({ show: false }); + this.props.history.push('/'); + this.props.loadLoopFunction(this.state.loopCache.getLoopName()); + }); + } else if (this.state.policyInstanceType === OnapConstant.operationalPolicyType) { + this.state.loopCache.updateOperationalPolicyProperties(this.state.policyName, editorData); + this.state.loopCache.updateOperationalPolicyPdpGroup(this.state.policyName, this.state.chosenPdpGroup, this.state.chosenPdpSubgroup); + LoopService.setOperationalPolicyProperties(this.state.loopCache.getLoopName(), this.state.loopCache.getOperationalPolicies()).then(resp => { + this.setState({ show: false }); + this.props.history.push('/'); + this.props.loadLoopFunction(this.state.loopCache.getLoopName()); + }); + } + } + } + + customValidation(editorData, templateName) { + // method for sub-classes to override with customized validation + return []; + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + componentDidMount() { + this.renderJsonEditor(); + } + + componentDidUpdate() { + if (this.state.showSucAlert === true || this.state.showFailAlert === true) { + let modalElement = document.getElementById("policyModal") + if (modalElement) { + modalElement.scrollTo(0, 0); + } + } + } + + createJsonEditor(toscaModel, editorData) { + /*JSONEditor.defaults.themes.myBootstrap4 = JSONEditor.defaults.themes.bootstrap4.extend({ + getTab: function(text,tabId) { + var liel = document.createElement('li'); + liel.classList.add('nav-item'); + var ael = document.createElement("a"); + ael.classList.add("nav-link"); + ael.setAttribute("style",'padding:10px;max-width:160px;'); + ael.setAttribute("href", "#" + tabId); + ael.setAttribute('data-toggle', 'tab'); + text.setAttribute("style",'word-wrap:break-word;'); + ael.appendChild(text); + liel.appendChild(ael); + return liel; + } + });*/ + return new JSONEditor(document.getElementById("editor"), + { + schema: toscaModel, + startval: editorData, + theme: 'bootstrap4', + iconlib: 'fontawesome5', + object_layout: 'grid', + disable_properties: false, + disable_edit_json: false, + disable_array_reorder: true, + disable_array_delete_last_row: true, + disable_array_delete_all_rows: false, + array_controls_top: true, + keep_oneof_values: false, + collapsed: true, + show_errors: 'always', + display_required_only: false, + show_opt_in: false, + prompt_before_delete: true, + required_by_default: false + }) + } + + renderJsonEditor() { + console.debug("Rendering PolicyModal ", this.state.policyName); + var toscaModel = {}; + var editorData = {}; + var pdpGroupValues = {}; + var chosenPdpGroupValue, chosenPdpSubgroupValue; + if (this.state.policyInstanceType === OnapConstant.microServiceType) { + toscaModel = this.state.loopCache.getMicroServiceJsonRepresentationForName(this.state.policyName); + editorData = this.state.loopCache.getMicroServicePropertiesForName(this.state.policyName); + pdpGroupValues = this.state.loopCache.getMicroServiceSupportedPdpGroup(this.state.policyName); + chosenPdpGroupValue = this.state.loopCache.getMicroServicePdpGroup(this.state.policyName); + chosenPdpSubgroupValue = this.state.loopCache.getMicroServicePdpSubgroup(this.state.policyName); + } else if (this.state.policyInstanceType === OnapConstant.operationalPolicyType) { + toscaModel = this.state.loopCache.getOperationalPolicyJsonRepresentationForName(this.state.policyName); + editorData = this.state.loopCache.getOperationalPolicyPropertiesForName(this.state.policyName); + pdpGroupValues = this.state.loopCache.getOperationalPolicySupportedPdpGroup(this.state.policyName); + chosenPdpGroupValue = this.state.loopCache.getOperationalPolicyPdpGroup(this.state.policyName); + chosenPdpSubgroupValue = this.state.loopCache.getOperationalPolicyPdpSubgroup(this.state.policyName); + } + + if (toscaModel == null) { + return; + } + + var pdpSubgroupValues = []; + if (typeof (chosenPdpGroupValue) !== "undefined") { + var selectedPdpGroup = pdpGroupValues.filter(entry => (Object.keys(entry)[0] === chosenPdpGroupValue)); + pdpSubgroupValues = selectedPdpGroup[0][chosenPdpGroupValue].map((pdpSubgroup) => { + return { label: pdpSubgroup, value: pdpSubgroup } + }); + } + this.setState({ + jsonEditor: this.createJsonEditor(toscaModel, editorData), + pdpGroup: pdpGroupValues, + pdpGroupList: pdpGroupValues.map(entry => { + return { label: Object.keys(entry)[0], value: Object.keys(entry)[0] }; + }), + pdpSubgroupList: pdpSubgroupValues, + chosenPdpGroup: chosenPdpGroupValue, + chosenPdpSubgroup: chosenPdpSubgroupValue + }) + } + + handlePdpGroupChange(e) { + var selectedPdpGroup = this.state.pdpGroup.filter(entry => (Object.keys(entry)[0] === e.value)); + const pdpSubgroupValues = selectedPdpGroup[0][e.value].map((pdpSubgroup) => { + return { label: pdpSubgroup, value: pdpSubgroup } + }); + if (this.state.chosenPdpGroup !== e.value) { + this.setState({ + chosenPdpGroup: e.value, + chosenPdpSubgroup: '', + pdpSubgroupList: pdpSubgroupValues + }); + } + } + + handlePdpSubgroupChange(e) { + this.setState({ chosenPdpSubgroup: e.value }); + } + + handleRefresh() { + var newLoopCache, toscaModel, editorData; + if (this.state.policyInstanceType === OnapConstant.microServiceType) { + LoopService.refreshMicroServicePolicyJson(this.state.loopCache.getLoopName(), this.state.policyName).then(data => { + newLoopCache = new LoopCache(data); + toscaModel = newLoopCache.getMicroServiceJsonRepresentationForName(this.state.policyName); + editorData = newLoopCache.getMicroServicePropertiesForName(this.state.policyName); + document.getElementById("editor").innerHTML = ""; + this.setState({ + loopCache: newLoopCache, + jsonEditor: this.createJsonEditor(toscaModel, editorData), + showSucAlert: true, + showMessage: "Successfully refreshed" + }); + }) + .catch(error => { + console.error("Error while refreshing the Operational Policy Json Representation"); + this.setState({ + showFailAlert: true, + showMessage: "Refreshing of UI failed" + }); + }); + } else if (this.state.policyInstanceType === OnapConstant.operationalPolicyType) { + LoopService.refreshOperationalPolicyJson(this.state.loopCache.getLoopName(), this.state.policyName).then(data => { + var newLoopCache = new LoopCache(data); + toscaModel = newLoopCache.getOperationalPolicyJsonRepresentationForName(this.state.policyName); + editorData = newLoopCache.getOperationalPolicyPropertiesForName(this.state.policyName); + document.getElementById("editor").innerHTML = ""; + this.setState({ + loopCache: newLoopCache, + jsonEditor: this.createJsonEditor(toscaModel, editorData), + showSucAlert: true, + showMessage: "Successfully refreshed" + }); + }) + .catch(error => { + console.error("Error while refreshing the Operational Policy Json Representation"); + this.setState({ + showFailAlert: true, + showMessage: "Refreshing of UI failed" + }); + }); + } + } + + disableAlert() { + this.setState({ showSucAlert: false, showFailAlert: false }); + } + + renderPdpGroupDropDown() { + if (this.state.policyInstanceType !== OnapConstant.operationalPolicyType || !this.state.loopCache.isOpenLoopTemplate()) { + return ( + <Form.Group as={ Row } controlId="formPlaintextEmail"> + <Form.Label column sm="2">Pdp Group Info</Form.Label> + <Col sm="3"> + <Select value={ { label: this.state.chosenPdpGroup, value: this.state.chosenPdpGroup } } onChange={ this.handlePdpGroupChange } options={ this.state.pdpGroupList }/> + </Col> + <Col sm="3"> + <Select value={ { label: this.state.chosenPdpSubgroup, value: this.state.chosenPdpSubgroup } } onChange={ this.handlePdpSubgroupChange } options={ this.state.pdpSubgroupList }/> + </Col> + </Form.Group> + ); + } + } + + renderOpenLoopMessage() { + if (this.state.policyInstanceType === OnapConstant.operationalPolicyType && this.state.loopCache.isOpenLoopTemplate()) { + return ( + "Operational Policy cannot be configured as only Open Loop is supported for this Template!" + ); + } + } + + renderModalTitle() { + return ( + <Modal.Title>Edit the policy</Modal.Title> + ); + } + + renderButton() { + var allElement = [(<Button variant="secondary" onClick={ this.handleClose }> + Close + </Button>)]; + if (this.state.policyInstanceType !== OnapConstant.operationalPolicyType || !this.state.loopCache.isOpenLoopTemplate()) { + allElement.push(( + <Button variant="primary" disabled={ this.readOnly } onClick={ this.handleSave }> + Save Changes + </Button> + )); + allElement.push(( + <Button variant="primary" disabled={ this.readOnly } onClick={ this.handleRefresh }> + Refresh + </Button> + )); + } + return allElement; + } + + render() { + return ( + <ModalStyled size="xl" backdrop="static" keyboard={ false } show={ this.state.show } onHide={ this.handleClose }> + <Modal.Header closeButton> + { this.renderModalTitle() } + </Modal.Header> + <Alert variant="success" show={ this.state.showSucAlert } onClose={ this.disableAlert } dismissible> + <DivWhiteSpaceStyled> + { this.state.showMessage } + </DivWhiteSpaceStyled> + </Alert> + <Alert variant="danger" show={ this.state.showFailAlert } onClose={ this.disableAlert } dismissible> + <DivWhiteSpaceStyled> + { this.state.showMessage } + </DivWhiteSpaceStyled> + </Alert> + <Modal.Body> + { this.renderOpenLoopMessage() } + <div id="editor"/> + { this.renderPdpGroupDropDown() } + </Modal.Body> + <Modal.Footer> + { this.renderButton() } + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyModal.test.js new file mode 100644 index 0000000..734f22e --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Policy/PolicyModal.test.js @@ -0,0 +1,128 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { mount } from 'enzyme'; +import PolicyModal from './PolicyModal'; +import LoopCache from '../../../api/LoopCache'; +import LoopService from '../../../api/LoopService'; +import OnapConstant from '../../../utils/OnapConstants'; + +describe('Verify PolicyModal', () => { + beforeEach(() => { + fetch.resetMocks(); + fetch.mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + text: () => "OK" + }); + }); + }) + const loopCacheStr = { + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "operationalPolicies": [{ + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", + "configurationsJson": { + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }, + "policyModel": { "policyPdpGroup": { "supportedPdpGroups": [{ "monitoring": ["xacml"] }] } }, + "jsonRepresentation": { "schema": {} } + }] + }; + const loopCache = new LoopCache(loopCacheStr); + const historyMock = { push: jest.fn() }; + const flushPromises = () => new Promise(setImmediate); + const match = { params: { policyName: "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", policyInstanceType: OnapConstant.operationalPolicyType } } + + it('Test handleClose', () => { + const handleClose = jest.spyOn(PolicyModal.prototype, 'handleClose'); + const component = mount(<PolicyModal history={ historyMock } match={ match } loopCache={ loopCache }/>) + + component.find('[variant="secondary"]').prop('onClick')(); + + expect(handleClose).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test handleSave', async () => { + const loadLoopFunction = jest.fn(); + const handleSave = jest.spyOn(PolicyModal.prototype, 'handleSave'); + const component = mount(<PolicyModal history={ historyMock } + loopCache={ loopCache } match={ match } loadLoopFunction={ loadLoopFunction }/>) + + component.find('[variant="primary"]').get(0).props.onClick(); + await flushPromises(); + component.update(); + + expect(handleSave).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test handleRefresh', async () => { + LoopService.refreshOperationalPolicyJson = jest.fn().mockImplementation(() => { + return Promise.resolve(loopCacheStr); + }); + const updateLoopFunction = jest.fn(); + const handleRefresh = jest.spyOn(PolicyModal.prototype, 'handleRefresh'); + const component = mount(<PolicyModal loopCache={ loopCache } match={ match } updateLoopFunction={ updateLoopFunction }/>) + + component.find('[variant="primary"]').get(1).props.onClick(); + await flushPromises(); + component.update(); + + expect(handleRefresh).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(true); + expect(component.state('showSucAlert')).toEqual(true); + expect(component.state('showMessage')).toEqual("Successfully refreshed"); + }); + + it('Test handlePdpGroupChange', () => { + const component = mount(<PolicyModal loopCache={ loopCache } match={ match }/>) + component.setState({ + "pdpGroup": [{ "option1": ["subPdp1", "subPdp2"] }], + "chosenPdpGroup": "option2" + }); + expect(component.state('chosenPdpGroup')).toEqual("option2"); + + const instance = component.instance(); + const event = { label: "option1", value: "option1" } + instance.handlePdpGroupChange(event); + expect(component.state('chosenPdpGroup')).toEqual("option1"); + expect(component.state('chosenPdpSubgroup')).toEqual(""); + expect(component.state('pdpSubgroupList')).toEqual([{ label: "subPdp1", value: "subPdp1" }, { label: "subPdp2", value: "subPdp2" }]); + }); + + it('Test handlePdpSubgroupChange', () => { + const component = mount(<PolicyModal loopCache={ loopCache } match={ match }/>) + + const instance = component.instance(); + const event = { label: "option1", value: "option1" } + instance.handlePdpSubgroupChange(event); + expect(component.state('chosenPdpSubgroup')).toEqual("option1"); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Policy/ToscaViewer.js b/gui-clamp/ui-react/src/components/dialogs/Policy/ToscaViewer.js new file mode 100644 index 0000000..ce4edce --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Policy/ToscaViewer.js @@ -0,0 +1,67 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import PolicyToscaService from '../../../api/PolicyToscaService'; +import styled from 'styled-components'; +import Button from 'react-bootstrap/Button'; + +const JsonEditorDiv = styled.div` + margin-top: 20px; + background-color: ${ props => props.theme.toscaTextareaBackgroundColor }; + text-align: justify; + font-size: ${ props => props.theme.toscaTextareaFontSize }; + width: 100%; + height: 30%; +` + +export default class ToscaViewer extends React.Component { + + state = { + toscaData: this.props.toscaData, + yamlPolicyTosca: this.getToscaModelYamlFor(this.props.toscaData), + } + + constructor(props, context) { + super(props, context); + this.getToscaModelYamlFor = this.getToscaModelYamlFor.bind(this); + } + + getToscaModelYamlFor(toscaData) { + PolicyToscaService.getToscaPolicyModelYaml(toscaData["policyModelType"], toscaData["version"]).then(respYamlPolicyTosca => { + this.setState({ + yamlPolicyTosca: respYamlPolicyTosca, + }) + }); + } + + render() { + return ( + <JsonEditorDiv> + <pre>{ this.state.yamlPolicyTosca }</pre> + <Button variant="secondary" title="Create a new policy version from the defined parameters" + onClick={ this.handleCreateNewVersion }>Create New Version</Button> + </JsonEditorDiv> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Policy/ViewAllPolicies.js b/gui-clamp/ui-react/src/components/dialogs/Policy/ViewAllPolicies.js new file mode 100644 index 0000000..a97deea --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Policy/ViewAllPolicies.js @@ -0,0 +1,427 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React, { forwardRef } from 'react' +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import styled from 'styled-components'; +import AddBox from '@material-ui/icons/AddBox'; +import ArrowDownward from '@material-ui/icons/ArrowDownward'; +import Check from '@material-ui/icons/Check'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded'; +import Edit from '@material-ui/icons/Edit'; +import FilterList from '@material-ui/icons/FilterList'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Remove from '@material-ui/icons/Remove'; +import SaveAlt from '@material-ui/icons/SaveAlt'; +import Search from '@material-ui/icons/Search'; +import ViewColumn from '@material-ui/icons/ViewColumn'; +import DehazeIcon from '@material-ui/icons/Dehaze'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; +import AddIcon from '@material-ui/icons/Add'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Switch from '@material-ui/core/Switch'; +import MaterialTable from "material-table"; +import PolicyService from '../../../api/PolicyService'; +import PolicyToscaService from '../../../api/PolicyToscaService'; +import Select from 'react-select'; +import Alert from 'react-bootstrap/Alert'; +import Tabs from 'react-bootstrap/Tabs'; +import Tab from 'react-bootstrap/Tab'; +import PolicyEditor from './PolicyEditor'; +import ToscaViewer from './ToscaViewer'; + +const DivWhiteSpaceStyled = styled.div` + white-space: pre; +` + +const ModalStyled = styled(Modal)` + @media (min-width: 1000px) { + .modal-xl { + max-width: 96%; + } + } + background-color: transparent; +` +const DetailedRow = styled.div` + margin: 0 auto; + background-color: ${ props => props.theme.policyEditorBackgroundColor }; + font-size: ${ props => props.theme.policyEditorFontSize }; + width: 97%; + margin-left: auto; + margin-right: 0; +` + + +const standardCellStyle = { backgroundColor: '#039be5', color: '#FFF', border: '1px solid black' }; +const cellPdpGroupStyle = { backgroundColor: '#039be5', color: '#FFF', border: '1px solid black' }; +const headerStyle = { backgroundColor: '#ddd', border: '2px solid black' }; +const rowHeaderStyle = { backgroundColor: '#ddd', fontSize: '15pt', text: 'bold', border: '1px solid black' }; + +export default class ViewAllPolicies extends React.Component { + state = { + show: true, + content: 'Please select a policy to display it', + selectedRowId: -1, + policiesListData: [], + toscaModelsListData: [], + jsonEditorForPolicy: new Map(), + prefixGrouping: false, + showSuccessAlert: false, + showFailAlert: false, + policyColumnsDefinition: [ + { + title: "Policy Name", field: "name", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Version", field: "version", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Type", field: "type", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Type Version", field: "type_version", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Deployed in PDP", field: "pdpGroupInfo.pdpGroup", + cellStyle: cellPdpGroupStyle, + headerStyle: headerStyle, + render: rowData => this.renderPdpGroupDropBox(rowData), + grouping: false + }, + { + title: "PDP Group", field: "pdpGroupInfo.pdpGroup", + cellStyle: cellPdpGroupStyle, + headerStyle: headerStyle + }, + { + title: "PDP SubGroup", field: "pdpGroupInfo.pdpSubGroup", + cellStyle: cellPdpGroupStyle, + headerStyle: headerStyle + } + ], + toscaColumnsDefinition: [ + { + title: "Policy Model Type", field: "policyModelType", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Policy Acronym", field: "policyAcronym", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Version", field: "version", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Uploaded By", field: "updatedBy", + cellStyle: standardCellStyle, + headerStyle: headerStyle + }, + { + title: "Uploaded Date", field: "updatedDate", editable: 'never', + cellStyle: standardCellStyle, + headerStyle: headerStyle + } + ], + tableIcons: { + Add: forwardRef((props, ref) => <AddBox { ...props } ref={ ref }/>), + Check: forwardRef((props, ref) => <Check { ...props } ref={ ref }/>), + Clear: forwardRef((props, ref) => <Clear { ...props } ref={ ref }/>), + Delete: forwardRef((props, ref) => <DeleteRoundedIcon { ...props } ref={ ref }/>), + DetailPanel: forwardRef((props, ref) => <ChevronRight { ...props } ref={ ref }/>), + Edit: forwardRef((props, ref) => <Edit { ...props } ref={ ref }/>), + Export: forwardRef((props, ref) => <SaveAlt { ...props } ref={ ref }/>), + Filter: forwardRef((props, ref) => <FilterList { ...props } ref={ ref }/>), + FirstPage: forwardRef((props, ref) => <FirstPage { ...props } ref={ ref }/>), + LastPage: forwardRef((props, ref) => <LastPage { ...props } ref={ ref }/>), + NextPage: forwardRef((props, ref) => <ChevronRight { ...props } ref={ ref }/>), + PreviousPage: forwardRef((props, ref) => <ChevronLeft { ...props } ref={ ref }/>), + ResetSearch: forwardRef((props, ref) => <Clear { ...props } ref={ ref }/>), + Search: forwardRef((props, ref) => <Search { ...props } ref={ ref }/>), + SortArrow: forwardRef((props, ref) => <ArrowDownward { ...props } ref={ ref }/>), + ThirdStateCheck: forwardRef((props, ref) => <Remove { ...props } ref={ ref }/>), + ViewColumn: forwardRef((props, ref) => <ViewColumn { ...props } ref={ ref }/>) + } + }; + + constructor(props, context) { + super(props, context); + this.handleClose = this.handleClose.bind(this); + this.renderPdpGroupDropBox = this.renderPdpGroupDropBox.bind(this); + this.handlePdpGroupChange = this.handlePdpGroupChange.bind(this); + this.handlePrefixGrouping = this.handlePrefixGrouping.bind(this); + this.handleDeletePolicy = this.handleDeletePolicy.bind(this); + this.disableAlert = this.disableAlert.bind(this); + this.getAllPolicies = this.getAllPolicies.bind(this); + this.getAllToscaModels = this.getAllToscaModels.bind(this); + this.getAllPolicies(); + this.getAllToscaModels(); + } + + getAllToscaModels() { + PolicyToscaService.getToscaPolicyModels().then(toscaModelsList => { + this.setState({ toscaModelsListData: toscaModelsList }); + }); + } + + handlePdpGroupChange(e) { + let pdpSplit = e.value.split("/"); + let selectedPdpGroup = pdpSplit[0]; + let selectedSubPdpGroup = pdpSplit[1]; + if (typeof selectedSubPdpGroup !== "undefined") { + let temp = this.state.policiesListData; + temp[this.state.selectedRowId]["pdpGroupInfo"] = { "pdpGroup": selectedPdpGroup, "pdpSubGroup": selectedSubPdpGroup }; + this.setState({ policiesListData: temp }); + } else { + delete this.state.policiesListData[this.state.selectedRowId]["pdpGroupInfo"]; + } + } + + renderPdpGroupDropBox(dataRow) { + let optionItems = [{ label: "NOT DEPLOYED", value: "NOT DEPLOYED" }]; + let selectedItem = { label: "NOT DEPLOYED", value: "NOT DEPLOYED" }; + if (typeof dataRow.supportedPdpGroups !== "undefined") { + for (const pdpGroup of dataRow["supportedPdpGroups"]) { + for (const pdpSubGroup of Object.values(pdpGroup)[0]) { + optionItems.push({ + label: Object.keys(pdpGroup)[0] + "/" + pdpSubGroup, + value: Object.keys(pdpGroup)[0] + "/" + pdpSubGroup + }); + } + } + } + if (typeof dataRow.pdpGroupInfo !== "undefined") { + selectedItem = { + label: dataRow["pdpGroupInfo"]["pdpGroup"] + "/" + dataRow["pdpGroupInfo"]["pdpSubGroup"], + value: dataRow["pdpGroupInfo"]["pdpGroup"] + "/" + dataRow["pdpGroupInfo"]["pdpSubGroup"] + }; + } + return (<div style={ { width: '250px' } }><Select value={ selectedItem } options={ optionItems } onChange={ this.handlePdpGroupChange }/></div>); + } + + getAllPolicies() { + PolicyService.getPoliciesList().then(allPolicies => { + this.setState({ policiesListData: allPolicies["policies"] }) + }); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/') + } + + handlePrefixGrouping(event) { + this.setState({ prefixGrouping: event.target.checked }); + } + + handleDeletePolicy(event, rowData) { + PolicyService.deletePolicy(rowData["type"], rowData["type_version"], rowData["name"], rowData["version"]).then( + respPolicyDeletion => { + if (typeof (respPolicyDeletion) === "undefined") { + //it indicates a failure + this.setState({ + showFailAlert: true, + showMessage: 'Policy Deletion Failure' + }); + } else { + this.setState({ + showSuccessAlert: true, + showMessage: 'Policy successfully Deleted' + }); + } + this.getAllPolicies(); + } + ) + } + + disableAlert() { + this.setState({ showSuccessAlert: false, showFailAlert: false }); + } + + renderPoliciesTab() { + return ( + <Tab eventKey="policies" title="Policies in Policy Framework"> + <Modal.Body> + <FormControlLabel + control={ <Switch checked={ this.state.prefixGrouping } onChange={ this.handlePrefixGrouping }/> } + label="Group by prefix" + /> + <MaterialTable + title={ "Policies" } + data={ this.state.policiesListData } + columns={ this.state.policyColumnsDefinition } + icons={ this.state.tableIcons } + onRowClick={ (event, rowData, togglePanel) => togglePanel() } + options={ { + grouping: true, + exportButton: true, + headerStyle: rowHeaderStyle, + rowStyle: rowData => ({ + backgroundColor: (this.state.selectedRowId !== -1 && this.state.selectedRowId === rowData.tableData.id) ? '#EEE' : '#FFF' + }), + actionsColumnIndex: -1 + } } + detailPanel={ [ + { + icon: ArrowForwardIosIcon, + tooltip: 'Show Configuration', + render: rowData => { + return ( + <DetailedRow> + <PolicyEditor policyModelType={ rowData["type"] } policyModelTypeVersion={ rowData["type_version"] } policyName={ rowData["name"] } policyVersion={ rowData["version"] } + policyProperties={ rowData["properties"] } policyUpdateFunction={ this.getAllPolicies }/> + </DetailedRow> + ) + }, + }, + { + icon: DehazeIcon, + tooltip: 'Show Raw Data', + render: rowData => { + return ( + <DetailedRow> + <pre>{ JSON.stringify(rowData, null, 2) }</pre> + </DetailedRow> + ) + }, + }, + ] } + actions={ [ + { + icon: forwardRef((props, ref) => <DeleteRoundedIcon { ...props } ref={ ref }/>), + tooltip: 'Delete Policy', + onClick: (event, rowData) => this.handleDeletePolicy(event, rowData) + } + ] } + /> + </Modal.Body> + </Tab> + ); + } + + renderToscaTab() { + return ( + <Tab eventKey="tosca models" title="Tosca Models in Policy Framework"> + <Modal.Body> + <FormControlLabel + control={ <Switch checked={ this.state.prefixGrouping } onChange={ this.handlePrefixGrouping }/> } + label="Group by prefix" + /> + <MaterialTable + title={ "Tosca Models" } + data={ this.state.toscaModelsListData } + columns={ this.state.toscaColumnsDefinition } + icons={ this.state.tableIcons } + onRowClick={ (event, rowData, togglePanel) => togglePanel() } + options={ { + grouping: true, + exportButton: true, + headerStyle: rowHeaderStyle, + rowStyle: rowData => ({ + backgroundColor: (this.state.selectedRowId !== -1 && this.state.selectedRowId === rowData.tableData.id) ? '#EEE' : '#FFF' + }), + actionsColumnIndex: -1 + } } + detailPanel={ [ + { + icon: ArrowForwardIosIcon, + tooltip: 'Show Tosca', + render: rowData => { + return ( + <DetailedRow> + <ToscaViewer toscaData={ rowData }/> + </DetailedRow> + ) + }, + }, + { + icon: DehazeIcon, + tooltip: 'Show Raw Data', + render: rowData => { + return ( + <DetailedRow> + <pre>{ JSON.stringify(rowData, null, 2) }</pre> + </DetailedRow> + ) + }, + }, + { + icon: AddIcon, + tooltip: 'Create a policy from this model', + render: rowData => { + return ( + <DetailedRow> + <PolicyEditor policyModelType={ rowData["policyModelType"] } policyModelTypeVersion={ rowData["version"] } policyProperties={ {} } policyUpdateFunction={ this.getAllPolicies }/> + </DetailedRow> + ) + }, + }, + ] } + /> + </Modal.Body> + </Tab> + ); + } + + render() { + return ( + <ModalStyled size="xl" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + </Modal.Header> + <Tabs id="controlled-tab-example" activeKey={ this.state.key } onSelect={ key => this.setState({ key, selectedRowData: {} }) }> + { this.renderPoliciesTab() } + { this.renderToscaTab() } + </Tabs> + <Alert variant="success" show={ this.state.showSuccessAlert } onClose={ this.disableAlert } dismissible> + <DivWhiteSpaceStyled> + { this.state.showMessage } + </DivWhiteSpaceStyled> + </Alert> + <Alert variant="danger" show={ this.state.showFailAlert } onClose={ this.disableAlert } dismissible> + <DivWhiteSpaceStyled> + { this.state.showMessage } + </DivWhiteSpaceStyled> + </Alert> + <Modal.Footer> + <Button variant="secondary" onClick={ this.handleClose }>Close</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/RefreshStatus.js b/gui-clamp/ui-react/src/components/dialogs/RefreshStatus.js new file mode 100644 index 0000000..e23ab3f --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/RefreshStatus.js @@ -0,0 +1,65 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import LoopActionService from '../../api/LoopActionService'; +import Spinner from 'react-bootstrap/Spinner'; +import styled from 'styled-components'; + +const StyledSpinnerDiv = styled.div` + justify-content: center !important; + display: flex !important; +`; + +export default class RefreshStatus extends React.Component { + state = { + loopName: this.props.loopCache.getLoopName() + }; + + componentWillReceiveProps(newProps) { + this.setState({ + loopName: newProps.loopCache.getLoopName() + }); + } + + componentDidMount() { + // refresh status and update loop logs + LoopActionService.refreshStatus(this.state.loopName).then(data => { + this.props.showSucAlert("Status successfully refreshed"); + this.props.updateLoopFunction(data); + this.props.history.push('/'); + }) + .catch(error => { + this.props.showFailAlert("Status refreshing failed"); + this.props.history.push('/'); + }); + } + + render() { + return ( + <StyledSpinnerDiv> + <Spinner animation="border" role="status"> + </Spinner> + </StyledSpinnerDiv> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/RefreshStatus.test.js b/gui-clamp/ui-react/src/components/dialogs/RefreshStatus.test.js new file mode 100644 index 0000000..7736ffd --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/RefreshStatus.test.js @@ -0,0 +1,72 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import RefreshStatus from './RefreshStatus'; +import LoopCache from '../../api/LoopCache'; +import LoopActionService from '../../api/LoopActionService'; + +describe('Verify RefreshStatus', () => { + + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca" + }); + + it('Test refresh status failed', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const showSucAlert = jest.fn(); + const showFailAlert = jest.fn(); + + const component = shallow(<RefreshStatus loopCache={ loopCache } history={ historyMock } showSucAlert={ showSucAlert } showFailAlert={ showFailAlert }/>) + await flushPromises(); + component.update(); + + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + + it('Test refresh status successful', async () => { + const flushPromises = () => new Promise(setImmediate); + const historyMock = { push: jest.fn() }; + const updateLoopFunction = jest.fn(); + const showSucAlert = jest.fn(); + const showFailAlert = jest.fn(); + + LoopActionService.refreshStatus = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + } + }); + }); + + const component = shallow(<RefreshStatus loopCache={ loopCache } + loopAction="submit" history={ historyMock } updateLoopFunction={ updateLoopFunction } showSucAlert={ showSucAlert } showFailAlert={ showFailAlert }/>) + await flushPromises(); + component.update(); + + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + }); + +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Tosca/ViewLoopTemplatesModal.js b/gui-clamp/ui-react/src/components/dialogs/Tosca/ViewLoopTemplatesModal.js new file mode 100644 index 0000000..7257337 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Tosca/ViewLoopTemplatesModal.js @@ -0,0 +1,173 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React, { forwardRef } from 'react' +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import styled from 'styled-components'; +import TemplateService from '../../../api/TemplateService'; +import ArrowUpward from '@material-ui/icons/ArrowUpward'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Search from '@material-ui/icons/Search'; +import MaterialTable from "material-table"; +import LoopCache from '../../../api/LoopCache'; +import SvgGenerator from '../../loop_viewer/svg/SvgGenerator'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` + +const cellStyle = { border: '1px solid black' }; +const headerStyle = { backgroundColor: '#ddd', border: '2px solid black' }; +const rowHeaderStyle = { backgroundColor: '#ddd', fontSize: '15pt', text: 'bold', border: '1px solid black' }; + +export default class ViewLoopTemplatesModal extends React.Component { + state = { + show: true, + content: 'Please select a loop template to display it', + selectedRow: -1, + loopTemplatesData: [], + fakeLoopCacheWithTemplate: new LoopCache({}), + loopTemplateColumnsDefinition: [ + { + title: "#", field: "index", render: rowData => rowData.tableData.id + 1, + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Template Name", field: "name", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Service Model Name", field: "modelService.serviceDetails.name", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Loop Type Allowed", field: "allowedLoopType", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "# Instances Allowed", field: "maximumInstancesAllowed", + cellStyle: cellStyle, + headerStyle: headerStyle + }, + { + title: "Modified Date", field: "updatedDate", editable: 'never', + cellStyle: cellStyle, + headerStyle: headerStyle + } + ], + tableIcons: { + FirstPage: forwardRef((props, ref) => <FirstPage { ...props } ref={ ref }/>), + LastPage: forwardRef((props, ref) => <LastPage { ...props } ref={ ref }/>), + NextPage: forwardRef((props, ref) => <ChevronRight { ...props } ref={ ref }/>), + PreviousPage: forwardRef((props, ref) => <ChevronLeft { ...props } ref={ ref }/>), + ResetSearch: forwardRef((props, ref) => <Clear { ...props } ref={ ref }/>), + Search: forwardRef((props, ref) => <Search { ...props } ref={ ref }/>), + SortArrow: forwardRef((props, ref) => <ArrowUpward { ...props } ref={ ref }/>) + } + }; + + constructor(props, context) { + super(props, context); + this.handleClose = this.handleClose.bind(this); + this.renderSvg = this.renderSvg.bind(this); + this.getLoopTemplate = this.getLoopTemplate.bind(this); + this.getAllLoopTemplates(); + } + + getAllLoopTemplates() { + TemplateService.getAllLoopTemplates().then(templatesData => { + // replace -1 in maximumInstancesAllowed with more meaningful 'No Limit' + for (let item in templatesData) { + if (templatesData[item].maximumInstancesAllowed === -1) { + templatesData[item].maximumInstancesAllowed = 'No Limit'; + } + } + this.setState({ loopTemplatesData: templatesData }) + }); + } + + getLoopTemplate(templateIdInDataArray) { + if (typeof templateIdInDataArray !== "undefined") { + this.setState({ + fakeLoopCacheWithTemplate: + new LoopCache({ + "loopTemplate": this.state.loopTemplatesData[templateIdInDataArray], + "name": "fakeLoop" + }) + }) + } else { + this.setState({ fakeLoopCacheWithTemplate: new LoopCache({}) }) + } + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/') + } + + renderSvg() { + return ( + <SvgGenerator loopCache={ this.state.fakeLoopCacheWithTemplate } clickable={ false } generatedFrom={ SvgGenerator.GENERATED_FROM_TEMPLATE }/> + ) + } + + render() { + return ( + <ModalStyled size="xl" show={ this.state.show } onHide={ this.handleClose } backdrop="static" keyboard={ false }> + <Modal.Header closeButton> + </Modal.Header> + <Modal.Body> + <MaterialTable + title={ "View Blueprint MicroService Templates" } + data={ this.state.loopTemplatesData } + columns={ this.state.loopTemplateColumnsDefinition } + icons={ this.state.tableIcons } + onRowClick={ (event, rowData) => { + this.getLoopTemplate(rowData.tableData.id); + this.setState({ selectedRow: rowData.tableData.id }) + } } + options={ { + headerStyle: rowHeaderStyle, + rowStyle: rowData => ({ + backgroundColor: (this.state.selectedRow !== -1 && this.state.selectedRow === rowData.tableData.id) ? '#EEE' : '#FFF' + }) + } } + /> + { this.renderSvg() } + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={ this.handleClose }>Close</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/Tosca/ViewLoopTemplatesModal.test.js b/gui-clamp/ui-react/src/components/dialogs/Tosca/ViewLoopTemplatesModal.test.js new file mode 100644 index 0000000..d93f0b0 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Tosca/ViewLoopTemplatesModal.test.js @@ -0,0 +1,163 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import ViewLoopTemplatesModal from './ViewLoopTemplatesModal'; +import { mount } from 'enzyme'; +import { BrowserRouter as Router } from 'react-router-dom'; + +describe('Verify ViewLoopTemplatesModal', () => { + beforeEach(() => { + fetch.resetMocks(); + }); + + it('Test API Successful', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "index": "1", + "name": "MTCA version 1", + "modelService.serviceDetails.name": "MTCA", + "allowedLoopType": "CLOSED", + "maximumInstancesAllowed": 1, + "updatedDate": "2019-09-06 19:09:42" + }); + } + }); + }); + const component = shallow(<ViewLoopTemplatesModal/>); + }); + + it('Test API Exception', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: false, + status: 500, + json: () => { + return Promise.resolve({ + "index": "1", + "name": "MTCA version 1", + "modelService.serviceDetails.name": "MTCA", + "allowedLoopType": "CLOSED", + "maximumInstancesAllowed": 1, + "updatedDate": "2019-09-06 19:09:42" + }); + } + }); + }); + const component = shallow(<ViewLoopTemplatesModal/>); + }); + + it('Test API Rejection', () => { + const myMockFunc = fetch.mockImplementationOnce(() => Promise.reject('error')); + setTimeout(() => myMockFunc().catch(e => { + console.info(e); + }), + 100 + ); + const component = shallow(<ViewLoopTemplatesModal/>); + expect(myMockFunc.mock.calls.length).toBe(1); + }); + + it('Test the tosca model view render method', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "index": "1", + "name": "MTCA version 1", + "modelService.serviceDetails.name": "MTCA", + "allowedLoopType": "CLOSED", + "maximumInstancesAllowed": 1, + "updatedDate": "2019-09-06 19:09:42" + }); + } + }); + }); + const component = shallow(<ViewLoopTemplatesModal/>); + component.setState({ + loopTemplateData: { + "index": "1", + "name": "MTCA version 1", + "modelService.serviceDetails.name": "MTCA", + "allowedLoopType": "CLOSED", + "maximumInstancesAllowed": 1, + "updatedDate": "2019-09-06 19:09:42" + } + }); + expect(component).toMatchSnapshot(); + }); + + it('Test Table icons', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "index": "1", + "name": "MTCA version 1", + "modelService.serviceDetails.name": "MTCA", + "allowedLoopType": "CLOSED", + "maximumInstancesAllowed": 1, + "updatedDate": "2019-09-06 19:09:42" + }); + } + }); + }); + const component = mount(<Router><ViewLoopTemplatesModal/></Router>); + expect(component.find('[className="MuiSelect-icon MuiTablePagination-selectIcon"]')).toBeTruthy(); + }); + + it('Test handleClose', () => { + fetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "index": "1", + "name": "MTCA version 1", + "modelService.serviceDetails.name": "MTCA", + "allowedLoopType": "CLOSED", + "maximumInstancesAllowed": 1, + "updatedDate": "2019-09-06 19:09:42" + }); + } + }); + }); + const historyMock = { push: jest.fn() }; + const handleClose = jest.spyOn(ViewLoopTemplatesModal.prototype, 'handleClose'); + const component = shallow(<ViewLoopTemplatesModal history={ historyMock }/>) + component.find('[variant="secondary"]').prop('onClick')(); + expect(handleClose).toHaveBeenCalledTimes(1); + expect(component.state('show')).toEqual(false); + expect(historyMock.push.mock.calls[0]).toEqual(['/']); + handleClose.mockClear(); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/Tosca/__snapshots__/ViewLoopTemplatesModal.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/Tosca/__snapshots__/ViewLoopTemplatesModal.test.js.snap new file mode 100644 index 0000000..73f6596 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/Tosca/__snapshots__/ViewLoopTemplatesModal.test.js.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify ViewLoopTemplatesModal Test the tosca model view render method 1`] = ` +<Styled(Modal) + backdrop="static" + keyboard={false} + onHide={[Function]} + show={true} + size="xl" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + /> + <ModalBody> + <WithStyles(Component) + columns={ + Array [ + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "index", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "render": [Function], + "title": "#", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "name", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Template Name", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "modelService.serviceDetails.name", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Service Model Name", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "allowedLoopType", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Loop Type Allowed", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "field": "maximumInstancesAllowed", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "# Instances Allowed", + }, + Object { + "cellStyle": Object { + "border": "1px solid black", + }, + "editable": "never", + "field": "updatedDate", + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "2px solid black", + }, + "title": "Modified Date", + }, + ] + } + data={Array []} + icons={ + Object { + "FirstPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "LastPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "NextPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "PreviousPage": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "ResetSearch": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "Search": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "SortArrow": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + } + } + onRowClick={[Function]} + options={ + Object { + "headerStyle": Object { + "backgroundColor": "#ddd", + "border": "1px solid black", + "fontSize": "15pt", + "text": "bold", + }, + "rowStyle": [Function], + } + } + title="View Blueprint MicroService Templates" + /> + <withRouter(SvgGenerator) + clickable={false} + generatedFrom="TEMPLATE" + loopCache={ + LoopCache { + "loopJsonCache": Object {}, + } + } + /> + </ModalBody> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + variant="secondary" + > + Close + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/dialogs/UserInfoModal.js b/gui-clamp/ui-react/src/components/dialogs/UserInfoModal.js new file mode 100644 index 0000000..e58c61a --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/UserInfoModal.js @@ -0,0 +1,115 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Form from 'react-bootstrap/Form'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import styled from 'styled-components'; +import UserService from '../../api/UserService'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` + +export default class UserInfoModal extends React.Component { + + constructor(props, context) { + super(props, context); + + this.handleClose = this.handleClose.bind(this); + this.renderPermissions = this.renderPermissions.bind(this); + this.renderUserName = this.renderUserName.bind(this); + this.state = { + show: true, + userInfo: {} + }; + } + + componentWillMount() { + UserService.getUserInfo().then(userInfo => { + this.setState({ userInfo: userInfo }) + }); + } + + handleClose() { + this.props.history.push('/'); + } + + renderPermissions() { + if (this.state.userInfo["allPermissions"]) { + var listOfPermissions = this.state.userInfo["allPermissions"].map(function (perm) { + return <Form.Control plaintext readOnly defaultValue={ perm }/>; + }) + return listOfPermissions; + } else { + return; + } + } + + renderUserName() { + if (this.state.userInfo["userName"]) { + return <Form.Control plaintext readOnly defaultValue={ this.state.userInfo["userName"] }/> + } else { + return; + } + } + + renderVersion() { + if (this.state.userInfo["cldsVersion"]) { + return <Form.Control plaintext readOnly defaultValue={ this.state.userInfo["cldsVersion"] }/> + } else { + return; + } + } + + render() { + return ( + <ModalStyled size="lg" show={ this.state.show } onHide={ this.handleClose }> + <Modal.Header closeButton> + <Modal.Title>User Info</Modal.Title> + </Modal.Header> + <Modal.Body> + <Form.Group as={ Row } controlId="userName"> + <Form.Label column sm="3">Current User:</Form.Label> + <Col>{ this.renderUserName() }</Col> + </Form.Group> + <Form.Group as={ Row } controlId="cldsVersion"> + <Form.Label column sm="3">CLDS Version:</Form.Label> + <Col>{ this.renderVersion() }</Col> + </Form.Group> + <Form.Group as={ Row } controlId="userPermissions"> + <Form.Label column sm="3">User Permissions:</Form.Label> + <Col> + { this.renderPermissions() } + </Col> + </Form.Group> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" type="null" onClick={ this.handleClose }>Cancel</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/dialogs/UserInfoModal.test.js b/gui-clamp/ui-react/src/components/dialogs/UserInfoModal.test.js new file mode 100644 index 0000000..7168792 --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/UserInfoModal.test.js @@ -0,0 +1,84 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import UserInfoModal from './UserInfoModal'; + +describe('Verify UserInfoModal', () => { + + beforeEach(() => { + fetch.resetMocks(); + fetch.mockImplementation(() => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => { + return Promise.resolve({ + "userName": "test", + "cldsVersion": "1.0.0" + }); + } + }); + }); + }) + + it('Test the render method full permission', () => { + const component = shallow(<UserInfoModal/>) + component.setState({ + userInfo: { + "userName": "test", + "cldsVersion": "1.0.0", + "allPermissions": ["permission1", "permission2"] + } + }); + expect(component).toMatchSnapshot(); + }); + + it('Test the render method no permission', () => { + const component = shallow(<UserInfoModal/>) + component.setState({ + userInfo: {} + }); + + expect(component.find('FormControl').length).toEqual(0); + }); + + it('Test the render method read permission', () => { + const component = shallow(<UserInfoModal/>) + component.setState({ + userInfo: { + "userName": "test", + "cldsVersion": "1.0.0", + "allPermissions": ["permission1", "permission2"] + } + }); + + expect(component.find('FormControl').length).toEqual(4); + + const forms = component.find('FormControl'); + expect(forms.get(0).props.defaultValue).toEqual("test"); + expect(forms.get(1).props.defaultValue).toEqual("1.0.0"); + expect(forms.get(2).props.defaultValue).toEqual("permission1"); + expect(forms.get(3).props.defaultValue).toEqual("permission2"); + }); +}); diff --git a/gui-clamp/ui-react/src/components/dialogs/__snapshots__/UserInfoModal.test.js.snap b/gui-clamp/ui-react/src/components/dialogs/__snapshots__/UserInfoModal.test.js.snap new file mode 100644 index 0000000..7c725bc --- /dev/null +++ b/gui-clamp/ui-react/src/components/dialogs/__snapshots__/UserInfoModal.test.js.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify UserInfoModal Test the render method full permission 1`] = ` +<Styled(Modal) + onHide={[Function]} + show={true} + size="lg" +> + <ModalHeader + closeButton={true} + closeLabel="Close" + > + <ModalTitle> + User Info + </ModalTitle> + </ModalHeader> + <ModalBody> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="userName" + > + <FormLabel + column={true} + sm="3" + srOnly={false} + > + Current User: + </FormLabel> + <Col> + <FormControl + defaultValue="test" + plaintext={true} + readOnly={true} + /> + </Col> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="cldsVersion" + > + <FormLabel + column={true} + sm="3" + srOnly={false} + > + CLDS Version: + </FormLabel> + <Col> + <FormControl + defaultValue="1.0.0" + plaintext={true} + readOnly={true} + /> + </Col> + </FormGroup> + <FormGroup + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "noGutters": false, + }, + "render": [Function], + } + } + controlId="userPermissions" + > + <FormLabel + column={true} + sm="3" + srOnly={false} + > + User Permissions: + </FormLabel> + <Col> + <FormControl + defaultValue="permission1" + plaintext={true} + readOnly={true} + /> + <FormControl + defaultValue="permission2" + plaintext={true} + readOnly={true} + /> + </Col> + </FormGroup> + </ModalBody> + <ModalFooter> + <Button + active={false} + disabled={false} + onClick={[Function]} + type="null" + variant="secondary" + > + Cancel + </Button> + </ModalFooter> +</Styled(Modal)> +`; diff --git a/gui-clamp/ui-react/src/components/loop_viewer/logs/LoopLogs.js b/gui-clamp/ui-react/src/components/loop_viewer/logs/LoopLogs.js new file mode 100644 index 0000000..b03b740 --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/logs/LoopLogs.js @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import Table from 'react-bootstrap/Table'; +import LoopCache from '../../../api/LoopCache'; +import styled from 'styled-components'; + +const LoopLogsHeaderDivStyled = styled.div` + background-color: ${ props => props.theme.loopLogsHeaderBackgroundColor }; + padding: 10px 10px; + color: ${ props => props.theme.loopLogsHeaderFontColor }; +` +const TableStyled = styled(Table)` + + overflow: auto; +` +const TableRow = ({ logRow }) => ( + <tr> + <td>{ logRow.logInstant }</td> + <td>{ logRow.logType }</td> + <td>{ logRow.logComponent }</td> + <td>{ logRow.message }</td> + </tr> + +) + +export default class LoopLogs extends React.Component { + + state = { + loopCache: new LoopCache({}) + } + + constructor(props) { + super(props); + this.renderLogs = this.renderLogs.bind(this); + this.state.loopCache = props.loopCache; + } + + shouldComponentUpdate(nextProps, nextState) { + return this.state.loopCache !== nextState.loopCache; + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopCache: newProps.loopCache + }); + } + + renderLogs() { + if (this.state.loopCache.getLoopLogsArray() != null) { + return ( + this.state.loopCache.getLoopLogsArray().map(row => <TableRow logRow={ row }/>) + ) + } + } + + render() { + return ( + <LoopLogsHeaderDivStyled> + <label>Loop Logs</label> + <TableStyled striped hover variant responsive> + <thead> + <tr> + <th><span align="left">Date</span></th> + <th><span align="left">Type</span></th> + <th><span align="left">Component</span></th> + <th><span align="right">Log</span></th> + </tr> + </thead> + <tbody> + { this.renderLogs() } + </tbody> + </TableStyled> + </LoopLogsHeaderDivStyled> + + ); + } +} diff --git a/gui-clamp/ui-react/src/components/loop_viewer/logs/LoopLogs.test.js b/gui-clamp/ui-react/src/components/loop_viewer/logs/LoopLogs.test.js new file mode 100644 index 0000000..d3a21d8 --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/logs/LoopLogs.test.js @@ -0,0 +1,70 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import LoopLogs from './LoopLogs'; +import LoopCache from '../../../api/LoopCache'; + +describe('Verify LoopLogs', () => { + + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "loopLogs": [ + { + "id": 1, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational policies UPDATED", + "logInstant": "2019-07-08T09:44:37Z" + } + ] + }); + + it('Test the render method', () => { + const component = shallow(<LoopLogs loopCache={ loopCache }/>) + expect(component).toMatchSnapshot(); + + const loopCacheUpdated = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "loopLogs": [ + { + "id": 1, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational policies UPDATED", + "logInstant": "2019-07-08T09:44:37Z" + }, + { + "id": 2, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational policies UPDATED", + "logInstant": "2019-07-08T09:44:50Z" + } + ] + }); + + component.setProps({ loopCache: loopCacheUpdated }); + expect(component.find('TableRow').length).toEqual(2); + }); +}); diff --git a/gui-clamp/ui-react/src/components/loop_viewer/logs/__snapshots__/LoopLogs.test.js.snap b/gui-clamp/ui-react/src/components/loop_viewer/logs/__snapshots__/LoopLogs.test.js.snap new file mode 100644 index 0000000..75b817b --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/logs/__snapshots__/LoopLogs.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify LoopLogs Test the render method 1`] = ` +<styled.div> + <label> + Loop Logs + </label> + <Styled(Component) + hover={true} + responsive={true} + striped={true} + variant={true} + > + <thead> + <tr> + <th> + <span + align="left" + > + Date + </span> + </th> + <th> + <span + align="left" + > + Type + </span> + </th> + <th> + <span + align="left" + > + Component + </span> + </th> + <th> + <span + align="right" + > + Log + </span> + </th> + </tr> + </thead> + <tbody> + <TableRow + logRow={ + Object { + "id": 1, + "logComponent": "CLAMP", + "logInstant": "2019-07-08T09:44:37Z", + "logType": "INFO", + "message": "Operational policies UPDATED", + } + } + /> + </tbody> + </Styled(Component)> +</styled.div> +`; diff --git a/gui-clamp/ui-react/src/components/loop_viewer/status/LoopStatus.js b/gui-clamp/ui-react/src/components/loop_viewer/status/LoopStatus.js new file mode 100644 index 0000000..2994c84 --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/status/LoopStatus.js @@ -0,0 +1,109 @@ +/*- +* ============LICENSE_START======================================================= +* ONAP CLAMP +* ================================================================================ +* Copyright (C) 2019 AT&T Intellectual Property. 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. +* ============LICENSE_END============================================ +* =================================================================== +* +*/ +import React from 'react'; +import Table from 'react-bootstrap/Table'; +import styled from 'styled-components'; +import LoopCache from '../../../api/LoopCache'; + +const LoopStatusViewDivStyled = styled.div` + background-color: ${ props => props.theme.loopViewerHeaderBackgroundColor }; + padding: 10px 10px; + color: ${ props => props.theme.loopViewerHeaderFontColor }; +` + +const TableStyled = styled(Table)` + overflow: auto; +` + +const TableRow = ({ statusRow }) => ( + <tr> + <td>{ statusRow.componentName }</td> + <td>{ statusRow.stateName }</td> + <td>{ statusRow.description }</td> + </tr> + +) + +export default class LoopStatus extends React.Component { + state = { + loopCache: new LoopCache({}) + } + + constructor(props) { + super(props); + this.renderStatus = this.renderStatus.bind(this); + this.state.loopCache = props.loopCache; + } + + + renderStatus() { + if (this.state.loopCache.getComponentStates() != null) { + return Object.keys(this.state.loopCache.getComponentStates()).map((key) => { + console.debug("Adding status for: ", key); + var res = {} + res[key] = this.state.loopCache.getComponentStates()[key]; + return (<TableRow statusRow={ { + 'componentName': key, + 'stateName': this.state.loopCache.getComponentStates()[key].componentState.stateName, + 'description': this.state.loopCache.getComponentStates()[key].componentState.description + } }/>) + }) + + } + } + + shouldComponentUpdate(nextProps, nextState) { + return this.state.loopCache !== nextState.loopCache; + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopCache: newProps.loopCache + }); + } + + render() { + return ( + <LoopStatusViewDivStyled> + <label>Loop Status: { this.state.loopCache.getComputedState() } + </label> + + <div> + <TableStyled striped hover variant responsive> + <thead> + <tr> + <th><span align="left">Component Name</span></th> + <th><span align="left">Component State</span></th> + <th><span align="right">Description</span></th> + </tr> + </thead> + <tbody> + { this.renderStatus() } + </tbody> + </TableStyled> + </div> + </LoopStatusViewDivStyled> + ); + } +} + diff --git a/gui-clamp/ui-react/src/components/loop_viewer/status/LoopStatus.test.js b/gui-clamp/ui-react/src/components/loop_viewer/status/LoopStatus.test.js new file mode 100644 index 0000000..b84067e --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/status/LoopStatus.test.js @@ -0,0 +1,78 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import LoopStatus from './LoopStatus'; +import LoopCache from '../../../api/LoopCache'; + +describe('Verify LoopStatus', () => { + + const loopCache = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "lastComputedState": "DESIGN", + "components": { + "POLICY": { + "componentState": { + "stateName": "NOT_SENT", + "description": "The policies defined have NOT yet been created on the policy engine" + } + }, + "DCAE": { + "componentState": { + "stateName": "BLUEPRINT_DEPLOYED", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop" + } + } + } + }); + + it('Test the render method', () => { + const component = shallow(<LoopStatus loopCache={ loopCache }/>) + + expect(component).toMatchSnapshot(); + + const loopCacheUpdated = new LoopCache({ + "name": "LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca", + "lastComputedState": "SUBMIT", + "components": { + "POLICY": { + "componentState": { + "stateName": "SENT", + "description": "The policies defined have NOT yet been created on the policy engine" + } + }, + "DCAE": { + "componentState": { + "stateName": "BLUEPRINT_DEPLOYED", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop" + } + } + } + }); + component.setProps({ loopCache: loopCacheUpdated }); + + const forms = component.find('TableRow'); + expect(forms.get(0).props.statusRow.stateName).toEqual("SENT"); + expect(component.find('label').text()).toContain('SUBMIT'); + }); +}); diff --git a/gui-clamp/ui-react/src/components/loop_viewer/status/__snapshots__/LoopStatus.test.js.snap b/gui-clamp/ui-react/src/components/loop_viewer/status/__snapshots__/LoopStatus.test.js.snap new file mode 100644 index 0000000..73da5ff --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/status/__snapshots__/LoopStatus.test.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify LoopStatus Test the render method 1`] = ` +<styled.div> + <label> + Loop Status: + DESIGN + </label> + <div> + <Styled(Component) + hover={true} + responsive={true} + striped={true} + variant={true} + > + <thead> + <tr> + <th> + <span + align="left" + > + Component Name + </span> + </th> + <th> + <span + align="left" + > + Component State + </span> + </th> + <th> + <span + align="right" + > + Description + </span> + </th> + </tr> + </thead> + <tbody> + <TableRow + statusRow={ + Object { + "componentName": "POLICY", + "description": "The policies defined have NOT yet been created on the policy engine", + "stateName": "NOT_SENT", + } + } + /> + <TableRow + statusRow={ + Object { + "componentName": "DCAE", + "description": "The DCAE blueprint has been found in the DCAE inventory but not yet instancianted for this loop", + "stateName": "BLUEPRINT_DEPLOYED", + } + } + /> + </tbody> + </Styled(Component)> + </div> +</styled.div> +`; diff --git a/gui-clamp/ui-react/src/components/loop_viewer/svg/SvgGenerator.js b/gui-clamp/ui-react/src/components/loop_viewer/svg/SvgGenerator.js new file mode 100644 index 0000000..f5f5047 --- /dev/null +++ b/gui-clamp/ui-react/src/components/loop_viewer/svg/SvgGenerator.js @@ -0,0 +1,246 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import React from 'react' +import styled from 'styled-components'; +import { withRouter } from "react-router-dom"; +import LoopCache from '../../../api/LoopCache'; +import OnapConstant from '../../../utils/OnapConstants'; + +const DivStyled = styled.div` + overflow-x: scroll; + display: flex; + width: 100%; + height: 100%; +` + +const emptySvg = (<svg> <text x="60" y="40">No LOOP (SVG)</text> </svg>); + +class SvgGenerator extends React.Component { + boxWidth = 200; + boxHeight = 100; + boxSpace = 50; + + static GENERATED_FROM_INSTANCE = "INSTANCE"; + static GENERATED_FROM_TEMPLATE = "TEMPLATE"; + + state = { + loopCache: new LoopCache({}), + clickable: false, + generatedFrom: SvgGenerator.GENERATED_FROM_INSTANCE, // INSTANCE / TEMPLATE + } + + constructor(props) { + super(props); + this.state.loopCache = props.loopCache; + this.state.clickable = props.clickable; + this.state.generatedFrom = props.generatedFrom; + this.handleSvgClick = this.handleSvgClick.bind(this); + this.renderSvg = this.renderSvg.bind(this); + } + + shouldComponentUpdate(nextProps, nextState) { + return this.state.loopCache !== nextState.loopCache; + } + + componentWillReceiveProps(newProps) { + if (this.state.loopCache !== newProps.loopCache) { + this.setState({ + loopCache: newProps.loopCache, + }); + } + } + + handleSvgClick(event) { + console.debug("svg click event received"); + if (this.state.clickable) { + var elementName = event.target.parentNode.getAttribute('policyId'); + console.info("SVG element clicked", elementName); + // Only allow movement to policy editing IF there busyLoadingCOunt is 0, + // meaning we are not waiting for refreshStatus to complete, for example + if (elementName !== null && !this.props.isBusyLoading()) { + this.props.history.push("/policyModal/"+event.target.parentNode.getAttribute('policyType')+"/"+elementName); + } + } + } + + createVesBox (xPos) { + return this.createOneBox(xPos,null,null,'VES Collector','VES',null); + } + + createOneArrow(xPos) { + return ( + <svg width={this.boxSpace} height={this.boxHeight} x={xPos}> + <defs> + <marker viewBox="0 0 20 20" markerWidth="20" markerHeight="20" orient="auto" refX="8.5" refY="5" id="arrow"> + <path d="m 1 5 l 0 -3 l 7 3 l -7 3 z" + stroke-width= "1" stroke-linecap= "butt" stroke-dasharray= "10000, 1" + fill="#000000" stroke="#000000" /> + </marker> + </defs> + <line x1="0" y1="50%" x2="100%" y2="50%" stroke-width="2" color="black" stroke="black" marker-end="url(#arrow)"/> + </svg> + ); + } + + createBeginCircle(xPos, text) { + return ( + <svg width={this.boxWidth} height={this.boxHeight} x={xPos}> + <circle cx={this.boxWidth-30} cy="50%" r="30" stroke-width="1" color="black" stroke="black" fill="#27ae60"/> + <text x={this.boxWidth-30} y="50%" text-anchor="middle" dominant-baseline="middle" textLength="20%" lengthAdjust="spacingAndGlyphs" >{text}</text> + </svg> + ); + } + + createEndCircle(xPos, text) { + return ( + <svg width={this.boxWidth} height={this.boxHeight} x={xPos}> + <circle cx={30} cy="50%" r="30" stroke-width="2" color="black" stroke="black" fill="#27ae60"/> + <text x={30} y="50%" text-anchor="middle" dominant-baseline="middle" textLength="20%" lengthAdjust="spacingAndGlyphs" >{text}</text> + </svg> + ); + } + + createOneBox(xPos, policyId, loopElementModelId , name, title, policyType) { + return ( + <svg width={this.boxWidth} height={this.boxHeight} x={xPos} title="test"> + <g policyId={policyId} loopElementModelId={loopElementModelId} policyType={policyType}> + <rect width="100%" height="100%" stroke-width="2" color="black" stroke="black" fill="#1abc9c"/> + <text x="50%" y="15%" color="white" fill="white" dominant-baseline="middle" text-anchor="middle" textLength="50%" lengthAdjust="spacingAndGlyphs">{title}</text> + <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" textLength="80%" lengthAdjust="spacingAndGlyphs" >{name}</text> + <text x="50%" y="80%" text-anchor="middle" dominant-baseline="middle" textLength="110%" lengthAdjust="spacingAndGlyphs" >{policyId}</text> + </g> + </svg> + ); + } + + createSvgFromTemplate() { + const allElements = []; + var xPos = 0; + + allElements.push(this.createBeginCircle(xPos,"Start")) + xPos+=(this.boxWidth+this.boxSpace); + + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + + allElements.push(this.createVesBox(xPos)); + xPos+=(this.boxWidth+this.boxSpace); + + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + //createOneBox(xPos, policyId, loopElementModelId , name, title, policyType) + for (var loopElement of this.state.loopCache.getAllLoopElementModels()) { + + allElements.push(this.createOneBox(xPos, + loopElement['name'], + loopElement['name'], + loopElement['shortName'], + loopElement['loopElementType'], + loopElement['loopElementType'])) + xPos+=(this.boxWidth+this.boxSpace); + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + } + + allElements.push(this.createEndCircle(xPos, "End")) + xPos+=(this.boxWidth+this.boxSpace); + + return allElements; + } + + createSvgFromInstance() { + const allElements = []; + var xPos = 0; + + allElements.push(this.createBeginCircle(xPos,"Start")) + xPos+=(this.boxWidth+this.boxSpace); + + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + + allElements.push(this.createVesBox(xPos)); + xPos+=(this.boxWidth+this.boxSpace); + + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + + for (var msPolicy in this.state.loopCache.getMicroServicePolicies()) { + var loopElementModelName = this.state.loopCache.getMicroServicePolicies()[msPolicy]['loopElementModel']; + if (loopElementModelName !== undefined) { + loopElementModelName = loopElementModelName['name']; + } + allElements.push(this.createOneBox(xPos, + this.state.loopCache.getMicroServicePolicies()[msPolicy]['name'], + loopElementModelName, + this.state.loopCache.getMicroServicePolicies()[msPolicy]['policyModel']['policyAcronym'], + 'microservice', + OnapConstant.microServiceType)) + xPos+=(this.boxWidth+this.boxSpace); + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + } + + for (var opPolicy in this.state.loopCache.getOperationalPolicies()) { + loopElementModelName = this.state.loopCache.getOperationalPolicies()[opPolicy]['loopElementModel']; + if (loopElementModelName !== undefined) { + loopElementModelName = loopElementModelName['name']; + } + allElements.push(this.createOneBox(xPos, + this.state.loopCache.getOperationalPolicies()[opPolicy]['name'], + loopElementModelName, + this.state.loopCache.getOperationalPolicies()[opPolicy]['policyModel']['policyAcronym'], + 'operational', + OnapConstant.operationalPolicyType)) + xPos+=(this.boxWidth+this.boxSpace); + allElements.push(this.createOneArrow(xPos-this.boxSpace)); + } + + allElements.push(this.createEndCircle(xPos, "End")) + xPos+=(this.boxWidth+this.boxSpace); + + return allElements; + } + + renderSvg() { + if (this.state.loopCache.getLoopName() === undefined) { + return [emptySvg]; + } + if (this.state.generatedFrom === SvgGenerator.GENERATED_FROM_INSTANCE) { + return this.createSvgFromInstance(); + } else if (this.state.generatedFrom === SvgGenerator.GENERATED_FROM_TEMPLATE) { + return this.createSvgFromTemplate(); + } + } + + render() { + var allTheElements = this.renderSvg(); + var svgWidth = this.boxWidth*allTheElements.length; + var svgHeight = this.boxHeight+50; + return ( + + <DivStyled onClick={this.handleSvgClick} > + <svg height={svgHeight} width={svgWidth} viewBox="0,0,{svgWidth},{svgHeight}" preserveAspectRatio="none"> + <svg x="-50" y="25"> + {allTheElements} + </svg> + </svg> + </DivStyled> + ); + } +} + +export default withRouter(SvgGenerator); diff --git a/gui-clamp/ui-react/src/components/menu/MenuBar.js b/gui-clamp/ui-react/src/components/menu/MenuBar.js new file mode 100644 index 0000000..8956c8b --- /dev/null +++ b/gui-clamp/ui-react/src/components/menu/MenuBar.js @@ -0,0 +1,128 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import Nav from 'react-bootstrap/Nav'; +import Navbar from 'react-bootstrap/Navbar'; +import NavDropdown from 'react-bootstrap/NavDropdown'; +import OnapConstants from '../../utils/OnapConstants'; +import 'bootstrap-css-only/css/bootstrap.min.css'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; + +const StyledLink = styled(Link)` + color: ${ props => props.theme.menuFontColor }; + background-color: ${ props => props.theme.menuBackgroundColor }; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ${ props => props.theme.menuHighlightedBackgroundColor }; + color: ${ props => props.theme.menuHighlightedFontColor }; + } +`; +const StyledNavLink = styled(Nav.Link)` + color: ${ props => props.theme.menuFontColor }; + background-color: ${ props => props.theme.menuBackgroundColor }; + font-weight: normal; + padding: .25rem 1.5rem; + + :hover { + background-color: ${ props => props.theme.menuHighlightedBackgroundColor }; + color: ${ props => props.theme.menuHighlightedFontColor }; + } +`; + +const StyledNavDropdown = styled(NavDropdown)` + color: ${ props => props.theme.menuFontColor }; + + & .dropdown-toggle { + color: ${ props => props.theme.menuFontColor }; + background-color: ${ props => props.theme.backgroundColor }; + font-weight: normal; + + :hover { + font-weight: bold; + } + } +`; + +export default class MenuBar extends React.Component { + state = { + loopName: this.props.loopName, + disabled: true + }; + + componentWillReceiveProps(newProps) { + if (newProps.loopName !== OnapConstants.defaultLoopName) { + this.setState({ disabled: false }); + } else { + this.setState({ disabled: true }); + } + } + + render() { + return ( + <Navbar.Collapse> + <StyledNavDropdown title="POLICY Framework"> + <NavDropdown.Item as={ StyledLink } to="/viewAllPolicies">View All Policies</NavDropdown.Item> + </StyledNavDropdown> + <StyledNavDropdown title="CLAMP Options"> + <NavDropdown.Item as={ StyledLink } to="/manageDictionaries">Tosca Metadata Dictionaries</NavDropdown.Item> + <NavDropdown.Divider/> + <NavDropdown.Item as={ StyledLink } to="/viewLoopTemplatesModal">View All Loop Templates</NavDropdown.Item> + </StyledNavDropdown> + <StyledNavDropdown title="LOOP Instance"> + <NavDropdown.Item as={ StyledLink } to="/createLoop">Create</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/openLoop">Open</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/closeLoop" disabled={ this.state.disabled }>Close</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/modifyLoop" disabled={ this.state.disabled }>Modify</NavDropdown.Item> + <NavDropdown.Divider/> + <NavDropdown.Item as={ StyledLink } to="/loopProperties" disabled={ this.state.disabled }>Properties</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/refreshStatus" disabled={ this.state.disabled }>Refresh Status</NavDropdown.Item> + </StyledNavDropdown> + <StyledNavDropdown title="LOOP Operations"> + <NavDropdown.Item as={ StyledLink } to="/submit" disabled={ this.state.disabled }>Create and deploy to Policy Framework (SUBMIT)</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/stop" disabled={ this.state.disabled }>Undeploy from Policy Framework (STOP)</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/restart" disabled={ this.state.disabled }>ReDeploy to Policy Framework (RESTART)</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/delete" disabled={ this.state.disabled }>Delete loop instance (DELETE)</NavDropdown.Item> + <NavDropdown.Divider/> + <NavDropdown.Item as={ StyledLink } to="/deploy" disabled={ this.state.disabled }>Deploy to DCAE (DEPLOY)</NavDropdown.Item> + <NavDropdown.Item as={ StyledLink } to="/undeploy" disabled={ this.state.disabled }>UnDeploy to DCAE (UNDEPLOY)</NavDropdown.Item> + </StyledNavDropdown> + <StyledNavDropdown title="Help"> + <StyledNavLink href="https://wiki.onap.org/" target="_blank">Wiki</StyledNavLink> + <StyledNavLink href="mailto:onap-discuss@lists.onap.org?subject=CLAMP&body=Please send us suggestions or feature enhancements or defect. If possible, please send us the steps to replicate any defect.">Contact + Us</StyledNavLink> + <NavDropdown.Item as={ StyledLink } to="/userInfo">User Info</NavDropdown.Item> + </StyledNavDropdown> + </Navbar.Collapse> + ); + } +} diff --git a/gui-clamp/ui-react/src/components/menu/MenuBar.test.js b/gui-clamp/ui-react/src/components/menu/MenuBar.test.js new file mode 100644 index 0000000..81117bb --- /dev/null +++ b/gui-clamp/ui-react/src/components/menu/MenuBar.test.js @@ -0,0 +1,46 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import MenuBar from './MenuBar'; + +describe('Verify MenuBar', () => { + + it('Test the render method', () => { + const component = shallow(<MenuBar/>) + + expect(component).toMatchSnapshot(); + }); + + it('Update loopName', () => { + const component = shallow(<MenuBar/>) + component.setProps({ loopName: "newLoop" }); + expect(component.state('disabled')).toBe(false); + }); + + it('Default loopName', () => { + const component = shallow(<MenuBar/>) + component.setProps({ loopName: "Empty (NO loop loaded yet)" }); + expect(component.state('disabled')).toBe(true); + }); +}); diff --git a/gui-clamp/ui-react/src/components/menu/__snapshots__/MenuBar.test.js.snap b/gui-clamp/ui-react/src/components/menu/__snapshots__/MenuBar.test.js.snap new file mode 100644 index 0000000..317c5a0 --- /dev/null +++ b/gui-clamp/ui-react/src/components/menu/__snapshots__/MenuBar.test.js.snap @@ -0,0 +1,1086 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verify MenuBar Test the render method 1`] = ` +<NavbarCollapse> + <Styled(NavDropdown) + title="POLICY Framework" + > + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={false} + to="/viewAllPolicies" + > + View All Policies + </DropdownItem> + </Styled(NavDropdown)> + <Styled(NavDropdown) + title="CLAMP Options" + > + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={false} + to="/manageDictionaries" + > + Tosca Metadata Dictionaries + </DropdownItem> + <DropdownDivider + role="separator" + /> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={false} + to="/viewLoopTemplatesModal" + > + View All Loop Templates + </DropdownItem> + </Styled(NavDropdown)> + <Styled(NavDropdown) + title="LOOP Instance" + > + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={false} + to="/createLoop" + > + Create + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={false} + to="/openLoop" + > + Open + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/closeLoop" + > + Close + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/modifyLoop" + > + Modify + </DropdownItem> + <DropdownDivider + role="separator" + /> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/loopProperties" + > + Properties + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/refreshStatus" + > + Refresh Status + </DropdownItem> + </Styled(NavDropdown)> + <Styled(NavDropdown) + title="LOOP Operations" + > + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/submit" + > + Create and deploy to Policy Framework (SUBMIT) + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/stop" + > + Undeploy from Policy Framework (STOP) + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/restart" + > + ReDeploy to Policy Framework (RESTART) + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/delete" + > + Delete loop instance (DELETE) + </DropdownItem> + <DropdownDivider + role="separator" + /> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/deploy" + > + Deploy to DCAE (DEPLOY) + </DropdownItem> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={true} + to="/undeploy" + > + UnDeploy to DCAE (UNDEPLOY) + </DropdownItem> + </Styled(NavDropdown)> + <Styled(NavDropdown) + title="Help" + > + <Styled(NavLink) + href="https://wiki.onap.org/" + target="_blank" + > + Wiki + </Styled(NavLink)> + <Styled(NavLink) + href="mailto:onap-discuss@lists.onap.org?subject=CLAMP&body=Please send us suggestions or feature enhancements or defect. If possible, please send us the steps to replicate any defect." + > + Contact Us + </Styled(NavLink)> + <DropdownItem + as={ + Object { + "$$typeof": Symbol(react.forward_ref), + "attrs": Array [], + "componentStyle": e { + "baseHash": 1014106698, + "baseStyle": undefined, + "componentId": "sc-bdfBwQ", + "isStatic": false, + "rules": Array [ + " + color: ", + [Function], + "; + background-color: ", + [Function], + "; + font-weight: normal; + display: block; + width: 100%; + padding: .25rem 1.5rem; + clear: both; + text-align: inherit; + white-space: nowrap; + border: 0; + + :hover { + text-decoration: none; + background-color: ", + [Function], + "; + color: ", + [Function], + "; + } +", + ], + "staticRulesId": "", + }, + "foldedComponentIds": Array [], + "render": [Function], + "shouldForwardProp": undefined, + "styledComponentId": "sc-bdfBwQ", + "target": Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "innerRef": [Function], + "onClick": [Function], + "replace": [Function], + "target": [Function], + "to": [Function], + }, + "render": [Function], + }, + "toString": [Function], + "warnTooManyClasses": [Function], + "withComponent": [Function], + } + } + disabled={false} + to="/userInfo" + > + User Info + </DropdownItem> + </Styled(NavDropdown)> +</NavbarCollapse> +`; diff --git a/gui-clamp/ui-react/src/index.js b/gui-clamp/ui-react/src/index.js new file mode 100644 index 0000000..9baeaee --- /dev/null +++ b/gui-clamp/ui-react/src/index.js @@ -0,0 +1,38 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import OnapClamp from './OnapClamp'; +import { Route, MemoryRouter } from 'react-router-dom' + + +const routing = ( + <MemoryRouter forceRefresh={ false }> + <Route path="/" component={ OnapClamp }/> + </MemoryRouter> +); + +export var mainClamp = ReactDOM.render( + routing, + document.getElementById('root') +) diff --git a/gui-clamp/ui-react/src/logo.png b/gui-clamp/ui-react/src/logo.png Binary files differnew file mode 100644 index 0000000..c6f6857 --- /dev/null +++ b/gui-clamp/ui-react/src/logo.png diff --git a/gui-clamp/ui-react/src/setupTests.js b/gui-clamp/ui-react/src/setupTests.js new file mode 100644 index 0000000..f553583 --- /dev/null +++ b/gui-clamp/ui-react/src/setupTests.js @@ -0,0 +1,28 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP POLICY-CLAMP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-17-updated'; + +configure({ adapter: new Adapter() }); +global.fetch = require('jest-fetch-mock');
\ No newline at end of file diff --git a/gui-clamp/ui-react/src/theme/globalStyle.js b/gui-clamp/ui-react/src/theme/globalStyle.js new file mode 100644 index 0000000..5245aae --- /dev/null +++ b/gui-clamp/ui-react/src/theme/globalStyle.js @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import { createGlobalStyle } from 'styled-components'; + +export const GlobalClampStyle = createGlobalStyle` + body { + padding: 0; + margin: 0; + font-family: ${ props => props.theme.fontFamily }; + font-size: ${ props => props.theme.fontSize }; + font-weight: normal; + } + + span { + font-family: ${ props => props.theme.fontFamily }; + font-size: ${ props => props.theme.fontSize }; + font-weight: bold; + } + + a { + font-family: ${ props => props.theme.fontFamily }; + font-size: ${ props => props.theme.fontSize }; + font-weight: bold; + } + + div { + font-family: ${ props => props.theme.fontFamily }; + font-size: ${ props => props.theme.fontSize }; + border-radius: 4px; + margin-top: 1px; + } + + label { + font-family: ${ props => props.theme.fontFamily }; + font-size: ${ props => props.theme.fontSize }; + font-weight: bold; + } + + button { + font-family: ${ props => props.theme.fontFamily }; + font-size: ${ props => props.theme.fontSize }; + font-weight: bold; + } + +` + +export const DefaultClampTheme = { + fontDanger: '#eb238e', + fontWarning: '#eb238e', + fontLight: '#ffffff', + fontDark: '#888888', + fontHighlight: '#ffff00', + fontNormal: 'black', + + backgroundColor: '#eeeeee', + fontFamily: 'Arial, Sans-serif', + fontSize: '16px', + + loopViewerBackgroundColor: 'white', + loopViewerFontColor: 'yellow', + loopViewerHeaderBackgroundColor: '#337ab7', + loopViewerHeaderFontColor: 'white', + + loopLogsHeaderBackgroundColor: 'white', + loopLogsHeaderFontColor: 'black', + + menuBackgroundColor: 'white', + menuFontColor: 'black', + menuHighlightedBackgroundColor: '#337ab7', + menuHighlightedFontColor: 'white', + + toscaTextareaBackgroundColor: 'white', + toscaTextareaFontSize: '13px', + + policyEditorBackgroundColor: 'white', + policyEditorFontSize: '13px' +}; diff --git a/gui-clamp/ui-react/src/utils/CsvToJson.js b/gui-clamp/ui-react/src/utils/CsvToJson.js new file mode 100644 index 0000000..6d95b31 --- /dev/null +++ b/gui-clamp/ui-react/src/utils/CsvToJson.js @@ -0,0 +1,204 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default function CsvToJson(rawCsvData, delimiter, internalDelimiter, csvHeaderNames, jsonKeyNames, mandatory) { + + let printDictKeys = ''; + let result = { jsonObjArray: [], errorMessages: '' }; + + // Validate that all parallel arrays passed in have same number of elements; + // this would be a developer error. + + let checkLength = csvHeaderNames.length; + + if (checkLength !== jsonKeyNames.length || checkLength !== mandatory.length) { + result.errorMessages = 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays parameters are not the same length'; + return result; + } + + if (checkLength < 1) { + result.errorMessages = 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays have no entries'; + return result; + } + + // Make a nice string to print in the error case to tell user what is the + // required heaer row format + + for (let i = 0; i < csvHeaderNames.length; ++i) { + if (i === 0) { + printDictKeys = csvHeaderNames[i]; + } else { + printDictKeys += ',' + csvHeaderNames[i]; + } + } + + let dictElems = rawCsvData.split('\n'); + let numColumns = 0; + let filteredDictElems = []; + + // The task of the following loop is to convert raw CSV rows into easily parseable + // and streamlined versions of the rows with an internalDelimiter replacing the standard + // comma; it is presumed (and checked) that the internalDelimiter cannot exist as a valid + // sequence of characters in the user's data. + + // This conversion process also strips leading and trailing whitespace from each row, + // discards empty rows, correctly interprets and removes all double quotes that programs like + // Excel use to support user columns that contain special characters, most notably, the comma + // delimiter. A double-quote that is contained within a double-quoted column value + // must appear in this raw data as a sequence of two double quotes. Furthermore, any column + // value in the raw CSV data that does not contain a delimiter may or may not be enclosed in + // double quotes. It is the Excel convention to not use double qoutes unless necessary, and + // there is no reasonable way to tell Excel to surround every column value with double quotes. + // Any files that were directly "exported" by CLAMP itself from the Managing Dictionaries + // capability, surround all columns with double quotes. + + for (let i = 0; i < dictElems.length; i++) { + + let oneRow = dictElems[i].trim(); + let j = 0; + let inQuote = false + let nextChar = undefined; + let prevChar = null; + + + if (oneRow === '') { + continue; // Skip blank rows + } else if (oneRow.indexOf(internalDelimiter) !== -1) { + result.errorMessages += '\nRow #' + i + ' contains illegal sequence of characters (' + internalDelimiter + ')'; + break; + } else { + nextChar = oneRow[1]; + } + + let newStr = ''; + numColumns = 1; + + // This "while loop" performs the very meticulous task of removing double quotes that + // are used by Excel to encase special characters as user string value data, + // and manages to correctly identify columns that are defined with or without + // double quotes and to process the comma delimiter correctly when encountered + // as a user value within a column. Such a column would have to be encased in + // double quotes; a comma found outside double quotes IS a delimiter. + + while (j < oneRow.length) { + if (oneRow[j] === '"') { + if (inQuote === false) { + if (prevChar !== delimiter && prevChar !== null) { + result.errorMessages += '\nMismatched double quotes or illegal whitespace around delimiter at row #' + (i + 1) + ' near column #' + numColumns; + break; + } else { + inQuote = true; + } + } else { + if (nextChar === '"') { + newStr += '"'; + ++j; + } else if ((nextChar !== delimiter) && (nextChar !== undefined)) { + result.errorMessages += '\nRow #' + (i + 1) + ' is badly formatted at column #' + numColumns + '. Perhaps an unescaped double quote.'; + break; + } else if (nextChar === delimiter) { + ++numColumns; + inQuote = false; + newStr += internalDelimiter; + prevChar = delimiter; + j += 2; + nextChar = oneRow[j + 1]; + continue; + } else { + ++numColumns; + inQuote = false; + break; + } + } + } else { + if (oneRow[j] === delimiter && inQuote === false) { + newStr += internalDelimiter; + ++numColumns; + } else { + newStr += oneRow[j]; + } + } + prevChar = oneRow[j]; + ++j; + nextChar = oneRow[j + 1]; // can result in undefined at the end + } + + if (result.errorMessages === '' && inQuote !== false) { + result.errorMessages += '\nMismatched double quotes at row #' + (i + 1); + break; + } else if (result.errorMessages === '' && numColumns < jsonKeyNames.length) { + result.errorMessages += '\nNot enough columns (' + jsonKeyNames.length + ') at row #' + (i + 1); + break; + } + + filteredDictElems.push(newStr); + } + + if (result.errorMessages !== '') { + return result; + } + + // Perform further checks on data that is now in JSON form + if (filteredDictElems.length < 2) { + result.errorMessages += '\nNot enough row data found in import file. Need at least a header row and one row of data'; + return result; + } + + // Now that we have something reliably parsed into sanitized columns lets run some checks + // and convert it all into an array of JSON objects to push to the back end if all the + // checks pass. + + let headers = filteredDictElems[0].split(internalDelimiter); + + // check that headers are included in proper order + for (let i = 0; i < jsonKeyNames.length; ++i) { + if (csvHeaderNames[i] !== headers[i]) { + result.errorMessages += 'Row 1 header key at column #' + (i + 1) + ' is a mismatch. Expected row header must contain at least:\n' + printDictKeys; + return result; + } + } + + // Convert the ASCII rows of data into an array of JSON obects that omit the header + // row which is not sent to the back end. + + for (let i = 1; i < filteredDictElems.length; i++) { + let data = filteredDictElems[i].split(internalDelimiter); + let obj = {}; + for (let j = 0; j < data.length && j < jsonKeyNames.length; j++) { + let value = data[j].trim(); + if (mandatory[j] === true && value === '') { + result.errorMessages += '\n' + csvHeaderNames[j] + ' at row #' + (i + 1) + ' is empty but requires a value.'; + } + obj[jsonKeyNames[j]] = value; + } + result.jsonObjArray.push(obj); + } + + if (result.errorMessages !== '') { + // If we have errors, return empty parse result even though some things + // may have parsed properly. We do not want to encourage the caller + // to think the data is good for use. + result.jsonObjArray = []; + } + + return result; +} diff --git a/gui-clamp/ui-react/src/utils/CsvToJson.test.js b/gui-clamp/ui-react/src/utils/CsvToJson.test.js new file mode 100644 index 0000000..e36e855 --- /dev/null +++ b/gui-clamp/ui-react/src/utils/CsvToJson.test.js @@ -0,0 +1,268 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +import CsvToJson from './CsvToJson' + +describe('Verify CsvToJson', () => { + + const hdrNames = [ + "Element Short Name", + "Element Name", + "Element Description", + "Element Type", + "Sub-Dictionary" + ]; + + const jsonKeyNames = [ + "shortName", + "name", + "description", + "type", + "subDictionary" + ]; + + const mandatory = [true, true, true, true, false]; + + it('Test CsvToJson No Error Case, Quoted Columns', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Type of Alert","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let expectedResult = { + errorMessages: '', + jsonObjArray: [ + { + description: "Type of Alert", + name: "Alert Type", + shortName: "alertType", + subDictionary: "", + type: "string" + } + ] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson No Error Case, Unquoted Columns', () => { + + let rawCsv = 'Element Short Name,Element Name,Element Description,Element Type,Sub-Dictionary\n'; + rawCsv += 'alertType,Alert Type,Type of Alert,string,,admin,2020-06-11T13:56:14.927437Z'; + + let expectedResult = { + errorMessages: '', + jsonObjArray: [ + { + description: "Type of Alert", + name: "Alert Type", + shortName: "alertType", + subDictionary: "", + type: "string" + } + ] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Properly Escaped Double Quote and Delimiter', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert ""Type""","Type of Alert, Varies","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let errorMessage = ''; + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [ + { + description: "Type of Alert, Varies", + name: 'Alert "Type"', + shortName: 'alertType', + subDictionary: "", + type: "string", + } + + ] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + + it('Test CsvToJson Error Header Mismatch Error Case', () => { + + let rawCsv = '"Element Short Names","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Type of Alert","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let errorMessage = 'Row 1 header key at column #1 is a mismatch. Expected row header must contain at least:\n'; + errorMessage += 'Element Short Name,Element Name,Element Description,Element Type,Sub-Dictionary'; + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Mismatched Double Quotes in Column', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alert"Type","Alert Type","Type of Alert","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let errorMessage = '\nRow #2 is badly formatted at column #1. Perhaps an unescaped double quote.' + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Illegal Whitespace', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += 'alertType , "Alert Type","Type of Alert","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let errorMessage = '\nMismatched double quotes or illegal whitespace around delimiter at row #2 near column #2'; + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Too Few Data Columns', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Type of Alert"'; + + let errorMessage = '\nNot enough columns (5) at row #2'; + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Wrong Header Column Order', () => { + + let rawCsv = '"Element Name","Element Short Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Type of Alert","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let errorMessage = 'Row 1 header key at column #1 is a mismatch. Expected row header must contain at least:\n'; + errorMessage += 'Element Short Name,Element Name,Element Description,Element Type,Sub-Dictionary'; + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Not Enough Rows', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + + let errorMessage = '\nNot enough row data found in import file. Need at least a header row and one row of data'; + + let expectedResult = { + errorMessages: errorMessage, + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Mandatory Field Is Empty', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"","Alert Type","Type of Alert","string","","admin","2020-06-11T13:56:14.927437Z"'; + + let expectedResult = { + errorMessages: '\nElement Short Name at row #2 is empty but requires a value.', + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '|', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Mismatched Double Quotes At End', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Alert Type Description","string","admin","2020-06-11T13:56:14.927437Z'; + + let expectedResult = { + errorMessages: '\nMismatched double quotes at row #2', + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '||', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Mismatched Mandatory Array Parameters', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Alert Type Description","string","admin","2020-06-11T13:56:14.927437Z'; + + let expectedResult = { + errorMessages: 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays parameters are not the same length', + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '||', hdrNames, jsonKeyNames, [true])).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Empty Mandatory Array Parameters', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Alert Type Description","string","admin","2020-06-11T13:56:14.927437Z'; + + let expectedResult = { + errorMessages: 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays have no entries', + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '||', [], [], [])).toEqual(expectedResult); + }); + + it('Test CsvToJson Error Illegal Data Contains Internal Delimiter', () => { + + let rawCsv = '"Element Short Name","Element Name","Element Description","Element Type","Sub-Dictionary"\n'; + rawCsv += '"alertType","Alert Type","Alert Type||Description","string","admin","2020-06-11T13:56:14.927437Z'; + + let expectedResult = { + errorMessages: '\nRow #1 contains illegal sequence of characters (||)', + jsonObjArray: [] + }; + + expect(CsvToJson(rawCsv, ',', '||', hdrNames, jsonKeyNames, mandatory)).toEqual(expectedResult); + }); +}) diff --git a/gui-clamp/ui-react/src/utils/OnapConstants.js b/gui-clamp/ui-react/src/utils/OnapConstants.js new file mode 100644 index 0000000..c5b96d4 --- /dev/null +++ b/gui-clamp/ui-react/src/utils/OnapConstants.js @@ -0,0 +1,32 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +// Maintain a list of ONAP CLAMP UI "constants" that can be used by any componenet within CLAMP + +const OnapConstants = { + defaultLoopName: "Empty (NO loop loaded yet)", + microServiceType: "MICRO-SERVICE-POLICY", + operationalPolicyType: "OPERATIONAL_POLICY_TYPE" +}; + +export default OnapConstants; diff --git a/gui-clamp/ui-react/src/utils/OnapUtils.js b/gui-clamp/ui-react/src/utils/OnapUtils.js new file mode 100644 index 0000000..9ba9382 --- /dev/null +++ b/gui-clamp/ui-react/src/utils/OnapUtils.js @@ -0,0 +1,65 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. 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. + * ============LICENSE_END============================================ + * =================================================================== + * + */ + +export default class OnapUtils { + + constructor() { + this.clickBlocked = false; + } + + static jsonEditorErrorFormatter(errors) { + + let messages = []; + let messagesOutputString = null; + + // errors is an array of JSON Editor "error" objects, where each + // object looks like this: + + // { + // message: "Please populate the required property "Threshold"" + // path: "root.signatures.0" + // property: "required" + // } + + // In this function we concatenate all the messages, removing any duplicates, + // and adding a newline between each message. The result returned is a single + // string that can be displayed to the user in an alert message + + if (!Array.isArray(errors)) { + console.error('jsoneEditorErrorFormatter was passed a non-array argument'); + } else { + for (let ii = 0; ii < errors.length; ++ii) { + if (!messages.includes(errors[ii].message)) { + messages.push(errors[ii].message); + if (messagesOutputString) { + messagesOutputString += '\n' + errors[ii].message; + } else { + messagesOutputString = errors[ii].message; + } + } + } + } + + return messagesOutputString; + } +} |