Skip to content

Commit

Permalink
Merge pull request mozilla#1625 from dannycoates/1302-completed-exp-page
Browse files Browse the repository at this point in the history
completed experiment page
  • Loading branch information
dannycoates authored Oct 19, 2016
2 parents 13b5edc + eda70df commit 25037aa
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 40 deletions.
43 changes: 43 additions & 0 deletions frontend/src/app/components/ExperimentEolDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';

export default class ExperimentEolDialog extends React.Component {

render() {
const { title } = this.props;
return (
<div className="modal-container">
<div id="retire-dialog-modal" className="modal feedback-modal modal-bounce-in">
<header>
<h3 className="title warning" data-l10n-id="disableHeader">Disable Experiment?</h3>
</header>
<form>

<div className="modal-content modal-form">
<p data-l10n-id="eolDisableMessage" data-l10n-args={JSON.stringify({ title })} className="centered">The {title} experiment has ended. Once you uninstall it you won't be able to re-install it through Test Pilot again.</p>
</div>
<div className="modal-actions">
<button onClick={e => this.proceed(e)} data-l10n-id="disableExperiment" data-l10n-args={JSON.stringify({ title })} className="submit button warning large">Disable {title}</button>
<a onClick={e => this.cancel(e)} data-l10n-id="retireCancelButton" className="cancel modal-escape" href="">Cancel</a>
</div>
</form>
</div>
</div>
);
}

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
};
6 changes: 3 additions & 3 deletions frontend/src/app/components/ExperimentRowCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') ?
Expand Down Expand Up @@ -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
};
4 changes: 2 additions & 2 deletions frontend/src/app/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
109 changes: 86 additions & 23 deletions frontend/src/app/containers/ExperimentPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -200,6 +200,15 @@ export class ExperimentDetail extends React.Component {
surveyURL={surveyURL}
onCancel={() => this.setState({ showPreFeedbackDialog: false })} />}

{showEolDialog &&
<ExperimentEolDialog
title={title}
onCancel={() => this.setState({ showEolDialog: false })}
onSubmit={e => {
this.setState({ showEolDialog: false });
this.renderUninstallSurvey(e);
}} />}

<View {...this.props}>

{!hasAddon && <section data-hook="testpilot-promo">
Expand Down Expand Up @@ -235,12 +244,7 @@ export class ExperimentDetail extends React.Component {
<h1 data-hook="title">{title}</h1>
<h4 data-hook="subtitle" className="subtitle">{subtitle}</h4>
</header>
{hasAddon && validVersion && <div className="experiment-controls" data-hook="active-user">
{!enabled && <a onClick={e => this.highlightPrivacy(e)} data-hook="highlight-privacy" className="highlight-privacy" data-l10n-id="highlightPrivacy">Your privacy</a>}
{enabled && <a onClick={e => this.handleFeedback(e)} data-l10n-id="giveFeedback" data-hook="feedback" id="feedback-button" className="button default" href={surveyURL}>Give Feedback</a>}
{enabled && <button onClick={e => this.renderUninstallSurvey(e)} style={{ width: progressButtonWidth }} data-hook="uninstall-experiment" id="uninstall-button" className={classnames(['button', 'secondary'], { 'state-change': isDisabling })}><span className="state-change-inner"></span><span data-l10n-id="disableExperimentTransition" className="transition-text">Disabling...</span><span data-l10n-id="disableExperiment" data-l10n-args={JSON.stringify({ title })} className="default-text">Disable <span data-hook="title">{title}</span></span></button>}
{!enabled && <button onClick={e => this.installExperiment(e)} style={{ width: progressButtonWidth }} data-hook="install-experiment" id="install-button" className={classnames(['button', 'default'], { 'state-change': isEnabling })}><span className="state-change-inner"></span><span data-l10n-id="enableExperimentTransition" className="transition-text">Enabling...</span><span data-l10n-id="enableExperiment" data-l10n-args={JSON.stringify({ title })} className="default-text">Enable <span data-hook="title">{title}</span></span></button>}
</div>}
{ this.renderExperimentControls() }
{ this.renderMinimumVersionNotice(title, hasAddon, min_release) }
</div>
</div>
Expand All @@ -258,7 +262,7 @@ export class ExperimentDetail extends React.Component {
</div>
<div className="details-sections">
<section className="user-count">
{ this.renderInstallationCount(installation_count, title) }
{ this.renderInstallationCount() }
</section>
{!hasAddon && <div data-hook="inactive-user">
{!!introduction && <section className="introduction" data-hook="introduction-container">
Expand Down Expand Up @@ -323,12 +327,7 @@ export class ExperimentDetail extends React.Component {

<div className="details-description">
{this.renderIncompatibleAddons()}
{completedDate && <div data-hook="eol-message">
<div className="eol-block"><div data-hook="ending-soon" data-l10n-id="eolMessage" data-l10n-args={JSON.stringify({ title, completedDate })}>
<strong>This experiment is ending on <span data-hook="completedDate">{completedDate}</span></strong>.<br/><br/>
After then you will still be able to use <span data-hook="title">{title}</span> but we will no longer be providing updates or support.</div>
</div>
</div>}
{this.renderEolBlock()}
{hasAddon && <div data-hook="active-user">
{!!introduction && <section className="introduction" data-hook="introduction-container">
<div data-hook="introduction-html" dangerouslySetInnerHTML={createMarkup(introduction)}></div>
Expand Down Expand Up @@ -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 (
<span data-l10n-id="completedDateLabel" data-l10n-args={JSON.stringify({ completedDate })}>Experiment End Date: {completedDate}</span>
);
}
if (!installation_count || installation_count <= 100) {
return (
<span data-l10n-id="userCountContainerAlt" className="bold" data-l10n-args={JSON.stringify({ title })}>
Just launched!</span>
);
}
const installCount = installation_count.toLocaleString();
return (
<span data-l10n-id="userCountContainer" data-l10n-args={JSON.stringify({ installation_count, title })}>There are <span data-l10n-id="userCount" className="bold" data-hook="install-count">{installation_count}</span>
people trying <span data-hook="title">{title}</span> right now!</span>
<span data-l10n-id="userCountContainer" data-l10n-args={JSON.stringify({ installation_count: installCount, title })}>There are <span data-l10n-id="userCount" className="bold">{installCount}</span>
people trying {title} right now!</span>
);
}

Expand All @@ -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 (
<div className="experiment-controls">
<button onClick={e => { e.preventDefault(); this.setState({ showEolDialog: true }); }} style={{ width: progressButtonWidth }} id="uninstall-button" className={classnames(['button', 'secondary', 'warning'], { 'state-change': isDisabling })}><span className="state-change-inner"></span><span data-l10n-id="disableExperimentTransition" className="transition-text">Disabling...</span><span data-l10n-id="disableExperiment" data-l10n-args={JSON.stringify({ title })} className="default-text">Disable {title}</span></button>
</div>
);
}
return null;
}
if (enabled) {
return (
<div className="experiment-controls">
<a onClick={e => this.handleFeedback(e)} data-l10n-id="giveFeedback" id="feedback-button" className="button default" href={surveyURL}>Give Feedback</a>
<button onClick={e => this.renderUninstallSurvey(e)} style={{ width: progressButtonWidth }} id="uninstall-button" className={classnames(['button', 'secondary'], { 'state-change': isDisabling })}><span className="state-change-inner"></span><span data-l10n-id="disableExperimentTransition" className="transition-text">Disabling...</span><span data-l10n-id="disableExperiment" data-l10n-args={JSON.stringify({ title })} className="default-text">Disable {title}</span></button>
</div>
);
}
return (
<div className="experiment-controls">
<a onClick={e => this.highlightPrivacy(e)} className="highlight-privacy" data-l10n-id="highlightPrivacy">Your privacy</a>
<button onClick={e => this.installExperiment(e)} style={{ width: progressButtonWidth }} id="install-button" className={classnames(['button', 'default'], { 'state-change': isEnabling })}><span className="state-change-inner"></span><span data-l10n-id="enableExperimentTransition" className="transition-text">Enabling...</span><span data-l10n-id="enableExperiment" data-l10n-args={JSON.stringify({ title })} className="default-text">Enable {title}</span></button>
</div>
);
}

renderEolBlock() {
const { experiment, isAfterCompletedDate } = this.props;
if (!experiment.completed || isAfterCompletedDate(experiment)) { return null; }

const title = experiment.title;
const completedDate = formatDate(experiment.completed);

return (
<div className="eol-block">
<div data-l10n-id="eolMessage" data-l10n-args={JSON.stringify({ title, completedDate })}>
<strong>This experiment is ending on {completedDate}</strong>.<br/><br/>
<span>After then you will still be able to use {title} but we will no longer be providing updates or support.</span>
</div>
</div>
);
}

// scrollOffset lets us scroll to the top of the highlight box shadow animation
highlightPrivacy(evt) {
const { getElementOffsetHeight, getElementY, setScrollY } = this.props;
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/containers/HomePageNoAddon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <LoadingPage />; }

const currentExperiments = experiments.filter(x => !isExperimentCompleted(x));
const currentExperiments = experiments.filter(x => !isAfterCompletedDate(x));

return (
<section data-hook="landing-page">
Expand Down Expand Up @@ -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
};
8 changes: 4 additions & 4 deletions frontend/src/app/containers/HomePageWithAddon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <LoadingPage />; }

Expand All @@ -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 (
<View {...this.props}>
Expand Down Expand Up @@ -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
};
2 changes: 1 addition & 1 deletion frontend/src/app/reducers/addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/styles/modules/_experiment-details.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 25037aa

Please sign in to comment.