diff --git a/frontend/src/app/components/ExperimentEolDialog.js b/frontend/src/app/components/ExperimentEolDialog.js
new file mode 100644
index 0000000000..0b0e41c062
--- /dev/null
+++ b/frontend/src/app/components/ExperimentEolDialog.js
@@ -0,0 +1,43 @@
+import React from 'react';
+
+export default class ExperimentEolDialog extends React.Component {
+
+ render() {
+ const { title } = this.props;
+ return (
+
+ );
+ }
+
+ proceed(e) {
+ e.preventDefault();
+ this.props.onSubmit(e);
+ }
+
+ cancel(e) {
+ e.preventDefault();
+ this.props.onCancel();
+ }
+}
+
+ExperimentEolDialog.propTypes = {
+ title: React.PropTypes.string,
+ onCancel: React.PropTypes.func,
+ onSubmit: React.PropTypes.func
+};
diff --git a/frontend/src/app/components/ExperimentRowCard.js b/frontend/src/app/components/ExperimentRowCard.js
index 4ee3a9426d..852091446e 100644
--- a/frontend/src/app/components/ExperimentRowCard.js
+++ b/frontend/src/app/components/ExperimentRowCard.js
@@ -10,12 +10,12 @@ const MAX_JUST_UPDATED_PERIOD = 2 * ONE_WEEK;
export default class ExperimentRowCard extends React.Component {
render() {
- const { hasAddon, experiment, enabled, isExperimentCompleted } = this.props;
+ const { hasAddon, experiment, enabled, isAfterCompletedDate } = this.props;
const { description, thumbnail } = experiment;
const installation_count = (experiment.installation_count) ? experiment.installation_count : 0;
const title = experiment.short_title || experiment.title;
- const isCompleted = isExperimentCompleted(experiment);
+ const isCompleted = isAfterCompletedDate(experiment);
// TODO: #1138 Replace this highly hackly hook so that the subtitle comes from the model
const subtitle = (experiment.title === 'No More 404s') ?
@@ -161,5 +161,5 @@ ExperimentRowCard.propTypes = {
getExperimentLastSeen: React.PropTypes.func.isRequired,
sendToGA: React.PropTypes.func.isRequired,
navigateTo: React.PropTypes.func.isRequired,
- isExperimentCompleted: React.PropTypes.func
+ isAfterCompletedDate: React.PropTypes.func
};
diff --git a/frontend/src/app/containers/App.js b/frontend/src/app/containers/App.js
index b4ee66315a..1f6f01ad9f 100644
--- a/frontend/src/app/containers/App.js
+++ b/frontend/src/app/containers/App.js
@@ -7,7 +7,7 @@ import { push as routerPush } from 'react-router-redux';
import cookies from 'js-cookie';
import Clipboard from 'clipboard';
-import { getInstalled, isExperimentEnabled, isExperimentCompleted, isInstalledLoaded } from '../reducers/addon';
+import { getInstalled, isExperimentEnabled, isAfterCompletedDate, isInstalledLoaded } from '../reducers/addon';
import { getExperimentBySlug, isExperimentsLoaded } from '../reducers/experiments';
import experimentSelector from '../selectors/experiment';
import { uninstallAddon, installAddon, enableExperiment, disableExperiment, pollAddon } from '../lib/addon';
@@ -132,7 +132,7 @@ export default connect(
isDev: state.browser.isDev,
isExperimentEnabled: experiment =>
isExperimentEnabled(state.addon, experiment),
- isExperimentCompleted,
+ isAfterCompletedDate,
isFirefox: state.browser.isFirefox,
isMinFirefox: state.browser.isMinFirefox,
routing: state.routing
diff --git a/frontend/src/app/containers/ExperimentPage.js b/frontend/src/app/containers/ExperimentPage.js
index 9845476423..edadfb9c24 100644
--- a/frontend/src/app/containers/ExperimentPage.js
+++ b/frontend/src/app/containers/ExperimentPage.js
@@ -9,6 +9,7 @@ import LoadingPage from './LoadingPage';
import NotFoundPage from './NotFoundPage';
import ExperimentDisableDialog from '../components/ExperimentDisableDialog';
+import ExperimentEolDialog from '../components/ExperimentEolDialog';
import ExperimentTourDialog from '../components/ExperimentTourDialog';
import MainInstallButton from '../components/MainInstallButton';
import ExperimentCardList from '../components/ExperimentCardList';
@@ -54,7 +55,8 @@ export class ExperimentDetail extends React.Component {
showPreFeedbackDialog: false,
changeHeaderOn: 125,
stickyHeaderSiblingHeight: 0,
- privacyScrollOffset: 15
+ privacyScrollOffset: 15,
+ showEolDialog: false
};
// HACK: Set this as a plain object property, so we don't trigger crazy
@@ -155,8 +157,9 @@ export class ExperimentDetail extends React.Component {
}
const { enabled, useStickyHeader, highlightMeasurementPanel,
- showDisableDialog, showTourDialog, isEnabling, isDisabling,
- progressButtonWidth, showPreFeedbackDialog, stickyHeaderSiblingHeight } = this.state;
+ showDisableDialog, showTourDialog,
+ showPreFeedbackDialog, showEolDialog,
+ stickyHeaderSiblingHeight } = this.state;
const { title, version, contribute_url, bug_report_url, discourse_url,
introduction, measurements, privacy_notice_url, changelog_url,
@@ -168,11 +171,8 @@ export class ExperimentDetail extends React.Component {
// TODO: #1138 - add optional subtitles the right way
const subtitle = (title === 'No More 404s') ? 'Powered by the Wayback Machine' : '';
- const installation_count = (experiment.installation_count) ? experiment.installation_count.toLocaleString() : '0';
const surveyURL = buildSurveyURL('givefeedback', title, installed, survey_url);
const modified = formatDate(experiment.modified);
- const completedDate = experiment.completed ? formatDate(experiment.completed) : null;
- const validVersion = this.isValidVersion(min_release);
let statusType = null;
if (experiment.error) {
@@ -200,6 +200,15 @@ export class ExperimentDetail extends React.Component {
surveyURL={surveyURL}
onCancel={() => this.setState({ showPreFeedbackDialog: false })} />}
+ {showEolDialog &&
+ this.setState({ showEolDialog: false })}
+ onSubmit={e => {
+ this.setState({ showEolDialog: false });
+ this.renderUninstallSurvey(e);
+ }} />}
+
{!hasAddon &&
@@ -235,12 +244,7 @@ export class ExperimentDetail extends React.Component {
{title}
{subtitle}
- {hasAddon && validVersion && }
+ { this.renderExperimentControls() }
{ this.renderMinimumVersionNotice(title, hasAddon, min_release) }
@@ -258,7 +262,7 @@ export class ExperimentDetail extends React.Component {
- { this.renderInstallationCount(installation_count, title) }
+ { this.renderInstallationCount() }
{!hasAddon &&
{!!introduction &&
@@ -323,12 +327,7 @@ export class ExperimentDetail extends React.Component {
{this.renderIncompatibleAddons()}
- {completedDate &&
-
- This experiment is ending on {completedDate} .
- After then you will still be able to use {title} but we will no longer be providing updates or support.
-
-
}
+ {this.renderEolBlock()}
{hasAddon &&
{!!introduction &&
@@ -438,16 +437,26 @@ export class ExperimentDetail extends React.Component {
// which has been sending telemetry pings via installs from dev
// TODO: figure out a non-hack way to toggle user counts when we have
// telemetry data coming in from prod
- renderInstallationCount(installation_count, title) {
- if (installation_count <= 100) {
+ renderInstallationCount() {
+ const { experiment, isAfterCompletedDate } = this.props;
+ const { completed, title, installation_count } = experiment;
+
+ if (isAfterCompletedDate(experiment)) {
+ const completedDate = formatDate(completed);
+ return (
+ Experiment End Date: {completedDate}
+ );
+ }
+ if (!installation_count || installation_count <= 100) {
return (
Just launched!
);
}
+ const installCount = installation_count.toLocaleString();
return (
- There are {installation_count}
- people trying {title} right now!
+ There are {installCount}
+ people trying {title} right now!
);
}
@@ -463,6 +472,59 @@ export class ExperimentDetail extends React.Component {
return null;
}
+ renderExperimentControls() {
+ const { enabled, isEnabling, isDisabling, progressButtonWidth } = this.state;
+ const { experiment, installed, isAfterCompletedDate, hasAddon } = this.props;
+ const { title, min_release, survey_url } = experiment;
+ const validVersion = this.isValidVersion(min_release);
+ const surveyURL = buildSurveyURL('givefeedback', title, installed, survey_url);
+
+ if (!hasAddon || !validVersion) {
+ return null;
+ }
+ if (isAfterCompletedDate(experiment)) {
+ if (enabled) {
+ return (
+
+ { e.preventDefault(); this.setState({ showEolDialog: true }); }} style={{ width: progressButtonWidth }} id="uninstall-button" className={classnames(['button', 'secondary', 'warning'], { 'state-change': isDisabling })}>Disabling... Disable {title}
+
+ );
+ }
+ return null;
+ }
+ if (enabled) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ renderEolBlock() {
+ const { experiment, isAfterCompletedDate } = this.props;
+ if (!experiment.completed || isAfterCompletedDate(experiment)) { return null; }
+
+ const title = experiment.title;
+ const completedDate = formatDate(experiment.completed);
+
+ return (
+
+
+ This experiment is ending on {completedDate} .
+ After then you will still be able to use {title} but we will no longer be providing updates or support.
+
+
+ );
+ }
+
// scrollOffset lets us scroll to the top of the highlight box shadow animation
highlightPrivacy(evt) {
const { getElementOffsetHeight, getElementY, setScrollY } = this.props;
@@ -592,6 +654,7 @@ ExperimentDetail.propTypes = {
installed: React.PropTypes.object,
installedAddons: React.PropTypes.array,
navigateTo: React.PropTypes.func,
+ isAfterCompletedDate: React.PropTypes.func,
isExperimentEnabled: React.PropTypes.func,
requireRestart: React.PropTypes.func,
sendToGA: React.PropTypes.func,
diff --git a/frontend/src/app/containers/HomePageNoAddon.js b/frontend/src/app/containers/HomePageNoAddon.js
index b0e62332da..6c498a51d4 100644
--- a/frontend/src/app/containers/HomePageNoAddon.js
+++ b/frontend/src/app/containers/HomePageNoAddon.js
@@ -9,11 +9,11 @@ import View from '../components/View';
export default class HomePageNoAddon extends React.Component {
render() {
- const { experiments, isExperimentCompleted } = this.props;
+ const { experiments, isAfterCompletedDate } = this.props;
if (experiments.length === 0) { return ; }
- const currentExperiments = experiments.filter(x => !isExperimentCompleted(x));
+ const currentExperiments = experiments.filter(x => !isAfterCompletedDate(x));
return (
@@ -78,5 +78,5 @@ HomePageNoAddon.propTypes = {
hasAddon: React.PropTypes.bool,
isFirefox: React.PropTypes.bool,
experiments: React.PropTypes.array,
- isExperimentCompleted: React.PropTypes.func
+ isAfterCompletedDate: React.PropTypes.func
};
diff --git a/frontend/src/app/containers/HomePageWithAddon.js b/frontend/src/app/containers/HomePageWithAddon.js
index ef15807fa7..8e8886e801 100644
--- a/frontend/src/app/containers/HomePageWithAddon.js
+++ b/frontend/src/app/containers/HomePageWithAddon.js
@@ -18,7 +18,7 @@ export default class HomePageWithAddon extends React.Component {
}
render() {
- const { experiments, getCookie, removeCookie, getWindowLocation, isExperimentCompleted } = this.props;
+ const { experiments, getCookie, removeCookie, getWindowLocation, isAfterCompletedDate } = this.props;
if (experiments.length === 0) { return ; }
@@ -30,8 +30,8 @@ export default class HomePageWithAddon extends React.Component {
showEmailDialog = true;
}
const { showPastExperiments } = this.state;
- const currentExperiments = experiments.filter(x => !isExperimentCompleted(x));
- const pastExperiments = experiments.filter(isExperimentCompleted);
+ const currentExperiments = experiments.filter(x => !isAfterCompletedDate(x));
+ const pastExperiments = experiments.filter(isAfterCompletedDate);
return (
@@ -79,5 +79,5 @@ HomePageWithAddon.propTypes = {
uninstallAddon: React.PropTypes.func,
sendToGA: React.PropTypes.func,
openWindow: React.PropTypes.func,
- isExperimentCompleted: React.PropTypes.func
+ isAfterCompletedDate: React.PropTypes.func
};
diff --git a/frontend/src/app/reducers/addon.js b/frontend/src/app/reducers/addon.js
index 838eb03530..6027be8d52 100644
--- a/frontend/src/app/reducers/addon.js
+++ b/frontend/src/app/reducers/addon.js
@@ -36,7 +36,7 @@ export const getInstalled = (state) => state.installed;
export const isExperimentEnabled = (state, experiment) =>
!!(experiment && experiment.addon_id in state.installed);
-export const isExperimentCompleted = (experiment) =>
+export const isAfterCompletedDate = (experiment) =>
((new Date(experiment.completed)).getTime() < Date.now());
export const isInstalledLoaded = (state) => state.installedLoaded;
diff --git a/frontend/src/styles/modules/_experiment-details.scss b/frontend/src/styles/modules/_experiment-details.scss
index fdd7d3958c..b4adc5f4c4 100644
--- a/frontend/src/styles/modules/_experiment-details.scss
+++ b/frontend/src/styles/modules/_experiment-details.scss
@@ -246,6 +246,29 @@
padding: 10px;
}
+.completed-block {
+ background: $error-red-background;
+ border: 1px solid $error-red-border;
+ border-radius: 3px;
+ margin-bottom: $grid-unit;
+ padding: $grid-unit;
+
+ h3,
+ p {
+ font-size: $font-unit * (7 / 6);
+ margin: 0;
+ }
+
+ h3 {
+ font-weight: bold;
+ }
+
+ p {
+ line-height: $line-unit;
+ padding-top: 10px;
+ }
+}
+
.details-sections {
section {
diff --git a/frontend/test/app/components/ExperimentEolDialog-test.js b/frontend/test/app/components/ExperimentEolDialog-test.js
new file mode 100644
index 0000000000..db3ea2b39a
--- /dev/null
+++ b/frontend/test/app/components/ExperimentEolDialog-test.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import { shallow, mount } from 'enzyme';
+
+import ExperimentEolDialog from '../../../src/app/components/ExperimentEolDialog';
+
+describe('app/components/ExperimentEolDialog', () => {
+ let props, mockClickEvent, subject;
+ beforeEach(function() {
+ props = {
+ onSubmit: sinon.spy(),
+ onCancel: sinon.spy()
+ };
+ mockClickEvent = {
+ preventDefault: sinon.spy()
+ };
+ subject = shallow( );
+ });
+
+ const findByL10nID = id => subject.findWhere(el => id === el.props()['data-l10n-id']);
+
+ it('should display expected content', () => {
+ expect(subject.find('#retire-dialog-modal')).to.have.property('length', 1);
+ });
+
+ it('calls onCancel when the cancel button is clicked', () => {
+ subject.find('.modal-actions a.cancel').simulate('click', mockClickEvent);
+ expect(props.onCancel.called).to.be.true;
+ });
+
+ it('calls onSubmit when the disable button is clicked', () => {
+ findByL10nID('disableExperiment').simulate('click', mockClickEvent);
+ expect(props.onSubmit.called).to.be.true;
+ });
+
+});
diff --git a/frontend/test/app/components/ExperimentRowCard-test.js b/frontend/test/app/components/ExperimentRowCard-test.js
index 81f04fa3dc..883082f3db 100644
--- a/frontend/test/app/components/ExperimentRowCard-test.js
+++ b/frontend/test/app/components/ExperimentRowCard-test.js
@@ -27,7 +27,7 @@ describe('app/components/ExperimentRowCard', () => {
navigateTo: sinon.spy(),
sendToGA: sinon.spy(),
getExperimentLastSeen: sinon.spy(),
- isExperimentCompleted: sinon.spy()
+ isAfterCompletedDate: sinon.spy()
};
subject = shallow( );
});
@@ -165,7 +165,7 @@ describe('app/components/ExperimentRowCard', () => {
experiment: { ...mockExperiment,
completed: moment().subtract(1, 'days').utc()
},
- isExperimentCompleted: () => true
+ isAfterCompletedDate: () => true
});
expect(findByL10nID('experimentCardLearnMore')).to.have.property('length', 1);
expect(findByL10nID('participantCount')).to.have.property('length', 0);
diff --git a/frontend/test/app/containers/ExperimentPage-test.js b/frontend/test/app/containers/ExperimentPage-test.js
index 4f2ace7018..46f3eb22e5 100644
--- a/frontend/test/app/containers/ExperimentPage-test.js
+++ b/frontend/test/app/containers/ExperimentPage-test.js
@@ -75,6 +75,7 @@ describe('app/components/ExperimentPage:ExperimentDetail', () => {
params: {},
uninstallAddon: sinon.spy(),
navigateTo: sinon.spy(),
+ isAfterCompletedDate: sinon.stub().returns(false),
isExperimentEnabled: sinon.spy(),
requireRestart: sinon.spy(),
sendToGA: sinon.spy(),
@@ -445,6 +446,42 @@ describe('app/components/ExperimentPage:ExperimentDetail', () => {
});
+ describe('with a completed experiment', () => {
+ beforeEach(() => {
+ subject.setProps({
+ experiment: Object.assign({}, mockExperiment, { completed: '2016-10-01' }),
+ isAfterCompletedDate: sinon.stub().returns(true)
+ });
+ });
+
+ it('does not render controls', () => {
+ expect(subject.find('.experiment-controls').length).to.equal(0);
+ });
+
+ it('displays the end date instead of install count', () => {
+ expect(findByL10nID('completedDateLabel').length).to.equal(1);
+ expect(findByL10nID('userCountContainer').length).to.equal(0);
+ expect(findByL10nID('userCountContainerAlt').length).to.equal(0);
+ });
+
+ describe('with experiment enabled', () => {
+ beforeEach(() => {
+ subject.setProps({ isExperimentEnabled: experiment => true });
+ });
+
+ it('only renders the disable button control', () => {
+ expect(findByL10nID('giveFeedback').length).to.equal(0);
+ expect(findByL10nID('disableExperiment').length).to.equal(1);
+ expect(subject.find('#uninstall-button').hasClass('warning')).to.equal(true);
+ });
+
+ it('shows a modal dialog when the disable button is clicked', () => {
+ expect(subject.state('showEolDialog')).to.equal(false);
+ subject.find('#uninstall-button').simulate('click', mockClickEvent);
+ expect(subject.state('showEolDialog')).to.equal(true);
+ });
+ });
+ });
});
});
diff --git a/frontend/test/app/containers/HomePageNoAddon-test.js b/frontend/test/app/containers/HomePageNoAddon-test.js
index 8599f706e0..c64b23b1c3 100644
--- a/frontend/test/app/containers/HomePageNoAddon-test.js
+++ b/frontend/test/app/containers/HomePageNoAddon-test.js
@@ -15,7 +15,7 @@ describe('app/containers/HomePageNoAddon', () => {
isFirefox: false,
uninstallAddon: sinon.spy(),
sendToGA: sinon.spy(),
- isExperimentCompleted: sinon.spy(x => !!x.completed)
+ isAfterCompletedDate: sinon.spy(x => !!x.completed)
};
subject = shallow( );
});
diff --git a/frontend/test/app/containers/HomePageWithAddon-test.js b/frontend/test/app/containers/HomePageWithAddon-test.js
index ee3c49c76d..0cba2abab8 100644
--- a/frontend/test/app/containers/HomePageWithAddon-test.js
+++ b/frontend/test/app/containers/HomePageWithAddon-test.js
@@ -20,7 +20,7 @@ describe('app/containers/HomePageWithAddon', () => {
subscribeToBasket: sinon.spy(),
sendToGA: sinon.spy(),
openWindow: sinon.spy(),
- isExperimentCompleted: sinon.spy(x => !!x.completed)
+ isAfterCompletedDate: sinon.spy(x => !!x.completed)
};
subject = shallow( );
});
diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl
index 1af3d49cfd..07674c527c 100644
--- a/locales/en-US/app.ftl
+++ b/locales/en-US/app.ftl
@@ -88,6 +88,7 @@ otherExperiments = Try out these experiments as well
giveFeedback = Give Feedback
+disableHeader = Disable Experiment?
disableExperiment = Disable {$title}
disableExperimentTransition = Disabling...
enableExperiment = Enable {$title}
@@ -161,6 +162,8 @@ shareEmail = E-mail
shareCopy = Copy
eolMessage = This experiment is ending on {$completedDate} . After then you will still be able to use {$title} but we will no longer be providing updates or support.
+eolDisableMessage = The {$title} experiment has ended. Once you uninstall it you won't be able to re-install it through Test Pilot again.
+completedDateLabel = Experiment End Date: {$completedDate}
incompatibleHeader = This experiment may not be compatible with add-ons you have installed.
incompatibleSubheader = We recommend disabling these add-ons before activating this experiment: