diff --git a/src/server_manager/web_app/app.ts b/src/server_manager/web_app/app.ts index 52a81a1b..0853becf 100644 --- a/src/server_manager/web_app/app.ts +++ b/src/server_manager/web_app/app.ts @@ -25,8 +25,8 @@ import {TokenManager} from './digitalocean_oauth'; import * as digitalocean_server from './digitalocean_server'; import {DisplayServer, DisplayServerRepository, makeDisplayServer} from './display_server'; import {parseManualServerConfig} from './management_urls'; - import {AppRoot} from './ui_components/app-root.js'; +import {Location} from './ui_components/outline-region-picker-step'; import {DisplayAccessKey, DisplayDataAmount, ServerView} from './ui_components/outline-server-view.js'; // The Outline DigitalOcean team's referral code: @@ -38,6 +38,19 @@ const DATA_LIMITS_VERSION = '1.1.0'; const CHANGE_HOSTNAME_VERSION = '1.2.0'; const MAX_ACCESS_KEY_DATA_LIMIT_BYTES = 50 * (10 ** 9); // 50GB +// DigitalOcean mapping of regions to flags +const FLAG_IMAGE_DIR = 'images/flags'; +const DIGITALOCEAN_FLAG_MAPPING: {[cityId: string]: string} = { + ams: `${FLAG_IMAGE_DIR}/netherlands.png`, + sgp: `${FLAG_IMAGE_DIR}/singapore.png`, + blr: `${FLAG_IMAGE_DIR}/india.png`, + fra: `${FLAG_IMAGE_DIR}/germany.png`, + lon: `${FLAG_IMAGE_DIR}/uk.png`, + sfo: `${FLAG_IMAGE_DIR}/us.png`, + tor: `${FLAG_IMAGE_DIR}/canada.png`, + nyc: `${FLAG_IMAGE_DIR}/us.png`, +}; + function dataLimitToDisplayDataAmount(limit: server.DataLimit): DisplayDataAmount|null { if (!limit) { return null; @@ -724,16 +737,10 @@ export class App { }) .then( (map) => { - // Change from a list of regions per location to just one region per location. - // Where there are multiple working regions in one location, arbitrarily use the - // first. - const availableRegionIds: {[cityId: string]: server.RegionId} = {}; - for (const cityId in map) { - if (map[cityId].length > 0) { - availableRegionIds[cityId] = map[cityId][0]; - } - } - regionPicker.availableRegionIds = availableRegionIds; + const locations = Object.entries(map).map(([cityId, regionIds]) => { + return this.createLocationModel(cityId, regionIds); + }); + regionPicker.locations = locations; }, (e) => { console.error(`Failed to get list of available regions: ${e}`); @@ -1248,4 +1255,13 @@ export class App { document.documentElement.setAttribute('dir', languageDir); window.localStorage.setItem('overrideLanguage', languageCode); } + + private createLocationModel(cityId: string, regionIds: string[]): Location { + return { + id: regionIds.length > 0 ? regionIds[0] : null, + name: this.appRoot.localize(`city-${cityId}`), + flag: DIGITALOCEAN_FLAG_MAPPING[cityId] || '', + available: regionIds.length > 0, + }; + } } diff --git a/src/server_manager/web_app/ui_components/app-root.js b/src/server_manager/web_app/ui_components/app-root.js index 5c601872..b73456df 100644 --- a/src/server_manager/web_app/ui_components/app-root.js +++ b/src/server_manager/web_app/ui_components/app-root.js @@ -35,7 +35,7 @@ import './outline-intro-step.js'; import './outline-language-picker.js'; import './outline-manual-server-entry.js'; import './outline-modal-dialog.js'; -import './outline-region-picker-step.js'; +import './outline-region-picker-step'; import './outline-server-progress-step.js'; import './outline-tos-view.js'; @@ -590,7 +590,7 @@ export class AppRoot extends mixinBehaviors getAndShowRegionPicker() { this.currentPage = 'regionPicker'; - this.$.regionPicker.init(); + this.$.regionPicker.reset(); return this.$.regionPicker; } @@ -629,7 +629,7 @@ export class AppRoot extends mixinBehaviors handleRegionSelected(/** @type {Event} */ e) { this.fire('SetUpServerRequested', { - regionId: this.$.regionPicker.getSelectedRegionId(), + regionId: e.detail.selectedRegionId, }); } diff --git a/src/server_manager/web_app/ui_components/cloud-install-styles.js b/src/server_manager/web_app/ui_components/cloud-install-styles.js index 32ef5a06..2b334abb 100644 --- a/src/server_manager/web_app/ui_components/cloud-install-styles.js +++ b/src/server_manager/web_app/ui_components/cloud-install-styles.js @@ -15,6 +15,7 @@ */ import {html} from '@polymer/polymer/lib/utils/html-tag.js'; +import {unsafeCSS} from 'lit-element'; // Polymer style module to share styles between steps // https://polymer-library.polymer-project.org/3.0/docs/devguide/style-shadow-dom#share-styles-between-elements @@ -240,5 +241,8 @@ styleElement.appendChild(html` transform: scaleX(-1); } `); - styleElement.register('cloud-install-styles'); + +// Shared styles for LitElement components +const commonStyleCss = styleElement.querySelector('template').content.textContent; +export const COMMON_STYLES = unsafeCSS(commonStyleCss); diff --git a/src/server_manager/web_app/ui_components/outline-region-picker-step.js b/src/server_manager/web_app/ui_components/outline-region-picker-step.js deleted file mode 100644 index b9524235..00000000 --- a/src/server_manager/web_app/ui_components/outline-region-picker-step.js +++ /dev/null @@ -1,200 +0,0 @@ -/* - Copyright 2018 The Outline Authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -import '@polymer/polymer/polymer-legacy.js'; - -import '@polymer/paper-button/paper-button.js'; -import '@polymer/paper-progress/paper-progress.js'; -import '@polymer/iron-icon/iron-icon.js'; -import '@polymer/iron-icons/iron-icons.js'; -import './cloud-install-styles.js'; -import './outline-step-view.js'; -import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; -import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -const FLAG_IMAGE_DIR = 'images/flags'; -const flagById = { - ams: `${FLAG_IMAGE_DIR}/netherlands.png`, - sgp: `${FLAG_IMAGE_DIR}/singapore.png`, - blr: `${FLAG_IMAGE_DIR}/india.png`, - fra: `${FLAG_IMAGE_DIR}/germany.png`, - lon: `${FLAG_IMAGE_DIR}/uk.png`, - sfo: `${FLAG_IMAGE_DIR}/us.png`, - tor: `${FLAG_IMAGE_DIR}/canada.png`, - nyc: `${FLAG_IMAGE_DIR}/us.png`, -}; - -Polymer({ - _template: html` - - - - - [[localize('region-title')]] - [[localize('region-description')]] - - - [[localize('region-setup')]] - - -
- -
- -
-`, - - is: 'outline-region-picker-step', - - properties: { - cities: { - type: Array, - readonly: true, - value: Object.keys(flagById), - }, - availableRegionIds: { - // One-to-one map from cityIds to regionIds. - type: Object, - readonly: true, - value: {}, - }, - selectedCityId: String, - creatingServer: Boolean, - localize: { - type: Function, - readonly: true, - }, - }, - - handleCreateServerTap: function() { - this.creatingServer = true; - this.fire('RegionSelected'); - }, - - getCityName: function(cityId, localize) { - if (!this.localize) { - return ''; - } - return this.localize(`city-${cityId}`) || ''; - }, - - getFlag: function(cityId) { - return flagById[cityId] || ''; - }, - - isAvailable: function(map, cityId) { - if (!map) { - return false; - } - return cityId in map; - }, - - citySelected: function(event) { - this.selectedCityId = event.model.get('item'); - }, - - _isSelectedCity: function(selectedCityId, cityId) { - if (!selectedCityId) { - return false; - } - return selectedCityId === cityId; - }, - - getSelectedRegionId: function() { - return this.availableRegionIds[this.selectedCityId]; - }, - - init: function() { - this.creatingServer = false; - this.selectedCityId = null; - // Ensure that no radio button is checked. - const checkedCityElement = this.$.cityContainer.querySelector('input[name="city"]:checked'); - if (checkedCityElement) { - checkedCityElement.checked = false; - } - }, - - isCreateButtonEnabled: function(creatingServer, selectedCityId) { - return !creatingServer && selectedCityId; - } -}); diff --git a/src/server_manager/web_app/ui_components/outline-region-picker-step.ts b/src/server_manager/web_app/ui_components/outline-region-picker-step.ts new file mode 100644 index 00000000..183f947f --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-region-picker-step.ts @@ -0,0 +1,159 @@ +/* + Copyright 2018 The Outline Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-progress/paper-progress'; +import '@polymer/iron-icon/iron-icon'; +import '@polymer/iron-icons/iron-icons'; +import './outline-step-view'; + +import {css, customElement, html, LitElement, property, unsafeCSS} from 'lit-element'; + +import {COMMON_STYLES} from './cloud-install-styles'; + +export interface Location { + id: string; + name: string; + flag: string; + available: boolean; +} + +@customElement('outline-region-picker-step') +export class OutlineRegionPicker extends LitElement { + @property({type: Array}) locations: Location[] = []; + @property({type: String}) selectedLocationId: string = null; + @property({type: Boolean}) isServerBeingCreated = false; + @property({type: Function}) localize: Function; + + static get styles() { + return [COMMON_STYLES, css` + input[type="radio"] { + display: none; + } + input[type="radio"]:checked + label.city-button { + background-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 4px; + border: 1px solid var(--primary-green); + } + input[type="radio"] + label.city-button:hover { + border: 1px solid var(--primary-green); + } + input[type="radio"] + label.city-button { + display: inline-block; + flex: 1; + /* Distribute space evenly, accounting for margins, so there are always 4 cards per row. */ + min-width: calc(25% - 24px); + position: relative; + margin: 4px; + text-align: center; + border: 1px solid; + border-color: rgba(0, 0, 0, 0); + cursor: pointer; + transition: 0.5s; + background: var(--background-contrast-color); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 4px; + } + input[type="radio"]:disabled + label.city-button { + /* TODO(alalama): make it look good and indicate disabled */ + filter: blur(2px); + } + .city-name { + color: var(--light-gray); + font-size: 16px; + line-height: 19px; + padding: 48px 0 24px 0; + } + paper-button { + background: var(--primary-green); + color: #fff; + text-align: center; + font-size: 14px; + } + .flag { + width: 86px; + height: 86px; + } + .card-content { + display: flex; + flex-flow: wrap; + padding-top: 24px; + } + .card-header { + height: 24px; + display: flex; + justify-content: flex-end; + } + iron-icon { + color: var(--primary-green); + padding: 6px 6px 0px 6px; + } + `]; + } + + render() { + return html` + + ${this.localize('region-title')} + ${this.localize('region-description')} + + + ${this.localize('region-setup')} + + +
+ ${this.locations.map(item => { + return html` + + `; + })} +
+ ${this.isServerBeingCreated ? html`` : ''} +
+ `; + } + + reset(): void { + this.isServerBeingCreated = false; + this.selectedLocationId = null; + } + + _isCreateButtonEnabled(isCreatingServer: boolean, selectedLocationId: string): boolean { + return !isCreatingServer && selectedLocationId != null; + } + + _locationSelected(event: Event): void { + const inputEl = event.target as HTMLInputElement; + this.selectedLocationId = inputEl.value; + } + + _handleCreateServerTap(): void { + this.isServerBeingCreated = true; + const params = { + bubbles: true, + composed: true, + detail: {selectedRegionId: this.selectedLocationId} + }; + const customEvent = new CustomEvent('RegionSelected', params); + this.dispatchEvent(customEvent); + } +}