blob: 63382fb718afd37baca427e003112dcfc34f946e [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371/*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @unrestricted
33 */
34Resources.IDBDatabaseView = class extends UI.VBox {
35 /**
36 * @param {!Resources.IndexedDBModel} model
37 * @param {?Resources.IndexedDBModel.Database} database
38 */
39 constructor(model, database) {
40 super();
41
42 this._model = model;
43 const databaseName = database ? database.databaseId.name : Common.UIString('Loading\u2026');
44
45 this._reportView = new UI.ReportView(databaseName);
46 this._reportView.show(this.contentElement);
47
48 const bodySection = this._reportView.appendSection('');
49 this._securityOriginElement = bodySection.appendField(Common.UIString('Security origin'));
50 this._versionElement = bodySection.appendField(Common.UIString('Version'));
51
52 const footer = this._reportView.appendSection('').appendRow();
53 this._clearButton = UI.createTextButton(
54 Common.UIString('Delete database'), () => this._deleteDatabase(), Common.UIString('Delete database'));
55 footer.appendChild(this._clearButton);
56
57 this._refreshButton = UI.createTextButton(
58 Common.UIString('Refresh database'), () => this._refreshDatabaseButtonClicked(),
59 Common.UIString('Refresh database'));
60 footer.appendChild(this._refreshButton);
61
62 if (database)
63 this.update(database);
64 }
65
66 _refreshDatabase() {
67 this._securityOriginElement.textContent = this._database.databaseId.securityOrigin;
68 this._versionElement.textContent = this._database.version;
69 }
70
71 _refreshDatabaseButtonClicked() {
72 this._model.refreshDatabase(this._database.databaseId);
73 }
74
75 /**
76 * @param {!Resources.IndexedDBModel.Database} database
77 */
78 update(database) {
79 this._database = database;
80 this._reportView.setTitle(this._database.databaseId.name);
81 this._refreshDatabase();
82 this._updatedForTests();
83 }
84
85 _updatedForTests() {
86 // Sniffed in tests.
87 }
88
89 async _deleteDatabase() {
90 const ok = await UI.ConfirmDialog.show(
91 Common.UIString('Please confirm delete of "%s" database.', this._database.databaseId.name), this.element);
92 if (ok)
93 this._model.deleteDatabase(this._database.databaseId);
94 }
95};
96
97/**
98 * @unrestricted
99 */
100Resources.IDBDataView = class extends UI.SimpleView {
101 /**
102 * @param {!Resources.IndexedDBModel} model
103 * @param {!Resources.IndexedDBModel.DatabaseId} databaseId
104 * @param {!Resources.IndexedDBModel.ObjectStore} objectStore
105 * @param {?Resources.IndexedDBModel.Index} index
106 * @param {function()} refreshObjectStoreCallback
107 */
108 constructor(model, databaseId, objectStore, index, refreshObjectStoreCallback) {
109 super(Common.UIString('IDB'));
110 this.registerRequiredCSS('resources/indexedDBViews.css');
111
112 this._model = model;
113 this._databaseId = databaseId;
114 this._isIndex = !!index;
115 this._refreshObjectStoreCallback = refreshObjectStoreCallback;
116
117 this.element.classList.add('indexed-db-data-view', 'storage-view');
118
119 this._refreshButton = new UI.ToolbarButton(Common.UIString('Refresh'), 'largeicon-refresh');
120 this._refreshButton.addEventListener(UI.ToolbarButton.Events.Click, this._refreshButtonClicked, this);
121
122 this._deleteSelectedButton = new UI.ToolbarButton(Common.UIString('Delete selected'), 'largeicon-delete');
123 this._deleteSelectedButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._deleteButtonClicked(null));
124
125 this._clearButton = new UI.ToolbarButton(Common.UIString('Clear object store'), 'largeicon-clear');
126 this._clearButton.addEventListener(UI.ToolbarButton.Events.Click, this._clearButtonClicked, this);
127
128 this._needsRefresh = new UI.ToolbarItem(UI.createLabel(Common.UIString('Data may be stale'), 'smallicon-warning'));
129 this._needsRefresh.setVisible(false);
130 this._needsRefresh.setTitle(Common.UIString('Some entries may have been modified'));
131
132 this._createEditorToolbar();
133
134 this._pageSize = 50;
135 this._skipCount = 0;
136
137 this.update(objectStore, index);
138 this._entries = [];
139 }
140
141 /**
142 * @return {!DataGrid.DataGrid}
143 */
144 _createDataGrid() {
145 const keyPath = this._isIndex ? this._index.keyPath : this._objectStore.keyPath;
146
147 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([]);
148 columns.push({id: 'number', title: Common.UIString('#'), sortable: false, width: '50px'});
149 columns.push(
150 {id: 'key', titleDOMFragment: this._keyColumnHeaderFragment(Common.UIString('Key'), keyPath), sortable: false});
151 if (this._isIndex) {
152 columns.push({
153 id: 'primaryKey',
154 titleDOMFragment: this._keyColumnHeaderFragment(Common.UIString('Primary key'), this._objectStore.keyPath),
155 sortable: false
156 });
157 }
158 columns.push({id: 'value', title: Common.UIString('Value'), sortable: false});
159
160 const dataGrid = new DataGrid.DataGrid(
161 columns, undefined, this._deleteButtonClicked.bind(this), this._updateData.bind(this, true));
162 dataGrid.setStriped(true);
163 dataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, event => this._updateToolbarEnablement(), this);
164 return dataGrid;
165 }
166
167 /**
168 * @param {string} prefix
169 * @param {*} keyPath
170 * @return {!DocumentFragment}
171 */
172 _keyColumnHeaderFragment(prefix, keyPath) {
173 const keyColumnHeaderFragment = createDocumentFragment();
174 keyColumnHeaderFragment.createTextChild(prefix);
175 if (keyPath === null)
176 return keyColumnHeaderFragment;
177
178 keyColumnHeaderFragment.createTextChild(' (' + Common.UIString('Key path: '));
179 if (Array.isArray(keyPath)) {
180 keyColumnHeaderFragment.createTextChild('[');
181 for (let i = 0; i < keyPath.length; ++i) {
182 if (i !== 0)
183 keyColumnHeaderFragment.createTextChild(', ');
184 keyColumnHeaderFragment.appendChild(this._keyPathStringFragment(keyPath[i]));
185 }
186 keyColumnHeaderFragment.createTextChild(']');
187 } else {
188 const keyPathString = /** @type {string} */ (keyPath);
189 keyColumnHeaderFragment.appendChild(this._keyPathStringFragment(keyPathString));
190 }
191 keyColumnHeaderFragment.createTextChild(')');
192 return keyColumnHeaderFragment;
193 }
194
195 /**
196 * @param {string} keyPathString
197 * @return {!DocumentFragment}
198 */
199 _keyPathStringFragment(keyPathString) {
200 const keyPathStringFragment = createDocumentFragment();
201 keyPathStringFragment.createTextChild('"');
202 const keyPathSpan = keyPathStringFragment.createChild('span', 'source-code indexed-db-key-path');
203 keyPathSpan.textContent = keyPathString;
204 keyPathStringFragment.createTextChild('"');
205 return keyPathStringFragment;
206 }
207
208 _createEditorToolbar() {
209 const editorToolbar = new UI.Toolbar('data-view-toolbar', this.element);
210
211 editorToolbar.appendToolbarItem(this._refreshButton);
Blink Reformat4c46d092018-04-07 15:32:37212
213 editorToolbar.appendToolbarItem(new UI.ToolbarSeparator());
214
215 this._pageBackButton = new UI.ToolbarButton(Common.UIString('Show previous page'), 'largeicon-play-back');
216 this._pageBackButton.addEventListener(UI.ToolbarButton.Events.Click, this._pageBackButtonClicked, this);
217 editorToolbar.appendToolbarItem(this._pageBackButton);
218
219 this._pageForwardButton = new UI.ToolbarButton(Common.UIString('Show next page'), 'largeicon-play');
220 this._pageForwardButton.setEnabled(false);
221 this._pageForwardButton.addEventListener(UI.ToolbarButton.Events.Click, this._pageForwardButtonClicked, this);
222 editorToolbar.appendToolbarItem(this._pageForwardButton);
223
Erik Luoe6a74642018-10-26 22:03:54224 this._keyInput = new UI.ToolbarInput(ls`Start from key`, 0.5);
225 this._keyInput.addEventListener(UI.ToolbarInput.Event.TextChanged, this._updateData.bind(this, false));
226 editorToolbar.appendToolbarItem(this._keyInput);
Harley Lif0f328e2018-12-15 01:49:47227 editorToolbar.appendToolbarItem(new UI.ToolbarSeparator());
228 editorToolbar.appendToolbarItem(this._clearButton);
229 editorToolbar.appendToolbarItem(this._deleteSelectedButton);
Blink Reformat4c46d092018-04-07 15:32:37230
231 editorToolbar.appendToolbarItem(this._needsRefresh);
232 }
233
234 /**
235 * @param {!Common.Event} event
236 */
237 _pageBackButtonClicked(event) {
238 this._skipCount = Math.max(0, this._skipCount - this._pageSize);
239 this._updateData(false);
240 }
241
242 /**
243 * @param {!Common.Event} event
244 */
245 _pageForwardButtonClicked(event) {
246 this._skipCount = this._skipCount + this._pageSize;
247 this._updateData(false);
248 }
249
Blink Reformat4c46d092018-04-07 15:32:37250 refreshData() {
251 this._updateData(true);
252 }
253
254 /**
255 * @param {!Resources.IndexedDBModel.ObjectStore} objectStore
256 * @param {?Resources.IndexedDBModel.Index} index
257 */
258 update(objectStore, index) {
259 this._objectStore = objectStore;
260 this._index = index;
261
262 if (this._dataGrid)
263 this._dataGrid.asWidget().detach();
264 this._dataGrid = this._createDataGrid();
265 this._dataGrid.asWidget().show(this.element);
266
267 this._skipCount = 0;
268 this._updateData(true);
269 }
270
271 /**
272 * @param {string} keyString
273 */
274 _parseKey(keyString) {
275 let result;
276 try {
277 result = JSON.parse(keyString);
278 } catch (e) {
279 result = keyString;
280 }
281 return result;
282 }
283
284 /**
285 * @param {boolean} force
286 */
287 _updateData(force) {
Erik Luoe6a74642018-10-26 22:03:54288 const key = this._parseKey(this._keyInput.value());
Blink Reformat4c46d092018-04-07 15:32:37289 const pageSize = this._pageSize;
290 let skipCount = this._skipCount;
291 let selected = this._dataGrid.selectedNode ? this._dataGrid.selectedNode.data['number'] : 0;
292 selected = Math.max(selected, this._skipCount); // Page forward should select top entry
293 this._refreshButton.setEnabled(false);
294 this._clearButton.setEnabled(!this._isIndex);
295
296 if (!force && this._lastKey === key && this._lastPageSize === pageSize && this._lastSkipCount === skipCount)
297 return;
298
299 if (this._lastKey !== key || this._lastPageSize !== pageSize) {
300 skipCount = 0;
301 this._skipCount = 0;
302 }
303 this._lastKey = key;
304 this._lastPageSize = pageSize;
305 this._lastSkipCount = skipCount;
306
307 /**
308 * @param {!Array.<!Resources.IndexedDBModel.Entry>} entries
309 * @param {boolean} hasMore
310 * @this {Resources.IDBDataView}
311 */
312 function callback(entries, hasMore) {
313 this._refreshButton.setEnabled(true);
314 this.clear();
315 this._entries = entries;
316 let selectedNode = null;
317 for (let i = 0; i < entries.length; ++i) {
318 const data = {};
319 data['number'] = i + skipCount;
320 data['key'] = entries[i].key;
321 data['primaryKey'] = entries[i].primaryKey;
322 data['value'] = entries[i].value;
323
324 const node = new Resources.IDBDataGridNode(data);
325 this._dataGrid.rootNode().appendChild(node);
326 if (data['number'] <= selected)
327 selectedNode = node;
328 }
329
330 if (selectedNode)
331 selectedNode.select();
332 this._pageBackButton.setEnabled(!!skipCount);
333 this._pageForwardButton.setEnabled(hasMore);
334 this._needsRefresh.setVisible(false);
335 this._updateToolbarEnablement();
336 this._updatedDataForTests();
337 }
338
339 const idbKeyRange = key ? window.IDBKeyRange.lowerBound(key) : null;
340 if (this._isIndex) {
341 this._model.loadIndexData(
342 this._databaseId, this._objectStore.name, this._index.name, idbKeyRange, skipCount, pageSize,
343 callback.bind(this));
344 } else {
345 this._model.loadObjectStoreData(
346 this._databaseId, this._objectStore.name, idbKeyRange, skipCount, pageSize, callback.bind(this));
347 }
348 }
349
350 _updatedDataForTests() {
351 // Sniffed in tests.
352 }
353
354 /**
355 * @param {?Common.Event} event
356 */
357 _refreshButtonClicked(event) {
358 this._updateData(true);
359 }
360
361 /**
362 * @param {!Common.Event} event
363 */
364 async _clearButtonClicked(event) {
365 this._clearButton.setEnabled(false);
366 await this._model.clearObjectStore(this._databaseId, this._objectStore.name);
367 this._clearButton.setEnabled(true);
368 this._updateData(true);
369 }
370
371 markNeedsRefresh() {
372 this._needsRefresh.setVisible(true);
373 }
374
375 /**
376 * @param {?DataGrid.DataGridNode} node
377 */
378 async _deleteButtonClicked(node) {
379 if (!node) {
380 node = this._dataGrid.selectedNode;
381 if (!node)
382 return;
383 }
384 const key = /** @type {!SDK.RemoteObject} */ (this._isIndex ? node.data.primaryKey : node.data.key);
385 const keyValue = /** @type {!Array<?>|!Date|number|string} */ (key.value);
386 await this._model.deleteEntries(this._databaseId, this._objectStore.name, window.IDBKeyRange.only(keyValue));
387 this._refreshObjectStoreCallback();
388 }
389
390 clear() {
391 this._dataGrid.rootNode().removeChildren();
392 this._entries = [];
393 }
394
395 _updateToolbarEnablement() {
396 const empty = !this._dataGrid || this._dataGrid.rootNode().children.length === 0;
397 this._clearButton.setEnabled(!empty);
398 this._deleteSelectedButton.setEnabled(!empty && this._dataGrid.selectedNode !== null);
399 }
400};
401
402/**
403 * @unrestricted
404 */
405Resources.IDBDataGridNode = class extends DataGrid.DataGridNode {
406 /**
407 * @param {!Object.<string, *>} data
408 */
409 constructor(data) {
410 super(data, false);
411 this.selectable = true;
412 }
413
414 /**
415 * @override
416 * @return {!Element}
417 */
418 createCell(columnIdentifier) {
419 const cell = super.createCell(columnIdentifier);
420 const value = /** @type {!SDK.RemoteObject} */ (this.data[columnIdentifier]);
421
422 switch (columnIdentifier) {
423 case 'value':
424 case 'key':
425 case 'primaryKey':
426 cell.removeChildren();
427 const objectElement = ObjectUI.ObjectPropertiesSection.defaultObjectPresentation(value, undefined, true);
428 cell.appendChild(objectElement);
429 break;
430 default:
431 }
432
433 return cell;
434 }
435};