@@ -27,13 +27,16 @@ import {DisposableStack} from '../../../base/disposable_stack';
2727import { CurrentTracingSession , RecordingManager } from '../recording_manager' ;
2828import { download } from '../../../base/download_utils' ;
2929import { RecordSubpage } from '../config/config_interfaces' ;
30- import { RecordPluginSchema } from '../serialization_schema' ;
30+ import { RecordPluginSchema , SavedSessionSchema } from '../serialization_schema' ;
3131import { Checkbox } from '../../../widgets/checkbox' ;
3232import { linkify } from '../../../widgets/anchor' ;
3333import { getPresetsForPlatform } from '../presets' ;
3434import { Icons } from '../../../base/semantic_icons' ;
3535import { shareRecordConfig } from '../config/config_sharing' ;
3636import { Card } from '../../../widgets/card' ;
37+ import { showModal } from '../../../widgets/modal' ;
38+ import { traceConfigToPb } from '../../../base/proto_utils_wasm' ;
39+ import protos from '../../../protos' ;
3740
3841type RecMgrAttrs = { recMgr : RecordingManager } ;
3942
@@ -68,7 +71,7 @@ export function targetSelectionPage(recMgr: RecordingManager): RecordSubpage {
6871 // Restore config
6972 const hasSavedProbes =
7073 state . lastSession !== undefined &&
71- state . lastSession . probes !== undefined &&
74+ state . lastSession . kind === 'probes' &&
7275 Object . keys ( state . lastSession . probes ) . length > 0 ;
7376
7477 if ( state . selectedConfigId || hasSavedProbes ) {
@@ -185,6 +188,7 @@ class RecordConfigSelector implements m.ClassComponent<RecMgrAttrs> {
185188 const isEmptySelected =
186189 recMgr . selectedConfigId === undefined &&
187190 recMgr . isConfigModified === false &&
191+ ! recMgr . hasCustomTraceConfig &&
188192 ! recMgr . recordConfig . hasActiveProbes ( ) ;
189193
190194 return [
@@ -225,62 +229,63 @@ class RecordConfigSelector implements m.ClassComponent<RecMgrAttrs> {
225229
226230 private renderSavedConfigsSection ( recMgr : RecordingManager ) {
227231 const hasActiveProbes = recMgr . recordConfig . hasActiveProbes ( ) ;
232+ const hasUnsavedCustomConfig =
233+ recMgr . hasCustomTraceConfig && recMgr . selectedConfigId === undefined ;
228234 const shouldHighlightSave =
235+ hasUnsavedCustomConfig ||
229236 ( hasActiveProbes && recMgr . selectedConfigId === undefined ) ||
230237 recMgr . isConfigModified === true ;
231- const hasSavedConfigs = recMgr . savedConfigs . length > 0 ;
232- const showSection = hasSavedConfigs || shouldHighlightSave ;
233- if ( ! showSection ) {
234- return null ;
235- }
236238 return [
237239 m ( 'h3' , 'User configs' ) ,
238240 m ( '.pf-config-selector__grid' , [
239241 // Saved configs
240- ...recMgr . savedConfigs . map ( ( config ) => {
242+ ...recMgr . savedConfigs . map ( ( saved ) => {
241243 const isSelected =
242- recMgr . selectedConfigId === `saved:${ config . name } ` &&
244+ recMgr . selectedConfigId === `saved:${ saved . name } ` &&
243245 recMgr . isConfigModified === false ;
246+ const config = saved . config ;
247+ const isCustom = config . kind === 'custom' ;
244248 return m (
245249 Card ,
246250 {
247251 className :
248252 'pf-preset-card' +
249253 ( isSelected ? ' pf-preset-card--selected' : '' ) ,
250- onclick : ( ) =>
251- recMgr . loadConfig ( {
252- config : config . config ,
253- configId : `saved:${ config . name } ` ,
254- configName : config . name ,
255- } ) ,
254+ onclick : ( ) => this . loadSavedConfig ( recMgr , saved ) ,
256255 tabindex : 0 ,
257256 } ,
258- m ( Icon , { icon : 'bookmark' } ) ,
259- m ( '.pf-preset-card__title' , config . name ) ,
257+ m ( Icon , { icon : isCustom ? 'description' : 'bookmark' } ) ,
258+ m ( '.pf-preset-card__title' , saved . name ) ,
259+ isCustom &&
260+ m (
261+ '.pf-preset-card__subtitle' ,
262+ `Imported from ${ config . customConfigFileName ?? 'textproto' } ` ,
263+ ) ,
260264 m ( '.pf-preset-card__actions' , [
261- m ( Button , {
262- icon : 'save' ,
263- compact : true ,
264- title : 'Overwrite with current settings' ,
265- onclick : ( e : Event ) => {
266- e . stopPropagation ( ) ;
267- if (
268- confirm (
269- `Overwrite config "${ config . name } " with current settings?` ,
270- )
271- ) {
272- recMgr . saveConfig ( config . name , recMgr . serializeSession ( ) ) ;
273- recMgr . app . raf . scheduleFullRedraw ( ) ;
274- }
275- } ,
276- } ) ,
265+ ! isCustom &&
266+ m ( Button , {
267+ icon : 'save' ,
268+ compact : true ,
269+ title : 'Overwrite with current settings' ,
270+ onclick : ( e : Event ) => {
271+ e . stopPropagation ( ) ;
272+ if (
273+ confirm (
274+ `Overwrite config "${ saved . name } " with current settings?` ,
275+ )
276+ ) {
277+ recMgr . saveConfig ( saved . name ) ;
278+ recMgr . app . raf . scheduleFullRedraw ( ) ;
279+ }
280+ } ,
281+ } ) ,
277282 m ( Button , {
278283 icon : 'share' ,
279284 compact : true ,
280285 title : 'Share configuration' ,
281286 onclick : ( e : Event ) => {
282287 e . stopPropagation ( ) ;
283- shareRecordConfig ( config . config ) ;
288+ shareRecordConfig ( saved . config ) ;
284289 } ,
285290 } ) ,
286291 m ( Button , {
@@ -289,8 +294,8 @@ class RecordConfigSelector implements m.ClassComponent<RecMgrAttrs> {
289294 title : 'Delete configuration' ,
290295 onclick : ( e : Event ) => {
291296 e . stopPropagation ( ) ;
292- if ( confirm ( `Delete "${ config . name } "?` ) ) {
293- recMgr . deleteConfig ( config . name ) ;
297+ if ( confirm ( `Delete "${ saved . name } "?` ) ) {
298+ recMgr . deleteConfig ( saved . name ) ;
294299 recMgr . app . raf . scheduleFullRedraw ( ) ;
295300 }
296301 } ,
@@ -315,26 +320,70 @@ class RecordConfigSelector implements m.ClassComponent<RecMgrAttrs> {
315320 ) ;
316321 return ;
317322 }
318- const savedConfig = recMgr . serializeSession ( ) ;
319- recMgr . saveConfig ( trimmedName , savedConfig ) ;
320- recMgr . loadConfig ( {
321- config : savedConfig ,
322- configId : `saved:${ trimmedName } ` ,
323- configName : trimmedName ,
324- } ) ;
323+ const saved = recMgr . saveConfig ( trimmedName ) ;
324+ this . loadSavedConfig ( recMgr , saved ) ;
325325 recMgr . app . raf . scheduleFullRedraw ( ) ;
326326 }
327327 } ,
328328 tabindex : 0 ,
329329 } ,
330- m ( Icon , { icon : 'tune' } ) ,
331- m ( '.pf-preset-card__title' , 'Custom' ) ,
330+ m ( Icon , { icon : hasUnsavedCustomConfig ? 'description' : 'tune' } ) ,
331+ m (
332+ '.pf-preset-card__title' ,
333+ hasUnsavedCustomConfig
334+ ? recMgr . customConfigFileName ?? 'Imported config'
335+ : 'Custom' ,
336+ ) ,
332337 m ( '.pf-preset-card__subtitle' , 'Click to save' ) ,
333338 ) ,
339+ this . renderImportCard ( recMgr ) ,
334340 ] ) ,
335341 ] ;
336342 }
337343
344+ private loadSavedConfig ( recMgr : RecordingManager , saved : SavedSessionSchema ) {
345+ recMgr . loadConfig ( {
346+ config : saved . config ,
347+ configId : `saved:${ saved . name } ` ,
348+ configName : saved . name ,
349+ } ) ;
350+ }
351+
352+ private renderImportCard ( recMgr : RecordingManager ) {
353+ return m (
354+ Card ,
355+ {
356+ className : 'pf-preset-card pf-preset-card--dashed' ,
357+ onclick : ( ) => this . openImportDialog ( recMgr ) ,
358+ tabindex : 0 ,
359+ } ,
360+ m ( Icon , { icon : 'upload_file' } ) ,
361+ m ( '.pf-preset-card__title' , 'Import' ) ,
362+ m ( '.pf-preset-card__subtitle' , 'Load textproto' ) ,
363+ ) ;
364+ }
365+
366+ private openImportDialog ( recMgr : RecordingManager ) {
367+ const input = document . createElement ( 'input' ) ;
368+ input . type = 'file' ;
369+ input . onchange = async ( ) => {
370+ const file = input . files ?. [ 0 ] ;
371+ if ( ! file ) return ;
372+ const text = await file . text ( ) ;
373+ const res = await traceConfigToPb ( text ) ;
374+ if ( ! res . ok ) {
375+ showModal ( {
376+ title : 'Import error' ,
377+ content : `Failed to parse config: ${ res . error } ` ,
378+ } ) ;
379+ return ;
380+ }
381+ const config = protos . TraceConfig . decode ( res . value ) ;
382+ recMgr . setCustomTraceConfig ( config , file . name ) ;
383+ } ;
384+ input . click ( ) ;
385+ }
386+
338387 private renderCard (
339388 icon : string ,
340389 title : string ,
0 commit comments