diff --git a/public/app/core/components/FilterInput/FilterInput.tsx b/packages/grafana-ui/src/components/FilterInput/FilterInput.tsx similarity index 71% rename from public/app/core/components/FilterInput/FilterInput.tsx rename to packages/grafana-ui/src/components/FilterInput/FilterInput.tsx index b155da7dd9e..93c6ce63603 100644 --- a/public/app/core/components/FilterInput/FilterInput.tsx +++ b/packages/grafana-ui/src/components/FilterInput/FilterInput.tsx @@ -1,6 +1,7 @@ import React, { FC } from 'react'; import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data'; -import { Input, Icon, Button } from '@grafana/ui'; +import { Button, Icon, Input } from '..'; +import { useFocus } from '../Input/utils'; export interface Props { value: string | undefined; @@ -12,9 +13,20 @@ export interface Props { } export const FilterInput: FC = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => { + const [inputRef, setInputFocus] = useFocus(); + const suffix = value !== '' ? ( - ) : null; @@ -23,6 +35,7 @@ export const FilterInput: FC = ({ value, placeholder, width, onChange, on } + ref={inputRef} suffix={suffix} width={width} type="text" diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 91574e14e46..a4acd8db75a 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -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, '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((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(); + const [suffixRef, suffixRect] = useMeasure(); + + const theme = useTheme2(); + const styles = getInputStyles({ theme, invalid: !!invalid, width }); + + return ( +
+ {!!addonBefore &&
{addonBefore}
} + +
+ {prefix && ( +
+ {prefix} +
+ )} + + + + {(suffix || loading) && ( +
+ {loading && } + {suffix} +
+ )} +
+ + {!!addonAfter &&
{addonAfter}
} +
+ ); +}); + +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((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(); - const [suffixRect, suffixRef] = useClientRect(); - - const theme = useTheme2(); - const styles = getInputStyles({ theme, invalid: !!invalid, width }); - - return ( -
- {!!addonBefore &&
{addonBefore}
} - -
- {prefix && ( -
- {prefix} -
- )} - - - - {(suffix || loading) && ( -
- {loading && } - {suffix} -
- )} -
- - {!!addonAfter &&
{addonAfter}
} -
- ); -}); - -Input.displayName = 'Input'; diff --git a/packages/grafana-ui/src/components/Input/utils.ts b/packages/grafana-ui/src/components/Input/utils.ts new file mode 100644 index 00000000000..b21aee2d848 --- /dev/null +++ b/packages/grafana-ui/src/components/Input/utils.ts @@ -0,0 +1,9 @@ +import { RefObject, useRef } from 'react'; + +export function useFocus(): [RefObject, () => void] { + const ref = useRef(null); + const setFocus = () => { + ref.current && ref.current.focus(); + }; + return [ref, setFocus]; +} diff --git a/packages/grafana-ui/src/components/Table/FilterList.tsx b/packages/grafana-ui/src/components/Table/FilterList.tsx index b21a7cf5f2f..0f961cd0922 100644 --- a/packages/grafana-ui/src/components/Table/FilterList.tsx +++ b/packages/grafana-ui/src/components/Table/FilterList.tsx @@ -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 = ({ 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) => { - setSearchFilter(event.currentTarget.value); + (v: string) => { + setSearchFilter(v); }, [setSearchFilter] ); @@ -46,12 +46,7 @@ export const FilterList: FC = ({ options, values, onChange }) => { return ( - + {!items.length && } {items.length && ( ({ background-color: ${theme.colors.action.hover}; } `, - filterListInput: css` - label: filterListInput; - `, })); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index e723ba62c70..95cfc9ff834 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/utils/useClientRect.ts b/packages/grafana-ui/src/utils/useClientRect.ts deleted file mode 100644 index 9dc88fd936f..00000000000 --- a/packages/grafana-ui/src/utils/useClientRect.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useState, useCallback } from 'react'; - -export const useClientRect = (): [{ width: number; height: number } | null, React.Ref] => { - const [rect, setRect] = useState<{ width: number; height: number } | null>(null); - const ref = useCallback((node: T) => { - if (node !== null) { - setRect(node.getBoundingClientRect()); - } - }, []); - return [rect, ref]; -}; diff --git a/public/app/core/components/PageActionBar/PageActionBar.tsx b/public/app/core/components/PageActionBar/PageActionBar.tsx index 5d2099ab807..1c771a4e0f0 100644 --- a/public/app/core/components/PageActionBar/PageActionBar.tsx +++ b/public/app/core/components/PageActionBar/PageActionBar.tsx @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react'; -import { FilterInput } from '../FilterInput/FilterInput'; -import { LinkButton } from '@grafana/ui'; +import { LinkButton, FilterInput } from '@grafana/ui'; export interface Props { searchQuery: string; diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index cd31ed8e581..d1da75059bb 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -1,12 +1,11 @@ import React, { useEffect } from 'react'; import { css, cx } from '@emotion/css'; import { connect, ConnectedProps } from 'react-redux'; -import { Pagination, Tooltip, LinkButton, Icon, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { Pagination, Tooltip, LinkButton, Icon, RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; import Page from 'app/core/components/Page/Page'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { getNavModel } from '../../core/selectors/navModel'; import { AccessControlAction, StoreState, UserDTO } from '../../types'; import { fetchUsers, changeQuery, changePage, changeFilter } from './state/actions'; diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 4e454075383..148db1799e3 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -7,11 +7,10 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { AlertRule, StoreState } from 'app/types'; import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions'; import { getAlertRuleItems, getSearchQuery } from './state/selectors'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { SelectableValue } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { setSearchQuery } from './state/reducers'; -import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui'; +import { Button, LinkButton, Select, VerticalGroup, FilterInput } from '@grafana/ui'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { ShowModalReactEvent } from '../../types/events'; import { AlertHowToModal } from './AlertHowToModal'; diff --git a/public/app/features/api-keys/ApiKeysActionBar.tsx b/public/app/features/api-keys/ApiKeysActionBar.tsx index 165c4e41abe..ef000521a4e 100644 --- a/public/app/features/api-keys/ApiKeysActionBar.tsx +++ b/public/app/features/api-keys/ApiKeysActionBar.tsx @@ -1,6 +1,5 @@ import React, { FC } from 'react'; -import { Button } from '@grafana/ui'; -import { FilterInput } from '../../core/components/FilterInput/FilterInput'; +import { Button, FilterInput } from '@grafana/ui'; interface Props { searchQuery: string; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx index c191748feb3..28f45d38ddc 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx @@ -1,11 +1,10 @@ import React, { useMemo, useState } from 'react'; import { FieldConfigSource, GrafanaTheme2, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; import { DashboardModel, PanelModel } from '../../state'; -import { CustomScrollbar, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { CustomScrollbar, RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui'; import { getPanelFrameCategory } from './getPanelFrameOptions'; import { getVizualizationOptions } from './getVizualizationOptions'; import { css } from '@emotion/css'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { OptionsPaneCategory } from './OptionsPaneCategory'; import { getFieldOverrideCategories } from './getFieldOverrideElements'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index dd0ca0094d3..2b4e699430c 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -1,14 +1,13 @@ import React, { FC, PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { DataSourcePluginMeta, NavModel } from '@grafana/data'; -import { Button, LinkButton, List, PluginSignatureBadge } from '@grafana/ui'; +import { Button, LinkButton, List, PluginSignatureBadge, FilterInput } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import Page from 'app/core/components/Page/Page'; import { StoreState } from 'app/types'; import { addDataSource, loadDataSourcePlugins } from './state/actions'; import { getDataSourcePlugins } from './state/selectors'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { setDataSourceTypeSearchQuery } from './state/reducers'; import { Card } from 'app/core/components/Card/Card'; import { PluginsErrorsInfo } from '../plugins/PluginsErrorsInfo'; diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index a04f16549d0..de7a75543f3 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -6,7 +6,7 @@ import { uniqBy } from 'lodash'; import { RichHistoryQuery, ExploreId } from 'app/types/explore'; // Utils -import { stylesFactory, useTheme, RangeSlider, MultiSelect, Select } from '@grafana/ui'; +import { stylesFactory, useTheme, RangeSlider, MultiSelect, Select, FilterInput } from '@grafana/ui'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { @@ -20,7 +20,6 @@ import { // Components import RichHistoryCard from './RichHistoryCard'; import { sortOrderOptions } from './RichHistory'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { useDebounce } from 'react-use'; export interface Props { diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 3570d0b3d47..9026d55acf6 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -6,14 +6,13 @@ import { uniqBy } from 'lodash'; import { RichHistoryQuery, ExploreId } from 'app/types/explore'; // Utils -import { stylesFactory, useTheme, Select, MultiSelect } from '@grafana/ui'; +import { stylesFactory, useTheme, Select, MultiSelect, FilterInput } from '@grafana/ui'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { filterAndSortQueries, createDatasourcesList, SortOrder } from 'app/core/utils/richHistory'; // Components import RichHistoryCard from './RichHistoryCard'; import { sortOrderOptions } from './RichHistory'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { useDebounce } from 'react-use'; export interface Props { diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx index 77660dc0fb3..0f2b1087640 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx @@ -1,8 +1,7 @@ import React, { useReducer } from 'react'; -import { HorizontalGroup, useStyles2, VerticalGroup } from '@grafana/ui'; +import { HorizontalGroup, useStyles2, VerticalGroup, FilterInput } from '@grafana/ui'; import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data'; import { css } from '@emotion/css'; -import { FilterInput } from '../../../../core/components/FilterInput/FilterInput'; import { SortPicker } from '../../../../core/components/Select/SortPicker'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView'; diff --git a/public/app/features/plugins/admin/components/SearchField.tsx b/public/app/features/plugins/admin/components/SearchField.tsx index 55444db3a52..e09996e76c0 100644 --- a/public/app/features/plugins/admin/components/SearchField.tsx +++ b/public/app/features/plugins/admin/components/SearchField.tsx @@ -1,6 +1,6 @@ +import { FilterInput } from '@grafana/ui'; import React, { useState, useRef } from 'react'; import { useDebounce } from 'react-use'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; interface Props { value?: string; diff --git a/public/app/features/search/components/ManageDashboards.tsx b/public/app/features/search/components/ManageDashboards.tsx index 15c477660ee..bd1d407b966 100644 --- a/public/app/features/search/components/ManageDashboards.tsx +++ b/public/app/features/search/components/ManageDashboards.tsx @@ -1,10 +1,9 @@ import React, { FC, memo, useState } from 'react'; import { css } from '@emotion/css'; -import { stylesFactory, useTheme, Spinner } from '@grafana/ui'; +import { stylesFactory, useTheme, Spinner, FilterInput } from '@grafana/ui'; import { GrafanaTheme } from '@grafana/data'; import { contextSrv } from 'app/core/services/context_srv'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { FolderDTO } from 'app/types'; import { useManageDashboards } from '../hooks/useManageDashboards'; import { SearchLayout } from '../types'; diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 4e5eb1cd83a..078bc4b0c83 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,13 +1,12 @@ import React, { PureComponent } from 'react'; import Page from 'app/core/components/Page/Page'; -import { DeleteButton, LinkButton } from '@grafana/ui'; +import { DeleteButton, LinkButton, FilterInput } from '@grafana/ui'; import { NavModel } from '@grafana/data'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { OrgRole, StoreState, Team } from 'app/types'; import { deleteTeam, loadTeams } from './state/actions'; import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { config } from 'app/core/config'; import { contextSrv, User } from 'app/core/services/context_srv'; import { connectWithCleanUp } from '../../core/components/connectWithCleanUp'; diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 5454998bdea..ef045e624ca 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -6,14 +6,13 @@ import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TeamMember, OrgUser } from 'app/types'; import { addTeamMember } from './state/actions'; import { getSearchMemberQuery, isSignedInUserTeamAdmin } from './state/selectors'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle'; import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import TeamMemberRow from './TeamMemberRow'; import { setSearchMemberQuery } from './state/reducers'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; -import { Button } from '@grafana/ui'; +import { Button, FilterInput } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; function mapStateToProps(state: any) { diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index c1dc64ef806..53bc6ccc08c 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -2,8 +2,7 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { setUsersSearchQuery } from './state/reducers'; import { getInviteesCount, getUsersSearchQuery } from './state/selectors'; -import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; -import { RadioButtonGroup, LinkButton } from '@grafana/ui'; +import { RadioButtonGroup, LinkButton, FilterInput } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types';