From 7634e18feb0ae1fa18fa153fcb497d005ef77af8 Mon Sep 17 00:00:00 2001 From: Joel Takvorian Date: Wed, 14 Jun 2023 21:26:19 +0200 Subject: [PATCH] Show scope as slider Borrow slider from patternfly, and adapt for vertical display --- .../netflow-topology/netflow-topology.tsx | 2 + .../components/scope-slider/scope-slider.css | 8 ++ .../components/scope-slider/scope-slider.tsx | 36 +++++++ web/src/components/slider/Slider.tsx | 99 +++++++++---------- web/src/components/slider/SliderStep.tsx | 37 +++++++ 5 files changed, 131 insertions(+), 51 deletions(-) create mode 100644 web/src/components/scope-slider/scope-slider.css create mode 100644 web/src/components/scope-slider/scope-slider.tsx create mode 100644 web/src/components/slider/SliderStep.tsx diff --git a/web/src/components/netflow-topology/netflow-topology.tsx b/web/src/components/netflow-topology/netflow-topology.tsx index 99c3794dd..77c17a4b8 100644 --- a/web/src/components/netflow-topology/netflow-topology.tsx +++ b/web/src/components/netflow-topology/netflow-topology.tsx @@ -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; @@ -97,6 +98,7 @@ export const NetflowTopology: React.FC<{ } else { return ( + void; +} + +export const ScopeSlider: React.FC = ({ 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 ( +
+ ({ value: idx, label: s[1] }))} + onChange={(value: number) => setMetricScope(scopes[value][0])} + vertical + /> +
+ ); +}; diff --git a/web/src/components/slider/Slider.tsx b/web/src/components/slider/Slider.tsx index 784709bcc..0b29be04f 100644 --- a/web/src/components/slider/Slider.tsx +++ b/web/src/components/slider/Slider.tsx @@ -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. @@ -71,10 +69,10 @@ export interface SliderProps extends Omit, '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 = ({ className, value = 0, @@ -98,6 +96,7 @@ export const Slider: React.FunctionComponent = ({ showBoundaries = true, 'aria-describedby': ariaDescribedby, 'aria-labelledby': ariaLabelledby, + vertical, ...props }: SliderProps) => { const sliderRailRef = React.useRef(); @@ -122,6 +121,9 @@ export const Slider: React.FunctionComponent = ({ 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)); @@ -141,7 +143,7 @@ export const Slider: React.FunctionComponent = ({ }; const onThumbClick = () => { - thumbRef.current.focus(); + thumbRef.current?.focus(); }; const onBlur = () => { @@ -162,6 +164,14 @@ export const Slider: React.FunctionComponent = ({ 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); @@ -174,7 +184,7 @@ export const Slider: React.FunctionComponent = ({ 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); @@ -183,7 +193,7 @@ export const Slider: React.FunctionComponent = ({ 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); @@ -193,7 +203,7 @@ export const Slider: React.FunctionComponent = ({ 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); @@ -207,51 +217,34 @@ export const Slider: React.FunctionComponent = ({ 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); @@ -262,7 +255,7 @@ export const Slider: React.FunctionComponent = ({ if (snapValue !== undefined) { onChange(snapValue); } else { - onChange(newValue); + onChange(targetValue); } } }; @@ -299,7 +292,7 @@ export const Slider: React.FunctionComponent = ({ } 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); @@ -366,7 +359,7 @@ export const Slider: React.FunctionComponent = ({ const thumbComponent = (
= ({ 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} /> ); @@ -392,7 +385,11 @@ export const Slider: React.FunctionComponent = ({ > {leftActions &&
{leftActions}
}
-
+
{customSteps && ( diff --git a/web/src/components/slider/SliderStep.tsx b/web/src/components/slider/SliderStep.tsx new file mode 100644 index 000000000..ad7d6f939 --- /dev/null +++ b/web/src/components/slider/SliderStep.tsx @@ -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, '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 = ({ + className, + label, + value, + isTickHidden = false, + isLabelHidden = false, + isActive = false, + ...props +}: SliderStepProps) => { + const style = { '--pf-c-slider__step--Left': `${value}%` } as React.CSSProperties; + return ( +
+ {!isTickHidden &&
} + {!isLabelHidden && label &&
{label}
} +
+ ); +}; +SliderStep.displayName = 'SliderStep';