<commit_before>import {Subject, animationFrameScheduler, fromEvent, interval, merge} from 'rxjs'
import {distinctUntilChanged, filter, map, scan, switchMap, takeUntil, withLatestFrom} from 'rxjs/operators'
import Hammer from 'hammerjs'
import Label from 'widget/label'
import Portal from 'widget/portal'
import PropTypes from 'prop-types'
import React from 'react'
import ReactResizeDetector from 'react-resize-detector'
import ViewportResizeDetector from 'widget/viewportResizeDetector'
import _ from 'lodash'
import styles from './slider.module.css'

const clamp = (value, {min, max}) => Math.max(min, Math.min(max, value))

const lerp = (rate, speed = 1) => (value, target) => value + (target - value) * (rate * speed)

const normalizeLinear = (value, min, max) => {
    const offset = min
    return (value - offset) / (max - offset)
}

const denormalizeLinear = (value, min, max) => {
    const offset = min
    return value * (max - offset) + offset
}

const normalizeLog = (value, min, max) => {
    const offset = min - 1
    return Math.log(value - offset) / Math.log(max - offset)
}

const denormalizeLog = (value, min, max) => {
    const offset = min - 1
    return Math.exp(value * Math.log(max - offset)) + offset
}

const normalize = (value, min, max, logScale) =>
    logScale ? normalizeLog(value, min, max) : normalizeLinear(value, min, max)

const denormalize = (value, min, max, logScale) =>
    logScale ? denormalizeLog(value, min, max) : denormalizeLinear(value, min, max)

class SliderContainer extends React.Component {
    clickTarget = React.createRef()

    renderTick({position, value, label}) {
        const left = `${Math.trunc(position)}px`
        return (
            <React.Fragment key={value}>

{label}

            </React.Fragment>
        )
    }

    renderAxis() {
        const {ticks} = this.props
        return (

                {ticks.map(tick => this.renderTick(tick))}

        )
    }

    renderClickTarget() {
        return (

        )
    }

    renderDynamics() {
        const {input, width, range, decimals, ticks, snap, minValue, maxValue} = this.props
        return (
            <SliderDynamics
                input={input}
                minValue={minValue}
                maxValue={maxValue}
                width={width}
                decimals={decimals}
                ticks={ticks}
                snap={snap}
                range={range}
                clickTarget={this.clickTarget}
                normalize={value => {
                    const {logScale, minValue, maxValue} = this.props
                    return normalize(value, minValue, maxValue, logScale)
                }}
                denormalize={value => {
                    const {logScale, minValue, maxValue} = this.props
                    return denormalize(value, minValue, maxValue, logScale)
                }}
            />
        )
    }

    render() {
        return (

                {this.renderAxis()}
                {this.renderClickTarget()}
                {this.renderDynamics()}

        )
    }
}

SliderContainer.propTypes = {
    input: PropTypes.object.isRequired,
    decimals: PropTypes.number,
    denormalize: PropTypes.func,
    disabled: PropTypes.any,
    info: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.func
    ]),
    maxValue: PropTypes.number,
    minValue: PropTypes.number,
    normalize: PropTypes.func,
    snap: PropTypes.bool,
    ticks: PropTypes.oneOfType([
        PropTypes.number,
        PropTypes.array
    ]),
    width: PropTypes.number
}

class SliderDynamics extends React.Component {
    state = {
        position: null,
        previewPosition: null,
        clickTargetOffset: null,
        dragging: null,
        inhibitInput: null
    }
    handle = React.createRef()
    subscriptions = []

    renderLeftRange() {
        const {position} = this.state
        const {width} = this.props
        return <div className={styles.range} style={{right: `${width - position}px`}}/>
    }

    renderRightRange() {
        const {position} = this.state
        return <div className={styles.range} style={{left: `${position}px`}}/>
    }

    renderRanges() {
        const {range} = this.props
        return (
            <React.Fragment>
                {range === 'left' ? this.renderLeftRange() : null}
                {range === 'right' ? this.renderRightRange() : null}
            </React.Fragment>
        )
    }

    renderPreview() {
        const {previewPosition} = this.state
        return previewPosition !== null ? (
            <div className={[styles.cursor, styles.preview].join(' ')} ref={this.preview}
                style={{left: `${previewPosition}px`}}/>
        ) : null
    }

    renderHandle() {
        const {position, dragging} = this.state
        return (
            <div className={[styles.cursor, styles.handle, dragging ? styles.dragging : null].join(' ')}
                ref={this.handle}
                style={{left: `${position}px`}}/>
        )
    }

    renderOverlay() {
        const {dragging} = this.state
        return dragging
            ? (
                <Portal>

                </Portal>
            ) : null
    }

    renderViewportResizer() {
        return (
            <ViewportResizeDetector onChange={() => this.setClickTargetOffset()}/>
        )
    }

    render() {
        return (
            <React.Fragment>
                {this.renderRanges()}
                {this.renderPreview()}
                {this.renderHandle()}
                {this.renderOverlay()}
                {this.renderViewportResizer()}
            </React.Fragment>
        )
    }

    componentDidMount() {
        const {clickTarget} = this.props
        const {position, previewPosition, clickTargetOffset} = this.state

        this.setHandlePositionByValue()
        const handle = new Hammer(this.handle.current, {
            threshold: 1
        })
        handle.get('pan').set({direction: Hammer.DIRECTION_HORIZONTAL})

        const click = new Hammer(clickTarget.current, {
            threshold: 1
        })

        const pan$ = fromEvent(handle, 'panstart panmove panend')
        const panStart$ = pan$.pipe(filter(e => e.type === 'panstart'))
        const panMove$ = pan$.pipe(filter(e => e.type === 'panmove'))
        const panEnd$ = pan$.pipe(filter(e => e.type === 'panend'))
        const animationFrame$ = interval(0, animationFrameScheduler)
        const stop$ = new Subject()

        const drag$ = panStart$.pipe(
            switchMap(() => {
                const start = position
                return merge(
                    panMove$.pipe(
                        map(pmEvent => this.clampPosition(start + pmEvent.deltaX)),
                        distinctUntilChanged()
                    ),
                    panEnd$.pipe(
                        map(() => this.snapPosition(previewPosition))
                    )
                )
            })
        )

        const click$ = fromEvent(click, 'tap').pipe(
            map(tap => this.snapPosition(tap.center.x - clickTargetOffset))
        )

        const clickOrDrag$ = merge(click$, drag$)

        // move on click or drag
        const move$ = clickOrDrag$.pipe(
            switchMap(cursor => {
                const start = position
                return animationFrame$.pipe(
                    map(() => cursor),
                    scan(lerp(.1), start),
                    takeUntil(stop$)
                )
            })
        )

        // enable preview on mouseover, disable on mouseleave
        this.subscriptions.push(
            merge(
                drag$.pipe(
                    map(cursor => this.snapPosition(cursor)),
                ),
                fromEvent(clickTarget.current, 'mousemove').pipe(
                    map(e => this.snapPosition(e.clientX - clickTargetOffset)),
                ),
                fromEvent(clickTarget.current, 'mouseleave').pipe(
                    map(() => null)
                )
            ).pipe(
                distinctUntilChanged()
            ).subscribe(previewPosition =>
                this.setPreviewPosition(previewPosition)
            )
        )

        // enable fullscreen pointer while dragging, disable when drag end
        this.subscriptions.push(
            merge(
                panStart$.pipe(map(() => true)),
                panEnd$.pipe(map(() => false))
            ).subscribe(dragging =>
                this.setDragging(dragging)
            )
        )

        // enable input when stopped, disabled when moving
        this.subscriptions.push(
            move$.pipe(
                withLatestFrom(clickOrDrag$),
                map(([a, b]) => Math.trunc(a) !== Math.trunc(b)),
                distinctUntilChanged()
            ).subscribe(status => {
                this.setInhibitInput(status)
                status === false && stop$.next()
            })
        )

        // render animation
        this.subscriptions.push(
            move$.subscribe(position =>
                this.setHandlePosition(position)
            )
        )

        // update input on click or pan end
        this.subscriptions.push(
            merge(click$, panEnd$).subscribe(() =>
                this.updateInputValue()
            )
        )
    }

    componentWillUnmount() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe())
    }

    componentDidUpdate(prevProps) {
        const {inhibitInput} = this.state
        if (!inhibitInput && !_.isEqual(prevProps, this.props))
            this.setHandlePositionByValue()
    }

    toPosition(value) {
        const {normalize, width} = this.props
        return normalize(value) * width
    }

    toValue(position) {
        const {denormalize, width} = this.props
        return denormalize(position / width)
    }

    clampPosition(position) {
        const {width} = this.props
        return clamp(position, {
            min: 0,
            max: width
        })
    }

    snapPosition(value) {
        const {ticks, snap} = this.props
        if (snap) {
            const closest = _(ticks)
                .map(({position}) => ({position, distance: Math.abs(position - value)}))
                .sortBy('distance')
                .head()
            return closest.position
        } else {
            return value
        }
    }

    setHandlePositionByValue() {
        const {input} = this.props
        this.setHandlePosition(this.toPosition(input.value))
    }

    setHandlePosition(position) {
        if (position >= 0) {
            position = Math.round(position)
            if (position !== this.state.position) {
                this.setState(prevState => ({...prevState, position}))
            }
        }
    }

    updateInputValue() {
        const {input, decimals} = this.props
        const {previewPosition} = this.state
        const value = this.toValue(this.snapPosition(previewPosition))
        const factor = Math.pow(10, decimals)
        const roundedValue = Math.round(value * factor) / factor
        input.set(roundedValue)
    }

    setPreviewPosition(previewPosition) {
        this.setState(prevState => ({
            ...prevState,
            previewPosition
        }))
    }

    setInhibitInput(inhibitInput) {
        this.setState(prevState => ({...prevState, inhibitInput}))
    }

    setDragging(dragging) {
        this.setState(prevState => ({...prevState, dragging}))
    }

    setClickTargetOffset() {
        const {clickTarget} = this.props
        const {clickTargetOffset} = this.state
        if (clickTargetOffset !== Math.trunc(clickTarget.current.getBoundingClientRect().left)) {
            this.setState(prevState => ({
                ...prevState,
                clickTargetOffset
            }))
        }
    }

}

SliderDynamics.propTypes = {
    denormalize: PropTypes.func.isRequired,
    input: PropTypes.object.isRequired,
    maxValue: PropTypes.number.isRequired,
    minValue: PropTypes.number.isRequired,
    normalize: PropTypes.func.isRequired,
    decimals: PropTypes.number,
    snap: PropTypes.bool,
    ticks: PropTypes.array,
    width: PropTypes.number
}

export default class Slider extends React.Component {
    state = {
        width: null,
        ticks: [],
        minValue: null,
        maxValue: null
    }

    static getDerivedStateFromProps(props, state) {
        const mapTicks = (ticks) =>
            ticks.map((tick, index) => {
                if (_.isObject(tick)) {
                    const value = tick.value || index
                    const label = tick.label || value
                    return {value, label}
                }
                if (_.isString(tick)) {
                    return {value: index, externalValue: tick, label: tick}
                }
                return {value: tick, externalValue: tick, label: tick}
            })

        const ticks = mapTicks(props.ticks ? props.ticks : [props.minValue, props.maxValue])
        const minValue = props.minValue || ticks.reduce((min, {value}) => Math.min(min, value), null)
        const maxValue = props.maxValue || ticks.reduce((max, {value}) => Math.max(max, value), null)
        return {
            ticks: ticks
                .filter(({value}) => value >= minValue && value <= maxValue)
                .map(tick => ({
                    ...tick,
                    position: state.width * normalize(tick.value, minValue, maxValue, props.logScale)
                })),
            minValue,
            maxValue
        }
    }

    renderInfo() {
        const {input, info} = this.props
        const {ticks} = this.state
        const tick = ticks && ticks.find(tick => tick.value === input.value)
        const label = (tick && tick.label) || input.value
        return info ? (

                {_.isFunction(info) ? info(label) : info}

        ) : null
    }

    renderSlider() {
        const {width} = this.state
        return (

                    <ReactResizeDetector
                        handleWidth
                        onResize={width =>
                            this.setState(prevState => ({...prevState, width}))
                        }/>
                    {width ? this.renderContainer() : null}

        )
    }

    renderContainer() {
        const {input, logScale, decimals = 0, snap, range = 'left', info, disabled} = this.props
        const {ticks, minValue, maxValue, width} = this.state
        return (
            <SliderContainer
                input={input}
                minValue={minValue}
                maxValue={maxValue}
                decimals={decimals}
                ticks={ticks}
                snap={snap}
                range={range}
                logScale={logScale}
                info={info}
                width={width}
                disabled={disabled}/>
        )

    }

    renderDisabledOverlay() {
        const {disabled} = this.props
        return disabled ? (

        ) : null
    }

    renderLabel() {
        const {label, tooltip, tooltipPlacement = 'top'} = this.props
        return label ? (

        ) : null
    }

    render() {
        return (

                {this.renderLabel()}
                {this.renderInfo()}
                {this.renderSlider()}
                {this.renderDisabledOverlay()}

        )
    }
}

Slider.propTypes = {
    input: PropTypes.object.isRequired,
    decimals: PropTypes.number,
    disabled: PropTypes.any,
    info: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.func
    ]),
    label: PropTypes.string,
    logScale: PropTypes.any,
    maxValue: PropTypes.number,
    minValue: PropTypes.number,
    range: PropTypes.oneOf(['none', 'left', 'right']),
    snap: PropTypes.any,
    ticks: PropTypes.oneOfType([
        // PropTypes.number,
        PropTypes.array
    ]),
    tooltip: PropTypes.string,
    tooltipPlacement: PropTypes.string
}
<commit_msg>Refactored and improved Slider Rx.<commit_after>import {animationFrameScheduler, combineLatest, fromEvent, interval, merge} from 'rxjs'
import {distinctUntilChanged, filter, map, scan, switchMap, withLatestFrom} from 'rxjs/operators'
import Hammer from 'hammerjs'
import Label from 'widget/label'
import Portal from 'widget/portal'
import PropTypes from 'prop-types'
import React from 'react'
import ReactResizeDetector from 'react-resize-detector'
import ViewportResizeDetector from 'widget/viewportResizeDetector'
import _ from 'lodash'
import styles from './slider.module.css'

const clamp = (value, {min, max}) => Math.max(min, Math.min(max, value))

const lerp = (rate, speed = 1) => (value, target) => value + (target - value) * (rate * speed)

const normalizeLinear = (value, min, max) => {
    const offset = min
    return (value - offset) / (max - offset)
}

const denormalizeLinear = (value, min, max) => {
    const offset = min
    return value * (max - offset) + offset
}

const normalizeLog = (value, min, max) => {
    const offset = min - 1
    return Math.log(value - offset) / Math.log(max - offset)
}

const denormalizeLog = (value, min, max) => {
    const offset = min - 1
    return Math.exp(value * Math.log(max - offset)) + offset
}

const normalize = (value, min, max, logScale) =>
    logScale ? normalizeLog(value, min, max) : normalizeLinear(value, min, max)

const denormalize = (value, min, max, logScale) =>
    logScale ? denormalizeLog(value, min, max) : denormalizeLinear(value, min, max)

class SliderContainer extends React.Component {
    clickTarget = React.createRef()

    renderTick({position, value, label}) {
        const left = `${Math.trunc(position)}px`
        return (
            <React.Fragment key={value}>

{label}

            </React.Fragment>
        )
    }

    renderAxis() {
        const {ticks} = this.props
        return (

                {ticks.map(tick => this.renderTick(tick))}

        )
    }

    renderClickTarget() {
        return (

        )
    }

    renderDynamics() {
        const {input, width, range, decimals, ticks, snap, minValue, maxValue} = this.props
        return (
            <SliderDynamics
                input={input}
                minValue={minValue}
                maxValue={maxValue}
                width={width}
                decimals={decimals}
                ticks={ticks}
                snap={snap}
                range={range}
                clickTarget={this.clickTarget}
                normalize={value => {
                    const {logScale, minValue, maxValue} = this.props
                    return normalize(value, minValue, maxValue, logScale)
                }}
                denormalize={value => {
                    const {logScale, minValue, maxValue} = this.props
                    return denormalize(value, minValue, maxValue, logScale)
                }}
            />
        )
    }

    render() {
        return (

                {this.renderAxis()}
                {this.renderClickTarget()}
                {this.renderDynamics()}

        )
    }
}

SliderContainer.propTypes = {
    input: PropTypes.object.isRequired,
    decimals: PropTypes.number,
    denormalize: PropTypes.func,
    disabled: PropTypes.any,
    info: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.func
    ]),
    maxValue: PropTypes.number,
    minValue: PropTypes.number,
    normalize: PropTypes.func,
    snap: PropTypes.bool,
    ticks: PropTypes.oneOfType([
        PropTypes.number,
        PropTypes.array
    ]),
    width: PropTypes.number
}

class SliderDynamics extends React.Component {
    state = {
        position: null,
        previewPosition: null,
        clickTargetOffset: null,
        dragging: null,
        inhibitInput: null
    }
    handle = React.createRef()
    subscriptions = []

    renderLeftRange() {
        const {position} = this.state
        const {width} = this.props
        return <div className={styles.range} style={{right: `${width - position}px`}}/>
    }

    renderRightRange() {
        const {position} = this.state
        return <div className={styles.range} style={{left: `${position}px`}}/>
    }

    renderRanges() {
        const {range} = this.props
        return (
            <React.Fragment>
                {range === 'left' ? this.renderLeftRange() : null}
                {range === 'right' ? this.renderRightRange() : null}
            </React.Fragment>
        )
    }

    renderPreview() {
        const {previewPosition} = this.state
        return previewPosition !== null ? (
            <div className={[styles.cursor, styles.preview].join(' ')} ref={this.preview}
                style={{left: `${previewPosition}px`}}/>
        ) : null
    }

    renderHandle() {
        const {position, dragging} = this.state
        return (
            <div className={[styles.cursor, styles.handle, dragging ? styles.dragging : null].join(' ')}
                ref={this.handle}
                style={{left: `${position}px`}}/>
        )
    }

    renderOverlay() {
        const {dragging} = this.state
        return dragging
            ? (
                <Portal>

                </Portal>
            ) : null
    }

    renderViewportResizer() {
        return (
            <ViewportResizeDetector onChange={() => this.setClickTargetOffset()}/>
        )
    }

    render() {
        return (
            <React.Fragment>
                {this.renderRanges()}
                {this.renderPreview()}
                {this.renderHandle()}
                {this.renderOverlay()}
                {this.renderViewportResizer()}
            </React.Fragment>
        )
    }

    componentDidMount() {
        const {clickTarget} = this.props

        this.setHandlePositionByValue()
        const handle = new Hammer(this.handle.current, {
            threshold: 1
        })
        handle.get('pan').set({direction: Hammer.DIRECTION_HORIZONTAL})

        const click = new Hammer(clickTarget.current, {
            threshold: 1
        })

        const mouseMove$ = fromEvent(clickTarget.current, 'mousemove')
        const mouseLeave$ = fromEvent(clickTarget.current, 'mouseleave')
        const pan$ = fromEvent(handle, 'panstart panmove panend')
        const panStart$ = pan$.pipe(filter(e => e.type === 'panstart'))
        const panMove$ = pan$.pipe(filter(e => e.type === 'panmove'))
        const panEnd$ = pan$.pipe(filter(e => e.type === 'panend'))
        const animationFrame$ = interval(0, animationFrameScheduler)

        const hoverPosition$ = merge(
            mouseMove$.pipe(
                map(e => e.clientX - this.state.clickTargetOffset)
            ),
            mouseLeave$.pipe(
                map(() => null)
            )
        )

        // target position by clicking
        const clickPosition$ = fromEvent(click, 'tap').pipe(
            map(tap => this.snapPosition(tap.center.x - this.state.clickTargetOffset))
        )

        // target position by dragging
        const dragPosition$ = panStart$.pipe(
            switchMap(() => {
                const {position} = this.state
                return merge(
                    panMove$.pipe(
                        map(event => this.clampPosition(position + event.deltaX)),
                        distinctUntilChanged()
                    ),
                    panEnd$.pipe(
                        map(() => this.snapPosition(this.state.previewPosition))
                    )
                )
            })
        )

        const targetPosition$ = merge(clickPosition$, dragPosition$)

        const handlePosition$ = targetPosition$.pipe(
            switchMap(targetPosition => {
                const {position} = this.state
                return animationFrame$.pipe(
                    map(() => targetPosition),
                    scan(lerp(.1), position),
                    map(position => Math.round(position)),
                    distinctUntilChanged()
                )
            })
        )

        const handleMoving$ = combineLatest(handlePosition$, targetPosition$).pipe(
            map(([currentPosition, targetPosition]) => currentPosition !== targetPosition),
            distinctUntilChanged()
        )

        const handleDragging$ = merge(
            panStart$.pipe(map(() => true)),
            panEnd$.pipe(map(() => false)),
        )

        const previewPosition$ = merge(targetPosition$, hoverPosition$).pipe(
            map(position => position === null ? null : this.snapPosition(position)),
            distinctUntilChanged()
        )

        const inhibitInput$ = handleMoving$.pipe(
            withLatestFrom(handleDragging$),
            map(([dragging, moving]) => dragging || moving)
        )

        const updateInput$ = merge(clickPosition$, panEnd$)

        // animate slider
        this.subscriptions.push(
            handlePosition$.subscribe(position =>
                this.setHandlePosition(position)
            )
        )

        // enable preview on mouseover, disable on mouseleave
        this.subscriptions.push(
            previewPosition$.subscribe(previewPosition =>
                this.setPreviewPosition(previewPosition)
            )
        )

        // enable fullscreen pointer while dragging, disable when drag end
        this.subscriptions.push(
            handleDragging$.subscribe(dragging =>
                this.setDragging(dragging)
            )
        )

        // enable input when stopped, disabled when moving
        this.subscriptions.push(
            inhibitInput$.subscribe(inhibit => {
                this.setInhibitInput(inhibit)
            })
        )

        // update input on click or pan end
        this.subscriptions.push(
            updateInput$.subscribe(() =>
                this.updateInputValue()
            )
        )
    }

    componentWillUnmount() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe())
    }

    componentDidUpdate(prevProps) {
        const {inhibitInput} = this.state
        if (!inhibitInput && !_.isEqual(prevProps, this.props)) {
            this.setHandlePositionByValue()
        }
    }

    toPosition(value) {
        const {normalize, width} = this.props
        return normalize(value) * width
    }

    toValue(position) {
        const {denormalize, width} = this.props
        return denormalize(position / width)
    }

    clampPosition(position) {
        const {width} = this.props
        return Math.round(
            clamp(position, {
                min: 0,
                max: width
            })
        )
    }

    snapPosition(value) {
        const {ticks, snap} = this.props
        if (snap) {
            const closest = _(ticks)
                .map(({position}) => ({position, distance: Math.abs(position - value)}))
                .sortBy('distance')
                .head()
            return Math.round(closest.position)
        } else {
            return value
        }
    }

    setHandlePositionByValue() {
        const {input} = this.props
        this.setHandlePosition(this.toPosition(input.value))
    }

    setHandlePosition(position) {
        if (position >= 0) {
            position = Math.round(position)
            if (position !== this.state.position) {
                this.setState({position})
            }
        }
    }

    updateInputValue() {
        const {input, decimals} = this.props
        const {previewPosition} = this.state
        const value = this.toValue(this.snapPosition(previewPosition))
        const factor = Math.pow(10, decimals)
        const roundedValue = Math.round(value * factor) / factor
        input.set(roundedValue)
    }

    setPreviewPosition(previewPosition) {
        this.setState({previewPosition})
    }

    setInhibitInput(inhibitInput) {
        this.setState({inhibitInput})
    }

    setDragging(dragging) {
        this.setState({dragging})
    }

    setClickTargetOffset() {
        const {clickTarget} = this.props
        const clickTargetOffset = Math.trunc(clickTarget.current.getBoundingClientRect().left)
        if (this.state.clickTargetOffset !== clickTargetOffset) {
            this.setState({clickTargetOffset})
        }
    }

}

SliderDynamics.propTypes = {
    denormalize: PropTypes.func.isRequired,
    input: PropTypes.object.isRequired,
    maxValue: PropTypes.number.isRequired,
    minValue: PropTypes.number.isRequired,
    normalize: PropTypes.func.isRequired,
    decimals: PropTypes.number,
    snap: PropTypes.bool,
    ticks: PropTypes.array,
    width: PropTypes.number
}

export default class Slider extends React.Component {
    state = {
        width: null,
        ticks: [],
        minValue: null,
        maxValue: null
    }

    static getDerivedStateFromProps(props, state) {
        const mapTicks = (ticks) =>
            ticks.map((tick, index) => {
                if (_.isObject(tick)) {
                    const value = tick.value || index
                    const label = tick.label || value
                    return {value, label}
                }
                if (_.isString(tick)) {
                    return {value: index, externalValue: tick, label: tick}
                }
                return {value: tick, externalValue: tick, label: tick}
            })

        const ticks = mapTicks(props.ticks ? props.ticks : [props.minValue, props.maxValue])
        const minValue = props.minValue || ticks.reduce((min, {value}) => Math.min(min, value), null)
        const maxValue = props.maxValue || ticks.reduce((max, {value}) => Math.max(max, value), null)
        return {
            ticks: ticks
                .filter(({value}) => value >= minValue && value <= maxValue)
                .map(tick => ({
                    ...tick,
                    position: state.width * normalize(tick.value, minValue, maxValue, props.logScale)
                })),
            minValue,
            maxValue
        }
    }

    renderInfo() {
        const {input, info} = this.props
        const {ticks} = this.state
        const tick = ticks && ticks.find(tick => tick.value === input.value)
        const label = (tick && tick.label) || input.value
        return info ? (

                {_.isFunction(info) ? info(label) : info}

        ) : null
    }

    renderSlider() {
        const {width} = this.state
        return (

                    <ReactResizeDetector
                        handleWidth
                        onResize={width =>
                            this.setState({width})
                        }/>
                    {width ? this.renderContainer() : null}

        )
    }

    renderContainer() {
        const {input, logScale, decimals = 0, snap, range = 'left', info, disabled} = this.props
        const {ticks, minValue, maxValue, width} = this.state
        return (
            <SliderContainer
                input={input}
                minValue={minValue}
                maxValue={maxValue}
                decimals={decimals}
                ticks={ticks}
                snap={snap}
                range={range}
                logScale={logScale}
                info={info}
                width={width}
                disabled={disabled}/>
        )

    }

    renderDisabledOverlay() {
        const {disabled} = this.props
        return disabled ? (

        ) : null
    }

    renderLabel() {
        const {label, tooltip, tooltipPlacement = 'top'} = this.props
        return label ? (

        ) : null
    }

    render() {
        return (

                {this.renderLabel()}
                {this.renderInfo()}
                {this.renderSlider()}
                {this.renderDisabledOverlay()}

        )
    }
}

Slider.propTypes = {
    input: PropTypes.object.isRequired,
    decimals: PropTypes.number,
    disabled: PropTypes.any,
    info: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.func
    ]),
    label: PropTypes.string,
    logScale: PropTypes.any,
    maxValue: PropTypes.number,
    minValue: PropTypes.number,
    range: PropTypes.oneOf(['none', 'left', 'right']),
    snap: PropTypes.any,
    ticks: PropTypes.oneOfType([
        // PropTypes.number,
        PropTypes.array
    ]),
    tooltip: PropTypes.string,
    tooltipPlacement: PropTypes.string
}
<|endoftext|>


<commit_before>var pk = pk || {};
/**
Attach custom scrollbars to an element

HTML:

		...long content....

Javascript:

	pk.scroll({
		element: document.getElementById('scroll'),
		axis: 'xy'
	});

@class pk.scroll
@constructor
@param options {Object}
@param options.element {Object} DOM element to attach drag handlers to
@param [options.sensitivity=60] {Number} Number in px to scroll incrementally on `mousewheel` and `key` events. Can also be expressed as percentage, e.g. `10%` of content
@param [options.axis=y] {Object} Object consisting of `x` and `y` {Boolean} values denoting scrollable axis, DOM element to attach drag handlers to. Defaults to element attribute `pk-scroll` or `y`
@return Object {Object} Consisting of original DOM element (item `0`)
@chainable
*/
(function(pk) {
    // HELPERS FOR jQUERY+ANGULAR
    if (typeof jQuery === 'object') {
        // jquery available
        jQuery.fn.extend({
            pkScroll: function(axis) {
                pk.scroll({
                    element: this[0],
                    axis: axis
                });
            }
        });
    }
    if (typeof angular === 'object') {
        // angular available
        (

            function() {
                angular.module('pk-scroll', ['ng'])
                    .directive('pkScroll', function() {
                        return {
                            restrict: 'A',
                            link: function(scope, el) {
                                pk.scroll({
                                    element: el[0],
                                    axis: el[0].getAttribute('pk-scroll')
                                });
                            }
                        };
                    });
            })();
    }
    pk.scroll = function(opt) {

        var el = opt.element,
			sensitivity=60;
        // INIT SCROLL STRUCTURE

        var tpl = "
\
            <" + el.nodeName + " class='pk-scroll-content'>\
                " + el.innerHTML + "\
            </" + el.nodeName + ">\

\

\

\

\

\

\

";
        el.innerHTML = '';
        el = pk.replaceEl(el, tpl);
        var container = el.children[0],
            trackY = el.children[1],
            floatY = trackY.children[0],
            trackX = el.children[2],
            floatX = trackX.children[0];
        // INIT VARIABLES
        var
            floatYh = 0,
            floatXw = 0,
            allowY = false,
            allowX = false,
            percY = 0,
            percX = 0,
            contentH = 0,
            contentW = 0,
            containerH = 0,
            containerW = 0,
            contentWidth = 0,
            contentHeight = 0,
            containerWidth = 0,
            containerHeight = 0,
            scrollDir = opt.axis ? opt.axis.toLowerCase() : (pk.attribute(el, 'pk-scroll') ? pk.attribute(el, 'pk-scroll') : "y");
        if (pk.getStyle(el, 'position') === "static") {
            el.style.position = "relative";
        }

        pk.bindEvent("scroll", container, function() {
            percY = container.scrollTop / (contentH - containerH);
            percX = container.scrollLeft / (contentW - containerW);
            percY = percY < 0 ? 0 : percY > 1 ? 1 : percY;
            percX = percX < 0 ? 0 : percX > 1 ? 1 : percX;
            floatY.style.top = (containerH - floatYh) * percY + 'px';
            floatX.style.left = (containerW - floatXw) * percX + 'px';
        });

        function resolveDimensions() {
            contentH = container.scrollHeight;
            contentW = container.scrollWidth;
            containerH = el.offsetHeight;
            containerW = el.offsetWidth;
            if (scrollDir.indexOf("y") > -1 && contentH > containerH) {
                allowY = true;
                pk.addClass(el, 'pk-scroll-enableY');
                floatYh = floatY.offsetHeight;
                container.scrollTop = (contentH - containerH) * percY;
            } else {
                allowY = false;
                pk.removeClass(el, 'pk-scroll-enableY');
                container.scrollTop = 0;
            }
            if (scrollDir.indexOf("x") > -1 && contentW > containerW) {
                allowX = true;
                pk.addClass(el, 'pk-scroll-enableX');
                floatXw = floatX.offsetWidth;
                container.scrollLeft = (contentW - containerW) * percX;
            } else {
                allowX = false;
                pk.removeClass(el, 'pk-scroll-enableX');
                container.scrollLeft = 0;
            }
        }
        resolveDimensions();

        setInterval(function() {
            var widthContainer = el.offsetWidth,
                heightContainer = el.offsetHeight,
                widthContent = container.scrollWidth,
                heightContent = container.scrollHeight;
            if (widthContainer !== containerWidth || heightContainer !== containerHeight || widthContent !== contentWidth || heightContent !== contentHeight) {
                contentWidth = widthContent;
                contentHeight = heightContent;
                containerWidth = widthContainer;
                containerHeight = heightContainer;
                resolveDimensions();
            }
        }, 500);

        // DRAG HANDLERS

        if (allowY) {
            pk.drag({
                element: floatY,
                move: {
                    y: true
                },
                container: {
                    element: trackY
                },
                listeners: {
                    dragging: function() {
                        container.scrollTop = (contentH - containerH) * (floatY.offsetTop / (trackY.offsetHeight - floatY.offsetHeight));
                    }
                }
            });
        }
        if (allowX) {
            pk.drag({
                element: floatX,
                move: {
                    x: true
                },
                container: {
                    element: trackX
                },
                listeners: {
                    dragging: function() {

                        container.scrollLeft = (contentW - containerW) * (floatX.offsetLeft / (trackX.offsetWidth - floatX.offsetWidth));
                    }
                }
            });
        }
        pk.bindEvent("click", floatY, function(e) {
            pk.preventBubble(e);
        });
        pk.bindEvent("click", floatX, function(e) {
            pk.preventBubble(e);
        });

        // TRACK CLICKING HANDLERS
        pk.bindEvent("click", trackY, function(e) {
            container.scrollTop = (pk.getEventOffset(e).y / containerH * (contentH - containerH));
        });
        pk.bindEvent("click", trackX, function(e) {
            container.scrollLeft = (pk.getEventOffset(e).x / containerW * (contentW - containerW));
        });

        // MOUSE WHEEL HANDLERS
        function mouseScroll(e) {
            if (e.wheelDelta > 0 || e.detail < 0) {
				if (allowY) {
					container.scrollTop -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
				} else {
					container.scrollLeft -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
				}
            }else if (allowY) {
					container.scrollTop += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
			} else {
				container.scrollLeft += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
			}
            /* Stop wheel propogation (prevent parent scrolling) */
            pk.preventBubble(e);
        }

        pk.bindEvent("mousewheel", container, mouseScroll);

        // TOUCH EVENT HANDLERS

        function getXy(e) {
            // touch event
            if (e.targetTouches && (e.targetTouches.length >= 1)) {
                return {
                    x: e.targetTouches[0].clientX,
                    y: e.targetTouches[0].clientY
                };
            }
            // mouse event
            return {
                x: e.clientX,
                y: e.clientY
            };
        }

        var pressed = false,
            startPos = {};

        function tap(e) {
            pressed = true;
            startPos = getXy(e);
            e.preventDefault();
            e.stopPropagation();
            return false;
        }

        function release(e) {
            pressed = false;
            e.preventDefault();
            e.stopPropagation();
            return false;
        }

        function drag(e) {
            var endPos, deltaX, deltaY;
            if (pressed) {
                endPos = getXy(e);
                deltaY = startPos.y - endPos.y;
                deltaX = startPos.x - endPos.x;
                if (deltaY > 2 || deltaY < -2) {
                    startPos.y = endPos.y;
                    container.scrollTop += deltaY;

                }
                if (deltaX > 2 || deltaX < -2) {
                    startPos.x = endPos.x;
                    container.scrollLeft += deltaX;
                }
            }
            e.preventDefault();
            e.stopPropagation();
            return false;
        }
        if (typeof window.ontouchstart !== 'undefined') {
            pk.bindEvent('touchstart', container[0], tap);
            pk.bindEvent('touchmove', container[0], drag);
            pk.bindEvent('touchend', window, release);
        }

        // KEYBOARD HANDLERS
        container.setAttribute("tabindex", 0);
        pk.bindEvent('keydown', container, function(e) {
            if (document.activeElement !== container) {
                return;
            }
            if (allowY) {
                switch (e.keyCode) {
                    case 38: //up cursor
                        container.scrollTop -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                    case 40: //down cursor
                    case 32: //spacebar
                        container.scrollTop += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                    case 33: //page up
                        container.scrollTop -= containerH;
                        pk.preventBubble(e);
                        break;
                    case 34: //page down
                        container.scrollTop += containerH;
                        pk.preventBubble(e);
                        break;
                    case 36: //home
                        container.scrollTop = 0;
                        pk.preventBubble(e);
                        break;
                    case 35: //end
                        container.scrollTop = contentH;
                        pk.preventBubble(e);
                        break;
                }
            }
            if (allowX) {
                switch (e.keyCode) {
                    case 37: //left cursor
                        container.scrollLeft -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                    case 39: //right cursor
                        container.scrollLeft += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                }
            }
            return {
                0: el
            };

        });
    };
    return pk;
})(pk);
<commit_msg>Update scroll.js<commit_after>var pk = pk || {};
/**
Attach custom scrollbars to an element

HTML:

		...long content....

Javascript:

	pk.scroll({
		element: document.getElementById('scroll'),
		axis: 'xy'
	});

@class pk.scroll
@constructor
@param options {Object}
@param options.element {Object} DOM element to attach drag handlers to
@param [options.sensitivity=60] {Number} Number in px to scroll incrementally on `mousewheel` and `key` events. Can also be expressed as percentage, e.g. `10%` of content
@param [options.axis=y] {Object} Object consisting of `x` and `y` {Boolean} values denoting scrollable axis, DOM element to attach drag handlers to. Defaults to element attribute `pk-scroll` or `y`
@return Object {Object} Consisting of original DOM element (item `0`)
@chainable
*/
(function(pk) {
    // HELPERS FOR jQUERY+ANGULAR
    if (typeof jQuery === 'object') {
        // jquery available
        jQuery.fn.extend({
            pkScroll: function(axis) {
                pk.scroll({
                    element: this[0],
                    axis: axis
                });
            }
        });
    }
    if (typeof angular === 'object') {
        // angular available
        (

            function() {
                angular.module('pk-scroll', ['ng'])
                    .directive('pkScroll', function() {
                        return {
                            restrict: 'A',
                            link: function(scope, el) {
                                pk.scroll({
                                    element: el[0],
                                    axis: el[0].getAttribute('pk-scroll')
                                });
                            }
                        };
                    });
            })();
    }
    pk.scroll = function(opt) {

        var el = opt.element,
			sensitivity=60,
			floatYh = 0,
            floatXw = 0,
            allowY = false,
            allowX = false,
            percY = 0,
            percX = 0,
            contentH = 0,
            contentW = 0,
            containerH = 0,
            containerW = 0,
            contentWidth = 0,
            contentHeight = 0,
            containerWidth = 0,
            containerHeight = 0,
            scrollDir = opt.axis ? opt.axis.toLowerCase() : (pk.attribute(el, 'pk-scroll') ? pk.attribute(el, 'pk-scroll') : "y"),
			tpl = "
\
            <" + el.nodeName + " class='pk-scroll-content'>\
                " + el.innerHTML + "\
            </" + el.nodeName + ">\

\

\

\

\

\

\

";
		el.innerHTML='';
        el = pk.replaceEl(el, tpl);
        var container = el.children[0],
            trackY = el.children[1],
            floatY = trackY.children[0],
            trackX = el.children[2],
            floatX = trackX.children[0];

        if (pk.getStyle(el, 'position') === "static") {
            el.style.position = "relative";
        }

        pk.bindEvent("scroll", container, function() {
            percY = container.scrollTop / (contentH - containerH);
            percX = container.scrollLeft / (contentW - containerW);
            percY = percY < 0 ? 0 : percY > 1 ? 1 : percY;
            percX = percX < 0 ? 0 : percX > 1 ? 1 : percX;
            floatY.style.top = (containerH - floatYh) * percY + 'px';
            floatX.style.left = (containerW - floatXw) * percX + 'px';
        });

        function resolveDimensions() {
            contentH = container.scrollHeight;
            contentW = container.scrollWidth;
            containerH = el.offsetHeight;
            containerW = el.offsetWidth;
            if (scrollDir.indexOf("y") > -1 && contentH > containerH) {
                allowY = true;
                pk.addClass(el, 'pk-scroll-enableY');
                floatYh = floatY.offsetHeight;
                container.scrollTop = (contentH - containerH) * percY;
            } else {
                allowY = false;
                pk.removeClass(el, 'pk-scroll-enableY');
                container.scrollTop = 0;
            }
            if (scrollDir.indexOf("x") > -1 && contentW > containerW) {
                allowX = true;
                pk.addClass(el, 'pk-scroll-enableX');
                floatXw = floatX.offsetWidth;
                container.scrollLeft = (contentW - containerW) * percX;
            } else {
                allowX = false;
                pk.removeClass(el, 'pk-scroll-enableX');
                container.scrollLeft = 0;
            }
        }
        resolveDimensions();

        setInterval(function() {
            var widthContainer = el.offsetWidth,
                heightContainer = el.offsetHeight,
                widthContent = container.scrollWidth,
                heightContent = container.scrollHeight;
            if (widthContainer !== containerWidth || heightContainer !== containerHeight || widthContent !== contentWidth || heightContent !== contentHeight) {
                contentWidth = widthContent;
                contentHeight = heightContent;
                containerWidth = widthContainer;
                containerHeight = heightContainer;
                resolveDimensions();
            }
        }, 100);

        // DRAG HANDLERS

        if (allowY) {
            pk.drag({
                element: floatY,
                move: {
                    y: true
                },
                container: {
                    element: trackY
                },
                listeners: {
                    dragging: function() {
                        container.scrollTop = (contentH - containerH) * (floatY.offsetTop / (trackY.offsetHeight - floatY.offsetHeight));
                    }
                }
            });
        }
        if (allowX) {
            pk.drag({
                element: floatX,
                move: {
                    x: true
                },
                container: {
                    element: trackX
                },
                listeners: {
                    dragging: function() {

                        container.scrollLeft = (contentW - containerW) * (floatX.offsetLeft / (trackX.offsetWidth - floatX.offsetWidth));
                    }
                }
            });
        }
        pk.bindEvent("click", floatY, function(e) {
            pk.preventBubble(e);
        });
        pk.bindEvent("click", floatX, function(e) {
            pk.preventBubble(e);
        });

        // TRACK CLICKING HANDLERS
        pk.bindEvent("click", trackY, function(e) {
            container.scrollTop = (pk.getEventOffset(e).y / containerH * (contentH - containerH));
        });
        pk.bindEvent("click", trackX, function(e) {
            container.scrollLeft = (pk.getEventOffset(e).x / containerW * (contentW - containerW));
        });

        // MOUSE WHEEL HANDLERS
        function mouseScroll(e) {
            if (e.wheelDelta > 0 || e.detail < 0) {
				if (allowY) {
					container.scrollTop -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
				} else {
					container.scrollLeft -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
				}
            }else if (allowY) {
					container.scrollTop += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
			} else {
				container.scrollLeft += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
			}
            /* Stop wheel propogation (prevent parent scrolling) */
            pk.preventBubble(e);
        }

        pk.bindEvent("mousewheel", container, mouseScroll);

        // TOUCH EVENT HANDLERS

        function getXy(e) {
            // touch event
            if (e.targetTouches && (e.targetTouches.length >= 1)) {
                return {
                    x: e.targetTouches[0].clientX,
                    y: e.targetTouches[0].clientY
                };
            }
            // mouse event
            return {
                x: e.clientX,
                y: e.clientY
            };
        }

        var pressed = false,
            startPos = {};

        function tap(e) {
            pressed = true;
            startPos = getXy(e);
            e.preventDefault();
            e.stopPropagation();
            return false;
        }

        function release(e) {
            pressed = false;
            e.preventDefault();
            e.stopPropagation();
            return false;
        }

        function drag(e) {
            var endPos, deltaX, deltaY;
            if (pressed) {
                endPos = getXy(e);
                deltaY = startPos.y - endPos.y;
                deltaX = startPos.x - endPos.x;
                if (deltaY > 2 || deltaY < -2) {
                    startPos.y = endPos.y;
                    container.scrollTop += deltaY;

                }
                if (deltaX > 2 || deltaX < -2) {
                    startPos.x = endPos.x;
                    container.scrollLeft += deltaX;
                }
            }
            e.preventDefault();
            e.stopPropagation();
            return false;
        }
        if (typeof window.ontouchstart !== 'undefined') {
            pk.bindEvent('touchstart', container[0], tap);
            pk.bindEvent('touchmove', container[0], drag);
            pk.bindEvent('touchend', window, release);
        }

        // KEYBOARD HANDLERS
        container.setAttribute("tabindex", 0);
        pk.bindEvent('keydown', container, function(e) {
            if (document.activeElement !== container) {
                return;
            }
            if (allowY) {
                switch (e.keyCode) {
                    case 38: //up cursor
                        container.scrollTop -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                    case 40: //down cursor
                    case 32: //spacebar
                        container.scrollTop += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerH * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                    case 33: //page up
                        container.scrollTop -= containerH;
                        pk.preventBubble(e);
                        break;
                    case 34: //page down
                        container.scrollTop += containerH;
                        pk.preventBubble(e);
                        break;
                    case 36: //home
                        container.scrollTop = 0;
                        pk.preventBubble(e);
                        break;
                    case 35: //end
                        container.scrollTop = contentH;
                        pk.preventBubble(e);
                        break;
                }
            }
            if (allowX) {
                switch (e.keyCode) {
                    case 37: //left cursor
                        container.scrollLeft -= typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                    case 39: //right cursor
                        container.scrollLeft += typeof sensitivity === 'string' && sensitivity.indexOf('%') ?  containerW * parseInt(sensitivity.replace('%',''),0)/100 : sensitivity;
                        pk.preventBubble(e);
                        break;
                }
            }
            return {
                0: el
            };

        });
    };
    return pk;
})(pk);
<|endoftext|>


<commit_before>import { stdout, stderr} from "process"

import { join } from "path"
import { inspect } from "util"

import { Writable } from "stream"

import { createLogger, WARN } from "bunyan"
import { environment, isDevelopment, isTest } from "./../share/environment"

const logPath = join(process.cwd(), "log", environment)

let logger = null

export default function() {
  if (logger === null) {
    logger = createInternalLogger()
  }

  return logger
}

function createInternalLogger() {
  let streams = createStreams()

  let logger = createLogger({
    name: environment,
    streams: streams
  })

  logger.exception = (err) => {
    if (isDevelopment()) {
      stderr.write(`EXCEPTION: ${err.stack}`)
    } else if (isTest() && err.message) {
      throw err
    }
  }

  process.on("uncaughtException", logger.exception)

  return logger
}

function createStreams() {
  let streams = [{ path: logPath, level: "debug" }]

  const developmentLogger = {
    type: "raw",
    level: "debug",
    stream: new Writable({
      objectMode: true,
      write: (obj, _, cb) => {
        const output = obj.level < WARN ? stdout : stderr
        const message = obj.msg.replace(/\n/g, "").replace(/\s+/g, " ")

        output.write(`${obj.time.toISOString()}: ${message}\n`)

        cb()
      }
    })
  }

  if (isDevelopment()) {
    streams.push(developmentLogger)
  }

  return streams
}
<commit_msg>remove unused import<commit_after>import { stdout, stderr} from "process"

import { join } from "path"
import { Writable } from "stream"

import { createLogger, WARN } from "bunyan"
import { environment, isDevelopment, isTest } from "./../share/environment"

const logPath = join(process.cwd(), "log", environment)

let logger = null

export default function() {
  if (logger === null) {
    logger = createInternalLogger()
  }

  return logger
}

function createInternalLogger() {
  let streams = createStreams()

  let logger = createLogger({
    name: environment,
    streams: streams
  })

  logger.exception = (err) => {
    if (isDevelopment()) {
      stderr.write(`EXCEPTION: ${err.stack}`)
    } else if (isTest() && err.message) {
      throw err
    }
  }

  process.on("uncaughtException", logger.exception)

  return logger
}

function createStreams() {
  let streams = [{ path: logPath, level: "debug" }]

  const developmentLogger = {
    type: "raw",
    level: "debug",
    stream: new Writable({
      objectMode: true,
      write: (obj, _, cb) => {
        const output = obj.level < WARN ? stdout : stderr
        const message = obj.msg.replace(/\n/g, "").replace(/\s+/g, " ")

        output.write(`${obj.time.toISOString()}: ${message}\n`)

        cb()
      }
    })
  }

  if (isDevelopment()) {
    streams.push(developmentLogger)
  }

  return streams
}
<|endoftext|>


<commit_before>const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const fetch = require('node-fetch');
const PeriodicTask = require('periodic-task');

const app = express();
const db = new sqlite3.Database('canaritus.db');
db.run('CREATE TABLE IF NOT EXISTS ids (id TEXT, UNIQUE(id))');
db.run('CREATE TABLE IF NOT EXISTS events (title TEXT, body TEXT, time DATETIME)');

const PORT = 3000;
const SERVER_KEY = process.env.SERVER_KEY;
const GCM_ENDPOINT = 'https://android.googleapis.com/gcm/send';
const HOST_URL = process.env.HOST_URL;
const HOST_CODE = process.env.HOST_CODE || 200;

var IS_HEALTHY = true; // eslint-disable-line no-var,vars-on-top

if (!SERVER_KEY) {
  console.log('SERVER_KEY env variable not set, will not be able to send push notification.');
}
if (!HOST_URL) {
  console.log('HOST_URL env variable not set, will not do health checks.');
}

const pingClients = () => {
  console.log('Pinging clients...');
  db.all('SELECT * FROM ids', (err, rows) => {
    if (err !== null) {
      console.log('Failed to select registration ids');
    } else {
      const ids = rows.map(x => x.id);
      fetch(GCM_ENDPOINT, {
        method: 'post',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          'Authorization': `key=${SERVER_KEY}`,
        },
        body: JSON.stringify({
          'registration_ids': ids,
        }),
      }).then((res) => {
        console.log('Notification pings sent, status:', res.status);
      });
    }
  });
};

const addEvent = (title, body) => {
  console.log(`Adding event "${title}: ${body}"`);
  db.run(`INSERT INTO events (title, body, time) VALUES('${title}', '${body}', datetime('now'))`, (err) => {
    if (err !== null) {
      console.log('Error adding event', err);
      return false;
    }
    pingClients();
    return true;
  });
};

const healthCheck = (url, status) => {
  console.log('Doing health check on ', url);
  return fetch(url).then((res) => {
    return res.status === status;
  });
};

app.use(bodyParser.json());
app.use(express.static(__dirname + '/../../dist'));

app.post('/subscribe', (req, res) => {
  const id = req.body.id;
  if (!id) {
    res.status(400).send('id is missing from the post');
  } else {
    console.log(`Adding ${id} to ids table`);
    db.run(`INSERT OR IGNORE INTO ids (id) VALUES('${id}')`, (err) => {
      if (err !== null) {
        console.log('Error adding id', err);
        res.sendStatus(500);
      } else {
        res.sendStatus(201);
      }
    });
  }
});

app.post('/unsubscribe', (req, res) => {
  const id = req.body.id;
  if (!id) {
    res.status(400).send('id is missing from the post');
  } else {
    console.log(`Removing ${id} from ids table`);
    db.run(`DELETE FROM ids WHERE id='${id}'`, (err) => {
      if (err !== null) {
        console.log('Error removing id', err);
        res.sendStatus(500);
      } else {
        res.sendStatus(204);
      }
    });
  }
});

app.post('/event', (req, res) => {
  const title = req.body.title;
  const body = req.body.body;
  if (!title || !body) {
    res.status(400).send('title and/or body missing from the post');
  } else {
    const success = addEvent(title, body);
    if (success) {
      res.sendStatus(201);
    } else {
      res.sendStatus(500);
    }
  }
});

app.get('/event', (req, res) => {
  db.get('SELECT * FROM events ORDER BY time DESC LIMIT 1', (err, event) => {
    if (err !== null) {
      res.status(500).send('Failed to get latest event');
    } else {
      res.status(200).json({
        title: event.title,
        body: event.body,
      });
    }
  });
});

/*
 * Periodic Tasks
 */

if (HOST_URL) {
  const healthTask = new PeriodicTask(60000, () => {
    healthCheck(HOST_URL, HOST_CODE).then((healthy) => {
      if (!healthy && IS_HEALTHY) {
        addEvent('Health degraded', 'Health check just failed, the host didn\'t return expected status code.');
        IS_HEALTHY = false;
      } else if (healthy && !IS_HEALTHY) {
        addEvent('Health recovered', 'Health check was just successful, crisis averted.');
        IS_HEALTHY = true;
      }
    }).catch((err) => {
      console.log('Error doing a healthcheck:', err);
      if (IS_HEALTHY) {
        addEvent('Health degraded', 'Health check just failed, failed to reach host.');
        IS_HEALTHY = false;
      }
    });
  });
  healthTask.run();
}

/*
 * Start server
 */
app.listen(PORT);
console.log(`Listening on port ${PORT}`);
<commit_msg>Enhance logging a little bit in server<commit_after>const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const fetch = require('node-fetch');
const PeriodicTask = require('periodic-task');

const app = express();
const db = new sqlite3.Database('canaritus.db');
db.run('CREATE TABLE IF NOT EXISTS ids (id TEXT, UNIQUE(id))');
db.run('CREATE TABLE IF NOT EXISTS events (title TEXT, body TEXT, time DATETIME)');

const PORT = 3000;
const SERVER_KEY = process.env.SERVER_KEY;
const GCM_ENDPOINT = 'https://android.googleapis.com/gcm/send';
const HOST_URL = process.env.HOST_URL;
const HOST_CODE = process.env.HOST_CODE || 200;

var IS_HEALTHY = true; // eslint-disable-line no-var,vars-on-top

if (!SERVER_KEY) {
  console.log('SERVER_KEY env variable not set, will not be able to send push notification.');
}
if (!HOST_URL) {
  console.log('HOST_URL env variable not set, will not do health checks.');
}

const timeStamp = () => {
  const now = new Date();
  const twoNum = (num) => {
    return num < 10 ? '0' + num : num.toString();
  };
  const date = `${now.getFullYear()}-${twoNum(now.getMonth())}-${twoNum(now.getDate())}`;
  const time = `${twoNum(now.getHours())}:${twoNum(now.getMinutes())}:${twoNum(now.getSeconds())}`;
  return `${date} ${time}`;
};

const log = (type, string) => {
  console.log(`${timeStamp()} - ${type} - ${string}`);
};

const pingClients = () => {
  log('PUSH', 'Pinging all clients');
  db.all('SELECT * FROM ids', (err, rows) => {
    if (err !== null) {
      log('PUSH', 'Failed to select registration ids');
    } else {
      const ids = rows.map(x => x.id);
      fetch(GCM_ENDPOINT, {
        method: 'post',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          'Authorization': `key=${SERVER_KEY}`,
        },
        body: JSON.stringify({
          'registration_ids': ids,
        }),
      }).then((res) => {
        log('PUSH', 'Notification pings sent, status:', res.status);
      });
    }
  });
};

const addEvent = (title, body) => {
  log('EVENT', `Adding event "${title}: ${body}"`);
  db.run(`INSERT INTO events (title, body, time) VALUES('${title}', '${body}', datetime('now'))`, (err) => {
    if (err !== null) {
      log('EVENT', 'Error adding event', err);
      return false;
    }
    pingClients();
    return true;
  });
};

const healthCheck = (url, status) => {
  log('UPTIME', 'Health check: ' + url);
  return fetch(url).then((res) => {
    return res.status === status;
  });
};

app.use(bodyParser.json());
app.use(express.static(__dirname + '/../../dist'));

app.post('/subscribe', (req, res) => {
  const id = req.body.id;
  if (!id) {
    res.status(400).send('id is missing from the post');
  } else {
    log('DB', `Adding ${id} to ids table`);
    db.run(`INSERT OR IGNORE INTO ids (id) VALUES('${id}')`, (err) => {
      if (err !== null) {
        log('DB', 'Error adding id', err);
        res.sendStatus(500);
      } else {
        res.sendStatus(201);
      }
    });
  }
});

app.post('/unsubscribe', (req, res) => {
  const id = req.body.id;
  if (!id) {
    res.status(400).send('id is missing from the post');
  } else {
    log('DB', `Removing ${id} from ids table`);
    db.run(`DELETE FROM ids WHERE id='${id}'`, (err) => {
      if (err !== null) {
        log('DB', 'Error removing id', err);
        res.sendStatus(500);
      } else {
        res.sendStatus(204);
      }
    });
  }
});

app.post('/event', (req, res) => {
  const title = req.body.title;
  const body = req.body.body;
  if (!title || !body) {
    res.status(400).send('title and/or body missing from the post');
  } else {
    const success = addEvent(title, body);
    if (success) {
      res.sendStatus(201);
    } else {
      res.sendStatus(500);
    }
  }
});

app.get('/event', (req, res) => {
  db.get('SELECT * FROM events ORDER BY time DESC LIMIT 1', (err, event) => {
    if (err !== null) {
      res.status(500).send('Failed to get latest event');
    } else {
      res.status(200).json({
        title: event.title,
        body: event.body,
      });
    }
  });
});

/*
 * Periodic Tasks
 */

if (HOST_URL) {
  log('UPTIME', `Creating periodic task for ${HOST_URL}`);
  const healthTask = new PeriodicTask(60000, () => {
    healthCheck(HOST_URL, HOST_CODE).then((healthy) => {
      if (!healthy && IS_HEALTHY) {
        addEvent('Health degraded', 'Health check just failed, the host didn\'t return expected status code.');
        IS_HEALTHY = false;
      } else if (healthy && !IS_HEALTHY) {
        addEvent('Health recovered', 'Health check was just successful, crisis averted.');
        IS_HEALTHY = true;
      }
    }).catch((err) => {
      log('UPTIME', 'Error doing a healthcheck:', err);
      if (IS_HEALTHY) {
        addEvent('Health degraded', 'Health check just failed, failed to reach host.');
        IS_HEALTHY = false;
      }
    });
  });
  healthTask.run();
}

/*
 * Start server
 */
app.listen(PORT);
log('SERVER', `Listening on port ${PORT}`);
<|endoftext|>


<commit_before>import { expect } from 'chai';
import '../../../config/chai/chai.config';

import Immutable from 'immutable';
import * as PSActionFactory from './PermissionsSummaryActionFactory';
import reducer from './PermissionsSummaryReducer';
import { getRolePermissions, getUserPermissions } from './PermissionsSummaryHelpers';

const allUsersById = {
  'auth0|foo': {
    email: 'foo',
    nickname: 'bar',
    organization: [],
    roles: ['foo', 'AuthenticatedUser'],
    user_id: 'auth0|foo',
    username: 'baz'
  }
};

const property = {
  title: 'title'
};

const data = {
  authenticatedUserPermissions: [],
  roleAcls: {
    Discover: [],
    Link: ['foo'],
    Read: [],
    Write: []
  },
  userAcls: {
    Discover: [],
    Link: [],
    Read: [],
    Write: ['auth0|foo'],
    Ownder: []
  }
};

describe('PermissionsSummaryReducer', function() {
  let INITIAL_STATE;

  beforeEach(function() {
    INITIAL_STATE = reducer(undefined, { type: 'init' });
  });

  describe('Load users and roles', function() {
    it('should update isGettingUsersRoles to true', function() {
      const entitySet = {};
      const action = PSActionFactory.getAllUsersAndRolesRequest(entitySet);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingUsersRoles').equal(true);
    });

    it('should update isGettingUsersRoles to false (success case)', function() {
      const users = {};
      const roles = [];
      const action = PSActionFactory.getAllUsersAndRolesSuccess(users, roles);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingUsersRoles').equal(false);
    });

    it('should update allUsersById', function() {
      const users = Immutable.fromJS([{test: 'user1'}, {test: 'user2'}]);
      const roles = Immutable.fromJS([{test: 'role1'}, {test: 'role2'}]);
      const action = PSActionFactory.getAllUsersAndRolesSuccess(users, roles);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('allUsersById').equal(users);
    });

    it('should update allRolesList', function() {
      const users = Immutable.fromJS([{test: 'user1'}, {test: 'user2'}]);
      const roles = Immutable.fromJS([{test: 'role1'}, {test: 'role2'}]);
      const action = PSActionFactory.getAllUsersAndRolesSuccess(users, roles);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('allRolesList').equal(roles);
    });

    it('should update isGettingUsersRoles to false (failure case)', function() {
      const action = PSActionFactory.getAllUsersAndRolesFailure();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingUsersRoles').equal(false);
    });
  });

  describe('Load permissions', function() {
    it('should update isGettingAcls to true', function() {
      const entitySet = {};
      const action = PSActionFactory.getAcls(entitySet);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingAcls').equal(true);
    });

    it('should update isGettingAcls to false', function() {
      const entitySetId = '';
      const property = {};
      const action = PSActionFactory.getUserRolePermissionsRequest(entitySetId, property);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingAcls').equal(false);
    });

    it('should update isGettingPermissions to true', function() {
      const entitySetId = '';
      const property = {};
      const action = PSActionFactory.getUserRolePermissionsRequest(entitySetId, property);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingPermissions').equal(true);
    });

    it('should update isGettingPermissions to false (success case)', function() {
      const action = PSActionFactory.getUserRolePermissionsSuccess();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingPermissions').equal(false);
    });

    it('should update isGettingPermissions to false (failure case)', function() {
      const action = PSActionFactory.getUserRolePermissionsFailure();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingPermissions').equal(false);
    });

    it('should get role permissions', function() {
      const action = PSActionFactory.setRolePermissions(property, data);
      const permissions = getRolePermissions(action);
      const result = {
        AuthenticatedUser: ['None'],
        foo: ['Link']
      };

      expect(permissions).to.eql(result);
    });

    it('should update property role permissions', function() {
      const action = PSActionFactory.setRolePermissions(property, data);
      const permissions = Immutable.fromJS(getRolePermissions(action));
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.deep.property(`propertyPermissions.${property.title}.rolePermissions`).eql(permissions);
    });

    it('should update entity role permissions', function() {
      const action = PSActionFactory.setRolePermissions(undefined, data);
      const permissions = Immutable.fromJS(getRolePermissions(action));
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('entityRolePermissions').eql(permissions);
    });

  // TODO: Fix errors in user permissions tests
    it('should get user permissions', function() {
      const action = PSActionFactory.setUserPermissions(property, data);
      const permissions = getUserPermissions(action, Immutable.fromJS(allUsersById));
      const result = [
        {
          email: 'foo',
          id: 'auth0|foo',
          individualPermissions: ['Write'],
          nickname: 'bar',
          permissions: ['Write', 'Link'],
          roles: ['foo']
        }
      ];

      // TODO: Reformat to accept different order in array e.g. permissions
      expect(permissions).to.eql(result);
    });
    //
    // it('should update property user permissions', function() {
    //   const allUsersById = {
    //     'auth0|foo': {
    //       email: 'foo',
    //       nickname: 'bar',
    //       organization: [],
    //       roles: ['foo'],
    //       user_id: 'auth0|foo',
    //       username: 'baz'
    //     }
    //   };
    //   const action = PSActionFactory.setUserPermissions(property, data);
    //   const permissions = Immutable.fromJS(getUserPermissions(action, Immutable.fromJS(allUsersById)));
    //   const state = reducer(INITIAL_STATE, action);
    //
    //   expect(state).to.have.deep.property(`propertyPermissions.${property.title}.userPermissions`).eql(permissions);
    // });
    //
    // it('should update entity user permissions', function() {
    //   const allUsersById = {
    //     'auth0|foo': {
    //       email: 'foo',
    //       nickname: 'bar',
    //       organization: [],
    //       roles: ['foo'],
    //       user_id: 'auth0|foo',
    //       username: 'baz'
    //     }
    //   };
    //   const action = PSActionFactory.setUserPermissions(undefined, data);
    //   const permissions = Immutable.fromJS(getUserPermissions(action, Immutable.fromJS(allUsersById)));
    //   const state = reducer(INITIAL_STATE, action);
    //
    //   expect(state).to.have.property('entityUserPermissions').eql(permissions);
    // });
  });

  describe('Reset permissions', function() {
    it('should reset permissions', function() {
      const action = PSActionFactory.resetPermissions();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.equal(INITIAL_STATE);
    });
  });
});
<commit_msg>Fix test for user permissions updates<commit_after>import { expect } from 'chai';
import '../../../config/chai/chai.config';

import Immutable from 'immutable';
import * as PSActionFactory from './PermissionsSummaryActionFactory';
import reducer from './PermissionsSummaryReducer';
import { getRolePermissions, getUserPermissions } from './PermissionsSummaryHelpers';

const allUsersById = {
  'auth0|foo': {
    email: 'foo',
    nickname: 'bar',
    organization: [],
    roles: ['foo', 'AuthenticatedUser'],
    user_id: 'auth0|foo',
    username: 'baz'
  }
};

const property = {
  title: 'title'
};

const data = {
  authenticatedUserPermissions: [],
  roleAcls: {
    Discover: [],
    Link: ['foo'],
    Read: [],
    Write: []
  },
  userAcls: {
    Discover: [],
    Link: [],
    Read: [],
    Write: ['auth0|foo'],
    Ownder: []
  }
};

describe('PermissionsSummaryReducer', function() {
  let INITIAL_STATE;

  beforeEach(function() {
    INITIAL_STATE = reducer(undefined, { type: 'init' });
  });

  describe('Load users and roles', function() {
    it('should update isGettingUsersRoles to true', function() {
      const entitySet = {};
      const action = PSActionFactory.getAllUsersAndRolesRequest(entitySet);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingUsersRoles').equal(true);
    });

    it('should update isGettingUsersRoles to false (success case)', function() {
      const users = {};
      const roles = [];
      const action = PSActionFactory.getAllUsersAndRolesSuccess(users, roles);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingUsersRoles').equal(false);
    });

    it('should update allUsersById', function() {
      const users = Immutable.fromJS([{test: 'user1'}, {test: 'user2'}]);
      const roles = Immutable.fromJS([{test: 'role1'}, {test: 'role2'}]);
      const action = PSActionFactory.getAllUsersAndRolesSuccess(users, roles);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('allUsersById').equal(users);
    });

    it('should update allRolesList', function() {
      const users = Immutable.fromJS([{test: 'user1'}, {test: 'user2'}]);
      const roles = Immutable.fromJS([{test: 'role1'}, {test: 'role2'}]);
      const action = PSActionFactory.getAllUsersAndRolesSuccess(users, roles);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('allRolesList').equal(roles);
    });

    it('should update isGettingUsersRoles to false (failure case)', function() {
      const action = PSActionFactory.getAllUsersAndRolesFailure();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingUsersRoles').equal(false);
    });
  });

  describe('Load permissions', function() {
    it('should update isGettingAcls to true', function() {
      const entitySet = {};
      const action = PSActionFactory.getAcls(entitySet);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingAcls').equal(true);
    });

    it('should update isGettingAcls to false', function() {
      const entitySetId = '';
      const property = {};
      const action = PSActionFactory.getUserRolePermissionsRequest(entitySetId, property);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingAcls').equal(false);
    });

    it('should update isGettingPermissions to true', function() {
      const entitySetId = '';
      const property = {};
      const action = PSActionFactory.getUserRolePermissionsRequest(entitySetId, property);
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingPermissions').equal(true);
    });

    it('should update isGettingPermissions to false (success case)', function() {
      const action = PSActionFactory.getUserRolePermissionsSuccess();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingPermissions').equal(false);
    });

    it('should update isGettingPermissions to false (failure case)', function() {
      const action = PSActionFactory.getUserRolePermissionsFailure();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('isGettingPermissions').equal(false);
    });

    it('should get role permissions', function() {
      const action = PSActionFactory.setRolePermissions(property, data);
      const permissions = getRolePermissions(action);
      const result = {
        AuthenticatedUser: ['None'],
        foo: ['Link']
      };

      expect(permissions).to.eql(result);
    });

    it('should update property role permissions', function() {
      const action = PSActionFactory.setRolePermissions(property, data);
      const permissions = Immutable.fromJS(getRolePermissions(action));
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.deep.property(`propertyPermissions.${property.title}.rolePermissions`).eql(permissions);
    });

    it('should update entity role permissions', function() {
      const action = PSActionFactory.setRolePermissions(undefined, data);
      const permissions = Immutable.fromJS(getRolePermissions(action));
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.have.property('entityRolePermissions').eql(permissions);
    });

  // TODO: Fix errors in user permissions tests
    it('should get user permissions', function() {
      const action = PSActionFactory.setUserPermissions(property, data);
      const permissions = getUserPermissions(action, Immutable.fromJS(allUsersById));
      const result = [
        {
          email: 'foo',
          id: 'auth0|foo',
          individualPermissions: ['Write'],
          nickname: 'bar',
          permissions: ['Write', 'Link'],
          roles: ['foo']
        }
      ];

      // TODO: Reformat to accept different order in array e.g. permissions
      expect(permissions).to.deep.eql(result);
    });

    it('should update property user permissions', function() {
      const action = PSActionFactory.setUserPermissions(property, data);
      const permissions = getUserPermissions(action, Immutable.fromJS(allUsersById));
      const state = reducer(INITIAL_STATE, action);

      expect(Promise.resolve(state)).to.eventually.have.deep.property(`propertyPermissions.${property.title}.userPermissions`).eql(Immutable.fromJS(permissions));
    });

    it('should update entity user permissions', function() {
      const action = PSActionFactory.setUserPermissions(undefined, data);
      const permissions = Immutable.fromJS(getUserPermissions(action, Immutable.fromJS(allUsersById)));
      const state = reducer(INITIAL_STATE, action);

      expect(Promise.resolve(state)).to.eventually.have.property('entityUserPermissions').eql(permissions);
    });
  });

  describe('Reset permissions', function() {
    it('should reset permissions', function() {
      const action = PSActionFactory.resetPermissions();
      const state = reducer(INITIAL_STATE, action);

      expect(state).to.equal(INITIAL_STATE);
    });
  });
});
<|endoftext|>


<commit_before>/**
Copyright (c) 2017 Kenny Do

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
**/
// Returns the host name of the url. Etc. "https://en.wikipedia.org/wiki/Cat" becomes en.wikipedia.org
export const getHostname = (urlToGetHostName) => {
	let hostname;
	try {
		hostname = new URL(urlToGetHostName).hostname;
		// Strip "www." if the URL starts with it.
		hostname = hostname.replace(/^www[a-z0-9]?\./, "");
	} catch (error) {
		return "";
	}
	return hostname;
};

// Returns true if it is a webpage
export const isAWebpage = (URL) => {
	if (URL === undefined) {
		return false;
	}
	if (URL.match(/^http:/) || URL.match(/^https:/)) {
		return true;
	}
	return false;
};

// Returns true if it is a IP
export const isAnIP = (URL) => {
	const hostname = getHostname(URL);
	let reIP = new RegExp("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+");
	if (reIP.test(hostname)) {
		return true;
	}
	return false;
};

// extract the main domain from sub domains (sub.sub.domain.com becomes domain.com)
export const extractMainDomain = (domain) => {
	const secondLvlDomains = {
		"biz": true,
		"com": true,
		"edu": true,
		"gov": true,
		"ltd": true,
		"net": true,
		"mod": true,
		"org": true,
		"police": true,
		"school": true
	};
	if (domain === "") {
		return "";
	}
	// Return the domain if it is an ip address
	let reIP = new RegExp("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+");
	if (reIP.test(domain)) {
		return domain;
	}
	// Delete a '.' if domain contains it at the end
	let editedDomain = domain;
	if (editedDomain.charAt(editedDomain.length - 1) === ".") {
		editedDomain = domain.slice(0, domain.length - 1);
	}
	const partsOfDomain = editedDomain.split(".");
	const length = partsOfDomain.length;
	if (length === 1) {
		return partsOfDomain[0];
	}
	const firstPartOfTopLevel = partsOfDomain[length - 2];

	// Check for country top level domain
	if (
		length > 2 &&
		(partsOfDomain[length - 2].length === 2 || secondLvlDomains[firstPartOfTopLevel]) &&
		partsOfDomain[length - 1].length === 2) {
		return `${partsOfDomain[length - 3]}.${partsOfDomain[length - 2]}.${partsOfDomain[length - 1]}`;
	}
	return `${partsOfDomain[length - 2] !== undefined ? `${partsOfDomain[length - 2]}.` : ""}${partsOfDomain[length - 1]}`;
};

// Gets the value of the setting
export const getSetting = (state, settingName) => state.settings[settingName].value;

// Puts the domain in the right format for browser.cookies.clean()
export const prepareCookieDomain = (cookie) => {
	let cookieDomain = cookie.domain;
	if (cookieDomain.charAt(0) === ".") {
		cookieDomain = cookieDomain.slice(1);
	}
	cookieDomain = cookie.secure ? `https://${cookieDomain}${cookie.path}` : `http://${cookieDomain}${cookie.path}`;
	return cookieDomain;
};

// Gets a sanitized cookieStoreId
export const getStoreId = (state, storeId) => {
	if (
		(storeId === "firefox-default") ||
		(!getSetting(state, "contextualIdentities") && storeId !== "firefox-private" && state.cache.browserDetect === "Firefox") ||
		(state.cache.browserDetect === "Chrome" && storeId === "0")) {
		return "default";
	}
	if (state.cache.browserDetect === "Chrome" && storeId === "1") {
		return "private";
	}

	return storeId;
};

// Converts a expression to its regular expression equivalent
export const globExpressionToRegExp = (glob) => {
	const normalizedGlob = glob.trim().toLowerCase();
	if (normalizedGlob.startsWith("*.")) {
		return `${normalizedGlob.replace("*.", "(^|\.)").replace(/\./g, "\\.").replace(/\*/g, "\.\*")}$`;
	}
	return `^${normalizedGlob.replace(/\./g, "\\.").replace(/\*/g, "\.\*")}$`;
};

// Returns the first availble matched expression
export const returnMatchedExpressionObject = (state, cookieStoreId, hostname) => {
	const storeId = getStoreId(state, cookieStoreId);
	const expressionList = state.lists[storeId] || [];
	return expressionList.find((expression) => new RegExp(globExpressionToRegExp(expression.expression)).test(hostname));
};

// Return optional attributes for the Cookie API calls
export const returnOptionalCookieAPIAttributes = (state, cookieAPIAttributes) => {
	let newAttributes = {
		...cookieAPIAttributes
	};

	// Add optional firstPartyDomain attribute
	if (state.cache.browserDetect === "Firefox" && state.cache.firstPartyIsolateSetting && !Object.prototype.hasOwnProperty.call(newAttributes, "firstPartyDomain")) {
		newAttributes.firstPartyDomain = undefined;
		return newAttributes;
	} else if (!(state.cache.browserDetect === "Firefox" && state.cache.firstPartyIsolateSetting)) {
		delete newAttributes.firstPartyDomain;
	}
	return newAttributes;
};
<commit_msg>Update libs.js<commit_after>/**
Copyright (c) 2017 Kenny Do

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
**/
// Returns the host name of the url. Etc. "https://en.wikipedia.org/wiki/Cat" becomes en.wikipedia.org
export const getHostname = (urlToGetHostName) => {
	let hostname;
	try {
		hostname = new URL(urlToGetHostName).hostname;
		// Strip "www." if the URL starts with it.
		hostname = hostname.replace(/^www[a-z0-9]?\./, "");
	} catch (error) {
		return "";
	}
	return hostname;
};

// Returns true if it is a webpage
export const isAWebpage = (URL) => {
	if (URL === undefined) {
		return false;
	}
	if (URL.match(/^http:/) || URL.match(/^https:/)) {
		return true;
	}
	return false;
};

// Returns true if it is a IP
export const isAnIP = (URL) => {
	const hostname = getHostname(URL);
	let reIP = new RegExp("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+");
	if (reIP.test(hostname)) {
		return true;
	}
	return false;
};

// extract the main domain from sub domains (sub.sub.domain.com becomes domain.com)
export const extractMainDomain = (domain) => {
	const secondLvlDomains = {
		"biz": true,
		"com": true,
		"edu": true,
		"gov": true,
		"ltd": true,
		"net": true,
		"mod": true,
		"org": true,
		"police": true,
		"school": true
	};
	if (domain === "") {
		return "";
	}
	// Return the domain if it is an ip address
	let reIP = new RegExp("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+");
	if (reIP.test(domain)) {
		return domain;
	}
	// Delete a '.' if domain contains it at the end
	let editedDomain = domain;
	if (editedDomain.charAt(editedDomain.length - 1) === ".") {
		editedDomain = domain.slice(0, domain.length - 1);
	}
	const partsOfDomain = editedDomain.split(".");
	const length = partsOfDomain.length;
	if (length === 1) {
		return partsOfDomain[0];
	}
	const firstPartOfTopLevel = partsOfDomain[length - 2];

	// Check for country top level domain
	if (
		length > 2 &&
		(partsOfDomain[length - 2].length === 2 || secondLvlDomains[firstPartOfTopLevel]) &&
		partsOfDomain[length - 1].length === 2) {
		return `${partsOfDomain[length - 3]}.${partsOfDomain[length - 2]}.${partsOfDomain[length - 1]}`;
	}
	return `${partsOfDomain[length - 2] !== undefined ? `${partsOfDomain[length - 2]}.` : ""}${partsOfDomain[length - 1]}`;
};

// Gets the value of the setting
export const getSetting = (state, settingName) => state.settings[settingName].value;

// Puts the domain in the right format for browser.cookies.clean()
export const prepareCookieDomain = (cookie) => {
	let cookieDomain = cookie.domain;
	if (cookieDomain.charAt(0) === ".") {
		cookieDomain = cookieDomain.slice(1);
	}
	cookieDomain = cookie.secure ? `https://${cookieDomain}${cookie.path}` : `http://${cookieDomain}${cookie.path}`;
	return cookieDomain;
};

// Gets a sanitized cookieStoreId
export const getStoreId = (state, storeId) => {
	if (
		(storeId === "firefox-default") ||
		(!getSetting(state, "contextualIdentities") && storeId !== "firefox-private" && state.cache.browserDetect === "Firefox") ||
		(state.cache.browserDetect === "Chrome" && storeId === "0" || state.cache.browserDetect === "Opera" && storeId === "0")) {
		return "default";
	}
	if (state.cache.browserDetect === "Chrome" && storeId === "1") {
		return "private";
	}

	return storeId;
};

// Converts a expression to its regular expression equivalent
export const globExpressionToRegExp = (glob) => {
	const normalizedGlob = glob.trim().toLowerCase();
	if (normalizedGlob.startsWith("*.")) {
		return `${normalizedGlob.replace("*.", "(^|\.)").replace(/\./g, "\\.").replace(/\*/g, "\.\*")}$`;
	}
	return `^${normalizedGlob.replace(/\./g, "\\.").replace(/\*/g, "\.\*")}$`;
};

// Returns the first availble matched expression
export const returnMatchedExpressionObject = (state, cookieStoreId, hostname) => {
	const storeId = getStoreId(state, cookieStoreId);
	const expressionList = state.lists[storeId] || [];
	return expressionList.find((expression) => new RegExp(globExpressionToRegExp(expression.expression)).test(hostname));
};

// Return optional attributes for the Cookie API calls
export const returnOptionalCookieAPIAttributes = (state, cookieAPIAttributes) => {
	let newAttributes = {
		...cookieAPIAttributes
	};

	// Add optional firstPartyDomain attribute
	if (state.cache.browserDetect === "Firefox" && state.cache.firstPartyIsolateSetting && !Object.prototype.hasOwnProperty.call(newAttributes, "firstPartyDomain")) {
		newAttributes.firstPartyDomain = undefined;
		return newAttributes;
	} else if (!(state.cache.browserDetect === "Firefox" && state.cache.firstPartyIsolateSetting)) {
		delete newAttributes.firstPartyDomain;
	}
	return newAttributes;
};
<|endoftext|>