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