Skip to content

Commit

Permalink
adds a search box to the toolbar featureflag list (#6527)
Browse files Browse the repository at this point in the history
* adds a search box to the toolbar featureflag list

* test flakes because react is re-rendering between the get and the click. add an assertion to try and slow cypress down to avoid this

* move filtering toolbar feature flags to a Set and out of CSS

* support initKeaTestLogic() with no args

* fix query-selector-all-deep jest bug

* add simple test case for feature flags logic

* combine selectors

* with more understanding of Fuse

* add simple test for flag filtering

Co-authored-by: Marius Andra <[email protected]>
  • Loading branch information
pauldambra and mariusandra authored Oct 20, 2021
1 parent f84b447 commit a957f0a
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 85 deletions.
2 changes: 1 addition & 1 deletion cypress/integration/invitesMembers.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Invite Signup', () => {
cy.get('[data-attr=invite-teammate-button]').first().click()
// Enter invite the user
cy.get('[data-attr=invite-email-input]').type(`fake+${Math.floor(Math.random() * 10000)}@posthog.com`)
cy.get('[data-attr=invite-team-member-submit]').click()
cy.get('[data-attr=invite-team-member-submit]').should('not.be.disabled').click()

// Log in as invited user
cy.get('[data-attr=invite-link]')
Expand Down
18 changes: 11 additions & 7 deletions frontend/src/test/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export function initKeaTestLogic<L extends Logic = Logic>({
props,
onLogic,
}: {
logic: LogicWrapper<L>
logic?: LogicWrapper<L>
props?: LogicWrapper<L>['props']
onLogic?: (l: BuiltLogic<L>) => any
}): void {
} = {}): void {
let builtLogic: BuiltLogic<L>
let unmount: () => void

Expand All @@ -33,13 +33,17 @@ export function initKeaTestLogic<L extends Logic = Logic>({
;(history as any).pushState = history.push
;(history as any).replaceState = history.replace
initKea({ beforePlugins: [testUtilsPlugin], routerLocation: history.location, routerHistory: history })
builtLogic = logic.build({ ...props })
await onLogic?.(builtLogic)
unmount = builtLogic.mount()
if (logic) {
builtLogic = logic.build({ ...props })
await onLogic?.(builtLogic)
unmount = builtLogic.mount()
}
})

afterEach(async () => {
unmount()
await expectLogic(logic).toFinishAllListeners()
if (logic) {
unmount()
await expectLogic(logic).toFinishAllListeners()
}
})
}
149 changes: 82 additions & 67 deletions frontend/src/toolbar/flags/FeatureFlags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import './featureFlags.scss'
import React from 'react'
import { useActions, useValues } from 'kea'
import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic'
import { Radio, Switch, Row, Typography, List, Button } from 'antd'
import { Radio, Switch, Row, Typography, List, Button, Input } from 'antd'
import { AnimatedCollapsible } from './AnimatedCollapsible'
import { PostHog } from 'posthog-js'
import { toolbarLogic } from '~/toolbar/toolbarLogic'
import { urls } from 'scenes/urls'
import { IconExternalLinkBold } from 'lib/components/icons'

export function FeatureFlags(): JSX.Element {
const { userFlagsWithCalculatedInfo, showLocalFeatureFlagWarning } = useValues(featureFlagsLogic)
const { setOverriddenUserFlag, deleteOverriddenUserFlag, setShowLocalFeatureFlagWarning } =
const { showLocalFeatureFlagWarning, searchTerm, filteredFlags } = useValues(featureFlagsLogic)
const { setOverriddenUserFlag, deleteOverriddenUserFlag, setShowLocalFeatureFlagWarning, setSearchTerm } =
useActions(featureFlagsLogic)
const { apiURL } = useValues(toolbarLogic)

Expand Down Expand Up @@ -40,79 +40,94 @@ export function FeatureFlags(): JSX.Element {
</div>
</div>
) : (
<List
dataSource={userFlagsWithCalculatedInfo}
renderItem={({
feature_flag,
value_for_user_without_override,
override,
hasVariants,
currentValue,
}) => {
return (
<div className="feature-flag-row">
<Row
className={
override ? 'feature-flag-row-header overridden' : 'feature-flag-row-header'
}
>
<Typography.Text ellipsis className="feature-flag-title">
{feature_flag.key}
</Typography.Text>
<a
className="feature-flag-external-link"
href={`${apiURL}${
feature_flag.id ? urls.featureFlag(feature_flag.id) : urls.featureFlags()
}`}
target="_blank"
rel="noopener noreferrer"
>
<IconExternalLinkBold />
</a>
<Switch
checked={!!currentValue}
onChange={(checked) => {
const newValue =
hasVariants && checked
? (feature_flag.filters?.multivariate?.variants[0]?.key as string)
: checked
if (newValue === value_for_user_without_override && override) {
deleteOverriddenUserFlag(override.id as number)
} else {
setOverriddenUserFlag(feature_flag.id as number, newValue)
}
}}
/>
</Row>

<AnimatedCollapsible collapsed={!hasVariants || !currentValue}>
<>
<Input.Search
allowClear
autoFocus
placeholder="Search"
value={searchTerm}
className={'feature-flag-row'}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<List
dataSource={filteredFlags}
renderItem={({
feature_flag,
value_for_user_without_override,
override,
hasVariants,
currentValue,
}) => {
return (
<div className={'feature-flag-row'}>
<Row
className={override ? 'variant-radio-group overridden' : 'variant-radio-group'}
className={
override ? 'feature-flag-row-header overridden' : 'feature-flag-row-header'
}
>
<Radio.Group
disabled={!currentValue}
value={currentValue}
onChange={(event) => {
const newValue = event.target.value
<Typography.Text ellipsis className="feature-flag-title">
{feature_flag.key}
</Typography.Text>
<a
className="feature-flag-external-link"
href={`${apiURL}${
feature_flag.id
? urls.featureFlag(feature_flag.id)
: urls.featureFlags()
}`}
target="_blank"
rel="noopener noreferrer"
>
<IconExternalLinkBold />
</a>
<Switch
checked={!!currentValue}
onChange={(checked) => {
const newValue =
hasVariants && checked
? (feature_flag.filters?.multivariate?.variants[0]
?.key as string)
: checked
if (newValue === value_for_user_without_override && override) {
deleteOverriddenUserFlag(override.id as number)
} else {
setOverriddenUserFlag(feature_flag.id as number, newValue)
}
}}
>
{feature_flag.filters?.multivariate?.variants.map((variant) => (
<Radio key={variant.key} value={variant.key}>
{`${variant.key} - ${variant.name} (${variant.rollout_percentage}%)`}
</Radio>
))}
</Radio.Group>
/>
</Row>
</AnimatedCollapsible>
</div>
)
}}
/>

<AnimatedCollapsible collapsed={!hasVariants || !currentValue}>
<Row
className={
override ? 'variant-radio-group overridden' : 'variant-radio-group'
}
>
<Radio.Group
disabled={!currentValue}
value={currentValue}
onChange={(event) => {
const newValue = event.target.value
if (newValue === value_for_user_without_override && override) {
deleteOverriddenUserFlag(override.id as number)
} else {
setOverriddenUserFlag(feature_flag.id as number, newValue)
}
}}
>
{feature_flag.filters?.multivariate?.variants.map((variant) => (
<Radio key={variant.key} value={variant.key}>
{`${variant.key} - ${variant.name} (${variant.rollout_percentage}%)`}
</Radio>
))}
</Radio.Group>
</Row>
</AnimatedCollapsible>
</div>
)
}}
/>
</>
)}
</div>
)
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/toolbar/flags/featureFlagsLogic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic'
import { toolbarLogic } from '~/toolbar/toolbarLogic'
import { CombinedFeatureFlagAndOverrideType } from '~/types'

const featureFlags = [
{ feature_flag: { name: 'flag 1' } },
{ feature_flag: { name: 'flag 2' } },
] as CombinedFeatureFlagAndOverrideType[]

const featureFlagsWithExtraInfo = [
{ currentValue: undefined, hasVariants: false, feature_flag: { name: 'flag 1' } },
{ currentValue: undefined, hasVariants: false, feature_flag: { name: 'flag 2' } },
]

global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(featureFlags),
} as any as Response)
)

describe('feature flags logic', () => {
let logic: ReturnType<typeof featureFlagsLogic.build>

initKeaTestLogic()

beforeEach(() => {
toolbarLogic({ apiURL: 'http://localhost' }).mount()
logic = featureFlagsLogic()
logic.mount()
})

it('has expected defaults', () => {
expectLogic(logic).toMatchValues({
userFlags: featureFlags,
searchTerm: '',
filteredFlags: featureFlagsWithExtraInfo,
})
})

it('can filter the flags', async () => {
await expectLogic(logic, () => {
logic.actions.setSearchTerm('2')
}).toMatchValues({
filteredFlags: [{ currentValue: undefined, hasVariants: false, feature_flag: { name: 'flag 2' } }],
})
})
})
27 changes: 21 additions & 6 deletions frontend/src/toolbar/flags/featureFlagsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { featureFlagsLogicType } from './featureFlagsLogicType'
import { PostHog } from 'posthog-js'
import { toolbarFetch } from '~/toolbar/utils'
import { toolbarLogic } from '~/toolbar/toolbarLogic'
import Fuse from 'fuse.js'

export const featureFlagsLogic = kea<featureFlagsLogicType>({
actions: {
getUserFlags: true,
setOverriddenUserFlag: (flagId: number, overrideValue: string | boolean) => ({ flagId, overrideValue }),
deleteOverriddenUserFlag: (overrideId: number) => ({ overrideId }),
setShowLocalFeatureFlagWarning: (showWarning: boolean) => ({ showWarning }),
setSearchTerm: (searchTerm: string) => ({ searchTerm }),
},

loaders: ({ values }) => ({
Expand All @@ -23,8 +25,7 @@ export const featureFlagsLogic = kea<featureFlagsLogicType>({
if (!response.ok) {
return []
}
const results = await response.json()
return results
return await response.json()
},
setOverriddenUserFlag: async ({ flagId, overrideValue }, breakpoint) => {
const response = await toolbarFetch(
Expand Down Expand Up @@ -67,6 +68,12 @@ export const featureFlagsLogic = kea<featureFlagsLogicType>({
],
}),
reducers: {
searchTerm: [
'',
{
setSearchTerm: (_, { searchTerm }) => searchTerm,
},
],
showLocalFeatureFlagWarning: [
false,
{
Expand All @@ -92,12 +99,20 @@ export const featureFlagsLogic = kea<featureFlagsLogicType>({
})
},
],
countFlagsOverridden: [
(s) => [s.userFlags],
(userFlags) => {
return userFlags.filter((flag) => !!flag.override).length
filteredFlags: [
(s) => [s.searchTerm, s.userFlagsWithCalculatedInfo],
(searchTerm, userFlagsWithCalculatedInfo) => {
return searchTerm
? new Fuse(userFlagsWithCalculatedInfo, {
threshold: 0.3,
keys: ['feature_flag.name'],
})
.search(searchTerm)
.map(({ item }) => item)
: userFlagsWithCalculatedInfo
},
],
countFlagsOverridden: [(s) => [s.userFlags], (userFlags) => userFlags.filter((flag) => !!flag.override).length],
},
events: ({ actions }) => ({
afterMount: () => {
Expand Down
5 changes: 1 addition & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,7 @@ export default {
// transform: undefined,

// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
transformIgnorePatterns: ['node_modules/(?!(query-selector-shadow-dom)/)'],

// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
Expand Down

0 comments on commit a957f0a

Please sign in to comment.