Merge pull request #632 from Jigsaw-Code/fortuna-webpack-polymer

Convert to Polymer 3
This commit is contained in:
Vinicius Fortuna 2020-05-06 17:53:47 -04:00 • committed by GitHub
commit 3411be10df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 4861 additions and 4768 deletions

View file

@ -1,42 +0,0 @@
{
"name": "outline-launcher",
"description": "Launches Outline Shadowsocks servers on the cloud",
"main": "",
"authors": [
"Outline"
],
"license": "Apache",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"app-layout": "PolymerElements/app-layout#^2.0.4",
"app-localize-behavior": "PolymerElements/app-localize-behavior#^2.0.1",
"font-roboto": "PolymerElements/font-roboto-local#^1.1.0",
"iron-autogrow-textarea": "PolymerElements/iron-autogrow-textarea#^2.1.1",
"iron-icons": "PolymerElements/iron-icons#^2.0.1",
"iron-collapse": "PolymerElements/iron-collapse#^2.2.1",
"iron-fit-behavior": "PolymerElements/iron-fit-behavior#^2.1.0",
"iron-pages": "PolymerElements/iron-pages#^2.0.1",
"paper-button": "PolymerElements/paper-button#^2.0.0",
"paper-checkbox": "PolymerElements/paper-checkbox#^2.0.4",
"paper-dialog": "PolymerElements/paper-dialog#^2.0.1",
"paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^2.1.0",
"paper-dropdown-menu": "PolymerElements/paper-dropdown-menu#^2.0.2",
"paper-icon-button": "PolymerElements/paper-icon-button#^2.0.1",
"paper-input": "PolymerElements/paper-input#^2.1.0",
"paper-item": "PolymerElements/paper-item#^2.0.0",
"paper-listbox": "PolymerElements/paper-listbox#^2.0.0",
"paper-progress": "PolymerElements/paper-progress#^2.0.1",
"paper-tabs": "PolymerElements/paper-tabs#^2.1.1",
"paper-toast": "PolymerElements/paper-toast#^2.0.0",
"paper-tooltip": "PolymerElements/paper-tooltip#^2.0.0",
"web-animations-js": "web-animations/web-animations-js",
"webcomponentsjs": "webcomponents/webcomponentsjs#^v1.1.0"
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Outline Authors
// Copyright 2020 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.
@ -12,24 +12,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const gulp = require('gulp');
const posthtml = require('gulp-posthtml');
const posthtmlcss = require('posthtml-postcss');
const postcss = require('postcss');
const rtl = require('postcss-rtl');
const replace = require('gulp-replace');
// Generates inline CSS RTL mirroring rules for Polymer components.
module.exports = function(src, dest) {
console.log('Generating RTL CSS');
const plugins = [rtl()];
const options = {from: undefined, sync: true};
const filterType = /\/css$/;
return gulp.src([src])
.pipe(posthtml([posthtmlcss(plugins, options, filterType)]))
const CSS_PROCESSOR = postcss([rtl()]);
function generateRtlCss(css) {
return CSS_PROCESSOR.process(css).css
// Replace the generated selectors with Shadow DOM selectors for Polymer compatibility.
.pipe(replace('[dir=rtl]', ':host(:dir(rtl))'))
.pipe(replace('[dir=ltr]', ':host(:dir(ltr))'))
.replace(/\[dir=rtl\]/g, ':host(:dir(rtl))')
.replace(/\[dir=ltr\]/g, ':host(:dir(ltr))')
// rtlcss generates [dir] selectors for rules unaffected by directionality; ignore them.
.pipe(replace('[dir]', ''))
.pipe(gulp.dest(dest));
}
.replace(/\[dir\]/g, '');
}
// This is a Webpack loader that searches for <style> blocks and edits the CSS to support RTL
// in a Polymer element.
module.exports = function loader(content, map, meta) {
const callback = this.async();
const styleRe = RegExp(/(<style[^>]*>)(\s*[^<\s](.*\n)*\s*)(<\/style>)/gm);
const newContent =
content.replace(styleRe, (match, g1, g2, g3, g4) => `${g1}${generateRtlCss(g2)}${g4}`);
callback(null, newContent);
return;
}

View file

@ -53,6 +53,7 @@ if (sentryDsn) {
}
});
}
// To clearly identify app restarts in Sentry.
console.info(`Outline Manager is starting`);

View file

@ -22,18 +22,11 @@
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"
/>
<link rel="stylesheet" href="/ui_components/style.css" />
<script src="/bower_components/webcomponentsjs/webcomponents-loader.js"></script>
<link rel="import" href="/ui_components/app-root.html" />
<script src="main.js"></script>
<!-- TODO: Add Outline servers to connect-src on-demand (in addition to the DigitalOcean API). -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline' outline: data:; connect-src https: 'self'; frame-src https://s3.amazonaws.com/outline-vpn/ ss:"
/>
<title>Outline Manager</title>
</head>
<body>

View file

@ -17,7 +17,29 @@
}
},
"dependencies": {
"@polymer/app-layout": "^3.0.0",
"@polymer/app-localize-behavior": "^3.0.0",
"@polymer/font-roboto": "^3.0.0",
"@polymer/iron-autogrow-textarea": "^3.0.0",
"@polymer/iron-collapse": "^3.0.0",
"@polymer/iron-fit-behavior": "^3.0.0",
"@polymer/iron-icons": "^3.0.0",
"@polymer/iron-pages": "^3.0.0",
"@polymer/paper-button": "^3.0.0",
"@polymer/paper-checkbox": "^3.0.0",
"@polymer/paper-dialog": "^3.0.0",
"@polymer/paper-dialog-scrollable": "^3.0.0",
"@polymer/paper-dropdown-menu": "^3.0.0",
"@polymer/paper-icon-button": "^3.0.0",
"@polymer/paper-input": "^3.0.0",
"@polymer/paper-item": "^3.0.0",
"@polymer/paper-listbox": "^3.0.0",
"@polymer/paper-progress": "^3.0.0",
"@polymer/paper-tabs": "^3.0.0",
"@polymer/paper-toast": "^3.0.0",
"@polymer/paper-tooltip": "^3.0.0",
"@sentry/electron": "^1.3.0",
"@webcomponents/webcomponentsjs": "^2.0.0",
"body-parser": "^1.18.3",
"byte-size": "^6.2.0",
"clipboard-polyfill": "^2.4.6",
@ -28,11 +50,13 @@
"jsonic": "^0.3.1",
"node-forge": "^0.7.1",
"request": "^2.87.0",
"request-lite": "^2.40.1"
"request-lite": "^2.40.1",
"web-animations-js": "^2.3.1"
},
"comments": [
"@types/node can't have a major version update due to typing changes in node 13. Electron depends on ^12 and we get 13, which breaks us",
"Pin electron version (i.e. 7.0.1, not ~7.0.0) as a workaround for issue https://github.com/electron-userland/electron-builder/issues/4157"
"Pin electron version (i.e. 7.0.1, not ~7.0.0) as a workaround for issue https://github.com/electron-userland/electron-builder/issues/4157",
"The `resolutions` was created by polymer-modulizer. We should check if we still need them."
],
"devDependencies": {
"@types/node": "~12.0.0",
@ -41,8 +65,8 @@
"@types/request": "^2.47.1",
"@types/semver": "^5.5.0",
"bower": "^1.8.0",
"browserify": "^14.5.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.5.3",
"electron": "7.0.1",
"electron-builder": "~22.4.0",
"electron-icon-maker": "^0.0.4",
@ -50,20 +74,27 @@
"gulp": "^4.0.0",
"gulp-posthtml": "^3.0.4",
"gulp-replace": "^1.0.0",
"postcss-rtl": "^1.4.0",
"html-webpack-plugin": "^4.2.1",
"postcss": "^7.0.29",
"postcss-rtl": "^1.7.3",
"posthtml-postcss": "^0.2.6",
"prettier": "1.19.1",
"pretty-quick": "2.0.1",
"style-loader": "^1.2.1",
"ts-loader": "^7.0.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},
"scripts": {
"postinstall": "bower install"
},
"prettier": {
"printWidth": 120,
"bracketSpacing": false,
"trailingComma": "es5"
}
},
"resolutions": {
"inherits": "2.0.3",
"samsam": "1.1.3",
"supports-color": "3.1.2",
"type-detect": "1.0.0"
},
"license": "Apache"
}

View file

@ -1,935 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/app-layout/app-drawer/app-drawer.html" />
<link rel="import" href="../bower_components/app-layout/app-drawer-layout/app-drawer-layout.html" />
<link rel="import" href="../bower_components/app-layout/app-toolbar/app-toolbar.html" />
<link rel="import" href="../bower_components/app-localize-behavior/app-localize-behavior.html" />
<link rel="import" href="../bower_components/iron-icon/iron-icon.html" />
<link rel="import" href="../bower_components/iron-icons/iron-icons.html" />
<link rel="import" href="../bower_components/iron-pages/iron-pages.html" />
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html" />
<link rel="import" href="../bower_components/paper-toast/paper-toast.html" />
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html" />
<link rel="import" href="../bower_components/paper-listbox/paper-listbox.html" />
<link rel="import" href="../bower_components/paper-menu-button/paper-menu-button.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-about-dialog.html" />
<link rel="import" href="./outline-do-oauth-step.html" />
<link rel="import" href="./outline-feedback-dialog.html" />
<link rel="import" href="./outline-survey-dialog.html" />
<link rel="import" href="./outline-intro-step.html" />
<link rel="import" href="./outline-manual-server-entry.html" />
<link rel="import" href="./outline-modal-dialog.html" />
<link rel="import" href="./outline-region-picker-step.html" />
<link rel="import" href="./outline-server-progress-step.html" />
<link rel="import" href="./outline-server-view.html" />
<link rel="import" href="./outline-tos-view.html" />
<dom-module id="app-root">
<template>
<style include="cloud-install-styles"></style>
<style>
:host {
--side-bar-width: 48px;
}
.app-container {
margin: 0 auto;
}
/* Large display desktops */
@media (min-width: 1281px) {
.app-container {
max-width: 1200px;
}
}
/* Laptop, desktops */
@media (min-width: 1025px) and (max-width: 1280px) {
.app-container {
max-width: 920px;
}
}
#toast {
align-items: center;
display: flex;
justify-content: space-between;
padding: 24px;
max-width: 450px;
}
#toast paper-icon-button {
/* prevents the icon from resizing when there is a long message in the toast */
flex-shrink: 0;
padding: 0;
height: 20px;
width: 20px;
}
/* rtl:begin:ignore */
#appDrawer {
--app-drawer-content-container: {
color: var(--medium-gray);
background-color: var(--background-contrast-color);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: right;
}
}
/* rtl:end:ignore */
#appDrawer > * {
width: 100%;
}
.servers {
overflow-y: scroll;
flex: 1;
}
.servers::-webkit-scrollbar {
/* Do not display the scroll bar in the drawer or side bar. It is not styled on some platforms. */
display: none;
}
.servers-section {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.servers-section:last-child {
border-bottom: none;
}
.servers-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 24px;
line-height: 39px;
}
.servers-header > span {
flex: 1;
}
.do-overflow-menu {
padding: 24px;
color: var(--dark-gray);
text-align: left;
display: flex;
flex-direction: column;
}
.do-overflow-menu h4 {
margin-top: 0;
white-space: nowrap;
}
.do-overflow-menu .account-info {
display: flex;
align-items: center;
color: var(--faded-gray);
}
.do-overflow-menu .account-info img {
margin-right: 12px;
width: 24px;
}
.do-overflow-menu .sign-out-button {
margin-top: 24px;
align-self: flex-end;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
}
.servers-container {
padding-right: 12px; /* In case the server name is wraps. */
}
.server {
display: flex;
align-items: center;
width: 100%; /* For the side bar icons. */
margin: 18px 0;
padding: 6px 0;
cursor: pointer;
}
.server.selected {
color: white;
border-left: 2px solid var(--primary-green);
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.server.syncing {
cursor: wait;
}
.syncing .server-icon {
animation: rotate 1.75s ease-out infinite;
opacity: 0.5;
}
.server-icon {
width: 22px;
height: 22px;
/* Prevent the image from shrinking when the server title spans multiple lines */
min-width: 22px !important;
margin: 0 24px;
}
.selected > .server-icon {
/* Account for the selected border width to preserve center alignment. */
margin-left: 22px;
}
.add-server-section {
padding: 24px 0;
text-transform: uppercase;
color: var(--primary-green);
font-size: 12px;
letter-spacing: 0.6px;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
}
.add-server-section paper-icon-item {
margin-left: 24px;
}
.add-server-section paper-icon-item iron-icon {
margin-right: 24px;
}
#appDrawer > paper-listbox {
color: var(--medium-gray);
background-color: var(--background-contrast-color);
}
#appDrawer > paper-listbox > * {
display: block;
cursor: pointer;
padding-left: 24px;
font-size: 14px;
line-height: 40px;
outline: none;
}
#appDrawer a {
color: inherit;
}
#appDrawer a:focus {
outline: none;
}
.legal-links {
margin: 36px -6px 0;
}
.legal-links > * {
margin: 0 6px;
}
app-toolbar [main-title] img {
height: 16px;
margin-top: 8px;
}
.side-bar-margin {
margin-left: var(--side-bar-width);
}
/* rtl:begin:ignore */
#sideBar {
--app-drawer-width: var(--side-bar-width);
--app-drawer-content-container: {
background-color: var(--background-contrast-color);
}
}
/* rtl:end:ignore */
.side-bar-container {
height: 100%;
text-align: center;
color: var(--light-gray);
display: flex;
flex-direction: column;
}
.side-bar-container .servers {
flex: initial; /* Prevent the server list pushing down the add server button. */
}
.side-bar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.side-bar-section.menu {
min-height: 32px;
}
.side-bar-section.servers-section {
padding: 24px 0;
}
.side-bar-section .server {
justify-content: center;
margin: 12px auto;
padding: 2px 0;
}
.side-bar-section .provider-icon {
margin-bottom: 12px;
padding: 12px 0;
opacity: 0.54;
filter: grayscale(100%);
}
.side-bar-section.add-server-section {
flex: 1 0 24px;
border-bottom: none;
}
.side-bar-section > .server-icon {
margin: 0;
}
#getConnectedDialog {
height: 562px;
background: white;
}
#getConnectedDialog iframe {
padding: 0;
margin: 0;
width: 100%;
border: none;
border-bottom: 1px solid #ccc;
height: 500px;
}
#getConnectedDialog .buttons {
margin-top: -5px; /* undo spacing added after iframe */
}
</style>
<outline-tos-view
id="tosView"
has-accepted-terms-of-service="{{userAcceptedTos}}"
hidden$="{{hasAcceptedTos}}"
localize="[[localize]]"
></outline-tos-view>
<div hidden$="{{!hasAcceptedTos}}">
<!-- This responsive width sets the minimum layout area to 648px. -->
<app-drawer-layout
id="drawerLayout"
responsive-width="886px"
on-narrow-changed="_computeShouldShowSideBar"
class$="[[sideBarMarginClass]]"
>
<app-drawer id="appDrawer" slot="drawer" on-opened-changed="_computeShouldShowSideBar">
<app-toolbar class="toolbar" hidden$="[[shouldShowSideBar]]">
<paper-icon-button icon="menu" on-click="_toggleAppDrawer"></paper-icon-button>
<div main-title><img src="images/outline-manager-logo.svg" /></div>
</app-toolbar>
<!-- Servers section -->
<div class="servers">
<!-- DigitalOcean servers -->
<div class="servers-section" hidden$="{{!isSignedInToDigitalOcean}}">
<div class="servers-header">
<span>[[localize('servers-digitalocean')]]</span>
<paper-menu-button
horizontal-align="left"
class=""
close-on-activate
no-animations
dynamic-align
no-overlap
>
<paper-icon-button icon="more-vert" slot="dropdown-trigger"></paper-icon-button>
<div class="do-overflow-menu" slot="dropdown-content">
<h4>[[localize('digitalocean-disconnect-account')]]</h4>
<div class="account-info"><img src="images/digital_ocean_logo.svg" />{{adminEmail}}</div>
<div class="sign-out-button" on-tap="signOutTapped">[[localize('digitalocean-disconnect')]]</div>
</div>
</paper-menu-button>
</div>
<div class="servers-container">
<template
is="dom-repeat"
items="{{serverList}}"
as="server"
filter="_isServerManaged"
sort="_sortServersByName"
>
<div
class$="server {{_computeServerClasses(selectedServer, server)}}"
data-server$="[[server]]"
on-tap="_showServer"
>
<img class="server-icon" src$="images/{{_computeServerImage(selectedServer, server)}}" />
<span>{{server.name}}</span>
</div>
</template>
</div>
</div>
<!-- Manual servers -->
<div class="servers-section" hidden$="{{!hasManualServers}}">
<div class="servers-header">
<span>[[localize('servers-manual')]]</span>
</div>
<div class="servers-container">
<template
is="dom-repeat"
items="{{serverList}}"
as="server"
filter="_isServerManual"
sort="_sortServersByName"
>
<div
class$="server {{_computeServerClasses(selectedServer, server)}}"
data-server$="[[server]]"
on-tap="_showServer"
>
<img class="server-icon" src$="images/{{_computeServerImage(selectedServer, server)}}" />
<span>{{server.name}}</span>
</div>
</template>
</div>
</div>
</div>
<!-- Add server -->
<div class="add-server-section" on-tap="showIntro">
<paper-icon-item>
<iron-icon icon="add" slot="item-icon"></iron-icon>[[localize('servers-add')]]
</paper-icon-item>
</div>
<!-- Links section -->
<paper-listbox on-tap="maybeCloseDrawer">
<a href="https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection"
>[[localize('nav-data-collection')]]</a
>
<span on-tap="submitFeedbackTapped">[[localize('nav-feedback')]]</span>
<a href="https://s3.amazonaws.com/outline-vpn/index.html#/en/support/">[[localize('nav-help')]]</a>
<span on-tap="aboutTapped">[[localize('nav-about')]]</span>
<div class="legal-links">
<a href="https://www.google.com/policies/privacy/">[[localize('nav-privacy')]]</a>
<a href="https://s3.amazonaws.com/outline-vpn/static_downloads/Outline-Terms-of-Service.html"
>[[localize('nav-terms')]]</a
>
<span on-tap="showLicensesTapped">[[localize('nav-licenses')]]</span>
</div>
</paper-listbox>
</app-drawer>
<app-header-layout>
<div class="app-container">
<iron-pages attr-for-selected="id" selected="{{ currentPage }}">
<outline-intro-step
id="intro"
is-signed-in-to-digital-ocean="{{isSignedInToDigitalOcean}}"
digital-ocean-email="{{adminEmail}}"
localize="[[localize]]"
></outline-intro-step>
<outline-do-oauth-step id="digitalOceanOauth" localize="[[localize]]"></outline-do-oauth-step>
<outline-manual-server-entry id="manualEntry" localize="[[localize]]"></outline-manual-server-entry>
<outline-region-picker-step id="regionPicker" localize="[[localize]]"></outline-region-picker-step>
<outline-server-progress-step
id="serverProgressStep"
localize="[[localize]]"
></outline-server-progress-step>
<div id="serverView">
<template is="dom-repeat" items="{{serverList}}" as="server">
<outline-server-view
id="serverView-{{_base64Encode(server.id)}}"
localize="[[localize]]"
hidden$="{{!_isServerSelected(selectedServer, server)}}"
></outline-server-view>
</template>
</div>
</iron-pages>
</div>
</app-header-layout>
</app-drawer-layout>
<!-- Side bar -->
<app-drawer id="sideBar" opened$="[[shouldShowSideBar]]" persistent>
<div class="side-bar-container">
<div class="side-bar-section menu">
<paper-icon-button icon="menu" on-click="_toggleAppDrawer"></paper-icon-button>
</div>
<div class="servers">
<!-- DigitalOcean servers -->
<div class="side-bar-section servers-section" hidden$="{{!isSignedInToDigitalOcean}}">
<img class="provider-icon" src="images/do_white_logo.svg" />
<template
is="dom-repeat"
items="{{serverList}}"
as="server"
filter="_isServerManaged"
sort="_sortServersByName"
>
<div
class$="server {{_computeServerClasses(selectedServer, server)}}"
data-server$="[[server]]"
on-tap="_showServer"
>
<img class="server-icon" src$="images/{{_computeServerImage(selectedServer, server)}}" />
</div>
</template>
</div>
<!-- Manual servers -->
<div class="side-bar-section servers-section" hidden$="{{!hasManualServers}}">
<img class="provider-icon" src="images/cloud.svg" />
<template
is="dom-repeat"
items="{{serverList}}"
as="server"
filter="_isServerManual"
sort="_sortServersByName"
>
<div
class$="server {{_computeServerClasses(selectedServer, server)}}"
data-server$="[[server]]"
on-tap="_showServer"
>
<img class="server-icon" src$="images/{{_computeServerImage(selectedServer, server)}}" />
</div>
</template>
</div>
</div>
<div class="side-bar-section add-server-section" on-tap="showIntro">
<paper-icon-item>
<iron-icon icon="add" slot="item-icon"></iron-icon>
</paper-icon-item>
</div>
</div>
</app-drawer>
<paper-toast id="toast"
><paper-icon-button icon="icons:close" on-tap="closeError"></paper-icon-button
></paper-toast>
<!-- Modal dialogs must be outside the app container; otherwise the backdrop covers them. -->
<outline-survey-dialog id="surveyDialog" localize="[[localize]]"></outline-survey-dialog>
<outline-feedback-dialog id="feedbackDialog" localize="[[localize]]"></outline-feedback-dialog>
<outline-about-dialog
id="aboutDialog"
outline-version="[[outlineVersion]]"
localize="[[localize]]"
></outline-about-dialog>
<outline-modal-dialog id="modalDialog"></outline-modal-dialog>
<outline-share-dialog id="shareDialog" localize="[[localize]]"></outline-share-dialog>
<outline-metrics-option-dialog id="metricsDialog" localize="[[localize]]"></outline-metrics-option-dialog>
<paper-dialog id="getConnectedDialog" modal>
<!-- iframe gets inserted here once we are given the invite URL. -->
<div class="buttons">
<paper-button on-tap="closeGetConnectedDialog" autofocus>[[localize('close')]]</paper-button>
</div>
</paper-dialog>
<paper-dialog id="licenses" modal restoreFocusOnClose>
<paper-dialog-scrollable>
<code id="licensesText">
[[localize('error-licenses')]]
</code>
</paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-dismiss autofocus>[[localize('close')]]</paper-button>
</div>
</paper-dialog>
</div>
</template>
<script>
const TOS_ACK_LOCAL_STORAGE_KEY = "tos-ack";
class AppRoot extends Polymer.mixinBehaviors([Polymer.AppLocalizeBehavior], Polymer.Element) {
static get is() {
return "app-root";
}
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,
},
hasManualServers: {
type: Boolean,
computed: "_computeHasManualServers(serverList.*)",
},
adminEmail: {
type: String,
value: "",
},
isSignedInToDigitalOcean: {
type: Boolean,
computed: "_computeIsSignedInToDigitalOcean(adminEmail)",
},
outlineVersion: String,
userAcceptedTos: {
type: Boolean,
// Get notified when the user clicks the OK button in the ToS view
observer: "_userAcceptedTosChanged",
},
hasAcceptedTos: {
type: Boolean,
computed: "_computeHasAcceptedTermsOfService(userAcceptedTos)",
},
currentPage: {
type: String,
value: "intro",
},
shouldShowSideBar: {
type: Boolean,
value: false,
},
sideBarMarginClass: {
type: String,
computed: "_computeSideBarMarginClass(shouldShowSideBar)",
},
};
}
constructor() {
super();
this.addEventListener("RegionSelected", this.handleRegionSelected);
this.addEventListener("SetUpGenericCloudProviderRequested", this.handleSetUpGenericCloudProviderRequested);
this.addEventListener("SetUpAwsRequested", this.handleSetUpAwsRequested);
this.addEventListener("SetUpGcpRequested", this.handleSetUpGcpRequested);
this.addEventListener("ManualServerEntryCancelled", this.handleManualCancelled);
}
setLanguage(newLanguage, langDir) {
const messagesUrl = `/messages/${newLanguage}.json`;
this.loadResources(messagesUrl, newLanguage);
if (langDir === "rtl") {
this.$.appDrawer.align = "right";
this.$.sideBar.align = "right";
}
this.language = newLanguage;
}
showIntro() {
this.maybeCloseDrawer();
this.selectedServer = undefined;
this.currentPage = "intro";
}
getDigitalOceanOauthFlow(onCancel) {
const oauthFlow = this.$.digitalOceanOauth;
oauthFlow.onCancel = onCancel;
return oauthFlow;
}
showDigitalOceanOauthFlow() {
this.currentPage = "digitalOceanOauth";
}
getAndShowDigitalOceanOauthFlow(onCancel) {
this.currentPage = "digitalOceanOauth";
const oauthFlow = this.getDigitalOceanOauthFlow(onCancel);
oauthFlow.showConnectAccount();
return oauthFlow;
}
getAndShowRegionPicker() {
this.currentPage = "regionPicker";
this.$.regionPicker.init();
return this.$.regionPicker;
}
getManualServerEntry() {
return this.$.manualEntry;
}
showProgress(serverName, showCancelButton) {
this.currentPage = "serverProgressStep";
this.$.serverProgressStep.serverName = serverName;
this.$.serverProgressStep.showCancelButton = showCancelButton;
this.$.serverProgressStep.start();
}
showServerView() {
this.$.serverProgressStep.stop();
this.currentPage = "serverView";
}
getServerView(displayServerId) {
if (!displayServerId) {
return null;
}
const selectedServerId = this._base64Encode(displayServerId);
return this.$.serverView.querySelector(`#serverView-${selectedServerId}`);
}
handleRegionSelected(e) {
this.fire("SetUpServerRequested", {
regionId: this.$.regionPicker.getSelectedRegionId(),
});
}
handleSetUpGenericCloudProviderRequested() {
this.handleManualServerSelected("generic");
}
handleSetUpAwsRequested() {
this.handleManualServerSelected("aws");
}
handleSetUpGcpRequested() {
this.handleManualServerSelected("gcp");
}
handleManualServerSelected(cloudProvider) {
this.$.manualEntry.clear();
this.$.manualEntry.cloudProvider = cloudProvider;
this.currentPage = "manualEntry";
}
handleManualCancelled() {
this.currentPage = "intro";
}
showError(errorMsg) {
this.showToast(errorMsg, Infinity);
}
showNotification(message, durationMs = 3000) {
this.showToast(message, durationMs);
}
showToast(message, duration) {
const toast = this.$.toast;
toast.close();
// Defer in order to trigger the toast animation, otherwise the
// update happens in place.
setTimeout(() => {
toast.show({
text: message,
duration: duration,
noOverlap: true,
});
}, 0);
}
closeError() {
this.$.toast.close();
}
// cb is 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");
this.showModalDialog(dialogTitle, dialogText, [this.localize("cancel"), this.localize("retry")]).then(
clickedButtonIndex => {
cb(clickedButtonIndex === 1); // pass true if user clicked retry
}
);
}
getConfirmation(title, text, confirmButtonText, continueFunc) {
this.showModalDialog(title, text, [this.localize("cancel"), confirmButtonText]).then(clickedButtonIndex => {
if (clickedButtonIndex === 1) {
// user clicked to confirm.
continueFunc();
}
});
}
showManualServerError(errorTitle, errorText) {
this.showModalDialog(errorTitle, errorText, [this.localize("cancel"), this.localize("retry")]).then(
clickedButtonIndex => {
if (clickedButtonIndex == 1) {
this.$.manualEntry.retryTapped();
} else {
this.$.manualEntry.cancelTapped();
}
}
);
}
_computeIsSignedInToDigitalOcean(adminEmail) {
return Boolean(adminEmail);
}
_computeHasManualServers(serverList) {
return this.serverList.filter(server => !server.isManaged).length > 0;
}
_userAcceptedTosChanged(userAcceptedTos) {
if (userAcceptedTos) {
window.localStorage[TOS_ACK_LOCAL_STORAGE_KEY] = Date.now();
}
}
_computeHasAcceptedTermsOfService(userAcceptedTos) {
return userAcceptedTos || !!window.localStorage[TOS_ACK_LOCAL_STORAGE_KEY];
}
_toggleAppDrawer() {
const drawerNarrow = this.$.drawerLayout.narrow;
const forceNarrow = this.$.drawerLayout.forceNarrow;
if (drawerNarrow) {
if (forceNarrow) {
// The window width is below the responsive threshold. Do not force narrow mode.
this.$.drawerLayout.forceNarrow = false;
}
this.$.appDrawer.toggle();
} else {
// Forcing narrow mode when the window width is above the responsive threshold effectively
// collapses the drawer. Conversely, reverting force narrow expands the drawer.
this.$.drawerLayout.forceNarrow = !forceNarrow;
}
}
maybeCloseDrawer() {
if (this.$.drawerLayout.narrow || this.$.drawerLayout.forceNarrow) {
this.$.appDrawer.close();
}
}
submitFeedbackTapped() {
this.$.feedbackDialog.open();
}
aboutTapped() {
this.$.aboutDialog.open();
}
signOutTapped() {
this.fire("SignOutRequested");
}
openManualInstallFeedback(prepopulatedMessage) {
this.$.feedbackDialog.open(prepopulatedMessage, true);
}
openShareDialog(accessKey, s3Url) {
this.$.shareDialog.open(accessKey, s3Url);
}
openGetConnectedDialog(inviteUrl) {
const dialog = this.$.getConnectedDialog;
if (dialog.children.length > 1) {
return; // The iframe is already loading.
}
// Reset the iframe's state, by replacing it with a newly constructed
// iframe. Unfortunately the location.reload API does not work in our case due to
// this Chrome error:
// "Blocked a frame with origin "outline://web_app" from accessing a cross-origin frame."
const iframe = document.createElement("iframe");
iframe.onload = function() {
dialog.open();
};
iframe.src = inviteUrl;
dialog.insertBefore(iframe, dialog.children[0]);
}
closeGetConnectedDialog() {
const dialog = this.$.getConnectedDialog;
dialog.close();
const oldIframe = dialog.children[0];
dialog.removeChild(oldIframe);
}
showMetricsDialogForNewServer() {
this.$.metricsDialog.showMetricsOptInDialog();
}
// Returns a Promise which fulfills with the index of the button clicked.
showModalDialog(title, text, buttons) {
return this.$.modalDialog.open(title, text, buttons);
}
closeModalDialog(title, text, buttons) {
return this.$.modalDialog.close();
}
showLicensesTapped() {
var xhr = new XMLHttpRequest();
xhr.onload = () => {
this.$.licensesText.innerText = xhr.responseText;
this.$.licenses.open();
};
xhr.onerror = () => {
console.error("could not load license.txt");
};
xhr.open("GET", "/ui_components/licenses/licenses.txt", true);
xhr.send();
}
_computeShouldShowSideBar() {
const drawerNarrow = this.$.drawerLayout.narrow;
const drawerOpened = this.$.appDrawer.opened;
if (drawerOpened && drawerNarrow) {
this.shouldShowSideBar = false;
} else {
this.shouldShowSideBar = drawerNarrow;
}
}
_computeSideBarMarginClass(shouldShowSideBar) {
return shouldShowSideBar ? "side-bar-margin" : "";
}
_isServerManaged(server) {
return server.isManaged;
}
_isServerManual(server) {
return !server.isManaged;
}
_sortServersByName(a, b) {
const aName = a.name.toUpperCase();
const bName = b.name.toUpperCase();
if (aName < bName) {
return -1;
} else if (aName > bName) {
return 1;
}
return 0;
}
_computeServerClasses(selectedServer, server) {
let serverClasses = [];
if (this._isServerSelected(selectedServer, server)) {
serverClasses.push("selected");
}
if (!server.isSynced) {
serverClasses.push("syncing");
}
return serverClasses.join(" ");
}
_computeServerImage(selectedServer, server) {
if (this._isServerSelected(selectedServer, server)) {
return "server-icon-selected.png";
}
return "server-icon.png";
}
_isServerSelected(selectedServer, server) {
return !!selectedServer && selectedServer.id === server.id;
}
_showServer(event) {
const server = event.model.server;
this.fire("ShowServerRequested", {displayServerId: server.id});
this.maybeCloseDrawer();
}
// Wrapper to encode a string in base64. This is necessary to set the server view IDs to
// the display server IDs, which are URLs, so they can be used with selector methods. The IDs
// are never decoded.
_base64Encode(s) {
return btoa(s).replace(/=/g, "");
}
}
customElements.define(AppRoot.is, AppRoot);
</script>
</dom-module>

View file

@ -1,243 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<!-- Polymer style module to share styles between steps
https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules -->
<dom-module id="cloud-install-styles">
<template>
<style>
:host {
--primary-green: #00bfa5;
--background-color: #263238;
--background-contrast-color: #2e3a3f;
--light-gray: rgba(255, 255, 255, 0.87);
--medium-gray: rgba(255, 255, 255, 0.54);
--dark-gray: rgba(0, 0, 0, 0.87);
--border-color: rgba(255, 255, 255, 0.12);
}
#title {
font-weight: 100;
color: rgba(0, 0, 0, 0.87);
font-size: 24px;
line-height: 1.5em;
display: inline-block;
-webkit-margin-before: 1.25em;
-webkit-margin-after: 0em;
-webkit-margin-start: 0px;
-webkit-margin-end: 0px;
}
paper-button {
font-size: 14px;
padding: 1em 3em 1em 3em;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
letter-spacing: 0.05em;
border-radius: 2px;
}
paper-button.primary {
transition: 0.5s;
color: white;
background-color: var(--background-color);
width: 56%;
}
paper-button.secondary {
background-color: none;
}
paper-button[disabled] {
background-color: rgba(0, 0, 0, 0.25);
color: var(--medium-gray);
}
paper-progress {
background: var(--background-contrast-color);
--paper-progress-active-color: var(--primary-green);
--paper-progress-container-color: var(--background-contrast-color);
--paper-progress-transition-duration: 1s;
--paper-progress-transition-timing-function: linear;
width: 100%;
}
hr {
margin: 2em 0 2em 0;
}
paper-dropdown-menu {
--paper-input-container-underline: {
color: var(--primary-green);
}
}
paper-input,
paper-textarea,
paper-dropdown-menu {
--paper-input-container-focus-color: var(--primary-green);
}
app-toolbar {
color: #eee;
font-size: 12px;
font-weight: 300;
letter-spacing: 0.02em;
color: var(--light-gray);
padding: 0;
min-height: 60px;
border-bottom: 1px solid var(--border-color);
}
app-toolbar paper-icon-button {
margin-left: 16px;
margin-right: 26px;
}
app-toolbar h2 {
color: #e0e2e3;
font-size: 19px;
font-weight: 400;
}
p {
color: rgba(0, 0, 0, 0.54);
margin-top: 12px;
margin-bottom: 24px;
}
paper-dialog {
max-width: 540px;
min-width: 480px;
margin: 56px 32px 32px 32px;
}
paper-dialog-scrollable {
width: 100%;
margin: 0;
}
paper-dialog-scrollable img {
padding-top: 24px;
padding-bottom: 24px;
}
paper-dialog .dialogBanner {
background-color: #eceff1;
text-align: center;
margin-top: 0;
padding-top: 50px;
height: 150px;
}
paper-dialog .dialogBanner img {
height: 150px;
}
paper-tooltip {
--paper-tooltip-text-color: #000;
--paper-tooltip-background: #fff;
--paper-tooltip-duration-in: 100;
--paper-tooltip-duration-out: 100;
}
.instructions-list {
margin: auto;
padding-right: 42px;
padding-left: 42px;
max-width: 400px;
}
.instructions-list ol {
margin: 0 0 0 0;
padding: 0 24px 0 56px;
text-align: center !important;
}
.instructions-list li {
padding: 0 12px 24px 20px;
text-align: left;
}
.instructions-list li:before {
padding: 0 0 30px 20px;
}
.instructions-list paper-button {
margin-top: 20px;
}
.instructions-list a {
text-decoration: underline;
color: rgba(0, 0, 0, 0.87);
font-weight: 500;
}
.instructions-list p {
color: rgba(0, 0, 0, 0.54);
}
.instructions-list img {
padding: 32px 0px 42px 0;
margin: auto;
}
.walkthrough {
background: #ffffff;
box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.12);
top: 0px;
display: block;
}
.walkthrough h3 {
font-size: 14px;
line-height: 24px;
color: rgba(0, 0, 0, 0.87);
max-width: 70%;
margin: auto;
padding: 0px 0px 24px 0px;
font-weight: normal;
}
a {
text-decoration: none;
color: var(--primary-green);
}
/*
Helps make sure elements using both iron-flex-layout and the hidden
attribute actually hide:
- https://github.com/Polymer/polymer/issues/3711
- https://github.com/PolymerElements/iron-flex-layout/issues/86
*/
[hidden] {
display: none !important;
}
.card-section {
display: flex;
padding: 24px;
background: var(--background-contrast-color);
border-radius: 2px;
}
/* Mirror progress indicators for RTL languages */
:host(:dir(rtl)) paper-progress {
transform: scaleX(-1);
}
</style>
</template>
</dom-module>

View file

@ -1,249 +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.
-->
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" />
<link rel="import" href="../bower_components/paper-item/paper-item.html" />
<link rel="import" href="../bower_components/paper-listbox/paper-listbox.html" />
<link rel="import" href="../bower_components/paper-input/paper-input.html" />
<link rel="import" href="../bower_components/paper-input/paper-textarea.html" />
<!-- this is needed to fix the "KeyframeEffect is not defined"
see https://github.com/PolymerElements/paper-swatch-picker/issues/36 -->
<link rel="import" href="../bower_components/web-animations-js/web-animations-next.min.html" />
<dom-module id="outline-feedback-dialog">
<template>
<style include="cloud-install-styles"></style>
<style>
:host {
margin: 0px;
}
#feedbackWrapper {
margin-top: 0;
}
/*
This element is tricky to size.
We need to constrain its height because paper-dialog doesn't handle things well when its
height exceeds that of the page. max-rows isn't a reliable way to constrain the element's
height because it does not consider line wrapping:
https://github.com/PolymerElements/paper-input/issues/158
By configuring a max-height and setting overflow-y:scroll on the paper-textarea we get its
autogrow behaviour, at the cost of always showing a vertical scrollbar (even when there is
just one line).
*/
#userFeedback {
max-height: 175px;
overflow-x: hidden;
overflow-y: scroll;
}
p a {
color: var(--primary-green);
}
p.disclaimer {
margin: 0;
font-size: 12px;
}
paper-dropdown-menu {
width: 100%;
}
paper-textarea {
--iron-autogrow-textarea: {
background: #eee;
}
}
#feedbackExplanation {
display: none;
}
.installationFailed #feedbackCategory {
display: none;
}
.installationFailed #userFeedback {
background-color: #eceff1;
margin-top: 1em;
}
.installationFailed #feedbackExplanation {
display: block;
}
</style>
<paper-dialog id="dialog" modal>
<h2>[[title]]</h2>
<div id="feedbackWrapper">
<p id="feedbackExplanation">[[feedbackExplanation]]</p>
<paper-dropdown-menu
id="feedbackCategory"
horizontal-align="left"
on-selected-item-changed="feedbackCategoryChanged"
>
<paper-listbox id="feedbackCategoryListbox" slot="dropdown-content" class="dropdown-content" selected="0">
<paper-item>[[localize('feedback-general')]]</paper-item>
<paper-item>[[localize('feedback-install')]]</paper-item>
<paper-item>[[localize('feedback-connection')]]</paper-item>
<paper-item>[[localize('feedback-connection-others')]]</paper-item>
<paper-item>[[localize('feedback-management')]]</paper-item>
<paper-item>[[localize('feedback-suggestion')]]</paper-item>
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
id="cloudProvider"
horizontal-align="left"
placeholder="[[localize('feedback-cloud-provider')]]"
hidden$="[[!shouldShowCloudProvider]]"
error-message="[[localize('feedback-cloud-provider-error')]]"
>
<paper-listbox id="cloudProviderListbox" slot="dropdown-content" class="dropdown-content">
<paper-item>DigitalOcean</paper-item>
<paper-item>Amazon Web Services</paper-item>
<paper-item>Google Cloud Platform</paper-item>
<paper-item>[[localize('feedback-other')]]</paper-item>
</paper-listbox>
</paper-dropdown-menu>
<paper-input
id="userEmail"
type="text"
placeholder="[[localize('feedback-email')]]"
on-value-changed="userEmailValueChanged"
></paper-input>
<p class="disclaimer" hidden$="[[!shouldShowLanguageDisclaimer]]">[[localize('feedback-disclaimer')]]</p>
<paper-textarea
id="userFeedback"
type="text"
label="[[localize('feedback-label')]]"
always-float-label
rows="4"
error-message="[[localize('feedback-error')]]"
on-value-changed="userFeedbackValueChanged"
></paper-textarea>
<p
inner-h-t-m-l="[[localize('feedback-privacy', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"
></p>
</div>
<!-- end of #feedbackWrapper -->
<p class="buttons">
<paper-button dialog-dismiss>[[localize('cancel')]]</paper-button>
<paper-button autofocus on-tap="submitTappedHandler">[[localize('feedback-submit')]]</paper-button>
</p>
</paper-dialog>
</template>
<script>
Polymer({
is: "outline-feedback-dialog",
properties: {
title: String,
feedbackExplanation: String,
feedbackCategories: {
type: Object,
readonly: true,
value: {
// Maps a category to its `feedbackCategoryListbox` item index.
GENERAL: 0,
INSTALLATION: 1,
CONNECTION: 2,
CONNECTION_OTHERS: 3,
MANAGEMENT: 4,
SUGGESTIONS: 5,
},
},
hasEnteredEmail: {
type: Boolean,
value: false,
},
shouldShowCloudProvider: {
type: Boolean,
value: false,
},
shouldShowLanguageDisclaimer: {
type: Boolean,
computed: "_computeShouldShowLanguageDisclaimer(hasEnteredEmail)",
},
localize: {
type: Function,
readonly: true,
},
},
open: function(prepopulatedMessage, showInstallationFailed) {
// Clear all fields, in case feedback had already been entered.
if (showInstallationFailed) {
this.title = this.localize("feedback-title-install");
this.feedbackExplanation = this.localize("feedback-explanation-install");
this.$.dialog.classList.add("installationFailed");
this.$.feedbackCategoryListbox.selected = this.feedbackCategories.INSTALLATION;
} else {
this.title = this.localize("feedback-title-generic");
this.feedbackExplanation = "";
this.$.dialog.classList.remove("installationFailed");
// TODO(alalama): figure out why the localized category doesn't get displayed the first
// time opening the dialog.
this.$.feedbackCategoryListbox.selected = this.feedbackCategories.GENERAL;
}
this.$.userFeedback.invalid = false;
this.$.userFeedback.value = prepopulatedMessage || "";
this.$.userEmail.value = "";
this.$.cloudProviderListbox.selected = undefined;
this.$.dialog.open();
},
submitTappedHandler: function() {
// Verify that userFeedback is entered.
if (!this.$.userFeedback.value) {
this.$.userFeedback.invalid = true;
return;
}
const data = {
feedbackCategory: this.$.feedbackCategory.selectedItemLabel,
userFeedback: this.$.userFeedback.value,
userEmail: this.$.userEmail.value,
};
const selectedCloudProvider = this.$.cloudProvider.selectedItemLabel;
if (this.shouldShowCloudProvider && !!selectedCloudProvider) {
data["cloudProvider"] = selectedCloudProvider;
}
this.fire("SubmitFeedback", data);
this.$.dialog.close();
},
userEmailValueChanged: function() {
this.hasEnteredEmail = !!this.$.userEmail.value;
},
feedbackCategoryChanged: function() {
const selectedCategory = this.$.feedbackCategoryListbox.selected;
if (
selectedCategory === this.feedbackCategories.INSTALLATION ||
selectedCategory === this.feedbackCategories.CONNECTION ||
selectedCategory === this.feedbackCategories.CONNECTION_OTHERS
) {
this.shouldShowCloudProvider = true;
} else {
this.shouldShowCloudProvider = false;
}
this.$.dialog.notifyResize();
},
userFeedbackValueChanged: function() {
// Hides any error message when the user starts typing feedback.
this.$.userFeedback.invalid = false;
// Make the paper-dialog (vertically) re-center.
this.$.dialog.notifyResize();
},
// Returns whether the window's locale is English (i.e. EN, en-US) and the user has
// entered their email.
_computeShouldShowLanguageDisclaimer: function(hasEnteredEmail) {
return !window.navigator.language.match(/^en/i) && hasEnteredEmail;
},
});
</script>
</dom-module>

View file

@ -1,52 +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.
-->
<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html" />
<iron-iconset-svg name="outline-iconset" size="24">
<svg>
<defs>
<g id="outline">
<path
d="M10,18 C14.418278,18 18,14.418278 18,10 C18,5.581722 14.418278,2 10,2 C5.581722,2 2,5.581722 2,10 C2,14.418278 5.581722,18 10,18 Z M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z"
id="Oval-2-Copy-75"
></path>
<path
d="M10,18 C12.7614237,18 15,15.7614237 15,13 C15,10.2385763 12.7614237,8 10,8 C7.23857625,8 5,10.2385763 5,13 C5,15.7614237 7.23857625,18 10,18 Z M10,20 C6.13400675,20 3,16.8659932 3,13 C3,9.13400675 6.13400675,6 10,6 C13.8659932,6 17,9.13400675 17,13 C17,16.8659932 13.8659932,20 10,20 Z"
id="Oval-2-Copy-76"
></path>
<path
d="M10,18 C11.1045695,18 12,17.1045695 12,16 C12,14.8954305 11.1045695,14 10,14 C8.8954305,14 8,14.8954305 8,16 C8,17.1045695 8.8954305,18 10,18 Z M10,20 C7.790861,20 6,18.209139 6,16 C6,13.790861 7.790861,12 10,12 C12.209139,12 14,13.790861 14,16 C14,18.209139 12.209139,20 10,20 Z"
id="Oval-2-Copy-77"
></path>
</g>
<g id="devices">
<path
d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4V6zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1zm-1 9h-4v-7h4v7z"
/>
</g>
<g id="share">
<path
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"
/>
</g>
<g id="key">
<path
d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"
/>
</g>
</defs>
</svg>
</iron-iconset-svg>

View file

@ -1,64 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="./cloud-install-styles.html" />
<dom-module id="outline-metrics-option-dialog">
<template>
<style include="cloud-install-styles">
.link {
text-decoration: underline;
color: rgba(0, 0, 0, 0.54);
}
</style>
<paper-dialog id="metricsEnabledDialog" modal>
<div class="dialogBanner"><img src="images/metrics.png" /></div>
<h3>[[localize('metrics-title')]]</h3>
<p
inner-h-t-m-l="[[localize('metrics-description', 'openLink', '<a class=link href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"
></p>
<div class="buttons">
<paper-button dialog-dismiss on-tap="disableMetricsRequested">[[localize('metrics-skip')]]</paper-button>
<paper-button autofocus dialog-dismiss on-tap="enableMetricsRequested"
>[[localize('metrics-share')]]</paper-button
>
</div>
</paper-dialog>
</template>
<script>
Polymer({
is: "outline-metrics-option-dialog",
properties: {
localize: {
type: Function,
readonly: true,
},
},
showMetricsOptInDialog: function() {
this.$.metricsEnabledDialog.open();
},
enableMetricsRequested: function() {
this.fire("EnableMetricsRequested");
},
disableMetricsRequested: function() {
this.fire("DisableMetricsRequested");
},
});
</script>
</dom-module>

View file

@ -1,74 +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.
-->
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<dom-module id="outline-modal-dialog">
<template>
<style include="cloud-install-styles"></style>
<style>
:host {
margin: 0px;
}
h3 {
margin-bottom: 0;
}
paper-button {
color: var(--primary-green);
}
</style>
<paper-dialog id="dialog" modal>
<h3 hidden$="[[!title]]">[[title]]</h3>
<div>[[text]]</div>
<p class="buttons">
<template is="dom-repeat" items="{{buttons}}">
<paper-button dialog-dismiss on-tap="buttonTapped">[[item]]</paper-button>
</template>
</p>
</paper-dialog>
</template>
<script>
Polymer({
is: "outline-modal-dialog",
properties: {
title: String,
text: String,
buttons: String,
},
// Returns a Promise which fulfills with the index of the button clicked.
open: function(title, text, buttons) {
this.title = title;
this.text = text;
this.buttons = buttons;
this.$.dialog.open();
return new Promise((fulfill, reject) => {
this.fulfill = fulfill;
this.reject = reject;
});
},
close: function() {
this.$.dialog.close();
},
buttonTapped: function(event) {
if (!this.fulfill) {
console.error("outline-modal-dialog: this.fulfill not defined");
return;
}
this.fulfill(event.model.index);
},
});
</script>
</dom-module>

View file

@ -1,204 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="../bower_components/paper-progress/paper-progress.html" />
<link rel="import" href="../bower_components/iron-icon/iron-icon.html" />
<link rel="import" href="../bower_components/iron-icons/iron-icons.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-step-view.html" />
<dom-module id="outline-region-picker-step">
<template>
<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>
</template>
<script>
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({
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;
},
});
</script>
</dom-module>

View file

@ -1,120 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-progress/paper-progress.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-progress-spinner.html" />
<link rel="import" href="./outline-step-view.html" />
<link rel="import" href="./style.css" />
<dom-module id="outline-server-progress-step">
<template>
<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>
</template>
<script>
Polymer({
is: "outline-server-progress-step",
properties: {
serverName: String,
showCancelButton: Boolean,
updateIntervalId: Number,
localize: Function,
},
start: 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);
},
stop: function() {
if (!this.updateIntervalId) {
return;
}
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
},
handleCancelTapped: function() {
this.fire("CancelServerCreationRequested");
},
});
</script>
</dom-module>

View file

@ -1,69 +0,0 @@
<!--
Copyright 2020 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.
-->
<!-- outline-server-settings-styles
This file holds common styles for outline-server-settings and outline-validated-input
-->
<dom-module id="outline-server-settings-styles">
<template>
<style>
/* Skip processing these with postcss-rtl as it incorrectly parses the border-color
in the paper-input-container-underline-focus mixin.
https://github.com/vkalinichev/postcss-rtl/issues/50 */
/* rtl:begin:ignore */
paper-input {
/* Matches the max width of outline-validated-input */
max-width: 545px;
/* Removes extra padding added by children of paper-input */
margin-top: -8px;
/* Create space for error messages */
margin-bottom: 14px;
--paper-input-container-label-focus: {
color: var(--primary-green);
}
--paper-input-container-underline-focus: {
border-color: var(--primary-green);
}
--paper-input-container-label: {
font-size: 14px;
line-height: 22px;
}
--paper-input-container-color: var(--medium-gray);
--paper-input-container-input: {
color: #fff;
}
--paper-input-container-invalid-color: #f28b82;
}
/* rtl:end:ignore */
paper-input[readonly] {
--paper-input-container-underline: {
display: none;
}
--paper-input-container-underline-focus: {
display: none;
}
--paper-input-container-underline-disabled: {
display: none;
}
--paper-input-container-disabled: {
opacity: 1;
}
}
</style>
</template>
</dom-module>

View file

@ -1,454 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html" />
<link rel="import" href="../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" />
<link rel="import" href="../bower_components/paper-input/paper-input.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-server-settings-styles.html" />
<link rel="import" href="./outline-iconset.html" />
<link rel="import" href="./outline-validated-input.html" />
<dom-module id="outline-server-settings">
<template>
<style include="cloud-install-styles"></style>
<style include="outline-server-settings-styles"></style>
<style>
.content {
flex-grow: 1;
}
.setting {
padding: 24px;
align-items: flex-start;
}
.setting:not(:first-child) {
margin-top: 8px;
}
.setting-icon,
img.setting-icon {
margin-right: 24px;
color: #fff;
opacity: 0.87;
}
.setting > div {
width: 100%;
}
.setting h3 {
margin: 0 0 16px 0;
padding: 0;
color: #fff;
font-size: 16px;
width: 100%;
}
.setting p {
margin-bottom: 12px;
width: 60%;
color: var(--medium-gray);
}
#experiments p {
width: 80%;
}
#experiments .sub-section p {
width: 100%;
}
.sub-section {
background: var(--border-color);
padding: 16px;
margin: 24px 0;
display: flex;
align-items: center;
border-radius: 2px;
}
.sub-section iron-icon {
margin-right: 16px;
}
.selection-container {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.selection-container > .content {
flex: 4;
}
.selection-container > paper-dropdown-menu {
flex: 1;
}
.data-limits-input {
display: flex;
align-items: center;
}
.data-limits-input paper-input:not([readonly]) {
width: auto;
--paper-input-container: {
width: 120px;
}
}
.detail {
margin-top: 0px;
font-size: 12px;
}
paper-input:not([readonly]) {
width: 60%;
}
paper-dropdown-menu {
border: 1px solid var(--medium-gray);
border-radius: 4px;
max-width: 150px;
--paper-input-container: {
padding: 0 4px;
text-align: center;
}
--paper-input-container-input: {
color: var(--medium-gray);
font-size: 14px;
}
--paper-dropdown-menu-ripple: {
display: none;
}
--paper-input-container-underline: {
display: none;
}
--paper-input-container-underline-focus: {
display: none;
}
}
.data-limits-input paper-dropdown-menu {
border: none;
--paper-input-container: {
width: 64px;
}
}
paper-listbox paper-item {
font-size: 14px;
}
paper-listbox paper-item:hover {
cursor: pointer;
background-color: #eee;
}
#data-limits-container .selection-container h3 {
font-weight: normal;
margin-bottom: 8px;
}
#data-limits-container .selection-container p {
margin: 0 0 24px 0;
}
#data-limits-container .selection-container span {
display: block;
margin-top: 6px;
}
paper-checkbox {
/* We want the ink to be the color we're going to, not coming from */
--paper-checkbox-checked-color: var(--primary-green);
--paper-checkbox-checked-ink-color: var(--dark-gray);
--paper-checkbox-unchecked-color: var(--light-gray);
--paper-checkbox-unchecked-ink-color: var(--primary-green);
}
.selection-container paper-checkbox {
margin-right: 4px;
}
</style>
<div class="container">
<div class="content">
<!-- DO information -->
<div class="setting card-section" hidden$="[[!isServerManaged]]">
<img class="setting-icon digital-ocean-icon" src="images/do_white_logo.svg" />
<div>
<h3>DigitalOcean</h3>
<paper-input
readonly
value="[[serverLocation]]"
label="[[localize('settings-server-location')]]"
hidden$="[[!serverLocation]]"
always-float-label
maxlength="100"
></paper-input>
<paper-input
readonly
value="[[serverMonthlyCost]] USD"
label="[[localize('settings-server-cost')]]"
hidden$="[[!serverMonthlyCost]]"
always-float-label
maxlength="100"
></paper-input>
<paper-input
readonly
value="[[serverMonthlyTransferLimit]]"
label="[[localize('settings-transfer-limit')]]"
hidden$="[[!serverMonthlyTransferLimit]]"
always-float-label
maxlength="100"
></paper-input>
</div>
</div>
<div class="setting card-section">
<iron-icon class="setting-icon" icon="outline-iconset:outline"></iron-icon>
<div>
<h3>[[localize('settings-server-info')]]</h3>
<!-- TODO: consider making this an outline-validated-input -->
<paper-input
id="serverNameInput"
class="server-name"
value="{{serverName}}"
label="[[localize('settings-server-name')]]"
always-float-label
maxlength="100"
on-keydown="_handleNameInputKeyDown"
on-blur="_handleNameInputBlur"
></paper-input>
<p class="detail">[[localize('settings-server-rename')]]</p>
<outline-validated-input
editable="[[isAccessKeyPortEditable]]"
visible="[[serverPortForNewAccessKeys]]"
label="[[localize('settings-access-key-port')]]"
allowed-pattern="[0-9]{1,5}"
max-length="5"
value="[[serverPortForNewAccessKeys]]"
client-side-validator="[[_validatePort]]"
event="ChangePortForNewAccessKeysRequested"
localize="[[localize]]"
></outline-validated-input>
<outline-validated-input
editable="[[isHostnameEditable]]"
visible="[[serverHostname]]"
label="[[localize('settings-server-hostname')]]"
max-length="253"
value="[[serverHostname]]"
event="ChangeHostnameForAccessKeysRequested"
localize="[[localize]]"
></outline-validated-input>
<paper-input
readonly
value="[[serverManagementApiUrl]]"
label="[[localize('settings-server-api-url')]]"
hidden$="[[!serverManagementApiUrl]]"
always-float-label
maxlength="100"
></paper-input>
<paper-input
readonly
value="[[serverCreationDate]]"
label="[[localize('settings-server-creation')]]"
hidden$="[[!serverCreationDate]]"
always-float-label
maxlength="100"
></paper-input>
<paper-input
readonly
value="[[serverId]]"
label="[[localize('settings-server-id')]]"
hidden$="[[!serverId]]"
always-float-label
maxlength="100"
></paper-input>
<paper-input
readonly
value="[[serverVersion]]"
label="[[localize('settings-server-version')]]"
hidden$="[[!serverVersion]]"
always-float-label
maxlength="100"
></paper-input>
</div>
</div>
<!-- Experiments -->
<div id="experiments" class="setting card-section" hidden$="[[!shouldShowExperiments]]">
<iron-icon class="setting-icon" icon="icons:build"></iron-icon>
<div>
<h3>[[localize('experiments')]]</h3>
<p>[[localize('experiments-description')]]</p>
<div class="sub-section">
<iron-icon icon="icons:error-outline"></iron-icon>
<p
inner-h-t-m-l="[[localize('experiments-disclaimer', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"
></p>
</div>
<div id="data-limits-container" hidden$="[[!supportsAccessKeyDataLimit]]">
<div class="selection-container">
<div class="content">
<h3>[[localize('data-limits')]]</h3>
<p>
[[localize('data-limits-description', 'date', dataLimitsAvailabilityDate)]]<br />
<span
inner-h-t-m-l="[[localize('experiments-feedback', 'openLink', '<a href=https://docs.google.com/forms/d/e/1FAIpQLSfP7q9GnJCQyWMpFTLd9zwCm7cvUa-2NR8a8SznwMSbuRnrWg/viewform>', 'closeLink', '</a>')]]"
></span>
</p>
</div>
<!-- NOTE: The dropdown is not automatically sized to the button's width:
https://github.com/PolymerElements/paper-dropdown-menu/issues/229 -->
<paper-dropdown-menu no-label-float horizontal-align="left">
<paper-listbox
slot="dropdown-content"
selected="{{_computeDataLimitsEnabledName(isAccessKeyDataLimitEnabled)}}"
attr-for-selected="name"
on-selected-changed="_accessKeyDataLimitEnabledChanged"
>
<paper-item name="enabled">[[localize('enabled')]]</paper-item>
<paper-item name="disabled">[[localize('disabled')]]</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="data-limits-input" hidden$="[[!isAccessKeyDataLimitEnabled]]">
<paper-input
id="accessKeyDataLimitInput"
value="[[accessKeyDataLimit.value]]"
label="Data limit per key"
always-float-label
allowed-pattern="[0-9]+"
required
auto-validate
maxlength="9"
on-keydown="_handleAccessKeyDataLimitInputKeyDown"
on-blur="_requestSetAccessKeyDataLimit"
></paper-input>
<paper-dropdown-menu no-label-float>
<paper-listbox
id="accessKeyDataLimitUnits"
slot="dropdown-content"
selected="[[accessKeyDataLimit.unit]]"
attr-for-selected="name"
on-selected-changed="_requestSetAccessKeyDataLimit"
>
<paper-item name="MB">MB</paper-item>
<paper-item name="GB">GB</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
</div>
</div>
</div>
<!-- Metrics controls -->
<div class="setting card-section">
<iron-icon class="setting-icon" icon="editor:insert-chart"></iron-icon>
<div>
<div class="selection-container">
<paper-checkbox checked="{{metricsEnabled}}"></paper-checkbox>
<h3>[[localize('settings-metrics-header')]]</h3>
</div>
<p
inner-h-t-m-l="[[localize('metrics-description', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"
></p>
</div>
</div>
</div>
</div>
</template>
<script>
Polymer({
is: "outline-server-settings",
properties: {
isServerManaged: Boolean,
serverName: String,
metricsEnabled: {type: Boolean, observer: "_metricsEnabledChanged"},
// Initialize to null so we can use the hidden attribute, which does not work well with undefined values.
serverId: {type: String, value: null},
serverHostname: {type: String, value: null},
serverManagementApiUrl: {type: String, value: null},
serverPortForNewAccessKeys: {type: Number, value: null},
serverVersion: {type: String, value: null},
isAccessKeyPortEditable: {type: Boolean, value: false},
isAccessKeyDataLimitEnabled: {type: Boolean, notify: true},
accessKeyDataLimit: {type: Object, value: null}, // type: app.DisplayDataAmount
supportsAccessKeyDataLimit: {type: Boolean, value: false}, // Whether the server supports data limits.
dataLimitsAvailabilityDate: {type: String}, // Date by which the feature stops being an experiment.
isHostnameEditable: {type: Boolean, value: true},
serverCreationDate: {type: String, value: null},
serverLocation: {type: String, value: null},
serverMonthlyCost: {type: String, value: null},
serverMonthlyTransferLimit: {type: String, value: null},
localize: {type: Function, readonly: true},
shouldShowExperiments: {type: Boolean, computed: "_computeShouldShowExperiments(supportsAccessKeyDataLimit)"},
},
update: function(name, metricsEnabled) {
this.initialName = name;
this.name = name;
this.metricsEnabled = metricsEnabled;
},
_handleNameInputKeyDown: function(event) {
if (event.key === "Escape") {
this.serverName = this.initialName;
this.$.serverNameInput.blur();
} else if (event.key === "Enter") {
this.$.serverNameInput.blur();
}
},
_handleNameInputBlur: function(event) {
const newName = this.serverName;
if (!newName) {
this.serverName = this.initialName;
return;
}
// Fire signal if name has changed.
if (newName !== this.initialName) {
this.fire("ServerRenameRequested", {newName});
}
},
_metricsEnabledChanged: function(newMetricsEnabled, oldMetricsEnabled) {
if (oldMetricsEnabled === undefined || newMetricsEnabled === undefined) {
return;
}
// Fire signal if metrics changed.
if (newMetricsEnabled !== oldMetricsEnabled) {
const metricsSignal = newMetricsEnabled ? "EnableMetricsRequested" : "DisableMetricsRequested";
this.fire(metricsSignal);
}
},
_accessKeyDataLimitEnabledChanged: function(e) {
const wasDataLimitEnabled = this.isAccessKeyDataLimitEnabled;
const isDataLimitEnabled = e.detail.value === "enabled";
if (isDataLimitEnabled === undefined || wasDataLimitEnabled === undefined) {
return;
} else if (isDataLimitEnabled === wasDataLimitEnabled) {
return;
}
this.isAccessKeyDataLimitEnabled = isDataLimitEnabled;
if (isDataLimitEnabled) {
this._requestSetAccessKeyDataLimit();
} else {
this.fire("RemoveAccessKeyDataLimitRequested");
}
},
_handleAccessKeyDataLimitInputKeyDown: function(event) {
if (event.key === "Escape") {
this.$.accessKeyDataLimitInput.value = this.accessKeyDataLimit.value;
this.$.accessKeyDataLimitInput.blur();
} else if (event.key === "Enter") {
this.$.accessKeyDataLimitInput.blur();
}
},
_requestSetAccessKeyDataLimit: function() {
if (this.$.accessKeyDataLimitInput.invalid) {
return;
}
const value = Number(this.$.accessKeyDataLimitInput.value);
const unit = this.$.accessKeyDataLimitUnits.selected;
this.fire("SetAccessKeyDataLimitRequested", {limit: {value, unit}});
},
_computeDataLimitsEnabledName: function(isAccessKeyDataLimitEnabled) {
return isAccessKeyDataLimitEnabled ? "enabled" : "disabled";
},
_computeShouldShowExperiments: function(supportsAccessKeyDataLimit) {
return supportsAccessKeyDataLimit;
},
_validatePort: function(value) {
const port = Number(value);
const valid = !Number.isNaN(port) && port >= 1 && port <= 65535 && Number.isInteger(port);
return valid ? "" : this.localize("error-keys-port-bad-input");
},
});
</script>
</dom-module>

View file

@ -1,961 +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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="../bower_components/iron-icons/iron-icons.html" />
<link rel="import" href="../bower_components/iron-pages/iron-pages.html" />
<link rel="import" href="../bower_components/iron-icons/editor-icons.html" />
<link rel="import" href="../bower_components/iron-icons/social-icons.html" />
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html" />
<link rel="import" href="../bower_components/paper-item/paper-item.html" />
<link rel="import" href="../bower_components/paper-listbox/paper-listbox.html" />
<link rel="import" href="../bower_components/paper-menu-button/paper-menu-button.html" />
<link rel="import" href="../bower_components/paper-progress/paper-progress.html" />
<link rel="import" href="../bower_components/paper-tabs/paper-tabs.html" />
<link rel="import" href="../bower_components/paper-tooltip/paper-tooltip.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-iconset.html" />
<link rel="import" href="./outline-help-bubble.html" />
<link rel="import" href="./outline-metrics-option-dialog.html" />
<link rel="import" href="./outline-server-settings.html" />
<link rel="import" href="./outline-share-dialog.html" />
<script src="../node_modules.js"></script>
<dom-module id="outline-server-view">
<template>
<style include="cloud-install-styles"></style>
<style>
.container {
display: flex;
flex-direction: column;
padding: 24px;
color: var(--light-gray);
}
.tabs-container {
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--border-color);
}
.tabs-spacer {
flex: 2;
}
paper-tabs {
flex: 1;
--paper-tabs-selection-bar-color: var(--primary-green);
--paper-tab-ink: var(--primary-green);
--paper-tab-content-unselected {
color: var(--dark-gray);
}
}
div[name="connections"],
div[name="settings"],
.access-key-list {
margin-top: 24px;
}
.server-header {
display: flex;
flex-direction: column;
margin: 24px 0;
}
.server-name {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.server-name h3 {
font-size: 36px;
font-weight: 400;
color: #ffffff;
flex: 11;
margin: 0 0 6px 0;
}
.server-location,
.unreachable-server paper-button {
color: var(--medium-gray);
}
.unreachable-server {
flex-direction: column;
align-items: center;
margin-top: 24px;
padding: 72px 48px;
}
.unreachable-server p {
line-height: 22px;
max-width: 50ch;
text-align: center;
color: var(--medium-gray);
}
.unreachable-server .button-container {
padding: 24px 0;
}
.unreachable-server paper-button.try-again-btn {
color: var(--primary-green);
}
.server-img {
width: 142px;
height: 142px;
margin: 24px;
}
.stats-container {
display: flex;
flex-direction: row;
margin: 0 -8px;
}
.stats-card {
flex-direction: column;
flex: 1;
margin: 0 8px;
}
.stats {
margin: 30px 0 18px 0;
color: #fff;
white-space: nowrap;
}
.stats > * {
font-weight: 300;
display: inline-block;
margin: 0;
}
.stats h3 {
font-size: 48px;
}
.stats p,
.stats-card p {
margin: 0;
font-size: 14px;
font-weight: normal;
}
@media (max-width: 938px) {
/* Reduce the cards' size so they fit comfortably on small displays. */
.stats {
margin-top: 24px;
}
.stats h3 {
font-size: 42px;
}
.stats-card p {
font-size: 12px;
}
}
.transfer-stats .stats,
.transfer-stats .stats p {
color: var(--primary-green);
}
.stats-card p,
.stats-card iron-icon {
color: var(--medium-gray);
}
.access-key-list {
flex-direction: column;
padding: 24px 16px 24px 30px;
}
.access-key-row {
display: flex;
align-items: center;
justify-content: center;
margin: 15px 0;
}
.header-row {
font-size: 12px;
color: var(--medium-gray);
margin-bottom: 12px;
}
.header-row paper-button {
color: white;
height: 32px;
font-size: 13px;
padding: 0 28px 0px 28px;
background-color: #00bfa5;
margin: 0px 12px 0px 96px;
height: 36px;
}
.header-row-spacer {
/* 24px (share icon) + 40px (overflow menu) + 8px (margin) */
min-width: 72px;
}
.measurement-container {
display: flex;
flex: 3;
align-items: center;
}
.measurement-container paper-progress {
max-width: 72px;
margin: 0 24px 0 12px;
--paper-progress-height: 8px;
--paper-progress-active-color: var(--primary-green);
}
.measurement-container paper-progress.data-limits {
border: 1px solid var(--primary-green);
border-radius: 2px;
}
@media (max-width: 640px) {
.measurement-container paper-progress {
width: 48px;
}
}
paper-progress.data-limits:hover {
cursor: pointer;
}
.measurement {
/* Space the usage bars evenly */
min-width: 9ch;
/* We don't want numbers separated from their units */
white-space: nowrap;
font-size: 14px;
color: var(--medium-gray);
line-height: 24px;
}
.access-key-container {
display: flex;
flex: 4;
align-items: center;
}
.sort-icon {
/* Disable click events on the sorting icons, so that the event gets propagated to the
parent, which defines the dataset elements needed for displaying the icon. */
pointer-events: none;
}
#manager-access-key-description {
font-size: 12px;
font-weight: 400;
margin-top: 4px;
color: var(--medium-gray);
}
.manager-access-key-icon.access-key-icon {
height: 48px;
}
.access-key-icon {
width: 42px;
height: 42px;
margin-right: 18px;
}
.access-key-name {
display: flex;
flex-direction: column;
font-weight: 500;
}
input.access-key-name {
font-family: inherit;
font-size: inherit;
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid transparent;
border-radius: 2px;
padding: 4px 8px;
position: relative;
left: -8px; /* Align with padding */
background: var(--background-contrast-color);
color: var(--light-gray);
}
input.access-key-name::placeholder {
opacity: inherit;
color: inherit;
}
input.access-key-name:hover {
border-bottom-color: var(--border-color);
}
input.access-key-name:focus {
border-bottom-color: var(--primary-green);
border-radius: 0px;
outline: none;
font-weight: normal;
}
.overflow-menu {
display: flex;
justify-content: flex-end;
padding: 0px;
min-width: 40px;
margin-left: 8px;
color: var(--medium-gray);
}
.overflow-menu paper-item {
cursor: pointer;
}
paper-item {
font-size: 14px;
}
paper-listbox iron-icon {
margin-right: 10px;
width: 18px;
}
paper-dropdown {
box-shadow: 0px 0px 20px #999999;
}
#addAccessKeyButton {
background: var(--primary-green);
color: #fff;
border-radius: 50%;
}
.actions {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
text-align: end;
}
.connect-button,
.share-button {
color: white;
padding: 0;
margin: 0;
height: 24px;
width: 24px;
}
.add-new-key {
color: var(--primary-green);
cursor: pointer;
}
outline-help-bubble {
text-align: center;
}
outline-help-bubble h3 {
padding-bottom: 0;
font-weight: 500;
line-height: 28px;
font-size: 16px;
margin: 0px 0px 12px 0px;
}
outline-help-bubble img {
width: 76px;
height: auto;
margin: 12px 0px 24px 0px;
}
.digital-ocean-icon {
opacity: 0.54;
}
.flex-1 {
flex: 1;
}
/* Mirror icons */
:host(:dir(rtl)) iron-icon,
:host(:dir(rtl)) .share-button,
:host(:dir(rtl)) .access-key-icon {
transform: scaleX(-1);
}
</style>
<div class="container">
<div class="server-header">
<div class="server-name">
<h3>[[serverName]]</h3>
<paper-menu-button
horizontal-align="right"
class="overflow-menu flex-1"
hidden$="{{!isServerReachable}}"
close-on-activate
no-animations
dynamic-align
no-overlap
>
<paper-icon-button icon="more-vert" slot="dropdown-trigger"></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item hidden$="[[!isServerManaged]]" on-tap="destroyServer">
<iron-icon icon="icons:remove-circle-outline"></iron-icon>[[localize('server-destroy')]]
</paper-item>
<paper-item hidden$="[[isServerManaged]]" on-tap="removeServer">
<iron-icon icon="icons:remove-circle-outline"></iron-icon>[[localize('server-remove')]]
</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
<div class="server-location" hidden$="{{!isServerReachable}}">[[serverLocation]]</div>
</div>
<div class="tabs-container" hidden$="{{!isServerReachable}}">
<div class="tabs-spacer"></div>
<paper-tabs selected="{{selectedTab}}" attr-for-selected="name" noink>
<paper-tab name="connections">[[localize('server-connections')]]</paper-tab>
<paper-tab name="settings" id="settingsTab">[[localize('server-settings')]]</paper-tab>
</paper-tabs>
</div>
<!-- Unreachable server display -->
<div class="card-section unreachable-server" hidden$="{{isServerReachable}}">
<img class="server-img" src="images/server-unreachable.png" />
<h3>[[localize('server-unreachable')]]</h3>
<p></p>
<div>[[localize('server-unreachable-description')]]</div>
<span hidden$="{{isServerManaged}}">[[localize('server-unreachable-managed-description')]]</span>
<span hidden$="{{!isServerManaged}}">[[localize('server-unreachable-manual-description')]]</span>
<div class="button-container">
<paper-button on-tap="removeServer" hidden$="{{isServerManaged}}">[[localize('server-remove')]]</paper-button>
<paper-button on-tap="destroyServer" hidden$="{{!isServerManaged}}"
>[[localize('server-destroy')]]</paper-button
>
<paper-button on-tap="retryDisplayingServer" class="try-again-btn">[[localize('retry')]]</paper-button>
</div>
</div>
<iron-pages selected="{{selectedTab}}" attr-for-selected="name" hidden$="{{!isServerReachable}}">
<div name="connections">
<div class="stats-container">
<div class="stats-card transfer-stats card-section">
<iron-icon icon="icons:swap-horiz"></iron-icon>
<div class="stats">
<h3>[[_getFormattedTransferredValue(totalInboundBytes, '0')]]</h3>
<p>[[_getFormattedTransferredUnit(totalInboundBytes, 'B')]]</p>
</div>
<p>[[localize('server-data-transfer')]]</p>
</div>
<div hidden$="[[!isServerManaged]]" class="stats-card card-section">
<div>
<img class="digital-ocean-icon" src="images/do_white_logo.svg" />
</div>
<div class="stats">
<h3>[[managedServerUtilzationPercentage]]</h3>
<p>/[[_formatBytesTransferred(monthlyOutboundTransferBytes)]]</p>
</div>
<p>[[localize('server-data-used')]]</p>
</div>
<div class="stats-card card-section">
<iron-icon icon="outline-iconset:key"></iron-icon>
<div class="stats">
<h3>[[accessKeyRows.length]]</h3>
<p>[[localize('server-keys')]]</p>
</div>
<p>[[localize('server-access')]]</p>
</div>
</div>
<div class="access-key-list card-section">
<!-- header row -->
<div class="access-key-row header-row">
<span class="access-key-container" on-tap="_setSortByOrToggleDirection" data-sort-by="name">
[[localize('server-access-keys')]]
<iron-icon
class="sort-icon"
icon="arrow-drop-down"
hidden$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'name', -1)]]"
></iron-icon>
<iron-icon
class="sort-icon"
icon="arrow-drop-up"
hidden$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'name', 1)]]"
></iron-icon>
</span>
<span class="measurement-container" on-tap="_setSortByOrToggleDirection" data-sort-by="usage">
[[localize('server-usage')]]
<iron-icon
class="sort-icon"
icon="arrow-drop-down"
hidden$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'usage', -1)]]"
></iron-icon>
<iron-icon
class="sort-icon"
icon="arrow-drop-up"
hidden$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'usage', 1)]]"
></iron-icon>
</span>
<span class="flex-1 header-row-spacer"></span>
</div>
<!-- admin row -->
<div class="access-key-row" id="managerRow">
<span class="access-key-container">
<img class="manager-access-key-icon access-key-icon" src="images/manager-key-avatar.svg" />
<div class="access-key-name">
<div>[[localize('server-my-access-key')]]</div>
<div id="manager-access-key-description">[[localize('server-connect-devices')]]</div>
</div>
</span>
<span class="measurement-container">
<span class="measurement">[[_formatBytesTransferred(myConnection.transferredBytes, "...")]]</span>
<paper-progress
value="[[myConnection.relativeTraffic]]"
class$="[[_computePaperProgressClass(isAccessKeyDataLimitEnabled)]]"
></paper-progress>
<paper-tooltip animation-delay="0" offset="0" position="top" hidden$="[[!isAccessKeyDataLimitEnabled]]">
[[_getDataLimitsUsageString(myConnection)]]
</paper-tooltip>
</span>
<span class="actions">
<span class="flex-1">
<paper-icon-button
icon="outline-iconset:devices"
class="connect-button"
on-tap="_handleConnectPressed"
></paper-icon-button>
</span>
<span class="overflow-menu flex-1"></span>
</span>
</div>
<div id="accessKeysContainer">
<!-- rows for each access key -->
<template
is="dom-repeat"
items="{{accessKeyRows}}"
filter="isRegularConnection"
sort="{{_sortAccessKeys(accessKeySortBy, accessKeySortDirection)}}"
observe="name transferredBytes"
>
<!-- TODO(alalama): why is observe not responding to rename? -->
<div class="access-key-row">
<span class="access-key-container">
<img class="access-key-icon" src="images/key-avatar.svg" />
<input
type="text"
class="access-key-name"
id$="access-key-[[item.id]]"
placeholder="{{item.placeholderName}}"
value="[[item.name]]"
on-blur="_handleNameInputBlur"
on-keydown="_handleNameInputKeyDown"
/>
</span>
<span class="measurement-container">
<span class="measurement">[[_formatBytesTransferred(item.transferredBytes, "...")]]</span>
<paper-progress
value="[[item.relativeTraffic]]"
class$="[[_computePaperProgressClass(isAccessKeyDataLimitEnabled)]]"
></paper-progress>
<paper-tooltip
animation-delay="0"
offset="0"
position="top"
hidden$="[[!isAccessKeyDataLimitEnabled]]"
>
[[_getDataLimitsUsageString(item)]]
</paper-tooltip>
</span>
<span class="actions">
<span class="flex-1">
<paper-icon-button
icon="outline-iconset:share"
class="share-button"
on-tap="_handleShareCodePressed"
></paper-icon-button>
</span>
<span class="flex-1">
<paper-menu-button
horizontal-align="right"
class="overflow-menu"
close-on-activate
no-animations
no-overlap
dynamic-align
>
<paper-icon-button icon="more-vert" slot="dropdown-trigger"></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item on-tap="_handleRenameAccessKeyPressed">
<iron-icon icon="icons:create"></iron-icon>[[localize('server-access-key-rename')]]
</paper-item>
<paper-item on-tap="_handleRemoveAccessKeyPressed">
<iron-icon icon="icons:delete"></iron-icon>[[localize('remove')]]
</paper-item>
</paper-listbox>
</paper-menu-button>
</span>
</span>
</div>
</template>
</div>
<!-- add key button -->
<div class="access-key-row" id="addAccessKeyRow">
<span class="access-key-container">
<paper-icon-button
icon="icons:add"
on-tap="_handleAddAccessKeyPressed"
id="addAccessKeyButton"
class="access-key-icon"
></paper-icon-button>
<div class="add-new-key" on-tap="_handleAddAccessKeyPressed">[[localize('server-access-key-new')]]</div>
</span>
</div>
</div>
</div>
<div name="settings">
<outline-server-settings
id="serverSettings"
server-id="[[serverId]]"
server-hostname="[[serverHostname]]"
server-name="[[serverName]]"
server-version="[[serverVersion]]"
is-hostname-editable="[[isHostnameEditable]]"
server-management-api-url="[[serverManagementApiUrl]]"
server-port-for-new-access-keys="[[serverPortForNewAccessKeys]]"
is-access-key-port-editable="[[isAccessKeyPortEditable]]"
access-key-data-limit="{{accessKeyDataLimit}}"
is-access-key-data-limit-enabled="{{isAccessKeyDataLimitEnabled}}"
data-limits-availability-date="[[dataLimitsAvailabilityDate]]"
supports-access-key-data-limit="[[supportsAccessKeyDataLimit]]"
server-creation-date="[[serverCreationDate]]"
server-monthly-cost="[[monthlyCost]]"
server-monthly-transfer-limit="[[_formatBytesTransferred(monthlyOutboundTransferBytes)]]"
is-server-managed="[[isServerManaged]]"
server-location="[[serverLocation]]"
localize="[[localize]]"
>
</outline-server-settings>
</div>
</iron-pages>
</div>
<outline-help-bubble id="getConnectedHelpBubble" vertical-align="bottom" horizontal-align="right">
<img src="images/connect-tip-2x.png" />
<h3>[[localize('server-help-connection-title')]]</h3>
<p>[[localize('server-help-connection-description')]]</p>
<paper-button on-tap="closeGetConnectedHelpBubble">[[localize('server-help-connection-ok')]]</paper-button>
</outline-help-bubble>
<outline-help-bubble id="addAccessKeyHelpBubble" vertical-align="bottom" horizontal-align="left">
<img src="images/key-tip-2x.png" />
<h3>[[localize('server-help-access-key-title')]]</h3>
<p>[[localize('server-help-access-key-description')]]</p>
<paper-button on-tap="closeAddAccessKeyHelpBubble">[[localize('server-help-access-key-next')]]</paper-button>
</outline-help-bubble>
<outline-help-bubble id="dataLimitsHelpBubble" vertical-align="top" horizontal-align="right">
<h3>[[localize('data-limits-dialog-title')]]</h3>
<p>[[localize('data-limits-dialog-text')]]</p>
<paper-button on-tap="closeDataLimitsHelpBubble">[[localize('ok')]]</paper-button>
</outline-help-bubble>
</template>
<script>
const MY_CONNECTION_USER_ID = "0";
Polymer({
is: "outline-server-view",
created: function() {
this.byteSize = require("byte-size");
this.byteSize.defaultOptions({
units: "metric",
toStringFn: function() {
return `${this.value} ${this.unit}`;
},
});
},
properties: {
serverId: String,
serverName: String,
serverHostname: String,
serverVersion: String,
isHostnameEditable: {type: Boolean, value: false},
serverManagementApiUrl: String,
serverPortForNewAccessKeys: Number,
isAccessKeyPortEditable: {type: Boolean, value: false},
serverCreationDate: String,
serverLocation: String,
accessKeyDataLimit: {type: Object, value: null},
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.
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: []},
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",
},
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,
},
},
observers: [
"_accessKeysAddedOrRemoved(accessKeyRows.splices)",
"_myConnectionChanged(myConnection)",
"_selectedTabChanged(selectedTab)",
],
// Parameter `accessKey` has the format {
// id: string,
// placeholderName: string,
// name: string,
// accessUrl: string,
// transferredBytes: number;
// relativeTraffic: number;
// }
addAccessKey: function(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.$$(`#access-key-${accessKey.id}`);
input.select();
},
removeAccessKey: function(accessKeyId) {
for (let ui in this.accessKeyRows) {
if (this.accessKeyRows[ui].id === accessKeyId) {
this.splice("accessKeyRows", ui, 1);
return;
}
}
},
_handleAddAccessKeyPressed: function() {
this.fire("AddAccessKeyRequested");
this.$.addAccessKeyHelpBubble.hide();
},
_handleNameInputKeyDown: function(event) {
const input = event.target;
if (event.key === "Escape") {
const accessKey = event.model.item;
input.value = accessKey.name;
input.blur();
} else if (event.key === "Enter") {
input.blur();
}
},
_handleNameInputBlur: function(event) {
const input = event.target;
const accessKey = event.model.item;
const displayName = input.value;
if (displayName === accessKey.name) {
return;
}
input.disabled = true;
this.fire("RenameAccessKeyRequested", {
accessKeyId: accessKey.id,
newName: displayName,
entry: {
commitName: () => {
accessKey.name = displayName;
input.disabled = false;
},
revertName: () => {
input.value = accessKey.name;
input.disabled = false;
},
},
});
},
_handleRenameAccessKeyPressed: function(event) {
const input = this.$.accessKeysContainer.querySelectorAll(".access-key-row .access-key-container > input")[
event.model.index
];
// This needs to be deferred because the closing menu messes up with the focus.
window.setTimeout(() => {
input.focus();
}, 0);
},
_handleConnectPressed: function() {
this.$.getConnectedHelpBubble.hide();
this.fire("OpenGetConnectedDialogRequested", {accessKey: this.myConnection.accessUrl});
},
_handleShareCodePressed: function(event) {
const accessKey = event.model.item;
this.fire("OpenShareDialogRequested", {accessKey: accessKey.accessUrl});
},
_handleRemoveAccessKeyPressed: function(e) {
const accessKey = e.model.item;
this.fire("RemoveAccessKeyRequested", {accessKeyId: accessKey.id});
},
_encodeURIComponent: encodeURIComponent,
setServerTransferredData(totalBytes) {
this.totalInboundBytes = totalBytes;
},
updateAccessKeyRow(accessKeyId, fields) {
let newAccessKeyRow;
if (accessKeyId === MY_CONNECTION_USER_ID) {
newAccessKeyRow = Object.assign({}, this.get("myConnection"), fields);
this.set("myConnection", newAccessKeyRow);
}
for (let ui in this.accessKeyRows) {
if (this.accessKeyRows[ui].id === accessKeyId) {
newAccessKeyRow = Object.assign({}, this.get(["accessKeyRows", ui]), fields);
this.set(["accessKeyRows", ui], newAccessKeyRow);
return;
}
}
},
_formatBytesTransferred: function(numBytes, emptyValue = "") {
if (!numBytes) {
// numBytes may not be set for manual servers, or may be 0 for
// unused access keys.
return emptyValue;
}
// Show 0 decimals for < 1MB, 1 decimal for >= 1MB, 2 decimals for >= 1GB.
let numDecimals = 0;
if (numBytes >= 10 ** 9) {
numDecimals = 2;
} else if (numBytes >= 10 ** 6) {
numDecimals = 1;
}
return this.byteSize(numBytes, {precision: numDecimals}).toString();
},
_getFormattedTransferredUnit: function(numBytes, emptyValue = "") {
const formattedTransfer = this._formatBytesTransferred(numBytes);
if (!formattedTransfer) {
return emptyValue;
}
const units = formattedTransfer.match(/[a-zA-Z%]+/g);
if (!units || units.length < 0) {
return emptyValue;
}
return units.pop();
},
_getFormattedTransferredValue: function(numBytes, emptyValue = "") {
const formattedTransfer = this._formatBytesTransferred(numBytes);
if (!formattedTransfer) {
return emptyValue;
}
const value = parseFloat(formattedTransfer);
return !!value ? value : emptyValue;
},
_computeManagedServerUtilzationPercentage: function(numBytes, monthlyLimitBytes) {
let utilizationPercentage = 0;
if (monthlyLimitBytes && numBytes) {
utilizationPercentage = Math.round((numBytes / monthlyLimitBytes) * 100);
}
if (document.documentElement.dir === "rtl") {
return `%${utilizationPercentage}`;
}
return `${utilizationPercentage}%`;
},
_accessKeysAddedOrRemoved: function(changeRecord) {
// Check for myConnection and regular access keys.
let hasNonAdminAccessKeys = false;
for (let ui in this.accessKeyRows) {
if (this.accessKeyRows[ui].id === MY_CONNECTION_USER_ID) {
this.myConnection = this.accessKeyRows[ui];
} else {
hasNonAdminAccessKeys = true;
}
}
this.hasNonAdminAccessKeys = hasNonAdminAccessKeys;
},
_myConnectionChanged: function(myConnection) {
if (!myConnection) {
this.$.getConnectedHelpBubble.hide();
}
},
_selectedTabChanged(selectedTab) {
if (this.selectedTab === "settings") {
this.closeAddAccessKeyHelpBubble();
this.closeGetConnectedHelpBubble();
this.closeDataLimitsHelpBubble();
this.$.serverSettings.update(this.serverName, this.metricsEnabled);
}
},
// Help bubbles should be shown after this outline-server-view
// is on the screen (e.g. selected in iron-pages). If help bubbles
// are initialized before this point, setPosition will not work and
// they will appear in the top left of the view.
showGetConnectedHelpBubble: function() {
return this._showHelpBubble("getConnectedHelpBubble", "managerRow");
},
showAddAccessKeyHelpBubble: function() {
return this._showHelpBubble("addAccessKeyHelpBubble", "addAccessKeyRow", "down", "left");
},
showDataLimitsHelpBubble: function() {
return this._showHelpBubble("dataLimitsHelpBubble", "settingsTab", "up", "right");
},
closeAddAccessKeyHelpBubble: function() {
this.$.addAccessKeyHelpBubble.hide();
},
closeGetConnectedHelpBubble: function() {
this.$.getConnectedHelpBubble.hide();
},
closeDataLimitsHelpBubble: function() {
this.$.dataLimitsHelpBubble.hide();
},
_showHelpBubble: function(helpBubbleId, positionTargetId, arrowDirection = "down", arrowAlignment = "right") {
return new Promise(resolve => {
const helpBubble = this.$[helpBubbleId];
helpBubble.show(this.$[positionTargetId], arrowDirection, arrowAlignment);
helpBubble.addEventListener("outline-help-bubble-dismissed", resolve);
});
},
isRegularConnection: function(item) {
return item.id !== MY_CONNECTION_USER_ID;
},
_setSortByOrToggleDirection: function(e) {
const sortBy = e.target.dataset.sortBy;
if (this.accessKeySortBy !== sortBy) {
this.accessKeySortBy = sortBy;
this.accessKeySortDirection = 1; // Sort ascending initially.
return;
}
this.accessKeySortDirection *= -1; // Toggle sort direction.
},
_sortAccessKeys: function(accessKeySortBy, accessKeySortDirection) {
if (accessKeySortBy === "usage") {
return function(a, b) {
return (a.transferredBytes - b.transferredBytes) * this.accessKeySortDirection;
}.bind(this);
}
// Default to sorting by name.
return function(a, b) {
const nameA = a.name.toUpperCase() || a.placeholderName.toUpperCase();
const nameB = b.name.toUpperCase() || b.placeholderName.toUpperCase();
if (nameA < nameB) {
return -1 * this.accessKeySortDirection;
}
if (nameA > nameB) {
return 1 * this.accessKeySortDirection;
}
return 0;
}.bind(this);
},
_computeShouldShowSortIcon: function(
accessKeySortBy,
accessKeySortDirection,
elementSortBy,
elementSortDirection
) {
return accessKeySortBy === elementSortBy && accessKeySortDirection === elementSortDirection;
},
destroyServer: function() {
this.fire("DeleteServerRequested");
},
removeServer: function() {
this.fire("ForgetServerRequested");
},
_computePaperProgressClass: function(isAccessKeyDataLimitEnabled) {
return isAccessKeyDataLimitEnabled ? "data-limits" : "";
},
_getDataLimitsUsageString: function(accessKey) {
if (!this.accessKeyDataLimit) {
return "";
}
const used = this._formatBytesTransferred(accessKey.transferredBytes, "0");
const total = this.accessKeyDataLimit.value + " " + this.accessKeyDataLimit.unit;
return this.localize("data-limits-usage", "used", used, "total", total);
},
});
</script>
</dom-module>

View file

@ -1,208 +0,0 @@
<!--
Copyright 2020 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.
-->
<!-- outline-validated-input
This is an input, with a cancel and a save button, which performs client-side validation and has an
event-based hook for server-side evaluation.
Attributes:
* editable: Is this input editable? Default: false
* visible: Is this input visible? Default: false
* label: The label for the input. Default: null
* value: The initial value entered in the input. SHOULD be a valid value. Default: null
* allowed-pattern: Regex describing what inputs are allowed. Users will be prevented from
entering inputs which don't follow the pattern. Default: ".*"
* max-length: The number of characters allowed in the input. Default: Number.POSITIVE_INFINITY
* client-side-validator: A function which takes a string and returns either an empty string on success
or an error message on failure. This function will be called on every keystroke. Default: () => ""
* event: The name of the event fired when the save button is tapped. Passes the
current input value as "value" and the Polymer object as "ui". The handler for this event
MUST call ui.setSavedState() or ui.setErrorState(message) depending on its result. Default: null
-->
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-server-settings-styles.html" />
<dom-module id="outline-validated-input">
<template>
<style include="cloud-install-styles"></style>
<style include="outline-server-settings-styles"></style>
<style>
#container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 545px;
}
paper-input {
width: 60%;
}
paper-button {
height: 36px;
min-width: 83px;
text-align: center;
}
paper-button[disabled] {
color: rgba(255, 255, 255, 0.3);
}
#saveButton {
background: var(--primary-green);
color: #fff;
}
#cancelButton {
color: #fff;
}
#saveButton[disabled] {
background-color: rgba(255, 255, 255, 0.12);
}
#cancelButton[disabled] {
background-color: rgba(0, 0, 0, 0);
}
</style>
<div id="container" hidden$="[[!visible]]">
<!-- We use on-focus instead of on-tap to handle the case where text is selected but the tap event isn't fired. -->
<paper-input
id="input"
readonly$="[[!editable]]"
allowed-pattern="[[allowedPattern]]"
value="[[value]]"
label="[[label]]"
maxlength="[[maxLength]]"
on-focus="_enterEditingState"
on-keyup="_onKeyUp"
always-float-label
>
</paper-input>
<paper-button id="cancelButton" hidden$="[[!_showButtons]]" disabled$="[[!_enableButtons]]" on-tap="_cancel">
[[localize('cancel')]]
</paper-button>
<paper-button id="saveButton" hidden$="[[!_showButtons]]" disabled$="[[!_enableButtons]]" on-tap="_save">
[[localize('save')]]
</paper-button>
</div>
</template>
<script>
Polymer({
is: "outline-validated-input",
properties: {
// Properties affecting the whole element
editable: {type: Boolean, value: false},
visible: {type: Boolean, value: false},
// Properties affecting the input
label: {type: String, readonly: true, value: null},
allowedPattern: {type: String, value: ".*"},
maxLength: {type: Number, value: Number.POSITIVE_INFINITY},
value: {type: String, value: null},
// `value` here is evaluated. If it were simply `() => "`, then clientSideValidator
// would default to just "", not a function returning "".
// Note also that we can't use paper-input's validator attribute because it will fight
// with any server-side validation and cause unpredictable results.
clientSideValidator: {
type: Function,
readonly: true,
value: () => {
return () => "";
},
},
// Properties affecting the buttons
_showButtons: {type: Boolean, value: false},
_enableButtons: {type: Boolean, value: false},
// Other properties
event: {type: String, value: null},
localize: {type: Function, readonly: true},
},
_onKeyUp: function(e) {
const input = this.$.input;
if (e.key === "Escape") {
this._cancel();
input.blur();
return;
} else if (e.key === "Enter") {
if (!input.invalid) {
this._save();
input.blur();
}
return;
}
const validationError = this.clientSideValidator(input.value);
if (validationError) {
input.invalid = true;
this.$.saveButton.disabled = true;
input.errorMessage = validationError;
} else {
input.invalid = false;
this.$.saveButton.disabled = false;
}
},
_cancel: function() {
const input = this.$.input;
input.value = this.value;
input.invalid = false;
this._showButtons = false;
},
_save: function() {
const input = this.$.input;
const value = input.value;
if (value === this.value) {
this._cancel();
return;
}
this.$.cancelButton.disabled = true;
this.$.saveButton.disabled = true;
input.readonly = true;
input.invalid = false;
// We use this heuristic to avoid having to pass a constructor as an attribute
const numberValue = Number(value);
const typedValue = Number.isNaN(numberValue) ? value : numberValue;
this.fire(this.event, {
validatedInput: typedValue,
ui: this,
});
},
_enterEditingState: function() {
if (!this.editable) {
return;
}
this._showButtons = true;
this.$.cancelButton.disabled = false;
this.$.saveButton.disabled = this.$.input.invalid;
},
enterSavedState: function() {
const input = this.$.input;
this.value = input.value;
this._showButtons = false;
input.readonly = false;
},
enterErrorState: function(message) {
const input = this.$.input;
this._enableButtons = true;
input.errorMessage = message;
input.invalid = true;
input.readonly = false;
input.focus();
},
});
</script>
</dom-module>

View file

@ -22,7 +22,6 @@ readonly OUT_DIR=$BUILD_DIR/server_manager/web_app
rm -rf $OUT_DIR
# Create do_install_script.ts, which has a variable with the content of do_install_server.sh.
mkdir -p $OUT_DIR/ts/server_manager/web_app
mkdir -p $OUT_DIR/sh/server_manager/web_app
pushd $ROOT_DIR/src/server_manager/install_scripts > /dev/null
@ -36,20 +35,9 @@ pushd $ROOT_DIR > /dev/null
node src/server_manager/install_scripts/build_install_script_ts.node.js \
build/server_manager/web_app/sh/server_manager/web_app/scripts.tgz > $ROOT_DIR/src/server_manager/install_scripts/do_install_script.ts
# Notice that we forward the build environment if defined.
webpack --config=src/server_manager/webpack.config.js ${BUILD_ENV:+--mode=${BUILD_ENV}}
popd > /dev/null
readonly STATIC_DIR=$OUT_DIR/static
mkdir -p $STATIC_DIR
# Browserify node_modules/ (just a couple of key NPMs) and app.
# TODO(fortuna): Use polymer-webpack-loader instead.
pushd $OUT_DIR > /dev/null
mkdir -p browserified/server_manager/web_app
$NODE_MODULES_BIN_DIR/browserify --require byte-size --require clipboard-polyfill -o static/node_modules.js
# Notice that we forward the build environment if defined.
webpack --config=src/server_manager/webpack.config.js ${BUILD_ENV:+--mode=${BUILD_ENV}}
popd > /dev/null
# Generate CSS rules to mirror the UI in RTL languages.
# TODO(fortuna): Move this to a Webpack loader.
node -e "require('./scripts/generate_rtl_css.js')('$STATIC_DIR/ui_components/*.html', '$STATIC_DIR/ui_components')"

View file

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import './ui_components/app-root.js';
import * as digitalocean_api from '../cloud/digitalocean_api';
import * as i18n from '../infrastructure/i18n';
import {getSentryApiUrl} from '../infrastructure/sentry';

View file

@ -0,0 +1,859 @@
/*
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/app-layout/app-drawer/app-drawer.js';
import '@polymer/app-layout/app-drawer-layout/app-drawer-layout.js';
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
import '@polymer/iron-icon/iron-icon.js';
import '@polymer/iron-icons/iron-icons.js';
import '@polymer/iron-pages/iron-pages.js';
import '@polymer/paper-icon-button/paper-icon-button.js';
import '@polymer/paper-toast/paper-toast.js';
import '@polymer/paper-dialog/paper-dialog.js';
import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js';
import '@polymer/paper-listbox/paper-listbox.js';
import '@polymer/paper-menu-button/paper-menu-button.js';
import './cloud-install-styles.js';
import './outline-about-dialog.js';
import './outline-do-oauth-step.js';
import './outline-feedback-dialog.js';
import './outline-survey-dialog.js';
import './outline-intro-step.js';
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';
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';
const TOS_ACK_LOCAL_STORAGE_KEY = 'tos-ack';
class AppRoot extends mixinBehaviors
([AppLocalizeBehavior], PolymerElement) {
static get template() {
return html`
<style include="cloud-install-styles"></style>
<style>
:host {
--side-bar-width: 48px;
}
.app-container {
margin: 0 auto;
}
/* Large display desktops */
@media (min-width: 1281px) {
.app-container {
max-width: 1200px;
}
}
/* Laptop, desktops */
@media (min-width: 1025px) and (max-width: 1280px) {
.app-container {
max-width: 920px;
}
}
#toast {
align-items: center;
display: flex;
justify-content: space-between;
padding: 24px;
max-width: 450px;
}
#toast paper-icon-button {
/* prevents the icon from resizing when there is a long message in the toast */
flex-shrink: 0;
padding: 0;
height: 20px;
width: 20px;
}
/* rtl:begin:ignore */
#appDrawer {
--app-drawer-content-container: {
color: var(--medium-gray);
background-color: var(--background-contrast-color);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: right;
}
}
/* rtl:end:ignore */
#appDrawer > * {
width: 100%;
}
.servers {
overflow-y: scroll;
flex: 1;
}
.servers::-webkit-scrollbar {
/* Do not display the scroll bar in the drawer or side bar. It is not styled on some platforms. */
display: none;
}
.servers-section {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.servers-section:last-child {
border-bottom: none;
}
.servers-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 24px;
line-height: 39px;
}
.servers-header > span {
flex: 1;
}
.do-overflow-menu {
padding: 24px;
color: var(--dark-gray);
text-align: left;
display: flex;
flex-direction: column;
}
.do-overflow-menu h4 {
margin-top: 0;
white-space: nowrap;
}
.do-overflow-menu .account-info {
display: flex;
align-items: center;
color: var(--faded-gray);
}
.do-overflow-menu .account-info img {
margin-right: 12px;
width: 24px;
}
.do-overflow-menu .sign-out-button {
margin-top: 24px;
align-self: flex-end;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
}
.servers-container {
padding-right: 12px; /* In case the server name is wraps. */
}
.server {
display: flex;
align-items: center;
width: 100%; /* For the side bar icons. */
margin: 18px 0;
padding: 6px 0;
cursor: pointer;
}
.server.selected {
color: white;
border-left: 2px solid var(--primary-green);
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.server.syncing {
cursor: wait;
}
.syncing .server-icon {
animation: rotate 1.75s ease-out infinite;
opacity: 0.5;
}
.server-icon {
width: 22px;
height: 22px;
/* Prevent the image from shrinking when the server title spans multiple lines */
min-width: 22px !important;
margin: 0 24px;
}
.selected > .server-icon {
/* Account for the selected border width to preserve center alignment. */
margin-left: 22px;
}
.add-server-section {
padding: 24px 0;
text-transform: uppercase;
color: var(--primary-green);
font-size: 12px;
letter-spacing: 0.6px;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
}
.add-server-section paper-icon-item {
margin-left: 24px;
}
.add-server-section paper-icon-item iron-icon {
margin-right: 24px;
}
#appDrawer > paper-listbox {
color: var(--medium-gray);
background-color: var(--background-contrast-color);
}
#appDrawer > paper-listbox > * {
display: block;
cursor: pointer;
padding-left: 24px;
font-size: 14px;
line-height: 40px;
outline: none;
}
#appDrawer a {
color: inherit;
}
#appDrawer a:focus {
outline: none;
}
.legal-links {
margin: 36px -6px 0;
}
.legal-links > * {
margin: 0 6px;
}
app-toolbar [main-title] img {
height: 16px;
margin-top: 8px;
}
.side-bar-margin {
margin-left: var(--side-bar-width);
}
/* rtl:begin:ignore */
#sideBar {
--app-drawer-width: var(--side-bar-width);
--app-drawer-content-container: {
background-color: var(--background-contrast-color);
}
}
/* rtl:end:ignore */
.side-bar-container {
height: 100%;
text-align: center;
color: var(--light-gray);
display: flex;
flex-direction: column;
}
.side-bar-container .servers {
flex: initial; /* Prevent the server list pushing down the add server button. */
}
.side-bar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.side-bar-section.menu {
min-height: 32px;
}
.side-bar-section.servers-section {
padding: 24px 0;
}
.side-bar-section .server {
justify-content: center;
margin: 12px auto;
padding: 2px 0;
}
.side-bar-section .provider-icon {
margin-bottom: 12px;
padding: 12px 0;
opacity: 0.54;
filter: grayscale(100%);
}
.side-bar-section.add-server-section {
flex: 1 0 24px;
border-bottom: none;
}
.side-bar-section > .server-icon {
margin: 0;
}
#getConnectedDialog {
height: 562px;
background: white;
}
#getConnectedDialog iframe {
padding: 0;
margin: 0;
width: 100%;
border: none;
border-bottom: 1px solid #ccc;
height: 500px;
}
#getConnectedDialog .buttons {
margin-top: -5px; /* undo spacing added after iframe */
}
</style>
<outline-tos-view id="tosView" has-accepted-terms-of-service="{{userAcceptedTos}}" hidden\$="{{hasAcceptedTos}}" localize="[[localize]]"></outline-tos-view>
<div hidden\$="{{!hasAcceptedTos}}">
<!-- This responsive width sets the minimum layout area to 648px. -->
<app-drawer-layout id="drawerLayout" responsive-width="886px" on-narrow-changed="_computeShouldShowSideBar" class\$="[[sideBarMarginClass]]">
<app-drawer id="appDrawer" slot="drawer" on-opened-changed="_computeShouldShowSideBar">
<app-toolbar class="toolbar" hidden\$="[[shouldShowSideBar]]">
<paper-icon-button icon="menu" on-click="_toggleAppDrawer"></paper-icon-button>
<div main-title=""><img src="images/outline-manager-logo.svg"></div>
</app-toolbar>
<!-- Servers section -->
<div class="servers">
<!-- DigitalOcean servers -->
<div class="servers-section" hidden\$="{{!isSignedInToDigitalOcean}}">
<div class="servers-header">
<span>[[localize('servers-digitalocean')]]</span>
<paper-menu-button horizontal-align="left" class="" close-on-activate="" no-animations="" dynamic-align="" no-overlap="">
<paper-icon-button icon="more-vert" slot="dropdown-trigger"></paper-icon-button>
<div class="do-overflow-menu" slot="dropdown-content">
<h4>[[localize('digitalocean-disconnect-account')]]</h4>
<div class="account-info"><img src="images/digital_ocean_logo.svg">{{adminEmail}}</div>
<div class="sign-out-button" on-tap="signOutTapped">[[localize('digitalocean-disconnect')]]</div>
</div>
</paper-menu-button>
</div>
<div class="servers-container">
<template is="dom-repeat" items="{{serverList}}" as="server" filter="_isServerManaged" sort="_sortServersByName">
<div class\$="server {{_computeServerClasses(selectedServer, server)}}" data-server\$="[[server]]" on-tap="_showServer">
<img class="server-icon" src\$="images/{{_computeServerImage(selectedServer, server)}}">
<span>{{server.name}}</span>
</div>
</template>
</div>
</div>
<!-- Manual servers -->
<div class="servers-section" hidden\$="{{!hasManualServers}}">
<div class="servers-header">
<span>[[localize('servers-manual')]]</span>
</div>
<div class="servers-container">
<template is="dom-repeat" items="{{serverList}}" as="server" filter="_isServerManual" sort="_sortServersByName">
<div class\$="server {{_computeServerClasses(selectedServer, server)}}" data-server\$="[[server]]" on-tap="_showServer">
<img class="server-icon" src\$="images/{{_computeServerImage(selectedServer, server)}}">
<span>{{server.name}}</span>
</div>
</template>
</div>
</div>
</div>
<!-- Add server -->
<div class="add-server-section" on-tap="showIntro">
<paper-icon-item>
<iron-icon icon="add" slot="item-icon"></iron-icon>[[localize('servers-add')]]
</paper-icon-item>
</div>
<!-- Links section -->
<paper-listbox on-tap="maybeCloseDrawer">
<a href="https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection">[[localize('nav-data-collection')]]</a>
<span on-tap="submitFeedbackTapped">[[localize('nav-feedback')]]</span>
<a href="https://s3.amazonaws.com/outline-vpn/index.html#/en/support/">[[localize('nav-help')]]</a>
<span on-tap="aboutTapped">[[localize('nav-about')]]</span>
<div class="legal-links">
<a href="https://www.google.com/policies/privacy/">[[localize('nav-privacy')]]</a>
<a href="https://s3.amazonaws.com/outline-vpn/static_downloads/Outline-Terms-of-Service.html">[[localize('nav-terms')]]</a>
<span on-tap="showLicensesTapped">[[localize('nav-licenses')]]</span>
</div>
</paper-listbox>
</app-drawer>
<app-header-layout>
<div class="app-container">
<iron-pages attr-for-selected="id" selected="{{ currentPage }}">
<outline-intro-step id="intro" is-signed-in-to-digital-ocean="{{isSignedInToDigitalOcean}}" digital-ocean-email="{{adminEmail}}" localize="[[localize]]"></outline-intro-step>
<outline-do-oauth-step id="digitalOceanOauth" localize="[[localize]]"></outline-do-oauth-step>
<outline-manual-server-entry id="manualEntry" localize="[[localize]]"></outline-manual-server-entry>
<outline-region-picker-step id="regionPicker" localize="[[localize]]"></outline-region-picker-step>
<outline-server-progress-step id="serverProgressStep" localize="[[localize]]"></outline-server-progress-step>
<div id="serverView">
<template is="dom-repeat" items="{{serverList}}" as="server">
<outline-server-view id="serverView-{{_base64Encode(server.id)}}" localize="[[localize]]" hidden\$="{{!_isServerSelected(selectedServer, server)}}"></outline-server-view>
</template>
</div>
</iron-pages>
</div>
</app-header-layout>
</app-drawer-layout>
<!-- Side bar -->
<app-drawer id="sideBar" opened\$="[[shouldShowSideBar]]" persistent="">
<div class="side-bar-container">
<div class="side-bar-section menu">
<paper-icon-button icon="menu" on-click="_toggleAppDrawer"></paper-icon-button>
</div>
<div class="servers">
<!-- DigitalOcean servers -->
<div class="side-bar-section servers-section" hidden\$="{{!isSignedInToDigitalOcean}}">
<img class="provider-icon" src="images/do_white_logo.svg">
<template is="dom-repeat" items="{{serverList}}" as="server" filter="_isServerManaged" sort="_sortServersByName">
<div class\$="server {{_computeServerClasses(selectedServer, server)}}" data-server\$="[[server]]" on-tap="_showServer">
<img class="server-icon" src\$="images/{{_computeServerImage(selectedServer, server)}}">
</div>
</template>
</div>
<!-- Manual servers -->
<div class="side-bar-section servers-section" hidden\$="{{!hasManualServers}}">
<img class="provider-icon" src="images/cloud.svg">
<template is="dom-repeat" items="{{serverList}}" as="server" filter="_isServerManual" sort="_sortServersByName">
<div class\$="server {{_computeServerClasses(selectedServer, server)}}" data-server\$="[[server]]" on-tap="_showServer">
<img class="server-icon" src\$="images/{{_computeServerImage(selectedServer, server)}}">
</div>
</template>
</div>
</div>
<div class="side-bar-section add-server-section" on-tap="showIntro">
<paper-icon-item>
<iron-icon icon="add" slot="item-icon"></iron-icon>
</paper-icon-item>
</div>
</div>
</app-drawer>
<paper-toast id="toast"><paper-icon-button icon="icons:close" on-tap="closeError"></paper-icon-button></paper-toast>
<!-- Modal dialogs must be outside the app container; otherwise the backdrop covers them. -->
<outline-survey-dialog id="surveyDialog" localize="[[localize]]"></outline-survey-dialog>
<outline-feedback-dialog id="feedbackDialog" localize="[[localize]]"></outline-feedback-dialog>
<outline-about-dialog id="aboutDialog" outline-version="[[outlineVersion]]" localize="[[localize]]"></outline-about-dialog>
<outline-modal-dialog id="modalDialog"></outline-modal-dialog>
<outline-share-dialog id="shareDialog" localize="[[localize]]"></outline-share-dialog>
<outline-metrics-option-dialog id="metricsDialog" localize="[[localize]]"></outline-metrics-option-dialog>
<paper-dialog id="getConnectedDialog" modal="">
<!-- iframe gets inserted here once we are given the invite URL. -->
<div class="buttons">
<paper-button on-tap="closeGetConnectedDialog" autofocus="">[[localize('close')]]</paper-button>
</div>
</paper-dialog>
<paper-dialog id="licenses" modal="" restorefocusonclose="">
<paper-dialog-scrollable>
<code id="licensesText">
[[localize('error-licenses')]]
</code>
</paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-dismiss="" autofocus="">[[localize('close')]]</paper-button>
</div>
</paper-dialog>
</div>
`;
}
static get is() {
return 'app-root';
}
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,
},
hasManualServers: {
type: Boolean,
computed: '_computeHasManualServers(serverList.*)',
},
adminEmail: {
type: String,
value: '',
},
isSignedInToDigitalOcean: {
type: Boolean,
computed: '_computeIsSignedInToDigitalOcean(adminEmail)',
},
outlineVersion: String,
userAcceptedTos: {
type: Boolean,
// Get notified when the user clicks the OK button in the ToS view
observer: '_userAcceptedTosChanged',
},
hasAcceptedTos: {
type: Boolean,
computed: '_computeHasAcceptedTermsOfService(userAcceptedTos)',
},
currentPage: {
type: String,
value: 'intro',
},
shouldShowSideBar: {
type: Boolean,
value: false,
},
sideBarMarginClass: {
type: String,
computed: '_computeSideBarMarginClass(shouldShowSideBar)',
},
};
}
constructor() {
super();
this.addEventListener('RegionSelected', this.handleRegionSelected);
this.addEventListener(
'SetUpGenericCloudProviderRequested', this.handleSetUpGenericCloudProviderRequested);
this.addEventListener('SetUpAwsRequested', this.handleSetUpAwsRequested);
this.addEventListener('SetUpGcpRequested', this.handleSetUpGcpRequested);
this.addEventListener('ManualServerEntryCancelled', this.handleManualCancelled);
}
setLanguage(newLanguage, langDir) {
const messagesUrl = `/messages/${newLanguage}.json`;
this.loadResources(messagesUrl, newLanguage);
if (langDir === 'rtl') {
this.$.appDrawer.align = 'right';
this.$.sideBar.align = 'right';
}
this.language = newLanguage;
}
showIntro() {
this.maybeCloseDrawer();
this.selectedServer = undefined;
this.currentPage = 'intro';
}
getDigitalOceanOauthFlow(onCancel) {
const oauthFlow = this.$.digitalOceanOauth;
oauthFlow.onCancel = onCancel;
return oauthFlow;
}
showDigitalOceanOauthFlow() {
this.currentPage = 'digitalOceanOauth';
}
getAndShowDigitalOceanOauthFlow(onCancel) {
this.currentPage = 'digitalOceanOauth';
const oauthFlow = this.getDigitalOceanOauthFlow(onCancel);
oauthFlow.showConnectAccount();
return oauthFlow;
}
getAndShowRegionPicker() {
this.currentPage = 'regionPicker';
this.$.regionPicker.init();
return this.$.regionPicker;
}
getManualServerEntry() {
return this.$.manualEntry;
}
showProgress(serverName, showCancelButton) {
this.currentPage = 'serverProgressStep';
this.$.serverProgressStep.serverName = serverName;
this.$.serverProgressStep.showCancelButton = showCancelButton;
this.$.serverProgressStep.start();
}
showServerView() {
this.$.serverProgressStep.stop();
this.currentPage = 'serverView';
}
getServerView(displayServerId) {
if (!displayServerId) {
return null;
}
const selectedServerId = this._base64Encode(displayServerId);
return this.$.serverView.querySelector(`#serverView-${selectedServerId}`);
}
handleRegionSelected(e) {
this.fire('SetUpServerRequested', {
regionId: this.$.regionPicker.getSelectedRegionId(),
});
}
handleSetUpGenericCloudProviderRequested() {
this.handleManualServerSelected('generic');
}
handleSetUpAwsRequested() {
this.handleManualServerSelected('aws');
}
handleSetUpGcpRequested() {
this.handleManualServerSelected('gcp');
}
handleManualServerSelected(cloudProvider) {
this.$.manualEntry.clear();
this.$.manualEntry.cloudProvider = cloudProvider;
this.currentPage = 'manualEntry';
}
handleManualCancelled() {
this.currentPage = 'intro';
}
showError(errorMsg) {
this.showToast(errorMsg, Infinity);
}
showNotification(message, durationMs = 3000) {
this.showToast(message, durationMs);
}
showToast(message, duration) {
const toast = this.$.toast;
toast.close();
// Defer in order to trigger the toast animation, otherwise the
// update happens in place.
setTimeout(() => {
toast.show({
text: message,
duration: duration,
noOverlap: true,
});
}, 0);
}
closeError() {
this.$.toast.close();
}
// cb is 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');
this.showModalDialog(dialogTitle, dialogText, [this.localize('cancel'), this.localize('retry')])
.then(clickedButtonIndex => {
cb(clickedButtonIndex === 1); // pass true if user clicked retry
});
}
getConfirmation(title, text, confirmButtonText, continueFunc) {
this.showModalDialog(title, text, [this.localize('cancel'), confirmButtonText])
.then(clickedButtonIndex => {
if (clickedButtonIndex === 1) {
// user clicked to confirm.
continueFunc();
}
});
}
showManualServerError(errorTitle, errorText) {
this.showModalDialog(errorTitle, errorText, [this.localize('cancel'), this.localize('retry')])
.then(clickedButtonIndex => {
if (clickedButtonIndex == 1) {
this.$.manualEntry.retryTapped();
} else {
this.$.manualEntry.cancelTapped();
}
});
}
_computeIsSignedInToDigitalOcean(adminEmail) {
return Boolean(adminEmail);
}
_computeHasManualServers(serverList) {
return this.serverList.filter(server => !server.isManaged).length > 0;
}
_userAcceptedTosChanged(userAcceptedTos) {
if (userAcceptedTos) {
window.localStorage[TOS_ACK_LOCAL_STORAGE_KEY] = Date.now();
}
}
_computeHasAcceptedTermsOfService(userAcceptedTos) {
return userAcceptedTos || !!window.localStorage[TOS_ACK_LOCAL_STORAGE_KEY];
}
_toggleAppDrawer() {
const drawerNarrow = this.$.drawerLayout.narrow;
const forceNarrow = this.$.drawerLayout.forceNarrow;
if (drawerNarrow) {
if (forceNarrow) {
// The window width is below the responsive threshold. Do not force narrow mode.
this.$.drawerLayout.forceNarrow = false;
}
this.$.appDrawer.toggle();
} else {
// Forcing narrow mode when the window width is above the responsive threshold effectively
// collapses the drawer. Conversely, reverting force narrow expands the drawer.
this.$.drawerLayout.forceNarrow = !forceNarrow;
}
}
maybeCloseDrawer() {
if (this.$.drawerLayout.narrow || this.$.drawerLayout.forceNarrow) {
this.$.appDrawer.close();
}
}
submitFeedbackTapped() {
this.$.feedbackDialog.open();
}
aboutTapped() {
this.$.aboutDialog.open();
}
signOutTapped() {
this.fire('SignOutRequested');
}
openManualInstallFeedback(prepopulatedMessage) {
this.$.feedbackDialog.open(prepopulatedMessage, true);
}
openShareDialog(accessKey, s3Url) {
this.$.shareDialog.open(accessKey, s3Url);
}
openGetConnectedDialog(inviteUrl) {
const dialog = this.$.getConnectedDialog;
if (dialog.children.length > 1) {
return; // The iframe is already loading.
}
// Reset the iframe's state, by replacing it with a newly constructed
// iframe. Unfortunately the location.reload API does not work in our case due to
// this Chrome error:
// "Blocked a frame with origin "outline://web_app" from accessing a cross-origin frame."
const iframe = document.createElement('iframe');
iframe.onload = function() {
dialog.open();
};
iframe.src = inviteUrl;
dialog.insertBefore(iframe, dialog.children[0]);
}
closeGetConnectedDialog() {
const dialog = this.$.getConnectedDialog;
dialog.close();
const oldIframe = dialog.children[0];
dialog.removeChild(oldIframe);
}
showMetricsDialogForNewServer() {
this.$.metricsDialog.showMetricsOptInDialog();
}
// Returns a Promise which fulfills with the index of the button clicked.
showModalDialog(title, text, buttons) {
return this.$.modalDialog.open(title, text, buttons);
}
closeModalDialog(title, text, buttons) {
return this.$.modalDialog.close();
}
showLicensesTapped() {
var xhr = new XMLHttpRequest();
xhr.onload = () => {
this.$.licensesText.innerText = xhr.responseText;
this.$.licenses.open();
};
xhr.onerror = () => {
console.error('could not load license.txt');
};
xhr.open('GET', '/ui_components/licenses/licenses.txt', true);
xhr.send();
}
_computeShouldShowSideBar() {
const drawerNarrow = this.$.drawerLayout.narrow;
const drawerOpened = this.$.appDrawer.opened;
if (drawerOpened && drawerNarrow) {
this.shouldShowSideBar = false;
} else {
this.shouldShowSideBar = drawerNarrow;
}
}
_computeSideBarMarginClass(shouldShowSideBar) {
return shouldShowSideBar ? 'side-bar-margin' : '';
}
_isServerManaged(server) {
return server.isManaged;
}
_isServerManual(server) {
return !server.isManaged;
}
_sortServersByName(a, b) {
const aName = a.name.toUpperCase();
const bName = b.name.toUpperCase();
if (aName < bName) {
return -1;
} else if (aName > bName) {
return 1;
}
return 0;
}
_computeServerClasses(selectedServer, server) {
let serverClasses = [];
if (this._isServerSelected(selectedServer, server)) {
serverClasses.push('selected');
}
if (!server.isSynced) {
serverClasses.push('syncing');
}
return serverClasses.join(' ');
}
_computeServerImage(selectedServer, server) {
if (this._isServerSelected(selectedServer, server)) {
return 'server-icon-selected.png';
}
return 'server-icon.png';
}
_isServerSelected(selectedServer, server) {
return !!selectedServer && selectedServer.id === server.id;
}
_showServer(event) {
const server = event.model.server;
this.fire('ShowServerRequested', {displayServerId: server.id});
this.maybeCloseDrawer();
}
// Wrapper to encode a string in base64. This is necessary to set the server view IDs to
// the display server IDs, which are URLs, so they can be used with selector methods. The IDs
// are never decoded.
_base64Encode(s) {
return btoa(s).replace(/=/g, '');
}
}
customElements.define(AppRoot.is, AppRoot);

View file

@ -0,0 +1,244 @@
/*
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 {html} from '@polymer/polymer/lib/utils/html-tag.js';
// 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
const styleElement = document.createElement('dom-module');
styleElement.appendChild(html`
<style>
:host {
--primary-green: #00bfa5;
--background-color: #263238;
--background-contrast-color: #2e3a3f;
--light-gray: rgba(255, 255, 255, 0.87);
--medium-gray: rgba(255, 255, 255, 0.54);
--dark-gray: rgba(0, 0, 0, 0.87);
--border-color: rgba(255, 255, 255, 0.12);
}
#title {
font-weight: 100;
color: rgba(0, 0, 0, 0.87);
font-size: 24px;
line-height: 1.5em;
display: inline-block;
-webkit-margin-before: 1.25em;
-webkit-margin-after: 0em;
-webkit-margin-start: 0px;
-webkit-margin-end: 0px;
}
paper-button {
font-size: 14px;
padding: 1em 3em 1em 3em;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
letter-spacing: 0.05em;
border-radius: 2px;
}
paper-button.primary {
transition: 0.5s;
color: white;
background-color: var(--background-color);
width: 56%;
}
paper-button.secondary {
background-color: none;
}
paper-button[disabled] {
background-color: rgba(0, 0, 0, 0.25);
color: var(--medium-gray);
}
paper-progress {
background: var(--background-contrast-color);
--paper-progress-active-color: var(--primary-green);
--paper-progress-container-color: var(--background-contrast-color);
--paper-progress-transition-duration: 1s;
--paper-progress-transition-timing-function: linear;
width: 100%;
}
hr {
margin: 2em 0 2em 0;
}
paper-dropdown-menu {
--paper-input-container-underline: {
color: var(--primary-green);
}
}
paper-input,
paper-textarea,
paper-dropdown-menu {
--paper-input-container-focus-color: var(--primary-green);
}
app-toolbar {
color: #eee;
font-size: 12px;
font-weight: 300;
letter-spacing: 0.02em;
color: var(--light-gray);
padding: 0;
min-height: 60px;
border-bottom: 1px solid var(--border-color);
}
app-toolbar paper-icon-button {
margin-left: 16px;
margin-right: 26px;
}
app-toolbar h2 {
color: #e0e2e3;
font-size: 19px;
font-weight: 400;
}
p {
color: rgba(0, 0, 0, 0.54);
margin-top: 12px;
margin-bottom: 24px;
}
paper-dialog {
max-width: 540px;
min-width: 480px;
margin: 56px 32px 32px 32px;
}
paper-dialog-scrollable {
width: 100%;
margin: 0;
}
paper-dialog-scrollable img {
padding-top: 24px;
padding-bottom: 24px;
}
paper-dialog .dialogBanner {
background-color: #eceff1;
text-align: center;
margin-top: 0;
padding-top: 50px;
height: 150px;
}
paper-dialog .dialogBanner img {
height: 150px;
}
paper-tooltip {
--paper-tooltip-text-color: #000;
--paper-tooltip-background: #fff;
--paper-tooltip-duration-in: 100;
--paper-tooltip-duration-out: 100;
}
.instructions-list {
margin: auto;
padding-right: 42px;
padding-left: 42px;
max-width: 400px;
}
.instructions-list ol {
margin: 0 0 0 0;
padding: 0 24px 0 56px;
text-align: center !important;
}
.instructions-list li {
padding: 0 12px 24px 20px;
text-align: left;
}
.instructions-list li:before {
padding: 0 0 30px 20px;
}
.instructions-list paper-button {
margin-top: 20px;
}
.instructions-list a {
text-decoration: underline;
color: rgba(0, 0, 0, 0.87);
font-weight: 500;
}
.instructions-list p {
color: rgba(0, 0, 0, 0.54);
}
.instructions-list img {
padding: 32px 0px 42px 0;
margin: auto;
}
.walkthrough {
background: #ffffff;
box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.12);
top: 0px;
display: block;
}
.walkthrough h3 {
font-size: 14px;
line-height: 24px;
color: rgba(0, 0, 0, 0.87);
max-width: 70%;
margin: auto;
padding: 0px 0px 24px 0px;
font-weight: normal;
}
a {
text-decoration: none;
color: var(--primary-green);
}
/*
Helps make sure elements using both iron-flex-layout and the hidden
attribute actually hide:
- https://github.com/Polymer/polymer/issues/3711
- https://github.com/PolymerElements/iron-flex-layout/issues/86
*/
[hidden] {
display: none !important;
}
.card-section {
display: flex;
padding: 24px;
background: var(--background-contrast-color);
border-radius: 2px;
}
/* Mirror progress indicators for RTL languages */
:host(:dir(rtl)) paper-progress {
transform: scaleX(-1);
}
</style>`);
styleElement.register('cloud-install-styles');

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,14 +12,15 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
*/
import '@polymer/polymer/polymer-legacy.js';
<link rel="import" href="./cloud-install-styles.html" />
<dom-module id="outline-about-dialog">
<template>
import '@polymer/paper-dialog/paper-dialog.js';
import './cloud-install-styles.js';
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">
#dialog {
width: 80%;
@ -58,37 +59,33 @@
}
</style>
<paper-dialog id="dialog" modal>
<paper-dialog id="dialog" modal="">
<div>
<img id="outlineLogo" src="images/manager-about-logo2x.png" />
<img id="outlineLogo" src="images/manager-about-logo2x.png">
</div>
<p id="version" inner-h-t-m-l="[[localize('about-version', 'version', outlineVersion)]]"></p>
<p
inner-h-t-m-l="[[localize('about-outline', 'jigsawUrl', 'https://jigsaw.google.com', 'shadowsocksUrl', 'https://shadowsocks.org', 'gitHubUrl', 'https://github.com/jigsaw-Code/?q=outline', 'redditUrl', 'https://www.reddit.com/r/outlinevpn', 'mediumUrl', 'https://medium.com/jigsaw')]]"
>
>
<p inner-h-t-m-l="[[localize('about-outline', 'jigsawUrl', 'https://jigsaw.google.com', 'shadowsocksUrl', 'https://shadowsocks.org', 'gitHubUrl', 'https://github.com/jigsaw-Code/?q=outline', 'redditUrl', 'https://www.reddit.com/r/outlinevpn', 'mediumUrl', 'https://medium.com/jigsaw')]]">
&gt;
</p>
<p>
<a href="https://jigsaw.google.com/">
<img src="images/jigsaw-logo.svg" />
<img src="images/jigsaw-logo.svg">
</a>
</p>
<div class="buttons">
<paper-button dialog-dismiss autofocus>[[localize('close')]]</paper-button>
<paper-button dialog-dismiss="" autofocus="">[[localize('close')]]</paper-button>
</div>
</paper-dialog>
</template>
`,
<script>
Polymer({
is: "outline-about-dialog",
properties: {
localize: {type: Function, readonly: true},
outlineVersion: String,
},
open: function() {
this.$.dialog.open();
},
});
</script>
</dom-module>
is: 'outline-about-dialog',
properties: {
localize: {type: Function, readonly: true},
outlineVersion: String,
},
open: function() {
this.$.dialog.open();
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,13 +12,15 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/iron-icons/iron-icons.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
*/
import '@polymer/polymer/polymer-legacy.js';
<dom-module id="outline-cloud-instructions-view">
<template>
import '@polymer/iron-icons/iron-icons.js';
import '@polymer/paper-button/paper-button.js';
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">
:host {
}
@ -81,25 +83,24 @@
<div class="thumbnail overlay" on-tap="_openImage">
<iron-icon icon="open-in-new"></iron-icon>
</div>
<img class="thumbnail" src$="[[thumbnailPath]]" />
<img class="thumbnail" src\$="[[thumbnailPath]]">
<div class="instructions">
<slot></slot>
</div>
</div>
</template>
<script>
Polymer({
is: "outline-cloud-instructions-view",
properties: {
title: String,
imagePath: String,
thumbnailPath: String,
instructions: Array,
localize: {type: Function, readonly: true},
},
_openImage: function() {
this.fire("OpenImageRequested", {imagePath: this.imagePath});
},
});
</script>
</dom-module>
`,
is: 'outline-cloud-instructions-view',
properties: {
title: String,
imagePath: String,
thumbnailPath: String,
instructions: Array,
localize: {type: Function, readonly: true},
},
_openImage: function() {
this.fire('OpenImageRequested', {imagePath: this.imagePath});
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,14 +12,16 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/iron-pages/iron-pages.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-step-view.html" />
*/
import '@polymer/polymer/polymer-legacy.js';
<dom-module id="outline-do-oauth-step">
<template>
import '@polymer/iron-pages/iron-pages.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';
Polymer({
_template: html`
<style include="cloud-install-styles">
:host {
}
@ -81,7 +83,7 @@
<span slot="step-description">[[localize('oauth-connect-description')]]</span>
<paper-card class="card">
<div class="container">
<img src="images/digital_ocean_logo.svg" />
<img src="images/digital_ocean_logo.svg">
<p>[[localize('oauth-connect-tag')]]</p>
</div>
<paper-button on-tap="_cancelTapped">[[localize('cancel')]]</paper-button>
@ -93,7 +95,7 @@
<span slot="step-description">[[localize('oauth-verify')]]</span>
<paper-card class="card">
<div class="container">
<img class="mirror" src="images/do_oauth_email.svg" />
<img class="mirror" src="images/do_oauth_email.svg">
<p>[[localize('oauth-verify-tag')]]</p>
</div>
<paper-button on-tap="_cancelTapped">[[localize('oauth-sign-out')]]</paper-button>
@ -105,7 +107,7 @@
<span slot="step-description">[[localize('oauth-billing')]]</span>
<paper-card class="card">
<div class="container">
<img class="mirror" src="images/do_oauth_billing.svg" />
<img class="mirror" src="images/do_oauth_billing.svg">
<p>[[localize('oauth-billing-tag')]]</p>
</div>
<paper-button on-tap="_cancelTapped">[[localize('oauth-sign-out')]]</paper-button>
@ -117,49 +119,52 @@
<span slot="step-description">[[localize('oauth-account-active')]]</span>
<paper-card class="card">
<div class="container">
<img class="mirror" src="images/do_oauth_done.svg" />
<img class="mirror" src="images/do_oauth_done.svg">
<p>[[localize('oauth-account-active-tag')]]</p>
</div>
<paper-button disabled>[[localize('oauth-sign-out')]]</paper-button>
<paper-button disabled="">[[localize('oauth-sign-out')]]</paper-button>
</paper-card>
</outline-step-view>
</iron-pages>
</template>
<script>
Polymer({
is: "outline-do-oauth-step",
properties: {
currentPage: {
type: String,
value: "connectAccount",
},
cancelButtonText: {
type: String,
value: "Cancel",
},
localize: {
type: Function,
readonly: true,
},
onCancel: Function,
},
_cancelTapped: function() {
if (this.onCancel) {
this.onCancel();
}
},
showEmailVerification: function() {
this.currentPage = "verifyEmail";
},
showBilling: function() {
this.currentPage = "enterBilling";
},
showAccountActive: function() {
this.currentPage = "accountActive";
},
showConnectAccount: function() {
this.currentPage = "connectAccount";
},
});
</script>
</dom-module>
`,
is: 'outline-do-oauth-step',
properties: {
currentPage: {
type: String,
value: 'connectAccount',
},
cancelButtonText: {
type: String,
value: 'Cancel',
},
localize: {
type: Function,
readonly: true,
},
onCancel: Function,
},
_cancelTapped: function() {
if (this.onCancel) {
this.onCancel();
}
},
showEmailVerification: function() {
this.currentPage = 'verifyEmail';
},
showBilling: function() {
this.currentPage = 'enterBilling';
},
showAccountActive: function() {
this.currentPage = 'accountActive';
},
showConnectAccount: function() {
this.currentPage = 'connectAccount';
}
});

View file

@ -0,0 +1,227 @@
/*
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-dialog/paper-dialog.js';
import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js';
import '@polymer/paper-item/paper-item.js';
import '@polymer/paper-listbox/paper-listbox.js';
import '@polymer/paper-input/paper-input.js';
import '@polymer/paper-input/paper-textarea.js';
// This is needed to fix the "KeyframeEffect is not defined"
// see https://github.com/PolymerElements/paper-swatch-picker/issues/36
import 'web-animations-js/web-animations-next.min.js';
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 {
margin: 0px;
}
#feedbackWrapper {
margin-top: 0;
}
/*
This element is tricky to size.
We need to constrain its height because paper-dialog doesn't handle things well when its
height exceeds that of the page. max-rows isn't a reliable way to constrain the element's
height because it does not consider line wrapping:
https://github.com/PolymerElements/paper-input/issues/158
By configuring a max-height and setting overflow-y:scroll on the paper-textarea we get its
autogrow behaviour, at the cost of always showing a vertical scrollbar (even when there is
just one line).
*/
#userFeedback {
max-height: 175px;
overflow-x: hidden;
overflow-y: scroll;
}
p a {
color: var(--primary-green);
}
p.disclaimer {
margin: 0;
font-size: 12px;
}
paper-dropdown-menu {
width: 100%;
}
paper-textarea {
--iron-autogrow-textarea: {
background: #eee;
}
}
#feedbackExplanation {
display: none;
}
.installationFailed #feedbackCategory {
display: none;
}
.installationFailed #userFeedback {
background-color: #eceff1;
margin-top: 1em;
}
.installationFailed #feedbackExplanation {
display: block;
}
</style>
<paper-dialog id="dialog" modal="">
<h2>[[title]]</h2>
<div id="feedbackWrapper">
<p id="feedbackExplanation">[[feedbackExplanation]]</p>
<paper-dropdown-menu id="feedbackCategory" horizontal-align="left" on-selected-item-changed="feedbackCategoryChanged">
<paper-listbox id="feedbackCategoryListbox" slot="dropdown-content" class="dropdown-content" selected="0">
<paper-item>[[localize('feedback-general')]]</paper-item>
<paper-item>[[localize('feedback-install')]]</paper-item>
<paper-item>[[localize('feedback-connection')]]</paper-item>
<paper-item>[[localize('feedback-connection-others')]]</paper-item>
<paper-item>[[localize('feedback-management')]]</paper-item>
<paper-item>[[localize('feedback-suggestion')]]</paper-item>
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu id="cloudProvider" horizontal-align="left" placeholder="[[localize('feedback-cloud-provider')]]" hidden\$="[[!shouldShowCloudProvider]]" error-message="[[localize('feedback-cloud-provider-error')]]">
<paper-listbox id="cloudProviderListbox" slot="dropdown-content" class="dropdown-content">
<paper-item>DigitalOcean</paper-item>
<paper-item>Amazon Web Services</paper-item>
<paper-item>Google Cloud Platform</paper-item>
<paper-item>[[localize('feedback-other')]]</paper-item>
</paper-listbox>
</paper-dropdown-menu>
<paper-input id="userEmail" type="text" placeholder="[[localize('feedback-email')]]" on-value-changed="userEmailValueChanged"></paper-input>
<p class="disclaimer" hidden\$="[[!shouldShowLanguageDisclaimer]]">[[localize('feedback-disclaimer')]]</p>
<paper-textarea id="userFeedback" type="text" label="[[localize('feedback-label')]]" always-float-label="" rows="4" error-message="[[localize('feedback-error')]]" on-value-changed="userFeedbackValueChanged"></paper-textarea>
<p inner-h-t-m-l="[[localize('feedback-privacy', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"></p>
</div>
<!-- end of #feedbackWrapper -->
<p class="buttons">
<paper-button dialog-dismiss="">[[localize('cancel')]]</paper-button>
<paper-button autofocus="" on-tap="submitTappedHandler">[[localize('feedback-submit')]]</paper-button>
</p>
</paper-dialog>
`,
is: 'outline-feedback-dialog',
properties: {
title: String,
feedbackExplanation: String,
feedbackCategories: {
type: Object,
readonly: true,
value: {
// Maps a category to its `feedbackCategoryListbox` item index.
GENERAL: 0,
INSTALLATION: 1,
CONNECTION: 2,
CONNECTION_OTHERS: 3,
MANAGEMENT: 4,
SUGGESTIONS: 5,
},
},
hasEnteredEmail: {
type: Boolean,
value: false,
},
shouldShowCloudProvider: {
type: Boolean,
value: false,
},
shouldShowLanguageDisclaimer: {
type: Boolean,
computed: '_computeShouldShowLanguageDisclaimer(hasEnteredEmail)',
},
localize: {
type: Function,
readonly: true,
},
},
open: function(prepopulatedMessage, showInstallationFailed) {
// Clear all fields, in case feedback had already been entered.
if (showInstallationFailed) {
this.title = this.localize('feedback-title-install');
this.feedbackExplanation = this.localize('feedback-explanation-install');
this.$.dialog.classList.add('installationFailed');
this.$.feedbackCategoryListbox.selected = this.feedbackCategories.INSTALLATION;
} else {
this.title = this.localize('feedback-title-generic');
this.feedbackExplanation = '';
this.$.dialog.classList.remove('installationFailed');
// TODO(alalama): figure out why the localized category doesn't get displayed the first
// time opening the dialog.
this.$.feedbackCategoryListbox.selected = this.feedbackCategories.GENERAL;
}
this.$.userFeedback.invalid = false;
this.$.userFeedback.value = prepopulatedMessage || '';
this.$.userEmail.value = '';
this.$.cloudProviderListbox.selected = undefined;
this.$.dialog.open();
},
submitTappedHandler: function() {
// Verify that userFeedback is entered.
if (!this.$.userFeedback.value) {
this.$.userFeedback.invalid = true;
return;
}
const data = {
feedbackCategory: this.$.feedbackCategory.selectedItemLabel,
userFeedback: this.$.userFeedback.value,
userEmail: this.$.userEmail.value,
};
const selectedCloudProvider = this.$.cloudProvider.selectedItemLabel;
if (this.shouldShowCloudProvider && !!selectedCloudProvider) {
data['cloudProvider'] = selectedCloudProvider;
}
this.fire('SubmitFeedback', data);
this.$.dialog.close();
},
userEmailValueChanged: function() {
this.hasEnteredEmail = !!this.$.userEmail.value;
},
feedbackCategoryChanged: function() {
const selectedCategory = this.$.feedbackCategoryListbox.selected;
if (selectedCategory === this.feedbackCategories.INSTALLATION ||
selectedCategory === this.feedbackCategories.CONNECTION ||
selectedCategory === this.feedbackCategories.CONNECTION_OTHERS) {
this.shouldShowCloudProvider = true;
} else {
this.shouldShowCloudProvider = false;
}
this.$.dialog.notifyResize();
},
userFeedbackValueChanged: function() {
// Hides any error message when the user starts typing feedback.
this.$.userFeedback.invalid = false;
// Make the paper-dialog (vertically) re-center.
this.$.dialog.notifyResize();
},
// Returns whether the window's locale is English (i.e. EN, en-US) and the user has
// entered their email.
_computeShouldShowLanguageDisclaimer: function(hasEnteredEmail) {
return !window.navigator.language.match(/^en/i) && hasEnteredEmail;
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,13 +12,14 @@
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';
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/iron-fit-behavior/iron-fit-behavior.html" />
<dom-module id="outline-help-bubble">
<template>
import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js';
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 {
@ -123,49 +124,48 @@
<div class="upArrowWrapper"><div class="upArrow"></div></div>
<div class="downArrowWrapper"><div class="downArrow"></div></div>
<div class="helpContent"><slot></slot></div>
</template>
<script>
Polymer({
is: "outline-help-bubble",
behaviors: [
// Take all behaviors from IronFitBehavior, used for positioning relative
// to .positionTarget.
Polymer.IronFitBehavior,
],
ready: function() {
// Prevent help bubble from overlapping with it's positionTarget.
this.setAttribute("no-overlap", true);
`,
// Help bubble should default to hidden until show is called.
this.setAttribute("hidden", true);
},
show: function(positionTarget, arrowDirection, leftOrRightOffset) {
this.removeAttribute("hidden");
is: 'outline-help-bubble',
// Set arrow direction.
this.classList.add("showArrow-" + arrowDirection);
behaviors: [
IronFitBehavior,
],
// Apply left or right offset to arrow, e.g. display an up-pointing
// arrow on the top right.
const isUpOrDown = arrowDirection === "up" || arrowDirection === "down";
if (isUpOrDown && (leftOrRightOffset === "left" || leftOrRightOffset === "right")) {
this.classList.add("offset-" + leftOrRightOffset);
}
ready: function() {
// Prevent help bubble from overlapping with it's positionTarget.
this.setAttribute('no-overlap', true);
// Position the help bubble.
this.positionTarget = positionTarget;
this.refit();
// Help bubble should default to hidden until show is called.
this.setAttribute('hidden', true);
},
// Listen to scroll and resize events so the help bubble can reposition if needed.
window.addEventListener("scroll", this.refit.bind(this));
window.addEventListener("resize", this.refit.bind(this));
},
hide: function() {
this.setAttribute("hidden", true);
window.removeEventListener("scroll", this.refit.bind(this));
window.removeEventListener("resize", this.refit.bind(this));
this.fire("outline-help-bubble-dismissed");
},
});
</script>
</dom-module>
show: function(positionTarget, arrowDirection, leftOrRightOffset) {
this.removeAttribute('hidden');
// Set arrow direction.
this.classList.add('showArrow-' + arrowDirection);
// Apply left or right offset to arrow, e.g. display an up-pointing
// arrow on the top right.
const isUpOrDown = arrowDirection === 'up' || arrowDirection === 'down';
if (isUpOrDown && (leftOrRightOffset === 'left' || leftOrRightOffset === 'right')) {
this.classList.add('offset-' + leftOrRightOffset);
}
// Position the help bubble.
this.positionTarget = positionTarget;
this.refit();
// Listen to scroll and resize events so the help bubble can reposition if needed.
window.addEventListener('scroll', this.refit.bind(this));
window.addEventListener('resize', this.refit.bind(this));
},
hide: function() {
this.setAttribute('hidden', true);
window.removeEventListener('scroll', this.refit.bind(this));
window.removeEventListener('resize', this.refit.bind(this));
this.fire('outline-help-bubble-dismissed');
}
});

View file

@ -0,0 +1,41 @@
/*
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/iron-iconset-svg/iron-iconset-svg.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
const template = html`<iron-iconset-svg name="outline-iconset" size="24">
<svg>
<defs>
<g id="outline">
<path d="M10,18 C14.418278,18 18,14.418278 18,10 C18,5.581722 14.418278,2 10,2 C5.581722,2 2,5.581722 2,10 C2,14.418278 5.581722,18 10,18 Z M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z" id="Oval-2-Copy-75"></path>
<path d="M10,18 C12.7614237,18 15,15.7614237 15,13 C15,10.2385763 12.7614237,8 10,8 C7.23857625,8 5,10.2385763 5,13 C5,15.7614237 7.23857625,18 10,18 Z M10,20 C6.13400675,20 3,16.8659932 3,13 C3,9.13400675 6.13400675,6 10,6 C13.8659932,6 17,9.13400675 17,13 C17,16.8659932 13.8659932,20 10,20 Z" id="Oval-2-Copy-76"></path>
<path d="M10,18 C11.1045695,18 12,17.1045695 12,16 C12,14.8954305 11.1045695,14 10,14 C8.8954305,14 8,14.8954305 8,16 C8,17.1045695 8.8954305,18 10,18 Z M10,20 C7.790861,20 6,18.209139 6,16 C6,13.790861 7.790861,12 10,12 C12.209139,12 14,13.790861 14,16 C14,18.209139 12.209139,20 10,20 Z" id="Oval-2-Copy-77"></path>
</g>
<g id="devices">
<path d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4V6zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1zm-1 9h-4v-7h4v7z"></path>
</g>
<g id="share">
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"></path>
</g>
<g id="key">
<path d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"></path>
</g>
</defs>
</svg>
</iron-iconset-svg>`;
document.head.appendChild(template.content);

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,15 +12,18 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-step-view.html" />
<link rel="import" href="./style.css" />
*/
import '@polymer/polymer/polymer-legacy.js';
<dom-module id="outline-intro-step">
<template>
import '@polymer/paper-button/paper-button.js';
import './cloud-install-styles.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 {
@ -54,7 +57,7 @@
cursor: pointer;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.02), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
/* Card shadows (common to all cards).` */
/* Card shadows (common to all cards). */
.card:hover {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.1), 0 1px 10px 0 rgba(0, 0, 0, 0.2);
}
@ -178,38 +181,34 @@
<div class="container">
<div id="digital-ocean" class="card" on-tap="connectToDigitalOceanTapped">
<div class="card-header">
<div class="tag" hidden$="{{isSignedInToDigitalOcean}}">[[localize('setup-recommended')]]</div>
<div class="email" hidden$="{{!isSignedInToDigitalOcean}}">{{digitalOceanEmail}}</div>
<img src="images/do_white_logo.svg" />
<div class="tag" hidden\$="{{isSignedInToDigitalOcean}}">[[localize('setup-recommended')]]</div>
<div class="email" hidden\$="{{!isSignedInToDigitalOcean}}">{{digitalOceanEmail}}</div>
<img src="images/do_white_logo.svg">
</div>
<div class="card-title">DigitalOcean</div>
<div class="card-body">
<div class="description">
<ul hidden$="{{isSignedInToDigitalOcean}}">
<ul hidden\$="{{isSignedInToDigitalOcean}}">
<li>[[localize('setup-do-easiest')]]</li>
<li>[[localize('setup-do-cost')]]</li>
<li>[[localize('setup-do-data')]]</li>
<li>[[localize('setup-do-cancel')]]</li>
</ul>
<p hidden$="{{!isSignedInToDigitalOcean}}">
<p hidden\$="{{!isSignedInToDigitalOcean}}">
[[localize('setup-do-create')]]
</p>
</div>
</div>
<div class="card-footer">
<paper-button class="primary" hidden$="{{isSignedInToDigitalOcean}}"
>[[localize('setup-action')]]</paper-button
>
<paper-button class="primary" hidden$="{{!isSignedInToDigitalOcean}}"
>[[localize('setup-create')]]</paper-button
>
<paper-button class="primary" hidden\$="{{isSignedInToDigitalOcean}}">[[localize('setup-action')]]</paper-button>
<paper-button class="primary" hidden\$="{{!isSignedInToDigitalOcean}}">[[localize('setup-create')]]</paper-button>
</div>
</div>
<div id="gcp" class="card" on-tap="setUpGcpTapped">
<div class="card-header">
<div class="tag">[[localize('setup-advanced')]]</div>
<img src="images/gcp-logo.svg" />
<img src="images/gcp-logo.svg">
</div>
<div class="card-title">Google Cloud Platform</div>
<div class="card-body">
@ -229,7 +228,7 @@
<div id="aws" class="card" on-tap="setUpAwsTapped">
<div class="card-header">
<div class="tag">[[localize('setup-advanced')]]</div>
<img src="images/aws-logo.svg" />
<img src="images/aws-logo.svg">
</div>
<div class="card-title">Amazon Lightsail</div>
<div class="card-body">
@ -249,7 +248,7 @@
<div id="manual-server" class="card" on-tap="setUpGenericCloudProviderTapped">
<div class="card-header">
<div class="tag">[[localize('setup-advanced')]]</div>
<img src="images/cloud.svg" />
<img src="images/cloud.svg">
</div>
<div class="card-title">[[localize('setup-anywhere')]]</div>
<div class="card-body">
@ -266,30 +265,32 @@
</div>
</div>
</outline-step-view>
</template>
<script>
Polymer({
is: "outline-intro-step",
properties: {
isSignedInToDigitalOcean: Boolean,
digitalOceanEmail: String,
localize: {
type: Function,
readonly: true,
},
},
connectToDigitalOceanTapped: function() {
this.fire("ConnectToDigitalOcean");
},
setUpGenericCloudProviderTapped: function() {
this.fire("SetUpGenericCloudProviderRequested");
},
setUpAwsTapped: function() {
this.fire("SetUpAwsRequested");
},
setUpGcpTapped: function() {
this.fire("SetUpGcpRequested");
},
});
</script>
</dom-module>
`,
is: 'outline-intro-step',
properties: {
isSignedInToDigitalOcean: Boolean,
digitalOceanEmail: String,
localize: {
type: Function,
readonly: true,
},
},
connectToDigitalOceanTapped: function() {
this.fire('ConnectToDigitalOcean');
},
setUpGenericCloudProviderTapped: function() {
this.fire('SetUpGenericCloudProviderRequested');
},
setUpAwsTapped: function() {
this.fire('SetUpAwsRequested');
},
setUpGcpTapped: function() {
this.fire('SetUpGcpRequested');
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,22 +12,26 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
<link rel="import" href="../bower_components/iron-collapse/iron-collapse.html" />
<link rel="import" href="../bower_components/iron-icon/iron-icon.html" />
<link rel="import" href="../bower_components/iron-icons/iron-icons.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="../bower_components/paper-input/paper-textarea.html" />
<link rel="import" href="../bower_components/paper-progress/paper-progress.html" />
<link rel="import" href="./cloud-install-styles.html" />
<link rel="import" href="./outline-cloud-instructions-view.html" />
<link rel="import" href="./outline-step-view.html" />
<link rel="import" href="./style.css" />
*/
import '@polymer/polymer/polymer-legacy.js';
import '@polymer/iron-collapse/iron-collapse.js';
import '@polymer/iron-icon/iron-icon.js';
import '@polymer/iron-icons/iron-icons.js';
import '@polymer/paper-button/paper-button.js';
import '@polymer/paper-dialog/paper-dialog.js';
import '@polymer/paper-input/paper-textarea.js';
import '@polymer/paper-progress/paper-progress.js';
import './cloud-install-styles.js';
import './outline-cloud-instructions-view.js';
import './outline-step-view.js';
import './style.css';
<dom-module id="outline-manual-server-entry">
<template>
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Polymer({
_template: html`
<style include="cloud-install-styles"></style>
<style>
:host {
@ -173,45 +177,31 @@
<div class="card">
<!-- GCP -->
<div class="section" hidden$="[[!isCloudProviderGcp]]">
<div class="section" hidden\$="[[!isCloudProviderGcp]]">
<div class="section-header">
<!-- TODO(alalama): localize numbers -->
<span class="stepcircle">1</span>
<div class="instructions">[[localize('gcp-create-server')]]</div>
<div class="drop-down" on-tap="_toggleGcpCreateServerDropDown">
<img src="images/gcp-logo.svg" />
<img src="images/gcp-logo.svg">
<span>[[localize('manual-server-instructions')]]</span>
<iron-icon id="gcpCreateServerDropDownIcon" icon="arrow-drop-down"></iron-icon>
</div>
</div>
<iron-collapse id="gcpCreateServerDropDown" class="instructions-collapse">
<div class="section-content-instructions">
<outline-cloud-instructions-view
title="[[localize('gcp-create-project')]]"
thumbnail-path="images/gcp-create-project-thumbnail.png"
image-path="images/gcp-create-project-screenshot.png"
localize="[[localize]]"
>
<outline-cloud-instructions-view title="[[localize('gcp-create-project')]]" thumbnail-path="images/gcp-create-project-thumbnail.png" image-path="images/gcp-create-project-screenshot.png" localize="[[localize]]">
<ol>
<li
inner-h-t-m-l="[[localize('gcp-create-new-project', 'openLink', '<a href=https://console.cloud.google.com/projectcreate>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"
></li>
<li inner-h-t-m-l="[[localize('gcp-create-new-project', 'openLink', '<a href=https://console.cloud.google.com/projectcreate>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"></li>
<li>[[localize('gcp-name-your-project')]]</li>
<li>[[localize('gcp-click-create')]]</li>
</ol>
</outline-cloud-instructions-view>
</div>
<div class="section-content-instructions">
<outline-cloud-instructions-view
title="[[localize('manual-server-create-firewall')]]"
thumbnail-path="images/gcp-thumbnail-1.png"
image-path="images/gcp-screenshot-1.png"
localize="[[localize]]"
>
<outline-cloud-instructions-view title="[[localize('manual-server-create-firewall')]]" thumbnail-path="images/gcp-thumbnail-1.png" image-path="images/gcp-screenshot-1.png" localize="[[localize]]">
<ol>
<li
inner-h-t-m-l="[[localize('gcp-firewall-create-0', 'openLink', '<a href=https://console.cloud.google.com/networking/firewalls/add>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"
></li>
<li inner-h-t-m-l="[[localize('gcp-firewall-create-0', 'openLink', '<a href=https://console.cloud.google.com/networking/firewalls/add>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"></li>
<li>[[localize('gcp-firewall-create-1')]]</li>
<li>[[localize('gcp-firewall-create-2')]]</li>
<li>[[localize('gcp-firewall-create-3')]]</li>
@ -221,16 +211,9 @@
</outline-cloud-instructions-view>
</div>
<div class="section-content-instructions">
<outline-cloud-instructions-view
title="[[localize('gcp-create-vm')]]"
thumbnail-path="images/gcp-create-instance-thumbnail.png"
image-path="images/gcp-create-instance-screenshot.png"
localize="[[localize]]"
>
<outline-cloud-instructions-view title="[[localize('gcp-create-vm')]]" thumbnail-path="images/gcp-create-instance-thumbnail.png" image-path="images/gcp-create-instance-screenshot.png" localize="[[localize]]">
<ol>
<li
inner-h-t-m-l="[[localize('gcp-create-new-vm', 'openLink', '<a href=https://console.cloud.google.com/compute/instancesAdd>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"
></li>
<li inner-h-t-m-l="[[localize('gcp-create-new-vm', 'openLink', '<a href=https://console.cloud.google.com/compute/instancesAdd>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"></li>
<li>[[localize('gcp-type-outline-server')]]</li>
<li>[[localize('gcp-select-region')]]</li>
<li>[[localize('gcp-select-machine-type')]]</li>
@ -243,30 +226,23 @@
</iron-collapse>
</div>
<!-- AWS -->
<div class="section" hidden$="[[!isCloudProviderAws]]">
<div class="section" hidden\$="[[!isCloudProviderAws]]">
<div class="section-header">
<span class="stepcircle">1</span>
<div class="instructions">
[[localize('manual-server-firewall')]]
</div>
<div class="drop-down" on-tap="_toggleAwsDropDown">
<img id="aws-logo" src="images/aws-logo.svg" />
<img id="aws-logo" src="images/aws-logo.svg">
<span>[[localize('manual-server-instructions')]]</span>
<iron-icon id="awsDropDownIcon" icon="arrow-drop-down"></iron-icon>
</div>
</div>
<iron-collapse id="awsDropDown" class="instructions-collapse">
<div class="section-content-instructions">
<outline-cloud-instructions-view
title="[[localize('manual-server-create-group')]]"
thumbnail-path="images/aws-lightsail-thumbnail-1.png"
image-path="images/aws-lightsail-screenshot-1.png"
localize="[[localize]]"
>
<outline-cloud-instructions-view title="[[localize('manual-server-create-group')]]" thumbnail-path="images/aws-lightsail-thumbnail-1.png" image-path="images/aws-lightsail-screenshot-1.png" localize="[[localize]]">
<ol>
<li
inner-h-t-m-l="[[localize('aws-lightsail-firewall-0', 'openLink', '<a href=https://lightsail.aws.amazon.com>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"
></li>
<li inner-h-t-m-l="[[localize('aws-lightsail-firewall-0', 'openLink', '<a href=https://lightsail.aws.amazon.com>', 'closeLink', '<iron-icon icon=open-in-new></iron-icon></a>')]]"></li>
<li>[[localize('aws-lightsail-firewall-1')]]</li>
<li>[[localize('aws-lightsail-firewall-2')]]</li>
<li>[[localize('aws-lightsail-firewall-3')]]</li>
@ -287,7 +263,7 @@
</div>
<div class="section-content">
<div id="command">
sudo bash -c "$(wget -qO-
sudo bash -c "\$(wget -qO-
https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/server_manager/install_scripts/install_server.sh)"
</div>
</div>
@ -301,141 +277,144 @@
</div>
</div>
<div class="section-content">
<paper-textarea
id="serverConfig"
type="text"
placeholder$="[[placeholderText]]"
class="code"
rows="4"
max-rows="4"
no-label-float
on-value-changed="onServerConfigChanged"
></paper-textarea>
<paper-textarea id="serverConfig" type="text" placeholder\$="[[placeholderText]]" class="code" rows="4" max-rows="4" no-label-float="" on-value-changed="onServerConfigChanged"></paper-textarea>
</div>
</div>
<div id="button-row">
<paper-button id="cancelButton" on-tap="cancelTapped" class="secondary">[[localize('cancel')]]</paper-button>
<paper-button id="doneButton" on-tap="doneTapped" class="primary" disabled$="[[!enableDoneButton]]"
>[[localize('done')]]</paper-button
>
<paper-button id="doneButton" on-tap="doneTapped" class="primary" disabled\$="[[!enableDoneButton]]">[[localize('done')]]</paper-button>
</div>
<paper-progress hidden$="[[!showConnection]]" indeterminate class="slow"></paper-progress>
<paper-progress hidden\$="[[!showConnection]]" indeterminate="" class="slow"></paper-progress>
</div>
</outline-step-view>
</template>
<script>
Polymer({
is: "outline-manual-server-entry",
properties: {
placeholderText: {
type: String,
value:
'{"apiUrl":"https://xxx.xxx.xxx.xxx:xxxxx/xxxxxxxxxxxxxxxxxxxxxx","certSha256":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}',
},
showConnection: Boolean,
cloudProvider: {
type: String,
value: "generic",
},
cloudProviderName: {
type: String,
computed: "_computeCloudProviderName(cloudProvider)",
},
isCloudProviderAws: {
type: Boolean,
computed: "_computeIsCloudProviderAws(cloudProvider)",
},
isCloudProviderGcp: {
type: Boolean,
computed: "_computeIsCloudProviderGcp(cloudProvider)",
},
isGenericCloudProvider: {
type: Boolean,
computed: "_computeIsGenericCloudProvider(cloudProvider)",
},
installScriptStepNumber: {
type: Number,
computed: "_computeInstallScriptStepNumber(isGenericCloudProvider)",
},
pasteJsonStepNumber: {
type: Number,
computed: "_computePasteJsonStepNumber(installScriptStepNumber)",
},
enableDoneButton: {
type: Boolean,
value: false,
},
localize: {
type: Function,
readonly: true,
},
},
doneTapped: function() {
this.showConnection = true;
this.fire("ManualServerEntered", {
userInput: this.$.serverConfig.value,
});
},
cancelTapped: function() {
this.fire("ManualServerEntryCancelled");
},
retryTapped: function() {
this.showConnection = false;
this.doneTapped();
},
clear: function() {
this.$.serverConfig.value = "";
this.showConnection = false;
for (const dropdown of Polymer.dom(this.root).querySelectorAll(".instructions-collapse")) {
dropdown.hide();
}
},
_computeCloudProviderName: function(cloudProvider) {
switch (cloudProvider) {
case "aws":
return "Amazon Web Services";
case "gcp":
return "Google Cloud Platform";
default:
return "";
}
},
_computeIsCloudProviderAws: function(cloudProvider) {
return cloudProvider === "aws";
},
_computeIsCloudProviderGcp: function(cloudProvider) {
return cloudProvider === "gcp";
},
_computeIsGenericCloudProvider: function(cloudProvider) {
return cloudProvider === "generic";
},
_computeInstallScriptStepNumber: function(isGenericCloudProvider) {
return isGenericCloudProvider ? 1 : 2;
},
_computePasteJsonStepNumber: function(installScriptStepNumber) {
return installScriptStepNumber + 1;
},
_toggleAwsDropDown: function() {
this._toggleDropDown(this.$.awsDropDown, this.$.awsDropDownIcon);
},
_toggleGcpFirewallDropDown: function() {
this._toggleDropDown(this.$.gcpFirewallDropDown, this.$.gcpFirewallDropDownIcon);
},
_toggleGcpCreateServerDropDown: function() {
this._toggleDropDown(this.$.gcpCreateServerDropDown, this.$.gcpCreateServerDropDownIcon);
},
_toggleGcpCreateProjectDropDown: function() {
this._toggleDropDown(this.$.gcpCreateProjectDropDown, this.$.gcpCreateProjectDropDownIcon);
},
_toggleDropDown: function(dropDown, icon) {
dropDown.toggle();
icon.icon = dropDown.opened ? "arrow-drop-up" : "arrow-drop-down";
},
onServerConfigChanged: function() {
this.fire("ManualServerEdited", {
userInput: this.$.serverConfig.value,
});
},
`,
is: 'outline-manual-server-entry',
properties: {
placeholderText: {
type: String,
value:
'{"apiUrl":"https://xxx.xxx.xxx.xxx:xxxxx/xxxxxxxxxxxxxxxxxxxxxx","certSha256":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}',
},
showConnection: Boolean,
cloudProvider: {
type: String,
value: 'generic',
},
cloudProviderName: {
type: String,
computed: '_computeCloudProviderName(cloudProvider)',
},
isCloudProviderAws: {
type: Boolean,
computed: '_computeIsCloudProviderAws(cloudProvider)',
},
isCloudProviderGcp: {
type: Boolean,
computed: '_computeIsCloudProviderGcp(cloudProvider)',
},
isGenericCloudProvider: {
type: Boolean,
computed: '_computeIsGenericCloudProvider(cloudProvider)',
},
installScriptStepNumber: {
type: Number,
computed: '_computeInstallScriptStepNumber(isGenericCloudProvider)',
},
pasteJsonStepNumber: {
type: Number,
computed: '_computePasteJsonStepNumber(installScriptStepNumber)',
},
enableDoneButton: {
type: Boolean,
value: false,
},
localize: {
type: Function,
readonly: true,
},
},
doneTapped: function() {
this.showConnection = true;
this.fire('ManualServerEntered', {
userInput: this.$.serverConfig.value,
});
</script>
</dom-module>
},
cancelTapped: function() {
this.fire('ManualServerEntryCancelled');
},
retryTapped: function() {
this.showConnection = false;
this.doneTapped();
},
clear: function() {
this.$.serverConfig.value = '';
this.showConnection = false;
for (const dropdown of dom(this.root).querySelectorAll('.instructions-collapse')) {
dropdown.hide();
}
},
_computeCloudProviderName: function(cloudProvider) {
switch (cloudProvider) {
case 'aws':
return 'Amazon Web Services';
case 'gcp':
return 'Google Cloud Platform';
default:
return '';
}
},
_computeIsCloudProviderAws: function(cloudProvider) {
return cloudProvider === 'aws';
},
_computeIsCloudProviderGcp: function(cloudProvider) {
return cloudProvider === 'gcp';
},
_computeIsGenericCloudProvider: function(cloudProvider) {
return cloudProvider === 'generic';
},
_computeInstallScriptStepNumber: function(isGenericCloudProvider) {
return isGenericCloudProvider ? 1 : 2;
},
_computePasteJsonStepNumber: function(installScriptStepNumber) {
return installScriptStepNumber + 1;
},
_toggleAwsDropDown: function() {
this._toggleDropDown(this.$.awsDropDown, this.$.awsDropDownIcon);
},
_toggleGcpFirewallDropDown: function() {
this._toggleDropDown(this.$.gcpFirewallDropDown, this.$.gcpFirewallDropDownIcon);
},
_toggleGcpCreateServerDropDown: function() {
this._toggleDropDown(this.$.gcpCreateServerDropDown, this.$.gcpCreateServerDropDownIcon);
},
_toggleGcpCreateProjectDropDown: function() {
this._toggleDropDown(this.$.gcpCreateProjectDropDown, this.$.gcpCreateProjectDropDownIcon);
},
_toggleDropDown: function(dropDown, icon) {
dropDown.toggle();
icon.icon = dropDown.opened ? 'arrow-drop-up' : 'arrow-drop-down';
},
onServerConfigChanged: function() {
this.fire('ManualServerEdited', {
userInput: this.$.serverConfig.value,
});
}
});

View file

@ -0,0 +1,63 @@
/*
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-dialog/paper-dialog.js';
import './cloud-install-styles.js';
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">
.link {
text-decoration: underline;
color: rgba(0, 0, 0, 0.54);
}
</style>
<paper-dialog id="metricsEnabledDialog" modal="">
<div class="dialogBanner"><img src="images/metrics.png"></div>
<h3>[[localize('metrics-title')]]</h3>
<p inner-h-t-m-l="[[localize('metrics-description', 'openLink', '<a class=link href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"></p>
<div class="buttons">
<paper-button dialog-dismiss="" on-tap="disableMetricsRequested">[[localize('metrics-skip')]]</paper-button>
<paper-button autofocus="" dialog-dismiss="" on-tap="enableMetricsRequested">[[localize('metrics-share')]]</paper-button>
</div>
</paper-dialog>
`,
is: 'outline-metrics-option-dialog',
properties: {
localize: {
type: Function,
readonly: true,
},
},
showMetricsOptInDialog: function() {
this.$.metricsEnabledDialog.open();
},
enableMetricsRequested: function() {
this.fire('EnableMetricsRequested');
},
disableMetricsRequested: function() {
this.fire('DisableMetricsRequested');
}
});

View file

@ -0,0 +1,77 @@
/*
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-dialog/paper-dialog.js';
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 {
margin: 0px;
}
h3 {
margin-bottom: 0;
}
paper-button {
color: var(--primary-green);
}
</style>
<paper-dialog id="dialog" modal="">
<h3 hidden\$="[[!title]]">[[title]]</h3>
<div>[[text]]</div>
<p class="buttons">
<template is="dom-repeat" items="{{buttons}}">
<paper-button dialog-dismiss="" on-tap="buttonTapped">[[item]]</paper-button>
</template>
</p>
</paper-dialog>
`,
is: 'outline-modal-dialog',
properties: {
title: String,
text: String,
buttons: String,
},
// Returns a Promise which fulfills with the index of the button clicked.
open: function(title, text, buttons) {
this.title = title;
this.text = text;
this.buttons = buttons;
this.$.dialog.open();
return new Promise((fulfill, reject) => {
this.fulfill = fulfill;
this.reject = reject;
});
},
close: function() {
this.$.dialog.close();
},
buttonTapped: function(event) {
if (!this.fulfill) {
console.error('outline-modal-dialog: this.fulfill not defined');
return;
}
this.fulfill(event.model.index);
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,13 +12,16 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
*/
import '@polymer/polymer/polymer-legacy.js';
<!-- This is similar to the client's, except this spins endlessly and does not
support different connection states -->
<dom-module id="outline-progress-spinner">
<template>
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
// This is similar to the client's, except this spins endlessly and does not
// support different connection states.
Polymer({
_template: html`
<style>
@-webkit-keyframes rotating {
from {
@ -69,14 +72,11 @@ support different connection states -->
}
</style>
<div id="container">
<img id="lg" src="images/connected_large.png" />
<img id="md" src="images/connected_large.png" />
<img id="sm" src="images/connected_large.png" />
<img id="lg" src="images/connected_large.png">
<img id="md" src="images/connected_large.png">
<img id="sm" src="images/connected_large.png">
</div>
</template>
<script>
Polymer({
is: "outline-progress-spinner",
});
</script>
</dom-module>
`,
is: 'outline-progress-spinner'
});

View file

@ -0,0 +1,200 @@
/*
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;
}
});

View file

@ -0,0 +1,121 @@
/*
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,
},
start: 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);
},
stop: function() {
if (!this.updateIntervalId) {
return;
}
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
},
handleCancelTapped: function() {
this.fire('CancelServerCreationRequested');
}
});

View file

@ -0,0 +1,69 @@
/*
Copyright 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag.js';
// outline-server-settings-styles
// This file holds common styles for outline-server-settings and outline-validated-input
const styleElement = document.createElement('dom-module');
styleElement.appendChild(html`
<style>
/* Skip processing these with postcss-rtl as it incorrectly parses the border-color
in the paper-input-container-underline-focus mixin.
https://github.com/vkalinichev/postcss-rtl/issues/50 */
/* rtl:begin:ignore */
paper-input {
/* Matches the max width of outline-validated-input */
max-width: 545px;
/* Removes extra padding added by children of paper-input */
margin-top: -8px;
/* Create space for error messages */
margin-bottom: 14px;
--paper-input-container-label-focus: {
color: var(--primary-green);
}
--paper-input-container-underline-focus: {
border-color: var(--primary-green);
}
--paper-input-container-label: {
font-size: 14px;
line-height: 22px;
}
--paper-input-container-color: var(--medium-gray);
--paper-input-container-input: {
color: #fff;
}
--paper-input-container-invalid-color: #f28b82;
}
/* rtl:end:ignore */
paper-input[readonly] {
--paper-input-container-underline: {
display: none;
}
--paper-input-container-underline-focus: {
display: none;
}
--paper-input-container-underline-disabled: {
display: none;
}
--paper-input-container-disabled: {
opacity: 1;
}
}
</style>`);
styleElement.register('outline-server-settings-styles');

View file

@ -0,0 +1,365 @@
/*
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-checkbox/paper-checkbox.js';
import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js';
import '@polymer/paper-input/paper-input.js';
import './cloud-install-styles.js';
import './outline-server-settings-styles.js';
import './outline-iconset.js';
import './outline-validated-input.js';
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 include="outline-server-settings-styles"></style>
<style>
.content {
flex-grow: 1;
}
.setting {
padding: 24px;
align-items: flex-start;
}
.setting:not(:first-child) {
margin-top: 8px;
}
.setting-icon,
img.setting-icon {
margin-right: 24px;
color: #fff;
opacity: 0.87;
}
.setting > div {
width: 100%;
}
.setting h3 {
margin: 0 0 16px 0;
padding: 0;
color: #fff;
font-size: 16px;
width: 100%;
}
.setting p {
margin-bottom: 12px;
width: 60%;
color: var(--medium-gray);
}
#experiments p {
width: 80%;
}
#experiments .sub-section p {
width: 100%;
}
.sub-section {
background: var(--border-color);
padding: 16px;
margin: 24px 0;
display: flex;
align-items: center;
border-radius: 2px;
}
.sub-section iron-icon {
margin-right: 16px;
}
.selection-container {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.selection-container > .content {
flex: 4;
}
.selection-container > paper-dropdown-menu {
flex: 1;
}
.data-limits-input {
display: flex;
align-items: center;
}
.data-limits-input paper-input:not([readonly]) {
width: auto;
--paper-input-container: {
width: 120px;
}
}
.detail {
margin-top: 0px;
font-size: 12px;
}
paper-input:not([readonly]) {
width: 60%;
}
paper-dropdown-menu {
border: 1px solid var(--medium-gray);
border-radius: 4px;
max-width: 150px;
--paper-input-container: {
padding: 0 4px;
text-align: center;
}
--paper-input-container-input: {
color: var(--medium-gray);
font-size: 14px;
}
--paper-dropdown-menu-ripple: {
display: none;
}
--paper-input-container-underline: {
display: none;
}
--paper-input-container-underline-focus: {
display: none;
}
}
.data-limits-input paper-dropdown-menu {
border: none;
--paper-input-container: {
width: 64px;
}
}
paper-listbox paper-item {
font-size: 14px;
}
paper-listbox paper-item:hover {
cursor: pointer;
background-color: #eee;
}
#data-limits-container .selection-container h3 {
font-weight: normal;
margin-bottom: 8px;
}
#data-limits-container .selection-container p {
margin: 0 0 24px 0;
}
#data-limits-container .selection-container span {
display: block;
margin-top: 6px;
}
paper-checkbox {
/* We want the ink to be the color we're going to, not coming from */
--paper-checkbox-checked-color: var(--primary-green);
--paper-checkbox-checked-ink-color: var(--dark-gray);
--paper-checkbox-unchecked-color: var(--light-gray);
--paper-checkbox-unchecked-ink-color: var(--primary-green);
}
.selection-container paper-checkbox {
margin-right: 4px;
}
</style>
<div class="container">
<div class="content">
<!-- DO information -->
<div class="setting card-section" hidden\$="[[!isServerManaged]]">
<img class="setting-icon digital-ocean-icon" src="images/do_white_logo.svg">
<div>
<h3>DigitalOcean</h3>
<paper-input readonly="" value="[[serverLocation]]" label="[[localize('settings-server-location')]]" hidden\$="[[!serverLocation]]" always-float-label="" maxlength="100"></paper-input>
<paper-input readonly="" value="[[serverMonthlyCost]] USD" label="[[localize('settings-server-cost')]]" hidden\$="[[!serverMonthlyCost]]" always-float-label="" maxlength="100"></paper-input>
<paper-input readonly="" value="[[serverMonthlyTransferLimit]]" label="[[localize('settings-transfer-limit')]]" hidden\$="[[!serverMonthlyTransferLimit]]" always-float-label="" maxlength="100"></paper-input>
</div>
</div>
<div class="setting card-section">
<iron-icon class="setting-icon" icon="outline-iconset:outline"></iron-icon>
<div>
<h3>[[localize('settings-server-info')]]</h3>
<!-- TODO: consider making this an outline-validated-input -->
<paper-input id="serverNameInput" class="server-name" value="{{serverName}}" label="[[localize('settings-server-name')]]" always-float-label="" maxlength="100" on-keydown="_handleNameInputKeyDown" on-blur="_handleNameInputBlur"></paper-input>
<p class="detail">[[localize('settings-server-rename')]]</p>
<outline-validated-input editable="[[isAccessKeyPortEditable]]" visible="[[serverPortForNewAccessKeys]]" label="[[localize('settings-access-key-port')]]" allowed-pattern="[0-9]{1,5}" max-length="5" value="[[serverPortForNewAccessKeys]]" client-side-validator="[[_validatePort]]" event="ChangePortForNewAccessKeysRequested" localize="[[localize]]"></outline-validated-input>
<outline-validated-input editable="[[isHostnameEditable]]" visible="[[serverHostname]]" label="[[localize('settings-server-hostname')]]" max-length="253" value="[[serverHostname]]" event="ChangeHostnameForAccessKeysRequested" localize="[[localize]]"></outline-validated-input>
<paper-input readonly="" value="[[serverManagementApiUrl]]" label="[[localize('settings-server-api-url')]]" hidden\$="[[!serverManagementApiUrl]]" always-float-label="" maxlength="100"></paper-input>
<paper-input readonly="" value="[[serverCreationDate]]" label="[[localize('settings-server-creation')]]" hidden\$="[[!serverCreationDate]]" always-float-label="" maxlength="100"></paper-input>
<paper-input readonly="" value="[[serverId]]" label="[[localize('settings-server-id')]]" hidden\$="[[!serverId]]" always-float-label="" maxlength="100"></paper-input>
<paper-input readonly="" value="[[serverVersion]]" label="[[localize('settings-server-version')]]" hidden\$="[[!serverVersion]]" always-float-label="" maxlength="100"></paper-input>
</div>
</div>
<!-- Experiments -->
<div id="experiments" class="setting card-section" hidden\$="[[!shouldShowExperiments]]">
<iron-icon class="setting-icon" icon="icons:build"></iron-icon>
<div>
<h3>[[localize('experiments')]]</h3>
<p>[[localize('experiments-description')]]</p>
<div class="sub-section">
<iron-icon icon="icons:error-outline"></iron-icon>
<p inner-h-t-m-l="[[localize('experiments-disclaimer', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"></p>
</div>
<div id="data-limits-container" hidden\$="[[!supportsAccessKeyDataLimit]]">
<div class="selection-container">
<div class="content">
<h3>[[localize('data-limits')]]</h3>
<p>
[[localize('data-limits-description', 'date', dataLimitsAvailabilityDate)]]<br>
<span inner-h-t-m-l="[[localize('experiments-feedback', 'openLink', '<a href=https://docs.google.com/forms/d/e/1FAIpQLSfP7q9GnJCQyWMpFTLd9zwCm7cvUa-2NR8a8SznwMSbuRnrWg/viewform>', 'closeLink', '</a>')]]"></span>
</p>
</div>
<!-- NOTE: The dropdown is not automatically sized to the button's width:
https://github.com/PolymerElements/paper-dropdown-menu/issues/229 -->
<paper-dropdown-menu no-label-float="" horizontal-align="left">
<paper-listbox slot="dropdown-content" selected="{{_computeDataLimitsEnabledName(isAccessKeyDataLimitEnabled)}}" attr-for-selected="name" on-selected-changed="_accessKeyDataLimitEnabledChanged">
<paper-item name="enabled">[[localize('enabled')]]</paper-item>
<paper-item name="disabled">[[localize('disabled')]]</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="data-limits-input" hidden\$="[[!isAccessKeyDataLimitEnabled]]">
<paper-input id="accessKeyDataLimitInput" value="[[accessKeyDataLimit.value]]" label="Data limit per key" always-float-label="" allowed-pattern="[0-9]+" required="" auto-validate="" maxlength="9" on-keydown="_handleAccessKeyDataLimitInputKeyDown" on-blur="_requestSetAccessKeyDataLimit"></paper-input>
<paper-dropdown-menu no-label-float="">
<paper-listbox id="accessKeyDataLimitUnits" slot="dropdown-content" selected="[[accessKeyDataLimit.unit]]" attr-for-selected="name" on-selected-changed="_requestSetAccessKeyDataLimit">
<paper-item name="MB">MB</paper-item>
<paper-item name="GB">GB</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
</div>
</div>
</div>
<!-- Metrics controls -->
<div class="setting card-section">
<iron-icon class="setting-icon" icon="editor:insert-chart"></iron-icon>
<div>
<div class="selection-container">
<paper-checkbox checked="{{metricsEnabled}}"></paper-checkbox>
<h3>[[localize('settings-metrics-header')]]</h3>
</div>
<p inner-h-t-m-l="[[localize('metrics-description', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/index.html#/en/support/dataCollection>', 'closeLink', '</a>')]]"></p>
</div>
</div>
</div>
</div>
`,
is: 'outline-server-settings',
properties: {
isServerManaged: Boolean,
serverName: String,
metricsEnabled: {type: Boolean, observer: '_metricsEnabledChanged'},
// Initialize to null so we can use the hidden attribute, which does not work well with
// undefined values.
serverId: {type: String, value: null},
serverHostname: {type: String, value: null},
serverManagementApiUrl: {type: String, value: null},
serverPortForNewAccessKeys: {type: Number, value: null},
serverVersion: {type: String, value: null},
isAccessKeyPortEditable: {type: Boolean, value: false},
isAccessKeyDataLimitEnabled: {type: Boolean, notify: true},
accessKeyDataLimit: {type: Object, value: null}, // type: app.DisplayDataAmount
supportsAccessKeyDataLimit:
{type: Boolean, value: false}, // Whether the server supports data limits.
dataLimitsAvailabilityDate:
{type: String}, // Date by which the feature stops being an experiment.
isHostnameEditable: {type: Boolean, value: true},
serverCreationDate: {type: String, value: null},
serverLocation: {type: String, value: null},
serverMonthlyCost: {type: String, value: null},
serverMonthlyTransferLimit: {type: String, value: null},
localize: {type: Function, readonly: true},
shouldShowExperiments:
{type: Boolean, computed: '_computeShouldShowExperiments(supportsAccessKeyDataLimit)'},
},
update: function(name, metricsEnabled) {
this.initialName = name;
this.name = name;
this.metricsEnabled = metricsEnabled;
},
_handleNameInputKeyDown: function(event) {
if (event.key === 'Escape') {
this.serverName = this.initialName;
this.$.serverNameInput.blur();
} else if (event.key === 'Enter') {
this.$.serverNameInput.blur();
}
},
_handleNameInputBlur: function(event) {
const newName = this.serverName;
if (!newName) {
this.serverName = this.initialName;
return;
}
// Fire signal if name has changed.
if (newName !== this.initialName) {
this.fire('ServerRenameRequested', {newName});
}
},
_metricsEnabledChanged: function(newMetricsEnabled, oldMetricsEnabled) {
if (oldMetricsEnabled === undefined || newMetricsEnabled === undefined) {
return;
}
// Fire signal if metrics changed.
if (newMetricsEnabled !== oldMetricsEnabled) {
const metricsSignal =
newMetricsEnabled ? 'EnableMetricsRequested' : 'DisableMetricsRequested';
this.fire(metricsSignal);
}
},
_accessKeyDataLimitEnabledChanged: function(e) {
const wasDataLimitEnabled = this.isAccessKeyDataLimitEnabled;
const isDataLimitEnabled = e.detail.value === 'enabled';
if (isDataLimitEnabled === undefined || wasDataLimitEnabled === undefined) {
return;
} else if (isDataLimitEnabled === wasDataLimitEnabled) {
return;
}
this.isAccessKeyDataLimitEnabled = isDataLimitEnabled;
if (isDataLimitEnabled) {
this._requestSetAccessKeyDataLimit();
} else {
this.fire('RemoveAccessKeyDataLimitRequested');
}
},
_handleAccessKeyDataLimitInputKeyDown: function(event) {
if (event.key === 'Escape') {
this.$.accessKeyDataLimitInput.value = this.accessKeyDataLimit.value;
this.$.accessKeyDataLimitInput.blur();
} else if (event.key === 'Enter') {
this.$.accessKeyDataLimitInput.blur();
}
},
_requestSetAccessKeyDataLimit: function() {
if (this.$.accessKeyDataLimitInput.invalid) {
return;
}
const value = Number(this.$.accessKeyDataLimitInput.value);
const unit = this.$.accessKeyDataLimitUnits.selected;
this.fire('SetAccessKeyDataLimitRequested', {limit: {value, unit}});
},
_computeDataLimitsEnabledName: function(isAccessKeyDataLimitEnabled) {
return isAccessKeyDataLimitEnabled ? 'enabled' : 'disabled';
},
_computeShouldShowExperiments: function(supportsAccessKeyDataLimit) {
return supportsAccessKeyDataLimit;
},
_validatePort: function(value) {
const port = Number(value);
const valid = !Number.isNaN(port) && port >= 1 && port <= 65535 && Number.isInteger(port);
return valid ? '' : this.localize('error-keys-port-bad-input');
}
});

View file

@ -0,0 +1,902 @@
/*
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-dialog/paper-dialog.js';
import '@polymer/iron-icons/iron-icons.js';
import '@polymer/iron-pages/iron-pages.js';
import '@polymer/iron-icons/editor-icons.js';
import '@polymer/iron-icons/social-icons.js';
import '@polymer/paper-icon-button/paper-icon-button.js';
import '@polymer/paper-item/paper-item.js';
import '@polymer/paper-listbox/paper-listbox.js';
import '@polymer/paper-menu-button/paper-menu-button.js';
import '@polymer/paper-progress/paper-progress.js';
import '@polymer/paper-tabs/paper-tabs.js';
import '@polymer/paper-tooltip/paper-tooltip.js';
import './cloud-install-styles.js';
import './outline-iconset.js';
import './outline-help-bubble.js';
import './outline-metrics-option-dialog.js';
import './outline-server-settings.js';
import './outline-share-dialog.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import * as byte_size from 'byte-size';
byte_size.defaultOptions({
units: 'metric',
toStringFn: function() {
return `${this.value} ${this.unit}`;
},
});
const MY_CONNECTION_USER_ID = '0';
Polymer({
_template: html`
<style include="cloud-install-styles"></style>
<style>
.container {
display: flex;
flex-direction: column;
padding: 24px;
color: var(--light-gray);
}
.tabs-container {
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--border-color);
}
.tabs-spacer {
flex: 2;
}
paper-tabs {
flex: 1;
--paper-tabs-selection-bar-color: var(--primary-green);
--paper-tab-ink: var(--primary-green);
--paper-tab-content-unselected {
color: var(--dark-gray);
}
}
div[name="connections"],
div[name="settings"],
.access-key-list {
margin-top: 24px;
}
.server-header {
display: flex;
flex-direction: column;
margin: 24px 0;
}
.server-name {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.server-name h3 {
font-size: 36px;
font-weight: 400;
color: #ffffff;
flex: 11;
margin: 0 0 6px 0;
}
.server-location,
.unreachable-server paper-button {
color: var(--medium-gray);
}
.unreachable-server {
flex-direction: column;
align-items: center;
margin-top: 24px;
padding: 72px 48px;
}
.unreachable-server p {
line-height: 22px;
max-width: 50ch;
text-align: center;
color: var(--medium-gray);
}
.unreachable-server .button-container {
padding: 24px 0;
}
.unreachable-server paper-button.try-again-btn {
color: var(--primary-green);
}
.server-img {
width: 142px;
height: 142px;
margin: 24px;
}
.stats-container {
display: flex;
flex-direction: row;
margin: 0 -8px;
}
.stats-card {
flex-direction: column;
flex: 1;
margin: 0 8px;
}
.stats {
margin: 30px 0 18px 0;
color: #fff;
white-space: nowrap;
}
.stats > * {
font-weight: 300;
display: inline-block;
margin: 0;
}
.stats h3 {
font-size: 48px;
}
.stats p,
.stats-card p {
margin: 0;
font-size: 14px;
font-weight: normal;
}
@media (max-width: 938px) {
/* Reduce the cards' size so they fit comfortably on small displays. */
.stats {
margin-top: 24px;
}
.stats h3 {
font-size: 42px;
}
.stats-card p {
font-size: 12px;
}
}
.transfer-stats .stats,
.transfer-stats .stats p {
color: var(--primary-green);
}
.stats-card p,
.stats-card iron-icon {
color: var(--medium-gray);
}
.access-key-list {
flex-direction: column;
padding: 24px 16px 24px 30px;
}
.access-key-row {
display: flex;
align-items: center;
justify-content: center;
margin: 15px 0;
}
.header-row {
font-size: 12px;
color: var(--medium-gray);
margin-bottom: 12px;
}
.header-row paper-button {
color: white;
height: 32px;
font-size: 13px;
padding: 0 28px 0px 28px;
background-color: #00bfa5;
margin: 0px 12px 0px 96px;
height: 36px;
}
.header-row-spacer {
/* 24px (share icon) + 40px (overflow menu) + 8px (margin) */
min-width: 72px;
}
.measurement-container {
display: flex;
flex: 3;
align-items: center;
}
.measurement-container paper-progress {
max-width: 72px;
margin: 0 24px 0 12px;
--paper-progress-height: 8px;
--paper-progress-active-color: var(--primary-green);
}
.measurement-container paper-progress.data-limits {
border: 1px solid var(--primary-green);
border-radius: 2px;
}
@media (max-width: 640px) {
.measurement-container paper-progress {
width: 48px;
}
}
paper-progress.data-limits:hover {
cursor: pointer;
}
.measurement {
/* Space the usage bars evenly */
min-width: 9ch;
/* We don't want numbers separated from their units */
white-space: nowrap;
font-size: 14px;
color: var(--medium-gray);
line-height: 24px;
}
.access-key-container {
display: flex;
flex: 4;
align-items: center;
}
.sort-icon {
/* Disable click events on the sorting icons, so that the event gets propagated to the
parent, which defines the dataset elements needed for displaying the icon. */
pointer-events: none;
}
#manager-access-key-description {
font-size: 12px;
font-weight: 400;
margin-top: 4px;
color: var(--medium-gray);
}
.manager-access-key-icon.access-key-icon {
height: 48px;
}
.access-key-icon {
width: 42px;
height: 42px;
margin-right: 18px;
}
.access-key-name {
display: flex;
flex-direction: column;
font-weight: 500;
}
input.access-key-name {
font-family: inherit;
font-size: inherit;
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid transparent;
border-radius: 2px;
padding: 4px 8px;
position: relative;
left: -8px; /* Align with padding */
background: var(--background-contrast-color);
color: var(--light-gray);
}
input.access-key-name::placeholder {
opacity: inherit;
color: inherit;
}
input.access-key-name:hover {
border-bottom-color: var(--border-color);
}
input.access-key-name:focus {
border-bottom-color: var(--primary-green);
border-radius: 0px;
outline: none;
font-weight: normal;
}
.overflow-menu {
display: flex;
justify-content: flex-end;
padding: 0px;
min-width: 40px;
margin-left: 8px;
color: var(--medium-gray);
}
.overflow-menu paper-item {
cursor: pointer;
}
paper-item {
font-size: 14px;
}
paper-listbox iron-icon {
margin-right: 10px;
width: 18px;
}
paper-dropdown {
box-shadow: 0px 0px 20px #999999;
}
#addAccessKeyButton {
background: var(--primary-green);
color: #fff;
border-radius: 50%;
}
.actions {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
text-align: end;
}
.connect-button,
.share-button {
color: white;
padding: 0;
margin: 0;
height: 24px;
width: 24px;
}
.add-new-key {
color: var(--primary-green);
cursor: pointer;
}
outline-help-bubble {
text-align: center;
}
outline-help-bubble h3 {
padding-bottom: 0;
font-weight: 500;
line-height: 28px;
font-size: 16px;
margin: 0px 0px 12px 0px;
}
outline-help-bubble img {
width: 76px;
height: auto;
margin: 12px 0px 24px 0px;
}
.digital-ocean-icon {
opacity: 0.54;
}
.flex-1 {
flex: 1;
}
/* Mirror icons */
:host(:dir(rtl)) iron-icon,
:host(:dir(rtl)) .share-button,
:host(:dir(rtl)) .access-key-icon {
transform: scaleX(-1);
}
</style>
<div class="container">
<div class="server-header">
<div class="server-name">
<h3>[[serverName]]</h3>
<paper-menu-button horizontal-align="right" class="overflow-menu flex-1" hidden\$="{{!isServerReachable}}" close-on-activate="" no-animations="" dynamic-align="" no-overlap="">
<paper-icon-button icon="more-vert" slot="dropdown-trigger"></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item hidden\$="[[!isServerManaged]]" on-tap="destroyServer">
<iron-icon icon="icons:remove-circle-outline"></iron-icon>[[localize('server-destroy')]]
</paper-item>
<paper-item hidden\$="[[isServerManaged]]" on-tap="removeServer">
<iron-icon icon="icons:remove-circle-outline"></iron-icon>[[localize('server-remove')]]
</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
<div class="server-location" hidden\$="{{!isServerReachable}}">[[serverLocation]]</div>
</div>
<div class="tabs-container" hidden\$="{{!isServerReachable}}">
<div class="tabs-spacer"></div>
<paper-tabs selected="{{selectedTab}}" attr-for-selected="name" noink="">
<paper-tab name="connections">[[localize('server-connections')]]</paper-tab>
<paper-tab name="settings" id="settingsTab">[[localize('server-settings')]]</paper-tab>
</paper-tabs>
</div>
<!-- Unreachable server display -->
<div class="card-section unreachable-server" hidden\$="{{isServerReachable}}">
<img class="server-img" src="images/server-unreachable.png">
<h3>[[localize('server-unreachable')]]</h3>
<p></p>
<div>[[localize('server-unreachable-description')]]</div>
<span hidden\$="{{isServerManaged}}">[[localize('server-unreachable-managed-description')]]</span>
<span hidden\$="{{!isServerManaged}}">[[localize('server-unreachable-manual-description')]]</span>
<div class="button-container">
<paper-button on-tap="removeServer" hidden\$="{{isServerManaged}}">[[localize('server-remove')]]</paper-button>
<paper-button on-tap="destroyServer" hidden\$="{{!isServerManaged}}">[[localize('server-destroy')]]</paper-button>
<paper-button on-tap="retryDisplayingServer" class="try-again-btn">[[localize('retry')]]</paper-button>
</div>
</div>
<iron-pages selected="{{selectedTab}}" attr-for-selected="name" hidden\$="{{!isServerReachable}}">
<div name="connections">
<div class="stats-container">
<div class="stats-card transfer-stats card-section">
<iron-icon icon="icons:swap-horiz"></iron-icon>
<div class="stats">
<h3>[[_getFormattedTransferredValue(totalInboundBytes, '0')]]</h3>
<p>[[_getFormattedTransferredUnit(totalInboundBytes, 'B')]]</p>
</div>
<p>[[localize('server-data-transfer')]]</p>
</div>
<div hidden\$="[[!isServerManaged]]" class="stats-card card-section">
<div>
<img class="digital-ocean-icon" src="images/do_white_logo.svg">
</div>
<div class="stats">
<h3>[[managedServerUtilzationPercentage]]</h3>
<p>/[[_formatBytesTransferred(monthlyOutboundTransferBytes)]]</p>
</div>
<p>[[localize('server-data-used')]]</p>
</div>
<div class="stats-card card-section">
<iron-icon icon="outline-iconset:key"></iron-icon>
<div class="stats">
<h3>[[accessKeyRows.length]]</h3>
<p>[[localize('server-keys')]]</p>
</div>
<p>[[localize('server-access')]]</p>
</div>
</div>
<div class="access-key-list card-section">
<!-- header row -->
<div class="access-key-row header-row">
<span class="access-key-container" on-tap="_setSortByOrToggleDirection" data-sort-by="name">
[[localize('server-access-keys')]]
<iron-icon class="sort-icon" icon="arrow-drop-down" hidden\$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'name', -1)]]"></iron-icon>
<iron-icon class="sort-icon" icon="arrow-drop-up" hidden\$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'name', 1)]]"></iron-icon>
</span>
<span class="measurement-container" on-tap="_setSortByOrToggleDirection" data-sort-by="usage">
[[localize('server-usage')]]
<iron-icon class="sort-icon" icon="arrow-drop-down" hidden\$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'usage', -1)]]"></iron-icon>
<iron-icon class="sort-icon" icon="arrow-drop-up" hidden\$="[[!_computeShouldShowSortIcon(accessKeySortBy, accessKeySortDirection, 'usage', 1)]]"></iron-icon>
</span>
<span class="flex-1 header-row-spacer"></span>
</div>
<!-- admin row -->
<div class="access-key-row" id="managerRow">
<span class="access-key-container">
<img class="manager-access-key-icon access-key-icon" src="images/manager-key-avatar.svg">
<div class="access-key-name">
<div>[[localize('server-my-access-key')]]</div>
<div id="manager-access-key-description">[[localize('server-connect-devices')]]</div>
</div>
</span>
<span class="measurement-container">
<span class="measurement">[[_formatBytesTransferred(myConnection.transferredBytes, "...")]]</span>
<paper-progress value="[[myConnection.relativeTraffic]]" class\$="[[_computePaperProgressClass(isAccessKeyDataLimitEnabled)]]"></paper-progress>
<paper-tooltip animation-delay="0" offset="0" position="top" hidden\$="[[!isAccessKeyDataLimitEnabled]]">
[[_getDataLimitsUsageString(myConnection)]]
</paper-tooltip>
</span>
<span class="actions">
<span class="flex-1">
<paper-icon-button icon="outline-iconset:devices" class="connect-button" on-tap="_handleConnectPressed"></paper-icon-button>
</span>
<span class="overflow-menu flex-1"></span>
</span>
</div>
<div id="accessKeysContainer">
<!-- rows for each access key -->
<template is="dom-repeat" items="{{accessKeyRows}}" filter="isRegularConnection" sort="{{_sortAccessKeys(accessKeySortBy, accessKeySortDirection)}}" observe="name transferredBytes">
<!-- TODO(alalama): why is observe not responding to rename? -->
<div class="access-key-row">
<span class="access-key-container">
<img class="access-key-icon" src="images/key-avatar.svg">
<input type="text" class="access-key-name" id\$="access-key-[[item.id]]" placeholder="{{item.placeholderName}}" value="[[item.name]]" on-blur="_handleNameInputBlur" on-keydown="_handleNameInputKeyDown">
</span>
<span class="measurement-container">
<span class="measurement">[[_formatBytesTransferred(item.transferredBytes, "...")]]</span>
<paper-progress value="[[item.relativeTraffic]]" class\$="[[_computePaperProgressClass(isAccessKeyDataLimitEnabled)]]"></paper-progress>
<paper-tooltip animation-delay="0" offset="0" position="top" hidden\$="[[!isAccessKeyDataLimitEnabled]]">
[[_getDataLimitsUsageString(item)]]
</paper-tooltip>
</span>
<span class="actions">
<span class="flex-1">
<paper-icon-button icon="outline-iconset:share" class="share-button" on-tap="_handleShareCodePressed"></paper-icon-button>
</span>
<span class="flex-1">
<paper-menu-button horizontal-align="right" class="overflow-menu" close-on-activate="" no-animations="" no-overlap="" dynamic-align="">
<paper-icon-button icon="more-vert" slot="dropdown-trigger"></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item on-tap="_handleRenameAccessKeyPressed">
<iron-icon icon="icons:create"></iron-icon>[[localize('server-access-key-rename')]]
</paper-item>
<paper-item on-tap="_handleRemoveAccessKeyPressed">
<iron-icon icon="icons:delete"></iron-icon>[[localize('remove')]]
</paper-item>
</paper-listbox>
</paper-menu-button>
</span>
</span>
</div>
</template>
</div>
<!-- add key button -->
<div class="access-key-row" id="addAccessKeyRow">
<span class="access-key-container">
<paper-icon-button icon="icons:add" on-tap="_handleAddAccessKeyPressed" id="addAccessKeyButton" class="access-key-icon"></paper-icon-button>
<div class="add-new-key" on-tap="_handleAddAccessKeyPressed">[[localize('server-access-key-new')]]</div>
</span>
</div>
</div>
</div>
<div name="settings">
<outline-server-settings id="serverSettings" server-id="[[serverId]]" server-hostname="[[serverHostname]]" server-name="[[serverName]]" server-version="[[serverVersion]]" is-hostname-editable="[[isHostnameEditable]]" server-management-api-url="[[serverManagementApiUrl]]" server-port-for-new-access-keys="[[serverPortForNewAccessKeys]]" is-access-key-port-editable="[[isAccessKeyPortEditable]]" access-key-data-limit="{{accessKeyDataLimit}}" is-access-key-data-limit-enabled="{{isAccessKeyDataLimitEnabled}}" data-limits-availability-date="[[dataLimitsAvailabilityDate]]" supports-access-key-data-limit="[[supportsAccessKeyDataLimit]]" server-creation-date="[[serverCreationDate]]" server-monthly-cost="[[monthlyCost]]" server-monthly-transfer-limit="[[_formatBytesTransferred(monthlyOutboundTransferBytes)]]" is-server-managed="[[isServerManaged]]" server-location="[[serverLocation]]" localize="[[localize]]">
</outline-server-settings>
</div>
</iron-pages>
</div>
<outline-help-bubble id="getConnectedHelpBubble" vertical-align="bottom" horizontal-align="right">
<img src="images/connect-tip-2x.png">
<h3>[[localize('server-help-connection-title')]]</h3>
<p>[[localize('server-help-connection-description')]]</p>
<paper-button on-tap="closeGetConnectedHelpBubble">[[localize('server-help-connection-ok')]]</paper-button>
</outline-help-bubble>
<outline-help-bubble id="addAccessKeyHelpBubble" vertical-align="bottom" horizontal-align="left">
<img src="images/key-tip-2x.png">
<h3>[[localize('server-help-access-key-title')]]</h3>
<p>[[localize('server-help-access-key-description')]]</p>
<paper-button on-tap="closeAddAccessKeyHelpBubble">[[localize('server-help-access-key-next')]]</paper-button>
</outline-help-bubble>
<outline-help-bubble id="dataLimitsHelpBubble" vertical-align="top" horizontal-align="right">
<h3>[[localize('data-limits-dialog-title')]]</h3>
<p>[[localize('data-limits-dialog-text')]]</p>
<paper-button on-tap="closeDataLimitsHelpBubble">[[localize('ok')]]</paper-button>
</outline-help-bubble>
`,
is: 'outline-server-view',
properties: {
serverId: String,
serverName: String,
serverHostname: String,
serverVersion: String,
isHostnameEditable: {type: Boolean, value: false},
serverManagementApiUrl: String,
serverPortForNewAccessKeys: Number,
isAccessKeyPortEditable: {type: Boolean, value: false},
serverCreationDate: String,
serverLocation: String,
accessKeyDataLimit: {type: Object, value: null},
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.
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: []},
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',
},
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,
},
},
observers: [
'_accessKeysAddedOrRemoved(accessKeyRows.splices)',
'_myConnectionChanged(myConnection)',
'_selectedTabChanged(selectedTab)',
],
// Parameter `accessKey` has the format {
// id: string,
// placeholderName: string,
// name: string,
// accessUrl: string,
// transferredBytes: number;
// relativeTraffic: number;
// }
addAccessKey: function(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.$$(`#access-key-${accessKey.id}`);
input.select();
},
removeAccessKey: function(accessKeyId) {
for (let ui in this.accessKeyRows) {
if (this.accessKeyRows[ui].id === accessKeyId) {
this.splice('accessKeyRows', ui, 1);
return;
}
}
},
_handleAddAccessKeyPressed: function() {
this.fire('AddAccessKeyRequested');
this.$.addAccessKeyHelpBubble.hide();
},
_handleNameInputKeyDown: function(event) {
const input = event.target;
if (event.key === 'Escape') {
const accessKey = event.model.item;
input.value = accessKey.name;
input.blur();
} else if (event.key === 'Enter') {
input.blur();
}
},
_handleNameInputBlur: function(event) {
const input = event.target;
const accessKey = event.model.item;
const displayName = input.value;
if (displayName === accessKey.name) {
return;
}
input.disabled = true;
this.fire('RenameAccessKeyRequested', {
accessKeyId: accessKey.id,
newName: displayName,
entry: {
commitName: () => {
accessKey.name = displayName;
input.disabled = false;
},
revertName: () => {
input.value = accessKey.name;
input.disabled = false;
},
},
});
},
_handleRenameAccessKeyPressed: function(event) {
const input = this.$.accessKeysContainer.querySelectorAll(
'.access-key-row .access-key-container > input')[event.model.index];
// This needs to be deferred because the closing menu messes up with the focus.
window.setTimeout(() => {
input.focus();
}, 0);
},
_handleConnectPressed: function() {
this.$.getConnectedHelpBubble.hide();
this.fire('OpenGetConnectedDialogRequested', {accessKey: this.myConnection.accessUrl});
},
_handleShareCodePressed: function(event) {
const accessKey = event.model.item;
this.fire('OpenShareDialogRequested', {accessKey: accessKey.accessUrl});
},
_handleRemoveAccessKeyPressed: function(e) {
const accessKey = e.model.item;
this.fire('RemoveAccessKeyRequested', {accessKeyId: accessKey.id});
},
_encodeURIComponent: encodeURIComponent,
setServerTransferredData(totalBytes) {
this.totalInboundBytes = totalBytes;
},
updateAccessKeyRow(accessKeyId, fields) {
let newAccessKeyRow;
if (accessKeyId === MY_CONNECTION_USER_ID) {
newAccessKeyRow = Object.assign({}, this.get('myConnection'), fields);
this.set('myConnection', newAccessKeyRow);
}
for (let ui in this.accessKeyRows) {
if (this.accessKeyRows[ui].id === accessKeyId) {
newAccessKeyRow = Object.assign({}, this.get(['accessKeyRows', ui]), fields);
this.set(['accessKeyRows', ui], newAccessKeyRow);
return;
}
}
},
_formatBytesTransferred: function(numBytes, emptyValue = '') {
if (!numBytes) {
// numBytes may not be set for manual servers, or may be 0 for
// unused access keys.
return emptyValue;
}
// Show 0 decimals for < 1MB, 1 decimal for >= 1MB, 2 decimals for >= 1GB.
let numDecimals = 0;
if (numBytes >= 10 ** 9) {
numDecimals = 2;
} else if (numBytes >= 10 ** 6) {
numDecimals = 1;
}
return byte_size(numBytes, {precision: numDecimals}).toString();
},
_getFormattedTransferredUnit: function(numBytes, emptyValue = '') {
const formattedTransfer = this._formatBytesTransferred(numBytes);
if (!formattedTransfer) {
return emptyValue;
}
const units = formattedTransfer.match(/[a-zA-Z%]+/g);
if (!units || units.length < 0) {
return emptyValue;
}
return units.pop();
},
_getFormattedTransferredValue: function(numBytes, emptyValue = '') {
const formattedTransfer = this._formatBytesTransferred(numBytes);
if (!formattedTransfer) {
return emptyValue;
}
const value = parseFloat(formattedTransfer);
return !!value ? value : emptyValue;
},
_computeManagedServerUtilzationPercentage: function(numBytes, monthlyLimitBytes) {
let utilizationPercentage = 0;
if (monthlyLimitBytes && numBytes) {
utilizationPercentage = Math.round((numBytes / monthlyLimitBytes) * 100);
}
if (document.documentElement.dir === 'rtl') {
return `%${utilizationPercentage}`;
}
return `${utilizationPercentage}%`;
},
_accessKeysAddedOrRemoved: function(changeRecord) {
// Check for myConnection and regular access keys.
let hasNonAdminAccessKeys = false;
for (let ui in this.accessKeyRows) {
if (this.accessKeyRows[ui].id === MY_CONNECTION_USER_ID) {
this.myConnection = this.accessKeyRows[ui];
} else {
hasNonAdminAccessKeys = true;
}
}
this.hasNonAdminAccessKeys = hasNonAdminAccessKeys;
},
_myConnectionChanged: function(myConnection) {
if (!myConnection) {
this.$.getConnectedHelpBubble.hide();
}
},
_selectedTabChanged(selectedTab) {
if (this.selectedTab === 'settings') {
this.closeAddAccessKeyHelpBubble();
this.closeGetConnectedHelpBubble();
this.closeDataLimitsHelpBubble();
this.$.serverSettings.update(this.serverName, this.metricsEnabled);
}
},
// Help bubbles should be shown after this outline-server-view
// is on the screen (e.g. selected in iron-pages). If help bubbles
// are initialized before this point, setPosition will not work and
// they will appear in the top left of the view.
showGetConnectedHelpBubble: function() {
return this._showHelpBubble('getConnectedHelpBubble', 'managerRow');
},
showAddAccessKeyHelpBubble: function() {
return this._showHelpBubble('addAccessKeyHelpBubble', 'addAccessKeyRow', 'down', 'left');
},
showDataLimitsHelpBubble: function() {
return this._showHelpBubble('dataLimitsHelpBubble', 'settingsTab', 'up', 'right');
},
closeAddAccessKeyHelpBubble: function() {
this.$.addAccessKeyHelpBubble.hide();
},
closeGetConnectedHelpBubble: function() {
this.$.getConnectedHelpBubble.hide();
},
closeDataLimitsHelpBubble: function() {
this.$.dataLimitsHelpBubble.hide();
},
_showHelpBubble: function(
helpBubbleId, positionTargetId, arrowDirection = 'down', arrowAlignment = 'right') {
return new Promise(resolve => {
const helpBubble = this.$[helpBubbleId];
helpBubble.show(this.$[positionTargetId], arrowDirection, arrowAlignment);
helpBubble.addEventListener('outline-help-bubble-dismissed', resolve);
});
},
isRegularConnection: function(item) {
return item.id !== MY_CONNECTION_USER_ID;
},
_setSortByOrToggleDirection: function(e) {
const sortBy = e.target.dataset.sortBy;
if (this.accessKeySortBy !== sortBy) {
this.accessKeySortBy = sortBy;
this.accessKeySortDirection = 1; // Sort ascending initially.
return;
}
this.accessKeySortDirection *= -1; // Toggle sort direction.
},
_sortAccessKeys: function(accessKeySortBy, accessKeySortDirection) {
if (accessKeySortBy === 'usage') {
return function(a, b) {
return (a.transferredBytes - b.transferredBytes) * this.accessKeySortDirection;
}.bind(this);
}
// Default to sorting by name.
return function(a, b) {
const nameA = a.name.toUpperCase() || a.placeholderName.toUpperCase();
const nameB = b.name.toUpperCase() || b.placeholderName.toUpperCase();
if (nameA < nameB) {
return -1 * this.accessKeySortDirection;
}
if (nameA > nameB) {
return 1 * this.accessKeySortDirection;
}
return 0;
}.bind(this);
},
_computeShouldShowSortIcon: function(
accessKeySortBy, accessKeySortDirection, elementSortBy, elementSortDirection) {
return accessKeySortBy === elementSortBy && accessKeySortDirection === elementSortDirection;
},
destroyServer: function() {
this.fire('DeleteServerRequested');
},
removeServer: function() {
this.fire('ForgetServerRequested');
},
_computePaperProgressClass: function(isAccessKeyDataLimitEnabled) {
return isAccessKeyDataLimitEnabled ? 'data-limits' : '';
},
_getDataLimitsUsageString: function(accessKey) {
if (!this.accessKeyDataLimit) {
return '';
}
const used = this._formatBytesTransferred(accessKey.transferredBytes, '0');
const total = this.accessKeyDataLimit.value + ' ' + this.accessKeyDataLimit.unit;
return this.localize('data-limits-usage', 'used', used, 'total', total);
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,12 +12,19 @@
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.
-->
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html" />
*/
import '@polymer/paper-dialog/paper-dialog.js';
<dom-module id="outline-share-dialog">
<template>
import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import * as clipboard from 'clipboard-polyfill';
// TODO(alalama): add a language selector. This should be a separate instance of
// Polymer.AppLocalizeBehavior so the app language is not changed. Consider refactoring l10n into a
// separate Polymer behavior.
Polymer({
_template: html`
<style include="cloud-install-styles"></style>
<style>
:host {
@ -93,17 +100,15 @@
position: absolute;
}
</style>
<paper-dialog id="dialog" modal>
<paper-dialog id="dialog" modal="">
<div id="dialog-header">
<h3>[[localize('share-title')]]</h3>
<p
inner-h-t-m-l="[[localize('share-description', 'openLink', '<a href=https://securityplanner.org/#/all-recommendations>', 'closeLink', '</a>')]]"
></p>
<p inner-h-t-m-l="[[localize('share-description', 'openLink', '<a href=https://securityplanner.org/#/all-recommendations>', 'closeLink', '</a>')]]"></p>
</div>
<div contenteditable id="selectableText" style="-webkit-text-size-adjust: 100%;">
<div contenteditable="" id="selectableText" style="-webkit-text-size-adjust: 100%;">
<p>[[localize('share-invite')]]</p>
<p><a href$="{{s3Url}}">{{s3Url}}</a></p>
<p><a href\$="{{s3Url}}">{{s3Url}}</a></p>
<p>-----</p>
<p>[[localize('share-invite-trouble')]]</p>
<ol>
@ -113,43 +118,36 @@
</li>
<li>
[[localize('share-invite-instructions')]]
<a href="https://github.com/Jigsaw-Code/outline-client/blob/master/docs/invitation-instructions.md"
>https://github.com/Jigsaw-Code/outline-client/blob/master/docs/invitation-instructions.md</a
>
<a href="https://github.com/Jigsaw-Code/outline-client/blob/master/docs/invitation-instructions.md">https://github.com/Jigsaw-Code/outline-client/blob/master/docs/invitation-instructions.md</a>
</li>
</ol>
</div>
<div id="button-row">
<paper-button id="copyButton" on-tap="copyClicked">[[localize('share-invite-copy')]]</paper-button>
<paper-button id="doneButton" dialog-confirm>[[localize('done')]]</paper-button>
<paper-button id="doneButton" dialog-confirm="">[[localize('done')]]</paper-button>
</div>
<div id="copyText" hidden>[[localize('share-invite-copied')]]</div>
<div id="copyText" hidden="">[[localize('share-invite-copied')]]</div>
</paper-dialog>
</template>
<script>
// TODO(alalama): add a language selector. This should be a separate instance of Polymer.AppLocalizeBehavior
// so the app language is not changed. Consider refactoring l10n into a separate Polymer behavior.
Polymer({
is: "outline-share-dialog",
properties: {
localize: {type: Function, readonly: true},
},
created: function() {
this.clipboard = require("clipboard-polyfill");
},
open: function(acessKey, s3Url) {
this.acessKey = acessKey;
this.s3Url = s3Url;
this.$.copyText.setAttribute("hidden", true);
this.$.dialog.open();
},
copyClicked: function() {
var dt = new this.clipboard.DT();
dt.setData("text/plain", this.$.selectableText.innerText);
dt.setData("text/html", this.$.selectableText.innerHTML);
this.clipboard.write(dt);
this.$.copyText.removeAttribute("hidden");
},
});
</script>
</dom-module>
`,
is: 'outline-share-dialog',
properties: {
localize: {type: Function, readonly: true},
},
open: function(acessKey, s3Url) {
this.acessKey = acessKey;
this.s3Url = s3Url;
this.$.copyText.setAttribute('hidden', true);
this.$.dialog.open();
},
copyClicked: function() {
var dt = new clipboard.DT();
dt.setData('text/plain', this.$.selectableText.innerText);
dt.setData('text/html', this.$.selectableText.innerHTML);
clipboard.write(dt);
this.$.copyText.removeAttribute('hidden');
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,11 +12,13 @@
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.
-->
<link rel="import" href="../bower_components/polymer/polymer.html" />
*/
import '@polymer/polymer/polymer-legacy.js';
<dom-module id="outline-step-view">
<template>
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">
:host {
display: block;
@ -61,20 +63,18 @@
<div class="step-title"><slot name="step-title"></slot></div>
<div class="step-description"><slot name="step-description"></slot></div>
</div>
<div class="step-action" hidden$="[[!displayAction]]"><slot name="step-action"></slot></div>
<div class="step-action" hidden\$="[[!displayAction]]"><slot name="step-action"></slot></div>
</div>
<slot></slot>
</div>
</template>
<script>
Polymer({
is: "outline-step-view",
properties: {
displayAction: {
type: Boolean,
value: false,
},
},
});
</script>
</dom-module>
`,
is: 'outline-step-view',
properties: {
displayAction: {
type: Boolean,
value: false,
},
}
});

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2020 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,17 +12,20 @@
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/neon-animation/animations/slide-down-animation.js';
import '@polymer/neon-animation/animations/slide-from-bottom-animation.js';
import '@polymer/paper-button/paper-button.js';
import '@polymer/paper-dialog/paper-dialog.js';
<link rel="import" href="../bower_components/neon-animation/animations/slide-down-animation.html" />
<link rel="import" href="../bower_components/neon-animation/animations/slide-from-bottom-animation.html" />
<link rel="import" href="../bower_components/paper-button/paper-button.html" />
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html" />
<link rel="import" href="../bower_components/polymer/polymer-element.html" />
<link rel="import" href="../bower_components/polymer/lib/mixins/dir-mixin.html" />
import {DirMixin} from '@polymer/polymer/lib/mixins/dir-mixin.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
<dom-module id="outline-survey-dialog">
<template>
class OutlineSurveyDialog extends DirMixin
(PolymerElement) {
static get template() {
return html`
<style include="cloud-install-styles"></style>
<style>
:host {
@ -72,57 +75,49 @@
line-height: 14px;
}
</style>
<paper-dialog
id="dialog"
vertical-align="bottom"
no-cancel-on-outside-click
entry-animation="slide-from-bottom-animation"
exit-animation="slide-down-animation"
>
<paper-dialog id="dialog" vertical-align="bottom" no-cancel-on-outside-click="" entry-animation="slide-from-bottom-animation" exit-animation="slide-down-animation">
<div id="container">
<img id="outlineLogo" src="images/manager-profile-2x.png" />
<img id="outlineLogo" src="images/manager-profile-2x.png">
<div id="content">
<h3>[[title]]</h3>
<div id="button-container">
<paper-button dialog-dismiss noink>
<paper-button dialog-dismiss="" noink="">
[[localize('survey-decline')]]
</paper-button>
<paper-button dialog-confirm autofocus noink>
<paper-button dialog-confirm="" autofocus="" noink="">
<a href="[[surveyLink]]">[[localize('survey-go-to-survey')]]</a>
</paper-button>
</div>
</div>
</div>
<div id="disclaimer">
<hr />
<hr>
<p>[[localize('survey-disclaimer')]]</p>
</div>
</paper-dialog>
</template>
<script>
class OutlineSurveyDialog extends Polymer.DirMixin(Polymer.Element) {
static get is() {
return "outline-survey-dialog";
}
`;
}
static get properties() {
return {
localize: {
type: Function,
readonly: true,
},
surveyLink: String,
title: String,
};
}
static get is() {
return 'outline-survey-dialog';
}
open(title, surveyLink) {
this.title = title;
this.surveyLink = surveyLink;
this.$.dialog.horizontalAlign = this.dir === "ltr" ? "left" : "right";
this.$.dialog.open();
}
}
customElements.define(OutlineSurveyDialog.is, OutlineSurveyDialog);
</script>
</dom-module>
static get properties() {
return {
localize: {
type: Function,
readonly: true,
},
surveyLink: String,
title: String,
};
}
open(title, surveyLink) {
this.title = title;
this.surveyLink = surveyLink;
this.$.dialog.horizontalAlign = this.dir === 'ltr' ? 'left' : 'right';
this.$.dialog.open();
}
}
customElements.define(OutlineSurveyDialog.is, OutlineSurveyDialog);

View file

@ -1,4 +1,4 @@
<!--
/*
Copyright 2018 The Outline Authors
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,10 +12,12 @@
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} from '@polymer/polymer/lib/legacy/polymer-fn.js';
<dom-module id="outline-tos-view">
<template>
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Polymer({
_template: html`
<style include="cloud-install-styles"></style>
<style>
:host {
@ -49,32 +51,29 @@
}
</style>
<div class="container">
<img id="logo" src="images/launcher-icon.png" />
<img id="logo" src="images/launcher-icon.png">
</div>
<div id="tos">
<span
inner-h-t-m-l="[[localize('terms-of-service', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/static_downloads/Outline-Terms-of-Service.html>', 'closeLink', '</a>')]]"
></span>
<span inner-h-t-m-l="[[localize('terms-of-service', 'openLink', '<a href=https://s3.amazonaws.com/outline-vpn/static_downloads/Outline-Terms-of-Service.html>', 'closeLink', '</a>')]]"></span>
<paper-button on-tap="acceptTermsOfService">[[localize('okay')]]</paper-button>
</div>
</template>
<script>
Polymer({
is: "outline-tos-view",
properties: {
hasAcceptedTermsOfService: {
type: Boolean,
value: false,
notify: true,
},
localize: {
type: Function,
readonly: true,
},
},
acceptTermsOfService: function() {
this.hasAcceptedTermsOfService = true;
},
});
</script>
</dom-module>
`,
is: 'outline-tos-view',
properties: {
hasAcceptedTermsOfService: {
type: Boolean,
value: false,
notify: true,
},
localize: {
type: Function,
readonly: true,
},
},
acceptTermsOfService: function() {
this.hasAcceptedTermsOfService = true;
}
});

View file

@ -0,0 +1,203 @@
/*
Copyright 2020 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 './cloud-install-styles.js';
import './outline-server-settings-styles.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
// outline-validated-input
// This is an input, with a cancel and a save button, which performs client-side validation and has
// an event-based hook for server-side evaluation.
//
// Attributes:
// * editable: Is this input editable? Default: false
// * visible: Is this input visible? Default: false
// * label: The label for the input. Default: null
// * value: The initial value entered in the input. SHOULD be a valid value. Default: null
// * allowed-pattern: Regex describing what inputs are allowed. Users will be prevented from
// entering inputs which don't follow the pattern. Default: ".*"
// * max-length: The number of characters allowed in the input. Default: Number.POSITIVE_INFINITY
// * client-side-validator: A function which takes a string and returns either an empty string on
// success or an error message on failure. This function will be called on every keystroke.
// Default: () => ""
// * event: The name of the event fired when the save button is tapped. Passes the current input
// value as "value" and the Polymer object as "ui". The handler for this event MUST call
// ui.setSavedState() or ui.setErrorState(message) depending on its result. Default: null
Polymer({
_template: html`
<style include="cloud-install-styles"></style>
<style include="outline-server-settings-styles"></style>
<style>
#container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 545px;
}
paper-input {
width: 60%;
}
paper-button {
height: 36px;
min-width: 83px;
text-align: center;
}
paper-button[disabled] {
color: rgba(255, 255, 255, 0.3);
}
#saveButton {
background: var(--primary-green);
color: #fff;
}
#cancelButton {
color: #fff;
}
#saveButton[disabled] {
background-color: rgba(255, 255, 255, 0.12);
}
#cancelButton[disabled] {
background-color: rgba(0, 0, 0, 0);
}
</style>
<div id="container" hidden\$="[[!visible]]">
<!-- We use on-focus instead of on-tap to handle the case where text is selected but the tap event isn't fired. -->
<paper-input id="input" readonly\$="[[!editable]]" allowed-pattern="[[allowedPattern]]" value="[[value]]" label="[[label]]" maxlength="[[maxLength]]" on-focus="_enterEditingState" on-keyup="_onKeyUp" always-float-label="">
</paper-input>
<paper-button id="cancelButton" hidden\$="[[!_showButtons]]" disabled\$="[[!_enableButtons]]" on-tap="_cancel">
[[localize('cancel')]]
</paper-button>
<paper-button id="saveButton" hidden\$="[[!_showButtons]]" disabled\$="[[!_enableButtons]]" on-tap="_save">
[[localize('save')]]
</paper-button>
</div>
`,
is: 'outline-validated-input',
properties: {
// Properties affecting the whole element
editable: {type: Boolean, value: false},
visible: {type: Boolean, value: false},
// Properties affecting the input
label: {type: String, readonly: true, value: null},
allowedPattern: {type: String, value: '.*'},
maxLength: {type: Number, value: Number.POSITIVE_INFINITY},
value: {type: String, value: null},
// `value` here is evaluated. If it were simply `() => "`, then clientSideValidator
// would default to just "", not a function returning "".
// Note also that we can't use paper-input's validator attribute because it will fight
// with any server-side validation and cause unpredictable results.
clientSideValidator: {
type: Function,
readonly: true,
value: () => {
return () => '';
},
},
// Properties affecting the buttons
_showButtons: {type: Boolean, value: false},
_enableButtons: {type: Boolean, value: false},
// Other properties
event: {type: String, value: null},
localize: {type: Function, readonly: true},
},
_onKeyUp: function(e) {
const input = this.$.input;
if (e.key === 'Escape') {
this._cancel();
input.blur();
return;
} else if (e.key === 'Enter') {
if (!input.invalid) {
this._save();
input.blur();
}
return;
}
const validationError = this.clientSideValidator(input.value);
if (validationError) {
input.invalid = true;
this.$.saveButton.disabled = true;
input.errorMessage = validationError;
} else {
input.invalid = false;
this.$.saveButton.disabled = false;
}
},
_cancel: function() {
const input = this.$.input;
input.value = this.value;
input.invalid = false;
this._showButtons = false;
},
_save: function() {
const input = this.$.input;
const value = input.value;
if (value === this.value) {
this._cancel();
return;
}
this.$.cancelButton.disabled = true;
this.$.saveButton.disabled = true;
input.readonly = true;
input.invalid = false;
// We use this heuristic to avoid having to pass a constructor as an attribute
const numberValue = Number(value);
const typedValue = Number.isNaN(numberValue) ? value : numberValue;
this.fire(this.event, {
validatedInput: typedValue,
ui: this,
});
},
_enterEditingState: function() {
if (!this.editable) {
return;
}
this._showButtons = true;
this.$.cancelButton.disabled = false;
this.$.saveButton.disabled = this.$.input.invalid;
},
enterSavedState: function() {
const input = this.$.input;
this.value = input.value;
this._showButtons = false;
input.readonly = false;
},
enterErrorState: function(message) {
const input = this.$.input;
this._enableButtons = true;
input.errorMessage = message;
input.invalid = true;
input.readonly = false;
input.focus();
}
});

View file

@ -14,11 +14,16 @@
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const config = {
mode: 'production',
entry: path.resolve(__dirname, './web_app/main.ts'),
entry: [
require.resolve('@webcomponents/webcomponentsjs/webcomponents-loader.js'),
path.resolve(__dirname, './web_app/ui_components/style.css'),
path.resolve(__dirname, './web_app/main.ts'),
],
target: 'electron-renderer',
devtool: 'inline-source-map',
output: {
@ -26,29 +31,49 @@ const config = {
filename: 'main.js'
},
module: {
rules: [{
test: /\.ts(x)?$/,
exclude: /node_modules/,
use: [
'ts-loader',
],
}]
rules: [
{
test: /\.ts(x)?$/,
exclude: /node_modules/,
use: [
'ts-loader',
'./src/server_manager/css-in-js-rtl-loader',
],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'./src/server_manager/css-in-js-rtl-loader',
],
},
{
test: /\.css?$/,
use: [
'style-loader',
'css-loader',
],
}
]
},
resolve: {extensions: ['.tsx', '.ts', '.js']},
plugins: [
// Hack to protect against @sentry/electron not having process.type defined.
new webpack.DefinePlugin({
// Hack to protect against @sentry/electron not having process.type defined.
'process.type': JSON.stringify('renderer'),
// Statically link the Roboto font, rather than link to fonts.googleapis.com
'window.polymerSkipLoadingFontRoboto': JSON.stringify(true),
}),
new CopyPlugin(
[
{from: 'bower_components', to: 'bower_components'},
{from: 'ui_components', to: 'ui_components'},
{from: 'index.html', to: '.'},
{from: 'images', to: 'images'},
{from: 'messages', to: 'messages'},
],
{context: __dirname}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './index.html'),
}),
],
};

1350
yarn.lock

File diff suppressed because it is too large Load diff