blob: 6bf4bb057a140ff44f2ebdd649d5d99fec7fb47b [file] [log] [blame]
// Copyright 2020 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.
/**
* @fileoverview
* 'crostini-port-forwarding' is the settings port forwarding subpage for
* Crostini.
*/
import 'chrome://resources/cr_elements/icons.m.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './crostini_port_forwarding_add_port_dialog.js';
import '../../controls/settings_toggle_button.js';
import '../../settings_page/settings_section.js';
import '../../settings_page_styles.css.js';
import '../../settings_shared.css.js';
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {WebUIListenerBehavior, WebUIListenerBehaviorInterface} from 'chrome://resources/js/web_ui_listener_behavior.m.js';
import {html, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ContainerInfo, GuestId} from '../guest_os/guest_os_browser_proxy.js';
import {containerLabel, equalContainerId} from '../guest_os/guest_os_container_select.js';
import {recordSettingChange} from '../metrics_recorder.js';
import {PrefsBehavior, PrefsBehaviorInterface} from '../prefs_behavior.js';
import {CrostiniBrowserProxy, CrostiniBrowserProxyImpl, CrostiniPortActiveSetting, CrostiniPortProtocol, CrostiniPortSetting, DEFAULT_CONTAINER_ID, DEFAULT_CROSTINI_CONTAINER, DEFAULT_CROSTINI_VM} from './crostini_browser_proxy.js';
/**
* @constructor
* @extends {PolymerElement}
* @implements {PrefsBehaviorInterface}
* @implements {WebUIListenerBehaviorInterface}
*/
const CrostiniPortForwardingBase =
mixinBehaviors([PrefsBehavior, WebUIListenerBehavior], PolymerElement);
/** @polymer */
class CrostiniPortForwardingElement extends CrostiniPortForwardingBase {
static get is() {
return 'settings-crostini-port-forwarding';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/** Preferences state. */
prefs: {
type: Object,
notify: true,
},
/** @private */
showAddPortDialog_: {
type: Boolean,
value: false,
},
/**
* The forwarded ports for display in the UI.
* @private {!Array<!CrostiniPortSetting>}
*/
allPorts_: {
type: Array,
notify: true,
value() {
return [];
},
},
/**
* The known ContainerIds for display in the UI.
* @private {!Array<!ContainerInfo>}
*/
allContainers_: {
type: Array,
notify: true,
value() {
return [];
},
},
};
}
static get observers() {
return [
'onCrostiniPortsChanged_(prefs.crostini.port_forwarding.ports.value)',
];
}
constructor() {
super();
/**
* List of ports are currently being forwarded.
* @private {!Array<?CrostiniPortActiveSetting>}
*/
this.activePorts_ = [];
/**
* Tracks the last port that was selected for removal.
* @private {?CrostiniPortActiveSetting}
*/
this.lastMenuOpenedPort_ = null;
/** @private {!CrostiniBrowserProxy} */
this.browserProxy_ = CrostiniBrowserProxyImpl.getInstance();
}
/** @override */
connectedCallback() {
super.connectedCallback();
this.addWebUIListener(
'crostini-port-forwarder-active-ports-changed',
(ports) => this.onCrostiniPortsActiveStateChanged_(ports));
this.addWebUIListener(
'crostini-container-info', (infos) => this.onContainerInfo_(infos));
this.browserProxy_.getCrostiniActivePorts().then(
(ports) => this.onCrostiniPortsActiveStateChanged_(ports));
this.browserProxy_.requestContainerInfo();
}
/**
* @param {!CrostiniPortProtocol} protocol
* @private
*/
getProtocolName(protocol) {
return Object.keys(CrostiniPortProtocol)
.find(k => CrostiniPortProtocol[k] === protocol);
}
/**
* @param {!Array<!ContainerInfo>} containerInfos
*/
onContainerInfo_(containerInfos) {
this.set('allContainers_', containerInfos);
}
/**
* @param {!Array<!CrostiniPortSetting>} ports List of ports.
* @private
*/
onCrostiniPortsChanged_(ports) {
this.splice('allPorts_', 0, this.allPorts_.length);
for (const port of ports) {
port.is_active = this.activePorts_.some(
activePort => activePort.port_number === port.port_number &&
activePort.protocol_type === port.protocol_type);
port.container_id = port.container_id || {
vm_name: port['vm_name'] || DEFAULT_CROSTINI_VM,
container_name: port['container_name'] || DEFAULT_CROSTINI_CONTAINER,
};
this.push('allPorts_', port);
}
this.notifyPath('allContainers_');
}
/**
* @param {!Array<!CrostiniPortActiveSetting>} ports List of ports that are
* active.
* @private
*/
onCrostiniPortsActiveStateChanged_(ports) {
this.activePorts_ = ports;
for (let i = 0; i < this.allPorts_.length; i++) {
this.set(
`allPorts_.${i}.${'is_active'}`,
this.activePorts_.some(
activePort =>
activePort.port_number === this.allPorts_[i].port_number &&
activePort.protocol_type ===
this.allPorts_[i].protocol_type));
}
}
/**
* @param {!Event} event
* @private
*/
onAddPortClick_(event) {
this.showAddPortDialog_ = true;
}
/**
* @param {!Event} event
* @private
*/
onAddPortDialogClose_(event) {
this.showAddPortDialog_ = false;
}
/**
* @param {!Event} event
* @private
*/
onShowRemoveAllPortsMenuClick_(event) {
const menu = /** @type {!CrActionMenuElement} */
(this.$.removeAllPortsMenu.get());
menu.showAt(/** @type {!HTMLElement} */ (event.target));
}
/**
* @param {!Event} event
* @private
*/
onRemoveSinglePortClick_(event) {
const dataSet = /** @type {{portNumber: string, protocolType: string}} */
(event.currentTarget.dataset);
const containerId = /** @type {GuestId} */
(event.currentTarget['dataContainerId']);
const protocolType = /** @type {!CrostiniPortProtocol} */
(Number(dataSet.protocolType));
this.browserProxy_
.removeCrostiniPortForward(
containerId, Number(dataSet.portNumber), protocolType)
.then(result => {
// TODO(crbug.com/848127): Error handling for result
recordSettingChange();
});
}
/**
* @param {!Event} event
* @private
*/
onRemoveAllPortsClick_(event) {
const menu = /** @type {!CrActionMenuElement} */
(this.$.removeAllPortsMenu.get());
assert(menu.open);
for (const container of this.allContainers_) {
this.browserProxy_.removeAllCrostiniPortForwards(container.id);
}
recordSettingChange();
menu.close();
}
/**
* @param {!Event} event
* @private
*/
onPortActivationChange_(event) {
const dataSet = /** @type {{portNumber: string, protocolType: string}} */
(event.currentTarget.dataset);
const id = /** @type {GuestId} */
(event.currentTarget['dataContainerId']);
const portNumber = Number(dataSet.portNumber);
const protocolType = /** @type {!CrostiniPortProtocol} */
(Number(dataSet.protocolType));
if (event.target.checked) {
event.target.checked = false;
this.browserProxy_
.activateCrostiniPortForward(id, portNumber, protocolType)
.then(result => {
if (!result) {
this.$.errorToast.show();
}
// TODO(crbug.com/848127): Elaborate on error handling for result
});
} else {
this.browserProxy_
.deactivateCrostiniPortForward(id, portNumber, protocolType)
.then(
result => {
// TODO(crbug.com/848127): Error handling for result
});
}
}
/**
* @param {!Array<!CrostiniPortSetting>} allPorts
* @param {!GuestId} id
* @return boolean
* @private
*/
showContainerId_(allPorts, id) {
return allPorts.some(port => equalContainerId(port.container_id, id)) &&
allPorts.some(
port => !equalContainerId(port.container_id, DEFAULT_CONTAINER_ID));
}
/**
* @param {!GuestId} id
* @return string
* @private
*/
containerLabel_(id) {
return this.showContainerId_(this.allPorts_, id) ?
containerLabel(id, DEFAULT_CROSTINI_VM) :
'';
}
/**
* @param {!Array<!CrostiniPortSetting>} allPorts
* @param {!GuestId} id
* @return boolean
* @private
*/
hasContainerPorts_(allPorts, id) {
return allPorts.some(port => equalContainerId(port.container_id, id));
}
/**
* @param {!GuestId} id
* @return function
* @private
*/
byContainerId_(id) {
return port => equalContainerId(port.container_id, id);
}
}
customElements.define(
CrostiniPortForwardingElement.is, CrostiniPortForwardingElement);