mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-10-25 05:50:47 +00:00
Merge pull request #632 from Jigsaw-Code/fortuna-webpack-polymer
Convert to Polymer 3
This commit is contained in:
commit
3411be10df
48 changed files with 4861 additions and 4768 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ if (sentryDsn) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// To clearly identify app restarts in Sentry.
|
||||
console.info(`Outline Manager is starting`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
859
src/server_manager/web_app/ui_components/app-root.js
Normal file
859
src/server_manager/web_app/ui_components/app-root.js
Normal 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);
|
||||
244
src/server_manager/web_app/ui_components/cloud-install-styles.js
Normal file
244
src/server_manager/web_app/ui_components/cloud-install-styles.js
Normal 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');
|
||||
|
|
@ -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')]]">
|
||||
>
|
||||
</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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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});
|
||||
}
|
||||
});
|
||||
|
|
@ -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';
|
||||
}
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
41
src/server_manager/web_app/ui_components/outline-iconset.js
Normal file
41
src/server_manager/web_app/ui_components/outline-iconset.js
Normal 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);
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
@ -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'
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
902
src/server_manager/web_app/ui_components/outline-server-view.js
Normal file
902
src/server_manager/web_app/ui_components/outline-server-view.js
Normal 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);
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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'),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue