Merge pull request #655 from Jigsaw-Code/fortuna-types

Add types to Javascript
This commit is contained in:
Vinicius Fortuna 2020-05-20 01:10:33 -04:00 • committed by GitHub
commit 49af369702
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 182 additions and 121 deletions

View file

@ -22,6 +22,8 @@ import {Surveys} from '../model/survey';
import {App} from './app';
import {TokenManager} from './digitalocean_oauth';
import {DisplayServer, DisplayServerRepository, makeDisplayServer} from './display_server';
import {AppRoot} from './ui_components/app-root.js';
import {ServerView} from './ui_components/outline-server-view.js';
const TOKEN_WITH_NO_SERVERS = 'no-server-token';
const TOKEN_WITH_ONE_SERVER = 'one-server-token';
@ -257,11 +259,12 @@ enum AppRootScreen {
DIALOG
}
class FakePolymerAppRoot implements polymer.Base {
class FakePolymerAppRoot extends AppRoot {
events = new EventEmitter();
backgroundScreen = AppRootScreen.NONE;
currentScreen = AppRootScreen.NONE;
serverView = {setServerTransferredData: () => {}, serverId: '', initHelpBubbles: () => {}};
serverView = {setServerTransferredData: () => {}, serverId: '', initHelpBubbles: () => {}} as
unknown as ServerView;
serverList: DisplayServer[] = [];
is: 'fake-polymer-app-root';
@ -290,9 +293,9 @@ class FakePolymerAppRoot implements polymer.Base {
showModalDialog() {
this.backgroundScreen = this.currentScreen;
this.setScreen(AppRootScreen.DIALOG);
const promise = new Promise(() => {});
const promise = new Promise<number>(() => 0);
// Supress Promise not handled warning.
promise.then(() => {});
promise.then(v => v);
return promise;
}
@ -304,7 +307,7 @@ class FakePolymerAppRoot implements polymer.Base {
this.backgroundScreen = AppRootScreen.NONE;
}
getServerView() {
getServerView(serverId: string): ServerView {
return this.serverView;
}

View file

@ -27,6 +27,9 @@ 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 {DisplayAccessKey, DisplayDataAmount, ServerView} from './ui_components/outline-server-view.js';
// The Outline DigitalOcean team's referral code:
// https://www.digitalocean.com/help/referral-program/
const UNUSED_DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716';
@ -38,20 +41,6 @@ const CHANGE_HOSTNAME_VERSION = '1.2.0';
export const DATA_LIMITS_AVAILABILITY_DATE = new Date('2020-06-02');
const MAX_ACCESS_KEY_DATA_LIMIT_BYTES = 50 * (10 ** 9); // 50GB
interface UiAccessKey {
id: string;
placeholderName: string;
name: string;
accessUrl: string;
transferredBytes: number;
relativeTraffic: number;
}
interface DisplayDataAmount {
unit: 'MB'|'GB';
value: number;
}
function dataLimitToDisplayDataAmount(limit: server.DataLimit): DisplayDataAmount|null {
if (!limit) {
return null;
@ -99,7 +88,7 @@ async function computeDefaultAccessKeyDataLimit(
}
}
async function showHelpBubblesOnce(serverView: polymer.Base) {
async function showHelpBubblesOnce(serverView: ServerView) {
if (!window.localStorage.getItem('addAccessKeyHelpBubble-dismissed')) {
await serverView.showAddAccessKeyHelpBubble();
window.localStorage.setItem('addAccessKeyHelpBubble-dismissed', 'true');
@ -137,7 +126,7 @@ export class App {
private serverBeingCreated: server.ManagedServer;
constructor(
private appRoot: polymer.Base, private readonly version: string,
private appRoot: AppRoot, private readonly version: string,
private createDigitalOceanSession: DigitalOceanSessionFactory,
private createDigitalOceanServerRepository: DigitalOceanServerRepositoryFactory,
private manualServerRepository: server.ManualServerRepository,
@ -565,7 +554,7 @@ export class App {
});
} else {
// Display the unreachable server state within the server view.
const serverView = this.appRoot.getServerView(displayServer.id);
const serverView = this.appRoot.getServerView(displayServer.id) as ServerView;
serverView.isServerReachable = false;
serverView.isServerManaged = isManagedServer(server);
serverView.serverName = displayServer.name; // Don't get the name from the remote server.
@ -892,7 +881,7 @@ export class App {
this.showTransferStats(selectedServer, view);
}
private showMetricsOptInWhenNeeded(selectedServer: server.Server, serverView: polymer.Base) {
private showMetricsOptInWhenNeeded(selectedServer: server.Server, serverView: ServerView) {
const showMetricsOptInOnce = () => {
// Sanity check to make sure the running server is still displayed, i.e.
// it hasn't been deleted.
@ -922,7 +911,7 @@ export class App {
}
}
private async refreshTransferStats(selectedServer: server.Server, serverView: polymer.Base) {
private async refreshTransferStats(selectedServer: server.Server, serverView: ServerView) {
try {
const stats = await selectedServer.getDataUsage();
let totalBytes = 0;
@ -963,7 +952,7 @@ export class App {
}
}
private showTransferStats(selectedServer: server.Server, serverView: polymer.Base) {
private showTransferStats(selectedServer: server.Server, serverView: ServerView) {
this.refreshTransferStats(selectedServer, serverView);
// Get transfer stats once per minute for as long as server is selected.
const statsRefreshRateMs = 60 * 1000;
@ -986,7 +975,7 @@ export class App {
// Converts the access key from the remote service format to the
// format used by outline-server-view.
private convertToUiAccessKey(remoteAccessKey: server.AccessKey): UiAccessKey {
private convertToUiAccessKey(remoteAccessKey: server.AccessKey): DisplayAccessKey {
return {
id: remoteAccessKey.id,
placeholderName: `${this.appRoot.localize('key', 'keyId', remoteAccessKey.id)}`,
@ -1224,7 +1213,8 @@ export class App {
this.appRoot.showError(this.appRoot.localize('error-server-rename'));
const oldName = this.selectedServer.getName();
view.serverName = oldName;
view.$.serverSettings.serverName = oldName;
// tslint:disable-next-line:no-any
(view.$.serverSettings as any).serverName = oldName;
}
}

View file

@ -24,6 +24,7 @@ import * as digitalocean_server from './digitalocean_server';
import {DisplayServerRepository} from './display_server';
import {ManualServerRepository} from './manual_server';
import {DEFAULT_PROMPT_IMPRESSION_DELAY_MS, OutlineSurveys} from './survey';
import {AppRoot} from './ui_components/app-root.js';
const SUPPORTED_LANGUAGES: {[key: string]: {id: string, dir: string}} = {
'am': {id: 'am', dir: 'ltr'},
@ -101,7 +102,7 @@ document.addEventListener('WebComponentsReady', () => {
document.documentElement.setAttribute('dir', languageDirection);
// NOTE: this cast is safe and allows us to leverage Polymer typings since we haven't migrated to
// Polymer 3, which adds typescript support.
const appRoot = document.getElementById('appRoot') as unknown as polymer.Base;
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
appRoot.setLanguage(language.string(), languageDirection);
new App(
appRoot, version, digitalocean_api.createDigitalOceanSession,

View file

@ -36,7 +36,6 @@ import './outline-manual-server-entry.js';
import './outline-modal-dialog.js';
import './outline-region-picker-step.js';
import './outline-server-progress-step.js';
import './outline-server-view.js';
import './outline-tos-view.js';
import {AppLocalizeBehavior} from '@polymer/app-localize-behavior/app-localize-behavior.js';
@ -44,9 +43,13 @@ import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {DisplayServer} from '../display_server';
import {ServerView} from './outline-server-view.js';
const TOS_ACK_LOCAL_STORAGE_KEY = 'tos-ack';
class AppRoot extends mixinBehaviors
export class AppRoot extends mixinBehaviors
([AppLocalizeBehavior], PolymerElement) {
static get template() {
return html`
@ -467,30 +470,15 @@ class AppRoot extends mixinBehaviors
static get properties() {
return {
// Properties language and useKeyIfMissing are used by Polymer.AppLocalizeBehavior.
language: {
type: String,
readonly: true,
},
useKeyIfMissing: {
type: Boolean,
value: true,
},
serverList: {
type: Array,
value: [],
},
selectedServer: {
type: Object, // DisplayServer in display_server.ts
value: undefined,
},
language: {type: String, readonly: true},
useKeyIfMissing: {type: Boolean},
serverList: {type: Array},
selectedServer: {type: Object},
hasManualServers: {
type: Boolean,
computed: '_computeHasManualServers(serverList.*)',
},
adminEmail: {
type: String,
value: '',
},
adminEmail: {type: String},
isSignedInToDigitalOcean: {
type: Boolean,
computed: '_computeIsSignedInToDigitalOcean(adminEmail)',
@ -505,14 +493,8 @@ class AppRoot extends mixinBehaviors
type: Boolean,
computed: '_computeHasAcceptedTermsOfService(userAcceptedTos)',
},
currentPage: {
type: String,
value: 'intro',
},
shouldShowSideBar: {
type: Boolean,
value: false,
},
currentPage: {type: String},
shouldShowSideBar: {type: Boolean},
sideBarMarginClass: {
type: String,
computed: '_computeSideBarMarginClass(shouldShowSideBar)',
@ -522,6 +504,18 @@ class AppRoot extends mixinBehaviors
constructor() {
super();
/** @type {DisplayServer} */
this.selectedServer = undefined;
this.language = '';
this.useKeyIfMissing = true;
/** @type {DisplayServer[]} */
this.serverList = [];
this.adminEmail = '';
this.outlineVersion = '';
this.currentPage = 'intro';
this.shouldShowSideBar = false;
this.addEventListener('RegionSelected', this.handleRegionSelected);
this.addEventListener(
'SetUpGenericCloudProviderRequested', this.handleSetUpGenericCloudProviderRequested);
@ -530,6 +524,11 @@ class AppRoot extends mixinBehaviors
this.addEventListener('ManualServerEntryCancelled', this.handleManualCancelled);
}
/**
* Sets the language and direction for the application
* @param {string} newLanguage
* @param {string} langDir
*/
setLanguage(newLanguage, langDir) {
const messagesUrl = `./messages/${newLanguage}.json`;
this.loadResources(messagesUrl, newLanguage);
@ -573,6 +572,10 @@ class AppRoot extends mixinBehaviors
return this.$.manualEntry;
}
/**
* @param {string} serverName
* @param {boolean} showCancelButton
*/
showProgress(serverName, showCancelButton) {
this.currentPage = 'serverProgressStep';
this.$.serverProgressStep.serverName = serverName;
@ -585,6 +588,11 @@ class AppRoot extends mixinBehaviors
this.currentPage = 'serverView';
}
/**
* Gets the ServerView for the server given by its id
* @param {string} displayServerId
* @returns {ServerView}
*/
getServerView(displayServerId) {
if (!displayServerId) {
return null;
@ -593,7 +601,7 @@ class AppRoot extends mixinBehaviors
return this.$.serverView.querySelector(`#serverView-${selectedServerId}`);
}
handleRegionSelected(e) {
handleRegionSelected(/** @type {Event} */ e) {
this.fire('SetUpServerRequested', {
regionId: this.$.regionPicker.getSelectedRegionId(),
});
@ -611,7 +619,7 @@ class AppRoot extends mixinBehaviors
this.handleManualServerSelected('gcp');
}
handleManualServerSelected(cloudProvider) {
handleManualServerSelected(/** @type {'generic'|'aws'|'gcp'} */ cloudProvider) {
this.$.manualEntry.clear();
this.$.manualEntry.cloudProvider = cloudProvider;
this.currentPage = 'manualEntry';
@ -621,14 +629,19 @@ class AppRoot extends mixinBehaviors
this.currentPage = 'intro';
}
showError(errorMsg) {
showError(/** @type {string} */ errorMsg) {
this.showToast(errorMsg, Infinity);
}
showNotification(message, durationMs = 3000) {
showNotification(/** @type {string} */ message, durationMs = 3000) {
this.showToast(message, durationMs);
}
/**
* Show a toast with a message
* @param {string} message
* @param {number} duration in seconds
*/
showToast(message, duration) {
const toast = this.$.toast;
toast.close();
@ -647,8 +660,11 @@ class AppRoot extends mixinBehaviors
this.$.toast.close();
}
// cb is a function which accepts a single boolean which is true iff
// the user chose to retry the failing operation.
/**
* @param {(retry: boolean) => void} cb a function which accepts a single boolean which is true
* iff
* the user chose to retry the failing operation.
*/
showConnectivityDialog(cb) {
const dialogTitle = this.localize('error-connectivity-title');
const dialogText = this.localize('error-connectivity');
@ -668,6 +684,10 @@ class AppRoot extends mixinBehaviors
});
}
/**
* @param {string} errorTitle
* @param {string} errorText
*/
showManualServerError(errorTitle, errorText) {
this.showModalDialog(errorTitle, errorText, [this.localize('cancel'), this.localize('retry')])
.then(clickedButtonIndex => {
@ -730,7 +750,7 @@ class AppRoot extends mixinBehaviors
this.fire('SignOutRequested');
}
openManualInstallFeedback(prepopulatedMessage) {
openManualInstallFeedback(/** @type {string} */ prepopulatedMessage) {
this.$.feedbackDialog.open(prepopulatedMessage, true);
}
@ -738,7 +758,7 @@ class AppRoot extends mixinBehaviors
this.$.shareDialog.open(accessKey, s3Url);
}
openGetConnectedDialog(inviteUrl) {
openGetConnectedDialog(/** @type {string} */ inviteUrl) {
const dialog = this.$.getConnectedDialog;
if (dialog.children.length > 1) {
return; // The iframe is already loading.
@ -766,7 +786,12 @@ class AppRoot extends mixinBehaviors
this.$.metricsDialog.showMetricsOptInDialog();
}
// Returns a Promise which fulfills with the index of the button clicked.
/**
* @param {string} title
* @param {string} text
* @param {string[]} buttons
* @returns {Promise<number>} a Promise which fulfills with the index of the button clicked.
*/
showModalDialog(title, text, buttons) {
return this.$.modalDialog.open(title, text, buttons);
}

View file

@ -55,6 +55,23 @@ function makePublicEvent(eventName, detail) {
return new CustomEvent(eventName, params);
}
/**
* An access key to be displayed
* @typedef {Object} DisplayAccessKey
* @prop {string} id
* @prop {string} placeholderName
* @prop {string} name
* @prop {string} accessUrl
* @prop {number} transferredBytes
* @prop {number} relativeTraffic
*/
/**
* @typedef {Object} DisplayDataAmount
* @prop {'MB'|'GB'} unit
* @prop {number} value
*/
export class ServerView extends DirMixin(PolymerElement) {
static get template() {
return html`
@ -564,60 +581,35 @@ export class ServerView extends DirMixin(PolymerElement) {
serverName: String,
serverHostname: String,
serverVersion: String,
isHostnameEditable: {type: Boolean, value: false},
isHostnameEditable: {type: Boolean},
serverManagementApiUrl: String,
serverPortForNewAccessKeys: Number,
isAccessKeyPortEditable: {type: Boolean, value: false},
isAccessKeyPortEditable: {type: Boolean},
serverCreationDate: String,
serverLocation: String,
accessKeyDataLimit: {type: Object, value: null},
accessKeyDataLimit: {type: Object},
isAccessKeyDataLimitEnabled: {type: Boolean},
supportsAccessKeyDataLimit:
{type: Boolean, value: false}, // Whether the server supports data limits.
dataLimitsAvailabilityDate:
{type: String}, // Date by which the feature stops being an experiment.
supportsAccessKeyDataLimit: {type: Boolean},
dataLimitsAvailabilityDate: {type: String},
isServerManaged: Boolean,
isServerReachable: Boolean,
// Callback for retrying to display an unreachable server.
retryDisplayingServer: Function,
// myConnection has the same fields as each item in accessKeyRows. It may
// be unset in some old versions of Outline that allowed deleting this
// row.
myConnection: Object,
totalInboundBytes: Number,
accessKeyRows: {type: Array, value: []},
accessKeyRows: {type: Array},
hasNonAdminAccessKeys: Boolean,
metricsEnabled: Boolean,
// Initialize monthlyOutboundTransferBytes and monthlyCost to 0, so they can
// be bound to hidden attributes. Initializing to undefined does not
// cause hidden$=... expressions to be evaluated and so elements may be
// shown incorrectly. See:
// https://stackoverflow.com/questions/33700125/polymer-1-0-hidden-attribute-negate-operator
// https://www.polymer-project.org/1.0/docs/devguide/data-binding.html
monthlyOutboundTransferBytes: {type: Number, value: 0},
monthlyCost: {type: Number, value: 0},
selectedTab: {
type: String,
value: 'connections',
},
monthlyOutboundTransferBytes: {type: Number},
monthlyCost: {type: Number},
selectedTab: {type: String},
managedServerUtilzationPercentage: {
type: Number,
computed:
'_computeManagedServerUtilzationPercentage(totalInboundBytes, monthlyOutboundTransferBytes)',
},
accessKeySortBy: {
type: String,
value: 'name',
},
accessKeySortDirection: {
// 1 == ascending, -1 == descending
type: Number,
value: 1,
},
localize: {
type: Function,
readonly: true,
},
accessKeySortBy: {type: String},
accessKeySortDirection: {type: Number},
localize: {type: Function, readonly: true},
};
}
@ -629,23 +621,72 @@ export class ServerView extends DirMixin(PolymerElement) {
];
}
// Parameter `accessKey` has the format {
// id: string,
// placeholderName: string,
// name: string,
// accessUrl: string,
// transferredBytes: number;
// relativeTraffic: number;
// }
addAccessKey(accessKey) {
// TODO(fortuna): Restore loading animation.
// TODO(fortuna): Restore highlighting.
this.push('accessKeyRows', accessKey);
// Force render the access key list so that the input is present in the DOM
this.$.accessKeysContainer.querySelector('dom-repeat').render();
const input = this.shadowRoot.querySelector(`#access-key-${accessKey.id}`);
input.select();
}
constructor() {
super();
this.serverId = '';
this.serverName = '';
this.serverHostname = '';
this.serverVersion = '';
this.isHostnameEditable = false;
this.serverManagementApiUrl = '';
/** @type {number} */
this.serverPortForNewAccessKeys = null;
this.isAccessKeyPortEditable = false;
this.serverCreationDate = '';
this.serverLocation = '';
/** @type {DisplayDataAmount} */
this.accessKeyDataLimit = null;
this.isAccessKeyDataLimitEnabled = false;
/** Whether the server supports data limits. */
this.supportsAccessKeyDataLimit = false;
/** Date by which the feature stops being an experiment. */
this.dataLimitsAvailabilityDate = '';
this.isServerManaged = false;
this.isServerReachable = false;
/**
* Callback for retrying to display an unreachable server.
* @type {() => void)}
*/
this.retryDisplayingServer = null;
/**
* myConnection has the same fields as each item in accessKeyRows. It may
* be unset in some old versions of Outline that allowed deleting this row
* @type {DisplayAccessKey}
*/
this.myConnection = null;
this.totalInboundBytes = 0;
/** @type {DisplayAccessKey[]} */
this.accessKeyRows = [];
this.hasNonAdminAccessKeys = false;
this.metricsEnabled = true;
// Initialize monthlyOutboundTransferBytes and monthlyCost to 0, so they can
// be bound to hidden attributes. Initializing to undefined does not
// cause hidden$=... expressions to be evaluated and so elements may be
// shown incorrectly. See:
// https://stackoverflow.com/questions/33700125/polymer-1-0-hidden-attribute-negate-operator
// https://www.polymer-project.org/1.0/docs/devguide/data-binding.html
this.monthlyOutboundTransferBytes = 0;
this.monthlyCost = 0;
this.selectedTab = 'connections';
this.accessKeySortBy = 'name';
/** The direction to sort: 1 == ascending, -1 == descending */
this.accessKeySortDirection = 1;
/** @type {(msgId: string, ...params: string[]) => string} */
this.localize = null;
}
/**
* @param {DisplayAccessKey} accessKey
*/
addAccessKey(accessKey) {
// TODO(fortuna): Restore loading animation.
// TODO(fortuna): Restore highlighting.
this.push('accessKeyRows', accessKey);
// Force render the access key list so that the input is present in the DOM
this.$.accessKeysContainer.querySelector('dom-repeat').render();
const input = this.shadowRoot.querySelector(`#access-key-${accessKey.id}`);
input.select();
}
removeAccessKey(accessKeyId) {
for (let ui in this.accessKeyRows) {

View file

@ -9,6 +9,7 @@
"outDir": "build/server_manager/web_app/js",
"sourceMap": true,
"experimentalDecorators":true,
"allowJs": true,
},
"include": [
"src/**/*.ts"