diff --git a/frontend/common/useSearchThrottle.ts b/frontend/common/useSearchThrottle.ts index 3004c1ff159e..5f3d62c3666b 100644 --- a/frontend/common/useSearchThrottle.ts +++ b/frontend/common/useSearchThrottle.ts @@ -1,10 +1,11 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import useThrottle from './useThrottle' export default function useSearchThrottle( defaultValue?: string, cb?: () => void, ) { + const firstRender = useRef(true) const [search, setSearch] = useState(defaultValue || '') const [searchInput, setSearchInput] = useState(search) const searchItems = useThrottle((search: string) => { @@ -12,6 +13,11 @@ export default function useSearchThrottle( cb?.() }, 100) useEffect(() => { + if (firstRender.current && !searchInput) { + firstRender.current = false + return + } + firstRender.current = false searchItems(searchInput) //eslint-disable-next-line }, [searchInput]); diff --git a/frontend/web/components/pages/FeaturesPage.js b/frontend/web/components/pages/FeaturesPage.js index e387ed246234..0b658ee63ee1 100644 --- a/frontend/web/components/pages/FeaturesPage.js +++ b/frontend/web/components/pages/FeaturesPage.js @@ -19,8 +19,8 @@ import TableTagFilter from 'components/tables/TableTagFilter' import { setViewMode } from 'common/useViewMode' import TableFilterOptions from 'components/tables/TableFilterOptions' import { getViewMode } from 'common/useViewMode' -import { TagStrategy } from 'common/types/responses' import EnvironmentDocumentCodeHelp from 'components/EnvironmentDocumentCodeHelp' +import TableValueFilter from 'components/tables/TableValueFilter' const FeaturesPage = class extends Component { static displayName = 'FeaturesPage' @@ -32,12 +32,14 @@ const FeaturesPage = class extends Component { constructor(props, context) { super(props, context) this.state = { + is_enabled: null, loadedOnce: false, search: null, showArchived: false, sort: { label: 'Name', sortBy: 'name', sortOrder: 'asc' }, tag_strategy: 'INTERSECTION', tags: [], + value_search: null, } ES6Component(this) getTags(getStore(), { @@ -106,11 +108,14 @@ const FeaturesPage = class extends Component { getFilter = () => ({ is_archived: this.state.showArchived, + is_enabled: + this.state.is_enabled === null ? undefined : this.state.is_enabled, tag_strategy: this.state.tag_strategy, tags: !this.state.tags || !this.state.tags.length ? undefined : this.state.tags.join(','), + value_search: this.state.value_search ? this.state.value_search : undefined, }) onSave = (isCreate) => { @@ -165,6 +170,9 @@ const FeaturesPage = class extends Component { render() { const { environmentId, projectId } = this.props.match.params const readOnly = Utils.getFlagsmithHasFeature('read_only_mode') + const enabledStateFilter = Utils.getFlagsmithHasFeature( + 'feature_enabled_state_filter', + ) const environment = ProjectStore.getEnvironment(environmentId) return (
+ {enabledStateFilter && ( + { + this.setState( + { + is_enabled: enabled, + value_search: valueSearch, + }, + this.filter, + ) + }} + /> + )} { const nullFalseyA = @@ -46,20 +47,25 @@ const UserPage = class extends Component { constructor(props, context) { super(props, context) this.state = { + is_enabled: null, preselect: Utils.fromParam().flag, showArchived: false, tag_strategy: 'INTERSECTION', tags: [], + value_search: null, } } getFilter = () => ({ is_archived: this.state.showArchived, + is_enabled: + this.state.is_enabled === null ? undefined : this.state.is_enabled, tag_strategy: this.state.tag_strategy, tags: !this.state.tags || !this.state.tags.length ? undefined : this.state.tags.join(','), + value_search: this.state.value_search ? this.state.value_search : undefined, }) componentDidMount() { @@ -262,6 +268,9 @@ const UserPage = class extends Component { render() { const { actualFlags } = this.state const { environmentId, projectId } = this.props.match.params + const enabledStateFilter = Utils.getFlagsmithHasFeature( + 'feature_enabled_state_filter', + ) const preventAddTrait = !AccountStore.getOrganisation().persist_trait_data return ( @@ -483,6 +492,29 @@ const UserPage = class extends Component { ) }} /> + {enabledStateFilter && ( + { + this.setState( + { + is_enabled: enabled, + value_search: valueSearch, + }, + this.filter, + ) + }} + /> + )} void | Promise + onChange: (value: any) => void | Promise value: string } diff --git a/frontend/web/components/tables/TableValueFilter.tsx b/frontend/web/components/tables/TableValueFilter.tsx new file mode 100644 index 000000000000..de18170341ec --- /dev/null +++ b/frontend/web/components/tables/TableValueFilter.tsx @@ -0,0 +1,144 @@ +import React, { FC, useEffect, useMemo, useRef, useState } from 'react' +import TableFilter from './TableFilter' +import Input from 'components/base/forms/Input' +import Utils from 'common/utils/utils' +import { useGetTagsQuery } from 'common/services/useTag' +import Tag from 'components/tags/Tag' +import TableFilterItem from './TableFilterItem' +import Constants from 'common/constants' +import { TagStrategy } from 'common/types/responses' +import { AsyncStorage } from 'polyfill-react-native' +import InputGroup from 'components/base/forms/InputGroup' +import useSearchThrottle from 'common/useSearchThrottle' + +type TableFilterType = { + value: { + enabled: boolean | null + valueSearch: string | null + } + enabled: boolean | null + isLoading: boolean + onChange: (value: TableFilterType['value']) => void + className?: string + useLocalStorage?: boolean + projectId: string +} + +const enabledOptions = [ + { + label: 'Any', + value: null, + }, + { + label: 'Enabled', + value: true, + }, + { + label: 'Disabled', + value: false, + }, +] +const TableTagFilter: FC = ({ + className, + isLoading, + onChange, + projectId, + useLocalStorage, + value, +}) => { + const checkedLocalStorage = useRef(false) + const { searchInput, setSearchInput } = useSearchThrottle( + value.valueSearch || '', + () => { + onChange({ + enabled: value.enabled, + valueSearch: searchInput, + }) + }, + ) + useEffect(() => { + if (checkedLocalStorage.current && useLocalStorage) { + AsyncStorage.setItem(`${projectId}-value`, JSON.stringify(value)) + } + }, [value, projectId, useLocalStorage]) + useEffect(() => { + const checkLocalStorage = async function () { + if (useLocalStorage && !checkedLocalStorage.current) { + checkedLocalStorage.current = true + const storedValue = await AsyncStorage.getItem(`${projectId}-value`) + if (storedValue) { + try { + const storedValueObject = JSON.parse(storedValue) + onChange(storedValueObject) + } catch (e) {} + } + } + } + checkLocalStorage() + }, [useLocalStorage, projectId]) + return ( +
+ + Value{' '} + {(value.enabled !== null || !!value.valueSearch) && ( + {1} + )} + + } + > +
+
+ ({ + ...base, + '&:hover': { borderColor: '$bt-brand-secondary' }, + border: '1px solid $bt-brand-secondary', + height: 18, + }), + }} + onChange={(e: (typeof enabledOptions)[number]) => { + onChange({ + enabled: e.value, + valueSearch: value.valueSearch, + }) + }} + value={enabledOptions.find((v) => v.value === value.enabled)} + options={enabledOptions} + /> + } + /> + { + setSearchInput(Utils.safeParseEventValue(e)) + }} + tooltip='This will filter your features based on the remote configuration value you define.' + inputProps={{ style: { height: 60 } }} + value={searchInput} + textarea + rows={2} + className='full-width mt-2' + type='text' + size='xSmall' + placeholder='Enter a feature value' + search + /> +
+
+
+
+ ) +} + +export default TableTagFilter