reviewapp.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. Ext.require([
  2. '*'
  3. ]);
  4. Ext.BLANK_IMAGE_URL = '../libs/ext-4.0/resources/themes/images/default/tree/s.gif';
  5. Ext.onReady(function() {
  6. // Employee Data Model
  7. Ext.regModel('Employee', {
  8. fields: [
  9. {name:'id', type:'int'},
  10. {name:'first_name', type:'string'},
  11. {name:'last_name', type:'string'},
  12. {name:'title', type:'string'}
  13. ],
  14. hasMany: {model:'Review', name:'reviews'}
  15. });
  16. // Review Data Model
  17. Ext.regModel('Review', {
  18. fields: [
  19. {name:'review_date', label:'Date', type:'date', dateFormat:'d-m-Y'},
  20. {name:'attendance', label:'Attendance', type:'int'},
  21. {name:'attitude', label:'Attitude', type:'int'},
  22. {name:'communication', label:'Communication', type:'int'},
  23. {name:'excellence', label:'Excellence', type:'int'},
  24. {name:'skills', label:'Skills', type:'int'},
  25. {name:'teamwork', label:'Teamwork', type:'int'},
  26. {name:'employee_id', label:'Employee ID', type:'int'}
  27. ],
  28. belongsTo: 'Employee'
  29. });
  30. // Instance of a Data Store to hold Employee records
  31. var employeeStore = new Ext.data.Store({
  32. storeId:'employeeStore',
  33. model:'Employee',
  34. data:[
  35. {id:1, first_name:'Michael', last_name:'Scott', title:'Regional Manager'},
  36. {id:2, first_name:'Dwight', last_name:'Schrute', title:'Sales Rep'},
  37. {id:3, first_name:'Jim', last_name:'Halpert', title:'Sales Rep'},
  38. {id:4, first_name:'Pam', last_name:'Halpert', title:'Office Administrator'},
  39. {id:5, first_name:'Andy', last_name:'Bernard', title:'Sales Rep'},
  40. {id:6, first_name:'Stanley', last_name:'Hudson', title:'Sales Rep'},
  41. {id:7, first_name:'Phyllis', last_name:'Lapin-Vance', title:'Sales Rep'},
  42. {id:8, first_name:'Kevin', last_name:'Malone', title:'Accountant'},
  43. {id:9, first_name:'Angela', last_name:'Martin', title:'Senior Accountant'},
  44. {id:10, first_name:'Meredith', last_name:'Palmer', title:'Supplier Relations Rep'}
  45. ],
  46. autoLoad:true
  47. });
  48. /**
  49. * App.RadarStore
  50. * @extends Ext.data.Store
  51. * This is a specialized Data Store with dynamically generated fields
  52. * data reformating capabilities to transform Employee and Review data
  53. * into the format required by the Radar Chart.
  54. *
  55. * The constructor demonstrates dynamically generating store fields.
  56. * populateReviewScores() populates the store using records from
  57. * the reviewStore which holds all the employee review scores.
  58. *
  59. * calculateAverageScores() iterates through each metric in the
  60. * review and calculates an average across all available reviews.
  61. *
  62. * Most of the actual data population and updates done by
  63. * addUpdateRecordFromReviews() and removeRecordFromReviews()
  64. * called when add/update/delete events are triggered on the ReviewStore.
  65. */
  66. Ext.define('App.RadarStore', {
  67. extend: 'Ext.data.Store',
  68. constructor: function(config) {
  69. config = config || {};
  70. var dynamicFields = ['metric', 'avg']; // initalize the non-dynamic fields first
  71. employeeStore.each(function(record){ // loops through all the employees to setup the dynamic fields
  72. dynamicFields.push('eid_' + record.get('id'));
  73. });
  74. Ext.apply(config, {
  75. storeId:'radarStore', // let's us look it up later using Ext.data.StoreMgr.lookup('radarStore')
  76. fields:dynamicFields,
  77. data:[]
  78. });
  79. App.RadarStore.superclass.constructor.call(this, config);
  80. },
  81. addUpdateRecordFromReviews: function(reviews) {
  82. var me = this;
  83. Ext.Array.each(reviews, function(review, recordIndex, all) { // add a new radarStore record for each review record
  84. var eid = 'eid_' + review.get('employee_id'); // creates a unique id for each employee column in the store
  85. review.fields.each(function(field) {
  86. if(field.name !== "employee_id" && field.name !== "review_date") { // filter out the fields we don't need
  87. var metricRecord = me.findRecord('metric', field.name); // checks for an existing metric record in the store
  88. if(metricRecord) {
  89. metricRecord.set(eid, review.get(field.name)); // updates existing record with field value from review
  90. } else {
  91. var newRecord = {}; // creates a new object we can populate with dynamic keys and values to create a new record
  92. newRecord[eid] = review.get(field.name);
  93. newRecord['metric'] = field.label;
  94. me.add(newRecord);
  95. }
  96. }
  97. });
  98. });
  99. this.calculateAverageScores(); // update average scores
  100. },
  101. /**
  102. * Calculates an average for each metric across all employees.
  103. * We use this to create the average series always shown in the Radar Chart.
  104. */
  105. calculateAverageScores: function() {
  106. var me = this; // keeps the store in scope during Ext.Array.each
  107. var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
  108. var Review = Ext.ModelMgr.getModel('Review');
  109. Ext.Array.each(Review.prototype.fields.keys, function(fieldName) { // loop through the Review model fields and calculate average scores
  110. if(fieldName !== "employee_id" && fieldName !== "review_date") { // ignore non-score fields
  111. var avgScore = Math.round(reviewStore.average(fieldName)); // takes advantage of Ext.data.Store.average()
  112. var record = me.findRecord('metric', fieldName);
  113. if(record) {
  114. record.set('avg', avgScore);
  115. } else {
  116. me.add({metric:fieldName, avg:avgScore});
  117. }
  118. }
  119. });
  120. },
  121. populateReviewScores: function() {
  122. var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
  123. this.addUpdateRecordFromReviews(reviewStore.data.items); // add all the review records to this store
  124. },
  125. removeRecordFromReviews: function(reviews) {
  126. var me = this;
  127. Ext.Array.each(reviews, function(review, recordIndex, all) {
  128. var eid = 'eid_' + review.get('employee_id');
  129. me.each(function(record) {
  130. delete record.data[eid];
  131. });
  132. });
  133. // upate average scores
  134. this.calculateAverageScores();
  135. }
  136. }); // end App.RadarStore definition
  137. /** Creates an instance of App.RadarStore here so we
  138. * here so we can re-use it during the life of the app.
  139. * Otherwise we'd have to create a new instance everytime
  140. * refreshRadarChart() is run.
  141. */
  142. var radarStore = new App.RadarStore();
  143. var reviewStore = new Ext.data.Store({
  144. storeId:'reviewStore',
  145. model:'Review',
  146. data:[
  147. {review_date:'01-04-2011', attendance:10, attitude:6, communication:6, excellence:3, skills:3, teamwork:3, employee_id:1},
  148. {review_date:'01-04-2011', attendance:6, attitude:5, communication:2, excellence:8, skills:9, teamwork:5, employee_id:2},
  149. {review_date:'01-04-2011', attendance:5, attitude:4, communication:3, excellence:5, skills:6, teamwork:2, employee_id:3},
  150. {review_date:'01-04-2011', attendance:8, attitude:2, communication:4, excellence:2, skills:5, teamwork:6, employee_id:4},
  151. {review_date:'01-04-2011', attendance:4, attitude:1, communication:5, excellence:7, skills:5, teamwork:5, employee_id:5},
  152. {review_date:'01-04-2011', attendance:5, attitude:2, communication:4, excellence:7, skills:9, teamwork:8, employee_id:6},
  153. {review_date:'01-04-2011', attendance:10, attitude:7, communication:8, excellence:7, skills:3, teamwork:4, employee_id:7},
  154. {review_date:'01-04-2011', attendance:10, attitude:8, communication:8, excellence:4, skills:8, teamwork:7, employee_id:8},
  155. {review_date:'01-04-2011', attendance:6, attitude:4, communication:9, excellence:7, skills:6, teamwork:5, employee_id:9},
  156. {review_date:'01-04-2011', attendance:7, attitude:5, communication:9, excellence:4, skills:2, teamwork:4, employee_id:10}
  157. ],
  158. listeners: {
  159. add:function(store, records, storeIndex) {
  160. var radarStore = Ext.data.StoreMgr.lookup('radarStore');
  161. if(radarStore) { // only add records if an instance of the rardarStore already exists
  162. radarStore.addUpdateRecordFromReviews(records); // add a new radarStore records for new review records
  163. }
  164. }, // end add listener
  165. update: function(store, record, operation) {
  166. radarStore.addUpdateRecordFromReviews([record]);
  167. refreshRadarChart();
  168. },
  169. remove: function(store, records, storeIndex) {
  170. // update the radarStore and regenerate the radarChart
  171. Ext.data.StoreMgr.lookup('radarStore').removeRecordFromReviews(records);
  172. refreshRadarChart();
  173. } // end remove listener
  174. }
  175. });
  176. /**
  177. * App.PerformanceRadar
  178. * @extends Ext.chart.Chart
  179. * This is a specialized Radar Chart which we use to display employee
  180. * performance reviews.
  181. *
  182. * The class will be registered with an xtype of 'performanceradar'
  183. */
  184. Ext.define('App.PerformanceRadar', {
  185. extend: 'Ext.chart.Chart',
  186. alias: 'widget.performanceradar', // register xtype performanceradar
  187. constructor: function(config) {
  188. config = config || {};
  189. this.setAverageSeries(config); // make sure average is always present
  190. Ext.apply(config, {
  191. id:'radarchart',
  192. theme:'Category2',
  193. animate:true,
  194. store: Ext.data.StoreMgr.lookup('radarStore'),
  195. margin:'0 0 50 0',
  196. width:350,
  197. height:500,
  198. insetPadding:80,
  199. legend:{
  200. position: 'bottom'
  201. },
  202. axes: [{
  203. type:'Radial',
  204. position:'radial',
  205. label:{
  206. display: true
  207. }
  208. }]
  209. }); // end Ext.apply
  210. App.PerformanceRadar.superclass.constructor.call(this, config);
  211. }, // end constructor
  212. setAverageSeries: function(config) {
  213. var avgSeries = {
  214. type: 'radar',
  215. xField: 'metric',
  216. yField: 'avg',
  217. title: 'Avg',
  218. labelDisplay:'over',
  219. showInLegend: true,
  220. showMarkers: true,
  221. markerCfg: {
  222. radius: 5,
  223. size: 5,
  224. stroke:'#0677BD',
  225. fill:'#0677BD'
  226. },
  227. style: {
  228. 'stroke-width': 2,
  229. 'stroke':'#0677BD',
  230. fill: 'none'
  231. }
  232. }
  233. if(config.series) {
  234. config.series.push(avgSeries); // if a series is passed in then append the average to it
  235. } else {
  236. config.series = [avgSeries]; // if a series isn't passed just create average
  237. }
  238. }
  239. }); // end Ext.ux.Performance radar definition
  240. /**
  241. * App.EmployeeDetail
  242. * @extends Ext.Panel
  243. * This is a specialized Panel which is used to show information about
  244. * an employee and the reviews we have on record for them.
  245. *
  246. * This demonstrates adding 2 custom properties (tplMarkup and
  247. * startingMarkup) to the class. It also overrides the initComponent
  248. * method and adds a new method called updateDetail.
  249. *
  250. * The class will be registered with an xtype of 'employeedetail'
  251. */
  252. Ext.define('App.EmployeeDetail', {
  253. extend: 'Ext.panel.Panel',
  254. // register the App.EmployeeDetail class with an xtype of employeedetail
  255. alias: 'widget.employeedetail',
  256. // add tplMarkup as a new property
  257. tplMarkup: [
  258. '<b>{first_name}&nbsp;{last_name}</b>&nbsp;&nbsp;',
  259. 'Title: {title}<br/><br/>',
  260. '<b>Last Review</b>&nbsp;&nbsp;',
  261. 'Attendance:&nbsp;{attendance}&nbsp;&nbsp;',
  262. 'Attitude:&nbsp;{attitude}&nbsp;&nbsp;',
  263. 'Communication:&nbsp;{communication}&nbsp;&nbsp;',
  264. 'Excellence:&nbsp;{excellence}&nbsp;&nbsp;',
  265. 'Skills:&nbsp;{skills}&nbsp;&nbsp;',
  266. 'Teamwork:&nbsp;{teamwork}'
  267. ],
  268. height:90,
  269. bodyPadding: 7,
  270. // override initComponent to create and compile the template
  271. // apply styles to the body of the panel
  272. initComponent: function() {
  273. this.tpl = new Ext.Template(this.tplMarkup);
  274. // call the superclass's initComponent implementation
  275. App.EmployeeDetail.superclass.initComponent.call(this);
  276. }
  277. });
  278. Ext.define('App.ReviewWindow', {
  279. extend: 'Ext.window.Window',
  280. constructor: function(config) {
  281. config = config || {};
  282. Ext.apply(config, {
  283. title:'Employee Performance Review',
  284. width:320,
  285. height:420,
  286. layout:'fit',
  287. items:[{
  288. xtype:'form',
  289. id:'employeereviewcomboform',
  290. fieldDefaults: {
  291. labelAlign: 'left',
  292. labelWidth: 90,
  293. anchor: '100%'
  294. },
  295. bodyPadding:5,
  296. items:[{
  297. xtype:'fieldset',
  298. title:'Employee Info',
  299. items:[{
  300. xtype:'hiddenfield',
  301. name:'employee_id'
  302. },{
  303. xtype:'textfield',
  304. name:'first_name',
  305. fieldLabel:'First Name',
  306. allowBlank:false
  307. },{
  308. xtype:'textfield',
  309. name:'last_name',
  310. fieldLabel:'Last Name',
  311. allowBlank:false
  312. },{
  313. xtype:'textfield',
  314. name:'title',
  315. fieldLabel:'Title',
  316. allowBlank:false
  317. }]
  318. },{
  319. xtype:'fieldset',
  320. title:'Performance Review',
  321. items:[{
  322. xtype:'datefield',
  323. name:'review_date',
  324. fieldLabel:'Review Date',
  325. format:'d-m-Y',
  326. maxValue: new Date(),
  327. value: new Date(),
  328. allowBlank:false
  329. },{
  330. xtype:'slider',
  331. name:'attendance',
  332. fieldLabel:'Attendance',
  333. value:5,
  334. increment:1,
  335. minValue:1,
  336. maxValue:10
  337. },{
  338. xtype:'slider',
  339. name:'attitude',
  340. fieldLabel:'Attitude',
  341. value:5,
  342. minValue: 1,
  343. maxValue: 10
  344. },{
  345. xtype:'slider',
  346. name:'communication',
  347. fieldLabel:'Communication',
  348. value:5,
  349. increment:1,
  350. minValue:1,
  351. maxValue:10
  352. },{
  353. xtype:'numberfield',
  354. name:'excellence',
  355. fieldLabel:'Excellence',
  356. value:5,
  357. minValue: 1,
  358. maxValue: 10
  359. },{
  360. xtype:'numberfield',
  361. name:'skills',
  362. fieldLabel:'Skills',
  363. value:5,
  364. minValue: 1,
  365. maxValue: 10
  366. },{
  367. xtype:'numberfield',
  368. name:'teamwork',
  369. fieldLabel:'Teamwork',
  370. value:5,
  371. minValue: 1,
  372. maxValue: 10
  373. }]
  374. }]
  375. }],
  376. buttons:[{
  377. text:'Cancel',
  378. width:80,
  379. handler:function() {
  380. this.up('window').close();
  381. }
  382. },
  383. {
  384. text:'Save',
  385. width:80,
  386. handler:function(btn, eventObj) {
  387. var window = btn.up('window');
  388. var form = window.down('form').getForm();
  389. if (form.isValid()) {
  390. window.getEl().mask('saving data...');
  391. var vals = form.getValues();
  392. var employeeStore = Ext.data.StoreMgr.lookup('employeeStore');
  393. var currentEmployee = employeeStore.findRecord('id', vals['employee_id']);
  394. // look up id for this employee to see if they already exist
  395. if(vals['employee_id'] && currentEmployee) {
  396. currentEmployee.set('first_name', vals['first_name']);
  397. currentEmployee.set('last_name', vals['last_name']);
  398. currentEmployee.set('title', vals['title']);
  399. var currentReview = Ext.data.StoreMgr.lookup('reviewStore').findRecord('employee_id', vals['employee_id']);
  400. currentReview.set('review_date', vals['review_date']);
  401. currentReview.set('attendance', vals['attendance']);
  402. currentReview.set('attitude', vals['attitude']);
  403. currentReview.set('communication', vals['communication']);
  404. currentReview.set('excellence', vals['excellence']);
  405. currentReview.set('skills', vals['skills']);
  406. currentReview.set('teamwork', vals['teamwork']);
  407. } else {
  408. var newId = employeeStore.getCount() + 1;
  409. employeeStore.add({
  410. id: newId,
  411. first_name: vals['first_name'],
  412. last_name: vals['last_name'],
  413. title: vals['title']
  414. });
  415. Ext.data.StoreMgr.lookup('reviewStore').add({
  416. review_date: vals['review_date'],
  417. attendance: vals['attendance'],
  418. attitude: vals['attitude'],
  419. communication: vals['communication'],
  420. excellence: vals['excellence'],
  421. skills: vals['skills'],
  422. teamwork: vals['teamwork'],
  423. employee_id: newId
  424. });
  425. }
  426. window.getEl().unmask();
  427. window.close();
  428. }
  429. }
  430. }]
  431. }); // end Ext.apply
  432. App.ReviewWindow.superclass.constructor.call(this, config);
  433. } // end constructor
  434. });
  435. // adds a record to the radar chart store and
  436. // creates a series in the chart for selected employees
  437. function refreshRadarChart(employees) {
  438. employees = employees || []; // in case its called with nothing we'll at least have an empty array
  439. var existingRadarChart = Ext.getCmp('radarchart'); // grab the radar chart component (used down below)
  440. var reportsPanel = Ext.getCmp('reportspanel'); // grab the reports panel component (used down below)
  441. var dynamicSeries = []; // setup an array of chart series that we'll create dynamically
  442. for(var index = 0; index < employees.length; index++) {
  443. var fullName = employees[index].get('first_name') + ' ' + employees[index].get('last_name');
  444. var eid = 'eid_' + employees[index].get('id');
  445. // add to the dynamic series we're building
  446. dynamicSeries.push({
  447. type: 'radar',
  448. title: fullName,
  449. xField: 'metric',
  450. yField: eid,
  451. labelDisplay: 'over',
  452. showInLegend: true,
  453. showMarkers: true,
  454. markerCfg: {
  455. radius: 5,
  456. size: 5
  457. },
  458. style: {
  459. 'stroke-width': 2,
  460. fill: 'none'
  461. }
  462. });
  463. } // end for loop
  464. // destroy the existing chart
  465. existingRadarChart.destroy();
  466. // create the new chart using the dynamic series we just made
  467. var newRadarChart = new App.PerformanceRadar({series:dynamicSeries});
  468. // mask the panel while we switch out charts
  469. reportsPanel.getEl().mask('updating chart...');
  470. // display the new one
  471. reportsPanel.add(newRadarChart);
  472. // un mask the reports panel
  473. reportsPanel.getEl().unmask();
  474. }
  475. function refreshEmployeeDetails(employees) {
  476. var detailsPanel = Ext.getCmp('detailspanel');
  477. var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
  478. var items = [];
  479. for(var index = 0; index < employees.length; index++) {
  480. var templateData = Ext.applyIf(employees[index].data, reviewStore.findRecord('employee_id', employees[index].get('id')).data);
  481. var employeePanel = new App.EmployeeDetail({
  482. title:employees[index].get('first_name') + ' ' + employees[index].get('last_name'),
  483. data:templateData // combined employee and latest review dataTransfer
  484. });
  485. items.push(employeePanel);
  486. }
  487. detailsPanel.getEl().mask('updating details...');
  488. detailsPanel.removeAll();
  489. detailsPanel.add(items);
  490. detailsPanel.getEl().unmask();
  491. }
  492. // sets Up Checkbox Selection Model for the Employee Grid
  493. var checkboxSelModel = new Ext.selection.CheckboxModel();
  494. var viewport = new Ext.container.Viewport({
  495. id:'mainviewport',
  496. layout: 'border', // sets up Ext.layout.container.Border
  497. items: [{
  498. xtype:'panel',
  499. region:'center',
  500. layout:'auto',
  501. autoScroll:true,
  502. title:'Employee Performance Manager',
  503. tbar:[{
  504. text:'Add Employee',
  505. tooltip:'Add a new employee',
  506. iconCls:'add',
  507. handler:function() { // display a window to add a new employee
  508. new App.ReviewWindow().show();
  509. }
  510. }],
  511. items:[{
  512. xtype:'grid',
  513. store:Ext.data.StoreMgr.lookup('employeeStore'),
  514. height:300,
  515. columns:[{
  516. text:'First Name',
  517. dataIndex:'first_name',
  518. flex:2
  519. },
  520. {
  521. text:'Last Name',
  522. dataIndex:'last_name',
  523. flex:2
  524. },
  525. {
  526. text:'Title',
  527. dataIndex:'title',
  528. flex:3
  529. },
  530. {
  531. xtype:'actioncolumn',
  532. width:45,
  533. items:[{
  534. icon:'images/edit.png',
  535. tooltip:'Edit Employee',
  536. handler:function(grid, rowIndex, colIndex) {
  537. var employee = grid.getStore().getAt(rowIndex);
  538. var review = reviewStore.findRecord('employee_id', employee.get('id'));
  539. var win = new App.ReviewWindow({hidden:true});
  540. var form = win.down('form').getForm();
  541. form.loadRecord(employee);
  542. form.loadRecord(review);
  543. win.show();
  544. }
  545. },
  546. {
  547. icon:'images/delete.png',
  548. tooltip:'Delete Employee',
  549. width:75,
  550. handler:function(grid, rowIndex, colIndex) {
  551. Ext.Msg.confirm('Remove Employee?', 'Are you sure you want to remove this employee?',
  552. function(choice) {
  553. if(choice === 'yes') {
  554. var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
  555. var employee = grid.getStore().getAt(rowIndex);
  556. var reviewIndex = reviewStore.find('employee_id', employee.get('id'));
  557. reviewStore.removeAt(reviewIndex);
  558. grid.getStore().removeAt(rowIndex);
  559. }
  560. }
  561. );
  562. }
  563. }]
  564. }],
  565. selModel: new Ext.selection.CheckboxModel(),
  566. columnLines: true,
  567. viewConfig: {stripeRows:true},
  568. listeners:{
  569. selectionchange:function(selModel, selected) {
  570. refreshRadarChart(selected);
  571. refreshEmployeeDetails(selected);
  572. }
  573. }
  574. },{
  575. xtype:'container',
  576. id:'detailspanel',
  577. layout:{
  578. type:'vbox',
  579. align:'stretch',
  580. autoSize:true
  581. }
  582. }]
  583. },{
  584. xtype:'panel', // sets up the chart panel (starts collapsed)
  585. region:'east',
  586. id:'reportspanel',
  587. title:'Performance Report',
  588. width:350,
  589. layout: 'fit',
  590. items:[{
  591. xtype:'performanceradar' // this instantiates a App.PerformanceRadar object
  592. }]
  593. }] // mainviewport items array ends here
  594. });
  595. });