mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-10-22 12:31:26 +00:00
Outline Region Picker refactor (#714)
* Updates region picker to Polymer 3 * commit hook * Update region picker to LitElement (Typescript) * Auto-formatting * Auto-formatting * Formatting * Refactor * Addresses review comments * Addresses review comments * Exports cloud-install-styles for LitElement components * Fixes LitElement CSS registration * Makes region card CSS rtl friendly
This commit is contained in:
parent
a403a00ecf
commit
cfdcfa8c30
5 changed files with 194 additions and 215 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</style>`);
|
||||
|
||||
styleElement.register('cloud-install-styles');
|
||||
|
||||
// Shared styles for LitElement components
|
||||
const commonStyleCss = styleElement.querySelector('template').content.textContent;
|
||||
export const COMMON_STYLES = unsafeCSS(commonStyleCss);
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
<style include="cloud-install-styles"></style>
|
||||
|
||||
<style>
|
||||
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;
|
||||
padding-top: 24px;
|
||||
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;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--primary-green);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 6px;
|
||||
}
|
||||
</style>
|
||||
<outline-step-view display-action="">
|
||||
<span slot="step-title">[[localize('region-title')]]</span>
|
||||
<span slot="step-description">[[localize('region-description')]]</span>
|
||||
<span slot="step-action">
|
||||
<paper-button id="createServerButton" on-tap="handleCreateServerTap" disabled\$="[[!isCreateButtonEnabled(creatingServer, selectedCityId)]]">
|
||||
[[localize('region-setup')]]
|
||||
</paper-button>
|
||||
</span>
|
||||
<div class="card-content" id="cityContainer">
|
||||
<template is="dom-repeat" items="{{cities}}">
|
||||
<input type="radio" city\$="{{item}}" name="city" id\$="{{item}}" disabled\$="{{!isAvailable(availableRegionIds, item)}}" on-change="citySelected" \\="">
|
||||
<label for\$="{{item}}" class="city-button">
|
||||
<iron-icon icon="check-circle" hidden\$="{{!_isSelectedCity(selectedCityId, item)}}"></iron-icon>
|
||||
<img class="flag" src\$="{{getFlag(item)}}">
|
||||
<div class="city-name">{{getCityName(item, localize)}}</div>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<paper-progress hidden\$="[[!creatingServer]]" indeterminate="" class="slow"></paper-progress>
|
||||
</outline-step-view>
|
||||
`,
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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`
|
||||
<outline-step-view display-action="">
|
||||
<span slot="step-title">${this.localize('region-title')}</span>
|
||||
<span slot="step-description">${this.localize('region-description')}</span>
|
||||
<span slot="step-action">
|
||||
<paper-button id="createServerButton" @tap="${this._handleCreateServerTap}" ?disabled="${!this._isCreateButtonEnabled(this.isServerBeingCreated, this.selectedLocationId)}">
|
||||
${this.localize('region-setup')}
|
||||
</paper-button>
|
||||
</span>
|
||||
<div class="card-content" id="cityContainer">
|
||||
${this.locations.map(item => {
|
||||
return html`
|
||||
<input type="radio" id="card-${item.id}" name="city" value="${item.id}" ?disabled="${!item.available}" .checked="${this.selectedLocationId === item.id}" @change="${this._locationSelected}">
|
||||
<label for="card-${item.id}" class="city-button">
|
||||
<div class="card-header">
|
||||
${this.selectedLocationId === item.id ? html`<iron-icon icon="check-circle"></iron-icon>` : ''}
|
||||
</div>
|
||||
<img class="flag" src="${item.flag}">
|
||||
<div class="city-name">${item.name}</div>
|
||||
</label>`;
|
||||
})}
|
||||
</div>
|
||||
${this.isServerBeingCreated ? html`<paper-progress indeterminate="" class="slow"></paper-progress>` : ''}
|
||||
</outline-step-view>
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue