blob: ce2116b0d048df7e79b5709c6ed98b7ebf3de04c [file] [log] [blame]
Sean Condonf4f54a12018-10-10 23:25:46 +01001/*
Sean Condon91481822019-01-01 13:56:14 +00002 * Copyright 2019-present Open Networking Foundation
Sean Condonf4f54a12018-10-10 23:25:46 +01003 *
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 Condond88f3662019-04-03 16:35:30 +010016import {Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core';
Sean Condon91481822019-01-01 13:56:14 +000017import {animate, state, style, transition, trigger} from '@angular/animations';
Sean Condona3ad7792020-01-04 19:26:34 +000018import {DetailsPanelBaseImpl, FnService, LionService, LogService, WebSocketService} from 'gui2-fw-lib/public_api';
Sean Condond88f3662019-04-03 16:35:30 +010019import {Host, Link, LinkType, NodeType, UiElement} from '../../layer/forcesvg/models';
Sean Condon91481822019-01-01 13:56:14 +000020import {Params, Router} from '@angular/router';
Sean Condonf4f54a12018-10-10 23:25:46 +010021
Sean Condon91481822019-01-01 13:56:14 +000022
23interface ButtonAttrs {
24 gid: string;
25 tt: string;
26 path: string;
27}
28
29const SHOWDEVICEVIEW: ButtonAttrs = {
30 gid: 'deviceTable',
31 tt: 'tt_ctl_show_device',
32 path: 'device',
33};
34const SHOWFLOWVIEW: ButtonAttrs = {
35 gid: 'flowTable',
36 tt: 'title_flows',
37 path: 'flow',
38};
39const SHOWPORTVIEW: ButtonAttrs = {
40 gid: 'portTable',
41 tt: 'tt_ctl_show_port',
42 path: 'port',
43};
44const SHOWGROUPVIEW: ButtonAttrs = {
45 gid: 'groupTable',
46 tt: 'tt_ctl_show_group',
47 path: 'group',
48};
49const SHOWMETERVIEW: ButtonAttrs = {
50 gid: 'meterTable',
51 tt: 'tt_ctl_show_meter',
52 path: 'meter',
53};
54const SHOWPIPECONFVIEW: ButtonAttrs = {
55 gid: 'pipeconfTable',
56 tt: 'tt_ctl_show_pipeconf',
57 path: 'pipeconf',
58};
Sean Condond88f3662019-04-03 16:35:30 +010059const RELATEDINTENTS: ButtonAttrs = {
60 gid: 'm_relatedIntents',
61 tt: 'tr_btn_show_related_traffic',
62 path: 'relatedIntents',
63};
64const CREATEHOSTTOHOSTFLOW: ButtonAttrs = {
65 gid: 'm_endstation',
66 tt: 'tr_btn_create_h2h_flow',
67 path: 'create_h2h_flow',
68};
69const CREATEMULTISOURCEFLOW: ButtonAttrs = {
70 gid: 'm_flows',
71 tt: 'tr_btn_create_msrc_flow',
72 path: 'create_msrc_flow',
73};
74
Sean Condon91481822019-01-01 13:56:14 +000075
76interface 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 Condond88f3662019-04-03 16:35:30 +010090 *
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 Condonf4f54a12018-10-10 23:25:46 +010096 */
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 Condon98b6ddb2019-12-24 08:07:40 +0000103 '../../../../gui2-fw-lib/lib/widget/panel.css',
104 '../../../../gui2-fw-lib/lib/widget/panel-theme.css'
Sean Condonf4f54a12018-10-10 23:25:46 +0100105 ],
106 animations: [
107 trigger('detailsPanelState', [
108 state('true', style({
109 transform: 'translateX(0%)',
Sean Condon91481822019-01-01 13:56:14 +0000110 opacity: '1.0'
Sean Condonf4f54a12018-10-10 23:25:46 +0100111 })),
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 Condon91481822019-01-01 13:56:14 +0000121export class DetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
Sean Condond88f3662019-04-03 16:35:30 +0100122 @Input() selectedNodes: UiElement[] = []; // Populated when user selects node or link
Sean Condonb2c483c2019-01-16 20:28:55 +0000123 @Input() on: boolean = false; // Override the parent class attribute
Sean Condon91481822019-01-01 13:56:14 +0000124
125 // deferred localization strings
Sean Condond88f3662019-04-03 16:35:30 +0100126 lionFnTopo; // Function
127 lionFnFlow; // Function for flow bundle
Sean Condon91481822019-01-01 13:56:14 +0000128 showDetails: ShowDetails; // Will be populated on callback. Cleared if nothing is selected
Sean Condonf4f54a12018-10-10 23:25:46 +0100129
130 constructor(
131 protected fs: FnService,
132 protected log: LogService,
Sean Condon91481822019-01-01 13:56:14 +0000133 protected router: Router,
Sean Condonf4f54a12018-10-10 23:25:46 +0100134 protected wss: WebSocketService,
Sean Condon91481822019-01-01 13:56:14 +0000135 private lion: LionService
Sean Condonf4f54a12018-10-10 23:25:46 +0100136 ) {
Sean Condon95fb5742019-04-02 12:16:55 +0100137 super(fs, log, wss, 'topo');
Sean Condon91481822019-01-01 13:56:14 +0000138
139 if (this.lion.ubercache.length === 0) {
Sean Condond88f3662019-04-03 16:35:30 +0100140 this.lionFnTopo = this.dummyLion;
141 this.lionFnFlow = this.dummyLion;
142 this.lion.loadCbs.set('detailscore', () => this.doLion());
Sean Condon91481822019-01-01 13:56:14 +0000143 } else {
144 this.doLion();
145 }
146
147 this.log.debug('Topo DetailsComponent constructed');
Sean Condonf4f54a12018-10-10 23:25:46 +0100148 }
149
Sean Condon91481822019-01-01 13:56:14 +0000150 /**
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 Condon91481822019-01-01 13:56:14 +0000156 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 Condond88f3662019-04-03 16:35:30 +0100176 * and expect ShowDetails to be updated from data sent back from server.
177 *
Sean Condon91481822019-01-01 13:56:14 +0000178 * 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 Condond88f3662019-04-03 16:35:30 +0100181 * When multiple items are selected fabricate the ShowDetails here, and
182 * present buttons that allow custom actions
183 *
Sean Condon91481822019-01-01 13:56:14 +0000184 * 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 Condond88f3662019-04-03 16:35:30 +0100189 if (changes['selectedNodes']) {
190 this.selectedNodes = changes['selectedNodes'].currentValue;
Sean Condon91481822019-01-01 13:56:14 +0000191 let type: any;
Sean Condond88f3662019-04-03 16:35:30 +0100192 if (this.selectedNodes.length === 0) {
Sean Condon91481822019-01-01 13:56:14 +0000193 // Selection has been cleared
194 this.showDetails = <ShowDetails>{};
195 return;
Sean Condond88f3662019-04-03 16:35:30 +0100196 } 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 Condon91481822019-01-01 13:56:14 +0000233 }
234
Sean Condond88f3662019-04-03 16:35:30 +0100235 // 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 Condon91481822019-01-01 13:56:14 +0000239 this.wss.sendEvent('requestDetails', {
Sean Condond88f3662019-04-03 16:35:30 +0100240 id: selectedNode.id,
Sean Condon91481822019-01-01 13:56:14 +0000241 class: type,
242 });
Sean Condond88f3662019-04-03 16:35:30 +0100243 } else if (selectedNode.hasOwnProperty('type')) { // Must be link
244 const link: Link = <Link>selectedNode;
Sean Condon91481822019-01-01 13:56:14 +0000245 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 Condond88f3662019-04-03 16:35:30 +0100266 this.log.warn('Unexpected type for selected element', selectedNode);
Sean Condon91481822019-01-01 13:56:14 +0000267 }
Sean Condon91481822019-01-01 13:56:14 +0000268 }
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 Condond88f3662019-04-03 16:35:30 +0100290 case 'relatedIntents':
291 return RELATEDINTENTS;
292 case 'createHostToHostFlow':
293 return CREATEHOSTTOHOSTFLOW;
294 case 'createMultiSourceFlow':
295 return CREATEMULTISOURCEFLOW;
Sean Condon91481822019-01-01 13:56:14 +0000296 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 Condond88f3662019-04-03 16:35:30 +0100309 * When multiple hosts are selected other actions have to be accommodated
310 *
Sean Condon91481822019-01-01 13:56:14 +0000311 * @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 Condond88f3662019-04-03 16:35:30 +0100315 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 Condonadeb7162019-04-13 20:56:14 +0100322 this.wss.sendEvent('topo2RequestRelatedIntents', {
Sean Condond88f3662019-04-03 16:35:30 +0100323 'ids': ids,
324 'hover': ''
325 });
326
327 } else if (path === 'create_h2h_flow' && this.showDetails.id === 'multiple') {
Sean Condonadeb7162019-04-13 20:56:14 +0100328 this.wss.sendEvent('topo2AddHostIntent', {
Sean Condond88f3662019-04-03 16:35:30 +0100329 '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 Condonadeb7162019-04-13 20:56:14 +0100336 this.wss.sendEvent('topo2AddMultiSourceIntent', {
Sean Condond88f3662019-04-03 16:35:30 +0100337 '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 Condon91481822019-01-01 13:56:14 +0000344 if (navPath === 'device') {
345 navPath = 'devId';
346 }
347 const queryPar: Params = {};
Sean Condond88f3662019-04-03 16:35:30 +0100348 queryPar[navPath] = this.showDetails.id;
Sean Condon91481822019-01-01 13:56:14 +0000349 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 Condond88f3662019-04-03 16:35:30 +0100357 this.lionFnTopo = this.lion.bundle('core.view.Topo');
358 this.lionFnFlow = this.lion.bundle('core.view.Flow');
Sean Condonf4f54a12018-10-10 23:25:46 +0100359 }
360
361}