wizardProgressPageController.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. var App = require('app');
  19. /**
  20. * Mixin for wizard controller for showing command progress on wizard pages
  21. * This should
  22. * @type {Ember.Mixin}
  23. */
  24. App.wizardProgressPageControllerMixin = Em.Mixin.create({
  25. controllerName: '',
  26. clusterDeployState: 'WIZARD_DEPLOY',
  27. status: 'IN_PROGRESS',
  28. tasks: [],
  29. commands: [],
  30. currentRequestIds: [], //todo: replace with using requestIds from tasks
  31. logs: [],
  32. currentTaskId: null,
  33. POLL_INTERVAL: 4000,
  34. isSubmitDisabled: true,
  35. isBackButtonDisabled: true,
  36. stages: [],
  37. currentPageRequestId: null,
  38. isSingleRequestPage: false,
  39. isCommandLevelRetry: function () {
  40. return !this.get('isSingleRequestPage');
  41. }.property('isSingleRequestPage'),
  42. showRetry: false,
  43. /**
  44. * Show whether tasks data was loaded
  45. * @type {Boolean}
  46. */
  47. isLoading: false,
  48. k: Em.K,
  49. /**
  50. * tasksMessagesPrefix should be overloaded by any controller including the mixin
  51. */
  52. tasksMessagesPrefix: '',
  53. loadStep: function () {
  54. this.clearStep();
  55. var self = this;
  56. if (!self.isSingleRequestPage) {
  57. this.initStep();
  58. } else {
  59. var requestIds = this.get('content.tasksRequestIds');
  60. var currentRequestId = requestIds && requestIds[0][0];
  61. if (!currentRequestId) {
  62. this.set('isLoaded', false);
  63. this.submitRequest();
  64. } else {
  65. self.set('currentPageRequestId', currentRequestId);
  66. self.doPollingForPageRequest();
  67. }
  68. }
  69. },
  70. initStep: function () {
  71. this.initializeTasks();
  72. if (!this.isSingleRequestPage) {
  73. this.loadTasks();
  74. }
  75. this.addObserver('tasks.@each.status', this, 'onTaskStatusChange');
  76. if (this.isSingleRequestPage) {
  77. var dfd = $.Deferred();
  78. var dfdObject = {
  79. deferred: dfd,
  80. isJqueryPromise: true
  81. };
  82. this.onTaskStatusChange(dfdObject);
  83. return dfd.promise();
  84. } else {
  85. this.onTaskStatusChange();
  86. }
  87. },
  88. clearStep: function () {
  89. this.removeObserver('tasks.@each.status', this, 'onTaskStatusChange');
  90. this.set('isSubmitDisabled', true);
  91. this.set('isBackButtonDisabled', true);
  92. this.set('tasks', []);
  93. this.set('currentRequestIds', []);
  94. this.set('isLoaded', false);
  95. },
  96. /**
  97. * Clear stages info for single page request.
  98. */
  99. clearStage: function() {
  100. this.setDBProperty('tasksRequestIds', null);
  101. this.setDBProperty('tasksStatuses', null);
  102. this.set('showRetry', false);
  103. this.set('content.tasksRequestIds', null);
  104. this.set('content.tasksStatuses', null);
  105. this.set('content.currentTaskId', null);
  106. this.get('stages').clear();
  107. },
  108. retry: function () {
  109. this.set('showRetry', false);
  110. this.get('tasks').setEach('status','PENDING');
  111. this.loadStep();
  112. },
  113. submitRequest: function () {
  114. return App.ajax.send({
  115. name: this.get('request.ajaxName'),
  116. data: this.get('request.ajaxData'),
  117. sender: this,
  118. error: 'onSingleRequestError',
  119. success: 'submitRequestSuccess',
  120. kdcCancelHandler: 'failTaskOnKdcCheck'
  121. });
  122. },
  123. submitRequestSuccess: function(data, result, request) {
  124. if (data) {
  125. this.set('currentPageRequestId', data.Requests.id);
  126. this.doPollingForPageRequest();
  127. } else {
  128. //Step has been successfully completed
  129. if (request.status === 200) {
  130. this.set('status', 'COMPLETED');
  131. this.set('isSubmitDisabled', false);
  132. this.set('isLoaded', true);
  133. }
  134. }
  135. },
  136. failTaskOnKdcCheck: function() {
  137. this.set('status', 'FAILED');
  138. this.set('isLoaded', true);
  139. this.set('showRetry', true);
  140. },
  141. doPollingForPageRequest: function () {
  142. App.ajax.send({
  143. name: 'admin.poll.kerberize.cluster.request',
  144. sender: this,
  145. data: {
  146. requestId: this.get('currentPageRequestId')
  147. },
  148. success: 'initializeStages'
  149. });
  150. },
  151. initializeStages: function (data) {
  152. var self = this;
  153. var stages = [];
  154. this.set('logs', []);
  155. data.stages.forEach(function (_stage) {
  156. stages.pushObject(Em.Object.create(_stage.Stage));
  157. }, this);
  158. if (!this.get('stages').length) {
  159. this.get('stages').pushObjects(stages);
  160. this.initStep().done(function(){
  161. self.updatePageWithPolledData(data);
  162. });
  163. } else {
  164. this.updatePageWithPolledData(data);
  165. }
  166. },
  167. updatePageWithPolledData: function(data) {
  168. // If all tasks completed no need to update each task status.
  169. // Preferable to skip polling of data for completed tasks after page refresh.
  170. if (this.get('status') === 'COMPLETED') return;
  171. var self = this;
  172. var tasks = [];
  173. var currentPageRequestId = this.get('currentPageRequestId');
  174. var currentTaskId = this.get('currentTaskId');
  175. var currentTask = this.get('tasks').findProperty('id', currentTaskId);
  176. var currentStage = data.stages.findProperty('Stage.stage_id', currentTask.get('stageId'));
  177. var tasksInCurrentStage = currentStage.tasks;
  178. this.set('logs',tasksInCurrentStage);
  179. this.setRequestIds(this.get('currentTaskId'), [this.get('currentPageRequestId')]);
  180. if (!tasksInCurrentStage.someProperty('Tasks.status', 'PENDING') && !tasksInCurrentStage.someProperty('Tasks.status', 'QUEUED') && !tasksInCurrentStage.someProperty('Tasks.status', 'IN_PROGRESS')) {
  181. this.set('currentRequestIds', []);
  182. if (tasksInCurrentStage.someProperty('Tasks.status', 'FAILED') || tasksInCurrentStage.someProperty('Tasks.status', 'TIMEDOUT') || tasksInCurrentStage.someProperty('Tasks.status', 'ABORTED')) {
  183. this.setTaskStatus(currentTaskId, 'FAILED');
  184. } else {
  185. this.setTaskStatus(currentTaskId, 'COMPLETED');
  186. }
  187. } else {
  188. var completedActions = tasksInCurrentStage.filterProperty('Tasks.status', 'COMPLETED').length
  189. + tasksInCurrentStage.filterProperty('Tasks.status', 'FAILED').length
  190. + tasksInCurrentStage.filterProperty('Tasks.status', 'ABORTED').length
  191. + tasksInCurrentStage.filterProperty('Tasks.status', 'TIMEDOUT').length;
  192. var queuedActions = tasksInCurrentStage.filterProperty('Tasks.status', 'QUEUED').length;
  193. var inProgressActions = tasksInCurrentStage.filterProperty('Tasks.status', 'IN_PROGRESS').length;
  194. var progress = completedActions == this.get('tasks.length') ? 100 : Math.floor(((queuedActions * 0.09) + (inProgressActions * 0.35) + completedActions ) / tasksInCurrentStage.length * 100);
  195. this.get('tasks').findProperty('id', currentTaskId).set('progress', progress);
  196. }
  197. // start polling if current step status not completed or failed
  198. if (!(this.get('status') === 'COMPLETED' && this.get('status') === 'FAILED')) {
  199. window.setTimeout(function () {
  200. self.doPollingForPageRequest();
  201. }, self.POLL_INTERVAL);
  202. }
  203. },
  204. initializeTasks: function () {
  205. var self = this;
  206. var commands = this.isSingleRequestPage ? this.get('stages') : this.get('commands');
  207. var currentStep = App.router.get(this.get('content.controllerName') + '.currentStep');
  208. var tasksMessagesPrefix = this.get('tasksMessagesPrefix');
  209. // check that all stages have been completed for single request type
  210. var allStagesCompleted = commands.everyProperty('status', 'COMPLETED');
  211. for (var i = 0; i < commands.length; i++) {
  212. this.get('tasks').pushObject(Ember.Object.create({
  213. title: self.isSingleRequestPage ? commands[i].get('context') : Em.I18n.t(tasksMessagesPrefix + currentStep + '.task' + i + '.title'),
  214. // set COMPLETED status for task if all stages completed successfully
  215. status: allStagesCompleted ? 'COMPLETED' : 'PENDING',
  216. id: i,
  217. stageId: self.isSingleRequestPage ? commands[i].get('stage_id') : null,
  218. command: self.isSingleRequestPage ? 'k' : commands[i],
  219. showRetry: false,
  220. showRollback: false,
  221. name: self.isSingleRequestPage ? commands[i].get('context') : Em.I18n.t(tasksMessagesPrefix + currentStep + '.task' + i + '.title'),
  222. displayName: self.isSingleRequestPage ? commands[i].get('context') : Em.I18n.t(tasksMessagesPrefix + currentStep + '.task' + i + '.title'),
  223. progress: 0,
  224. isRunning: false,
  225. requestIds: self.isSingleRequestPage ? [this.get('stages')[0].request_id] : []
  226. }));
  227. }
  228. this.set('isLoaded', true);
  229. },
  230. loadTasks: function () {
  231. var self = this;
  232. var loadedStatuses = this.get('content.tasksStatuses');
  233. var loadedRequestIds = this.get('content.tasksRequestIds');
  234. if (loadedStatuses && loadedStatuses.length === this.get('tasks').length) {
  235. this.get('tasks').forEach(function (task, i) {
  236. self.setTaskStatus(task.get('id'), loadedStatuses[i]);
  237. self.setRequestIds(task.get('id'), loadedRequestIds[i]);
  238. });
  239. if (loadedStatuses.contains('IN_PROGRESS')) {
  240. var curTaskId = this.get('tasks')[loadedStatuses.indexOf('IN_PROGRESS')].get('id');
  241. this.set('currentRequestIds', this.get('content.requestIds'));
  242. this.set('currentTaskId', curTaskId);
  243. this.doPolling();
  244. } else if (loadedStatuses.contains('QUEUED')) {
  245. var curTaskId = this.get('tasks')[loadedStatuses.indexOf('QUEUED')].get('id');
  246. this.set('currentTaskId', curTaskId);
  247. this.runTask(curTaskId);
  248. }
  249. }
  250. },
  251. setTaskStatus: function (taskId, status) {
  252. this.get('tasks').findProperty('id', taskId).set('status', status);
  253. },
  254. setRequestIds: function (taskId, requestIds) {
  255. this.get('tasks').findProperty('id', taskId).set('requestIds', requestIds);
  256. },
  257. retryTask: function () {
  258. var task = this.get('tasks').findProperty('status', 'FAILED');
  259. task.set('showRetry', false);
  260. task.set('showRollback', false);
  261. task.set('status', 'PENDING');
  262. },
  263. onTaskStatusChange: function (dfdObject) {
  264. var statuses = this.get('tasks').mapProperty('status');
  265. var tasksRequestIds = this.get('tasks').mapProperty('requestIds');
  266. var requestIds = this.get('currentRequestIds');
  267. // save task info
  268. App.router.get(this.get('content.controllerName')).saveTasksStatuses(statuses);
  269. App.router.get(this.get('content.controllerName')).saveTasksRequestIds(tasksRequestIds);
  270. App.router.get(this.get('content.controllerName')).saveRequestIds(requestIds);
  271. // call saving of cluster status asynchronous
  272. // synchronous executing cause problems in Firefox
  273. var successCallbackData;
  274. if (dfdObject && dfdObject.isJqueryPromise) {
  275. successCallbackData = {deferred: dfdObject.deferred};
  276. }
  277. App.clusterStatus.setClusterStatus({
  278. clusterName: App.router.getClusterName(),
  279. clusterState: this.get('clusterDeployState'),
  280. wizardControllerName: this.get('content.controllerName'),
  281. localdb: App.db.data
  282. }, {successCallback: this.statusChangeCallback, sender: this, successCallbackData: successCallbackData});
  283. },
  284. /**
  285. * Method that called after saving persist data to server.
  286. * Switch task according its status.
  287. */
  288. statusChangeCallback: function (data) {
  289. if (!this.get('tasks').someProperty('status', 'IN_PROGRESS') && !this.get('tasks').someProperty('status', 'QUEUED') && !this.get('tasks').someProperty('status', 'FAILED')) {
  290. var nextTask = this.get('tasks').findProperty('status', 'PENDING');
  291. if (nextTask) {
  292. this.set('status', 'IN_PROGRESS');
  293. var taskStatus = this.isSingleRequestPage ? 'IN_PROGRESS' : 'QUEUED';
  294. this.setTaskStatus(nextTask.get('id'), taskStatus);
  295. this.set('currentTaskId', nextTask.get('id'));
  296. this.runTask(nextTask.get('id'));
  297. } else {
  298. this.set('status', 'COMPLETED');
  299. this.set('isSubmitDisabled', false);
  300. this.set('isBackButtonDisabled', false);
  301. }
  302. } else if (this.get('tasks').someProperty('status', 'FAILED')) {
  303. this.set('status', 'FAILED');
  304. this.set('isBackButtonDisabled', false);
  305. if (this.get('isCommandLevelRetry')) {
  306. this.get('tasks').findProperty('status', 'FAILED').set('showRetry', true);
  307. } else {
  308. this.set('showRetry', true);
  309. }
  310. if (App.supports.autoRollbackHA) {
  311. this.get('tasks').findProperty('status', 'FAILED').set('showRollback', true);
  312. }
  313. }
  314. this.get('tasks').filterProperty('status', 'COMPLETED').setEach('showRetry', false);
  315. this.get('tasks').filterProperty('status', 'COMPLETED').setEach('showRollback', false);
  316. if (data && data.deferred) {
  317. data.deferred.resolve();
  318. }
  319. },
  320. /**
  321. * Run command of appropriate task
  322. */
  323. runTask: function (taskId) {
  324. this[this.get('tasks').findProperty('id', taskId).get('command')]();
  325. },
  326. onTaskError: function () {
  327. this.setTaskStatus(this.get('currentTaskId'), 'FAILED');
  328. },
  329. onSingleRequestError: function (jqXHR, ajaxOptions, error, opt) {
  330. App.ajax.defaultErrorHandler(jqXHR, opt.url, opt.method, jqXHR.status);
  331. this.set('status', 'FAILED');
  332. this.set('isLoaded', true);
  333. this.set('showRetry', true);
  334. },
  335. onTaskCompleted: function () {
  336. this.setTaskStatus(this.get('currentTaskId'), 'COMPLETED');
  337. },
  338. /**
  339. * check whether component installed on specified hosts
  340. * @param {string} componentName
  341. * @param {string[]} hostNames
  342. * @return {$.ajax}
  343. */
  344. checkInstalledComponents: function (componentName, hostNames) {
  345. return App.ajax.send({
  346. name: 'host_component.installed.on_hosts',
  347. sender: this,
  348. data: {
  349. componentName: componentName,
  350. hostNames: hostNames.join(',')
  351. }
  352. });
  353. },
  354. /**
  355. * Create component on single or multiple hosts.
  356. *
  357. * @method createComponent
  358. * @param {string} componentName - name of the component
  359. * @param {(string|string[])} hostName - host/hosts where components should be installed
  360. * @param {string} serviceName - name of the services
  361. */
  362. createComponent: function (componentName, hostName, serviceName) {
  363. var hostNames = (Array.isArray(hostName)) ? hostName : [hostName];
  364. var self = this;
  365. this.set('showRetry', false);
  366. this.checkInstalledComponents(componentName, hostNames).then(function (data) {
  367. var hostsWithComponents = data.items.mapProperty('HostRoles.host_name');
  368. var result = hostNames.map(function(item) {
  369. return {
  370. componentName: componentName,
  371. hostName: item,
  372. hasComponent: hostsWithComponents.contains(item)
  373. };
  374. });
  375. var hostsWithoutComponents = result.filterProperty('hasComponent', false).mapProperty('hostName');
  376. var taskNum = 1;
  377. var requestData = {
  378. "RequestInfo": {
  379. "query": hostsWithoutComponents.map(function(item) {
  380. return 'Hosts/host_name=' + item;
  381. }).join('|')
  382. },
  383. "Body": {
  384. "host_components": [
  385. {
  386. "HostRoles": {
  387. "component_name": componentName
  388. }
  389. }
  390. ]
  391. }
  392. };
  393. if (!!hostsWithoutComponents.length) {
  394. App.ajax.send({
  395. name: 'wizard.step8.register_host_to_component',
  396. sender: self,
  397. data: {
  398. data: JSON.stringify(requestData),
  399. hostName: result.mapProperty('hostName'),
  400. componentName: componentName,
  401. serviceName: serviceName,
  402. taskNum: taskNum,
  403. cluster: App.get('clusterName')
  404. },
  405. success: 'onCreateComponent',
  406. error: 'onCreateComponent'
  407. });
  408. } else {
  409. self.onCreateComponent(null, null, {
  410. hostName: result.mapProperty('hostName'),
  411. componentName: componentName,
  412. serviceName: serviceName,
  413. taskNum: taskNum
  414. }, self);
  415. }
  416. });
  417. },
  418. onCreateComponent: function () {
  419. var hostName = arguments[2].hostName;
  420. var componentName = arguments[2].componentName;
  421. var taskNum = arguments[2].taskNum;
  422. var serviceName = arguments[2].serviceName;
  423. this.updateComponent(componentName, hostName, serviceName, "Install", taskNum);
  424. },
  425. onCreateComponentError: function (error) {
  426. if (error.responseText.indexOf('org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException') !== -1) {
  427. this.onCreateComponent();
  428. } else {
  429. this.onTaskError();
  430. }
  431. },
  432. /**
  433. * Update component status on selected hosts.
  434. *
  435. * @param {string} componentName
  436. * @param {(string|string[])} hostName
  437. * @param {string} serviceName
  438. * @param {string} context
  439. * @param {number} taskNum
  440. * @returns {$.ajax}
  441. */
  442. updateComponent: function (componentName, hostName, serviceName, context, taskNum) {
  443. if (!(hostName instanceof Array)) {
  444. hostName = [hostName];
  445. }
  446. var state = context.toLowerCase() == "start" ? "STARTED" : "INSTALLED";
  447. return App.ajax.send({
  448. name: 'common.host_components.update',
  449. sender: this,
  450. data: {
  451. HostRoles: {
  452. state: state
  453. },
  454. query: 'HostRoles/component_name=' + componentName + '&HostRoles/host_name.in(' + hostName.join(',') + ')&HostRoles/maintenance_state=OFF',
  455. context: context + " " + App.format.role(componentName),
  456. hostName: hostName,
  457. taskNum: taskNum || 1,
  458. componentName: componentName,
  459. serviceName: serviceName
  460. },
  461. success: 'startPolling',
  462. error: 'onTaskError'
  463. });
  464. },
  465. startPolling: function (data) {
  466. if (data) {
  467. this.get('currentRequestIds').push(data.Requests.id);
  468. var tasksCount = arguments[2].taskNum || 1;
  469. if (tasksCount === this.get('currentRequestIds').length) {
  470. this.setRequestIds(this.get('currentTaskId'), this.get('currentRequestIds'));
  471. this.doPolling();
  472. }
  473. } else {
  474. this.onTaskCompleted();
  475. }
  476. },
  477. doPolling: function () {
  478. this.setTaskStatus(this.get('currentTaskId'), 'IN_PROGRESS');
  479. var requestIds = this.get('currentRequestIds');
  480. this.set('logs', []);
  481. for (var i = 0; i < requestIds.length; i++) {
  482. App.ajax.send({
  483. name: 'admin.high_availability.polling',
  484. sender: this,
  485. data: {
  486. requestId: requestIds[i]
  487. },
  488. success: 'parseLogs',
  489. error: 'onTaskError'
  490. });
  491. }
  492. },
  493. parseLogs: function (logs) {
  494. this.get('logs').pushObject(logs.tasks);
  495. if (this.get('currentRequestIds').length === this.get('logs').length) {
  496. var tasks = [];
  497. this.get('logs').forEach(function (logs) {
  498. tasks.pushObjects(logs);
  499. }, this);
  500. var self = this;
  501. var currentTaskId = this.get('currentTaskId');
  502. if (!tasks.someProperty('Tasks.status', 'PENDING') && !tasks.someProperty('Tasks.status', 'QUEUED') && !tasks.someProperty('Tasks.status', 'IN_PROGRESS')) {
  503. this.set('currentRequestIds', []);
  504. if (tasks.someProperty('Tasks.status', 'FAILED') || tasks.someProperty('Tasks.status', 'TIMEDOUT') || tasks.someProperty('Tasks.status', 'ABORTED')) {
  505. this.setTaskStatus(currentTaskId, 'FAILED');
  506. } else {
  507. this.setTaskStatus(currentTaskId, 'COMPLETED');
  508. }
  509. } else {
  510. var actionsPerHost = tasks.length;
  511. var completedActions = tasks.filterProperty('Tasks.status', 'COMPLETED').length
  512. + tasks.filterProperty('Tasks.status', 'FAILED').length
  513. + tasks.filterProperty('Tasks.status', 'ABORTED').length
  514. + tasks.filterProperty('Tasks.status', 'TIMEDOUT').length;
  515. var queuedActions = tasks.filterProperty('Tasks.status', 'QUEUED').length;
  516. var inProgressActions = tasks.filterProperty('Tasks.status', 'IN_PROGRESS').length;
  517. var progress = Math.ceil(((queuedActions * 0.09) + (inProgressActions * 0.35) + completedActions ) / actionsPerHost * 100);
  518. this.get('tasks').findProperty('id', currentTaskId).set('progress', progress);
  519. window.setTimeout(function () {
  520. self.doPolling();
  521. }, self.POLL_INTERVAL);
  522. }
  523. }
  524. },
  525. showHostProgressPopup: function (event) {
  526. var popupTitle = event.contexts[0].title;
  527. var requestIds = event.contexts[0].requestIds;
  528. var stageId = event.contexts[0].stageId;
  529. var hostProgressPopupController = App.router.get('highAvailabilityProgressPopupController');
  530. hostProgressPopupController.initPopup(popupTitle, requestIds, this, true, stageId);
  531. },
  532. done: function () {
  533. if (!this.get('isSubmitDisabled')) {
  534. this.removeObserver('tasks.@each.status', this, 'onTaskStatusChange');
  535. App.router.send('next');
  536. }
  537. },
  538. back: function () {
  539. if (!this.get('isBackButtonDisabled')) {
  540. this.removeObserver('tasks.@each.status', this, 'onTaskStatusChange');
  541. App.router.send('back');
  542. }
  543. }
  544. });