chrome-devtools-frontend
Version:
Chrome DevTools UI
324 lines (285 loc) • 13.3 kB
text/typescript
// Copyright 2022 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.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Protocol from '../../generated/protocol.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js';
import * as ApplicationComponents from './components/components.js';
import {SharedStorageForOrigin} from './SharedStorageModel.js';
import {StorageItemsView} from './StorageItemsView.js';
const UIStrings = {
/**
*@description Text in SharedStorage Items View of the Application panel
*/
sharedStorage: 'Shared Storage',
/**
*@description Text representing the name of a value stored in the "Shared Storage Items" table
*/
key: 'Key',
/**
*@description Text for the value of something
*/
value: 'Value',
/**
*@description Name for the "Shared Storage Items" table that shows the content of the Shared Storage.
*/
sharedStorageItems: 'Shared Storage Items',
/**
*@description Text for announcing that the "Shared Storage Items" table was cleared, that is, all
* entries were deleted.
*/
sharedStorageItemsCleared: 'Shared Storage items cleared',
/**
*@description Text for announcing that the filtered "Shared Storage Items" table was cleared, that is,
* all filtered entries were deleted.
*/
sharedStorageFilteredItemsCleared: 'Shared Storage filtered items cleared',
/**
*@description Text in SharedStorage Items View of the Application panel
*/
selectAValueToPreview: 'Select a value to preview',
/**
*@description Text for announcing a Shared Storage key/value item has been deleted
*/
sharedStorageItemDeleted: 'The storage item was deleted.',
/**
*@description Text for announcing a Shared Storage key/value item has been edited
*/
sharedStorageItemEdited: 'The storage item was edited.',
/**
*@description Text for announcing a Shared Storage key/value item edit request has been canceled
*/
sharedStorageItemEditCanceled: 'The storage item edit was canceled.',
/**
*@description Text for announcing number of entries after filtering
*@example {5} PH1
*/
sharedStorageNumberEntries: 'Number of entries shown in table: {PH1}',
};
const str_ = i18n.i18n.registerUIStrings('panels/application/SharedStorageItemsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
interface WrappedEntry {
key: string;
value: string;
}
export namespace SharedStorageItemsDispatcher {
// TODO(crbug.com/1167717): Make this a const enum.
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
FilteredItemsCleared = 'FilteredItemsCleared',
ItemDeleted = 'ItemDeleted',
ItemEdited = 'ItemEdited',
ItemsCleared = 'ItemsCleared',
ItemsRefreshed = 'ItemsRefreshed',
}
export interface ItemDeletedEvent {
key: string;
}
export interface ItemEditedEvent {
columnIdentifier: string;
oldText: string;
newText: string;
}
export type EventTypes = {
[Events.FilteredItemsCleared]: void,
[Events.ItemDeleted]: ItemDeletedEvent,
[Events.ItemEdited]: ItemEditedEvent,
[Events.ItemsCleared]: void,
[Events.ItemsRefreshed]: void,
};
}
export class SharedStorageItemsView extends StorageItemsView {
#sharedStorage: SharedStorageForOrigin;
readonly outerSplitWidget: UI.SplitWidget.SplitWidget;
readonly innerSplitWidget: UI.SplitWidget.SplitWidget;
#metadataView: LegacyWrapper.LegacyWrapper
.LegacyWrapper<UI.Widget.VBox, ApplicationComponents.SharedStorageMetadataView.SharedStorageMetadataView>;
readonly dataGrid: DataGrid.DataGrid.DataGridImpl<Protocol.Storage.SharedStorageEntry>;
#noDisplayView: UI.Widget.VBox;
#eventListeners: Common.EventTarget.EventDescriptor[];
readonly sharedStorageItemsDispatcher: Common.ObjectWrapper.ObjectWrapper<SharedStorageItemsDispatcher.EventTypes>;
constructor(sharedStorage: SharedStorageForOrigin) {
super(i18nString(UIStrings.sharedStorage), 'sharedStoragePanel');
this.#sharedStorage = sharedStorage;
this.element.classList.add('storage-view', 'table');
const columns = ([
{id: 'key', title: i18nString(UIStrings.key), sortable: false, editable: true, longText: true, weight: 50},
{id: 'value', title: i18nString(UIStrings.value), sortable: false, editable: true, longText: true, weight: 50},
] as DataGrid.DataGrid.ColumnDescriptor[]);
this.dataGrid = new DataGrid.DataGrid.DataGridImpl({
displayName: i18nString(UIStrings.sharedStorageItems),
columns,
editCallback: this.#editingCallback.bind(this),
deleteCallback: this.#deleteCallback.bind(this),
refreshCallback: this.refreshItems.bind(this),
});
this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, event => {
void this.#previewEntry(event.data);
});
this.dataGrid.addEventListener(DataGrid.DataGrid.Events.DeselectedNode, () => {
void this.#previewEntry(null);
});
this.dataGrid.setStriped(true);
this.dataGrid.setName('SharedStorageItemsView');
const dataGridWidget = this.dataGrid.asWidget();
dataGridWidget.setMinimumSize(0, 100);
this.#metadataView = LegacyWrapper.LegacyWrapper.legacyWrapper(
UI.Widget.VBox,
new ApplicationComponents.SharedStorageMetadataView.SharedStorageMetadataView(
sharedStorage, sharedStorage.securityOrigin));
this.#metadataView.setMinimumSize(0, 275);
const innerResizer = this.#metadataView.element.createChild('div', 'metadata-view-resizer');
this.innerSplitWidget = new UI.SplitWidget.SplitWidget(
/* isVertical: */ false, /* secondIsSidebar: */ false, 'sharedStorageInnerSplitViewState');
this.innerSplitWidget.setSidebarWidget(this.#metadataView);
this.innerSplitWidget.setMainWidget(dataGridWidget);
this.innerSplitWidget.installResizer(innerResizer);
this.#noDisplayView = new UI.Widget.VBox();
this.#noDisplayView.setMinimumSize(0, 25);
const outerResizer = this.#noDisplayView.element.createChild('div', 'preview-panel-resizer');
this.outerSplitWidget = new UI.SplitWidget.SplitWidget(
/* isVertical: */ false, /* secondIsSidebar: */ true, 'sharedStorageOuterSplitViewState');
this.outerSplitWidget.show(this.element);
this.outerSplitWidget.setMainWidget(this.innerSplitWidget);
this.outerSplitWidget.setSidebarWidget(this.#noDisplayView);
this.outerSplitWidget.installResizer(outerResizer);
this.#noDisplayView.contentElement.classList.add('placeholder');
const noDisplayDiv = this.#noDisplayView.contentElement.createChild('div');
noDisplayDiv.textContent = i18nString(UIStrings.selectAValueToPreview);
this.#eventListeners = [];
Common.EventTarget.removeEventListeners(this.#eventListeners);
this.#sharedStorage = sharedStorage;
this.#eventListeners = [
this.#sharedStorage.addEventListener(
SharedStorageForOrigin.Events.SharedStorageChanged, this.#sharedStorageChanged, this),
];
this.sharedStorageItemsDispatcher =
new Common.ObjectWrapper.ObjectWrapper<SharedStorageItemsDispatcher.EventTypes>();
}
// Use `createView()` instead of the constructor to create a view, so that entries can be awaited asynchronously.
static async createView(sharedStorage: SharedStorageForOrigin): Promise<SharedStorageItemsView> {
const view = new SharedStorageItemsView(sharedStorage);
await view.updateEntriesOnly();
return view;
}
async updateEntriesOnly(): Promise<void> {
if (!this.isShowing()) {
return;
}
const entries = await this.#sharedStorage.getEntries();
if (entries) {
this.#showSharedStorageItems(entries);
}
}
async #sharedStorageChanged(): Promise<void> {
await this.refreshItems();
}
override async refreshItems(): Promise<void> {
if (!this.isShowing()) {
return;
}
await this.#metadataView.getComponent().render();
await this.updateEntriesOnly();
this.sharedStorageItemsDispatcher.dispatchEventToListeners(SharedStorageItemsDispatcher.Events.ItemsRefreshed);
}
override async deleteSelectedItem(): Promise<void> {
if (!this.dataGrid.selectedNode) {
return;
}
await this.#deleteCallback(this.dataGrid.selectedNode);
}
override async deleteAllItems(): Promise<void> {
if (!this.hasFilter()) {
await this.#sharedStorage.clear();
await this.refreshItems();
this.sharedStorageItemsDispatcher.dispatchEventToListeners(SharedStorageItemsDispatcher.Events.ItemsCleared);
UI.ARIAUtils.alert(i18nString(UIStrings.sharedStorageItemsCleared));
return;
}
await Promise.all(this.dataGrid.rootNode()
.children.filter(node => node.data.key)
.map(node => this.#sharedStorage.deleteEntry(node.data.key)));
await this.refreshItems();
this.sharedStorageItemsDispatcher.dispatchEventToListeners(
SharedStorageItemsDispatcher.Events.FilteredItemsCleared);
UI.ARIAUtils.alert(i18nString(UIStrings.sharedStorageFilteredItemsCleared));
}
async #editingCallback(
editingNode: DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>, columnIdentifier: string,
oldText: string, newText: string): Promise<void> {
if (columnIdentifier === 'key' && newText === '') {
// The Shared Storage backend does not currently allow '' as a key, so we only set a new entry with a new key if its new key is nonempty.
await this.refreshItems();
UI.ARIAUtils.alert(i18nString(UIStrings.sharedStorageItemEditCanceled));
return;
}
if (columnIdentifier === 'key') {
await this.#sharedStorage.deleteEntry(oldText);
await this.#sharedStorage.setEntry(newText, editingNode.data.value || '', false);
} else {
// The Shared Storage backend does not currently allow '' as a key, so we use ' ' as the default key instead.
await this.#sharedStorage.setEntry(editingNode.data.key || ' ', newText, false);
}
await this.refreshItems();
this.sharedStorageItemsDispatcher.dispatchEventToListeners(
SharedStorageItemsDispatcher.Events.ItemEdited,
{columnIdentifier, oldText, newText} as SharedStorageItemsDispatcher.ItemEditedEvent);
UI.ARIAUtils.alert(i18nString(UIStrings.sharedStorageItemEdited));
}
#showSharedStorageItems(items: Protocol.Storage.SharedStorageEntry[]): void {
const rootNode = this.dataGrid.rootNode();
const [selectedKey] = rootNode.children.filter(node => node.selected).map(node => node.data.key);
rootNode.removeChildren();
let selectedNode: DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>|null = null;
const filteredItems = (item: Protocol.Storage.SharedStorageEntry): string => `${item.key} ${item.value}`;
const filteredList = this.filter(items, filteredItems);
for (const item of filteredList) {
const node = new DataGrid.DataGrid.DataGridNode({key: item.key, value: item.value}, false);
node.selectable = true;
rootNode.appendChild(node);
if (!selectedNode || item.key === selectedKey) {
selectedNode = node;
}
}
if (selectedNode) {
selectedNode.selected = true;
}
this.dataGrid.addCreationNode(false);
this.setCanDeleteSelected(Boolean(selectedNode));
UI.ARIAUtils.alert(i18nString(UIStrings.sharedStorageNumberEntries, {PH1: filteredList.length}));
}
async #deleteCallback(node: DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>): Promise<void> {
if (!node || node.isCreationNode || !this.#sharedStorage) {
return;
}
const key = node.data.key;
await this.#sharedStorage.deleteEntry(key);
await this.refreshItems();
this.sharedStorageItemsDispatcher.dispatchEventToListeners(
SharedStorageItemsDispatcher.Events.ItemDeleted, {key} as SharedStorageItemsDispatcher.ItemDeletedEvent);
UI.ARIAUtils.alert(i18nString(UIStrings.sharedStorageItemDeleted));
}
async #previewEntry(entry: DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>|null): Promise<void> {
const key = entry?.data?.key;
const value = entry?.data?.value;
const wrappedEntry = key && {key: key as string, value: value as string || ''} as WrappedEntry;
if (wrappedEntry) {
const preview = SourceFrame.JSONView.JSONView.createViewSync(wrappedEntry);
// Selection could've changed while the preview was loaded
if (entry.selected) {
this.outerSplitWidget.setSidebarWidget(preview);
}
} else {
this.outerSplitWidget.setSidebarWidget(this.#noDisplayView);
}
}
getEntriesForTesting(): Array<Protocol.Storage.SharedStorageEntry> {
return this.dataGrid.rootNode()
.children.filter(node => node.data.key)
.map(node => (node.data as Protocol.Storage.SharedStorageEntry));
}
}