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