diff --git a/src/plugins/IOThrottling.js b/src/plugins/IOThrottling.js index d145a16d..2a82f3fd 100644 --- a/src/plugins/IOThrottling.js +++ b/src/plugins/IOThrottling.js @@ -1,8 +1,10 @@ 'use strict'; const OverlayPlugin = require('./util/OverlayPlugin'); +const {dropdownSelect, textInput, chipTag} = require('./util/components'); const PROFILES = require('./util/iothrottling-profiles'); +const PROFILE_CUSTOM_NAME = 'Custom'; const BYTES_PER_KILOBYTE = 1024; const BYTES_PER_MEGABYTE = BYTES_PER_KILOBYTE << 10; @@ -30,7 +32,25 @@ module.exports = class IOThrottling extends OverlayPlugin { // Register plugin this.instance.diskIO = this; - this.fields = {}; + this.lastReadByteRateReceived = 0; + + // Array of profiles for dropdown + this.profilesForDropdown = PROFILES.map((profile) => { + const divContainer = document.createElement('div'); + const label = document.createElement('div'); + const name = document.createElement('div'); + divContainer.appendChild(name); + divContainer.appendChild(label); + divContainer.style.display = 'flex'; + divContainer.style.justifyContent = 'space-between'; + name.innerHTML = profile.name; + label.innerHTML = profile.label || ''; + return { + element: divContainer, + value: profile.readByteRate ?? profile.name, + valueToDisplay: profile.name || '', + }; + }); // Render components this.renderToolbarButton(); @@ -38,8 +58,10 @@ module.exports = class IOThrottling extends OverlayPlugin { // Listen for diskio messages: "readbyterate " (or "cachecleared") this.instance.registerEventCallback('diskio', (message) => { + // console.log(message); const values = message.split(' '); if (values[0] === 'readbyterate' && values.length === 2) { + this.lastReadByteRateReceived = values[1] / BYTES_PER_MEGABYTE; this.updateDiskIOValues(values[1] / BYTES_PER_MEGABYTE); } }); @@ -69,14 +91,16 @@ module.exports = class IOThrottling extends OverlayPlugin { */ renderWidget() { // Create elements - this.widget = document.createElement('div'); - this.form = document.createElement('form'); - // Generate title - const title = document.createElement('div'); - title.className = 'gm-title'; - title.innerHTML = this.i18n.IOTHROTTLING_TITLE || 'Disk I/O'; - this.form.appendChild(title); + const {modal, container} = this.createTemplateModal({ + title: this.i18n.IOTHROTTLING_TITLE || 'Disk I/O', + classes: 'gm-iothrottling-plugin', + width: 378, + height: 422, + }); + + this.widget = modal; + this.container = container; // Generate input rows const inputs = document.createElement('div'); @@ -85,107 +109,101 @@ module.exports = class IOThrottling extends OverlayPlugin { const IOThrottlingLabel = this.i18n.IOTHROTTLING_PROFILE || 'Profile'; inputs.innerHTML = ''; - // Create select - this.select = document.createElement('select'); - this.select.className = 'gm-iothrottling-select'; - const defaultOption = new Option(this.i18n.IOTHROTTLING_PROFILE_NONE || 'None'); - this.select.add(defaultOption); - this.select.onchange = this.changeProfile.bind(this); - inputs.appendChild(this.select); - - // Add option for each child - PROFILES.forEach((profile) => { - const option = new Option(profile.label, profile.name); - this.select.add(option); + this.dropdownProfile = dropdownSelect.createDropdown({ + items: this.profilesForDropdown, + value: this.profilesForDropdown.find((profile) => profile.value===0).value, + onChange: (newValue) => { + // PROFILES.find((profile) => profile.value===newValue)?.readByteRate, + this.updateDiskIOValues(newValue); + }, }); + inputs.appendChild(this.dropdownProfile.element); - this.readByteRateDiv = document.createElement('div'); - this.readByteRateDiv.classList.add('gm-fields'); - const readByteRateLabel = document.createElement('label'); - readByteRateLabel.innerHTML = this.i18n.IOTHROTTLING_READ_BYTERATE || 'Read speed limit:'; - this.readByteRate = document.createElement('input'); - this.readByteRate.className = 'gm-iothrottling-readbyterate'; - this.readByteRate.placeholder = this.i18n.IOTHROTTLING_READ_BYTERATE_EXAMPLE || 'eg: 100'; - this.readByteRate.title = this.i18n.READ_BYTE_RATE || 'Read speed limit'; - this.readByteRate.required = true; - this.readByteRate.pattern = '[0-9]*'; - this.readByteRateDiv.appendChild(readByteRateLabel); - const readByteRateSpeed = document.createElement('label'); - readByteRateSpeed.innerHTML = this.i18n.IOTHROTTLING_BYTERATE_UNIT || 'MiB per sec'; - readByteRateSpeed.classList.add('gm-units'); - this.readByteRateDiv.appendChild(readByteRateSpeed); - this.readByteRateDiv.appendChild(this.readByteRate); - - // Add submit button - this.submitBtn = document.createElement('button'); - this.submitBtn.className = 'gm-iothrottling-update'; - this.submitBtn.innerHTML = this.i18n.IOTHROTTLING_UPDATE || 'Update'; - this.submitBtn.onclick = this.sendDataToInstance.bind(this); + const readByteRateDiv = document.createElement('div'); + readByteRateDiv.classList.add('gm-fields'); + const readByteRateContainer = document.createElement('div'); + readByteRateContainer.classList.add('gm-fields-container'); + readByteRateDiv.appendChild(readByteRateContainer); + + const readByteRateText = document.createElement('div'); + readByteRateText.innerHTML = this.i18n.IOTHROTTLING_READ_BYTERATE || 'Read speed limit:'; + + this.readByteRate = textInput.createTextInput({ + value: '50', + regexFilter: /^[0-9]*$/, + }); + this.readByteRate.element.className = 'gm-iothrottling-readbyterate'; + + const readByteRateSpeedText = document.createElement('div'); + readByteRateSpeedText.innerHTML = this.i18n.IOTHROTTLING_BYTERATE_UNIT || 'MiB per sec'; + readByteRateSpeedText.classList.add('gm-units'); + const readByteRateSpeedNoneText = document.createElement('div'); + readByteRateSpeedNoneText.innerHTML = this.i18n.IOTHROTTLING_BYTERATE_NONE || 'No disk performance alteration'; + readByteRateSpeedNoneText.classList.add('gm-noThrottling'); + const readByteRateSpeedCustomText = document.createElement('div'); + readByteRateSpeedCustomText.innerHTML = + this.i18n.IOTHROTTLING_BYTERATE_CUSTOM || 'Enter the read speed limit you wish to emulate.'; + readByteRateSpeedCustomText.classList.add('gm-customThrottling'); + + readByteRateContainer.appendChild(readByteRateText); + readByteRateContainer.appendChild(this.readByteRate.element); + readByteRateContainer.appendChild(readByteRateSpeedText); + readByteRateContainer.appendChild(readByteRateSpeedNoneText); + readByteRateDiv.appendChild(readByteRateSpeedCustomText); + + // Separator + const separator = document.createElement('div'); + separator.className = 'gm-separator'; + // Add apply button + const applyBtnDiv = document.createElement('div'); + applyBtnDiv.className = 'gm-iothrottling-apply'; + const statusDiv = document.createElement('div'); + statusDiv.className = 'gm-iothrottling-status'; + const statusText = document.createElement('div'); + statusText.innerHTML = 'Status:'; + statusText.className = 'gm-iothrottling-status-text'; + const appliedTag = chipTag.createChip({ + text: 'Applied', + }); + const statusNotApplied = document.createElement('div'); + statusNotApplied.innerHTML = 'Not applied'; + statusNotApplied.className = 'gm-iothrottling-notapplied-text'; + + statusDiv.appendChild(statusText); + statusDiv.appendChild(statusNotApplied); + statusDiv.appendChild(appliedTag.element); + + const applyBtn = document.createElement('button'); + applyBtn.className = 'gm-btn'; + applyBtn.innerHTML = this.i18n.IOTHROTTLING_UPDATE || 'Apply'; + applyBtn.onclick = this.sendDataToInstance.bind(this); + + applyBtnDiv.appendChild(statusDiv); + applyBtnDiv.appendChild(applyBtn); // Add clear cache button + const clearCacheDiv = document.createElement('div'); + clearCacheDiv.className = 'gm-iothrottling-clearcache'; + const clearCacheLabel = this.i18n.CLEAR_CACHE_PROFILE || 'Disk cache'; + clearCacheDiv.innerHTML = ''; + this.clearCacheBtn = document.createElement('button'); - this.clearCacheBtn.className = 'gm-iothrottling-clearcache'; - this.clearCacheBtn.innerHTML = this.i18n.IOTHROTTLING_CLEAR_CACHE || 'Clear cache'; + this.clearCacheBtn.className = 'gm-btn'; + this.clearCacheBtn.innerHTML = this.i18n.IOTHROTTLING_CLEAR_CACHE || 'Clear'; this.clearCacheBtn.onclick = this.clearCache.bind(this); - const clearCacheDiv = document.createElement('div'); clearCacheDiv.appendChild(this.clearCacheBtn); // Setup - this.form.appendChild(inputs); - this.form.appendChild(this.readByteRateDiv); - this.form.appendChild(clearCacheDiv); - this.form.appendChild(this.submitBtn); - - this.setFieldsReadOnly(true); - this.resetFields('0'); - this.displayFields(false); - - this.widget.className = 'gm-overlay gm-iothrottling-plugin gm-hidden'; - - // Add close button - const close = document.createElement('div'); - close.className = 'gm-close-btn'; - close.onclick = this.toggleWidget.bind(this); - - this.widget.appendChild(close); - this.widget.appendChild(this.form); + this.container.appendChild(inputs); + this.container.appendChild(readByteRateDiv); + this.container.appendChild(applyBtnDiv); + this.container.appendChild(separator); + this.container.appendChild(clearCacheDiv); // Render into document this.instance.root.appendChild(this.widget); } - /** - * Set custom fields read-only or not. - * - * @param {boolean} readOnly Desired read-only status. - */ - setFieldsReadOnly(readOnly) { - this.readByteRate.readOnly = readOnly; - } - - /** - * Reset custom fields value. - * - * @param {number} value Desired value. - */ - resetFields(value) { - value = typeof value !== 'undefined' ? value : ''; - this.readByteRate.value = value; - } - - /** - * Toggle custom fields input visibility. - * - * @param {boolean} display Whether or not inputs should be visible. - */ - displayFields(display) { - if (display) { - this.readByteRateDiv.classList.remove('gm-hidden'); - } else { - this.readByteRateDiv.classList.add('gm-hidden'); - } - } - /** * Send information to instance. * @@ -194,14 +212,11 @@ module.exports = class IOThrottling extends OverlayPlugin { sendDataToInstance(event) { event.preventDefault(); - if (this.form.checkValidity()) { - const json = { - channel: 'diskio', - messages: ['set readbyterate ' + this.readByteRate.value * BYTES_PER_MEGABYTE, 'clearcache'], - }; - this.instance.sendEvent(json); - this.toggleWidget(); - } + const json = { + channel: 'diskio', + messages: ['set readbyterate ' + this.readByteRate.getValue() * BYTES_PER_MEGABYTE, 'clearcache'], + }; + this.instance.sendEvent(json); } /** @@ -210,75 +225,61 @@ module.exports = class IOThrottling extends OverlayPlugin { * @param {Event} event Event. */ clearCache(event) { + this.container.classList.remove('gm-iothrottling-cache-cleared'); + // Force a reflow to restart the animation + void this.container.offsetWidth; + this.container.classList.add('gm-iothrottling-cache-cleared'); event.preventDefault(); const json = {channel: 'diskio', messages: ['clearcache']}; this.instance.sendEvent(json); } - /** - * Handles profile change. - */ - changeProfile() { - const profile = PROFILES.find((elem) => elem.name === this.select.value); - if (profile && profile.name !== 'Custom') { - this.loadDetails(profile); - this.displayFields(true); - } else if (profile && profile.name === 'Custom') { - this.setFieldsReadOnly(false); - this.displayFields(true); - } else { - this.resetFields('0'); - this.displayFields(false); - } - } - /** * Handles disk I/O parameters changes. Keeps UI in sync with the instance state. * * @param {number} readSpeed Read byte rate. */ updateDiskIOValues(readSpeed) { - readSpeed = Number(readSpeed); + this.container.classList.remove('gm-iothrottling-saved'); + // Handle the chipTag display. If readSpeed is custom (trigger from dropdown) + const readSpeedIsCustom = readSpeed === PROFILE_CUSTOM_NAME; + if (readSpeedIsCustom) { + // if the lastReadByteRateReceived exists in the profiles the active profil isn't a custom one + const profile = this.profilesForDropdown.find((p) => p.value === this.lastReadByteRateReceived); + + if (!profile) { + this.container.classList.add('gm-iothrottling-saved'); + } + } else if (readSpeed === this.lastReadByteRateReceived) { + // if readSpeed isn't custom and is equal to the lastReadByteRateReceived then the active profil is dropdown profil + this.container.classList.add('gm-iothrottling-saved'); + } - if (Number.isNaN(readSpeed) || readSpeed <= 0) { - this.select.value = this.i18n.IOTHROTTLING_PROFILE_NONE || 'None'; - this.resetFields('0'); - this.displayFields(false); + // if readspeed is not a number or is less than 0 then set select "none" profile + if (!readSpeedIsCustom && (Number.isNaN(readSpeed) || readSpeed <= 0)) { + this.readByteRate.setValue(0); + this.dropdownProfile.setValue(this.profilesForDropdown.find((profile) => profile.value===0)); + // Display Read speed limit: No disk performance alteration + this.container.classList.add('gm-iothrottling-none'); return; } - this.readByteRate.value = readSpeed; - const profile = PROFILES.find((elem) => elem.readByteRate === readSpeed); - if (profile && profile.name !== 'Custom') { - this.select.value = profile.name; - this.loadDetails(profile); - this.displayFields(true); - } else { - this.select.value = 'Custom'; - this.setFieldsReadOnly(false); - this.displayFields(true); - } - } + this.container.classList.remove('gm-iothrottling-none'); + this.container.classList.remove('gm-iothrottling-custom'); + const profile = this.profilesForDropdown.find((prof) => prof.value === readSpeed); - /** - * Load fields with the given profile info. - * - * @param {Object} profile Selected profile. - */ - loadDetails(profile) { - if (profile.name !== 'Custom') { - this.setFieldsReadOnly(true); - this.readByteRate.value = profile.readByteRate; + if (!readSpeedIsCustom && profile) { + this.readByteRate.setReadOnly(true); + this.dropdownProfile.setValue(profile); + this.readByteRate.setValue(readSpeed); + } else { + // custom + this.container.classList.add('gm-iothrottling-custom'); + this.readByteRate.setReadOnly(false); + const custom = this.profilesForDropdown.find((prof) => prof.value === PROFILE_CUSTOM_NAME); + this.dropdownProfile.setValue(custom); + this.readByteRate.setValue(this.lastReadByteRateReceived); } } - - /** - * Activate disk I/O throttling. Keeps UI in sync with the instance state. - * - * @param {number} readSpeed Read byte rate. - */ - setActive(readSpeed) { - this.updateDiskIOValues(Number(readSpeed) / BYTES_PER_KILOBYTE); - } }; diff --git a/src/plugins/util/iothrottling-profiles.js b/src/plugins/util/iothrottling-profiles.js index 809b00f7..83b40ca2 100644 --- a/src/plugins/util/iothrottling-profiles.js +++ b/src/plugins/util/iothrottling-profiles.js @@ -10,23 +10,27 @@ * */ module.exports = [ + { + name: 'None', + label: '(No disk performance alteration)', + readByteRate: 0, + }, { name: 'High-end device', - label: 'High-end device', + label: '(200 MiB per second)', readByteRate: 200, }, { name: 'Mid-range device', - label: 'Mid-range device', + label: '(100 MiB per second)', readByteRate: 100, }, { name: 'Low-end device', - label: 'Low-end device', + label: '(50 MiB per second)', readByteRate: 50, }, { name: 'Custom', - label: 'Custom device', }, ]; diff --git a/src/scss/base/_animations.scss b/src/scss/base/_animations.scss index 70bc1374..589bdfda 100644 --- a/src/scss/base/_animations.scss +++ b/src/scss/base/_animations.scss @@ -9,3 +9,27 @@ opacity: 1; } } + +@keyframes blink-alert { + 0% { + opacity: 1; + } + 10% { + opacity: 0.3; + } + 20% { + opacity: 1; + } + 30% { + opacity: 0.3; + } + 40% { + opacity: 1; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 1; + } +} \ No newline at end of file diff --git a/src/scss/base/_genymotion.scss b/src/scss/base/_genymotion.scss index 4a7273b1..1d92a94a 100644 --- a/src/scss/base/_genymotion.scss +++ b/src/scss/base/_genymotion.scss @@ -209,3 +209,28 @@ filter: inherit; } } + +.gm-separator { + width: 100%; + height: 1px; + background-color: color-mix(in srgb, var(--gm-modal-bg-color), grey 10%); + margin: 10px 0; +} + +.gm-btn{ + border: none; + background: none; + text-transform: uppercase; + color: var(--gm-text-color); + font-weight: bold; + cursor: pointer; + border: none; + padding: 10px 15px; + border-radius: 4px; + + &:hover{ + background: var(--gm-primary-color); + background: color-mix(in srgb, var(--gm-primary-color), transparent 85%); + color: var(--gm-primary-color) + } +} \ No newline at end of file diff --git a/src/scss/components/_iothrottling.scss b/src/scss/components/_iothrottling.scss index e52236ce..8d2d47cf 100644 --- a/src/scss/components/_iothrottling.scss +++ b/src/scss/components/_iothrottling.scss @@ -2,57 +2,94 @@ * IO Throttling plugin styles */ .device-renderer-instance .gm-iothrottling-plugin { - .gm-inputs { - margin-bottom: 15px; - select { - font-size: medium; - width: 100%; - margin-top: 10px; - padding: 2px; + .gm-noThrottling, + .gm-customThrottling { + display: none; + } + .gm-iothrottling-none { + .gm-units, + .gm-iothrottling-readbyterate { + display: none; + } + .gm-noThrottling { + display: block; + } + } + .gm-iothrottling-custom { + .gm-customThrottling { + display: block; + //text italic + font-style: italic; + margin-top: 15px; } } - label { - margin-top: 20px; - display: block; - float: left; + .gm-iothrottling-cache-cleared{ + .gm-iothrottling-clearcache{ + label { + animation: blink-alert 1.5s ease-in-out + } + } } - .gm-fields { + .gm-tag-success { + display: none; + } + .gm-iothrottling-notapplied-text { display: block; - margin: 0; - padding: 0; - width: 100%; - height: 50px; } - - .gm-units { - text-align: right; - float: right; - width: 160px; + .gm-iothrottling-saved { + .gm-tag-success { + display: block; + } + .gm-iothrottling-notapplied-text { + display: none; + } } - input { - font-size: medium; - font-family: Helvetica, sans-serif; - margin-top: 10px; - width: 30%; - text-align: right; - float: right; + .gm-modal-body { + display: flex; + flex-direction: column; + } - &:read-only { - border-bottom: 1px solid transparent; - background: var(--gm-btn-bg-color-disabled); - } + .gm-fields { + margin: 15px 0; + min-height: 40px; + flex: 1; + align-items: flex-start; - &:required:invalid, - &:focus:invalid { - border-bottom: 1px solid var(--gm-btn-bg-color); + .gm-fields-container{ + display: flex; + align-items: center; + gap: 10px; + .gm-iothrottling-readbyterate{ + width: 49px; + input{ + text-align: center; + } + } + .gm-units{ + margin-left: 20px; + } } } - button { - margin-top: 20px; + .gm-iothrottling-clearcache, + .gm-iothrottling-apply{ + display: flex; + justify-content: space-between; + align-items: center; + height: 60px; + .gm-iothrottling-status{ + display: flex; + align-items: center; + gap: 5px; + .gm-iothrottling-notapplied-text{ + font-style: italic; + color: #7f7f7f // TODO + + } + } } }