@@ -32,8 +32,11 @@ export class SideNav implements OnInit, OnDestroy {
3232 selectedRunInternal = '' ;
3333 selectedTagInternal = '' ;
3434 selectedHostInternal = '' ;
35+ selectedHostsInternal : string [ ] = [ ] ;
36+ selectedHostsPending : string [ ] = [ ] ;
3537 selectedModuleInternal = '' ;
3638 navigationParams : { [ key : string ] : string | boolean } = { } ;
39+ multiHostEnabledTools : string [ ] = [ 'trace_viewer' , 'trace_viewer@' ] ;
3740
3841 hideCaptureProfileButton = false ;
3942
@@ -65,6 +68,11 @@ export class SideNav implements OnInit, OnDestroy {
6568 return HLO_TOOLS . includes ( this . selectedTag ) ;
6669 }
6770
71+ get isMultiHostsEnabled ( ) {
72+ const tag = this . selectedTag || '' ;
73+ return this . multiHostEnabledTools . includes ( tag ) ;
74+ }
75+
6876 // Getter for valid run given url router or user selection.
6977 get selectedRun ( ) {
7078 return this . runs . find ( validRun => validRun === this . selectedRunInternal ) ||
@@ -90,6 +98,10 @@ export class SideNav implements OnInit, OnDestroy {
9098 this . moduleList [ 0 ] || '' ;
9199 }
92100
101+ get selectedHosts ( ) {
102+ return this . selectedHostsInternal ;
103+ }
104+
93105 // https://github.com/angular/angular/issues/11023#issuecomment-752228784
94106 mergeRouteParams ( ) : Map < string , string > {
95107 const params = new Map < string , string > ( ) ;
@@ -119,20 +131,25 @@ export class SideNav implements OnInit, OnDestroy {
119131 const run = params . get ( 'run' ) || '' ;
120132 const tag = params . get ( 'tool' ) || params . get ( 'tag' ) || '' ;
121133 const host = params . get ( 'host' ) || '' ;
134+ const hostsParam = params . get ( 'hosts' ) ;
122135 const opName = params . get ( 'node_name' ) || params . get ( 'opName' ) || '' ;
123136 const moduleName = params . get ( 'module_name' ) || '' ;
124137 this . navigationParams [ 'firstLoad' ] = true ;
125138 if ( opName ) {
126139 this . navigationParams [ 'opName' ] = opName ;
127140 }
128- if ( this . selectedRunInternal === run && this . selectedTagInternal === tag &&
129- this . selectedHostInternal === host ) {
130- return ;
131- }
132141 this . selectedRunInternal = run ;
133142 this . selectedTagInternal = tag ;
134- this . selectedHostInternal = host ;
135143 this . selectedModuleInternal = moduleName ;
144+
145+ if ( this . isMultiHostsEnabled ) {
146+ if ( hostsParam ) {
147+ this . selectedHostsInternal = hostsParam . split ( ',' ) ;
148+ }
149+ this . selectedHostsPending = [ ...this . selectedHostsInternal ] ;
150+ } else {
151+ this . selectedHostInternal = host ;
152+ }
136153 this . update ( ) ;
137154 }
138155
@@ -153,9 +170,13 @@ export class SideNav implements OnInit, OnDestroy {
153170 const navigationEvent : NavigationEvent = {
154171 run : this . selectedRun ,
155172 tag : this . selectedTag ,
156- host : this . selectedHost ,
157173 ...this . navigationParams ,
158174 } ;
175+ if ( this . isMultiHostsEnabled ) {
176+ navigationEvent . hosts = this . selectedHosts ;
177+ } else {
178+ navigationEvent . host = this . selectedHost ;
179+ }
159180 if ( this . is_hlo_tool ) {
160181 navigationEvent . moduleName = this . selectedModule ;
161182 }
@@ -242,8 +263,21 @@ export class SideNav implements OnInit, OnDestroy {
242263 this . afterUpdateTag ( ) ;
243264 }
244265
245- onTagSelectionChange ( tag : string ) {
266+ async onTagSelectionChange ( tag : string ) {
246267 this . selectedTagInternal = tag ;
268+ this . selectedHostsInternal = [ ] ;
269+ this . selectedHostsPending = [ ] ;
270+ this . selectedHostInternal = '' ;
271+
272+ if ( this . isMultiHostsEnabled ) {
273+ this . hosts = await this . getHostsForSelectedTag ( ) ;
274+ if ( this . hosts . length > 0 ) {
275+ this . selectedHostsInternal = [ this . hosts [ 0 ] ] ;
276+ } else {
277+ this . selectedHostsInternal = [ ] ;
278+ }
279+ this . selectedHostsPending = [ ...this . selectedHostsInternal ] ;
280+ }
247281 this . afterUpdateTag ( ) ;
248282 }
249283
@@ -255,18 +289,51 @@ export class SideNav implements OnInit, OnDestroy {
255289 // Keep them under the same update function as initial step of the separation.
256290 async updateHosts ( ) {
257291 this . hosts = await this . getHostsForSelectedTag ( ) ;
292+ if ( this . isMultiHostsEnabled ) {
293+ if ( this . selectedHostsInternal . length === 0 && this . hosts . length > 0 ) {
294+ this . selectedHostsInternal = [ this . hosts [ 0 ] ] ;
295+ }
296+ this . selectedHostsPending = [ ...this . selectedHostsInternal ] ;
297+ } else {
298+ if ( ! this . selectedHostInternal && this . hosts . length > 0 ) {
299+ this . selectedHostInternal = this . hosts [ 0 ] ;
300+ }
301+ }
258302 if ( this . is_hlo_tool ) {
259303 this . moduleList = await this . getModuleListForSelectedTag ( ) ;
260304 }
261305
262306 this . afterUpdateHost ( ) ;
263307 }
264308
265- onHostSelectionChange ( host : string ) {
266- this . selectedHostInternal = host ;
309+ onHostSelectionChange ( selection : string ) {
310+ this . selectedHostInternal = selection ;
311+ this . navigateTools ( ) ;
312+ }
313+
314+ onHostsSelectionChange ( selection : string [ ] ) {
315+ this . selectedHostsPending =
316+ Array . isArray ( selection ) ? selection : [ selection ] ;
317+ }
318+
319+ onSubmitHosts ( ) {
320+ this . selectedHostsInternal = [ ...this . selectedHostsPending ] ;
267321 this . navigateTools ( ) ;
268322 }
269323
324+ toggleAllHosts ( ) {
325+ const allAvailableHosts = this . hosts ;
326+
327+ const areAllSelected = allAvailableHosts . length > 0 &&
328+ allAvailableHosts . length === this . selectedHostsPending . length ;
329+
330+ if ( areAllSelected ) {
331+ this . selectedHostsPending = [ ] ;
332+ } else {
333+ this . selectedHostsPending = [ ...allAvailableHosts ] ;
334+ }
335+ }
336+
270337 onModuleSelectionChange ( module : string ) {
271338 this . selectedModuleInternal = module ;
272339 this . navigateTools ( ) ;
@@ -276,26 +343,65 @@ export class SideNav implements OnInit, OnDestroy {
276343 this . navigateTools ( ) ;
277344 }
278345
346+ // Helper function to serialize query parameters
347+ private serializeQueryParams (
348+ params : { [ key : string ] : string | string [ ] | boolean | undefined } ) : string {
349+ const searchParams = new URLSearchParams ( ) ;
350+ for ( const key in params ) {
351+ if ( params . hasOwnProperty ( key ) ) {
352+ const value = params [ key ] ;
353+ // Only include non-null/non-undefined values
354+ if ( value !== undefined && value !== null ) {
355+ if ( Array . isArray ( value ) ) {
356+ // Arrays are handled as comma-separated strings (like 'hosts')
357+ searchParams . set ( key , value . join ( ',' ) ) ;
358+ } else if ( typeof value === 'boolean' ) {
359+ // Only set boolean flags if they are explicitly true
360+ if ( value === true ) {
361+ searchParams . set ( key , 'true' ) ;
362+ }
363+ } else {
364+ searchParams . set ( key , String ( value ) ) ;
365+ }
366+ }
367+ }
368+ }
369+ const queryString = searchParams . toString ( ) ;
370+ return queryString ? `?${ queryString } ` : '' ;
371+ }
372+
279373 updateUrlHistory ( ) {
280- // TODO(xprof): change to camel case when constructing url
281- const toolQueryParams = Object . keys ( this . navigationParams )
282- . map ( key => {
283- return `${ key } =${ this . navigationParams [ key ] } ` ;
284- } )
285- . join ( '&' ) ;
286- const toolQueryParamsString =
287- toolQueryParams . length ? `&${ toolQueryParams } ` : '' ;
288- const moduleNameQuery =
289- this . is_hlo_tool ? `&module_name=${ this . selectedModule } ` : '' ;
290- const url = `${ window . parent . location . origin } ?tool=${
291- this . selectedTag } &host=${ this . selectedHost } &run=${ this . selectedRun } ${
292- toolQueryParamsString } ${ moduleNameQuery } #profile`;
374+ const navigationEvent = this . getNavigationEvent ( ) ;
375+ const queryParams : { [ key : string ] : string | string [ ] | boolean |
376+ undefined } = { ...navigationEvent } ;
377+
378+ if ( this . isMultiHostsEnabled ) {
379+ // For multi-host enabled tools, ensure 'hosts' is a comma-separated string in the URL
380+ if ( queryParams [ 'hosts' ] && Array . isArray ( queryParams [ 'hosts' ] ) ) {
381+ queryParams [ 'hosts' ] = ( queryParams [ 'hosts' ] as string [ ] ) . join ( ',' ) ;
382+ }
383+ delete queryParams [ 'host' ] ; // Remove single host param
384+ } else {
385+ // For other tools, ensure 'host' is used
386+ delete queryParams [ 'hosts' ] ; // Remove multi-host param
387+ }
388+
389+ // Get current path to avoid changing the base URL
390+ const pathname = window . parent . location . pathname ;
391+
392+ // Use the custom serialization helper
393+ const queryString = this . serializeQueryParams ( queryParams ) ;
394+ const url = pathname + queryString ;
395+
293396 window . parent . history . pushState ( { } , '' , url ) ;
294397 }
295398
296399 navigateTools ( ) {
297400 const navigationEvent = this . getNavigationEvent ( ) ;
298401 this . communicationService . onNavigateReady ( navigationEvent ) ;
402+
403+ // This router.navigate call remains, as it's responsible for Angular
404+ // routing
299405 this . router . navigate (
300406 [
301407 this . selectedTag || 'empty' ,
0 commit comments