Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion redisinsight/ui/src/components/config/Config.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { cloneDeep } from 'lodash'
import { BuildType } from 'uiSrc/constants/env'
import { localStorageService } from 'uiSrc/services'
import { setFeaturesToHighlight, setOnboarding } from 'uiSrc/slices/app/features'
import { getFeatureFlags, setFeaturesToHighlight, setOnboarding } from 'uiSrc/slices/app/features'
import { getNotifications } from 'uiSrc/slices/app/notifications'
import { render, mockedStore, cleanup, MOCKED_HIGHLIGHTING_FEATURES } from 'uiSrc/utils/test-utils'

Expand Down Expand Up @@ -63,6 +63,7 @@ describe('Config', () => {
getNotifications(),
getWBGuides(),
getWBTutorials(),
getFeatureFlags(),
getUserConfigSettings(),
]
expect(store.getActions()).toEqual([...afterRenderActions])
Expand Down Expand Up @@ -95,6 +96,7 @@ describe('Config', () => {
getNotifications(),
getWBGuides(),
getWBTutorials(),
getFeatureFlags(),
getUserConfigSettings(),
setSettingsPopupState(true),
]
Expand Down
4 changes: 3 additions & 1 deletion redisinsight/ui/src/components/config/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BrowserStorageItem } from 'uiSrc/constants'
import { BuildType } from 'uiSrc/constants/env'
import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting'
import { localStorageService } from 'uiSrc/services'
import { setFeaturesToHighlight, setOnboarding } from 'uiSrc/slices/app/features'
import { fetchFeatureFlags, setFeaturesToHighlight, setOnboarding } from 'uiSrc/slices/app/features'
import { fetchNotificationsAction } from 'uiSrc/slices/app/notifications'

import {
Expand Down Expand Up @@ -53,6 +53,8 @@ const Config = () => {
dispatch(fetchGuides())
dispatch(fetchTutorials())

dispatch(fetchFeatureFlags())

// fetch config settings, after that take spec
if (pathname !== SETTINGS_PAGE_PATH) {
dispatch(fetchUserConfigSettings(() => dispatch(fetchUserSettingsSpec())))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import { render, screen } from 'uiSrc/utils/test-utils'

import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'
import { FeatureFlags } from 'uiSrc/constants'

import FeatureFlagComponent from './FeatureFlagComponent'

jest.mock('uiSrc/slices/app/features', () => ({
...jest.requireActual('uiSrc/slices/app/features'),
appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({
name: {
flag: false
}
}),
}))

const InnerComponent = () => (<span data-testid="inner-component" />)
describe('FeatureFlagComponent', () => {
it('should not render component by default', () => {
render(
<FeatureFlagComponent name={'name' as FeatureFlags}>
<InnerComponent />
</FeatureFlagComponent>
)

expect(screen.queryByTestId('inner-component')).not.toBeInTheDocument()
})

it('should render component', () => {
(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({
name: {
flag: true
}
})

render(
<FeatureFlagComponent name={'name' as FeatureFlags}>
<InnerComponent />
</FeatureFlagComponent>
)

expect(screen.getByTestId('inner-component')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import { useSelector } from 'react-redux'
import { FeatureFlags } from 'uiSrc/constants'
import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'

export interface Props {
name: FeatureFlags
children: React.ReactElement
}

const FeatureFlagComponent = (props: Props) => {
const { children, name } = props
const { [name]: feature } = useSelector(appFeatureFlagsFeaturesSelector)
const { flag, variant } = feature ?? { flag: false }

return flag ? React.cloneElement(children, { variant }) : null
}

export default FeatureFlagComponent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import FeatureFlagComponent from './FeatureFlagComponent'

export default FeatureFlagComponent
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { useDispatch, useSelector } from 'react-redux'
import { io, Socket } from 'socket.io-client'

import { remove } from 'lodash'
import { SocketEvent } from 'uiSrc/constants'
import { SocketEvent, SocketFeaturesEvent } from 'uiSrc/constants'
import { NotificationEvent } from 'uiSrc/constants/notifications'
import { setNewNotificationAction } from 'uiSrc/slices/app/notifications'
import { setIsConnected } from 'uiSrc/slices/app/socket-connection'
import { getBaseApiUrl, Nullable } from 'uiSrc/utils'
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
import { setTotalUnread } from 'uiSrc/slices/recommendations/recommendations'
import { RecommendationsSocketEvents } from 'uiSrc/constants/recommendations'
import { getFeatureFlagsSuccess } from 'uiSrc/slices/app/features'

const CommonAppSubscription = () => {
const { id: instanceId } = useSelector(connectedInstanceSelector)
Expand Down Expand Up @@ -38,6 +39,13 @@ const CommonAppSubscription = () => {
dispatch(setNewNotificationAction(data))
})

socketRef.current.on(SocketFeaturesEvent.Features, (data) => {
dispatch(getFeatureFlagsSuccess(data))

// or
// dispatch(fetchFeatureFlags())
})

// Catch disconnect
socketRef.current?.on(SocketEvent.Disconnect, () => {
unSubscribeFromAllRecommendations()
Expand Down
2 changes: 2 additions & 0 deletions redisinsight/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import CodeBlock from './code-block'
import ShowChildByCondition from './show-child-by-condition'
import RecommendationVoting from './recommendation-voting'
import RecommendationCopyComponent from './recommendation-copy-component'
import FeatureFlagComponent from './feature-flag-component'

export {
NavigationMenu,
Expand Down Expand Up @@ -57,4 +58,5 @@ export {
ShowChildByCondition,
RecommendationVoting,
RecommendationCopyComponent,
FeatureFlagComponent,
}
2 changes: 2 additions & 0 deletions redisinsight/ui/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ enum ApiEndpoints {
REDISEARCH = 'redisearch',
REDISEARCH_SEARCH = 'redisearch/search',
HISTORY = 'history',

FEATURES = 'features',
}

export const DEFAULT_SEARCH_MATCH = '*'
Expand Down
3 changes: 3 additions & 0 deletions redisinsight/ui/src/constants/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum FeatureFlags {
liveRecommendations = 'liveRecommendations'
}
1 change: 1 addition & 0 deletions redisinsight/ui/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export * from './durationUnits'
export * from './streamViews'
export * from './bulkActions'
export * from './workbench'
export * from './featureFlags'
export { ApiEndpoints, BrowserStorageItem, ApiStatusCode, apiErrors }
4 changes: 4 additions & 0 deletions redisinsight/ui/src/constants/socketEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export enum SocketEvent {
Disconnect = 'disconnect',
ConnectionError = 'connect_error',
}

export enum SocketFeaturesEvent {
Features = 'features'
}
27 changes: 26 additions & 1 deletion redisinsight/ui/src/pages/instance/InstancePage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { instance, mock } from 'ts-mockito'
import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'
import { BrowserStorageItem } from 'uiSrc/constants'
import { localStorageService } from 'uiSrc/services'
import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'
import InstancePage, { getDefaultSizes, Props } from './InstancePage'

const mockedProps = mock<Props>()
Expand All @@ -17,6 +18,15 @@ jest.mock('uiSrc/services', () => ({
},
}))

jest.mock('uiSrc/slices/app/features', () => ({
...jest.requireActual('uiSrc/slices/app/features'),
appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({
liveRecommendations: {
flag: false
}
}),
}))

let store: typeof mockedStore
beforeEach(() => {
cleanup()
Expand Down Expand Up @@ -50,7 +60,22 @@ describe('InstancePage', () => {
expect(queryByTestId('expand-cli')).toBeInTheDocument()
})

it('should render with LiveTimeRecommendations Component', () => {
it('should not render LiveTimeRecommendations Component by default', () => {
const { queryByTestId } = render(
<BrowserRouter>
<InstancePage {...instance(mockedProps)} />
</BrowserRouter>
)

expect(queryByTestId('recommendations-trigger')).not.toBeInTheDocument()
})

it('should render LiveTimeRecommendations Component with feature flag', () => {
(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({
liveRecommendations: {
flag: true
}
})
const { queryByTestId } = render(
<BrowserRouter>
<InstancePage {...instance(mockedProps)} />
Expand Down
15 changes: 11 additions & 4 deletions redisinsight/ui/src/pages/instance/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ import cx from 'classnames'

import { setInitialAnalyticsSettings } from 'uiSrc/slices/analytics/settings'
import {
fetchConnectedInstanceAction, fetchConnectedInstanceInfoAction,
fetchConnectedInstanceAction,
fetchConnectedInstanceInfoAction,
fetchInstancesAction,
getDatabaseConfigInfoAction,
instancesSelector,
} from 'uiSrc/slices/instances/instances'
import { fetchRecommendationsAction, resetRecommendationsHighlighting } from 'uiSrc/slices/recommendations/recommendations'
import {
fetchRecommendationsAction,
resetRecommendationsHighlighting
} from 'uiSrc/slices/recommendations/recommendations'
import {
appContextSelector,
setAppContextConnectedInstanceId,
setAppContextInitialState,
setDbConfig,
} from 'uiSrc/slices/app/context'
import { resetPatternKeysData } from 'uiSrc/slices/browser/keys'
import { BrowserStorageItem } from 'uiSrc/constants'
import { BrowserStorageItem, FeatureFlags } from 'uiSrc/constants'
import { localStorageService } from 'uiSrc/services'
import { FeatureFlagComponent } from 'uiSrc/components'
import { resetOutput } from 'uiSrc/slices/cli/cli-output'
import { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'
import BottomGroupComponents from 'uiSrc/components/bottom-group-components/BottomGroupComponents'
Expand Down Expand Up @@ -123,7 +128,9 @@ const InstancePage = ({ routes = [] }: Props) => {

return (
<>
<LiveTimeRecommendations />
<FeatureFlagComponent name={FeatureFlags.liveRecommendations}>
<LiveTimeRecommendations />
</FeatureFlagComponent>
<EuiResizableContainer
direction="vertical"
style={{ height: '100%' }}
Expand Down
56 changes: 51 additions & 5 deletions redisinsight/ui/src/slices/app/features.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { remove } from 'lodash'
import { BrowserStorageItem } from 'uiSrc/constants'
import { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants'
import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting'
import { localStorageService } from 'uiSrc/services'
import { apiService, localStorageService } from 'uiSrc/services'
import { StateAppFeatures } from 'uiSrc/slices/interfaces'
import { AppDispatch, RootState } from 'uiSrc/slices/store'
import { getPagesForFeatures } from 'uiSrc/utils/highlighting'
import { OnboardingSteps } from 'uiSrc/constants/onboarding'
import { Maybe } from 'uiSrc/utils'
import { isStatusSuccessful, Maybe } from 'uiSrc/utils'

export const initialState: StateAppFeatures = {
highlighting: {
Expand All @@ -19,6 +19,14 @@ export const initialState: StateAppFeatures = {
currentStep: 0,
totalSteps: 0,
isActive: false,
},
featureFlags: {
loading: false,
features: {
liveRecommendations: {
flag: false
}
}
}
}

Expand Down Expand Up @@ -81,7 +89,17 @@ const appFeaturesSlice = createSlice({
}

localStorageService.set(BrowserStorageItem.onboardingStep, step)
}
},
getFeatureFlags: (state) => {
state.featureFlags.loading = true
},
getFeatureFlagsSuccess: (state, { payload }) => {
state.featureFlags.loading = false
state.featureFlags.features = payload.features
},
getFeatureFlagsFailure: (state) => {
state.featureFlags.loading = false
},
}
})

Expand All @@ -92,14 +110,19 @@ export const {
skipOnboarding,
setOnboardPrevStep,
setOnboardNextStep,
setOnboarding
setOnboarding,
getFeatureFlags,
getFeatureFlagsSuccess,
getFeatureFlagsFailure
} = appFeaturesSlice.actions

export const appFeatureSelector = (state: RootState) => state.app.features
export const appFeatureHighlightingSelector = (state: RootState) => state.app.features.highlighting
export const appFeaturePagesHighlightingSelector = (state: RootState) => state.app.features.highlighting.pages

export const appFeatureOnboardingSelector = (state: RootState) => state.app.features.onboarding
export const appFeatureFlagsSelector = (state: RootState) => state.app.features.featureFlags
export const appFeatureFlagsFeaturesSelector = (state: RootState) => state.app.features.featureFlags.features

export default appFeaturesSlice.reducer

Expand All @@ -113,3 +136,26 @@ export function incrementOnboardStepAction(step: OnboardingSteps, skipCount = 0,
}
}
}

export function fetchFeatureFlags(
onSuccessAction?: (data: any) => void,
onFailAction?: () => void
) {
return async (dispatch: AppDispatch) => {
dispatch(getFeatureFlags())

try {
const { data, status } = await apiService.get(
ApiEndpoints.FEATURES
)

if (isStatusSuccessful(status)) {
dispatch(getFeatureFlagsSuccess(data))
onSuccessAction?.(data)
}
} catch (error) {
dispatch(getFeatureFlagsFailure())
onFailAction?.()
}
}
}
Loading