From b63606e90cdd9d068051161efdf6b3e94bfedb19 Mon Sep 17 00:00:00 2001 From: alalamav <2132122+alalamav@users.noreply.github.com> Date: Mon, 20 Jul 2020 17:32:29 -0400 Subject: [PATCH] Make data limits a permanent feature (#698) --- src/server_manager/messages/en.json | 3 +- .../messages/master_messages.json | 22 +++-- src/server_manager/web_app/app.spec.ts | 2 +- src/server_manager/web_app/app.ts | 21 +++-- .../web_app/gallery_app/main.ts | 30 ++++--- src/server_manager/web_app/main.ts | 14 ++-- .../web_app/shadowbox_server.ts | 26 ++++-- .../ui_components/outline-server-settings.js | 82 ++++++++++--------- .../ui_components/outline-server-view.js | 9 +- src/shadowbox/package.json | 2 +- src/shadowbox/server/manager_service.ts | 18 +++- 11 files changed, 133 insertions(+), 96 deletions(-) diff --git a/src/server_manager/messages/en.json b/src/server_manager/messages/en.json index 9c7e4f4a..ea721c79 100644 --- a/src/server_manager/messages/en.json +++ b/src/server_manager/messages/en.json @@ -22,9 +22,10 @@ "confirmation-server-remove": "This action removes your server from the Outline Manager, but does not block proxy access to users. You will still need to manually delete the Outline server from your host machine.", "confirmation-server-remove-title": "Remove Server?", "data-limits": "Data limits", - "data-limits-description": "Set a 30 day trailing data transfer limit for access keys on this server. Available at least until {date}.", + "data-limits-description": "Set a 30 day trailing data transfer limit for access keys on this server.", "data-limits-dialog-text": "Go to the Settings tab to set a data transfer limit for access keys on this server.", "data-limits-dialog-title": "Avoid data overages", + "data-limits-disclaimer": "Since you are currently reporting metrics, use of the data limits feature will be included. Please see the {openLink}data collection policy{closeLink} for more details.", "data-limits-usage": "{used} of {total} used", "destroy": "Destroy", "digitalocean-disconnect": "Disconnect", diff --git a/src/server_manager/messages/master_messages.json b/src/server_manager/messages/master_messages.json index b9bcc592..e214998f 100644 --- a/src/server_manager/messages/master_messages.json +++ b/src/server_manager/messages/master_messages.json @@ -126,14 +126,8 @@ "description": "This string appears as a title in a section to configure access key data transfer limits." }, "data_limits_description": { - "message": "Set a 30 day trailing data transfer limit for access keys on this server. Available at least until $DATE$.", - "description": "This string appears as an explanation to the access key data transfer limits feature.", - "placeholders": { - "DATE": { - "content": "{date}", - "example": "December 9, 2019" - } - } + "message": "Set a 30 day trailing data transfer limit for access keys on this server.", + "description": "This string appears as an explanation to the access key data transfer limits feature." }, "data_limits_dialog_text": { "message": "Go to the Settings tab to set a data transfer limit for access keys on this server.", @@ -143,6 +137,18 @@ "message": "Avoid data overages", "description": "This string appears as a title in a dialog to advertise the data limits feature." }, + "data_limits_disclaimer": { + "message": "Since you are currently reporting metrics, use of the data limits feature will be included. Please see the $START_OF_LINK$data collection policy$END_OF_LINK$ for more details.", + "description": "This string provides a data collection disclaimer to opting into the data limits features.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, "data_limits_usage": { "message": "$USED$ of $TOTAL$ used", "description": "This string appears in a tooltip when the data limits feature is enabled. It lets the user know how much data an access key has used, relative to the limit.", diff --git a/src/server_manager/web_app/app.spec.ts b/src/server_manager/web_app/app.spec.ts index 9524da5b..a3151304 100644 --- a/src/server_manager/web_app/app.spec.ts +++ b/src/server_manager/web_app/app.spec.ts @@ -247,7 +247,7 @@ function createTestApp( return new App( polymerAppRoot, VERSION, fakeDigitalOceanSessionFactory, fakeDigitalOceanServerRepositoryFactory, manualServerRepo, displayServerRepository, - digitalOceanTokenManager, new FakeSurveys()); + digitalOceanTokenManager); } enum AppRootScreen { diff --git a/src/server_manager/web_app/app.ts b/src/server_manager/web_app/app.ts index f4ff1abd..52a81a1b 100644 --- a/src/server_manager/web_app/app.ts +++ b/src/server_manager/web_app/app.ts @@ -20,7 +20,6 @@ import * as digitalocean_api from '../cloud/digitalocean_api'; import * as errors from '../infrastructure/errors'; import {sleep} from '../infrastructure/sleep'; import * as server from '../model/server'; -import {Surveys} from '../model/survey'; import {TokenManager} from './digitalocean_oauth'; import * as digitalocean_server from './digitalocean_server'; @@ -37,8 +36,6 @@ const UNUSED_DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716'; const CHANGE_KEYS_PORT_VERSION = '1.0.0'; const DATA_LIMITS_VERSION = '1.1.0'; const CHANGE_HOSTNAME_VERSION = '1.2.0'; -// Date by which the data limits feature experiment will be permanently added or removed. -export const DATA_LIMITS_AVAILABILITY_DATE = new Date('2020-06-02'); const MAX_ACCESS_KEY_DATA_LIMIT_BYTES = 50 * (10 ** 9); // 50GB function dataLimitToDisplayDataAmount(limit: server.DataLimit): DisplayDataAmount|null { @@ -88,6 +85,13 @@ async function computeDefaultAccessKeyDataLimit( } } +// Returns whether the user has seen a notification for the updated feature metrics data collection +// policy. +function hasSeenFeatureMetricsNotification(): boolean { + return !!window.localStorage.getItem('dataLimitsHelpBubble-dismissed') && + !!window.localStorage.getItem('dataLimits-feature-collection-notification'); +} + async function showHelpBubblesOnce(serverView: ServerView) { if (!window.localStorage.getItem('addAccessKeyHelpBubble-dismissed')) { await serverView.showAddAccessKeyHelpBubble(); @@ -131,7 +135,7 @@ export class App { private createDigitalOceanServerRepository: DigitalOceanServerRepositoryFactory, private manualServerRepository: server.ManualServerRepository, private displayServerRepository: DisplayServerRepository, - private digitalOceanTokenManager: TokenManager, private surveys: Surveys) { + private digitalOceanTokenManager: TokenManager) { appRoot.setAttribute('outline-version', this.version); appRoot.addEventListener('ConnectToDigitalOcean', (event: CustomEvent) => { @@ -838,10 +842,10 @@ export class App { view.serverPortForNewAccessKeys = selectedServer.getPortForNewAccessKeys(); view.serverCreationDate = localizeDate(selectedServer.getCreatedDate(), this.appRoot.language); view.serverVersion = selectedServer.getVersion(); - view.dataLimitsAvailabilityDate = - localizeDate(DATA_LIMITS_AVAILABILITY_DATE, this.appRoot.language); view.accessKeyDataLimit = dataLimitToDisplayDataAmount(selectedServer.getAccessKeyDataLimit()); view.isAccessKeyDataLimitEnabled = !!view.accessKeyDataLimit; + view.showFeatureMetricsDisclaimer = selectedServer.getMetricsEnabled() && + !selectedServer.getAccessKeyDataLimit() && !hasSeenFeatureMetricsNotification(); const version = this.selectedServer.getVersion(); if (version) { @@ -1029,7 +1033,9 @@ export class App { this.appRoot.showNotification(this.appRoot.localize('saved')); serverView.accessKeyDataLimit = dataLimitToDisplayDataAmount(limit); this.refreshTransferStats(this.selectedServer, serverView); - this.surveys.presentDataLimitsEnabledSurvey(); + // Don't display the feature collection disclaimer anymore. + serverView.showFeatureMetricsDisclaimer = false; + window.localStorage.setItem('dataLimits-feature-collection-notification', 'true'); } catch (error) { console.error(`Failed to set access key data limit: ${error}`); this.appRoot.showError(this.appRoot.localize('error-set-data-limit')); @@ -1045,7 +1051,6 @@ export class App { await this.selectedServer.removeAccessKeyDataLimit(); this.appRoot.showNotification(this.appRoot.localize('saved')); this.refreshTransferStats(this.selectedServer, serverView); - this.surveys.presentDataLimitsDisabledSurvey(); } catch (error) { console.error(`Failed to remove access key data limit: ${error}`); this.appRoot.showError(this.appRoot.localize('error-remove-data-limit')); diff --git a/src/server_manager/web_app/gallery_app/main.ts b/src/server_manager/web_app/gallery_app/main.ts index afc4b98a..74024802 100644 --- a/src/server_manager/web_app/gallery_app/main.ts +++ b/src/server_manager/web_app/gallery_app/main.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import IntlMessageFormat from 'intl-messageformat'; - import '../ui_components/outline-about-dialog'; import '../ui_components/outline-do-oauth-step'; import '../ui_components/outline-feedback-dialog'; @@ -21,6 +19,7 @@ import '../ui_components/outline-share-dialog'; import '../ui_components/outline-sort-span'; import '../ui_components/outline-survey-dialog'; +import IntlMessageFormat from 'intl-messageformat'; import {css, customElement, html, LitElement, property} from 'lit-element'; async function makeLocalize(language: string) { @@ -97,7 +96,8 @@ export class TestApp extends LitElement {

outline-about-dialog

- +
@@ -107,30 +107,34 @@ export class TestApp extends LitElement {

outline-feedback-dialog

- - + +

outline-share-dialog

- +

outline-sort-icon

{ - const el = this.select('outline-sort-span'); - el.direction *= -1; - }}>Column Header + const el = this.select('outline-sort-span'); + el.direction *= -1; + }}>Column Header

outline-survey-dialog

- +
`; diff --git a/src/server_manager/web_app/main.ts b/src/server_manager/web_app/main.ts index d90d31eb..be5cc0c4 100644 --- a/src/server_manager/web_app/main.ts +++ b/src/server_manager/web_app/main.ts @@ -18,15 +18,18 @@ import * as digitalocean_api from '../cloud/digitalocean_api'; import * as i18n from '../infrastructure/i18n'; import {getSentryApiUrl} from '../infrastructure/sentry'; -import {App, DATA_LIMITS_AVAILABILITY_DATE} from './app'; +import {App} from './app'; import {DigitalOceanTokenManager} from './digitalocean_oauth'; import * as digitalocean_server from './digitalocean_server'; import {DisplayServerRepository} from './display_server'; import {ManualServerRepository} from './manual_server'; -import {DEFAULT_PROMPT_IMPRESSION_DELAY_MS, OutlineSurveys} from './survey'; import {AppRoot} from './ui_components/app-root.js'; -type LanguageDef = {id: string, name: string, dir: string}; +type LanguageDef = { + id: string, + name: string, + dir: string +}; const SUPPORTED_LANGUAGES: {[key: string]: LanguageDef} = { 'am': {id: 'am', name: 'አማርኛ', dir: 'ltr'}, 'ar': {id: 'ar', name: 'العربية', dir: 'rtl'}, @@ -120,10 +123,7 @@ document.addEventListener('WebComponentsReady', () => { new App( appRoot, version, digitalocean_api.createDigitalOceanSession, digitalOceanServerRepositoryFactory, new ManualServerRepository('manualServers'), - new DisplayServerRepository(), new DigitalOceanTokenManager(), - new OutlineSurveys( - appRoot.$.surveyDialog, localStorage, DEFAULT_PROMPT_IMPRESSION_DELAY_MS, - DATA_LIMITS_AVAILABILITY_DATE)) + new DisplayServerRepository(), new DigitalOceanTokenManager()) .start(); }); diff --git a/src/server_manager/web_app/shadowbox_server.ts b/src/server_manager/web_app/shadowbox_server.ts index 850e4c6c..1ea21eef 100644 --- a/src/server_manager/web_app/shadowbox_server.ts +++ b/src/server_manager/web_app/shadowbox_server.ts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as semver from 'semver'; + import * as errors from '../infrastructure/errors'; import * as server from '../model/server'; @@ -63,30 +65,36 @@ export class ShadowboxServer implements server.Server { return this.apiRequest('access-keys/' + accessKeyId, {method: 'DELETE'}); } - setAccessKeyDataLimit(limit: server.DataLimit): Promise { + async setAccessKeyDataLimit(limit: server.DataLimit): Promise { console.info(`Setting access key data limit: ${JSON.stringify(limit)}`); const requestOptions = { method: 'PUT', headers: new Headers({'Content-Type': 'application/json'}), body: JSON.stringify({limit}) }; - return this.apiRequest('experimental/access-key-data-limit', requestOptions).then(() => { - this.serverConfig.accessKeyDataLimit = limit; - }); + await this.apiRequest(this.getAccessKeyDataLimitPath(), requestOptions); + this.serverConfig.accessKeyDataLimit = limit; } - removeAccessKeyDataLimit(): Promise { + async removeAccessKeyDataLimit(): Promise { console.info(`Removing access key data limit`); - return this.apiRequest('experimental/access-key-data-limit', {method: 'DELETE'}) - .then(() => { - delete this.serverConfig.accessKeyDataLimit; - }); + await this.apiRequest(this.getAccessKeyDataLimitPath(), {method: 'DELETE'}); + delete this.serverConfig.accessKeyDataLimit; } getAccessKeyDataLimit(): server.DataLimit|undefined { return this.serverConfig.accessKeyDataLimit; } + private getAccessKeyDataLimitPath(): string { + const version = this.getVersion(); + if (semver.gte(version, '1.4.0')) { + // Data limits became a permanent feature in shadowbox v1.4.0. + return 'server/access-key-data-limit'; + } + return 'experimental/access-key-data-limit'; + } + getDataUsage(): Promise { return this.apiRequest('metrics/transfer'); } diff --git a/src/server_manager/web_app/ui_components/outline-server-settings.js b/src/server_manager/web_app/ui_components/outline-server-settings.js index bd1badde..5e1a89e0 100644 --- a/src/server_manager/web_app/ui_components/outline-server-settings.js +++ b/src/server_manager/web_app/ui_components/outline-server-settings.js @@ -100,6 +100,12 @@ Polymer({ width: 120px; } } + .data-limits-disclaimer { + margin: 0 0 8px 0; + } + .data-limits-disclaimer p { + width: 100%; + } .detail { margin-top: 0px; font-size: 12px; @@ -142,12 +148,9 @@ Polymer({ 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; + width: 80%; } #data-limits-container .selection-container span { display: block; @@ -191,6 +194,39 @@ Polymer({
+ +
+ +
+
+
+

[[localize('data-limits')]]

+

[[localize('data-limits-description')]]

+
+ + + + [[localize('enabled')]] + [[localize('disabled')]] + + +
+
+ +

+
+
+ + + + MB + GB + + +
+
+
@@ -201,34 +237,6 @@ Polymer({

-
-
-
-

[[localize('data-limits')]]

-

- [[localize('data-limits-description', 'date', dataLimitsAvailabilityDate)]]
- -

-
- - - - [[localize('enabled')]] - [[localize('disabled')]] - - -
-
- - - - MB - GB - - -
-
@@ -264,16 +272,14 @@ Polymer({ 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. + showFeatureMetricsDisclaimer: {type: Boolean, value: false}, 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)'}, + shouldShowExperiments: {type: Boolean, value: false}, }, setServerName: function(name) { @@ -346,10 +352,6 @@ Polymer({ 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); diff --git a/src/server_manager/web_app/ui_components/outline-server-view.js b/src/server_manager/web_app/ui_components/outline-server-view.js index 1acfa576..ced91d7a 100644 --- a/src/server_manager/web_app/ui_components/outline-server-view.js +++ b/src/server_manager/web_app/ui_components/outline-server-view.js @@ -556,7 +556,7 @@ export class ServerView extends DirMixin(PolymerElement) {
- +
@@ -601,7 +601,7 @@ export class ServerView extends DirMixin(PolymerElement) { accessKeyDataLimit: {type: Object}, isAccessKeyDataLimitEnabled: {type: Boolean}, supportsAccessKeyDataLimit: {type: Boolean}, - dataLimitsAvailabilityDate: {type: String}, + showFeatureMetricsDisclaimer: {type: Boolean}, isServerManaged: Boolean, isServerReachable: Boolean, retryDisplayingServer: Function, @@ -650,8 +650,7 @@ export class ServerView extends DirMixin(PolymerElement) { this.isAccessKeyDataLimitEnabled = false; /** Whether the server supports data limits. */ this.supportsAccessKeyDataLimit = false; - /** Date by which the feature stops being an experiment. */ - this.dataLimitsAvailabilityDate = ''; + this.showFeatureMetricsDisclaimer = false; this.isServerManaged = false; this.isServerReachable = false; /** @@ -747,7 +746,7 @@ export class ServerView extends DirMixin(PolymerElement) { return row } return {...row, name: displayName}; - }); + }); }, revertName: () => { input.value = accessKey.name; diff --git a/src/shadowbox/package.json b/src/shadowbox/package.json index 2db84caf..bb72d898 100644 --- a/src/shadowbox/package.json +++ b/src/shadowbox/package.json @@ -1,7 +1,7 @@ { "name": "outline-server", "private": true, - "version": "1.3.1", + "version": "1.4.0", "description": "Outline server", "main": "build/server/main.js", "author": "Outline", diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts index 3e5ad27c..9c758e34 100644 --- a/src/shadowbox/server/manager_service.ts +++ b/src/shadowbox/server/manager_service.ts @@ -77,6 +77,10 @@ export function bindService( apiServer: restify.Server, apiPrefix: string, service: ShadowsocksManagerService) { apiServer.put(`${apiPrefix}/name`, service.renameServer.bind(service)); apiServer.get(`${apiPrefix}/server`, service.getServer.bind(service)); + apiServer.put( + `${apiPrefix}/server/access-key-data-limit`, service.setAccessKeyDataLimit.bind(service)); + apiServer.del( + `${apiPrefix}/server/access-key-data-limit`, service.removeAccessKeyDataLimit.bind(service)); apiServer.put( `${apiPrefix}/server/hostname-for-access-keys`, service.setHostnameForAccessKeys.bind(service)); @@ -94,13 +98,21 @@ export function bindService( apiServer.get(`${apiPrefix}/metrics/enabled`, service.getShareMetrics.bind(service)); apiServer.put(`${apiPrefix}/metrics/enabled`, service.setShareMetrics.bind(service)); - // Experimental APIs + // Redirect former experimental APIs apiServer.put( `${apiPrefix}/experimental/access-key-data-limit`, - service.setAccessKeyDataLimit.bind(service)); + redirect(`${apiPrefix}/server/access-key-data-limit`)); apiServer.del( `${apiPrefix}/experimental/access-key-data-limit`, - service.removeAccessKeyDataLimit.bind(service)); + redirect(`${apiPrefix}/server/access-key-data-limit`)); +} + +// Returns a request handler that redirects a bound request path to `url` with HTTP status code 308. +function redirect(url: string): restify.RequestHandlerType { + return (req: restify.Request, res: restify.Response, next: restify.Next) => { + logging.debug(`Redirecting ${req.url} => ${url}`); + res.redirect(308, url, next); + }; } function validateAccessKeyId(accessKeyId: unknown): string {