Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2336)

Unified Diff: chrome/common/extensions/docs/examples/extensions/storage_api_devtools/storage/panel.js

Issue 12760009: Write a devtools extension to inspect chrome.storage data Base URL: https://src.chromium.org/svn/trunk/src/
Patch Set: small fixes Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/common/extensions/docs/examples/extensions/storage_api_devtools/storage/panel.js
===================================================================
--- chrome/common/extensions/docs/examples/extensions/storage_api_devtools/storage/panel.js (revision 0)
+++ chrome/common/extensions/docs/examples/extensions/storage_api_devtools/storage/panel.js (revision 0)
@@ -0,0 +1,561 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Called by devtools.js to add content script or main world contexts.
+var onContextAdded = function(contexts){};
+// Called by panel.js to notify that the inspected window has unloaded.
+var onInspectedWindowUnloaded = function(){};
+
+// Escape key names starting with '$' since they are ignored by AngularJS.
+function escapeKey(key) {
+ if (key[0] == '$' || key[0] == '_')
+ return '_' + key;
+ else
+ return key;
+}
+
+function unescapeKey(key) {
+ if (key[0] == '_')
+ return key.substr(1);
+ else
+ return key;
+}
+
+function StorageDevTools() {
+ var contexts = [];
+ var current = -1;
+ var port;
+ var devtoolsId;
+ var devtoolsRegistered = false;
+ var storage = this;
+
+ // Events
+ // Fired when execution contexts have been added.
+ this.onContextAdded = function() {};
+ // Fired when the model of the current context changed.
+ this.onModelUpdated = function() {};
+ // Fired when the inspected window navigates or closes.
+ this.onInvalidated = function() {};
+ // Fired when an error occurred.
+ this.onError = function(details) {};
+
+ function initializeContexts() {
+ contexts.forEach(function(context) {
+ if (!context.initialized) {
+ context.initialized = true;
+ var options = {};
+ if (context.type == 'contentScripts')
+ options = {scriptExecutionContext: context.securityOrigin};
+ evalScript('/storage/inspectedWindow/initStorage.js', options,
+ {devtoolsId: devtoolsId, reportUsage: 'true',
+ contextId: context.id});
+ }
+ });
+ }
+
+ function onDevToolsMessage(message) {
+ var prefix = 'devtools.';
+ if (message && message.name) {
+ if (message.name == '_inspectedWindowUnload') {
+ current = -1;
+ contexts = [];
+ storage.onInvalidated();
+ window.onInspectedWindowUnloaded(); // exported by devtools.js
+ } else if (message.name.substr(0, prefix.length) == prefix &&
+ message.contextId != undefined)
+ onInspectedWindowMessage(message.name.substr(prefix.length),
+ message);
+ else
+ console.warn('Unknown message: ', message);
+ }
+ }
+
+ onContextAdded = (function(contexts_) {
+ contexts_.forEach(function(cur) {
+ cur.id = cur.extensionId || '<main_world>';
+ cur.model = newStorageModel();
+ });
+ contexts = contexts.concat(contexts_);
+
+ if (contexts.length && current == -1)
+ current = 0;
+
+ if (!devtoolsRegistered) {
+ devtoolsRegistered = true;
+ registerDevToolsPanel('storage', function(devtoolsId_, port_) {
+ devtoolsId = devtoolsId_; // Used in initializeContexts().
+ port = port_;
+ port.onMessage.addListener(onDevToolsMessage);
+
+ initializeContexts();
+ });
+ } else if (port && devtoolsId)
+ initializeContexts();
+
+ storage.onContextAdded();
+ });
+
+ function newStorageModel() {
+ return {
+ local: {
+ items: {},
+ newItem: {},
+ stat: {
+ itemNumber: 0,
+ totalBytes: 0,
+ maxItemLength: 0,
+ },
+ limits: {
+ quotaBytes: chrome.storage.local.QUOTA_BYTES
+ }
+ },
+ sync: {
+ items: {},
+ newItem: {},
+ stat: {
+ itemNumber: 0,
+ totalBytes: 0,
+ maxItemLength: 0,
+ },
+ limits: {
+ quotaBytes: chrome.storage.sync.QUOTA_BYTES,
+ quotaBytesPerItem: chrome.storage.sync.QUOTA_BYTES_PER_ITEM,
+ maxItems: chrome.storage.sync.MAX_ITEMS,
+ maxWriteOperationsPerHour:
+ chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_HOUR,
+ maxSustainedWriteOperationsPerMinute:
+ chrome.storage.sync.MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE
+ }
+ }
+ };
+ }
+
+ this.currentModel = (function() {
+ if (contexts.length == 0)
+ return {};
+ else
+ return contexts[current].model;
+ });
+
+ this.modelFromContext = (function(contextId) {
+ var result = null;
+ contexts.forEach(function(cur) {
+ if (cur.id == contextId)
+ result = cur.model;
+ });
+ return result;
+ });
+
+ this.switchContext = (function(id) {
+ contexts.forEach(function(cur, index) {
+ if (cur.id == id)
+ current = index;
+ });
+ return this.currentModel();
+ });
+
+ this.getContexts = (function() {
+ var result = [];
+ contexts.forEach(function(cur, index) {
+ result.push({
+ id: cur.id,
+ index: index,
+ type: cur.type,
+ securityOrigin: cur.securityOrigin
+ });
+ });
+ return result;
+ });
+
+ this.getCurrentContextIndex = (function() {
+ return current;
+ });
+
+ this.reload = (function() {
+ current = -1;
+ contexts = [];
+ window.devtoolsReload(); // Exported by devtools.js
+ });
+
+ function checkAreaName(areaName) {
+ return areaName == 'local' || areaName == 'sync';
+ }
+
+ function updateStorageItemNumber(model, areaName) {
+ var area = model[areaName];
+ area.stat.itemNumber = Object.keys(area.items).length;
+ }
+
+ function updateStorageBytesInUse(model, areaName) {
+ var area = model[areaName];
+ area.stat.totalBytes = Object.keys(area.items).reduce(function(prev, cur) {
+ return prev + (area.items[cur].bytesInUse || 0);
+ }, 0);
+ area.stat.maxItemLength = Object.keys(area.items).reduce(
+ function(prev, cur) {
+ return Math.max(prev, area.items[cur].bytesInUse || 0);
+ }, 0);
+ }
+
+ function setStorageUnlimited(model, result) {
+ if (result)
+ model.local.limits.quotaBytes = 0;
+ }
+
+ function appendStorageItem(model, areaName, key, value) {
+ model[areaName].items[escapeKey(key)] = {value: value};
+ updateStorageItemNumber(model, areaName);
+ }
+
+ function updateStorageItem(model, areaName, key, details) {
+ var item_details = model[areaName].items[escapeKey(key)];
+ if (!item_details) {
+ if (details.value) {
+ // (Is it possible that reportUsage is handled first?)
+ appendStorageItem(model, areaName, key, details.value);
+ }
+ return;
+ }
+ if (details.value != undefined)
+ item_details.value = details.value;
+ if (details.bytesInUse != undefined) {
+ item_details.bytesInUse = details.bytesInUse;
+ updateStorageBytesInUse(model, areaName);
+ }
+ }
+
+ function removeStorageItem(model, areaName, key) {
+ delete model[areaName].items[escapeKey(key)];
+ updateStorageItemNumber(model, areaName);
+ updateStorageBytesInUse(model, areaName);
+ }
+
+ function initStorageArea(model, areaName, items) {
+ Object.keys(items).forEach(function(key) {
+ appendStorageItem(model, areaName, key, items[key], false);
+ });
+ }
+
+ function onInspectedWindowMessage(name, message) {
+ var model = storage.modelFromContext(message.contextId);
+ if (!model)
+ return;
+ switch(name) {
+ case 'init':
+ if (message.areaName && message.items &&
+ checkAreaName(message.areaName))
+ initStorageArea(model, message.areaName, message.items);
+ storage.onModelUpdated();
+ break;
+ case 'reportUsage':
+ if (message.areaName && message.key &&
+ (typeof message.bytesInUse) == 'number' &&
+ checkAreaName(message.areaName))
+ updateStorageItem(model, message.areaName, message.key,
+ {bytesInUse: message.bytesInUse});
+ storage.onModelUpdated();
+ break;
+ case 'update':
+ if (message.areaName && message.changes &&
+ checkAreaName(message.areaName)) {
+ Object.keys(message.changes).forEach(function(key) {
+ var change = message.changes[key];
+ if (change.newValue != undefined)
+ updateStorageItem(model, message.areaName, key,
+ {value: change.newValue});
+ else
+ removeStorageItem(model, message.areaName, key);
+ });
+ storage.onModelUpdated();
+ }
+ break;
+ case 'unlimitedStorage':
+ if (message.unlimitedStorage != undefined) {
+ setStorageUnlimited(model, message.unlimitedStorage);
+ storage.onModelUpdated();
+ }
+ break;
+ case 'reportError':
+ if (message.lastError)
+ storage.onError(message.lastError);
+ break;
+ default:
+ console.warn('Unknown devtools.* message: ', message);
+ break;
+ }
+ }
+
+ this.updateInspectedWindowStorage = function(areaName, updateType, extra) {
+ if (!extra)
+ extra = {};
+ if (contexts[current])
+ extra.contextId = contexts[current].id;
+ extra.areaName = areaName;
+ extra.name = updateType;
+ port.postMessage(extra);
+ }
+
+ return this;
+}
+
+angular.module('StorageData', []).directive('ngBlur', function() {
+ return function($scope, element, attributes) {
+ element.bind('blur', function() {
+ $scope.$apply(attributes.ngBlur);
+ });
+ };
+}).
+directive('ngFocusIf', function() {
+ return function($scope, element, attributes) {
+ $scope.$watch(attributes.ngFocusIf, function(newValue, oldValue) {
+ if (newValue)
+ {
+ element[0].style.display = '';
+ element[0].focus();
+ }
+ });
+ };
+}).
+directive('ngI18nContent', function() {
+ return function($scope, element, attributes) {
+ element[0].appendChild(document.createTextNode(chrome.i18n.getMessage(
+ attributes.ngI18nContent)));
+ };
+}).
+directive('ngI18nReorder', function() {
+ return function($scope, element, attributes) {
+ var e = element[0];
+ e.style.display = 'flex';
+ if (e.style.display != 'flex')
+ e.style.display = '-webkit-flex';
+ var order = chrome.i18n.getMessage(attributes.ngI18nReorder).split(/\s+/);
+ order.forEach(function(cur, index) {
+ if (e.children[index])
+ e.children[index].style.webkitOrder =
+ e.children[index].style.webkitOrder = cur;
+ });
+ };
+}).
+// data-bind-chrome-storage="{area: 'local|sync', key: 'keyName',
+// model: ..., [defaultValue: ...]}"
+directive('bindChromeStorage', function() {
+ return function($scope, element, attributes) {
+ var details = $scope.$eval(attributes.bindChromeStorage);
+
+ // Update storage when model changes.
+ $scope.$watch(details.model, function(newValue, oldValue) {
+ var items = {};
+ items[details.key] = $scope.$eval(details.model);
+ chrome.storage[details.area].set(items);
+ });
+
+ // Initialize model from storage or default value.
+ chrome.storage[details.area].get(details.key, function(items) {
+ function apply(value) {
+ $scope.$apply(details.model + ' = ' + JSON.stringify(value));
+ }
+ if (items[details.key] != undefined)
+ apply(items[details.key]);
+ else if (details.defaultValue != undefined)
+ apply(details.defaultValue);
+ });
+ };
+}).
+factory('storage', function() {
+ var storage = new StorageDevTools();
+ return storage;
+});
+
+function StorageDataCtrl($scope, storage) {
+ var kLastUsedArea = '____/lastUsedArea';
+ $scope.available = true;
+ $scope.filterOrHighlight = 'filter';
+ $scope.filterField = 'keyValue';
+ chrome.storage.sync.get(kLastUsedArea, function(items) {
+ if (items[kLastUsedArea] && !$scope.currentArea)
+ $scope.currentArea = items[kLastUsedArea];
+ });
+
+ storage.onModelUpdated = (function() {
+ $scope.$apply(function($scope){});
+ });
+
+ storage.onInvalidated = (function() {
+ $scope.available = false;
+ $scope.$apply(function($scope){});
+ });
+
+ storage.onContextAdded = (function() {
+ $scope.available = true;
+ $scope.contexts = storage.getContexts();
+ // This assumes that $scope.contexts and contexts in storage are
+ // arranged in the same order.
+ $scope.currentContext = $scope.contexts[
+ storage.getCurrentContextIndex()] || {};
+ $scope.storage = storage.currentModel();
+ $scope.switchArea($scope.currentArea || 'local');
+ });
+
+ storage.onError = (function(details) {
+ alert(details.message);
+ });
+
+ $scope.firstEditPrompt = (function() {
+ var key = '____/firstEdit';
+ chrome.storage.sync.get(key, function(items) {
+ if (!items[key] && !$scope.readonly) {
+ alert(chrome.i18n.getMessage('panels_storage_firstTimeEditWarning'));
+ items[key] = true;
+ chrome.storage.sync.set(items);
+ }
+ });
+ });
+
+ $scope.switchContext = (function(context) {
+ $scope.storage = storage.switchContext(context.id);
+ $scope.switchArea($scope.currentArea);
+ });
+
+ $scope.switchArea = (function(area) {
+ var current = {};
+ current[area] = $scope.storage[area];
+ $scope.current = current;
+ $scope.currentArea = area;
+ var items = {};
+ items[kLastUsedArea] = area;
+ chrome.storage.sync.set(items);
+ });
+
+ $scope.reinit = (function() {
+ storage.reload();
+ });
+
+ $scope.unescapeKey = unescapeKey;
+
+ $scope.beforeEditValue = (function(details) {
+ if ($scope.readonly)
+ return;
+ details.valueString = $scope.dumpValue(details.value);
+ details.editingValue = true;
+ });
+
+ $scope.onEditValue = (function(details) {
+ var valid = true;
+ try {
+ JSON.parse(details.valueString);
+ } catch (e) {
+ valid = false;
+ }
+ details.invalid = !valid;
+ });
+
+ $scope.editValue = (function(areaName, key, details) {
+ if (details.invalid)
+ return;
+ // TODO Use JSON or object?
+ storage.updateInspectedWindowStorage(areaName, 'updateValue',
+ {key: unescapeKey(key), value: details.valueString});
+ details.editingValue = false;
+ });
+
+ $scope.beforeEditKey = (function(key, details) {
+ if ($scope.readonly)
+ return;
+ details.keyString = unescapeKey(key);
+ details.editingKey = true;
+ });
+
+ $scope.editKey = (function(areaName, key, details) {
+ if (unescapeKey(key) != details.keyString)
+ storage.updateInspectedWindowStorage(areaName, 'updateKey',
+ {key: unescapeKey(key), newKey: details.keyString});
+ details.editingKey = false;
+ });
+
+ $scope.remove = (function(areaName, key) {
+ if ($scope.readonly)
+ return;
+ storage.updateInspectedWindowStorage(areaName, 'removeKey',
+ {key: unescapeKey(key)});
+ });
+
+ $scope.clear = (function(areaName) {
+ if ($scope.readonly)
+ return;
+ if (window.confirm(chrome.i18n.getMessage('panels_storage_confirmClear')))
+ storage.updateInspectedWindowStorage(areaName, 'clear');
+ });
+
+ $scope.onEditNewItem = (function(properties) {
+ var valid = true;
+ try {
+ JSON.parse(properties.newItem.value);
+ } catch (e) {
+ valid = false;
+ }
+ properties.newItem.keyInvalid =
+ (!properties.newItem.key || properties.items[properties.newItem.key]) ?
+ true : false;
+ properties.newItem.valueInvalid = !valid;
+ if (valid)
+ properties.newItem.size = properties.newItem.key.length +
+ properties.newItem.value.length;
+ });
+
+ $scope.addNewItem = (function(areaName, properties) {
+ if (properties.newItem.keyInvalid ||
+ properties.newItem.valueInvalid ||
+ !properties.newItem.value ||
+ !properties.newItem.key)
+ return;
+ storage.updateInspectedWindowStorage(areaName, 'updateValue',
+ {key: properties.newItem.key, value: properties.newItem.value});
+ properties.newItem = {};
+ });
+
+ $scope.dumpValue = (function(value) {
+ return JSON.stringify(value, null, ' ');
+ });
+
+ function matchQuery(str) {
+ return str.indexOf($scope.query) != -1;
+ }
+
+ function filterItem(key, details) {
+ var match = false;
+
+ switch($scope.filterField) {
+ case 'key':
+ match = matchQuery(key);
+ break;
+ case 'value':
+ match = matchQuery(JSON.stringify(details.value));
+ break;
+ case 'keyValue':
+ match = matchQuery(key) || matchQuery(JSON.stringify(details.value));
+ break;
+ }
+
+ if ($scope.filterOrHighlight == 'filter') {
+ details.hidden = !match;
+ delete details.highlight;
+ } else {
+ details.highlight = match;
+ delete details.hidden;
+ }
+ }
+
+ $scope.filterItems = (function() {
+ if ($scope.query) {
+ var items = $scope.current[$scope.currentArea].items;
+ for (key in items)
+ filterItem(key, items[key]);
+ }
+ });
+
+ $scope.percentage = (function(value) {
+ return Math.round(value * 100).toString() + ' %';
+ });
+}
+
Property changes on: chrome/common/extensions/docs/examples/extensions/storage_api_devtools/storage/panel.js
___________________________________________________________________
Added: svn:eol-style
+ LF
Added: svn:mime-type
+ text/javascript

Powered by Google App Engine
This is Rietveld 408576698