Skip to content

Commit

Permalink
Show scope as slider
Browse files Browse the repository at this point in the history
Borrow slider from patternfly, and adapt for vertical display
  • Loading branch information
jotak committed Jun 23, 2023
1 parent 148f610 commit 7634e18
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 51 deletions.
2 changes: 2 additions & 0 deletions web/src/components/netflow-topology/netflow-topology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ThreeDTopologyContent from './3d/three-d-topology-content';
import componentFactory from './2d/componentFactories/componentFactory';
import stylesComponentFactory from './2d/componentFactories/stylesComponentFactory';
import layoutFactory from './2d/layouts/layoutFactory';
import { ScopeSlider } from '../scope-slider/scope-slider';

export const NetflowTopology: React.FC<{
loading?: boolean;
Expand Down Expand Up @@ -97,6 +98,7 @@ export const NetflowTopology: React.FC<{
} else {
return (
<VisualizationProvider data-test="visualization-provider" controller={controller}>
<ScopeSlider metricScope={metricScope} setMetricScope={setMetricScope} />
<TopologyContent
k8sModels={k8sModels}
metricFunction={metricFunction}
Expand Down
8 changes: 8 additions & 0 deletions web/src/components/scope-slider/scope-slider.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#scope-slider {
position: relative;
width: 300px;
height: 0;
top: 150px;
left: -100px;
z-index: 2;
}
36 changes: 36 additions & 0 deletions web/src/components/scope-slider/scope-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { MetricScope } from '../../model/flow-query';
import { Slider } from '../slider/Slider';

import './scope-slider.css';

export interface ScopeSliderProps {
metricScope: MetricScope;
setMetricScope: (ms: MetricScope) => void;
}

export const ScopeSlider: React.FC<ScopeSliderProps> = ({ metricScope, setMetricScope }) => {
const { t } = useTranslation('plugin__netobserv-plugin');

const scopes: [MetricScope, string][] = [
['resource', t('Resource')],
['owner', t('Owner')],
['namespace', t('Namespace')],
['host', t('Node')]
];
const index = scopes.findIndex(s => s[0] === metricScope);

return (
<div id={'scope-slider'}>
<Slider
value={index < 0 ? 2 : index}
showTicks
max={3}
customSteps={scopes.map((s, idx) => ({ value: idx, label: s[1] }))}
onChange={(value: number) => setMetricScope(scopes[value][0])}
vertical
/>
</div>
);
};
99 changes: 48 additions & 51 deletions web/src/components/slider/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { useState } from 'react';
import styles from '@patternfly/react-styles/css/components/Slider/slider';
import { css } from '@patternfly/react-styles';
import { SliderStep } from './SliderStep';
import { InputGroup, InputGroupText } from '../InputGroup';
import { TextInput } from '../TextInput';
import { Tooltip } from '../Tooltip';
import { InputGroup, InputGroupText, TextInput, Tooltip } from '@patternfly/react-core';

/** Properties for creating custom steps in a slider. These properties should be passed in as
* an object within an array to the slider component's customSteps property.
Expand Down Expand Up @@ -71,10 +69,10 @@ export interface SliderProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onCh
thumbAriaLabel?: string;
/** Current value of the slider. */
value?: number;
/** Vertical display mode. */
vertical?: boolean;
}

const getPercentage = (current: number, max: number) => (100 * current) / max;

export const Slider: React.FunctionComponent<SliderProps> = ({
className,
value = 0,
Expand All @@ -98,6 +96,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
showBoundaries = true,
'aria-describedby': ariaDescribedby,
'aria-labelledby': ariaLabelledby,
vertical,
...props
}: SliderProps) => {
const sliderRailRef = React.useRef<HTMLDivElement>();
Expand All @@ -122,6 +121,9 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
const style = { '--pf-c-slider--value': `${stylePercent}%` } as React.CSSProperties;
const widthChars = React.useMemo(() => localInputValue.toString().length, [localInputValue]);
const inputStyle = { '--pf-c-slider__value--c-form-control--width-chars': widthChars } as React.CSSProperties;
if (vertical) {
style['transform'] = 'rotate(270deg)';
}

const onChangeHandler = (value: string) => {
setLocalInputValue(Number(value));
Expand All @@ -141,7 +143,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
};

const onThumbClick = () => {
thumbRef.current.focus();
thumbRef.current?.focus();
};

const onBlur = () => {
Expand All @@ -162,6 +164,14 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
return Number(Number(localValue).toFixed(2)).toString();
};

// Position hooks
const mousePos = vertical ? (e: React.MouseEvent) => e.clientY : (e: React.MouseEvent) => e.clientX;
const touchPos = vertical
? (e: React.TouchEvent) => e.touches[0].clientY
: (e: React.TouchEvent) => e.touches[0].clientX;
const minPos = vertical ? (r: DOMRect) => r.bottom : (r: DOMRect) => r.left;
const maxPos = vertical ? (r: DOMRect) => r.top : (r: DOMRect) => r.right;

const handleThumbDragEnd = () => {
document.removeEventListener('mousemove', callbackThumbMove);
document.removeEventListener('mouseup', callbackThumbUp);
Expand All @@ -174,7 +184,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
e.stopPropagation();
e.preventDefault();

diff = e.clientX - thumbRef.current.getBoundingClientRect().left;
diff = mousePos(e) - minPos(thumbRef.current!.getBoundingClientRect());

document.addEventListener('mousemove', callbackThumbMove);
document.addEventListener('mouseup', callbackThumbUp);
Expand All @@ -183,7 +193,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
const handleTouchStart = (e: React.TouchEvent) => {
e.stopPropagation();

diff = e.touches[0].clientX - thumbRef.current.getBoundingClientRect().left;
diff = touchPos(e) - minPos(thumbRef.current!.getBoundingClientRect());

document.addEventListener('touchmove', callbackThumbMove, { passive: false });
document.addEventListener('touchend', callbackThumbUp);
Expand All @@ -193,7 +203,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
const onSliderRailClick = (e: any) => {
handleThumbMove(e);
if (snapValue && !areCustomStepsContinuous) {
thumbRef.current.style.setProperty('--pf-c-slider--value', `${snapValue}%`);
thumbRef.current!.style.setProperty('--pf-c-slider--value', `${snapValue}%`);
setValue(snapValue);
if (onChange) {
onChange(snapValue);
Expand All @@ -207,51 +217,34 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
e.stopImmediatePropagation();
}

const clientPosition = e.touches && e.touches.length ? e.touches[0].clientX : e.clientX;

let newPosition = clientPosition - diff - sliderRailRef.current.getBoundingClientRect().left;

const end = sliderRailRef.current.offsetWidth - thumbRef.current.offsetWidth;

const start = 0;

if (newPosition < start) {
newPosition = 0;
}

if (newPosition > end) {
newPosition = end;
}

const newPercentage = getPercentage(newPosition, end);
const clientPosition = e.touches && e.touches.length ? touchPos(e) : mousePos(e);
const boundingRect = sliderRailRef.current!.getBoundingClientRect();
const refSize = maxPos(boundingRect) - minPos(boundingRect);
const relativePos = clientPosition - diff - minPos(boundingRect);
const ratio = Math.max(0, Math.min(1, relativePos / refSize));

thumbRef.current.style.setProperty('--pf-c-slider--value', `${newPercentage}%`);
// convert percentage to value
const newValue = Math.round(((newPercentage * (max - min)) / 100 + min) * 100) / 100;
setValue(newValue);
thumbRef.current!.style.setProperty('--pf-c-slider--value', `${100 * ratio}%`);
const targetValue = ratio * (max - min) + min;
setValue(Math.round(targetValue));

if (!customSteps) {
// snap to new value if not custom steps
snapValue = Math.round((Math.round((newValue - min) / step) * step + min) * 100) / 100;
thumbRef.current.style.setProperty('--pf-c-slider--value', `${snapValue}%`);
snapValue = Math.round((Math.round((targetValue - min) / step) * step + min) * 100) / 100;
thumbRef.current!.style.setProperty('--pf-c-slider--value', `${snapValue}%`);
setValue(snapValue);
}

/* If custom steps are discrete, snap to closest step value */
if (!areCustomStepsContinuous && customSteps) {
let percentage = newPercentage;
if (customSteps[customSteps.length - 1].value !== 100) {
percentage = (newPercentage * (max - min)) / 100 + min;
}
const stepIndex = customSteps.findIndex(stepObj => stepObj.value >= percentage);
if (customSteps[stepIndex].value === percentage) {
snapValue = customSteps[stepIndex].value;
const nextStepIndex = customSteps.findIndex(stepObj => stepObj.value >= targetValue);
if (customSteps[nextStepIndex].value === targetValue) {
snapValue = customSteps[nextStepIndex].value;
} else {
const midpoint = (customSteps[stepIndex].value + customSteps[stepIndex - 1].value) / 2;
if (midpoint > percentage) {
snapValue = customSteps[stepIndex - 1].value;
const midpoint = (customSteps[nextStepIndex].value + customSteps[nextStepIndex - 1].value) / 2;
if (midpoint > targetValue) {
snapValue = customSteps[nextStepIndex - 1].value;
} else {
snapValue = customSteps[stepIndex].value;
snapValue = customSteps[nextStepIndex].value;
}
}
setValue(snapValue);
Expand All @@ -262,7 +255,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
if (snapValue !== undefined) {
onChange(snapValue);
} else {
onChange(newValue);
onChange(targetValue);
}
}
};
Expand Down Expand Up @@ -299,7 +292,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
}

if (newValue !== localValue) {
thumbRef.current.style.setProperty('--pf-c-slider--value', `${newValue}%`);
thumbRef.current!.style.setProperty('--pf-c-slider--value', `${newValue}%`);
setValue(newValue);
if (onChange) {
onChange(newValue);
Expand Down Expand Up @@ -366,7 +359,7 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
const thumbComponent = (
<div
className={css(styles.sliderThumb)}
ref={thumbRef}
ref={thumbRef as any}
tabIndex={isDisabled ? -1 : 0}
role="slider"
aria-valuemin={customSteps ? customSteps[0].value : min}
Expand All @@ -377,10 +370,10 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
aria-disabled={isDisabled}
aria-describedby={ariaDescribedby}
aria-labelledby={ariaLabelledby}
onMouseDown={!isDisabled ? handleMouseDown : null}
onTouchStart={!isDisabled ? handleTouchStart : null}
onKeyDown={!isDisabled ? handleThumbKeys : null}
onClick={!isDisabled ? onThumbClick : null}
onMouseDown={!isDisabled ? handleMouseDown : undefined}
onTouchStart={!isDisabled ? handleTouchStart : undefined}
onKeyDown={!isDisabled ? handleThumbKeys : undefined}
onClick={!isDisabled ? onThumbClick : undefined}
/>
);

Expand All @@ -392,7 +385,11 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
>
{leftActions && <div className={css(styles.sliderActions)}>{leftActions}</div>}
<div className={css(styles.sliderMain)}>
<div className={css(styles.sliderRail)} ref={sliderRailRef} onClick={!isDisabled ? onSliderRailClick : null}>
<div
className={css(styles.sliderRail)}
ref={sliderRailRef as any}
onClick={!isDisabled ? onSliderRailClick : undefined}
>
<div className={css(styles.sliderRailTrack)} />
</div>
{customSteps && (
Expand Down
37 changes: 37 additions & 0 deletions web/src/components/slider/SliderStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/Slider/slider';
import { css } from '@patternfly/react-styles';

export interface SliderStepProps extends Omit<React.HTMLProps<HTMLDivElement>, 'label'> {
/** Additional classes added to the slider step. */
className?: string;
/** Flag indicating the step is active. */
isActive?: boolean;
/** Flag indicating that the label should be hidden. */
isLabelHidden?: boolean;
/** Flag indicating that the tick should be hidden. */
isTickHidden?: boolean;
/** Step label. **/
label?: string;
/** Step value. **/
value?: number;
}

export const SliderStep: React.FunctionComponent<SliderStepProps> = ({
className,
label,
value,
isTickHidden = false,
isLabelHidden = false,
isActive = false,
...props
}: SliderStepProps) => {
const style = { '--pf-c-slider__step--Left': `${value}%` } as React.CSSProperties;
return (
<div className={css(styles.sliderStep, isActive && styles.modifiers.active, className)} style={style} {...props}>
{!isTickHidden && <div className={css(styles.sliderStepTick)} />}
{!isLabelHidden && label && <div className={css(styles.sliderStepLabel)}>{label}</div>}
</div>
);
};
SliderStep.displayName = 'SliderStep';

0 comments on commit 7634e18

Please sign in to comment.