mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-08-04 14:37:34 +00:00
Display a real progress bar for server installs
This change replaces the fake progress bar with a real one, reflecting incremental installation progress. It also updates the text to avoid claiming that installation takes less than two minutes. (It takes about five minutes on GCP.) Additionally, this change converts outline-server-progress-step.js to Typescript.
This commit is contained in:
parent
a8a10c2c71
commit
b4e5981aa5
12 changed files with 226 additions and 182 deletions
|
|
@ -117,6 +117,8 @@ function cloud::add_encoded_kv_tag() {
|
|||
cloud::add_tag "kv:${key}:${value}"
|
||||
}
|
||||
|
||||
echo "true" | cloud::add_encoded_kv_tag "outline"
|
||||
|
||||
log_for_sentry "Starting install"
|
||||
|
||||
# DigitalOcean's docker image comes with ufw enabled by default, disable so when
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export SHADOWBOX_DIR="${SHADOWBOX_DIR:-${HOME:-/root}/shadowbox}"
|
|||
mkdir -p "${SHADOWBOX_DIR}"
|
||||
|
||||
# Save output for debugging
|
||||
exec &> "${SHADOWBOX_DIR}/install-shadowbox-output"
|
||||
true > "${SHADOWBOX_DIR}/install-shadowbox-output"
|
||||
|
||||
function cloud::public_ip() {
|
||||
curl curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip"
|
||||
|
|
@ -66,6 +66,8 @@ function cloud::set_guest_attribute() {
|
|||
curl -H "Metadata-Flavor: Google" -X PUT -d "${label_value}" "${SET_GUEST_ATTRIBUTE_URL}"
|
||||
}
|
||||
|
||||
cloud::set_guest_attribute "outline" "true"
|
||||
|
||||
# Enable BBR.
|
||||
# Recent DigitalOcean one-click images are based on Ubuntu 18 and have kernel 4.15+.
|
||||
log_for_sentry "Enabling BBR"
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@
|
|||
"setup-do-cost": "Only US$5 a month",
|
||||
"setup-do-create": "Create a new server with your DigitalOcean account for an additional US$5/30 days for 1 TB of data transfer.",
|
||||
"setup-do-data": "1 TB data transfer allowance",
|
||||
"setup-do-description": "This could take up to two minutes. You can destroy this server at anytime.",
|
||||
"setup-do-description": "This could take several minutes. You can destroy this server at any time.",
|
||||
"setup-do-easiest": "Easiest setup process",
|
||||
"setup-do-title": "Setting up Outline.",
|
||||
"setup-firewall-instructions": "Firewall instructions",
|
||||
|
|
|
|||
|
|
@ -1124,7 +1124,7 @@
|
|||
"description": "This string appears in the server setup view as an item of a list describing Outline's features. Refers to the monthly amount of transfer data offered by a cloud server provider. TB is an abbreviation for terabyte and should not be translated."
|
||||
},
|
||||
"setup_do_description": {
|
||||
"message": "This could take up to two minutes. You can destroy this server at anytime.",
|
||||
"message": "This could take several minutes. You can destroy this server at any time.",
|
||||
"description": "This string appears in the server setup view as a sub-header. Displayed when a server is being created along a progress bar."
|
||||
},
|
||||
"setup_do_easiest": {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,8 @@ export interface ManualServer extends Server {
|
|||
export interface ManagedServer extends Server {
|
||||
// Returns a promise that fulfills once installation is complete.
|
||||
waitOnInstall(): Promise<void>;
|
||||
// Enables notifications related to installation progress.
|
||||
setProgressListener(listener: (progress: number) => void): void;
|
||||
// Returns server host object.
|
||||
getHost(): ManagedServerHost;
|
||||
// Returns true when installation is complete.
|
||||
|
|
|
|||
|
|
@ -798,10 +798,13 @@ export class App {
|
|||
};
|
||||
}
|
||||
|
||||
private async setServerProgressView(server: server.Server): Promise<void> {
|
||||
private async setServerProgressView(server: server.ManagedServer): Promise<void> {
|
||||
const view = await this.appRoot.getServerView(server.getId());
|
||||
view.serverName = this.makeDisplayName(server);
|
||||
view.selectedPage = 'progressView';
|
||||
server.setProgressListener(progress => {
|
||||
view.installProgress = progress;
|
||||
});
|
||||
}
|
||||
|
||||
private showMetricsOptInWhenNeeded(selectedServer: server.Server, serverView: ServerView) {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ import {EventEmitter} from 'eventemitter3';
|
|||
|
||||
import {DigitalOceanSession, DropletInfo} from '../cloud/digitalocean_api';
|
||||
import * as errors from '../infrastructure/errors';
|
||||
import {asciiToHex, hexToString} from '../infrastructure/hex_encoding';
|
||||
import {hexToString} from '../infrastructure/hex_encoding';
|
||||
import { Region } from '../model/digitalocean';
|
||||
import {CloudLocation} from '../model/location';
|
||||
import * as server from '../model/server';
|
||||
|
||||
import {ShadowboxServer} from './shadowbox_server';
|
||||
|
|
@ -38,19 +37,16 @@ const DEPRECATED_API_PORT_TAG = 'apiport';
|
|||
// The tag key for the manager API url prefix.
|
||||
const DEPRECATED_API_PREFIX_TAG = 'apiprefix';
|
||||
|
||||
function makeKeyValueTagPrefix(key: string) {
|
||||
return makeKeyValueTag(key, '');
|
||||
}
|
||||
|
||||
function makeKeyValueTag(key: string, value: string) {
|
||||
return [KEY_VALUE_TAG, key, asciiToHex(value)].join(':');
|
||||
}
|
||||
|
||||
|
||||
// Possible install states for DigitaloceanServer.
|
||||
enum InstallState {
|
||||
// Unknown state - server may still be installing.
|
||||
UNKNOWN = 0,
|
||||
// Server status is "active"
|
||||
CREATED,
|
||||
// Userspace is running (detected by the presence of tags)
|
||||
BOOTED,
|
||||
// The server has generated its management service certificate.
|
||||
HAS_CERTIFICATE,
|
||||
// Server is running and has the API URL and certificate fingerprint set.
|
||||
SUCCESS,
|
||||
// Server is in an error state.
|
||||
|
|
@ -62,6 +58,7 @@ enum InstallState {
|
|||
export class DigitalOceanServer extends ShadowboxServer implements server.ManagedServer {
|
||||
private eventQueue = new EventEmitter();
|
||||
private installState: InstallState = InstallState.UNKNOWN;
|
||||
private listener: (progress: number) => void;
|
||||
|
||||
constructor(
|
||||
id: string, private digitalOcean: DigitalOceanSession, private dropletInfo: DropletInfo) {
|
||||
|
|
@ -78,11 +75,11 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// Poll this.installState for changes. This can poll quickly as it
|
||||
// will not make any network requests.
|
||||
const intervalId = setInterval(() => {
|
||||
if (this.installState === InstallState.UNKNOWN) {
|
||||
// installState not known, wait until next retry.
|
||||
if (!this.isInstallStateFinal()) {
|
||||
// Final installState not known, wait until next retry.
|
||||
return;
|
||||
}
|
||||
// State is now known, so we can stop checking.
|
||||
// Final state is now known, so we can stop checking.
|
||||
clearInterval(intervalId);
|
||||
if (this.installState === InstallState.SUCCESS) {
|
||||
fulfill();
|
||||
|
|
@ -104,12 +101,13 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// Synchronous function for updating the installState, which doesn't
|
||||
// refresh droplet info.
|
||||
const updateInstallState = (): void => {
|
||||
if (this.installState !== InstallState.UNKNOWN) {
|
||||
// State is already known, so it cannot be changed.
|
||||
if (this.isInstallStateFinal()) {
|
||||
// Final state is already known, so it cannot be changed.
|
||||
return;
|
||||
}
|
||||
if (this.getTagValue(INSTALL_ERROR_TAG)) {
|
||||
console.error(`error tag: ${this.getTagValue(INSTALL_ERROR_TAG)}`);
|
||||
const tagMap = this.getTagMap();
|
||||
if (tagMap[INSTALL_ERROR_TAG]) {
|
||||
console.error(`error tag: ${tagMap[INSTALL_ERROR_TAG]}`);
|
||||
this.setInstallState(InstallState.ERROR);
|
||||
} else if (Date.now() - startTimestamp >= TIMEOUT_MS) {
|
||||
console.error('hit timeout while waiting for installation');
|
||||
|
|
@ -119,22 +117,28 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// installed the server and can now make API calls.
|
||||
console.info('digitalocean_server: Successfully found API and cert tags');
|
||||
this.setInstallState(InstallState.SUCCESS);
|
||||
} else if (tagMap[CERTIFICATE_FINGERPRINT_TAG]) {
|
||||
this.setInstallState(InstallState.HAS_CERTIFICATE);
|
||||
} else if (Object.keys(tagMap).length > 0) {
|
||||
this.setInstallState(InstallState.BOOTED);
|
||||
} else if (this.dropletInfo?.status === 'active') {
|
||||
this.setInstallState(InstallState.CREATED);
|
||||
}
|
||||
};
|
||||
|
||||
// Attempt to set the install state immediately, based on the initial
|
||||
// droplet info, to possibly save on a refresh API call.
|
||||
updateInstallState();
|
||||
if (this.installState !== InstallState.UNKNOWN) {
|
||||
if (this.isInstallStateFinal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Periodically refresh the droplet info then try to update the install
|
||||
// state again.
|
||||
const intervalId = setInterval(async () => {
|
||||
// Check if install state is already known, so we don't make an unnecessary
|
||||
// request to fetch droplet info.
|
||||
if (this.installState !== InstallState.UNKNOWN) {
|
||||
// Check if the final install state has been reached, so we don't make an
|
||||
// unnecessary request to fetch droplet info.
|
||||
if (this.isInstallStateFinal()) {
|
||||
clearInterval(intervalId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -147,9 +151,9 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
return;
|
||||
}
|
||||
updateInstallState();
|
||||
// Immediately clear the interval if the installState is known to prevent
|
||||
// race conditions due to setInterval firing async.
|
||||
if (this.installState !== InstallState.UNKNOWN) {
|
||||
// Immediately clear the interval if the final install state has been
|
||||
// reached to prevent race conditions due to setInterval firing async.
|
||||
if (this.isInstallStateFinal()) {
|
||||
clearInterval(intervalId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -158,16 +162,42 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
setProgressListener(listener: (progress: number) => void): void {
|
||||
this.listener = listener;
|
||||
listener(this.installProgress());
|
||||
}
|
||||
|
||||
private setInstallState(installState: InstallState) {
|
||||
if (this.installState !== InstallState.UNKNOWN) {
|
||||
// Cannot change the install state once set.
|
||||
return;
|
||||
}
|
||||
if (installState === InstallState.UNKNOWN) {
|
||||
if (this.isInstallStateFinal()) {
|
||||
// The install state is final and cannot be changed.
|
||||
return;
|
||||
}
|
||||
this.installState = installState;
|
||||
this.setInstallCompleted();
|
||||
if (this.installState === InstallState.SUCCESS) {
|
||||
this.setInstallCompleted();
|
||||
}
|
||||
if (this.listener) {
|
||||
this.listener(this.installProgress());
|
||||
}
|
||||
}
|
||||
|
||||
private installProgress(): number {
|
||||
// Values are based on observed installation timing.
|
||||
// Installation typically takes 90 seconds in total.
|
||||
switch (this.installState) {
|
||||
case InstallState.UNKNOWN: return 0.1;
|
||||
case InstallState.CREATED: return 0.5;
|
||||
case InstallState.BOOTED: return 0.55;
|
||||
case InstallState.HAS_CERTIFICATE: return 0.6;
|
||||
case InstallState.SUCCESS: return 1.0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private isInstallStateFinal(): boolean {
|
||||
return this.installState === InstallState.SUCCESS ||
|
||||
this.installState === InstallState.ERROR ||
|
||||
this.installState === InstallState.DELETED;
|
||||
}
|
||||
|
||||
// Returns true on success, else false.
|
||||
|
|
@ -199,21 +229,23 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
}
|
||||
}
|
||||
|
||||
// Gets the value for the given key, stored in the DigitalOcean tags.
|
||||
private getTagValue(key: string): string {
|
||||
const tagPrefix = makeKeyValueTagPrefix(key);
|
||||
// Gets the key-value map stored in the DigitalOcean tags.
|
||||
private getTagMap(): {[key: string]: string} {
|
||||
const ret: {[key: string]: string} = {};
|
||||
const tagPrefix = KEY_VALUE_TAG + ':';
|
||||
for (const tag of this.dropletInfo.tags) {
|
||||
if (!startsWithCaseInsensitive(tag, tagPrefix)) {
|
||||
continue;
|
||||
}
|
||||
const encodedData = tag.slice(tagPrefix.length);
|
||||
const keyValuePair = tag.slice(tagPrefix.length);
|
||||
const [key, hexValue] = keyValuePair.split(':', 2);
|
||||
try {
|
||||
return hexToString(encodedData);
|
||||
ret[key.toLowerCase()] = hexToString(hexValue);
|
||||
} catch (e) {
|
||||
console.error('error decoding hex string');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Returns the public ipv4 address of this server.
|
||||
|
|
@ -228,11 +260,12 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
|
||||
// Gets the address for the user management api, throws an error if unavailable.
|
||||
private getManagementApiAddress(): string {
|
||||
let apiAddress = this.getTagValue(API_URL_TAG);
|
||||
const tagMap = this.getTagMap();
|
||||
let apiAddress = tagMap[API_URL_TAG];
|
||||
// Check the old tags for backward-compatibility.
|
||||
// TODO(fortuna): Delete this before we release v1.0
|
||||
if (!apiAddress) {
|
||||
const portNumber = this.getTagValue(DEPRECATED_API_PORT_TAG);
|
||||
const portNumber = tagMap[DEPRECATED_API_PORT_TAG];
|
||||
if (!portNumber) {
|
||||
throw new Error('Could not get API port number');
|
||||
}
|
||||
|
|
@ -240,7 +273,7 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
throw new Error('API hostname not set');
|
||||
}
|
||||
apiAddress = `https://${this.ipv4Address()}:${portNumber}/`;
|
||||
const apiPrefix = this.getTagValue(DEPRECATED_API_PREFIX_TAG);
|
||||
const apiPrefix = tagMap[DEPRECATED_API_PREFIX_TAG];
|
||||
if (apiPrefix) {
|
||||
apiAddress += apiPrefix + '/';
|
||||
}
|
||||
|
|
@ -254,7 +287,7 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// Gets the certificate fingerprint in base64 format, throws an error if
|
||||
// unavailable.
|
||||
private getCertificateFingerprint(): string {
|
||||
const fingerprint = this.getTagValue(CERTIFICATE_FINGERPRINT_TAG);
|
||||
const fingerprint = this.getTagMap()[CERTIFICATE_FINGERPRINT_TAG];
|
||||
if (fingerprint) {
|
||||
return btoa(fingerprint);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,16 @@ import {DataAmount, ManagedServerHost, MonetaryCost} from '../model/server';
|
|||
import {ShadowboxServer} from './shadowbox_server';
|
||||
|
||||
enum InstallState {
|
||||
// Unknown state - server may still be installing.
|
||||
// Unknown state - server request may still be pending.
|
||||
UNKNOWN = 0,
|
||||
// The instance has been created.
|
||||
INSTANCE_CREATED,
|
||||
// The static IP has been allocated.
|
||||
IP_ALLOCATED,
|
||||
// The system has booted (detected by the creation of guest tags)
|
||||
BOOTED,
|
||||
// The server has generated its management service certificate.
|
||||
HAS_CERTIFICATE,
|
||||
// Server is running and has the API URL and certificate fingerprint set.
|
||||
SUCCESS,
|
||||
// Server is in an error state.
|
||||
|
|
@ -40,6 +48,7 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
private readonly instanceReadiness: Promise<void>;
|
||||
private readonly gcpHost: GcpHost;
|
||||
private installState: InstallState = InstallState.UNKNOWN;
|
||||
private listener: (progress: number) => void = null;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
|
|
@ -51,9 +60,11 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
// Optimization: start the check for a static IP immediately.
|
||||
const hasStaticIp: Promise<boolean> = this.hasStaticIp();
|
||||
this.instanceReadiness = instanceCreation.then(async () => {
|
||||
this.setInstallState(InstallState.INSTANCE_CREATED);
|
||||
if (!await hasStaticIp) {
|
||||
await this.promoteEphemeralIp();
|
||||
}
|
||||
this.setInstallState(InstallState.IP_ALLOCATED);
|
||||
}).catch((e) => {
|
||||
this.setInstallState(InstallState.ERROR);
|
||||
throw e;
|
||||
|
|
@ -104,23 +115,29 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
}
|
||||
|
||||
isInstallCompleted(): boolean {
|
||||
return this.installState !== InstallState.UNKNOWN;
|
||||
return this.installState === InstallState.SUCCESS ||
|
||||
this.installState === InstallState.ERROR ||
|
||||
this.installState === InstallState.DELETED;
|
||||
}
|
||||
|
||||
async waitOnInstall(): Promise<void> {
|
||||
await this.instanceReadiness; // Throws if instance preparation fails.
|
||||
while (this.installState === InstallState.UNKNOWN) {
|
||||
while (!this.isInstallCompleted()) {
|
||||
const outlineGuestAttributes = await this.getOutlineGuestAttributes();
|
||||
if (outlineGuestAttributes.has('apiUrl') && outlineGuestAttributes.has('certSha256')) {
|
||||
const certSha256 = outlineGuestAttributes.get('certSha256');
|
||||
const apiUrl = outlineGuestAttributes.get('apiUrl');
|
||||
trustCertificate(certSha256);
|
||||
this.setManagementApiUrl(apiUrl);
|
||||
this.installState = InstallState.SUCCESS;
|
||||
this.setInstallState(InstallState.SUCCESS);
|
||||
break;
|
||||
} else if (outlineGuestAttributes.has('install-error')) {
|
||||
this.installState = InstallState.ERROR;
|
||||
this.setInstallState(InstallState.ERROR);
|
||||
break;
|
||||
} else if (outlineGuestAttributes.has('certSha256')) {
|
||||
this.setInstallState(InstallState.HAS_CERTIFICATE);
|
||||
} else if (outlineGuestAttributes.has('outline')) {
|
||||
this.setInstallState(InstallState.BOOTED);
|
||||
}
|
||||
|
||||
await sleep(GcpServer.GUEST_ATTRIBUTES_POLLING_INTERVAL_MS);
|
||||
|
|
@ -134,6 +151,25 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
}
|
||||
}
|
||||
|
||||
setProgressListener(listener: (progress: number) => void): void {
|
||||
this.listener = listener;
|
||||
listener(this.installProgress());
|
||||
}
|
||||
|
||||
private installProgress(): number {
|
||||
// Values are based on observed installation timing.
|
||||
// Installation typically takes ~5 minutes in total.
|
||||
switch (this.installState) {
|
||||
case InstallState.UNKNOWN: return 0.005;
|
||||
case InstallState.INSTANCE_CREATED: return 0.03;
|
||||
case InstallState.IP_ALLOCATED: return 0.04;
|
||||
case InstallState.BOOTED: return 0.2;
|
||||
case InstallState.HAS_CERTIFICATE: return 0.8;
|
||||
case InstallState.SUCCESS: return 1.0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async getOutlineGuestAttributes(): Promise<Map<string, string>> {
|
||||
const result = new Map<string, string>();
|
||||
const guestAttributes =
|
||||
|
|
@ -147,6 +183,9 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
|
||||
setInstallState(newState: InstallState): void {
|
||||
this.installState = newState;
|
||||
if (this.listener) {
|
||||
this.listener(this.installProgress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -222,6 +222,9 @@ export class FakeManagedServer extends FakeServer implements server.ManagedServe
|
|||
// shadowbox install time.
|
||||
return new Promise<void>((fulfill, reject) => {});
|
||||
}
|
||||
setProgressListener(listener: (progress: number) => void): void {
|
||||
listener(0.5);
|
||||
}
|
||||
getHost() {
|
||||
return {
|
||||
getMonthlyOutboundTransferLimit: () => ({terabytes: 1}),
|
||||
|
|
|
|||
|
|
@ -1,121 +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-progress/paper-progress.js';
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import './cloud-install-styles.js';
|
||||
import './outline-progress-spinner.js';
|
||||
import './outline-step-view.js';
|
||||
import './style.css';
|
||||
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
|
||||
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
|
||||
Polymer({
|
||||
_template: html`
|
||||
<style include="cloud-install-styles"></style>
|
||||
|
||||
<style>
|
||||
:host {
|
||||
text-align: center;
|
||||
}
|
||||
.card {
|
||||
margin-top: 72px;
|
||||
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: 2px;
|
||||
color: var(--light-gray);
|
||||
background: var(--background-contrast-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.servername {
|
||||
margin: 24px 0 72px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.card p {
|
||||
font-size: 14px;
|
||||
color: var(--light-gray);
|
||||
}
|
||||
outline-progress-spinner {
|
||||
margin-top: 72px;
|
||||
}
|
||||
paper-button {
|
||||
width: 100%;
|
||||
border: 1px solid var(--light-gray);
|
||||
border-radius: 2px;
|
||||
color: var(--light-gray);
|
||||
}
|
||||
</style>
|
||||
|
||||
<outline-step-view display-action="">
|
||||
<span slot="step-title">[[localize('setup-do-title')]]</span>
|
||||
<span slot="step-description">[[localize('setup-do-description')]]</span>
|
||||
<span slot="step-action">
|
||||
<paper-button id="cancelButton" hidden\$="{{!showCancelButton}}" on-tap="handleCancelTapped">[[localize('cancel')]]</paper-button>
|
||||
</span>
|
||||
<div class="card">
|
||||
<outline-progress-spinner></outline-progress-spinner>
|
||||
<div class="servername">
|
||||
<p>{{serverName}}</p>
|
||||
</div>
|
||||
<paper-progress id="bar" class="transiting"></paper-progress>
|
||||
</div>
|
||||
</outline-step-view>
|
||||
`,
|
||||
|
||||
is: 'outline-server-progress-step',
|
||||
|
||||
properties: {
|
||||
serverName: String,
|
||||
showCancelButton: Boolean,
|
||||
updateIntervalId: Number,
|
||||
localize: Function,
|
||||
},
|
||||
|
||||
startAnimation: function() {
|
||||
if (this.updateIntervalId) {
|
||||
this.stop();
|
||||
}
|
||||
this.$.bar.value = 0;
|
||||
const expected = 90; // seconds
|
||||
const uncertainty = 30; // seconds
|
||||
const startTime = Date.now() / 1000;
|
||||
// For smoothness, this should match the CSS transition duration.
|
||||
const updateInterval = 1.0; // seconds.
|
||||
this.updateIntervalId = setInterval(() => {
|
||||
const elapsed = Date.now() / 1000 - startTime;
|
||||
// This heuristic happens to correspond to a Weibull distribution.
|
||||
const k = expected / uncertainty;
|
||||
const lambda = expected / Math.pow(Math.log(2), 1 / k);
|
||||
const conditionalMedian =
|
||||
lambda * Math.pow(Math.pow(elapsed / lambda, k) + Math.log(2), 1 / k);
|
||||
this.$.bar.value = 100 * (elapsed / conditionalMedian);
|
||||
}, updateInterval * 1000);
|
||||
},
|
||||
|
||||
stopAnimation: function() {
|
||||
if (!this.updateIntervalId) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this.updateIntervalId);
|
||||
this.updateIntervalId = null;
|
||||
},
|
||||
|
||||
handleCancelTapped: function() {
|
||||
this.fire('CancelServerCreationRequested');
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
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-progress/paper-progress';
|
||||
import '@polymer/paper-button/paper-button';
|
||||
import './outline-progress-spinner';
|
||||
import './outline-step-view';
|
||||
import {css, customElement, html, LitElement, property} from 'lit-element';
|
||||
import {COMMON_STYLES} from './cloud-install-styles.js';
|
||||
|
||||
@customElement('outline-server-progress-step')
|
||||
export class OutlineServerProgressStep extends LitElement {
|
||||
@property({type: String}) serverName: string;
|
||||
@property({type: Number}) progress = 0;
|
||||
@property({type: Function}) localize: Function;
|
||||
|
||||
static get styles() {
|
||||
return [COMMON_STYLES, css`
|
||||
:host {
|
||||
text-align: center;
|
||||
}
|
||||
.card {
|
||||
margin-top: 72px;
|
||||
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: 2px;
|
||||
color: var(--light-gray);
|
||||
background: var(--background-contrast-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.servername {
|
||||
margin: 24px 0 72px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.card p {
|
||||
font-size: 14px;
|
||||
color: var(--light-gray);
|
||||
}
|
||||
outline-progress-spinner {
|
||||
margin-top: 72px;
|
||||
}
|
||||
paper-button {
|
||||
width: 100%;
|
||||
border: 1px solid var(--light-gray);
|
||||
border-radius: 2px;
|
||||
color: var(--light-gray);
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<outline-step-view display-action="">
|
||||
<span slot="step-title">${this.localize('setup-do-title')}</span>
|
||||
<span slot="step-description">${this.localize('setup-do-description')}</span>
|
||||
<span slot="step-action">
|
||||
<paper-button id="cancelButton" on-tap="${this.handleCancelTapped}">
|
||||
${this.localize('cancel')}
|
||||
</paper-button>
|
||||
</span>
|
||||
<div class="card">
|
||||
<outline-progress-spinner></outline-progress-spinner>
|
||||
<div class="servername">
|
||||
<p>${this.serverName}</p>
|
||||
</div>
|
||||
<paper-progress id="bar" class="transiting" value="${100 * this.progress}"></paper-progress>
|
||||
</div>
|
||||
</outline-step-view>`;
|
||||
}
|
||||
|
||||
private handleCancelTapped() {
|
||||
this.dispatchEvent(new CustomEvent('CancelServerCreationRequested'));
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import './cloud-install-styles.js';
|
|||
import './outline-iconset.js';
|
||||
import './outline-help-bubble.js';
|
||||
import './outline-metrics-option-dialog.js';
|
||||
import './outline-server-progress-step.js';
|
||||
import './outline-server-progress-step';
|
||||
import './outline-server-settings.js';
|
||||
import './outline-share-dialog.js';
|
||||
import './outline-sort-span.js';
|
||||
|
|
@ -403,8 +403,8 @@ export class ServerView extends DirMixin(PolymerElement) {
|
|||
</style>
|
||||
|
||||
<div class="container">
|
||||
<iron-pages id="pages" attr-for-selected="id" selected="[[selectedPage]]" on-changed="_selectedPageChanged">
|
||||
<outline-server-progress-step id="progressView" server-name="[[serverName]]" localize="[[localize]]"></outline-server-progress-step>
|
||||
<iron-pages id="pages" attr-for-selected="id" selected="[[selectedPage]]">
|
||||
<outline-server-progress-step id="progressView" server-name="[[serverName]]" localize="[[localize]]" progress="[[installProgress]]"></outline-server-progress-step>
|
||||
<div id="unreachableView">${this.unreachableViewTemplate}</div>
|
||||
<div id="managementView">${this.managementViewTemplate}</div>
|
||||
</iron-pages>
|
||||
|
|
@ -635,6 +635,7 @@ export class ServerView extends DirMixin(PolymerElement) {
|
|||
supportsDefaultDataLimit: Boolean,
|
||||
showFeatureMetricsDisclaimer: Boolean,
|
||||
isServerManaged: Boolean,
|
||||
installProgress: Number,
|
||||
isServerReachable: Boolean,
|
||||
retryDisplayingServer: Function,
|
||||
myConnection: Object,
|
||||
|
|
@ -685,6 +686,7 @@ export class ServerView extends DirMixin(PolymerElement) {
|
|||
this.supportsDefaultDataLimit = false;
|
||||
this.showFeatureMetricsDisclaimer = false;
|
||||
this.isServerManaged = false;
|
||||
this.installProgress = 0;
|
||||
this.isServerReachable = false;
|
||||
/**
|
||||
* Callback for retrying to display an unreachable server.
|
||||
|
|
@ -973,14 +975,6 @@ export class ServerView extends DirMixin(PolymerElement) {
|
|||
}
|
||||
}
|
||||
|
||||
_selectedPageChanged() {
|
||||
if (this.selectedPage === 'progressView') {
|
||||
this.$.progressView.startAnimation();
|
||||
} else {
|
||||
this.$.progressView.stopAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
_selectedTabChanged() {
|
||||
if (this.selectedTab === 'settings') {
|
||||
this._closeAddAccessKeyHelpBubble();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue