* Expose FilterInput from grafana/ui
* Make table filter case insensitive
* Update packages/grafana-ui/src/components/Table/FilterList.tsx
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
(cherry picked from commit f887576a27)
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
co-authored by
Dominik Prokop
parent
b82bc90180
commit
f793c0d2fc
@@ -0,0 +1,48 @@
|
||||
import React, { FC } from 'react';
|
||||
import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data';
|
||||
import { Button, Icon, Input } from '..';
|
||||
import { useFocus } from '../Input/utils';
|
||||
|
||||
export interface Props {
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
width?: number;
|
||||
onChange: (value: string) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => {
|
||||
const [inputRef, setInputFocus] = useFocus();
|
||||
|
||||
const suffix =
|
||||
value !== '' ? (
|
||||
<Button
|
||||
icon="times"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
setInputFocus();
|
||||
onChange('');
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Input
|
||||
autoFocus={autoFocus ?? false}
|
||||
prefix={<Icon name="search" />}
|
||||
ref={inputRef}
|
||||
suffix={suffix}
|
||||
width={width}
|
||||
type="text"
|
||||
value={value ? unEscapeStringFromRegex(value) : ''}
|
||||
onChange={(event) => onChange(escapeStringForRegex(event.currentTarget.value))}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { getFocusStyle, sharedInputStyle } from '../Forms/commonStyles';
|
||||
import { stylesFactory, useTheme2 } from '../../themes';
|
||||
import { Spinner } from '../Spinner/Spinner';
|
||||
import { useClientRect } from '../../utils/useClientRect';
|
||||
import useMeasure from 'react-use/lib/useMeasure';
|
||||
|
||||
export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'prefix' | 'size'> {
|
||||
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
|
||||
@@ -29,6 +29,55 @@ interface StyleDeps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||
const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, width = 0, ...restProps } = props;
|
||||
/**
|
||||
* Prefix & suffix are positioned absolutely within inputWrapper. We use client rects below to apply correct padding to the input
|
||||
* when prefix/suffix is larger than default (28px = 16px(icon) + 12px(left/right paddings)).
|
||||
* Thanks to that prefix/suffix do not overflow the input element itself.
|
||||
*/
|
||||
const [prefixRef, prefixRect] = useMeasure<HTMLDivElement>();
|
||||
const [suffixRef, suffixRect] = useMeasure<HTMLDivElement>();
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getInputStyles({ theme, invalid: !!invalid, width });
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
|
||||
|
||||
<div className={styles.inputWrapper}>
|
||||
{prefix && (
|
||||
<div className={styles.prefix} ref={prefixRef}>
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={ref}
|
||||
className={styles.input}
|
||||
{...restProps}
|
||||
style={{
|
||||
paddingLeft: prefixRect ? prefixRect.width + 12 : undefined,
|
||||
paddingRight: suffixRect ? suffixRect.width + 12 : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
{(suffix || loading) && (
|
||||
<div className={styles.suffix} ref={suffixRef}>
|
||||
{loading && <Spinner className={styles.loadingIndicator} inline={true} />}
|
||||
{suffix}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!addonAfter && <div className={styles.addon}>{addonAfter}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: StyleDeps) => {
|
||||
const prefixSuffixStaticWidth = '28px';
|
||||
const prefixSuffix = css`
|
||||
@@ -212,52 +261,3 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||
const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, width = 0, ...restProps } = props;
|
||||
/**
|
||||
* Prefix & suffix are positioned absolutely within inputWrapper. We use client rects below to apply correct padding to the input
|
||||
* when prefix/suffix is larger than default (28px = 16px(icon) + 12px(left/right paddings)).
|
||||
* Thanks to that prefix/suffix do not overflow the input element itself.
|
||||
*/
|
||||
const [prefixRect, prefixRef] = useClientRect<HTMLDivElement>();
|
||||
const [suffixRect, suffixRef] = useClientRect<HTMLDivElement>();
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getInputStyles({ theme, invalid: !!invalid, width });
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
|
||||
|
||||
<div className={styles.inputWrapper}>
|
||||
{prefix && (
|
||||
<div className={styles.prefix} ref={prefixRef}>
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={ref}
|
||||
className={styles.input}
|
||||
{...restProps}
|
||||
style={{
|
||||
paddingLeft: prefixRect ? prefixRect.width : undefined,
|
||||
paddingRight: suffixRect ? suffixRect.width : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
{(suffix || loading) && (
|
||||
<div className={styles.suffix} ref={suffixRef}>
|
||||
{loading && <Spinner className={styles.loadingIndicator} inline={true} />}
|
||||
{suffix}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!addonAfter && <div className={styles.addon}>{addonAfter}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { RefObject, useRef } from 'react';
|
||||
|
||||
export function useFocus(): [RefObject<HTMLInputElement>, () => void] {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const setFocus = () => {
|
||||
ref.current && ref.current.focus();
|
||||
};
|
||||
return [ref, setFocus];
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
|
||||
import { stylesFactory, useTheme2 } from '../../themes';
|
||||
import { Checkbox, Input, Label, VerticalGroup } from '..';
|
||||
import { Checkbox, FilterInput, Label, VerticalGroup } from '..';
|
||||
|
||||
interface Props {
|
||||
values: SelectableValue[];
|
||||
@@ -19,16 +19,16 @@ export const FilterList: FC<Props> = ({ options, values, onChange }) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const items = useMemo(() => options.filter((option) => option.label?.indexOf(searchFilter) !== -1), [
|
||||
options,
|
||||
searchFilter,
|
||||
]);
|
||||
const items = useMemo(
|
||||
() => options.filter((option) => option.label?.toLowerCase().includes(searchFilter.toLowerCase())),
|
||||
[options, searchFilter]
|
||||
);
|
||||
const gutter = theme.spacing.gridSize;
|
||||
const height = useMemo(() => Math.min(items.length * ITEM_HEIGHT, MIN_HEIGHT) + gutter, [gutter, items.length]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>) => {
|
||||
setSearchFilter(event.currentTarget.value);
|
||||
(v: string) => {
|
||||
setSearchFilter(v);
|
||||
},
|
||||
[setSearchFilter]
|
||||
);
|
||||
@@ -46,12 +46,7 @@ export const FilterList: FC<Props> = ({ options, values, onChange }) => {
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="md">
|
||||
<Input
|
||||
placeholder="filter values"
|
||||
className={styles.filterListInput}
|
||||
onChange={onInputChange}
|
||||
value={searchFilter}
|
||||
/>
|
||||
<FilterInput placeholder="Filter values" onChange={onInputChange} value={searchFilter} />
|
||||
{!items.length && <Label>No values</Label>}
|
||||
{items.length && (
|
||||
<List
|
||||
@@ -94,7 +89,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
background-color: ${theme.colors.action.hover};
|
||||
}
|
||||
`,
|
||||
filterListInput: css`
|
||||
label: filterListInput;
|
||||
`,
|
||||
}));
|
||||
|
||||
@@ -199,6 +199,7 @@ export { Badge, BadgeColor, BadgeProps } from './Badge/Badge';
|
||||
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
|
||||
|
||||
export { Input } from './Input/Input';
|
||||
export { FilterInput } from './FilterInput/FilterInput';
|
||||
export { FormInputSize } from './Forms/types';
|
||||
|
||||
export { Switch, InlineSwitch } from './Switch/Switch';
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export const useClientRect = <T extends HTMLElement>(): [{ width: number; height: number } | null, React.Ref<T>] => {
|
||||
const [rect, setRect] = useState<{ width: number; height: number } | null>(null);
|
||||
const ref = useCallback((node: T) => {
|
||||
if (node !== null) {
|
||||
setRect(node.getBoundingClientRect());
|
||||
}
|
||||
}, []);
|
||||
return [rect, ref];
|
||||
};
|
||||
Reference in New Issue
Block a user