Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 1 | /* |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 2 | * Copyright 2019-present Open Networking Foundation |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 16 | import {Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core'; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 17 | import {animate, state, style, transition, trigger} from '@angular/animations'; |
Sean Condon | 3dd062f | 2020-04-14 09:25:00 +0100 | [diff] [blame] | 18 | import {DetailsPanelBaseImpl, FnService, LionService, LogService, WebSocketService} from 'org_onosproject_onos/web/gui2-fw-lib/public_api'; |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 19 | import {Host, Link, LinkType, NodeType, UiElement} from '../../layer/forcesvg/models'; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 20 | import {Params, Router} from '@angular/router'; |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 21 | |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 22 | |
| 23 | interface ButtonAttrs { |
| 24 | gid: string; |
| 25 | tt: string; |
| 26 | path: string; |
| 27 | } |
| 28 | |
| 29 | const SHOWDEVICEVIEW: ButtonAttrs = { |
| 30 | gid: 'deviceTable', |
| 31 | tt: 'tt_ctl_show_device', |
| 32 | path: 'device', |
| 33 | }; |
| 34 | const SHOWFLOWVIEW: ButtonAttrs = { |
| 35 | gid: 'flowTable', |
| 36 | tt: 'title_flows', |
| 37 | path: 'flow', |
| 38 | }; |
| 39 | const SHOWPORTVIEW: ButtonAttrs = { |
| 40 | gid: 'portTable', |
| 41 | tt: 'tt_ctl_show_port', |
| 42 | path: 'port', |
| 43 | }; |
| 44 | const SHOWGROUPVIEW: ButtonAttrs = { |
| 45 | gid: 'groupTable', |
| 46 | tt: 'tt_ctl_show_group', |
| 47 | path: 'group', |
| 48 | }; |
| 49 | const SHOWMETERVIEW: ButtonAttrs = { |
| 50 | gid: 'meterTable', |
| 51 | tt: 'tt_ctl_show_meter', |
| 52 | path: 'meter', |
| 53 | }; |
| 54 | const SHOWPIPECONFVIEW: ButtonAttrs = { |
| 55 | gid: 'pipeconfTable', |
| 56 | tt: 'tt_ctl_show_pipeconf', |
| 57 | path: 'pipeconf', |
| 58 | }; |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 59 | const RELATEDINTENTS: ButtonAttrs = { |
| 60 | gid: 'm_relatedIntents', |
| 61 | tt: 'tr_btn_show_related_traffic', |
| 62 | path: 'relatedIntents', |
| 63 | }; |
| 64 | const CREATEHOSTTOHOSTFLOW: ButtonAttrs = { |
| 65 | gid: 'm_endstation', |
| 66 | tt: 'tr_btn_create_h2h_flow', |
| 67 | path: 'create_h2h_flow', |
| 68 | }; |
| 69 | const CREATEMULTISOURCEFLOW: ButtonAttrs = { |
| 70 | gid: 'm_flows', |
| 71 | tt: 'tr_btn_create_msrc_flow', |
| 72 | path: 'create_msrc_flow', |
| 73 | }; |
| 74 | |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 75 | |
| 76 | interface ShowDetails { |
| 77 | buttons: string[]; |
| 78 | glyphId: string; |
| 79 | id: string; |
| 80 | navPath: string; |
| 81 | propLabels: Object; |
| 82 | propOrder: string[]; |
| 83 | propValues: Object; |
| 84 | title: string; |
| 85 | } |
| 86 | /** |
| 87 | * ONOS GUI -- Topology Details Panel. |
| 88 | * Displays details of selected device. When no device is selected the panel slides |
| 89 | * off to the side and disappears |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 90 | * |
| 91 | * This Panel is a child of the Topology component and it gets the 'selectedNodes' |
| 92 | * from there as an input component. See TopologyComponent.nodeSelected() |
| 93 | * The topology component gets these by listening to events from ForceSvgComponent |
| 94 | * which gets them in turn from Device, Host, SubRegion and Link components. This |
| 95 | * is so that each component respects the hierarchy |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 96 | */ |
| 97 | @Component({ |
| 98 | selector: 'onos-details', |
| 99 | templateUrl: './details.component.html', |
| 100 | styleUrls: [ |
| 101 | './details.component.css', './details.theme.css', |
| 102 | '../../topology.common.css', |
Sean Condon | 98b6ddb | 2019-12-24 08:07:40 +0000 | [diff] [blame] | 103 | '../../../../gui2-fw-lib/lib/widget/panel.css', |
| 104 | '../../../../gui2-fw-lib/lib/widget/panel-theme.css' |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 105 | ], |
| 106 | animations: [ |
| 107 | trigger('detailsPanelState', [ |
| 108 | state('true', style({ |
| 109 | transform: 'translateX(0%)', |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 110 | opacity: '1.0' |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 111 | })), |
| 112 | state('false', style({ |
| 113 | transform: 'translateX(100%)', |
| 114 | opacity: '0' |
| 115 | })), |
| 116 | transition('0 => 1', animate('100ms ease-in')), |
| 117 | transition('1 => 0', animate('100ms ease-out')) |
| 118 | ]) |
| 119 | ] |
| 120 | }) |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 121 | export class DetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 122 | @Input() selectedNodes: UiElement[] = []; // Populated when user selects node or link |
Sean Condon | b2c483c | 2019-01-16 20:28:55 +0000 | [diff] [blame] | 123 | @Input() on: boolean = false; // Override the parent class attribute |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 124 | |
| 125 | // deferred localization strings |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 126 | lionFnTopo; // Function |
| 127 | lionFnFlow; // Function for flow bundle |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 128 | showDetails: ShowDetails; // Will be populated on callback. Cleared if nothing is selected |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 129 | |
| 130 | constructor( |
| 131 | protected fs: FnService, |
| 132 | protected log: LogService, |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 133 | protected router: Router, |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 134 | protected wss: WebSocketService, |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 135 | private lion: LionService |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 136 | ) { |
Sean Condon | 95fb574 | 2019-04-02 12:16:55 +0100 | [diff] [blame] | 137 | super(fs, log, wss, 'topo'); |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 138 | |
| 139 | if (this.lion.ubercache.length === 0) { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 140 | this.lionFnTopo = this.dummyLion; |
| 141 | this.lionFnFlow = this.dummyLion; |
| 142 | this.lion.loadCbs.set('detailscore', () => this.doLion()); |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 143 | } else { |
| 144 | this.doLion(); |
| 145 | } |
| 146 | |
| 147 | this.log.debug('Topo DetailsComponent constructed'); |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 148 | } |
| 149 | |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 150 | /** |
| 151 | * When the component is initializing set up the handler for callbacks of |
| 152 | * ShowDetails from the WSS. Set the variable showDetails when ever a callback |
| 153 | * is made |
| 154 | */ |
| 155 | ngOnInit(): void { |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 156 | this.wss.bindHandlers(new Map<string, (data) => void>([ |
| 157 | ['showDetails', (data) => { |
| 158 | this.showDetails = data; |
| 159 | // this.log.debug('showDetails received', data); |
| 160 | } |
| 161 | ] |
| 162 | ])); |
| 163 | this.log.debug('Topo DetailsComponent initialized'); |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * When the component is being unloaded then unbind the WSS handler. |
| 168 | */ |
| 169 | ngOnDestroy(): void { |
| 170 | this.wss.unbindHandlers(['showDetails']); |
| 171 | this.log.debug('Topo DetailsComponent destroyed'); |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * If changes are detected on the Input param selectedNode, call on WSS sendEvent |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 176 | * and expect ShowDetails to be updated from data sent back from server. |
| 177 | * |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 178 | * Note the difference in call to the WSS with requestDetails between a node |
| 179 | * and a link - the handling is done in TopologyViewMessageHandler#RequestDetails.process() |
| 180 | * |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 181 | * When multiple items are selected fabricate the ShowDetails here, and |
| 182 | * present buttons that allow custom actions |
| 183 | * |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 184 | * The WSS will call back asynchronously (see fn in ngOnInit()) |
| 185 | * |
| 186 | * @param changes Simple Changes set of updates |
| 187 | */ |
| 188 | ngOnChanges(changes: SimpleChanges): void { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 189 | if (changes['selectedNodes']) { |
| 190 | this.selectedNodes = changes['selectedNodes'].currentValue; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 191 | let type: any; |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 192 | if (this.selectedNodes.length === 0) { |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 193 | // Selection has been cleared |
| 194 | this.showDetails = <ShowDetails>{}; |
| 195 | return; |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 196 | } else if (this.selectedNodes.length > 1) { |
| 197 | // Don't send message to WSS just form dialog here |
| 198 | const propOrder: string[] = []; |
| 199 | const propValues: Object = {}; |
| 200 | const propLabels: Object = {}; |
| 201 | let numHosts: number = 0; |
| 202 | for (let i = 0; i < this.selectedNodes.length; i++) { |
| 203 | propOrder.push(i.toString()); |
| 204 | propLabels[i.toString()] = i.toString(); |
| 205 | propValues[i.toString()] = this.selectedNodes[i].id; |
| 206 | if (this.selectedNodes[i].hasOwnProperty('nodeType') && |
| 207 | (<Host>this.selectedNodes[i]).nodeType === NodeType.HOST) { |
| 208 | numHosts++; |
| 209 | } else { |
| 210 | numHosts = -128; // Negate the whole thing so other buttons will not be shown |
| 211 | } |
| 212 | } |
| 213 | const buttons: string[] = []; |
| 214 | if (numHosts === 2) { |
| 215 | buttons.push('createHostToHostFlow'); |
| 216 | } else if (numHosts > 2) { |
| 217 | buttons.push('createMultiSourceFlow'); |
| 218 | } |
| 219 | buttons.push('relatedIntents'); |
| 220 | |
| 221 | this.showDetails = <ShowDetails>{ |
| 222 | buttons: buttons, |
| 223 | glyphId: undefined, |
| 224 | id: 'multiple', |
| 225 | navPath: undefined, |
| 226 | propLabels: propLabels, |
| 227 | propOrder: propOrder, |
| 228 | propValues: propValues, |
| 229 | title: this.lionFnTopo('title_selected_items') |
| 230 | }; |
| 231 | this.log.debug('Details panel generated from multiple devices', this.showDetails); |
| 232 | return; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 233 | } |
| 234 | |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 235 | // If only one thing has been selected then request details of that from the server |
| 236 | const selectedNode = this.selectedNodes[0]; |
| 237 | if (selectedNode.hasOwnProperty('nodeType')) { // For Device, Host, SubRegion |
| 238 | type = (<Host>selectedNode).nodeType; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 239 | this.wss.sendEvent('requestDetails', { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 240 | id: selectedNode.id, |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 241 | class: type, |
| 242 | }); |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 243 | } else if (selectedNode.hasOwnProperty('type')) { // Must be link |
| 244 | const link: Link = <Link>selectedNode; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 245 | if (<LinkType><unknown>LinkType[link.type] === LinkType.UiEdgeLink) { // Number based enum |
| 246 | this.wss.sendEvent('requestDetails', { |
| 247 | key: link.id, |
| 248 | class: 'link', |
| 249 | sourceId: link.epA, |
| 250 | targetId: Link.deviceNameFromEp(link.epB), |
| 251 | targetPort: link.portB, |
| 252 | isEdgeLink: true |
| 253 | }); |
| 254 | } else { |
| 255 | this.wss.sendEvent('requestDetails', { |
| 256 | key: link.id, |
| 257 | class: 'link', |
| 258 | sourceId: Link.deviceNameFromEp(link.epA), |
| 259 | sourcePort: link.portA, |
| 260 | targetId: Link.deviceNameFromEp(link.epB), |
| 261 | targetPort: link.portB, |
| 262 | isEdgeLink: false |
| 263 | }); |
| 264 | } |
| 265 | } else { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 266 | this.log.warn('Unexpected type for selected element', selectedNode); |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 267 | } |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 268 | } |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Table of core button attributes to return per button icon |
| 273 | * @param btnName The name of the button |
| 274 | * @returns A structure with the button attributes |
| 275 | */ |
| 276 | buttonAttribs(btnName: string): ButtonAttrs { |
| 277 | switch (btnName) { |
| 278 | case 'showDeviceView': |
| 279 | return SHOWDEVICEVIEW; |
| 280 | case 'showFlowView': |
| 281 | return SHOWFLOWVIEW; |
| 282 | case 'showPortView': |
| 283 | return SHOWPORTVIEW; |
| 284 | case 'showGroupView': |
| 285 | return SHOWGROUPVIEW; |
| 286 | case 'showMeterView': |
| 287 | return SHOWMETERVIEW; |
| 288 | case 'showPipeConfView': |
| 289 | return SHOWPIPECONFVIEW; |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 290 | case 'relatedIntents': |
| 291 | return RELATEDINTENTS; |
| 292 | case 'createHostToHostFlow': |
| 293 | return CREATEHOSTTOHOSTFLOW; |
| 294 | case 'createMultiSourceFlow': |
| 295 | return CREATEMULTISOURCEFLOW; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 296 | default: |
| 297 | return <ButtonAttrs>{ |
| 298 | gid: btnName, |
| 299 | path: btnName |
| 300 | }; |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | /** |
| 305 | * Navigate using Angular Routing. Combines the parameters to generate a relative URL |
| 306 | * e.g. if params are 'meter', 'device' and 'null:0000000000001' then the |
| 307 | * navigation URL will become "http://localhost:4200/#/meter?devId=null:0000000000000002" |
| 308 | * |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 309 | * When multiple hosts are selected other actions have to be accommodated |
| 310 | * |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 311 | * @param path The path to navigate to |
| 312 | * @param navPath The parameter name to use |
| 313 | * @param selId the parameter value to use |
| 314 | */ |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 315 | navto(path: string): void { |
| 316 | this.log.debug('navigate to', path, 'for', |
| 317 | this.showDetails.navPath, '=', this.showDetails.id); |
| 318 | |
| 319 | const ids: string[] = []; |
| 320 | Object.values(this.showDetails.propValues).forEach((v) => ids.push(v)); |
| 321 | if (path === 'relatedIntents' && this.showDetails.id === 'multiple') { |
Sean Condon | adeb716 | 2019-04-13 20:56:14 +0100 | [diff] [blame] | 322 | this.wss.sendEvent('topo2RequestRelatedIntents', { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 323 | 'ids': ids, |
| 324 | 'hover': '' |
| 325 | }); |
| 326 | |
| 327 | } else if (path === 'create_h2h_flow' && this.showDetails.id === 'multiple') { |
Sean Condon | adeb716 | 2019-04-13 20:56:14 +0100 | [diff] [blame] | 328 | this.wss.sendEvent('topo2AddHostIntent', { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 329 | 'one': ids[0], |
| 330 | 'two': ids[1], |
| 331 | 'ids': ids |
| 332 | }); |
| 333 | |
| 334 | } else if (path === 'create_msrc_flow' && this.showDetails.id === 'multiple') { |
| 335 | // Should only happen when there are 3 or more ids |
Sean Condon | adeb716 | 2019-04-13 20:56:14 +0100 | [diff] [blame] | 336 | this.wss.sendEvent('topo2AddMultiSourceIntent', { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 337 | 'src': ids.slice(0, ids.length - 1), |
| 338 | 'dst': ids[ids.length - 1], |
| 339 | 'ids': ids |
| 340 | }); |
| 341 | |
| 342 | } else if (this.showDetails.id) { |
| 343 | let navPath = this.showDetails.navPath; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 344 | if (navPath === 'device') { |
| 345 | navPath = 'devId'; |
| 346 | } |
| 347 | const queryPar: Params = {}; |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 348 | queryPar[navPath] = this.showDetails.id; |
Sean Condon | 9148182 | 2019-01-01 13:56:14 +0000 | [diff] [blame] | 349 | this.router.navigate([path], { queryParams: queryPar }); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * Read the LION bundle for Details panel and set up the lionFn |
| 355 | */ |
| 356 | doLion() { |
Sean Condon | d88f366 | 2019-04-03 16:35:30 +0100 | [diff] [blame] | 357 | this.lionFnTopo = this.lion.bundle('core.view.Topo'); |
| 358 | this.lionFnFlow = this.lion.bundle('core.view.Flow'); |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 359 | } |
| 360 | |
| 361 | } |