2020-11-07 15:32:00 +03:00
// FIXME: HACK! Makes scrolling in number spinner work again. fixed in ExtJS >= 6.1
if ( Ext . isFirefox ) {
Ext . $eventNameMap . DOMMouseScroll = 'DOMMouseScroll' ;
}
2020-11-03 17:28:41 +03:00
Ext . onReady ( function ( ) {
const NOW = new Date ( ) ;
const COLORS = {
'keep-last' : 'orange' ,
'keep-hourly' : 'purple' ,
'keep-daily' : 'yellow' ,
'keep-weekly' : 'green' ,
'keep-monthly' : 'blue' ,
'keep-yearly' : 'red' ,
'all zero' : 'white' ,
} ;
const TEXT _COLORS = {
'keep-last' : 'black' ,
'keep-hourly' : 'white' ,
'keep-daily' : 'black' ,
'keep-weekly' : 'white' ,
'keep-monthly' : 'white' ,
'keep-yearly' : 'white' ,
'all zero' : 'black' ,
} ;
Ext . define ( 'PBS.prunesimulator.Documentation' , {
extend : 'Ext.Panel' ,
alias : 'widget.prunesimulatorDocumentation' ,
2020-11-06 14:12:59 +03:00
html : '<iframe style="width:100%;height:100%;border:0px;" src="./documentation.html"/>' ,
2020-11-03 17:28:41 +03:00
} ) ;
Ext . define ( 'PBS.prunesimulator.CalendarEvent' , {
extend : 'Ext.form.field.ComboBox' ,
alias : 'widget.prunesimulatorCalendarEvent' ,
editable : true ,
displayField : 'text' ,
valueField : 'value' ,
queryMode : 'local' ,
store : {
field : [ 'value' , 'text' ] ,
data : [
{ value : '0/2:00' , text : "Every two hours" } ,
{ value : '0/6:00' , text : "Every six hours" } ,
{ value : '2,22:30' , text : "At 02:30 and 22:30" } ,
2020-11-07 16:08:34 +03:00
{ value : '00:00' , text : "At 00:00" } ,
2020-11-03 17:28:41 +03:00
{ value : '08..17:00/30' , text : "From 08:00 to 17:30 every 30 minutes" } ,
{ value : 'HOUR:MINUTE' , text : "Custom schedule" } ,
] ,
} ,
tpl : [
'<ul class="x-list-plain"><tpl for=".">' ,
'<li role="option" class="x-boundlist-item">{text}</li>' ,
'</tpl></ul>' ,
] ,
displayTpl : [
'<tpl for=".">' ,
'{value}' ,
'</tpl>' ,
] ,
} ) ;
Ext . define ( 'PBS.prunesimulator.DayOfWeekSelector' , {
extend : 'Ext.form.field.ComboBox' ,
alias : 'widget.prunesimulatorDayOfWeekSelector' ,
editable : false ,
displayField : 'text' ,
valueField : 'value' ,
queryMode : 'local' ,
store : {
field : [ 'value' , 'text' ] ,
data : [
{ value : 'mon' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 1 ] ) } ,
{ value : 'tue' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 2 ] ) } ,
{ value : 'wed' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 3 ] ) } ,
{ value : 'thu' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 4 ] ) } ,
{ value : 'fri' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 5 ] ) } ,
{ value : 'sat' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 6 ] ) } ,
{ value : 'sun' , text : Ext . util . Format . htmlDecode ( Ext . Date . dayNames [ 0 ] ) } ,
] ,
} ,
} ) ;
Ext . define ( 'pbs-prune-list' , {
extend : 'Ext.data.Model' ,
fields : [
{
name : 'backuptime' ,
type : 'date' ,
dateFormat : 'timestamp' ,
} ,
{
name : 'mark' ,
type : 'string' ,
} ,
{
name : 'keepName' ,
type : 'string' ,
} ,
] ,
} ) ;
Ext . define ( 'PBS.prunesimulator.PruneList' , {
extend : 'Ext.panel.Panel' ,
alias : 'widget.prunesimulatorPruneList' ,
initComponent : function ( ) {
var me = this ;
if ( ! me . store ) {
throw "no store specified" ;
}
me . items = [
{
xtype : 'grid' ,
store : me . store ,
2020-11-06 14:12:59 +03:00
border : false ,
2020-11-03 17:28:41 +03:00
columns : [
{
header : 'Backup Time' ,
dataIndex : 'backuptime' ,
renderer : function ( value , metaData , record ) {
let text = Ext . Date . format ( value , 'Y-m-d H:i:s' ) ;
if ( record . data . mark === 'keep' ) {
if ( me . useColors ) {
let bgColor = COLORS [ record . data . keepName ] ;
let textColor = TEXT _COLORS [ record . data . keepName ] ;
return '<div style="background-color: ' + bgColor + '; ' +
'color: ' + textColor + ';">' + text + '</div>' ;
} else {
return text ;
}
} else {
return '<div style="text-decoration: line-through;">' + text + '</div>' ;
}
} ,
flex : 1 ,
sortable : false ,
} ,
{
header : 'Keep (reason)' ,
dataIndex : 'mark' ,
renderer : function ( value , metaData , record ) {
if ( record . data . mark === 'keep' ) {
return 'keep (' + record . data . keepName + ')' ;
} else {
return value ;
}
} ,
width : 200 ,
sortable : false ,
} ,
] ,
} ,
] ;
me . callParent ( ) ;
} ,
} ) ;
Ext . define ( 'PBS.prunesimulator.WeekTable' , {
extend : 'Ext.panel.Panel' ,
alias : 'widget.prunesimulatorWeekTable' ,
reload : function ( ) {
let me = this ;
let backups = me . store . data . items ;
2020-11-07 15:37:54 +03:00
let html = '<table class="cal">' ;
2020-11-03 17:28:41 +03:00
let now = new Date ( NOW . getTime ( ) ) ;
let skip = 7 - parseInt ( Ext . Date . format ( now , 'N' ) , 10 ) ;
let tableStartDate = Ext . Date . add ( now , Ext . Date . DAY , skip ) ;
let bIndex = 0 ;
for ( let i = 0 ; bIndex < backups . length ; i ++ ) {
html += '<tr>' ;
for ( let j = 0 ; j < 7 ; j ++ ) {
let date = Ext . Date . subtract ( tableStartDate , Ext . Date . DAY , j + 7 * i ) ;
let currentDay = Ext . Date . format ( date , 'd/m/Y' ) ;
2020-11-07 15:37:54 +03:00
let dayOfWeekCls = Ext . Date . format ( date , 'D' ) . toLowerCase ( ) ;
let firstOfMonthCls = Ext . Date . format ( date , 'd' ) === '01'
? 'first-of-month'
: '' ;
html += ` <td class="cal-day ${ dayOfWeekCls } ${ firstOfMonthCls } "> ` ;
const isBackupOnDay = function ( backup , day ) {
2020-11-03 17:28:41 +03:00
return backup && Ext . Date . format ( backup . data . backuptime , 'd/m/Y' ) === day ;
} ;
let backup = backups [ bIndex ] ;
2020-11-07 15:37:54 +03:00
html += '<table><tr>' ;
html += ` <th class="cal-day-date"> ${ Ext . Date . format ( date , 'D, d M Y' ) } </th> ` ;
2020-11-03 17:28:41 +03:00
while ( isBackupOnDay ( backup , currentDay ) ) {
html += '<tr><td>' ;
let text = Ext . Date . format ( backup . data . backuptime , 'H:i' ) ;
if ( backup . data . mark === 'remove' ) {
2020-11-07 15:37:54 +03:00
html += ` <span class="strikethrough"> ${ text } </span> ` ;
2020-11-03 17:28:41 +03:00
} else {
2020-11-07 15:37:54 +03:00
text += ` ( ${ backup . data . keepName } ) ` ;
2020-11-03 17:28:41 +03:00
if ( me . useColors ) {
let bgColor = COLORS [ backup . data . keepName ] ;
let textColor = TEXT _COLORS [ backup . data . keepName ] ;
2020-11-07 15:37:54 +03:00
html += ` <span style="background-color: ${ bgColor } ;
color : $ { textColor } ; " > $ { text } < / s p a n > ` ;
2020-11-03 17:28:41 +03:00
} else {
2020-11-07 15:37:54 +03:00
html += ` <span class="black"> ${ text } </span> ` ;
2020-11-03 17:28:41 +03:00
}
}
html += '</td></tr>' ;
backup = backups [ ++ bIndex ] ;
}
html += '</table>' ;
html += '</div>' ;
html += '</td>' ;
}
html += '</tr>' ;
}
me . setHtml ( html ) ;
} ,
initComponent : function ( ) {
let me = this ;
if ( ! me . store ) {
throw "no store specified" ;
}
let reload = function ( ) {
me . reload ( ) ;
} ;
me . store . on ( "datachanged" , reload ) ;
me . callParent ( ) ;
me . reload ( ) ;
} ,
} ) ;
Ext . define ( 'PBS.PruneSimulatorPanel' , {
extend : 'Ext.panel.Panel' ,
alias : 'widget.prunesimulatorPanel' ,
viewModel : {
} ,
getValues : function ( ) {
let me = this ;
let values = { } ;
Ext . Array . each ( me . query ( '[isFormField]' ) , function ( field ) {
let data = field . getSubmitData ( ) ;
Ext . Object . each ( data , function ( name , val ) {
values [ name ] = val ;
} ) ;
} ) ;
return values ;
} ,
controller : {
xclass : 'Ext.app.ViewController' ,
init : function ( view ) {
this . reloadFull ( ) ; // initial load
2020-11-07 15:34:20 +03:00
this . switchColor ( true ) ;
2020-11-03 17:28:41 +03:00
} ,
control : {
'field[fieldGroup=keep]' : { change : 'reloadPrune' } ,
} ,
reloadFull : function ( ) {
let me = this ;
let view = me . getView ( ) ;
let params = view . getValues ( ) ;
let [ hourSpec , minuteSpec ] = params [ 'schedule-time' ] . split ( ':' ) ;
if ( ! hourSpec || ! minuteSpec ) {
Ext . Msg . alert ( 'Error' , 'Invalid schedule' ) ;
return ;
}
let matchTimeSpec = function ( timeSpec , rangeMin , rangeMax ) {
let specValues = timeSpec . split ( ',' ) ;
let matches = { } ;
let assertValid = function ( value ) {
let num = Number ( value ) ;
if ( isNaN ( num ) ) {
throw value + " is not an integer" ;
} else if ( value < rangeMin || value > rangeMax ) {
throw "number '" + value + "' is not in the range '" + rangeMin + ".." + rangeMax + "'" ;
}
return num ;
} ;
specValues . forEach ( function ( value ) {
if ( value . includes ( '..' ) ) {
let [ start , end ] = value . split ( '..' ) ;
start = assertValid ( start ) ;
end = assertValid ( end ) ;
if ( start > end ) {
throw "interval start is bigger then interval end '" + start + " > " + end + "'" ;
}
for ( let i = start ; i <= end ; i ++ ) {
matches [ i ] = 1 ;
}
} else if ( value . includes ( '/' ) ) {
let [ start , step ] = value . split ( '/' ) ;
start = assertValid ( start ) ;
step = assertValid ( step ) ;
for ( let i = start ; i <= rangeMax ; i += step ) {
matches [ i ] = 1 ;
}
} else if ( value === '*' ) {
for ( let i = rangeMin ; i <= rangeMax ; i ++ ) {
matches [ i ] = 1 ;
}
} else {
value = assertValid ( value ) ;
matches [ value ] = 1 ;
}
} ) ;
return Object . keys ( matches ) ;
} ;
let hours , minutes ;
try {
hours = matchTimeSpec ( hourSpec , 0 , 23 ) ;
minutes = matchTimeSpec ( minuteSpec , 0 , 59 ) ;
} catch ( err ) {
Ext . Msg . alert ( 'Error' , err ) ;
2020-11-07 15:35:56 +03:00
return ;
2020-11-03 17:28:41 +03:00
}
let backups = me . populateFromSchedule (
params [ 'schedule-weekdays' ] ,
hours ,
minutes ,
params . numberOfWeeks ,
) ;
me . pruneSelect ( backups , params ) ;
view . pruneStore . setData ( backups ) ;
} ,
reloadPrune : function ( ) {
let me = this ;
let view = me . getView ( ) ;
let params = view . getValues ( ) ;
let backups = [ ] ;
view . pruneStore . getData ( ) . items . forEach ( function ( item ) {
backups . push ( {
backuptime : item . data . backuptime ,
} ) ;
} ) ;
me . pruneSelect ( backups , params ) ;
view . pruneStore . setData ( backups ) ;
} ,
// backups are sorted descending by date
populateFromSchedule : function ( weekdays , hours , minutes , weekCount ) {
let weekdayFlags = [
weekdays . includes ( 'sun' ) ,
weekdays . includes ( 'mon' ) ,
weekdays . includes ( 'tue' ) ,
weekdays . includes ( 'wed' ) ,
weekdays . includes ( 'thu' ) ,
weekdays . includes ( 'fri' ) ,
weekdays . includes ( 'sat' ) ,
] ;
let todaysDate = new Date ( NOW . getTime ( ) ) ;
let timesOnSingleDay = [ ] ;
hours . forEach ( function ( hour ) {
minutes . forEach ( function ( minute ) {
todaysDate . setHours ( hour ) ;
todaysDate . setMinutes ( minute ) ;
timesOnSingleDay . push ( todaysDate . getTime ( ) ) ;
} ) ;
} ) ;
// ordering here and iterating backwards through days
// ensures that everything is ordered
timesOnSingleDay . sort ( function ( a , b ) {
return a < b ;
} ) ;
let backups = [ ] ;
for ( let i = 0 ; i < 7 * weekCount ; i ++ ) {
let daysDate = Ext . Date . subtract ( todaysDate , Ext . Date . DAY , i ) ;
let weekday = parseInt ( Ext . Date . format ( daysDate , 'w' ) , 10 ) ;
if ( weekdayFlags [ weekday ] ) {
timesOnSingleDay . forEach ( function ( time ) {
backups . push ( {
backuptime : Ext . Date . subtract ( new Date ( time ) , Ext . Date . DAY , i ) ,
} ) ;
} ) ;
}
}
return backups ;
} ,
pruneMark : function ( backups , keepCount , keepName , idFunc ) {
if ( ! keepCount ) {
return ;
}
let alreadyIncluded = { } ;
let newlyIncluded = { } ;
let newlyIncludedCount = 0 ;
let finished = false ;
backups . forEach ( function ( backup ) {
let mark = backup . mark ;
let id = idFunc ( backup ) ;
if ( finished || alreadyIncluded [ id ] ) {
return ;
}
if ( mark ) {
if ( mark === 'keep' ) {
alreadyIncluded [ id ] = true ;
}
return ;
}
if ( ! newlyIncluded [ id ] ) {
if ( newlyIncludedCount >= keepCount ) {
finished = true ;
return ;
}
newlyIncluded [ id ] = true ;
newlyIncludedCount ++ ;
backup . mark = 'keep' ;
backup . keepName = keepName ;
} else {
backup . mark = 'remove' ;
}
} ) ;
} ,
// backups need to be sorted descending by date
pruneSelect : function ( backups , keepParams ) {
let me = this ;
if ( Number ( keepParams [ 'keep-last' ] ) +
Number ( keepParams [ 'keep-hourly' ] ) +
Number ( keepParams [ 'keep-daily' ] ) +
Number ( keepParams [ 'keep-weekly' ] ) +
Number ( keepParams [ 'keep-monthly' ] ) +
Number ( keepParams [ 'keep-yearly' ] ) === 0 ) {
backups . forEach ( function ( backup ) {
backup . mark = 'keep' ;
backup . keepName = 'all zero' ;
} ) ;
return ;
}
me . pruneMark ( backups , keepParams [ 'keep-last' ] , 'keep-last' , function ( backup ) {
return backup . backuptime ;
} ) ;
me . pruneMark ( backups , keepParams [ 'keep-hourly' ] , 'keep-hourly' , function ( backup ) {
return Ext . Date . format ( backup . backuptime , 'H/d/m/Y' ) ;
} ) ;
me . pruneMark ( backups , keepParams [ 'keep-daily' ] , 'keep-daily' , function ( backup ) {
return Ext . Date . format ( backup . backuptime , 'd/m/Y' ) ;
} ) ;
me . pruneMark ( backups , keepParams [ 'keep-weekly' ] , 'keep-weekly' , function ( backup ) {
// ISO-8601 week and week-based year
return Ext . Date . format ( backup . backuptime , 'W/o' ) ;
} ) ;
me . pruneMark ( backups , keepParams [ 'keep-monthly' ] , 'keep-monthly' , function ( backup ) {
return Ext . Date . format ( backup . backuptime , 'm/Y' ) ;
} ) ;
me . pruneMark ( backups , keepParams [ 'keep-yearly' ] , 'keep-yearly' , function ( backup ) {
return Ext . Date . format ( backup . backuptime , 'Y' ) ;
} ) ;
backups . forEach ( function ( backup ) {
backup . mark = backup . mark || 'remove' ;
} ) ;
} ,
2020-11-07 15:34:20 +03:00
toggleColors : function ( checkbox , checked ) {
this . switchColor ( checked ) ;
} ,
switchColor : function ( useColors ) {
let me = this ;
let view = me . getView ( ) ;
const getStyle = name =>
` background-color: ${ COLORS [ name ] } ; color: ${ TEXT _COLORS [ name ] } ; ` ;
for ( const field of view . query ( '[isFormField]' ) ) {
if ( field . fieldGroup !== 'keep' ) {
continue ;
}
if ( useColors ) {
field . setFieldStyle ( getStyle ( field . name ) ) ;
} else {
field . setFieldStyle ( 'background-color: white; color: #444;' ) ;
}
}
me . lookup ( 'weekTable' ) . useColors = useColors ;
me . lookup ( 'pruneList' ) . useColors = useColors ;
me . reloadPrune ( ) ;
} ,
2020-11-03 17:28:41 +03:00
} ,
keepItems : [
{
xtype : 'numberfield' ,
name : 'keep-last' ,
allowBlank : true ,
fieldLabel : 'keep-last' ,
minValue : 0 ,
value : 4 ,
fieldGroup : 'keep' ,
} ,
{
xtype : 'numberfield' ,
name : 'keep-hourly' ,
allowBlank : true ,
fieldLabel : 'keep-hourly' ,
minValue : 0 ,
value : 0 ,
fieldGroup : 'keep' ,
} ,
{
xtype : 'numberfield' ,
name : 'keep-daily' ,
allowBlank : true ,
fieldLabel : 'keep-daily' ,
minValue : 0 ,
value : 5 ,
fieldGroup : 'keep' ,
} ,
{
xtype : 'numberfield' ,
name : 'keep-weekly' ,
allowBlank : true ,
fieldLabel : 'keep-weekly' ,
minValue : 0 ,
value : 2 ,
fieldGroup : 'keep' ,
} ,
{
xtype : 'numberfield' ,
name : 'keep-monthly' ,
allowBlank : true ,
fieldLabel : 'keep-monthly' ,
minValue : 0 ,
value : 0 ,
fieldGroup : 'keep' ,
} ,
{
xtype : 'numberfield' ,
name : 'keep-yearly' ,
allowBlank : true ,
fieldLabel : 'keep-yearly' ,
minValue : 0 ,
value : 0 ,
fieldGroup : 'keep' ,
} ,
] ,
initComponent : function ( ) {
var me = this ;
me . pruneStore = Ext . create ( 'Ext.data.Store' , {
model : 'pbs-prune-list' ,
sorters : { property : 'backuptime' , direction : 'DESC' } ,
} ) ;
let scheduleItems = [
{
xtype : 'prunesimulatorDayOfWeekSelector' ,
name : 'schedule-weekdays' ,
fieldLabel : 'Day of week' ,
value : [ 'mon' , 'tue' , 'wed' , 'thu' , 'fri' , 'sat' , 'sun' ] ,
allowBlank : false ,
multiSelect : true ,
padding : '0 0 0 10' ,
} ,
{
xtype : 'prunesimulatorCalendarEvent' ,
name : 'schedule-time' ,
allowBlank : false ,
value : '0/6:00' ,
fieldLabel : 'Backup schedule' ,
padding : '0 0 0 10' ,
} ,
{
xtype : 'numberfield' ,
name : 'numberOfWeeks' ,
allowBlank : false ,
fieldLabel : 'Number of weeks' ,
minValue : 1 ,
value : 15 ,
2020-11-07 16:08:06 +03:00
maxValue : 260 , // five years
2020-11-03 17:28:41 +03:00
padding : '0 0 0 10' ,
} ,
{
xtype : 'button' ,
name : 'schedule-button' ,
text : 'Update Schedule' ,
2020-11-07 15:35:26 +03:00
handler : 'reloadFull' ,
2020-11-03 17:28:41 +03:00
} ,
] ;
me . items = [
{
xtype : 'panel' ,
2020-11-06 14:12:59 +03:00
layout : {
type : 'hbox' ,
align : 'stretch' ,
} ,
border : false ,
2020-11-03 17:28:41 +03:00
items : [
{
title : 'View' ,
layout : 'anchor' ,
flex : 1 ,
2020-11-06 14:12:59 +03:00
border : false ,
bodyPadding : 10 ,
2020-11-03 17:28:41 +03:00
items : [
{
xtype : 'checkbox' ,
name : 'showCalendar' ,
reference : 'showCalendar' ,
fieldLabel : 'Show Calendar:' ,
2020-11-07 15:36:56 +03:00
checked : true ,
2020-11-03 17:28:41 +03:00
} ,
{
xtype : 'checkbox' ,
name : 'showColors' ,
reference : 'showColors' ,
fieldLabel : 'Show Colors:' ,
2020-11-07 15:34:20 +03:00
checked : true ,
handler : 'toggleColors' ,
2020-11-03 17:28:41 +03:00
} ,
] ,
} ,
2020-11-06 14:12:59 +03:00
{ xtype : "panel" , width : 1 , border : 1 } ,
2020-11-03 17:28:41 +03:00
{
layout : 'anchor' ,
flex : 1 ,
2020-11-06 14:12:59 +03:00
border : false ,
2020-11-07 15:41:06 +03:00
title : 'Simulated Backup Schedule' ,
defaults : {
labelWidth : 120 ,
} ,
2020-11-06 14:12:59 +03:00
bodyPadding : 10 ,
2020-11-03 17:28:41 +03:00
items : scheduleItems ,
} ,
] ,
} ,
{
xtype : 'panel' ,
2020-11-06 14:12:59 +03:00
layout : {
type : 'hbox' ,
align : 'stretch' ,
} ,
2020-11-03 17:28:41 +03:00
flex : 1 ,
2020-11-06 14:12:59 +03:00
border : false ,
2020-11-03 17:28:41 +03:00
items : [
{
layout : 'anchor' ,
title : 'Prune Options' ,
2020-11-06 14:12:59 +03:00
border : false ,
bodyPadding : 10 ,
2020-11-03 17:28:41 +03:00
items : me . keepItems ,
flex : 1 ,
} ,
2020-11-06 14:12:59 +03:00
{ xtype : "panel" , width : 1 , border : 1 } ,
2020-11-03 17:28:41 +03:00
{
layout : 'fit' ,
title : 'Backups' ,
2020-11-06 14:12:59 +03:00
border : false ,
2020-11-03 17:28:41 +03:00
xtype : 'prunesimulatorPruneList' ,
store : me . pruneStore ,
reference : 'pruneList' ,
flex : 1 ,
} ,
] ,
} ,
{
layout : 'anchor' ,
title : 'Calendar' ,
autoScroll : true ,
flex : 2 ,
xtype : 'prunesimulatorWeekTable' ,
reference : 'weekTable' ,
store : me . pruneStore ,
bind : {
2020-11-07 15:33:07 +03:00
hidden : '{!showCalendar.checked}' ,
2020-11-03 17:28:41 +03:00
} ,
} ,
] ;
me . callParent ( ) ;
} ,
} ) ;
Ext . create ( 'Ext.container.Viewport' , {
layout : 'border' ,
renderTo : Ext . getBody ( ) ,
items : [
{
xtype : 'prunesimulatorPanel' ,
2020-11-07 15:41:06 +03:00
title : 'Proxmox Backup Server - Prune Simulator' ,
2020-11-03 17:28:41 +03:00
region : 'west' ,
layout : {
type : 'vbox' ,
align : 'stretch' ,
pack : 'start' ,
} ,
2020-11-07 15:41:06 +03:00
flex : 2 ,
maxWidth : 1100 ,
2020-11-03 17:28:41 +03:00
} ,
{
xtype : 'prunesimulatorDocumentation' ,
title : 'Usage' ,
2020-11-06 14:12:59 +03:00
border : false ,
2020-11-07 15:41:06 +03:00
flex : 1 ,
2020-11-03 17:28:41 +03:00
region : 'center' ,
} ,
] ,
} ) ;
} ) ;