1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// require cr.js 6// require cr/event_target.js 7// require cr/ui.js 8// require cr/ui/tabs.js 9// require cr/ui/tree.js 10// require cr/util.js 11 12(function() { 13'use strict'; 14 15/** 16 * @param {Object} object Object to be checked. 17 * @return {boolean} true if |object| is {}. 18 * @private 19 */ 20function isEmptyObject_(object) { 21 for (var i in object) 22 return false; 23 return true; 24} 25 26/** 27 * Copy properties from |source| to |destination|. 28 * @param {Object} source Source of the copy. 29 * @param {Object} destination Destination of the copy. 30 * @return {Object} |destination|. 31 * @private 32 */ 33function copyAttributes_(source, destination) { 34 for (var i in source) 35 destination[i] = source[i]; 36 return destination; 37}; 38 39/** 40 * Apply localization to |element| with i18n_template.js if available. 41 * @param {Element} element Element to be localized. 42 * @private 43 */ 44function localize_(element) { 45 if (window.i18nTemplate && window.templateData) 46 i18nTemplate.process(element, templateData); 47}; 48 49/** 50 * Returns 'N/A' (Not Available) text if |value| is undefined. 51 * @param {Object} value Object to print. 52 * @return {string} 'N/A' or ''. 53 * @private 54 */ 55function checkIfAvailable_(value) { 56 return value === undefined ? 'N/A' : ''; 57} 58 59/** 60 * Returns |value| itself if |value| is not undefined, 61 * else returns 'N/A' text. 62 * @param {?string} value String to print. 63 * @return {string} 'N/A' or |value|. 64 * @private 65 */ 66function stringToText_(value) { 67 return checkIfAvailable_(value) || value; 68} 69 70/** 71 * Separates |value| into segments. 72 * The length of first segment is at most |maxLength|. 73 * Length of other following segments are just |maxLength|. 74 * e.g. separateBackward_('abcdefghijk', 4) == ['abc','defg','hijk']; 75 * @param {string} value String to be separated. 76 * @param {number} maxLength Max length of segments. 77 * @return {Array.<string>} Array of segments. 78 * @private 79 */ 80function separateBackward_(value, maxLength) { 81 var result = []; 82 while (value.length > maxLength) { 83 result.unshift(value.slice(-3)); 84 value = value.slice(0, -3); 85 } 86 result.unshift(value); 87 return result; 88} 89 90/** 91 * Returns formatted string from number as number of bytes. 92 * e.g. numBytesToText(123456789) = '123.45 MB (123,456,789 B)'. 93 * If |value| is undefined, this function returns 'N/A'. 94 * @param {?number} value Number to print. 95 * @return {string} 'N/A' or formatted |value|. 96 * @private 97 */ 98function numBytesToText_(value) { 99 var result = checkIfAvailable_(value); 100 if (result) 101 return result; 102 103 var segments = separateBackward_(value.toString(), 3); 104 result = segments.join(',') + ' B'; 105 106 if (segments.length > 1) { 107 var UNIT = [' B', ' KB', ' MB', ' GB', ' TB', ' PB']; 108 result = segments[0] + '.' + segments[1].slice(0, 2) + 109 UNIT[Math.min(segments.length, UNIT.length) - 1] + 110 ' (' + result + ')'; 111 } 112 113 return result; 114} 115 116/** 117 * Return formatted date |value| if |value| is not undefined. 118 * If |value| is undefined, this function returns 'N/A'. 119 * @param {?number} value Number of milliseconds since 120 * UNIX epoch time (0:00, Jan 1, 1970, UTC). 121 * @return {string} Formatted text of date or 'N/A'. 122 * @private 123 */ 124function dateToText(value) { 125 var result = checkIfAvailable_(value); 126 if (result) 127 return result; 128 129 var time = new Date(value); 130 var now = new Date(); 131 var delta = Date.now() - value; 132 133 var SECOND = 1000; 134 var MINUTE = 60 * SECOND; 135 var HOUR = 60 * MINUTE; 136 var DAY = 23 * HOUR; 137 var WEEK = 7 * DAY; 138 139 var SHOW_SECOND = 5 * MINUTE; 140 var SHOW_MINUTE = 5 * HOUR; 141 var SHOW_HOUR = 3 * DAY; 142 var SHOW_DAY = 2 * WEEK; 143 var SHOW_WEEK = 3 * 30 * DAY; 144 145 if (delta < 0) { 146 result = 'access from future '; 147 } else if (delta < SHOW_SECOND) { 148 result = Math.ceil(delta / SECOND) + ' sec ago '; 149 } else if (delta < SHOW_MINUTE) { 150 result = Math.ceil(delta / MINUTE) + ' min ago '; 151 } else if (delta < SHOW_HOUR) { 152 result = Math.ceil(delta / HOUR) + ' hr ago '; 153 } else if (delta < SHOW_WEEK) { 154 result = Math.ceil(delta / DAY) + ' day ago '; 155 } 156 157 result += '(' + time.toString() + ')'; 158 return result; 159} 160 161/** 162 * Available disk space. 163 * @type {number|undefined} 164 */ 165var availableSpace = undefined; 166 167/** 168 * Root of the quota data tree, 169 * holding userdata as |treeViewObject.detail|. 170 * @type {cr.ui.Tree} 171 */ 172var treeViewObject = undefined; 173 174/** 175 * Key-value styled statistics data. 176 * This WebUI does not touch contents, just show. 177 * The value is hold as |statistics[key].detail|. 178 * @type {Object<string,Element>} 179 */ 180var statistics = {}; 181 182/** 183 * Initialize and return |treeViewObject|. 184 * @return {cr.ui.Tree} Initialized |treeViewObject|. 185 */ 186function getTreeViewObject() { 187 if (!treeViewObject) { 188 treeViewObject = $('tree-view'); 189 cr.ui.decorate(treeViewObject, cr.ui.Tree); 190 treeViewObject.detail = {payload: {}, children: {}}; 191 treeViewObject.addEventListener('change', updateDescription); 192 } 193 return treeViewObject; 194} 195 196/** 197 * Initialize and return a tree item, that represents specified storage type. 198 * @param {!string} type Storage type. 199 * @return {cr.ui.TreeItem} Initialized |storageObject|. 200 */ 201function getStorageObject(type) { 202 var treeViewObject = getTreeViewObject(); 203 var storageObject = treeViewObject.detail.children[type]; 204 if (!storageObject) { 205 storageObject = new cr.ui.TreeItem({ 206 label: type, 207 detail: {payload: {}, children: {}} 208 }); 209 storageObject.mayHaveChildren_ = true; 210 treeViewObject.detail.children[type] = storageObject; 211 treeViewObject.add(storageObject); 212 } 213 return storageObject; 214} 215 216/** 217 * Initialize and return a tree item, that represents specified 218 * storage type and hostname. 219 * @param {!string} type Storage type. 220 * @param {!string} host Hostname. 221 * @return {cr.ui.TreeItem} Initialized |hostObject|. 222 */ 223function getHostObject(type, host) { 224 var storageObject = getStorageObject(type); 225 var hostObject = storageObject.detail.children[host]; 226 if (!hostObject) { 227 hostObject = new cr.ui.TreeItem({ 228 label: host, 229 detail: {payload: {}, children: {}} 230 }); 231 hostObject.mayHaveChildren_ = true; 232 storageObject.detail.children[host] = hostObject; 233 storageObject.add(hostObject); 234 } 235 return hostObject; 236} 237 238/** 239 * Initialize and return a tree item, that represents specified 240 * storage type, hostname and origin url. 241 * @param {!string} type Storage type. 242 * @param {!string} host Hostname. 243 * @param {!string} origin Origin URL. 244 * @return {cr.ui.TreeItem} Initialized |originObject|. 245 */ 246function getOriginObject(type, host, origin) { 247 var hostObject = getHostObject(type, host); 248 var originObject = hostObject.detail.children[origin]; 249 if (!originObject) { 250 originObject = new cr.ui.TreeItem({ 251 label: origin, 252 detail: {payload: {}, children: {}} 253 }); 254 originObject.mayHaveChildren_ = false; 255 hostObject.detail.children[origin] = originObject; 256 hostObject.add(originObject); 257 } 258 return originObject; 259} 260 261/** 262 * Event Handler for |cr.quota.onAvailableSpaceUpdated|. 263 * |event.detail| contains |availableSpace|. 264 * |availableSpace| represents total available disk space. 265 * @param {CustomEvent} event AvailableSpaceUpdated event. 266 */ 267function handleAvailableSpace(event) { 268 /** 269 * @type {string} 270 */ 271 availableSpace = event.detail; 272 $('diskspace-entry').innerHTML = numBytesToText_(availableSpace); 273}; 274 275/** 276 * Event Handler for |cr.quota.onGlobalInfoUpdated|. 277 * |event.detail| contains a record which has: 278 * |type|: 279 * Storage type, that is either 'temporary' or 'persistent'. 280 * |usage|: 281 * Total storage usage of all hosts. 282 * |unlimitedUsage|: 283 * Total storage usage of unlimited-quota origins. 284 * |quota|: 285 * Total quota of the storage. 286 * 287 * |usage|, |unlimitedUsage| and |quota| can be missing, 288 * and some additional fields can be included. 289 * @param {CustomEvent} event GlobalInfoUpdated event. 290 */ 291function handleGlobalInfo(event) { 292 /** 293 * @type {{ 294 * type: {!string}, 295 * usage: {?number}, 296 * unlimitedUsage: {?number} 297 * quota: {?string} 298 * }} 299 */ 300 var data = event.detail; 301 var storageObject = getStorageObject(data.type); 302 copyAttributes_(data, storageObject.detail.payload); 303 storageObject.reveal(); 304 if (getTreeViewObject().selectedItem == storageObject) 305 updateDescription(); 306 307}; 308 309/** 310 * Event Handler for |cr.quota.onPerHostInfoUpdated|. 311 * |event.detail| contains records which have: 312 * |host|: 313 * Hostname of the entry. (e.g. 'example.com') 314 * |type|: 315 * Storage type. 'temporary' or 'persistent' 316 * |usage|: 317 * Total storage usage of the host. 318 * |quota|: 319 * Per-host quota. 320 * 321 * |usage| and |quota| can be missing, 322 * and some additional fields can be included. 323 * @param {CustomEvent} event PerHostInfoUpdated event. 324 */ 325function handlePerHostInfo(event) { 326 /** 327 * @type {Array<{ 328 * host: {!string}, 329 * type: {!string}, 330 * usage: {?number}, 331 * quota: {?number} 332 * }} 333 */ 334 var dataArray = event.detail; 335 336 for (var i = 0; i < dataArray.length; ++i) { 337 var data = dataArray[i]; 338 var hostObject = getHostObject(data.type, data.host); 339 copyAttributes_(data, hostObject.detail.payload); 340 hostObject.reveal(); 341 if (getTreeViewObject().selectedItem == hostObject) 342 updateDescription(); 343 344 } 345} 346 347/** 348 * Event Handler for |cr.quota.onPerOriginInfoUpdated|. 349 * |event.detail| contains records which have: 350 * |origin|: 351 * Origin URL of the entry. 352 * |type|: 353 * Storage type of the entry. 'temporary' or 'persistent'. 354 * |host|: 355 * Hostname of the entry. 356 * |inUse|: 357 * true if the origin is in use. 358 * |usedCount|: 359 * Used count of the storage from the origin. 360 * |lastAccessTime|: 361 * Last storage access time from the origin. 362 * Number of milliseconds since UNIX epoch (Jan 1, 1970, 0:00:00 UTC). 363 * |lastModifiedTime|: 364 * Last modified time of the storage from the origin. 365 * Number of milliseconds since UNIX epoch. 366 * 367 * |inUse|, |usedCount|, |lastAccessTime| and |lastModifiedTime| can be missing, 368 * and some additional fields can be included. 369 * @param {CustomEvent} event PerOriginInfoUpdated event. 370 */ 371function handlePerOriginInfo(event) { 372 /** 373 * @type {Array<{ 374 * origin: {!string}, 375 * type: {!string}, 376 * host: {!string}, 377 * inUse: {?boolean}, 378 * usedCount: {?number}, 379 * lastAccessTime: {?number} 380 * lastModifiedTime: {?number} 381 * }>} 382 */ 383 var dataArray = event.detail; 384 385 for (var i = 0; i < dataArray.length; ++i) { 386 var data = dataArray[i]; 387 var originObject = getOriginObject(data.type, data.host, data.origin); 388 copyAttributes_(data, originObject.detail.payload); 389 originObject.reveal(); 390 if (getTreeViewObject().selectedItem == originObject) 391 updateDescription(); 392 } 393} 394 395/** 396 * Event Handler for |cr.quota.onStatisticsUpdated|. 397 * |event.detail| contains misc statistics data as dictionary. 398 * @param {CustomEvent} event StatisticsUpdated event. 399 */ 400function handleStatistics(event) { 401 /** 402 * @type {Object.<string>} 403 */ 404 var data = event.detail; 405 for (var key in data) { 406 var entry = statistics[key]; 407 if (!entry) { 408 entry = cr.doc.createElement('tr'); 409 $('stat-entries').appendChild(entry); 410 statistics[key] = entry; 411 } 412 entry.detail = data[key]; 413 entry.innerHTML = 414 '<td>' + stringToText_(key) + '</td>' + 415 '<td>' + stringToText_(entry.detail) + '</td>'; 416 localize_(entry); 417 } 418} 419 420/** 421 * Update description on 'tree-item-description' field with 422 * selected item in tree view. 423 */ 424function updateDescription() { 425 var item = getTreeViewObject().selectedItem; 426 var tbody = $('tree-item-description'); 427 tbody.innerHTML = ''; 428 429 if (item) { 430 var keyAndLabel = [['type', 'Storage Type'], 431 ['host', 'Host Name'], 432 ['origin', 'Origin URL'], 433 ['usage', 'Total Storage Usage', numBytesToText_], 434 ['unlimitedUsage', 'Usage of Unlimited Origins', 435 numBytesToText_], 436 ['quota', 'Quota', numBytesToText_], 437 ['inUse', 'Origin is in use?'], 438 ['usedCount', 'Used count'], 439 ['lastAccessTime', 'Last Access Time', 440 dateToText], 441 ['lastModifiedTime', 'Last Modified Time', 442 dateToText] 443 ]; 444 for (var i = 0; i < keyAndLabel.length; ++i) { 445 var key = keyAndLabel[i][0]; 446 var label = keyAndLabel[i][1]; 447 var entry = item.detail.payload[key]; 448 if (entry === undefined) 449 continue; 450 451 var normalize = keyAndLabel[i][2] || stringToText_; 452 453 var row = cr.doc.createElement('tr'); 454 row.innerHTML = 455 '<td>' + label + '</td>' + 456 '<td>' + normalize(entry) + '</td>'; 457 localize_(row); 458 tbody.appendChild(row); 459 } 460 } 461} 462 463/** 464 * Dump |treeViewObject| or subtree to a object. 465 * @param {?{cr.ui.Tree|cr.ui.TreeItem}} opt_treeitem 466 * @return {Object} Dump result object from |treeViewObject|. 467 */ 468function dumpTreeToObj(opt_treeitem) { 469 var treeitem = opt_treeitem || getTreeViewObject(); 470 var res = {}; 471 res.payload = treeitem.detail.payload; 472 res.children = []; 473 for (var i in treeitem.detail.children) { 474 var child = treeitem.detail.children[i]; 475 res.children.push(dumpTreeToObj(child)); 476 } 477 478 if (isEmptyObject_(res.payload)) 479 delete res.payload; 480 481 if (res.children.length == 0) 482 delete res.children; 483 return res; 484} 485 486/** 487 * Dump |statistics| to a object. 488 * @return {Object} Dump result object from |statistics|. 489 */ 490function dumpStatisticsToObj() { 491 var result = {}; 492 for (var key in statistics) 493 result[key] = statistics[key].detail; 494 return result; 495} 496 497/** 498 * Event handler for 'dump-button' 'click'ed. 499 * Dump and show all data from WebUI page to 'dump-field' element. 500 */ 501function dump() { 502 var separator = '========\n'; 503 504 $('dump-field').textContent = 505 separator + 506 'Summary\n' + 507 separator + 508 JSON.stringify({availableSpace: availableSpace}, null, 2) + '\n' + 509 separator + 510 'Usage And Quota\n' + 511 separator + 512 JSON.stringify(dumpTreeToObj(), null, 2) + '\n' + 513 separator + 514 'Misc Statistics\n' + 515 separator + 516 JSON.stringify(dumpStatisticsToObj(), null, 2); 517} 518 519function onLoad() { 520 cr.ui.decorate('tabbox', cr.ui.TabBox); 521 localize_(document); 522 523 cr.quota.onAvailableSpaceUpdated.addEventListener('update', 524 handleAvailableSpace); 525 cr.quota.onGlobalInfoUpdated.addEventListener('update', handleGlobalInfo); 526 cr.quota.onPerHostInfoUpdated.addEventListener('update', handlePerHostInfo); 527 cr.quota.onPerOriginInfoUpdated.addEventListener('update', 528 handlePerOriginInfo); 529 cr.quota.onStatisticsUpdated.addEventListener('update', handleStatistics); 530 cr.quota.requestInfo(); 531 532 $('refresh-button').addEventListener('click', cr.quota.requestInfo, false); 533 $('dump-button').addEventListener('click', dump, false); 534} 535 536cr.doc.addEventListener('DOMContentLoaded', onLoad, false); 537})(); 538