Skip to content

Commit

Permalink
Merge pull request #385 from keen/feat/percentile
Browse files Browse the repository at this point in the history
feat: 🎸 Percentile
  • Loading branch information
dariuszlacheta authored Aug 13, 2020
2 parents bbb24ec + 97ef797 commit da6720e
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 635 deletions.
15 changes: 0 additions & 15 deletions lib/js/app/queryCreator/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { FC } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { v4 as uuid } from 'uuid';
import { ActionButton } from '@keen.io/ui-core';
import { FieldGroup } from '@keen.io/forms';

import { FiltersSettings, ActionContainer } from './App.styles';

Expand All @@ -14,7 +13,6 @@ import {
OrderBy,
Interval,
Limit,
Percentile,
Title,
FunnelSteps,
Filters,
Expand All @@ -24,11 +22,9 @@ import { showField } from './utils/showField';
import text from './text.json';

import {
getPercentile,
getEventCollection,
getAnalysis,
getFilters,
setPercentile,
removeFilter,
addFilter,
setFilters,
Expand All @@ -47,7 +43,6 @@ const App: FC<Props> = () => {
const dispatch = useDispatch();
const analysis = useSelector(getAnalysis);
const collection = useSelector(getEventCollection);
const percentile = useSelector(getPercentile);

const filters = useSelector(getFilters);

Expand Down Expand Up @@ -98,16 +93,6 @@ const App: FC<Props> = () => {
</Card>

{analysis === 'extraction' && <Extraction collection={collection} />}

{showField('percentile', analysis) && (
<FieldGroup>
<Percentile
value={percentile}
onReset={() => dispatch(setPercentile(null))}
onChange={(value) => dispatch(setPercentile(value))}
/>
</FieldGroup>
)}
</div>
);
};
Expand Down
30 changes: 30 additions & 0 deletions lib/js/app/queryCreator/components/Percentile/Percentile.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import styled from 'styled-components';
import { motion } from 'framer-motion';

export const Container = styled.div`
width: 68px;
position: relative;
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
`;

export const TooltipContainer = styled(motion.div)`
position: absolute;
left: 70%;
top: 70%;
pointer-events: none;
z-index: 1;
width: 200px;
font-family: 'Lato Regular', sans-serif;
font-size: 14px;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, fireEvent } from '@testing-library/react';

import Percentile from './Percentile';

import { MAX_PERCENTILE } from './constants';
import { MIN_PERCENTILE, MAX_PERCENTILE } from './constants';

test('set input value based on percentile', () => {
const { container } = render(
Expand Down Expand Up @@ -41,6 +41,18 @@ test('calls "onChange" handler with maximum percentile value', () => {
expect(mockFn).toHaveBeenCalledWith(MAX_PERCENTILE);
});

test('calls "onChange" handler with minimum percentile value', () => {
const mockFn = jest.fn();
const { container } = render(
<Percentile onChange={mockFn} onReset={jest.fn()} value={10} />
);

const input = container.querySelector('input[type="number"]');
fireEvent.change(input, { target: { value: -2 } });

expect(mockFn).toHaveBeenCalledWith(MIN_PERCENTILE);
});

test('calls "onReset" handler', () => {
const mockFn = jest.fn();
const { container } = render(
Expand Down
64 changes: 51 additions & 13 deletions lib/js/app/queryCreator/components/Percentile/Percentile.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import React, { FC, useEffect, useCallback } from 'react';
import { Label, Input } from '@keen.io/ui-core';
import React, { FC, useState, useEffect, useCallback, useRef } from 'react';
import { AnimatePresence } from 'framer-motion';
import { Tooltip } from '@keen.io/ui-core';

import text from './text.json';
import Title from '../Title';
import Input from '../Input';

import { Container, TooltipContainer } from './Percentile.styles';

import { getPercentileValue } from './utils/getPercentileValue';

import { MAX_PERCENTILE } from './constants';
import { HIDE_TIME } from './constants';

import text from './text.json';

type Props = {
/** Percentile value */
Expand All @@ -14,16 +22,32 @@ type Props = {
onChange: (value?: number) => void;
};

export const tooltipMotion = {
transition: { duration: 0.3 },
exit: { opacity: 0 },
};

const Percentile: FC<Props> = ({ value, onReset, onChange }) => {
const containerRef = useRef(null);
const hideTooltip = useRef(null);

const [tooltip, setTooltip] = useState(false);

useEffect(() => {
return () => onReset();
}, []);

const changeHandler = useCallback(
(eventValue) => {
if (eventValue) {
const percentile = parseInt(eventValue);
onChange(percentile > MAX_PERCENTILE ? MAX_PERCENTILE : percentile);
if (eventValue.target.value) {
const percentile = parseInt(eventValue.target.value);
const { value, outRange } = getPercentileValue(percentile);
onChange(value);
if (hideTooltip.current) clearTimeout(hideTooltip.current);
setTooltip(outRange);
hideTooltip.current = setTimeout(() => {
setTooltip(false);
}, HIDE_TIME);
} else {
onReset();
}
Expand All @@ -32,16 +56,30 @@ const Percentile: FC<Props> = ({ value, onReset, onChange }) => {
);

return (
<>
<Label>{text.label}</Label>
<Container ref={containerRef}>
<AnimatePresence>
{tooltip && (
<TooltipContainer
{...tooltipMotion}
initial={{ opacity: 0 }}
animate={{
opacity: 1,
}}
>
<Tooltip mode="dark" hasArrow={false}>
{text.message}
</Tooltip>
</TooltipContainer>
)}
</AnimatePresence>
<Title>{text.label}</Title>
<Input
type="number"
variant="solid"
value={value}
value={value ? value : ''}
placeholder={text.placeholder}
onChange={(e) => changeHandler(e.target.value)}
onChange={(e) => changeHandler(e)}
/>
</>
</Container>
);
};

Expand Down
4 changes: 4 additions & 0 deletions lib/js/app/queryCreator/components/Percentile/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export const MIN_PERCENTILE = 0;

export const MAX_PERCENTILE = 100;

export const HIDE_TIME = 3000;
5 changes: 3 additions & 2 deletions lib/js/app/queryCreator/components/Percentile/text.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"label": "Percentile value",
"placeholder": "Ex. 33"
"label": "Percentile",
"placeholder": "Eg. 75",
"message": "Percentile value has to be from range 0-100"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getPercentileValue } from './getPercentileValue';
import { MIN_PERCENTILE, MAX_PERCENTILE } from '../constants';

test('should return MIN_PERCENTILE and true', () => {
const { value, outRange } = getPercentileValue(-1);

expect(value).toBe(MIN_PERCENTILE);
expect(outRange).toBeTruthy();
});

test('should return MAX_PERCENTILE and true', () => {
const { value, outRange } = getPercentileValue(102);

expect(value).toBe(MAX_PERCENTILE);
expect(outRange).toBeTruthy();
});

test('should return 50 and false', () => {
const { value, outRange } = getPercentileValue(50);

expect(value).toBe(50);
expect(outRange).toBeFalsy();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { MIN_PERCENTILE, MAX_PERCENTILE } from '../constants';

export const getPercentileValue = (
value: number
): { value: number; outRange: boolean } => {
if (value < MIN_PERCENTILE) return { value: MIN_PERCENTILE, outRange: true };
if (value > MAX_PERCENTILE) return { value: MAX_PERCENTILE, outRange: true };
return { value, outRange: false };
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import styled from 'styled-components';

export const MenuItem = styled.div`
width: 25%;
flex-basis: 25%;
max-width: 320px;
flex-shrink: 0;
flex-shrink: 1;
`;

export const MenuItemPercentile = styled.div`
flex-basis: 68px;
`;

export const Container = styled.div`
display: flex;
margin-bottom: 20px;
${MenuItem} + ${MenuItem} {
${MenuItem} + ${MenuItem},
${MenuItemPercentile} + ${MenuItem},
${MenuItem} + ${MenuItemPercentile} {
margin-left: 20px;
}
`;
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React, { FC } from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { Container, MenuItem } from './QueryArguments.styles';
import {
Container,
MenuItem,
MenuItemPercentile,
} from './QueryArguments.styles';

import {
Analysis,
EventCollection,
TargetProperty,
Timeframe,
Percentile,
} from '../../components';
import { showField } from '../../utils/showField';

Expand All @@ -20,8 +25,10 @@ import {
getTargetProperty,
getTimeframe,
getTimezone,
getPercentile,
selectTimezone,
setTimeframe,
setPercentile,
DEFAULT_TIMEFRAME,
} from '../../modules/query';

Expand All @@ -34,6 +41,7 @@ const App: FC<Props> = () => {
const timeframe = useSelector(getTimeframe);
const targetProperty = useSelector(getTargetProperty);
const timezone = useSelector(getTimezone);
const percentile = useSelector(getPercentile);

return (
<Container>
Expand Down Expand Up @@ -67,6 +75,17 @@ const App: FC<Props> = () => {
/>
</MenuItem>
)}

{showField('percentile', analysis) && (
<MenuItemPercentile>
<Percentile
value={percentile}
onReset={() => dispatch(setPercentile(null))}
onChange={(value) => dispatch(setPercentile(value))}
/>
</MenuItemPercentile>
)}

{showField('timeframe', analysis) && (
<MenuItem>
<Timeframe
Expand Down
2 changes: 1 addition & 1 deletion lib/js/app/queryCreator/modules/query/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { ReducerState, QueryActions } from './types';
export const initialState: ReducerState = {
eventCollection: null,
targetProperty: null,
percentile: null,
percentile: undefined,
timezone: DEFAULT_TIMEZONE,
groupBy: undefined,
orderBy: undefined,
Expand Down
Loading

0 comments on commit da6720e

Please sign in to comment.