diff options
author | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2020-08-31 13:24:43 +0200 |
---|---|---|
committer | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2020-08-31 13:24:43 +0200 |
commit | 7058ffa19dde75c14eb89270c1a57926c0bce4cc (patch) | |
tree | d4d278eb926df35832fd69ea778bd9e6dec0f126 /sdnr/wt/odlux/apps/networkMapApp/src/components/details | |
parent | 4bd84bebdaa0c2d82050fbedd1fa8260eb62146d (diff) |
Add networkMap
Add NetworkMap to odlux
Issue-ID: CCSDK-2560
Signed-off-by: Aijana Schumann <aijana.schumann@highstreet-technologies.com>
Change-Id: I204bcace9d12f8a26edfa347ee9b7d292c52f030
Diffstat (limited to 'sdnr/wt/odlux/apps/networkMapApp/src/components/details')
3 files changed, 459 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx new file mode 100644 index 000000000..a2e51d30f --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx @@ -0,0 +1,205 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH 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 * as React from 'react' + +import connect, { IDispatcher, Connect } from '../../../../../framework/src/flux/connect'; + +import { site, Device } from '../../model/site'; +import Typography from '@material-ui/core/Typography'; +import { link } from '../../model/link'; +import { Breadcrumbs, Link, Paper } from '@material-ui/core'; +import SiteDetails from './siteDetails'; +import LinkDetails from './linkDetails'; +import { URL_API, URL_BASEPATH } from '../../config'; +import { SelectSiteAction, SelectLinkAction, AddToHistoryAction, ClearHistoryAction, CheckDeviceList, ClearDetailsAction } from '../../actions/detailsAction'; +import { HistoryEntry } from '../../model/historyEntry'; +import { HighlightLinkAction, HighlightSiteAction, RemoveHighlightingAction } from '../../actions/mapActions'; +import { isSite } from '../../utils/utils'; +import { IApplicationStoreState } from '../../../../../framework/src/store/applicationStore'; +import { NavigateToApplication } from '../../../../../framework/src/actions/navigationActions'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; + + +const Details: React.FunctionComponent<porps> = (props) => { + + const [message, setMessage] = React.useState("No data selected."); + + + //on mount + React.useEffect(() => { + const detailsId = getDetailsIdFromUrl(); + if (detailsId !== null && props.data?.name !== detailsId) { + loadDetailsData(detailsId) + } + + }, []); + + // if url changed + React.useEffect(() => { + const detailsId = getDetailsIdFromUrl(); + console.log(detailsId) + if (detailsId !== null && props.data?.name !== detailsId) { + loadDetailsData(detailsId) + } + else if(detailsId===null){ + setMessage("No data selected."); + props.clearDetails(); + props.undoMapSelection(); + } + + }, [props.location.pathname]); + + //update url if new element loaded + React.useEffect(() => { + if (props.data !== null) { + const currentUrl = window.location.href; + const parts = currentUrl.split(URL_BASEPATH); + const detailsPath = parts[1].split("/details/"); + props.history.replace(`/${URL_BASEPATH}${detailsPath[0]}/details/${props.data.name}`) + } + + }, [props.data]) + + const onLinkClick = async (id: string) => { + const result = await fetch(`${URL_API}/link/${id}`); + if(result.ok){ + const resultAsJson = await result.json(); + const link = resultAsJson as link; + props.selectLink(link); + props.addHistory({ id: props.data!.name, data: props.data! }); + props.highlightLink(link); + + } + } + + const backClick = (e: any) => { + if (isSite(props.breadcrumbs[0].data)) { + props.selectSite(props.breadcrumbs[0].data) + props.highlightSite(props.breadcrumbs[0].data); + + } else { + props.selectLink(props.breadcrumbs[0].data); + props.highlightLink(props.breadcrumbs[0].data); + + } + + props.clearHistory(); + e.preventDefault(); + } + + const createDetailPanel = (data: site | link) => { + + if (isSite(data)) { + return <SiteDetails navigate={props.navigateToApplication} updatedDevices={props.updatedDevices} loadDevices={props.loadDevices} site={data} onLinkClick={onLinkClick} /> + } else { + return <LinkDetails link={data} /> + } + } + + const getDetailsIdFromUrl = () =>{ + const currentUrl = window.location.href; + const parts = currentUrl.split(URL_BASEPATH); + const detailsPath = parts[1].split("/details/") + return detailsPath[1] ? detailsPath[1] : null; + } + + const loadDetailsData = (id: string) =>{ + + fetch(`${URL_API}/link/${id}`) + .then(res => { + if (res.ok) + return res.json() + else + return Promise.reject() + + }) + .then(result => { + props.selectLink(result) + props.highlightLink(result); + + }) + .catch(error => { + + fetch(`${URL_API}/site/${id}`) + .then(res => { + if (res.ok) + return res.json() + else return Promise.reject(); + }) + .then(result => { + props.selectSite(result); + props.highlightSite(result); + }) + .catch(error =>{ + setMessage("No element with name " + id + " found"); + props.clearDetails(); + props.undoMapSelection(); + }); + }) + } + + + return (<div style={{ width: '30%', background: "#bbbdbf", padding: "20px", alignSelf:"stretch" }}> + <Paper style={{ height:"100%"}} id="site-details-panel" > + { + props.breadcrumbs.length > 0 && + <Breadcrumbs style={{ marginLeft: "15px", marginTop: "5px" }} aria-label="breadcrumb"> + <Link color="inherit" href="/" onClick={backClick}> + {props.breadcrumbs[0].id} + </Link> + <Link> + {props.data?.name} + </Link> + </Breadcrumbs> + } + { + props.data !== null ? + createDetailPanel(props.data) + : <Typography style={{ marginTop: "5px" }} align="center" variant="body1">{message}</Typography> + + } + </Paper> + </div>) +} + +type porps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>; + +//select always via details? +const mapStateToProps = (state: IApplicationStoreState) => ({ + data: state.network.details?.data, + breadcrumbs: state.network.details.history, + updatedDevices: state.network.details.checkedDevices +}); + +const mapDispatchToProps = (dispatcher: IDispatcher) => ({ + selectSite: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)), + selectLink: (link: link) => dispatcher.dispatch(new SelectLinkAction(link)), + clearDetails: () => dispatcher.dispatch(new ClearDetailsAction()), + addHistory: (newEntry: HistoryEntry) => dispatcher.dispatch(new AddToHistoryAction(newEntry)), + clearHistory: () => dispatcher.dispatch(new ClearHistoryAction()), + highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)), + highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)), + loadDevices: async (networkElements: Device[]) => { await dispatcher.dispatch(CheckDeviceList(networkElements)) }, + navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path, "test3")), + undoMapSelection: () => dispatcher.dispatch(new RemoveHighlightingAction()) + +}) + + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Details));
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx new file mode 100644 index 000000000..de1bf6b16 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx @@ -0,0 +1,101 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH 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 * as React from 'react'; + +import { link } from '../../model/link'; +import { TextField, Tabs, Tab, Typography, AppBar, Button, Link } from '@material-ui/core'; +import DenseTable from '../denseTable'; +import { LatLonToDMS } from '../../utils/mapUtils'; + +type panelId = "siteA" | "siteB"; +type props = { link: link }; + +const LinkDetails: React.FunctionComponent<props> = (props) => { + + const [value, setValue] = React.useState<panelId>("siteA"); + const [height, setHeight] = React.useState(330); + + const handleResize = () =>{ + console.log("resize") + const el = document.getElementById('site-details-panel')?.getBoundingClientRect(); + const el2 = document.getElementById('site-tabs')?.getBoundingClientRect(); + + if(el && el2){ + if(props.link.type==="microwave") + setHeight(el!.height - el2!.y -30); + else + setHeight(el!.height - el2!.y +20); + + } + } + + //on mount + React.useEffect(()=>{ + handleResize(); + + //window.addEventListener("resize", handleResize); + },[]); + + React.useEffect(()=>{ + handleResize(); + }, [props.link]) + + const onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: panelId) => { + setValue(newValue); + } + + const onCalculateLinkClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>{ + e.preventDefault(); + const siteA= props.link.locationA; + const siteB =props.link.locationB; + const nameA = props.link.siteA; + const nameB = props.link.siteB; + const distance = props.link.length > 0 ? props.link.length : props.link.calculatedLength; + const azimuthA = props.link.azimuthA; + const azimuthB = props.link.azimuthB; + window.open(`/#/linkCalculation?lat1=${siteA.lat}&lon1=${siteA.lon}&lat2=${siteB.lat}&lon2=${siteB.lon}&siteA=${nameA}&siteB=${nameB}&azimuthA=${azimuthA}&azimuthB=${azimuthB}&distance=${distance}`) + + } + + const data = [ + + {name:"Site Name", val1: props.link.siteA, val2: props.link.siteB}, + {name:"Latitude", val1: LatLonToDMS(props.link.locationA.lat), val2: LatLonToDMS(props.link.locationB.lat)}, + {name:"Longitude", val1: LatLonToDMS(props.link.locationA.lon, true), val2: LatLonToDMS(props.link.locationB.lon, true)}, + {name:"Azimuth in °", val1: props.link.azimuthA.toFixed(2), val2: props.link.azimuthB.toFixed(2)} +]; + + return (<div style={{ paddingLeft: "15px", paddingRight: "15px", paddingTop: "0px", display: 'flex', flexDirection: 'column' }}> + <h2>{props.link.name}</h2> + <TextField disabled style={{ marginTop: "5px" }} value="Unkown" label="Operator" /> + <TextField disabled style={{ marginTop: "5px" }} value={props.link.type} label="Type" /> + <TextField disabled style={{ marginTop: "5px" }} value={props.link.length.toFixed(2)} label="Distance planned in km" /> + <TextField disabled style={{ marginTop: "5px" }} value={props.link.calculatedLength.toFixed(2)} label="Distance calculated in km" /> + + <AppBar position="static" id="site-tabs" style={{ marginTop: "20px", background: '#2E3B55' }}> + <Typography style={{ margin:"5px"}}>SITE DETAILS</Typography> + </AppBar> + <DenseTable height={height} hover={false} headers={["", "Site A", "Site B"]} data={data} /> + { + props.link.type==="microwave" && <Button style={{marginTop:20}} fullWidth variant="contained" color="primary" onClick={onCalculateLinkClick}>Calculate link</Button> + } + </div>) +} + +export default LinkDetails;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx new file mode 100644 index 000000000..a95666e38 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx @@ -0,0 +1,153 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH 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 * as React from 'react'; +import { TextField, Tabs, Tab, Typography, AppBar, Button, Tooltip } from '@material-ui/core'; + + +import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../../framework/src/components/material-table"; + + +import { site, Device } from '../../model/site'; +import DenseTable from '../denseTable'; +import { LatLonToDMS } from '../../utils/mapUtils'; + +type minLinks = { name: string, azimuth: string} + +const FaultAlarmNotificationTable = MaterialTable as MaterialTableCtorType<minLinks>; + + +type panelId="links" | "nodes"; +type props = { site: site, updatedDevices: Device[]|null, navigate(applicationName: string, path?: string):void, onLinkClick(id: string): void, loadDevices(devices:Device[]): void }; + +const SiteDetails: React.FunctionComponent<props> = (props) => { + + const [value, setValue] = React.useState<panelId>("links"); + const [height, setHeight] = React.useState(330); + + const handleResize = () =>{ + //console.log("resize") + const el = document.getElementById('site-details-panel')?.getBoundingClientRect(); + const el2 = document.getElementById('site-tabs')?.getBoundingClientRect(); + + if(el && el2){ + setHeight(el!.height - el2!.y +20); + } + + } + + //on mount + React.useEffect(()=>{ + handleResize(); + + window.addEventListener("resize", ()=>{console.log("really got resized.")}); + },[]); + + // on update + React.useEffect(()=>{ + + props.loadDevices(props.site.devices); + handleResize(); + + }, [props.site]) + + const onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: panelId) => { + setValue(newValue); + } + + const linkRows: minLinks[] = props.site.links.map(link=> + { + return {name: link.name, azimuth: link.azimuthB.toFixed(2) } + }); + + + + return (<div style={{ padding: '15px', display: "flex", flexDirection:"column", minWidth:0, minHeight:0 }}> + <h2 >{props.site.name}</h2> + { + props.site.operator !== '' && props.site.operator !== null ? + <TextField disabled={true} value={props.site.operator} label="Operator" /> : + <TextField disabled={true} value="Unkown" label="Operator" style={{ marginTop: "5px" }} /> + } + { + props.site.type !== undefined && props.site.type.length > 0 && + <TextField disabled={true} value={props.site.type} label="Type" style={{ marginTop: "5px" }} /> + } + { + props.site.address !== undefined && props.site.address.length > 0 && + <TextField disabled={true} value={props.site.address} label="Adress" style={{ marginTop: "5px" }} /> + } + { + props.site.heighAGLInMeters !== undefined && props.site.heighAGLInMeters > 0 && + <TextField disabled={true} value={props.site.heighAGLInMeters} label="AMSL in meters" style={{ marginTop: "5px" }} /> + } + { + props.site.antennaHeightAGLInMeters !== undefined && props.site.antennaHeightAGLInMeters > 0 && + <TextField disabled={true} value={props.site.antennaHeightAGLInMeters} label="Atenna above ground in meters" style={{ marginTop: "5px" }} /> + } + + <TextField style={{ marginTop: "5px" }} disabled={true} value={LatLonToDMS(props.site.geoLocation.lat)} label="Latitude" /> + <TextField style={{ marginTop: "5px" }} disabled={true} value={LatLonToDMS(props.site.geoLocation.lon, true)} label="Longitude" /> + + <AppBar position="static" style={{ marginTop: "5px", background: '#2E3B55' }}> + <Tabs id="site-tabs" value={value} onChange={onHandleTabChange} aria-label="simple tabs example"> + <Tab label="Links" value="links" /> + <Tab label="Nodes" value="nodes" /> + </Tabs> + </AppBar> + { + value === "links" && + <> + { + props.site.links.length === 0 && + <Typography variant="body1" style={{ marginTop: '10px' }}>No links available.</Typography> + } + + { + props.site.links.length > 0 && + <DenseTable height={height} hover={true} headers={["Link Name", "Azimuth in °"]} data={linkRows} onClick={props.onLinkClick} ></DenseTable> + /** + * + * */ + + + } + + </> + + } + { + value === "nodes" && + <> + { + props.site.devices.length === 0 && + <Typography variant="body1" style={{ marginTop: '10px' }}>No nodes available.</Typography> + } + + { + props.site.devices.length>0 && props.updatedDevices !== null && + <DenseTable navigate={props.navigate} height={height} hover={false} headers={["ID","Name","Type", "Manufacturer","Owner","Status", "Ports", "Actions"]} actions={true} data={props.updatedDevices!} /> + } + </> + } + </div> + ) + +} + +export default SiteDetails;
\ No newline at end of file |