diff options
Diffstat (limited to 'ui-react/src')
31 files changed, 3346 insertions, 0 deletions
diff --git a/ui-react/src/LoopUI.js b/ui-react/src/LoopUI.js new file mode 100644 index 000000000..c7080a2b6 --- /dev/null +++ b/ui-react/src/LoopUI.js @@ -0,0 +1,205 @@ +/*- + * ============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 MenuBar from './components/menu/MenuBar'; +import Navbar from 'react-bootstrap/Navbar'; +import logo from './logo.png'; +import { GlobalClampStyle } from './theme/globalStyle.js'; + +import LoopSvg from './components/loop_viewer/svg/LoopSvg'; +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 { Route, Redirect } from 'react-router-dom' +import OpenLoopModal from './components/dialogs/OpenLoop/OpenLoopModal'; +import OperationalPolicyModal from './components/dialogs/OperationalPolicy/OperationalPolicyModal'; +import ConfigurationPolicyModal from './components/dialogs/ConfigurationPolicy/ConfigurationPolicyModal'; +import LoopProperties from './components/dialogs/LoopProperties'; +import UserInfo from './components/dialogs/UserInfo'; +import LoopService from './api/LoopService'; +import PerformAction from './components/menu/PerformActions'; +import RefreshStatus from './components/menu/RefreshStatus'; + +const ProjectNameStyled = styled.a` + vertical-align: middle; + padding-left: 30px; + font-size: 30px; + +` +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 { + + static defaultLoopName="Empty (NO loop loaded yet)"; + + state = { + userName: null, + loopName: LoopUI.defaultLoopName, + loopCache: new LoopCache({}) + }; + + constructor() { + super(); + this.getUser = this.getUser.bind(this); + this.updateLoopCache = this.updateLoopCache.bind(this); + this.loadLoop = this.loadLoop.bind(this); + } + + componentWillMount() { + this.getUser(); + } + + getUser() { + UserService.login().then(user => { + this.setState({ userName: user }) + }); + } + + renderMenuNavBar() { + return ( + <MenuBar/> + ); + } + + renderUserLoggedNavBar() { + return ( + <Navbar.Text> + Signed in as: <a href="/login">{this.state.userName}</a> + </Navbar.Text> + ); + } + + renderLogoNavBar() { + return ( + <Navbar.Brand> + <img height="50px" width="234px" src={logo} alt="" /> + <ProjectNameStyled>CLAMP</ProjectNameStyled> + </Navbar.Brand> + ); + } + + renderNavBar() { + return ( + <Navbar expand="lg"> + {this.renderLogoNavBar()} + {this.renderMenuNavBar()} + {this.renderUserLoggedNavBar()} + </Navbar> + ); + } + + renderLoopViewHeader() { + return ( + <LoopViewHeaderDivStyled> + Loop Viewer - {this.state.loopName} + </LoopViewHeaderDivStyled> + ); + } + + renderLoopViewBody() { + return ( + <LoopViewBodyDivStyled> + <LoopSvg loopCache={this.state.loopCache} /> + <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) { + this.setState({ loopCache: new LoopCache(loopJson) }); + this.setState({ loopName: this.state.loopCache.getLoopName() }); + console.info(this.state.loopName+" loop loaded successfully"); + } + + loadLoop(loopName) { + LoopService.getLoop(loopName).then(loop => { + console.debug("Updating loopCache"); + this.updateLoopCache(loop); + }); + } + + render() { + return ( + <div id="main_div"> + <Route path="/operationalPolicyModal" + render={(routeProps) => (<OperationalPolicyModal {...routeProps} loopCache={this.getLoopCache()} loadLoopFunction={this.loadLoop}/>)} /> + <Route path="/configurationPolicyModal/:componentName" render={(routeProps) => (<ConfigurationPolicyModal {...routeProps} loopCache={this.getLoopCache()} loadLoopFunction={this.loadLoop}/>)} /> + <Route path="/openLoop" render={(routeProps) => (<OpenLoopModal {...routeProps} loadLoopFunction={this.loadLoop} />)} /> + <Route path="/loopProperties" render={(routeProps) => (<LoopProperties {...routeProps} loopCache={this.getLoopCache()} loadLoopFunction={this.loadLoop}/>)} /> + <Route path="/userInfo" render={(routeProps) => (<UserInfo {...routeProps} />)} /> + <Route path="/closeLoop" render={(routeProps) => (<Redirect to='/'/>)} /> + <Route path="/submit" render={(routeProps) => (<PerformAction {...routeProps} loopAction="submit" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache}/>)} /> + <Route path="/stop" render={(routeProps) => (<PerformAction {...routeProps} loopAction="stop" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache}/>)} /> + <Route path="/restart" render={(routeProps) => (<PerformAction {...routeProps} loopAction="restart" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache}/>)} /> + <Route path="/delete" render={(routeProps) => (<PerformAction {...routeProps} loopAction="delete" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache}/>)} /> + <Route path="/undeploy" render={(routeProps) => (<PerformAction {...routeProps} loopAction="undeploy" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache}/>)} /> + <Route path="/refreshStatus" render={(routeProps) => (<RefreshStatus {...routeProps} loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache}/>)} /> + <GlobalClampStyle /> + {this.renderNavBar()} + {this.renderLoopViewer()} + </div> + ); + } +} diff --git a/ui-react/src/NotFound.js b/ui-react/src/NotFound.js new file mode 100644 index 000000000..d4b53fd71 --- /dev/null +++ b/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/ui-react/src/OnapClamp.js b/ui-react/src/OnapClamp.js new file mode 100644 index 000000000..506f6e09d --- /dev/null +++ b/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/ui-react/src/React-Spinner.jpg b/ui-react/src/React-Spinner.jpg Binary files differnew file mode 100644 index 000000000..227689764 --- /dev/null +++ b/ui-react/src/React-Spinner.jpg diff --git a/ui-react/src/__test__/LoopCache.test.js b/ui-react/src/__test__/LoopCache.test.js new file mode 100644 index 000000000..e5b50259d --- /dev/null +++ b/ui-react/src/__test__/LoopCache.test.js @@ -0,0 +1,217 @@ +/*- + * ============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 LoopCache from '../api/LoopCache'; + +const json = require('./LoopCache_mokeLoopJsonCache.json'); + +describe('Verify LoopCache functions', () => { + const loopCache = new LoopCache(json); + it('getLoopName', () => { + expect(loopCache.getLoopName()).toBe("LOOP_Jbv1z_v1_0_ResourceInstanceName1_tca"); + }); + + it('getOperationalPolicyConfigurationJson', () => { + const opPolicyConfig = { + "guard_policies": {}, + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + }; + expect(loopCache.getOperationalPolicyConfigurationJson()).toStrictEqual(opPolicyConfig); + }); + + it('getOperationalPolicies', () => { + const opPolicy = [{ + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca", + "configurationsJson": { + "guard_policies": {}, + "operational_policy": { + "controlLoop": {}, + "policies": [] + + } + } + }]; + expect(loopCache.getOperationalPolicies()).toStrictEqual(opPolicy); + }); + + 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('getMicroServicesJsonForType', () => { + const msJson = { + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "properties": {"domain": "measurementsForVfScaling"}, + "shared": false, + "jsonRepresentation": {"schema": {}} + }; + expect(loopCache.getMicroServicesJsonForType("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msJson); + expect(loopCache.getMicroServicesJsonForType("TCA_h2NMX_v1_0_ResourceInstanceName1_tca_2")).toBeNull(); + }); + + it('getMicroServiceProperties', () => { + const msProp = {"domain": "measurementsForVfScaling"}; + expect(loopCache.getMicroServiceProperties("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msProp); + expect(loopCache.getMicroServiceProperties("TCA_h2NMX_v1_0_ResourceInstanceName1_tca_2")).toBeNull(); + }); + + it('getMicroServiceJsonRepresentationForType', () => { + const msJsonRepresentation = {"schema": {}}; + expect(loopCache.getMicroServiceJsonRepresentationForType("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(msJsonRepresentation); + }); + + it('getMicroServiceJsonRepresentationForType', () => { + const msJsonRepresentation = {"schema": {}}; + expect(loopCache.getMicroServiceJsonRepresentationForType("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 and Guard 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('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 = [{ + "name": "OPERATIONAL_h2NMX_v1_0_ResourceInstanceName1_tca_new", + "configurationsJson": { + "guard_policies": {}, + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + } + }]; + loopCache.updateOperationalPolicyProperties(newOpPolicy); + expect(loopCache.getOperationalPolicies()).toStrictEqual(newOpPolicy); + }); + + it('updateMicroServiceProperties', () => { + const newMsPolicy = { + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "properties": {"domain": "measurementsForVfScalingNew"}, + "shared": true, + "jsonRepresentation": {"schema": {}} + };; + loopCache.updateMicroServiceProperties("TCA_h2NMX_v1_0_ResourceInstanceName1_tca", newMsPolicy); + expect(loopCache.getMicroServicesJsonForType("TCA_h2NMX_v1_0_ResourceInstanceName1_tca")).toStrictEqual(newMsPolicy); + }); + }); diff --git a/ui-react/src/__test__/LoopCache_mokeLoopJsonCache.json b/ui-react/src/__test__/LoopCache_mokeLoopJsonCache.json new file mode 100644 index 000000000..184eaf7cd --- /dev/null +++ b/ui-react/src/__test__/LoopCache_mokeLoopJsonCache.json @@ -0,0 +1,117 @@ +{ + "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" + } + }, + "modelPropertiesJson": { + "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": { + "guard_policies": {}, + "operational_policy": { + "controlLoop": {}, + "policies": [] + } + } + } + ], + "microServicePolicies": [ + { + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "properties": {"domain": "measurementsForVfScaling"}, + "shared": false, + "jsonRepresentation": {"schema": {}} + } + ], + "loopLogs": [ + { + "id": 1, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational and Guard policies UPDATED", + "logInstant": "2019-07-08T09:44:37Z" + } + ] +} diff --git a/ui-react/src/__test__/OpenLoopModal.test.js b/ui-react/src/__test__/OpenLoopModal.test.js new file mode 100644 index 000000000..044eeda89 --- /dev/null +++ b/ui-react/src/__test__/OpenLoopModal.test.js @@ -0,0 +1,35 @@ +/*- + * ============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 '../components/dialogs/OpenLoop/OpenLoopModal'; + +describe('Verify OpenLoopModal', () => { + + it('Test the render method', () => { + const component = shallow(<OpenLoopModal/>); + expect(component).toMatchSnapshot(); + }); + + +}); diff --git a/ui-react/src/api/LoopActionService.js b/ui-react/src/api/LoopActionService.js new file mode 100644 index 000000000..6e45ce4b9 --- /dev/null +++ b/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.log("LoopActionService perform action: " + uiAction + " closedloopName=" + cl_name); + const svcAction = uiAction.toLowerCase(); + return fetch("/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) { + alert("Action Successful: " + uiAction); + return data; + }) + .catch(function(error) { + console.log("Action Failure: " + uiAction); + return Promise.reject(error); + }); + } + + + static refreshStatus(cl_name) { + console.log("Refresh the status for closedloopName=" + cl_name); + + return fetch("/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/ui-react/src/api/LoopCache.js b/ui-react/src/api/LoopCache.js new file mode 100644 index 000000000..3ee5acc68 --- /dev/null +++ b/ui-react/src/api/LoopCache.js @@ -0,0 +1,116 @@ +/*- + * ============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(type, newMsProperties) { + for (var policy in this.loopJsonCache["microServicePolicies"]) { + if (this.loopJsonCache["microServicePolicies"][policy]["name"] === type) { + this.loopJsonCache["microServicePolicies"][policy]["properties"] = newMsProperties; + } + } + } + + updateGlobalProperties(newGlobalProperties) { + this.loopJsonCache["globalPropertiesJson"] = newGlobalProperties; + } + + updateOperationalPolicyProperties(newOpProperties) { + this.loopJsonCache["operationalPolicies"] = newOpProperties; + } + + getLoopName() { + return this.loopJsonCache["name"]; + } + + getOperationalPolicyConfigurationJson() { + return this.loopJsonCache["operationalPolicies"]["0"]["configurationsJson"]; + } + + getOperationalPolicies() { + return this.loopJsonCache["operationalPolicies"]; + } + + getGlobalProperties() { + return this.loopJsonCache["globalPropertiesJson"]; + } + + getDcaeDeploymentProperties() { + return this.loopJsonCache["globalPropertiesJson"]["dcaeDeployParameters"]; + } + + getMicroServicePolicies() { + return this.loopJsonCache["microServicePolicies"]; + } + + 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["properties"]; + } + return null; + } + + getMicroServiceJsonRepresentationForName(name) { + var msConfig = this.getMicroServiceForName(name); + if (msConfig !== null) { + return msConfig["jsonRepresentation"]; + } + return null; + } + + getResourceDetailsVfProperty() { + return this.loopJsonCache["modelPropertiesJson"]["resourceDetails"]["VF"]; + } + + getResourceDetailsVfModuleProperty() { + return this.loopJsonCache["modelPropertiesJson"]["resourceDetails"]["VFModule"]; + } + + getLoopLogsArray() { + return this.loopJsonCache.loopLogs; + } + + getComputedState() { + return this.loopJsonCache.lastComputedState; + } + + getComponentStates() { + return this.loopJsonCache.components; + } +} diff --git a/ui-react/src/api/LoopService.js b/ui-react/src/api/LoopService.js new file mode 100644 index 000000000..031ec638f --- /dev/null +++ b/ui-react/src/api/LoopService.js @@ -0,0 +1,131 @@ +/*- + * ============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('/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 getLoop(loopName) { + return fetch('/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 getSvg(loopName) { + return fetch('/restservices/clds/v2/loop/svgRepresentation/' + loopName, { + method: 'GET', + credentials: 'same-origin', + }) + .then(function (response) { + console.debug("svgRepresentation response received: ", response.status); + if (response.ok) { + return response.text(); + } else { + console.error("svgRepresentation query failed"); + return ""; + } + }) + .catch(function (error) { + console.error("svgRepresentation error received", error); + return ""; + }); + } + + static setMicroServiceProperties(loopName, jsonData) { + return fetch('/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 updateGlobalProperties(loopName, jsonData) { + return fetch('/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 ""; + }); + } +} diff --git a/ui-react/src/api/UserService.js b/ui-react/src/api/UserService.js new file mode 100644 index 000000000..be21e692a --- /dev/null +++ b/ui-react/src/api/UserService.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============================================ + * =================================================================== + * + */ + +export default class UserService { + static notLoggedUserName='Anonymous'; + static login() { + return fetch('/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('/restservices/clds/v1/clds/cldsInfo', { + method: 'GET', + credentials: 'same-origin', + }) + .then(function (response) { + console.debug("getUserInfo response received, status code:", response.status); + if (response.ok) { + return response.json(); + } + }) + .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/ui-react/src/api/example.json b/ui-react/src/api/example.json new file mode 100644 index 000000000..108cf78e2 --- /dev/null +++ b/ui-react/src/api/example.json @@ -0,0 +1,417 @@ +{ + "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" + } + }, + "modelPropertiesJson": { + "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 + }, + "Vloadbalancerms..vdns..module-3": { + "vfModuleModelInvariantUUID": "4c10ba9b-f88f-415e-9de3-5d33336047fa", + "vfModuleModelVersion": "1", + "vfModuleModelName": "Vloadbalancerms..vdns..module-3", + "vfModuleModelUUID": "4fa73b49-8a6c-493e-816b-eb401567b720", + "vfModuleModelCustomizationUUID": "bafcdab0-801d-4d81-9ead-f464640a38b1", + "min_vf_module_instances": 0, + "vf_module_label": "vdns", + "max_vf_module_instances": 50, + "vf_module_type": "Expansion", + "isBase": false, + "initial_count": 0, + "volume_group": false + }, + "Vloadbalancerms..base_template..module-0": { + "vfModuleModelInvariantUUID": "921f7c96-ebdd-42e6-81b9-1cfc0c9796f3", + "vfModuleModelVersion": "1", + "vfModuleModelName": "Vloadbalancerms..base_template..module-0", + "vfModuleModelUUID": "63734409-f745-4e4d-a38b-131638a0edce", + "vfModuleModelCustomizationUUID": "86baddea-c730-4fb8-9410-cd2e17fd7f27", + "min_vf_module_instances": 1, + "vf_module_label": "base_template", + "max_vf_module_instances": 1, + "vf_module_type": "Base", + "isBase": true, + "initial_count": 1, + "volume_group": false + }, + "Vloadbalancerms..vlb..module-2": { + "vfModuleModelInvariantUUID": "a772a1f4-0064-412c-833d-4749b15828dd", + "vfModuleModelVersion": "1", + "vfModuleModelName": "Vloadbalancerms..vlb..module-2", + "vfModuleModelUUID": "0f5c3f6a-650a-4303-abb6-fff3e573a07a", + "vfModuleModelCustomizationUUID": "96a78aad-4ffb-4ef0-9c4f-deb03bf1d806", + "min_vf_module_instances": 0, + "vf_module_label": "vlb", + "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": { + "guard_policies": { + "guard.minmax.new": { + "recipe": "", + "clname": "LOOP_h2NMX_v1_0_ResourceInstanceName1_tca", + "actor": "", + "targets": "", + "min": "gg", + "max": "gg", + "limit": "", + "timeUnits": "", + "timeWindow": "", + "guardActiveStart": "00:00:00Z", + "guardActiveEnd": "00:00:01Z" + } + }, + "operational_policy": { + "controlLoop": { + "trigger_policy": "new", + "timeout": "0", + "abatement": "false", + "controlLoopName": "LOOP_h2NMX_v1_0_ResourceInstanceName1_tca" + }, + "policies": [ + { + "id": "new", + "recipe": "", + "retry": "0", + "timeout": "0", + "actor": "", + "payload": "", + "success": "", + "failure": "", + "failure_timeout": "", + "failure_retries": "", + "failure_exception": "", + "failure_guard": "", + "target": { + "type": "VM", + "resourceID": "" + } + } + ] + } + } + } + ], + "microServicePolicies": [ + { + "name": "TCA_h2NMX_v1_0_ResourceInstanceName1_tca", + "modelType": "onap.policies.monitoring.cdap.tca.hi.lo.app", + "properties": { + "domain": "measurementsForVfScaling", + "metricsPerEventName": [ + { + "policyVersion": "ff", + "thresholds": [ + { + "severity": "CRITICAL", + "fieldPath": "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta", + "thresholdValue": 0, + "closedLoopEventStatus": "ONSET", + "closedLoopControlName": "ff", + "version": "ff", + "direction": "LESS" + } + ], + "policyName": "ff", + "controlLoopSchemaType": "VM", + "policyScope": "ff", + "eventName": "ff" + } + ] + }, + "shared": false, + "jsonRepresentation": { + "schema": { + "uniqueItems": "true", + "format": "tabs-top", + "type": "array", + "title": "TCA Policy JSON", + "items": { + "type": "object", + "title": "TCA Policy JSON", + "required": [ + "domain", + "metricsPerEventName" + ], + "properties": { + "domain": { + "propertyOrder": 1001, + "default": "measurementsForVfScaling", + "title": "Domain name to which TCA needs to be applied", + "type": "string" + }, + "metricsPerEventName": { + "propertyOrder": 1002, + "uniqueItems": "true", + "format": "tabs-top", + "title": "Contains eventName and threshold details that need to be applied to given eventName", + "type": "array", + "items": { + "type": "object", + "required": [ + "controlLoopSchemaType", + "eventName", + "policyName", + "policyScope", + "policyVersion", + "thresholds" + ], + "properties": { + "policyVersion": { + "propertyOrder": 1007, + "title": "TCA Policy Scope Version", + "type": "string" + }, + "thresholds": { + "propertyOrder": 1008, + "uniqueItems": "true", + "format": "tabs-top", + "title": "Thresholds associated with eventName", + "type": "array", + "items": { + "type": "object", + "required": [ + "closedLoopControlName", + "closedLoopEventStatus", + "direction", + "fieldPath", + "severity", + "thresholdValue", + "version" + ], + "properties": { + "severity": { + "propertyOrder": 1013, + "title": "Threshold Event Severity", + "type": "string", + "enum": [ + "CRITICAL", + "MAJOR", + "MINOR", + "WARNING", + "NORMAL" + ] + }, + "fieldPath": { + "propertyOrder": 1012, + "title": "Json field Path as per CEF message which needs to be analyzed for TCA", + "type": "string", + "enum": [ + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedOctetsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedUnicastPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedMulticastPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedBroadcastPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedDiscardedPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedErrorPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedOctetsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedUnicastPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedMulticastPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedBroadcastPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedDiscardedPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedErrorPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedTotalPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedOctetsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedUnicastPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedMulticastPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedBroadcastPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedDiscardedPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedErrorPacketsDelta", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedTotalPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedOctetsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedUnicastPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedMulticastPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedBroadcastPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedDiscardedPacketsAccumulated", + "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].transmittedErrorPacketsAccumulated", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuIdle", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuUsageInterrupt", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuUsageNice", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuUsageSoftIrq", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuUsageSteal", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuUsageSystem", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].cpuWait", + "$.event.measurementsForVfScalingFields.cpuUsageArray[*].percentUsage", + "$.event.measurementsForVfScalingFields.meanRequestLatency", + "$.event.measurementsForVfScalingFields.memoryUsageArray[*].memoryBuffered", + "$.event.measurementsForVfScalingFields.memoryUsageArray[*].memoryCached", + "$.event.measurementsForVfScalingFields.memoryUsageArray[*].memoryConfigured", + "$.event.measurementsForVfScalingFields.memoryUsageArray[*].memoryFree", + "$.event.measurementsForVfScalingFields.memoryUsageArray[*].memoryUsed", + "$.event.measurementsForVfScalingFields.additionalMeasurements[*].arrayOfFields[0].value" + ] + }, + "thresholdValue": { + "propertyOrder": 1014, + "title": "Threshold value for the field Path inside CEF message", + "type": "integer" + }, + "closedLoopEventStatus": { + "propertyOrder": 1010, + "title": "Closed Loop Event Status of the threshold", + "type": "string", + "enum": [ + "ONSET", + "ABATED" + ] + }, + "closedLoopControlName": { + "propertyOrder": 1009, + "title": "Closed Loop Control Name associated with the threshold", + "type": "string" + }, + "version": { + "propertyOrder": 1015, + "title": "Version number associated with the threshold", + "type": "string" + }, + "direction": { + "propertyOrder": 1011, + "title": "Direction of the threshold", + "type": "string", + "enum": [ + "LESS", + "LESS_OR_EQUAL", + "GREATER", + "GREATER_OR_EQUAL", + "EQUAL" + ] + } + } + } + }, + "policyName": { + "propertyOrder": 1005, + "title": "TCA Policy Scope Name", + "type": "string" + }, + "controlLoopSchemaType": { + "propertyOrder": 1003, + "title": "Specifies Control Loop Schema Type for the event Name e.g. VNF, VM", + "type": "string", + "enum": [ + "VM", + "VNF" + ] + }, + "policyScope": { + "propertyOrder": 1006, + "title": "TCA Policy Scope", + "type": "string" + }, + "eventName": { + "propertyOrder": 1004, + "title": "Event name to which thresholds need to be applied", + "type": "string" + } + } + } + } + } + } + } + } + } + ], + "loopLogs": [ + { + "id": 2, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Micro Service policies UPDATED", + "logInstant": "2019-07-08T09:44:53Z" + }, + { + "id": 1, + "logType": "INFO", + "logComponent": "CLAMP", + "message": "Operational and Guard policies UPDATED", + "logInstant": "2019-07-08T09:44:37Z" + } + ] +} diff --git a/ui-react/src/components/dialogs/ConfigurationPolicy/ConfigurationPolicyModal.js b/ui-react/src/components/dialogs/ConfigurationPolicy/ConfigurationPolicyModal.js new file mode 100644 index 000000000..4fbb7832c --- /dev/null +++ b/ui-react/src/components/dialogs/ConfigurationPolicy/ConfigurationPolicyModal.js @@ -0,0 +1,126 @@ +/*- + * ============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 styled from 'styled-components'; +import LoopService from '../../../api/LoopService'; +import JSONEditor from '@json-editor/json-editor'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` + +export default class ConfigurationPolicyModal extends React.Component { + + state = { + show: true, + loopCache: this.props.loopCache, + jsonEditor: null, + componentName: this.props.match.params.componentName, + }; + + constructor(props, context) { + super(props, context); + this.handleClose = this.handleClose.bind(this); + this.handleSave = this.handleSave.bind(this); + this.renderJsonEditor = this.renderJsonEditor.bind(this); + } + + handleSave() { + var errors = this.state.jsonEditor.validate(); + var editorData = this.state.jsonEditor.getValue(); + + if (errors.length !== 0) { + console.error("Errors detected during config policy data validation ", errors); + this.setState({ show: false }); + this.props.history.push('/'); + } + else { + console.info("NO validation errors found in config policy data"); + this.state.loopCache.updateMicroServiceProperties(this.state.componentName, editorData[0]); + LoopService.setMicroServiceProperties(this.state.loopCache.getLoopName(), this.state.loopCache.getMicroServiceForName(this.state.componentName)).then(resp => { + this.setState({ show: false }); + this.props.history.push('/'); + this.props.loadLoopFunction(this.state.loopCache.getLoopName()); + }); + } + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + componentDidMount() { + this.renderJsonEditor(); + } + + renderJsonEditor() { + console.debug("Rendering ConfigurationPolicyModal ", this.state.componentName); + var toscaModel = this.state.loopCache.getMicroServiceJsonRepresentationForName(this.state.componentName); + if (toscaModel == null) { + return; + } + var editorData = this.state.loopCache.getMicroServicePropertiesForName(this.state.componentName); + + JSONEditor.defaults.options.theme = 'bootstrap4'; + //JSONEditor.defaults.options.iconlib = 'bootstrap2'; + JSONEditor.defaults.options.object_layout = 'grid'; + JSONEditor.defaults.options.disable_properties = true; + JSONEditor.defaults.options.disable_edit_json = false; + JSONEditor.defaults.options.disable_array_reorder = true; + JSONEditor.defaults.options.disable_array_delete_last_row = true; + JSONEditor.defaults.options.disable_array_delete_all_rows = false; + JSONEditor.defaults.options.show_errors = 'always'; + + this.setState({ + jsonEditor: new JSONEditor(document.getElementById("editor"), + { schema: toscaModel.schema, startval: editorData }) + }) + } + + render() { + return ( + <ModalStyled size="lg" show={this.state.show} onHide={this.handleClose}> + <Modal.Header closeButton> + <Modal.Title>Configuration policies</Modal.Title> + </Modal.Header> + <Modal.Body> + <div id="editor" /> + + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={this.handleClose}> + Close + </Button> + <Button variant="primary" onClick={this.handleSave}> + Save Changes + </Button> + </Modal.Footer> + </ModalStyled> + + ); + } +}
\ No newline at end of file diff --git a/ui-react/src/components/dialogs/LoopProperties.js b/ui-react/src/components/dialogs/LoopProperties.js new file mode 100644 index 000000000..dac77655f --- /dev/null +++ b/ui-react/src/components/dialogs/LoopProperties.js @@ -0,0 +1,119 @@ +/*- + * ============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 LoopProperties 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); + } + + 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} > + <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" onClick={this.handleSave}>Save Changes</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/ui-react/src/components/dialogs/OpenLoop/OpenLoopModal.js b/ui-react/src/components/dialogs/OpenLoop/OpenLoopModal.js new file mode 100644 index 000000000..fbfc727eb --- /dev/null +++ b/ui-react/src/components/dialogs/OpenLoop/OpenLoopModal.js @@ -0,0 +1,111 @@ +/*- + * ============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'; + +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.state = { + show: true, + chosenLoopName: '', + loopNames: [] + }; + } + + componentWillMount() { + this.getLoopNames(); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/'); + } + + handleDropdownListChange(e) { + this.setState({ chosenLoopName: e.value }); + } + + getLoopNames() { + LoopService.getLoopNames().then(loopNames => { + const loopOptions = loopNames.map((loopName) => { return { label: loopName, value: loopName } }); + this.setState({ loopNames: loopOptions }) + }); + } + + handleOpen() { + console.info("Loop " + this.state.chosenLoopName + " is chosen"); + this.setState({ show: false }); + this.props.history.push('/'); + this.props.loadLoopFunction(this.state.chosenLoopName); + } + + render() { + return ( + <ModalStyled size="lg" show={this.state.show} onHide={this.handleClose}> + <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 controlId="formBasicChecbox"> + <Form.Check> + <FormCheck.Label>Read Only</FormCheck.Label> + <CheckBoxStyled type="checkbox" /> + </Form.Check> + </Form.Group> + </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/ui-react/src/components/dialogs/OperationalPolicy/OperationalPolicy.css b/ui-react/src/components/dialogs/OperationalPolicy/OperationalPolicy.css new file mode 100644 index 000000000..94a91c234 --- /dev/null +++ b/ui-react/src/components/dialogs/OperationalPolicy/OperationalPolicy.css @@ -0,0 +1,73 @@ +.disabled { + background-color: #dddd; +} + +label { + text-align: right; + vertical-align: middle; +} + +.leftPolicyPanel { + padding: 0 10px 0 0; +} + +.idError { + color: red; + padding: 50px 0px; + text-align: center; + display: none; +} + +.policyPanel { + background-color: #f5f5f5; + padding: 15px 5px 0 5px; +} + +.form-group.clearfix { + display: -webkit-flex; + display: flex; + align-items: center; +} + +label { + margin-bottom: 0px; +} + +.withnote { + margin-bottom: 0px; +} + +.note { + font-size:10px; + margin-left: 250px; + font-weight: normal; +} + +#policyTable { + cursor: pointer; + width: 100%; +} + +#policyTable tr { + border-bottom: 1px solid #ddd; + border-collapse: collapse; + text-align: left; + font-size: 12px; + font-weight: normal; +} + +#policyTable td { + padding: 8px 10px; +} + +#policyTable tr.highlight { + background-color: #f5f5f5; + font-weight: bold; + font-size: 13px; +} + +#policyTableHolder { + height: 200px; + width: 100%; + overflow: auto; +}
\ No newline at end of file diff --git a/ui-react/src/components/dialogs/OperationalPolicy/OperationalPolicyModal.js b/ui-react/src/components/dialogs/OperationalPolicy/OperationalPolicyModal.js new file mode 100644 index 000000000..2a812c877 --- /dev/null +++ b/ui-react/src/components/dialogs/OperationalPolicy/OperationalPolicyModal.js @@ -0,0 +1,552 @@ +/*- + * ============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 './OperationalPolicy.css' +import styled from 'styled-components'; + +const ModalStyled = styled(Modal)` + background-color: transparent; +` + +export default class OperationalPolicyModal extends React.Component { + + state = { + show: true, + loopCache: this.props.loopCache, + }; + + allPolicies = []; + policyIds = []; + + constructor(props, context) { + super(props, context); + + this.handleClose = this.handleClose.bind(this); + this.initPolicySelect = this.initPolicySelect.bind(this); + this.initPolicySelect(); + } + + handleClose() { + this.setState({ show: false }); + this.props.history.push('/') + } + + initPolicySelect() { + if (this.allPolicies['operational_policy'] === undefined || this.allPolicies['operational_policy'] === null) { + this.allPolicies = this.state.loopCache.getOperationalPolicyConfigurationJson(); + } + // Provision all policies ID first + if (this.policyIds.length === 0 && this.allPolicies['operational_policy'] !== undefined) { + + for (let i = 0; i < this.allPolicies['operational_policy']['policies'].length; i++) { + this.policyIds.push(this.allPolicies['operational_policy']['policies'][i]['id']); + } + } + } + + renderPolicyIdSelect() { + return ( + <select type="text" id="trigger_policy" name="trigger_policy" + className="form-control"> + <option value="">-- choose an option --</option> + {this.policyIds.map(policyId => (<option key={policyId}>{policyId}</option>))} + </select> + ); + } + + serializeElement(element) { + var o = {}; + element.serializeArray().forEach(function () { + if (o[this.name]) { + if (!o[this.name].push) { + o[this.name] = [o[this.name]]; + } + o[this.name].push(this.value || ''); + } else { + o[this.name] = this.value || ''; + } + }); + return o; + } + + // When we change the name of a policy + isDuplicatedId(event) { + // update policy id structure + var formNum = document.getElementById(event.target).closest('.formId').attr('id').substring(6); + var policyId = document.getElementById(event.target).val(); + if (this.policyIds.includes(policyId)) { + console.log("Duplicated ID, cannot proceed"); + return true; + } else { + this.duplicated = false; + this.policyIds.splice(this.policyIds.indexOf(document.getElementById("#formId" + formNum + " #id").val()), 1); + this.policyIds.push(document.getElementById(event.target).val()); + // Update the tab now + document.getElementById("#go_properties_tab" + formNum).text(document.getElementById(event.target).val()); + } + } + + configureComponents() { + console.log("Load properties to op policy"); + // Set the header + document.getElementsByClassName('form-control').forEach(function () { + this.val(this.allPolicies['operational_policy']['controlLoop'][this.id]); + }); + // Set the sub-policies + this.allPolicies['operational_policy']['policies'].forEach(function (opPolicyElemIndex, opPolicyElemValue) { + + /* var formNum = add_one_more(); + forEach(document.getElementsByClassName('policyProperties').find('.form-control'), function(opPolicyPropIndex, opPolicyPropValue) { + + $("#formId" + formNum + " .policyProperties").find("#" + opPolicyPropValue.id).val( + allPolicies['operational_policy']['policies'][opPolicyElemIndex][opPolicyPropValue.id]); + }); + + // Initial TargetResourceId options + initTargetResourceIdOptions(allPolicies['operational_policy']['policies'][opPolicyElemIndex]['target']['type'], formNum); + $.each($('.policyTarget').find('.form-control'), function(opPolicyTargetPropIndex, opPolicyTargetPropValue) { + + $("#formId" + formNum + " .policyTarget").find("#" + opPolicyTargetPropValue.id).val( + allPolicies['operational_policy']['policies'][opPolicyElemIndex]['target'][opPolicyTargetPropValue.id]); + }); + + // update the current tab label + $("#go_properties_tab" + formNum).text( + allPolicies['operational_policy']['policies'][opPolicyElemIndex]['id']); + // Check if there is a guard set for it + $.each(allPolicies['guard_policies'], function(guardElemId, guardElemValue) { + + if (guardElemValue.recipe === $($("#formId" + formNum + " #recipe")[0]).val()) { + // Found one, set all guard prop + $.each($('.guardProperties').find('.form-control'), function(guardPropElemIndex, + guardPropElemValue) { + + guardElemValue['id'] = guardElemId; + $("#formId" + formNum + " .guardProperties").find("#" + guardPropElemValue.id).val( + guardElemValue[guardPropElemValue.id]); + }); + iniGuardPolicyType(guardElemId, formNum); + // And finally enable the flag + $("#formId" + formNum + " #enableGuardPolicy").prop("checked", true); + } + });*/ + }); + } + + render() { + return ( + <ModalStyled size="lg" show={this.state.show} onHide={this.handleClose}> + <Modal.Header closeButton> + <Modal.Title>Operational policies</Modal.Title> + </Modal.Header> + <Modal.Body> + <div attribute-test="policywindowproperties" id="configure-widgets" + className="disabled-block-container"> + <div attribute-test="policywindowpropertiesb" className="modal-body row"> + <div className="panel panel-default col-sm-10 policyPanel"> + <form id="operationalPolicyHeaderForm" className="form-horizontal"> + <div className="form-group clearfix"> + <label className="col-sm-2">Parent policy</label> + <div className="col-sm-3" style={{ padding: '0px' }}> + {this.renderPolicyIdSelect()} + </div> + + <label htmlFor="timeout" className="col-sm-3" + style={{ paddingLeft: '5px', paddingRight: '10px' }}>Overall + Time Limit</label> + <div className="col-sm-2" style={{ paddingLeft: '0px' }}> + <input type="text" ng-pattern="/^[0-9]*$/" ng-model="number" + className="form-control" id="timeout" name="timeout" /> + </div> + + <label htmlFor="abatement" className="col-sm-2">Abatement</label> + <div className="col-sm-2" style={{ paddingLeft: '0px' }}> + <select className="form-control" id="abatement" name="abatement"> + <option value="false">False</option> + <option value="true">True</option> + </select> + </div> + </div> + <div className="form-group clearfix row"> + <label className="col-sm-4 control-label" htmlFor="clname">ControlLoopName</label> + <div className="col-sm-8"> + <input type="text" className="form-control" name="controlLoopName" + readOnly="readonly" id="clname" value={this.state.loopCache.getLoopName()} /> + </div> + </div> + </form> + <div className="panel-heading" style={{ backgroundColor: 'white' }}> + <ul id="nav_Tabs" className="nav nav-tabs"> + <li> + <a id="add_one_more" href="#desc_tab"> + <span + className="glyphicon glyphicon-plus" aria-hidden="true"> + </span> + </a> + </li> + </ul> + </div> + <div className="panel-body"> + <div className="tab-content"> + <div id="properties_tab" className="tab-pane fade in active"></div> + </div> + </div> + </div> + + <span id="formSpan" style={{ display: 'none' }}> + <form className="policyProperties form-horizontal" + style={{ border: '2px dotted gray' }} + title="Operational Policy Properties"> + <div className="form-group clearfix"> + <label className="col-sm-4 control-label" htmlFor="id">ID</label> + <div className="col-sm-8"> + <input type="text" className="form-control" name="id" id="id" + onKeyUp="updateTabLabel($event)" /> + <span >ID must be unique</span> + </div> + </div> + <div className="form-group clearfix"> + <label className="col-sm-4 control-label" htmlFor="recipe">Recipe</label> + <div className="col-sm-8"> + <select className="form-control" name="recipe" id="recipe" + ng-model="recipe" ng-click="updateGuardRecipe($event)"> + <option value="">-- choose an option --</option> + <option value="Restart">Restart</option> + <option value="Rebuild">Rebuild</option> + <option value="Migrate">Migrate</option> + <option value="Health-Check">Health-Check</option> + <option value="ModifyConfig">ModifyConfig</option> + <option value="VF Module Create">VF Module Create</option> + <option value="VF Module Delete">VF Module Delete</option> + <option value="Reroute">Reroute</option> + </select> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="retry" className="col-sm-4 control-label"> Retry</label> + <div className="col-sm-8"> + <input type="text" maxLength="5" className="form-control" id="retry" + ng-pattern="/^[0-9]*$/" ng-model="number" name="retry"> + </input> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="timeout" className="col-sm-4 control-label"> + Timeout</label> + <div className="col-sm-8"> + <input type="text" maxLength="5" className="form-control" + id="timeout" ng-pattern="/^[0-9]*$/" ng-model="number" + name="timeout"></input> + </div> + </div> + + <div className="form-group clearfix"> + <label htmlFor="actor" className="col-sm-4 control-label"> Actor</label> + <div className="col-sm-8"> + <select className="form-control" id="actor" name="actor" ng-click="updateGuardActor($event)" ng-model="actor"> + <option value="">-- choose an option --</option> + <option value="APPC">APPC</option> + <option value="SO">SO</option> + <option value="VFC">VFC</option> + <option value="SDNC">SDNC</option>° + <option value="SDNR">SDNR</option>° + </select> + </div> + + <label htmlFor="payload" className="col-sm-4 control-label"> + Payload (YAML)</label> + <div className="col-sm-8"> + <textarea className="form-control" id="payload" name="payload"></textarea> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="success" className="col-sm-4 control-label">When + Success</label> + <div className="col-sm-8"> + <select className="form-control" id="success" name="success" + ng-model="null_dump" + ng-options="policy for policy in policy_ids track by policy"> + <option value="">-- choose an option --</option> + </select> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="failure" className="col-sm-4 control-label">When + Failure</label> + <div className="col-sm-8"> + <select className="form-control" id="failure" name="failure" + ng-model="null_dump" + ng-options="policy for policy in policy_ids track by policy"> + <option value="">-- choose an option --</option> + </select> + + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="failure_timeout" className="col-sm-4 control-label">When + Failure Timeout</label> + <div className="col-sm-8"> + <select className="form-control" id="failure_timeout" + name="failure_timeout" ng-model="null_dump" + ng-options="policy for policy in policy_ids track by policy"> + <option value="">-- choose an option --</option> + </select> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="failure_retries" className="col-sm-4 control-label">When + Failure Retries</label> + <div className="col-sm-8"> + <select className="form-control" id="failure_retries" + name="failure_retries" ng-model="null_dump" + ng-options="policy for policy in policy_ids track by policy"> + <option value="">-- choose an option --</option> + </select> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="failure_exception" className="col-sm-4 control-label">When + Failure Exception</label> + <div className="col-sm-8"> + <select className="form-control" id="failure_exception" + name="failure_exception" ng-model="null_dump" + ng-options="policy for policy in policy_ids track by policy"> + <option value="">-- choose an option --</option> + </select> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="failure_guard" className="col-sm-4 control-label">When + Failure Guard</label> + <div className="col-sm-8"> + <select className="form-control" id="failure_guard" + name="failure_guard" ng-model="null_dump" + ng-options="policy for policy in policy_ids track by policy"> + <option value="">-- choose an option --</option> + </select> + </div> + </div> + </form> + <form className="policyTarget form-horizontal" + title="Operational Policy Target" style={{ border: '2px dotted gray' }}> + <div className="form-group clearfix"> + <label htmlFor="type" className="col-sm-4 control-label"> Target + Type</label> + <div className="col-sm-8"> + <select className="form-control" name="type" id="type" + ng-click="initTargetResourceId($event)" ng-model="type"> + <option value="">-- choose an option --</option> + <option value="VFMODULE">VFMODULE</option> + <option value="VM">VM</option> + <option value="VNF">VNF</option> + </select> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="resourceID" className="col-sm-4 control-label"> + Target ResourceId</label> + <div className="col-sm-8"> + <select className="form-control" name="resourceID" id="resourceID" + ng-click="changeTargetResourceId($event)" + ng-model="resourceId"> + <option value="">-- choose an option --</option> + </select> + </div> + </div> + <div id="metadata"> + <div className="form-group clearfix"> + <label htmlFor="modelInvariantId" className="col-sm-4 control-label"> + Model Invariant Id</label> + <div className="col-sm-8"> + <input className="form-control" name="modelInvariantId" + id="modelInvariantId" readOnly /> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="modelVersionId" className="col-sm-4 control-label"> + Model Version Id</label> + <div className="col-sm-8"> + <input className="form-control" name="modelVersionId" + id="modelVersionId" readOnly /> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="modelName" className="col-sm-4 control-label"> + Model Name</label> + <div className="col-sm-8"> + <input className="form-control" name="modelName" id="modelName" + readOnly /> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="modelVersion" className="col-sm-4 control-label"> + Model Version</label> + <div className="col-sm-8"> + <input className="form-control" name="modelVersion" + id="modelVersion" readOnly /> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="modelCustomizationId" className="col-sm-4 control-label"> + Model Customization Id</label> + <div className="col-sm-8"> + <input className="form-control" name="modelCustomizationId" + id="modelCustomizationId" readOnly /> + </div> + </div> + </div> + </form> + <div className="form-group clearfix"> + <label htmlFor="enableGuardPolicy" className="col-sm-4 control-label"> + Enable Guard Policy</label> + <div className="col-sm-8"> + <input type="checkbox" className="form-control" + name="enableGuardPolicy" id="enableGuardPolicy" /> + </div> + + <div className="col-sm-8"> + <label htmlFor="guardPolicyType" className="col-sm-4 control-label"> + Guard Policy Type</label> <select className="form-control" + name="guardPolicyType" id="guardPolicyType" + ng-click="changeGuardPolicyType()" ng-model="guardType"> + <option value="GUARD_MIN_MAX">MinMax</option> + <option value="GUARD_YAML">FrequencyLimiter</option> + </select> + </div> + </div> + <form className="guardProperties form-horizontal" + title="Guard policy associated" style={{ border: '2px dotted gray' }}> + + <div className="form-group clearfix withnote"> + <label className="col-sm-4 control-label" htmlFor="id">Guard Policy ID</label> + <div className="col-sm-8"> + <input type="text" className="form-control" name="id" id="id" ng-blur="changeGuardId()" ng-model="id" /> + </div> + </div> + <div> + <label className="form-group note">Note: Prefix will be added to Guard Policy ID automatically based on Guard Policy Type</label> + </div> + <div className="form-group clearfix"> + <label className="col-sm-4 control-label" htmlFor="recipe">Recipe</label> + <div className="col-sm-8"> + <input type="text" className="form-control" name="recipe" + readOnly="readonly" id="recipe" /> + </div> + </div> + <div className="form-group clearfix"> + <label className="col-sm-4 control-label" htmlFor="clname">ControlLoopName</label> + <div className="col-sm-8"> + <input type="text" className="form-control" name="clname" + readOnly="readonly" id="clname" ng-model="clname" /> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="actor" className="col-sm-4 control-label">Actor</label> + <div className="col-sm-8"> + <input type="text" className="form-control" name="actor" + readOnly="readonly" id="actor" /> + </div> + </div> + <div className="form-group clearfix"> + + <label htmlFor="targets" className="col-sm-4 control-label">Guard + targets</label> + <div className="col-sm-8"> + <input className="form-control" name="targets" id="targets" /> + </div> + </div> + + <div className="form-group clearfix" id="minMaxGuardPolicyDiv"> + <label htmlFor="min" className="col-sm-4 control-label"> Min + Guard</label> + <div className="col-sm-8"> + <input className="form-control" name="min" id="min" /> + </div> + <label htmlFor="max" className="col-sm-4 control-label"> Max + Guard</label> + <div className="col-sm-8"> + <input className="form-control" name="max" id="max" /> + </div> + </div> + <div className="form-group clearfix" + id="frequencyLimiterGuardPolicyDiv" style={{ display: 'none' }}> + <label htmlFor="limit" className="col-sm-4 control-label">Limit</label> + <div className="col-sm-8"> + <input className="form-control" name="limit" id="limit" /> + </div> + <label htmlFor="timeUnits" className="col-sm-4 control-label">Time Units</label> + <div className="col-sm-8"> + <select className="form-control" name="timeUnits" + id="timeUnits"> + <option value=""></option> + <option value="minute">minute</option> + <option value="hour">hour</option> + <option value="day">day</option> + <option value="week">week</option> + <option value="month">month</option> + <option value="year">year</option> + </select> + </div> + <label htmlFor="timeWindow" className="col-sm-4 control-label">Time Window</label> + <div className="col-sm-8"> + <input className="form-control" name="timeWindow" id="timeWindow" /> + </div> + </div> + <div className="form-group clearfix"> + <label htmlFor="guardActiveStart" className="col-sm-4 control-label"> + Guard Active Start</label> + <div className="col-sm-8"> + <input className="form-control" name="guardActiveStart" + id="guardActiveStart" value="00:00:00Z" /> + </div> + <label htmlFor="guardActiveEnd" className="col-sm-4 control-label"> + Guard Active End</label> + <div className="col-sm-8"> + <input className="form-control" name="guardActiveEnd" + id="guardActiveEnd" value="00:00:01Z" /> + </div> + </div> + + </form> + + </span> + </div> + </div> + + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={this.handleClose}> + Close + </Button> + <Button variant="primary" onClick={this.handleClose}> + Save Changes + </Button> + </Modal.Footer> + </ModalStyled> + + ); + } +}
\ No newline at end of file diff --git a/ui-react/src/components/dialogs/OperationalPolicy/template.json b/ui-react/src/components/dialogs/OperationalPolicy/template.json new file mode 100644 index 000000000..6b4477f88 --- /dev/null +++ b/ui-react/src/components/dialogs/OperationalPolicy/template.json @@ -0,0 +1,52 @@ +{ + "operationalPolicies": [ + { + "name": "OPERATIONAL_LOOP_NAME", + "configurationsJson": { + "guard_policies": { + "guard.minmax.new": { + "recipe": "", + "clname": "LOOP_NAME", + "actor": "", + "targets": "", + "min": "", + "max": "", + "limit": "", + "timeUnits": "", + "timeWindow": "", + "guardActiveStart": "00:00:00Z", + "guardActiveEnd": "00:00:01Z" + } + }, + "operational_policy": { + "controlLoop": { + "trigger_policy": "new", + "timeout": "0", + "abatement": "false", + "controlLoopName": "LOOP_h2NMX_v1_0_ResourceInstanceName1_tca" + }, + "policies": [ + { + "id": "new", + "recipe": "", + "retry": "0", + "timeout": "0", + "actor": "", + "payload": "", + "success": "", + "failure": "", + "failure_timeout": "", + "failure_retries": "", + "failure_exception": "", + "failure_guard": "", + "target": { + "type": "VM", + "resourceID": "" + } + } + ] + } + } + } + ] +}
\ No newline at end of file diff --git a/ui-react/src/components/dialogs/UserInfo.js b/ui-react/src/components/dialogs/UserInfo.js new file mode 100644 index 000000000..b8d68b849 --- /dev/null +++ b/ui-react/src/components/dialogs/UserInfo.js @@ -0,0 +1,161 @@ +/*- + * ============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 UserInfo extends React.Component { + + constructor(props, context) { + super(props, context); + + this.handleClose = this.handleClose.bind(this); + this.initialValues = this.initialValues.bind(this); + this.renderReadTemplatePermission = this.renderReadTemplatePermission.bind(this); + this.renderReadModelPermission = this.renderReadModelPermission.bind(this); + this.renderReadToscaPermission = this.renderReadToscaPermission.bind(this); + this.renderUpdateTemplatePermission = this.renderUpdateTemplatePermission.bind(this); + this.renderUpdateModelPermission = this.renderUpdateModelPermission.bind(this); + this.renderUpdateToscaPermission = this.renderUpdateToscaPermission.bind(this); + this.renderUserName = this.renderUserName.bind(this); + this.state = { + show: true, + userInfo: {permissionReadTemplate: true, + permissionReadCl: true, + permissionReadTosca: true, + permissionUpdateCl: true, + permissionUpdateTemplate: true, + permissionUpdateTosca: true, + userName: 'admin', + cldsVersion: '1.0.0' + } + }; + + } + initialValues() { + UserService.getUserInfo().then(userInfo => { + this.setState({ userInfo: userInfo }) + }); + } + handleClose() { + this.props.history.push('/'); + } + renderReadTemplatePermission() { + if (this.state.userInfo["permissionReadTemplate"]) { + return <Form.Control plaintext readOnly defaultValue="Read Template" /> + } else { + return; + } + } + renderReadModelPermission() { + if (this.state.userInfo["permissionReadCl"]) { + return <Form.Control plaintext readOnly defaultValue="Read Model" /> + } else { + return; + } + } + renderReadToscaPermission() { + if (this.state.userInfo["permissionReadTosca"]) { + return <Form.Control plaintext readOnly defaultValue="Read Tosca" /> + } else { + return; + } + } + renderUpdateTemplatePermission() { + if (this.state.userInfo["permissionUpdateTemplate"]) { + return <Form.Control plaintext readOnly defaultValue="Edit Template" /> + } else { + return; + } + } + renderUpdateModelPermission() { + if (this.state.userInfo["permissionUpdateCl"]) { + return <Form.Control plaintext readOnly defaultValue="Edit Model" /> + } else { + return; + } + } + renderUpdateToscaPermission() { + if (this.state.userInfo["permissionUpdateTosca"]) { + return <Form.Control plaintext readOnly defaultValue="Edit Tosca" /> + } 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} onEntered={this.initialValues}> + <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.renderReadTemplatePermission()} + {this.renderReadModelPermission()} + {this.renderReadToscaPermission()} + {this.renderUpdateTemplatePermission()} + {this.renderUpdateModelPermission()} + {this.renderUpdateToscaPermission()} + </Col> + </Form.Group> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" type="null" onClick={this.handleClose}>Cancel</Button> + </Modal.Footer> + </ModalStyled> + ); + } +} diff --git a/ui-react/src/components/loop_viewer/logs/LoopLogs.js b/ui-react/src/components/loop_viewer/logs/LoopLogs.js new file mode 100644 index 000000000..b6a777a40 --- /dev/null +++ b/ui-react/src/components/loop_viewer/logs/LoopLogs.js @@ -0,0 +1,97 @@ +/*- + * ============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.loopViewerHeaderBackgroundColor}; + padding: 10px 10px; + color: ${props => props.theme.loopViewerHeaderFontColor}; +` +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/ui-react/src/components/loop_viewer/status/LoopStatus.js b/ui-react/src/components/loop_viewer/status/LoopStatus.js new file mode 100644 index 000000000..141a41f51 --- /dev/null +++ b/ui-react/src/components/loop_viewer/status/LoopStatus.js @@ -0,0 +1,105 @@ +/*- +* ============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/ui-react/src/components/loop_viewer/svg/LoopComponentConverter.js b/ui-react/src/components/loop_viewer/svg/LoopComponentConverter.js new file mode 100644 index 000000000..a409d2cd0 --- /dev/null +++ b/ui-react/src/components/loop_viewer/svg/LoopComponentConverter.js @@ -0,0 +1,18 @@ +export default class LoopComponentConverter { + + static buildMapOfComponents(loopCache) { + var componentsMap = new Map([]); + if (typeof (loopCache.getMicroServicePolicies()) !== "undefined") { + loopCache.getMicroServicePolicies().forEach(ms => { + componentsMap.set(ms.name, "/configurationPolicyModal/"+ms.name); + }) + } + if (typeof (loopCache.getOperationalPolicies()) !== "undefined") { + loopCache.getOperationalPolicies().forEach(op => { + componentsMap.set(op.name, "/operationalPolicyModal"); + }) + } + componentsMap.set("OperationalPolicy","/operationalPolicyModal"); + return componentsMap; + } +} diff --git a/ui-react/src/components/loop_viewer/svg/LoopSvg.js b/ui-react/src/components/loop_viewer/svg/LoopSvg.js new file mode 100644 index 000000000..3ac2f31fd --- /dev/null +++ b/ui-react/src/components/loop_viewer/svg/LoopSvg.js @@ -0,0 +1,101 @@ +/*- + * ============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 LoopCache from '../../../api/LoopCache'; +import { withRouter } from "react-router"; +import LoopService from '../../../api/LoopService'; +import LoopComponentConverter from './LoopComponentConverter'; + +const LoopViewSvgDivStyled = styled.div` + overflow: hidden; + background-color: ${props => (props.theme.loopViewerBackgroundColor)}; + border: 1px solid; + border-color: ${props => (props.theme.loopViewerHeaderColor)}; + margin-left: auto; + margin-right:auto; + text-align: center; + +` + +class LoopViewSvg extends React.Component { + + static emptySvg = "<svg><text x=\"20\" y=\"40\">No LOOP (SVG)</text></svg>"; + + state = { + svgContent: LoopViewSvg.emptySvg, + loopCache: new LoopCache({}), + componentModalMapping: new Map([]), + } + + constructor(props) { + super(props); + this.handleSvgClick = this.handleSvgClick.bind(this); + this.getSvg = this.getSvg.bind(this); + this.state.loopCache = props.loopCache; + this.state.componentModalMapping = LoopComponentConverter.buildMapOfComponents(props.loopCache); + this.getSvg(props.loopCache.getLoopName()); + } + + shouldComponentUpdate(nextProps, nextState) { + return this.state.svgContent !== nextState.svgContent; + } + + componentWillReceiveProps(newProps) { + this.setState({ + loopCache: newProps.loopCache, + componentModalMapping: LoopComponentConverter.buildMapOfComponents(newProps.loopCache), + + }); + this.getSvg(newProps.loopCache.getLoopName()); + } + + getSvg(loopName) { + if (typeof loopName !== "undefined") { + LoopService.getSvg(loopName).then(svgXml => { + if (svgXml.length !== 0) { + this.setState({ svgContent: svgXml }) + } else { + this.setState({ svgContent: LoopViewSvg.emptySvg }) + } + }); + } + } + + handleSvgClick(event) { + console.debug("svg click event received"); + var elementName = event.target.parentNode.parentNode.parentNode.getAttribute('data-element-id'); + console.info("SVG element clicked", elementName); + this.props.history.push(this.state.componentModalMapping.get(elementName)); + } + + render() { + return ( + <LoopViewSvgDivStyled dangerouslySetInnerHTML={{ __html: this.state.svgContent }} onClick={this.handleSvgClick}> + + </LoopViewSvgDivStyled> + ); + } +} + +export default withRouter(LoopViewSvg);
\ No newline at end of file diff --git a/ui-react/src/components/loop_viewer/svg/example.svg b/ui-react/src/components/loop_viewer/svg/example.svg new file mode 100644 index 000000000..a7c40ee27 --- /dev/null +++ b/ui-react/src/components/loop_viewer/svg/example.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg"><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="start-circle" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><circle fill="none" r="17" cx="18" cy="41"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="Arrow-82c14603-02fc-4df7-8977-9b10e4c775d1" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><line y2="41" fill="none" x1="35" x2="123" y1="41"/><polygon fill="none" points=" 121 39 121 43 125 41"/><polygon points=" 121 39 121 43 125 41" stroke="none"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="VES" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><rect fill="none" x="127" width="123" y="1" height="82"/></g><g fill-opacity="0" fill="rgb(0,0,0)" text-rendering="optimizeQuality" shape-rendering="geometricPrecision" stroke="rgb(0,0,0)" stroke-opacity="0" stroke-width="2"><rect x="127" width="123" y="1" height="82" stroke="none"/></g><g text-rendering="optimizeQuality" stroke-width="2" shape-rendering="geometricPrecision" font-family="sans-serif"><text x="176.5" xml:space="preserve" y="46.5" stroke="none">VES</text><line y2="83" fill="none" x1="147" x2="147" y1="1"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="Arrow-dbbb2d5a-e9c4-446d-92b9-c71908854434" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><line y2="41" fill="none" x1="250" x2="338" y1="41"/><polygon fill="none" points=" 336 39 336 43 340 41"/><polygon points=" 336 39 336 43 340 41" stroke="none"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="TCA_Jbv1z_v1_0_ResourceInstanceName1_tca" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><rect fill="none" x="342" width="123" y="1" height="82"/></g><g fill-opacity="0" fill="rgb(0,0,0)" text-rendering="optimizeQuality" shape-rendering="geometricPrecision" stroke="rgb(0,0,0)" stroke-opacity="0" stroke-width="2"><rect x="342" width="123" y="1" height="82" stroke="none"/></g><g text-rendering="optimizeQuality" stroke-width="2" shape-rendering="geometricPrecision" font-family="sans-serif"><text x="392" xml:space="preserve" y="46.5" stroke="none">TCA</text><line y2="61" fill="none" x1="342" x2="465" y1="61"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="Arrow-3892abbc-c49c-40df-984b-8959b6df44e6" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><line y2="41" fill="none" x1="465" x2="553" y1="41"/><polygon fill="none" points=" 551 39 551 43 555 41"/><polygon points=" 551 39 551 43 555 41" stroke="none"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="OperationalPolicy" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><rect fill="none" x="557" width="123" y="1" height="82"/></g><g fill-opacity="0" fill="rgb(0,0,0)" text-rendering="optimizeQuality" shape-rendering="geometricPrecision" stroke="rgb(0,0,0)" stroke-opacity="0" stroke-width="2"><rect x="557" width="123" y="1" height="82" stroke="none"/></g><g text-rendering="optimizeQuality" stroke-width="2" shape-rendering="geometricPrecision" font-family="sans-serif"><text x="564.5" xml:space="preserve" y="46.5" stroke="none">OperationalPolicy</text><line y2="1" fill="none" x1="557" x2="618" y1="42"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="Arrow-44a8b77e-d0eb-4c0d-82b6-0822ff35573f" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="2"><line y2="41" fill="none" x1="680" x2="768" y1="41"/><polygon fill="none" points=" 766 39 766 43 770 41"/><polygon points=" 766 39 766 43 770 41" stroke="none"/></g></g></g><g fill-opacity="1" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" font-family="'Dialog'" font-style="normal" data-element-id="stop-circle" stroke-linejoin="miter" font-size="12px" image-rendering="auto" stroke-dashoffset="0"><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"/><g><g shape-rendering="geometricPrecision" text-rendering="optimizeQuality" stroke-width="4"><circle fill="none" r="17" cx="789" cy="41"/></g></g></g></svg>
\ No newline at end of file diff --git a/ui-react/src/components/menu/MenuBar.js b/ui-react/src/components/menu/MenuBar.js new file mode 100644 index 000000000..5022152e3 --- /dev/null +++ b/ui-react/src/components/menu/MenuBar.js @@ -0,0 +1,87 @@ +/*- + * ============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 Nav from 'react-bootstrap/Nav'; +import Navbar from 'react-bootstrap/Navbar'; +import NavDropdown from 'react-bootstrap/NavDropdown'; +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.menuColor}; + 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.loopViewerHeaderBackgroundColor}; + color: ${props => props.theme.loopViewerHeaderFontColor}; + } +`; +const StyledNavLink = styled(Nav.Link)` + color: ${props => props.theme.menuColor}; + background-color: ${props => props.theme.menuBackgroundColor}; + font-weight: normal; + padding: .25rem 1.5rem; + :hover { + background-color: ${props => props.theme.loopViewerHeaderBackgroundColor}; + color: ${props => props.theme.loopViewerHeaderFontColor} + } +`; +export default class MenuBar extends React.Component { + + render () { + return ( + <Navbar.Collapse> + <NavDropdown title="Closed Loop"> + <NavDropdown.Item as={StyledLink} to="/openLoop">Open CL</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/loopProperties">Properties CL</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/closeLoop">Close Model</NavDropdown.Item> + </NavDropdown> + <NavDropdown title="Manage"> + <NavDropdown.Item as={StyledLink} to="/submit">Submit</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/stop">Stop</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/restart">Restart</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/delete">Delete</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/deploy">Deploy</NavDropdown.Item> + <NavDropdown.Item as={StyledLink} to="/undeploy">UnDeploy</NavDropdown.Item> + </NavDropdown> + <NavDropdown title="View"> + <NavDropdown.Item as={StyledLink} to="/refreshStatus">Refresh Status</NavDropdown.Item> + </NavDropdown> + <NavDropdown 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> + </NavDropdown> + </Navbar.Collapse> + ); + } +} diff --git a/ui-react/src/components/menu/PerformActions.js b/ui-react/src/components/menu/PerformActions.js new file mode 100644 index 000000000..9c34e141b --- /dev/null +++ b/ui-react/src/components/menu/PerformActions.js @@ -0,0 +1,85 @@ +/*- + * ============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 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; + console.log("Perform action:" + action); + LoopActionService.performAction(loopName, action).then(pars => { + alert("Action " + action + " successfully performed"); + // refresh status and update loop logs + this.refreshStatus(loopName); + }) + .catch(error => { + alert("Action " + action + " failed"); + // refresh status and update loop logs + this.refreshStatus(loopName); + }); + + } + + refreshStatus(loopName) { + LoopActionService.refreshStatus(loopName).then(data => { + this.props.updateLoopFunction(data); + this.props.history.push('/'); + }) + .catch(error => { + this.props.history.push('/'); + }); + } + + render() { + return ( + <StyledSpinnerDiv> + <Spinner animation="border" role="status"> + </Spinner> + </StyledSpinnerDiv> + ); + } +} diff --git a/ui-react/src/components/menu/RefreshStatus.js b/ui-react/src/components/menu/RefreshStatus.js new file mode 100644 index 000000000..cf08655ee --- /dev/null +++ b/ui-react/src/components/menu/RefreshStatus.js @@ -0,0 +1,66 @@ +/*- + * ============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() { + console.log("Refresh status for: " + this.state.loopName); + // refresh status and update loop logs + LoopActionService.refreshStatus(this.state.loopName).then(data => { + alert("Status successfully refreshed") + this.props.updateLoopFunction(data); + this.props.history.push('/'); + }) + .catch(error => { + alert("Status refreshing failed"); + this.props.history.push('/'); + }); + } + + render() { + return ( + <StyledSpinnerDiv> + <Spinner animation="border" role="status"> + </Spinner> + </StyledSpinnerDiv> + ); + } +} diff --git a/ui-react/src/index.js b/ui-react/src/index.js new file mode 100644 index 000000000..39df36427 --- /dev/null +++ b/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, BrowserRouter } from 'react-router-dom' + + +const routing = ( + <BrowserRouter forceRefresh={false}> + <Route path="/" component={OnapClamp}/> + </BrowserRouter> +); + +ReactDOM.render( + routing, + document.getElementById('root') +) diff --git a/ui-react/src/logo.png b/ui-react/src/logo.png Binary files differnew file mode 100644 index 000000000..c6f6857a5 --- /dev/null +++ b/ui-react/src/logo.png diff --git a/ui-react/src/setupTests.js b/ui-react/src/setupTests.js new file mode 100644 index 000000000..fc7b0dce1 --- /dev/null +++ b/ui-react/src/setupTests.js @@ -0,0 +1,4 @@ +import Enzyme from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +Enzyme.configure({ adapter: new Adapter() }); diff --git a/ui-react/src/theme/globalStyle.js b/ui-react/src/theme/globalStyle.js new file mode 100644 index 000000000..cbd86b199 --- /dev/null +++ b/ui-react/src/theme/globalStyle.js @@ -0,0 +1,91 @@ +/*- + * ============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; + color: ${props => props.theme.fontNormal}; + background-color: ${props => props.theme.backgroundColor}; + } + + span { + font-family: ${props => props.theme.fontFamily}; + font-size: ${props => props.theme.fontSize}; + font-weight: bold; + color: ${props => props.theme.fontNormal}; + background-color: ${props => props.theme.backgroundColor}; + } + + a { + font-family: ${props => props.theme.fontFamily}; + font-size: ${props => props.theme.fontSize}; + font-weight: bold; + color: ${props => props.theme.fontNormal}; + background-color: ${props => props.theme.backgroundColor}; + + } + + div { + font-family: ${props => props.theme.fontFamily}; + font-size: ${props => props.theme.fontSize}; + border-radius: 4px; + color: ${props => props.theme.fontNormal}; + background-color: ${props => (props.theme.backgroundColor)}; + margin-top: 1px; + } + + svg { + overflow: hidden; + width: 100%; + height: 100%; + } +` + +export const DefaultClampTheme = { + fontDanger: '#eb238e', + fontWarning: '#eb238e', + fontLight: '#ffffff', + fontDark: '#888888', + fontHighlight: '#ffff00', + fontNormal: 'black', + + backgroundColor: '#eeeeee', + fontFamily: 'Arial, Sans-serif', + fontSize: '15px', + + loopViewerBackgroundColor: 'white', + loopViewerFontColor: 'yellow', + loopViewerHeaderBackgroundColor: '#337ab7', + loopViewerHeaderFontColor: 'white', + + menuBackgroundColor: 'white', + menuFontColor: 'black', + menuHighlightedBackgroundColor: '#337ab7', + menuHighlightedFontColor: 'white', +}; |