Source: segments-experiment-web/src/main/resources/META-INF/resources/js/components/ReviewExperimentModal.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import React, {
	useCallback,
	useContext,
	useState,
	useEffect,
	useRef
} from 'react';
import PropTypes from 'prop-types';
import ClayButton from '@clayui/button';
import ClayLoadingIndicator from '@clayui/loading-indicator';
import ClayModal, {useModal} from '@clayui/modal';
import {SplitPicker} from './SplitPicker/SplitPicker.es';
import {SliderWithLabel} from './SliderWithLabel.es';
import {SegmentsVariantType} from '../types.es';
import {
	INITIAL_CONFIDENCE_LEVEL,
	MAX_CONFIDENCE_LEVEL,
	MIN_CONFIDENCE_LEVEL,
	percentageNumberToIndex
} from '../util/percentages.es';
import BusyButton from './BusyButton/BusyButton.es';
import SegmentsExperimentContext from '../context.es';
import {StateContext} from '../state/context.es';
import {useDebounceCallback} from '../util/hooks.es';
import {SUCCESS_ANIMATION_FILE_NAME} from '../util/contants.es';

const TIME_ESTIMATION_THROTTLE_TIME_MS = 1000;

function ReviewExperimentModal({onRun, variants, visible, setVisible}) {
	const [busy, setBusy] = useState(false);
	const [success, setSuccess] = useState(false);
	const [estimation, setEstimation] = useState({
		days: null,
		loading: true
	});
	const [confidenceLevel, setConfidenceLevel] = useState(
		INITIAL_CONFIDENCE_LEVEL
	);
	const [draftVariants, setDraftVariants] = useState(
		variants.map((variant, index) => {
			const remainingSplit = 100 % variants.length;
			const splitValue = parseInt(100 / variants.length, 10);

			let split;

			if (index === 0 && remainingSplit > 0) {
				split = splitValue + remainingSplit;
			} else {
				split = splitValue;
			}

			return {...variant, split};
		})
	);
	const {assetsPath, APIService} = useContext(SegmentsExperimentContext);
	const {experiment} = useContext(StateContext);

	const {observer, onClose} = useModal({
		onClose: () => setVisible(false)
	});

	const mounted = useRef();

	useEffect(() => {
		mounted.current = true;
		return () => {
			mounted.current = false;
		};
	});

	const successAnimationPath = `${assetsPath}${SUCCESS_ANIMATION_FILE_NAME}`;

	const [getEstimation] = useDebounceCallback(body => {
		APIService.getEstimatedTime(body)
			.then(({segmentsExperimentEstimatedDaysDuration}) => {
				if (mounted.current) {
					setEstimation({
						days: segmentsExperimentEstimatedDaysDuration,
						loading: false
					});
				}
			})
			.catch(_error => {
				if (mounted.current) {
					setEstimation({
						days: null,
						loading: false
					});
				}
			});
	}, TIME_ESTIMATION_THROTTLE_TIME_MS);

	useEffect(() => {
		setEstimation({loading: true});

		getEstimation({
			confidenceLevel,
			segmentsExperimentId: experiment.segmentsExperimentId,
			segmentsExperimentRels: JSON.stringify(
				_variantsToSplitVariantsMap(draftVariants)
			)
		});
	}, [
		draftVariants,
		confidenceLevel,
		getEstimation,
		experiment.segmentsExperimentId
	]);

	const [height, setHeight] = useState(0);

	const measureHeight = useCallback(
		node => {
			if (node !== null && !success) {
				setHeight(node.getBoundingClientRect().height);
			}
		},
		[setHeight, success]
	);

	return (
		visible && (
			<ClayModal observer={observer} size="lg">
				<ClayModal.Header>
					{success
						? Liferay.Language.get('test-started-successfully')
						: Liferay.Language.get('review-and-run-test')}
				</ClayModal.Header>
				<ClayModal.Body>
					{success ? (
						<div
							className="text-center"
							style={{height: height + 'px'}}
						>
							<img
								alt=""
								className="mb-4 mt-3"
								src={successAnimationPath}
								width="250px"
							/>
							<h3>
								{Liferay.Language.get('test-running-message')}
							</h3>
						</div>
					) : (
						<div ref={measureHeight}>
							<h3 className="sheet-subtitle border-bottom-0 text-secondary">
								{Liferay.Language.get('traffic-split')}
							</h3>

							<SplitPicker
								onChange={variants => {
									setDraftVariants(variants);
								}}
								variants={draftVariants}
							/>

							<hr />

							<h3 className="sheet-subtitle border-bottom-0 text-secondary">
								{Liferay.Language.get('confidence-level')}
							</h3>

							<SliderWithLabel
								label={Liferay.Language.get(
									'confidence-level-required'
								)}
								max={MAX_CONFIDENCE_LEVEL}
								min={MIN_CONFIDENCE_LEVEL}
								onValueChange={setConfidenceLevel}
								value={confidenceLevel}
							/>

							<hr />
							<div>
								<div className="d-flex">
									<label className="w-100">
										{Liferay.Language.get(
											'estimated-time-to-declare-winner'
										)}
									</label>

									<p className="mb-0 text-nowrap">
										{estimation.loading ? (
											<ClayLoadingIndicator
												className="my-0"
												small
											/>
										) : (
											estimation.days &&
											_getDaysMessage(estimation.days)
										)}
									</p>
								</div>
								<p className="small text-secondary">
									{Liferay.Language.get(
										'time-depends-on-confidence-level-and-traffic-to-the-variants'
									)}
								</p>
							</div>
						</div>
					)}
				</ClayModal.Body>

				<ClayModal.Footer
					last={
						success ? (
							<ClayButton.Group>
								<ClayButton
									displayType="primary"
									onClick={() => setVisible(false)}
								>
									{Liferay.Language.get('ok')}
								</ClayButton>
							</ClayButton.Group>
						) : (
							<ClayButton.Group spaced>
								<ClayButton
									displayType="secondary"
									onClick={onClose}
								>
									{Liferay.Language.get('cancel')}
								</ClayButton>

								<BusyButton
									busy={busy}
									disabled={busy}
									onClick={_handleRun}
								>
									{Liferay.Language.get('run')}
								</BusyButton>
							</ClayButton.Group>
						)
					}
				/>
			</ClayModal>
		)
	);

	/**
	 * Creates a splitVariantsMap `{ [segmentsExperimentRelId]: splitValue, ... }`
	 * and bubbles up `onRun` with `confidenceLevel` and `splitVariantsMap`
	 */
	function _handleRun() {
		const splitVariantsMap = _variantsToSplitVariantsMap(draftVariants);

		setBusy(true);

		onRun({
			confidenceLevel: percentageNumberToIndex(confidenceLevel),
			splitVariantsMap
		}).then(() => {
			if (mounted.current) {
				setBusy(false);
				setSuccess(true);
			}
		});
	}
}

function _variantsToSplitVariantsMap(variants) {
	return variants.reduce((acc, v) => {
		return {
			...acc,
			[v.segmentsExperimentRelId]: percentageNumberToIndex(v.split)
		};
	}, {});
}

function _getDaysMessage(days) {
	if (days === 1)
		return Liferay.Util.sub(Liferay.Language.get('x-day'), days);
	else return Liferay.Util.sub(Liferay.Language.get('x-days'), days);
}

ReviewExperimentModal.propTypes = {
	onRun: PropTypes.func.isRequired,
	setVisible: PropTypes.func.isRequired,
	variants: PropTypes.arrayOf(SegmentsVariantType),
	visible: PropTypes.bool.isRequired
};

export {ReviewExperimentModal};