diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e4071513c..f66f169018 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,10 @@ on: description: Enable SSH Debug type: boolean + enterprise: + description: Enterprise build + type: boolean + jobs: build-linux: if: contains(inputs.target, 'linux') || inputs.target == 'all' @@ -27,6 +31,7 @@ jobs: environment: ${{ inputs.environment }} target: ${{ inputs.target }} debug: ${{ inputs.debug }} + enterprise: ${{ inputs.enterprise }} build-macos: if: contains(inputs.target, 'macos') || inputs.target == 'all' @@ -36,6 +41,7 @@ jobs: environment: ${{ inputs.environment }} target: ${{ inputs.target }} debug: ${{ inputs.debug }} + enterprise: ${{ inputs.enterprise }} build-windows: if: contains(inputs.target, 'windows') || inputs.target == 'all' @@ -44,6 +50,7 @@ jobs: with: environment: ${{ inputs.environment }} debug: ${{ inputs.debug }} + enterprise: ${{ inputs.enterprise }} build-docker: if: contains(inputs.target, 'docker') || inputs.target == 'all' @@ -52,3 +59,4 @@ jobs: with: environment: ${{ inputs.environment }} debug: ${{ inputs.debug }} + enterprise: ${{ inputs.enterprise }} diff --git a/.github/workflows/manual-build-enterprise.yml b/.github/workflows/manual-build-enterprise.yml new file mode 100644 index 0000000000..ef087231fa --- /dev/null +++ b/.github/workflows/manual-build-enterprise.yml @@ -0,0 +1,104 @@ +name: 🚀 Manual build - Enterprise + +on: + # Manual trigger build + # No multi-select + # https://github.com/actions/runner/issues/2076 + workflow_dispatch: + inputs: + build_docker: + description: Build Docker + type: boolean + required: false + + build_windows_x64: + description: Build Windows x64 + type: boolean + required: false + + build_macos_x64: + description: Build macOS x64 + type: boolean + required: false + + build_macos_arm64: + description: Build macOS arm64 + type: boolean + required: false + + build_linux_appimage_x64: + description: Build Linux AppImage x64 + type: boolean + required: false + + build_linux_deb_x64: + description: Build Linux deb x64 + type: boolean + required: false + + environment: + description: Environment to run build + type: environment + default: 'development' + required: false + + debug: + description: Enable SSH Debug + type: boolean + +# Cancel a previous same workflow +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + get-selected: + runs-on: ubuntu-latest + outputs: # Set this to consume the output on other job + selected: ${{ steps.get-selected.outputs.selected}} + steps: + - uses: actions/checkout@v4 + + - id: get-selected + uses: joao-zanutto/get-selected@v1.1.1 + with: + format: 'list' + + - name: echo selected targets + run: echo ${{ steps.get-selected.outputs.selected }} + + manual-build: + needs: get-selected + uses: ./.github/workflows/build.yml + secrets: inherit + with: + target: ${{ needs.get-selected.outputs.selected }} + debug: ${{ inputs.debug }} + environment: ${{ inputs.environment }} + enterprise: true + + aws-upload: + uses: ./.github/workflows/aws-upload-dev.yml + secrets: inherit + needs: [manual-build] + if: always() + + clean: + uses: ./.github/workflows/clean-deployments.yml + # secrets: inherit + needs: [aws-upload] + if: always() + + # Remove artifacts from github actions + remove-artifacts: + name: Remove artifacts + needs: [aws-upload] + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Remove all artifacts + uses: ./.github/actions/remove-artifacts + + diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml index 7ae3d5a2cf..6d31bf9f71 100644 --- a/.github/workflows/manual-build.yml +++ b/.github/workflows/manual-build.yml @@ -85,6 +85,7 @@ jobs: target: ${{ needs.get-selected.outputs.selected }} debug: ${{ inputs.debug }} environment: ${{ inputs.environment }} + enterprise: false aws-upload: uses: ./.github/workflows/aws-upload-dev.yml diff --git a/.github/workflows/pipeline-build-docker.yml b/.github/workflows/pipeline-build-docker.yml index d40e948b29..9ccf5b2872 100644 --- a/.github/workflows/pipeline-build-docker.yml +++ b/.github/workflows/pipeline-build-docker.yml @@ -17,6 +17,10 @@ on: default: false type: boolean + enterprise: + description: Enterprise build + type: boolean + jobs: build: name: Build docker @@ -130,5 +134,6 @@ jobs: RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }} RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK }} + RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == 'false' }} diff --git a/.github/workflows/pipeline-build-linux.yml b/.github/workflows/pipeline-build-linux.yml index d82d2e2d56..0928f02784 100644 --- a/.github/workflows/pipeline-build-linux.yml +++ b/.github/workflows/pipeline-build-linux.yml @@ -20,6 +20,10 @@ on: default: false type: boolean + enterprise: + description: Enterprise build + type: boolean + jobs: build: name: Build linux @@ -115,3 +119,4 @@ jobs: RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }} RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK }} + RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == 'false' }} diff --git a/.github/workflows/pipeline-build-macos.yml b/.github/workflows/pipeline-build-macos.yml index eafc375ed4..cb1e4757e5 100644 --- a/.github/workflows/pipeline-build-macos.yml +++ b/.github/workflows/pipeline-build-macos.yml @@ -19,6 +19,10 @@ on: default: false type: boolean + enterprise: + description: Enterprise build + type: boolean + jobs: build: name: Build macos @@ -139,3 +143,4 @@ jobs: RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }} RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK }} + RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == 'false' }} diff --git a/.github/workflows/pipeline-build-windows.yml b/.github/workflows/pipeline-build-windows.yml index e708e1dc15..d3acfc4a98 100644 --- a/.github/workflows/pipeline-build-windows.yml +++ b/.github/workflows/pipeline-build-windows.yml @@ -12,6 +12,10 @@ on: default: false type: boolean + enterprise: + description: Enterprise build + type: boolean + jobs: build: name: Build windows @@ -84,3 +88,4 @@ jobs: RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }} RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK }} + RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == 'false' }} diff --git a/redisinsight/ui/src/components/config/Config.spec.tsx b/redisinsight/ui/src/components/config/Config.spec.tsx index dfc679e870..b94ce90dba 100644 --- a/redisinsight/ui/src/components/config/Config.spec.tsx +++ b/redisinsight/ui/src/components/config/Config.spec.tsx @@ -118,7 +118,7 @@ describe('Config', () => { await waitFor(() => expect(store.getActions()).not.toContainEqual(getUserSettingsSpec())) }) - it('should render expected actions when envDependant feature is off', () => { + it('should render expected actions when envDependent feature is off', () => { const initialStoreState = set( cloneDeep(initialStateDefault), `app.features.featureFlags.features.${FeatureFlags.envDependent}`, diff --git a/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.spec.tsx b/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.spec.tsx index 44da7ae952..d675ab4068 100644 --- a/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.spec.tsx +++ b/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.spec.tsx @@ -10,57 +10,159 @@ jest.mock('uiSrc/slices/app/features', () => ({ ...jest.requireActual('uiSrc/slices/app/features'), appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({ name: { - flag: false - } + flag: false, + }, + otherName: { + flag: false, + }, }), })) -const InnerComponent = () => () -const OtherwiseComponent = () => () +const InnerComponent = () => +const OtherwiseComponent = () => describe('FeatureFlagComponent', () => { - it('should not render component by default', () => { - render( - - - - ) - - expect(screen.queryByTestId('inner-component')).not.toBeInTheDocument() - }) + describe('Single feature', () => { + it('should not render component by default', () => { + render( + + + , + ) + + expect(screen.queryByTestId('inner-component')).not.toBeInTheDocument() + }) - it('should render component', () => { - (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ - name: { - flag: true - } + it('should render component', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + name: { + flag: true, + }, + }) + + render( + + + , + ) + + expect(screen.getByTestId('inner-component')).toBeInTheDocument() }) - render( - - - - ) + it('should render otherwise component if the feature flag not enabled', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + name: { + flag: false, + }, + }) + + const { queryByTestId } = render( + } + > + + , + ) - expect(screen.getByTestId('inner-component')).toBeInTheDocument() + expect(queryByTestId('inner-component')).not.toBeInTheDocument() + expect(queryByTestId('otherwise-component')).toBeInTheDocument() + }) }) - it('should render otherwise component if the feature flag not enabled', () => { - (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ - name: { - flag: false - } + describe('Multiple features', () => { + it('should not render component if any feature flag is disabled', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + name: { + flag: true, + }, + otherName: { + flag: false, + }, + }) + + const { queryByTestId } = render( + + + , + ) + + expect(queryByTestId('inner-component')).not.toBeInTheDocument() + }) + + it('should use enabledByDefault=true for unmatched features', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({}) + + const { queryByTestId } = render( + + + , + ) + + expect(queryByTestId('inner-component')).toBeInTheDocument() + }) + + it('should use enabledByDefault=false for unmatched features', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({}) + + const { queryByTestId } = render( + + + , + ) + + expect(queryByTestId('inner-component')).not.toBeInTheDocument() + }) + + it('should render component if all feature flags are enabled', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + name: { + flag: true, + }, + otherName: { + flag: true, + }, + }) + + const { queryByTestId } = render( + + + , + ) + + expect(queryByTestId('inner-component')).toBeInTheDocument() }) - const { queryByTestId } = render( - } - > - - - ) - - expect(queryByTestId('inner-component')).not.toBeInTheDocument() - expect(queryByTestId('otherwise-component')).toBeInTheDocument() + it('should render otherwise component if any feature flag is not enabled', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + name: { + flag: true, + }, + otherName: { + flag: false, + }, + }) + + const { queryByTestId } = render( + } + > + + , + ) + + expect(queryByTestId('inner-component')).not.toBeInTheDocument() + expect(queryByTestId('otherwise-component')).toBeInTheDocument() + }) }) }) diff --git a/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.tsx b/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.tsx index f5eebca80f..8b0d4aedd7 100644 --- a/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.tsx +++ b/redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.tsx @@ -5,7 +5,7 @@ import { FeatureFlags } from 'uiSrc/constants/featureFlags' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' export interface Props { - name: FeatureFlags + name: FeatureFlags | FeatureFlags[] children?: JSX.Element | JSX.Element[] otherwise?: React.ReactElement enabledByDefault?: boolean @@ -13,10 +13,14 @@ export interface Props { const FeatureFlagComponent = (props: Props) => { const { children, name, otherwise, enabledByDefault } = props - const { [name]: feature } = useSelector(appFeatureFlagsFeaturesSelector) - const { flag, variant } = feature ?? { flag: enabledByDefault } + const features = useSelector(appFeatureFlagsFeaturesSelector) - if (!flag) { + const nameArray = isArray(name) ? name : [name] + const matchingFeatures = nameArray + .map((feature) => features?.[feature] || { flag: enabledByDefault}) + const allFlagsEnabled = matchingFeatures.every((feature) => feature.flag) + + if (!allFlagsEnabled) { return otherwise ?? null } @@ -24,7 +28,7 @@ const FeatureFlagComponent = (props: Props) => { return null } - const cloneElement = (child: React.ReactElement) => React.cloneElement(child, { variant }) + const cloneElement = (child: React.ReactElement) => React.cloneElement(child) return isArray(children) ? <>{React.Children.map(children, cloneElement)} : cloneElement(children) } diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx index 1acdd60796..5da7843317 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx @@ -188,7 +188,7 @@ describe('InstanceHeader', () => { expect(screen.queryByTestId('user-profile-popover-content')).toBeInTheDocument() }) - expect(screen.queryByTestId('user-profile-badge')).toBeInTheDocument() + expect(screen.queryByTestId('cloud-user-profile-badge')).toBeInTheDocument() expect(screen.queryByTestId('profile-import-cloud-databases')).not.toBeInTheDocument() expect(screen.queryByTestId('profile-logout')).not.toBeInTheDocument() expect(screen.queryByTestId('cloud-admin-console-link')).toBeInTheDocument() @@ -217,10 +217,25 @@ describe('InstanceHeader', () => { expect(screen.queryByTestId('user-profile-popover-content')).toBeInTheDocument() }) - expect(screen.queryByTestId('user-profile-badge')).toBeInTheDocument() + expect(screen.queryByTestId('oauth-user-profile-badge')).toBeInTheDocument() expect(screen.queryByTestId('profile-import-cloud-databases')).toBeInTheDocument() expect(screen.queryByTestId('profile-logout')).toBeInTheDocument() expect(screen.queryByTestId('cloud-admin-console-link')).not.toBeInTheDocument() expect(screen.queryByTestId('profile-account-40-selected')).toHaveTextContent('Test account #40') }) + + it('should not show sso user profile if cloud ads feature is off', async () => { + const initialStoreState = set( + cloneDeep(initialStateDefault), + `app.features.featureFlags.features.${FeatureFlags.cloudAds}`, + { flag: false } + ) + + render(, { + store: mockStore(initialStoreState) + }) + + expect(screen.queryByTestId('oauth-user-profile-badge')).not.toBeInTheDocument() + expect(screen.queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx index 5690eeca40..6eb18d5b37 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx @@ -8,7 +8,7 @@ import { FeatureFlags, Pages } from 'uiSrc/constants' import { selectOnFocus, validateNumber } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { BuildType } from 'uiSrc/constants/env' -import { ConnectionType, OAuthSocialSource } from 'uiSrc/slices/interfaces' +import { ConnectionType } from 'uiSrc/slices/interfaces' import { checkDatabaseIndexAction, connectedInstanceInfoSelector, @@ -18,7 +18,7 @@ import { import { appInfoSelector } from 'uiSrc/slices/app/info' import { appContextDbIndex, clearBrowserKeyListData, setBrowserSelectedKey } from 'uiSrc/slices/app/context' -import { DatabaseOverview, FeatureFlagComponent, OAuthUserProfile } from 'uiSrc/components' +import { DatabaseOverview, FeatureFlagComponent } from 'uiSrc/components' import InlineItemEditor from 'uiSrc/components/inline-item-editor' import { CopilotTrigger, InsightsTrigger } from 'uiSrc/components/triggers' import ShortInstanceInfo from 'uiSrc/components/instance-header/components/ShortInstanceInfo' @@ -29,7 +29,7 @@ import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { isAnyFeatureEnabled } from 'uiSrc/utils/features' import { getConfig } from 'uiSrc/config' import { appReturnUrlSelector } from 'uiSrc/slices/app/url-handling' -import { CloudUserProfile } from 'uiSrc/components/instance-header/components/CloudUserProfile' +import UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile' import InstancesNavigationPopover from './components/instances-navigation-popover' import styles from './styles.module.scss' @@ -272,20 +272,7 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => { - - - - )} - > - - - - - - + diff --git a/redisinsight/ui/src/components/instance-header/components/CloudUserProfile.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx similarity index 61% rename from redisinsight/ui/src/components/instance-header/components/CloudUserProfile.tsx rename to redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx index ecb5362cd1..77667db297 100644 --- a/redisinsight/ui/src/components/instance-header/components/CloudUserProfile.tsx +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useSelector } from 'react-redux' import { cloudUserProfileSelector } from 'uiSrc/slices/user/cloud-user-profile' -import UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile' +import UserProfileBadge from 'uiSrc/components/instance-header/components/user-profile/UserProfileBadge' export const CloudUserProfile = () => { const { data, error } = useSelector(cloudUserProfileSelector) @@ -10,6 +10,6 @@ export const CloudUserProfile = () => { } return ( - + ) } diff --git a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx new file mode 100644 index 0000000000..fdadef4db5 --- /dev/null +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx @@ -0,0 +1,138 @@ +import { cloneDeep, set } from 'lodash' +import React from 'react' +import { CloudUser } from 'src/modules/cloud/user/models' +import { FeatureFlags } from 'uiSrc/constants' +import { + cleanup, + mockedStore, + render, + initialStateDefault, + mockStore, +} from 'uiSrc/utils/test-utils' +import UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile' + +const initialMockUser: CloudUser = { + id: 123, + name: 'John Smith', + currentAccountId: 45, + accounts: [ + { + id: 45, + name: 'Account 1', + }, + { + id: 46, + name: 'Account 2', + }, + ], + data: {}, +} + +type MockStoreStateProps = { + envDependent?: boolean + cloudSso?: boolean + cloudAds?: boolean + mockUser?: CloudUser +} + +const mockStoreStateWithFlags = ({ + envDependent = true, + cloudSso = true, + cloudAds = true, + mockUser = initialMockUser, +}: MockStoreStateProps = {}) => { + const keys = ['envDependent', 'cloudSso', 'cloudAds'] + const values = [envDependent, cloudSso, cloudAds] + + const initialStoreState = cloneDeep(initialStateDefault) + + for (let i = 0; i < keys.length; i++) { + set( + initialStoreState, + `app.features.featureFlags.features.${FeatureFlags[keys[i] as keyof typeof FeatureFlags]}`, + { flag: values[i] }, + ) + } + + set(initialStoreState, 'user.cloudProfile', { + error: '', + data: mockUser, + }) + set(initialStoreState, 'oauth.cloud.user', { + error: '', + loading: false, + initialLoading: false, + data: mockUser, + }) + + return mockStore(initialStoreState) +} + +describe('UserProfile', () => { + let store: typeof mockedStore + beforeEach(() => { + cleanup() + store = mockStoreStateWithFlags() + }) + + it('should render CloudUserProfile if envDependentFeature is disabled', () => { + store = mockStoreStateWithFlags({ envDependent: false }) + const { getByTestId } = render(, { + store, + }) + + expect(getByTestId('cloud-user-profile-badge')).toBeInTheDocument() + }) + + it('should not show cloud user profile badge profile name is empty', () => { + store = mockStoreStateWithFlags({ + envDependent: false, + mockUser: { + ...initialMockUser, + name: '', + }, + }) + const { queryByTestId } = render(, { + store, + }) + + expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() + }) + + it('should render OAuthUserProfile if envDependentFeature is enabled', () => { + store = mockStoreStateWithFlags() + const { queryByTestId } = render(, { + store, + }) + + expect(queryByTestId('oauth-user-profile-badge')).toBeInTheDocument() + }) + + it('should render nothing when envDependent=true, cloudAds=false, cloudSso=true', () => { + store = mockStoreStateWithFlags({ + envDependent: true, + cloudAds: false, + cloudSso: true, + }) + const { queryByTestId } = render(, { + store, + }) + + expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() + expect(queryByTestId('oauth-user-profile-badge')).not.toBeInTheDocument() + }) + + it('should render nothing when envDependent=true, cloudAds=true, cloudSso=false', () => { + store = mockStoreStateWithFlags({ + envDependent: true, + cloudAds: true, + cloudSso: false, + }) + const { queryByTestId } = render(, { + store, + }) + + expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() + expect(queryByTestId('oauth-user-profile-badge')).not.toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.tsx index 4cddf5747d..f358f5499f 100644 --- a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.tsx +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.tsx @@ -1,214 +1,36 @@ -import React, { useState } from 'react' -import { useDispatch } from 'react-redux' -import { EuiIcon, EuiLink, EuiLoadingSpinner, EuiPopover, EuiText } from '@elastic/eui' -import cx from 'classnames' -import { useHistory } from 'react-router-dom' -import { - logoutUserAction, -} from 'uiSrc/slices/oauth/cloud' -import CloudIcon from 'uiSrc/assets/img/oauth/cloud.svg?react' - -import { getUtmExternalLink } from 'uiSrc/utils/links' -import { EXTERNAL_LINKS } from 'uiSrc/constants/links' - -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' -import { getTruncatedName, Nullable } from 'uiSrc/utils' -import { fetchSubscriptionsRedisCloud, setSSOFlow } from 'uiSrc/slices/instances/cloud' -import { FeatureFlags, Pages } from 'uiSrc/constants' -import { FeatureFlagComponent } from 'uiSrc/components' -import { getConfig } from 'uiSrc/config' -import { CloudUser } from 'apiSrc/modules/cloud/user/models' -import styles from './styles.module.scss' - -export interface Props { - error: Nullable; - data: Nullable; - handleClickSelectAccount?: (id: number) => void; - handleClickCloudAccount?: () => void; - selectingAccountId?: number; -} - -const riConfig = getConfig() - -const UserProfile = (props: Props) => { +import React from 'react' +import { useSelector } from 'react-redux' +import { EuiFlexItem } from '@elastic/eui' +import { FeatureFlags } from 'uiSrc/constants' +import { OAuthSocialSource } from 'uiSrc/slices/interfaces' +import { OAuthUserProfile } from 'uiSrc/components' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' +import { CloudUserProfile } from './CloudUserProfile' + +const UserProfile = () => { const { - error, - data, - handleClickSelectAccount, - handleClickCloudAccount, - selectingAccountId - } = props - - const [isProfileOpen, setIsProfileOpen] = useState(false) - const [isImportLoading, setIsImportLoading] = useState(false) - - const dispatch = useDispatch() - const history = useHistory() - - if (!data || error) { - return null - } - - const handleClickImport = () => { - if (isImportLoading) return - - setIsImportLoading(true) - dispatch(setSSOFlow(OAuthSocialAction.Import)) - dispatch(fetchSubscriptionsRedisCloud( - null, - true, - () => { - history.push(Pages.redisCloudSubscriptions) - setIsImportLoading(false) - }, - () => setIsImportLoading(false) - )) - - sendEventTelemetry({ - event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED, - eventData: { - source: OAuthSocialSource.UserProfile - } - }) - } - - const handleClickLogout = () => { - setIsProfileOpen(false) - dispatch(logoutUserAction( - () => { - sendEventTelemetry({ - event: TelemetryEvent.CLOUD_SIGN_OUT_CLICKED - }) - } - )) + [FeatureFlags.envDependent]: envDependentFeature, + [FeatureFlags.cloudAds]: cloudAds, + [FeatureFlags.cloudSso]: cloudSso, + } = useSelector(appFeatureFlagsFeaturesSelector) + + if (!envDependentFeature?.flag) { + return ( + + + + ) } - const handleToggleProfile = () => { - if (!isProfileOpen) { - sendEventTelemetry({ - event: TelemetryEvent.CLOUD_PROFILE_OPENED - }) - } - setIsProfileOpen((v) => !v) + if (cloudAds?.flag && cloudSso?.flag) { + return ( + + + + ) } - const { accounts, currentAccountId, name } = data - - return ( -
- setIsProfileOpen(false)} - panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)} - button={( -
- {getTruncatedName(name) || 'R'} -
- )} - > -
-
- Account} - > - Redis Cloud account - -
- {accounts?.map(({ name, id }) => ( - - ))} -
-
- - Back to Redis Cloud Admin console - - - )} - > -
- Import Cloud databases - {isImportLoading ? ( - - ) : ( - - )} -
- -
- Cloud Console - - {name} - -
- -
-
- Logout - -
-
-
-
-
- ) + return null } export default UserProfile diff --git a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.spec.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.spec.tsx new file mode 100644 index 0000000000..591847dc30 --- /dev/null +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.spec.tsx @@ -0,0 +1,230 @@ +import React from 'react' +import { CloudUser } from 'src/modules/cloud/user/models' +import { + act, + fireEvent, + render, + screen, + waitForEuiPopoverVisible, + within, +} from 'uiSrc/utils/test-utils' +import * as appFeaturesSlice from 'uiSrc/slices/app/features' +import UserProfileBadge, { UserProfileBadgeProps } from './UserProfileBadge' + +const mockUser: CloudUser = { + id: 123, + name: 'John Smith', + currentAccountId: 45, + accounts: [ + { + id: 45, + name: 'Account 1', + }, + { + id: 46, + name: 'Account 2', + }, + ], + data: {}, +} + +const TEST_IDS = { + badge: 'test-user-profile-badge', + profileTitle: 'profile-title', + accountsList: 'user-profile-popover-accounts', + selectedAccountCheckmark: (accountId: number) => + `user-profile-selected-account-${accountId}`, + selectingAccountSpinner: (accountId: number) => + `user-profile-selecting-account-${accountId}`, + cloudAdminConsoleLink: 'cloud-admin-console-link', + importCloudDatabases: 'profile-import-cloud-databases', + logoutButton: 'profile-logout', + accountFullName: 'account-full-name', + cloudConsoleLink: 'cloud-console-link', +} + +jest.mock('uiSrc/config', () => ({ + getConfig: jest.fn(() => { + const { getConfig: actualGetConfig } = jest.requireActual('uiSrc/config') + const actualConfig = actualGetConfig() + return { + ...actualConfig, + app: { + ...actualConfig.app, + smConsoleRedirect: 'https://foo.bar', + }, + } + }), +})) + +const mockFeatureFlags = (envDependent = true) => { + jest + .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector') + .mockReturnValue({ + envDependent: { + flag: envDependent, + }, + }) +} + +describe('UserProfileBadge', () => { + let handleClickSelectAccount: jest.Mock + let handleClickCloudAccount: jest.Mock + const selectingAccountId = undefined + let renderUserProfileBadge: ( + props?: Partial, + ) => ReturnType + let renderAndOpenUserProfileBadge: ( + props?: Partial, + ) => Promise> + + beforeEach(() => { + mockFeatureFlags() + handleClickSelectAccount = jest.fn() + handleClickCloudAccount = jest.fn() + + renderUserProfileBadge = (props?: Partial) => + render( + , + ) + + renderAndOpenUserProfileBadge = async ( + props?: Partial, + ) => { + const resp = renderUserProfileBadge(props) + + await act(async () => { + fireEvent.click(screen.getByTestId('user-profile-btn')) + }) + await waitForEuiPopoverVisible() + + return resp + } + }) + + it('should show button with user initials if data is present', () => { + const { queryByRole } = renderUserProfileBadge() + expect(queryByRole('presentation')).toHaveTextContent('JS') + }) + + it('should not render anything if data is absent', () => { + const { container } = renderUserProfileBadge({ data: null }) + expect(container).toBeEmptyDOMElement() + }) + + it('should not render anything if error is provided', () => { + const { container } = renderUserProfileBadge({ error: 'An error occurred' }) + expect(container).toBeEmptyDOMElement() + }) + + it('should show expected header when envDependent flag is enabled', async () => { + const { getByTestId } = await renderAndOpenUserProfileBadge() + expect(getByTestId(TEST_IDS.profileTitle)).toHaveTextContent( + 'Redis Cloud account', + ) + }) + + it('should show expected header when envDependent flag is disabled', async () => { + mockFeatureFlags(false) + const { getByTestId } = await renderAndOpenUserProfileBadge() + expect(getByTestId(TEST_IDS.profileTitle)).toHaveTextContent('Account') + }) + + it('should show available accounts and selected account', async () => { + const { getByTestId } = await renderAndOpenUserProfileBadge() + + // eslint-disable-next-line no-restricted-syntax + for (const account of mockUser.accounts ?? []) { + const testId = `profile-account-${account.id}${account.id === mockUser.currentAccountId ? '-selected' : ''}` + const accountElement = getByTestId(testId) + expect(accountElement).toHaveTextContent(account.name) + + // checkbox icon for selected account + if (account.id === mockUser.currentAccountId) { + expect( + within(accountElement).queryByTestId( + TEST_IDS.selectedAccountCheckmark(account.id), + ), + ).toBeInTheDocument() + } else { + expect( + within(accountElement).queryByTestId( + TEST_IDS.selectedAccountCheckmark(account.id), + ), + ).not.toBeInTheDocument() + } + + // click on account + // eslint-disable-next-line no-await-in-loop + await act(async () => { + fireEvent.click(accountElement) + }) + expect(handleClickSelectAccount).toHaveBeenCalledTimes(1) + expect(handleClickSelectAccount).toHaveBeenCalledWith(account.id) + handleClickSelectAccount.mockReset() + } + }) + + it('should show spinner next to account when selectedAccountId is provided', async () => { + const selectingAccountId = 46 + const { getByTestId } = await renderAndOpenUserProfileBadge({ + selectingAccountId, + }) + + mockUser.accounts?.forEach((account) => { + const testId = `profile-account-${account.id}${account.id === mockUser.currentAccountId ? '-selected' : ''}` + const accountElement = getByTestId(testId) + expect(accountElement).toHaveTextContent(account.name) + + // spinner for selecting account + if (account.id === selectingAccountId) { + expect( + within(accountElement).queryByTestId( + TEST_IDS.selectingAccountSpinner(account.id), + ), + ).toBeInTheDocument() + } else { + expect( + within(accountElement).queryByTestId( + TEST_IDS.selectingAccountSpinner(account.id), + ), + ).not.toBeInTheDocument() + } + }) + }) + + it('should show expected links when envDependent flag is disabled', async () => { + mockFeatureFlags(false) + const { getByTestId, queryByTestId } = await renderAndOpenUserProfileBadge() + + const link = getByTestId(TEST_IDS.cloudAdminConsoleLink) + expect(link).toHaveAttribute('href', 'https://foo.bar') + expect(link).toHaveTextContent('Back to Redis Cloud Admin console') + + expect(queryByTestId(TEST_IDS.importCloudDatabases)).not.toBeInTheDocument() + expect(queryByTestId(TEST_IDS.logoutButton)).not.toBeInTheDocument() + expect(queryByTestId(TEST_IDS.accountFullName)).not.toBeInTheDocument() + expect(queryByTestId(TEST_IDS.cloudConsoleLink)).not.toBeInTheDocument() + }) + + it('should show expected links when envDependent flag is enabled', async () => { + mockFeatureFlags() + const { getByTestId, queryByTestId } = await renderAndOpenUserProfileBadge() + + expect( + queryByTestId(TEST_IDS.cloudAdminConsoleLink), + ).not.toBeInTheDocument() + expect(getByTestId(TEST_IDS.importCloudDatabases)).toBeInTheDocument() + expect(getByTestId(TEST_IDS.logoutButton)).toBeInTheDocument() + expect(getByTestId(TEST_IDS.accountFullName)).toBeInTheDocument() + expect(getByTestId(TEST_IDS.cloudConsoleLink)).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx new file mode 100644 index 0000000000..89118f2316 --- /dev/null +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx @@ -0,0 +1,216 @@ +import React, { useState } from 'react' +import { useDispatch } from 'react-redux' +import { EuiIcon, EuiLink, EuiLoadingSpinner, EuiPopover, EuiText } from '@elastic/eui' +import cx from 'classnames' +import { useHistory } from 'react-router-dom' +import { + logoutUserAction, +} from 'uiSrc/slices/oauth/cloud' +import CloudIcon from 'uiSrc/assets/img/oauth/cloud.svg?react' + +import { getUtmExternalLink } from 'uiSrc/utils/links' +import { EXTERNAL_LINKS } from 'uiSrc/constants/links' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' +import { getTruncatedName, Nullable } from 'uiSrc/utils' +import { fetchSubscriptionsRedisCloud, setSSOFlow } from 'uiSrc/slices/instances/cloud' +import { FeatureFlags, Pages } from 'uiSrc/constants' +import { FeatureFlagComponent } from 'uiSrc/components' +import { getConfig } from 'uiSrc/config' +import { CloudUser } from 'apiSrc/modules/cloud/user/models' +import styles from './styles.module.scss' + +export interface UserProfileBadgeProps { + "data-testid"?: string; + error: Nullable; + data: Nullable; + handleClickSelectAccount?: (id: number) => void; + handleClickCloudAccount?: () => void; + selectingAccountId?: number; +} + +const riConfig = getConfig() + +const UserProfileBadge = (props: UserProfileBadgeProps) => { + const { + error, + data, + handleClickSelectAccount, + handleClickCloudAccount, + selectingAccountId, + 'data-testid': dataTestId, + } = props + + const [isProfileOpen, setIsProfileOpen] = useState(false) + const [isImportLoading, setIsImportLoading] = useState(false) + + const dispatch = useDispatch() + const history = useHistory() + + if (!data || error) { + return null + } + + const handleClickImport = () => { + if (isImportLoading) return + + setIsImportLoading(true) + dispatch(setSSOFlow(OAuthSocialAction.Import)) + dispatch(fetchSubscriptionsRedisCloud( + null, + true, + () => { + history.push(Pages.redisCloudSubscriptions) + setIsImportLoading(false) + }, + () => setIsImportLoading(false) + )) + + sendEventTelemetry({ + event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED, + eventData: { + source: OAuthSocialSource.UserProfile + } + }) + } + + const handleClickLogout = () => { + setIsProfileOpen(false) + dispatch(logoutUserAction( + () => { + sendEventTelemetry({ + event: TelemetryEvent.CLOUD_SIGN_OUT_CLICKED + }) + } + )) + } + + const handleToggleProfile = () => { + if (!isProfileOpen) { + sendEventTelemetry({ + event: TelemetryEvent.CLOUD_PROFILE_OPENED + }) + } + setIsProfileOpen((v) => !v) + } + + const { accounts, currentAccountId, name } = data + + return ( +
+ setIsProfileOpen(false)} + panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)} + button={( +
+ {getTruncatedName(name) || 'R'} +
+ )} + > +
+
+ Account} + > + Redis Cloud account + +
+ {accounts?.map(({ name, id }) => ( + + ))} +
+
+ + Back to Redis Cloud Admin console + + + )} + > +
+ Import Cloud databases + {isImportLoading ? ( + + ) : ( + + )} +
+ +
+ Cloud Console + + {name} + +
+ +
+
+ Logout + +
+
+
+
+
+ ) +} + +export default UserProfileBadge diff --git a/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx b/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx index d004cc0b84..9419788be2 100644 --- a/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx +++ b/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx @@ -5,10 +5,11 @@ import { useSelector } from 'react-redux' import RedisDbBlueIcon from 'uiSrc/assets/img/icons/redis_db_blue.svg' import { CloudSsoUtmCampaign, OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' -import { OAuthConnectFreeDb, OAuthSsoHandlerDialog } from 'uiSrc/components' +import { FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog } from 'uiSrc/components' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' import { getUtmExternalLink } from 'uiSrc/utils/links' import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' +import { FeatureFlags } from 'uiSrc/constants' import styles from './styles.module.scss' @@ -44,7 +45,7 @@ const FilterNotAvailable = ({ onClose } : { onClose?: () => void }) => { )} {!freeInstances.length && ( - <> + Create a free trial Redis Stack database that supports filtering and extends the core capabilities of your Redis. @@ -84,7 +85,7 @@ const FilterNotAvailable = ({ onClose } : { onClose?: () => void }) => { Learn More - + )} ) diff --git a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.spec.tsx b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.spec.tsx index 480a84f106..83232cebf8 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.spec.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.spec.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { render, screen } from 'uiSrc/utils/test-utils' - +import { mockFeatureFlags, render, screen } from 'uiSrc/utils/test-utils' import { OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' import ModuleNotLoadedMinimalized from './ModuleNotLoadedMinimalized' @@ -42,6 +41,26 @@ describe('ModuleNotLoadedMinimalized', () => { render() expect(screen.getByTestId('tutorials-get-started-link')).toBeInTheDocument() - expect(screen.getByTestId('tutorials-docker-link')).toBeInTheDocument() + }) + + it('should render expected text and "Redis databases page" button when cloudAds feature flag is disabled', () => { + mockFeatureFlags({ + cloudAds: { + flag: false, + } + }) + + render() + + expect(screen.queryByTestId('tutorials-get-started-link')).not.toBeInTheDocument() + expect(screen.queryByTestId('connect-free-db-btn')).not.toBeInTheDocument() + expect(screen.getByText(/Redis Databases page/)).toBeInTheDocument() + expect(screen.getByText(/Open a database with Redis Query Engine/)).toBeInTheDocument() + }) + + it('should render expected text when cloudAds feature flag is enabled', () => { + render() + + expect(screen.getByText(/Create a free trial Redis Stack database with search and query/)).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.tsx b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.tsx index 0cbd4ce948..921e3449ea 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.tsx @@ -1,6 +1,7 @@ import React from 'react' import { useSelector } from 'react-redux' -import { EuiSpacer, EuiText, EuiTitle } from '@elastic/eui' +import { useHistory } from 'react-router-dom' +import { EuiButton, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui' import TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg' import { OAuthSocialAction, OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces' @@ -11,8 +12,9 @@ import { getUtmExternalLink } from 'uiSrc/utils/links' import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' import { getDbWithModuleLoaded, getSourceTutorialByCapability } from 'uiSrc/utils' import { useCapability } from 'uiSrc/services' -import { FeatureFlags } from 'uiSrc/constants' -import { MODULE_CAPABILITY_TEXT_NOT_AVAILABLE } from './constants' +import { FeatureFlags, Pages } from 'uiSrc/constants' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' +import { MODULE_CAPABILITY_TEXT_NOT_AVAILABLE, MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE } from './constants' import styles from './styles.module.scss' export interface Props { @@ -22,11 +24,17 @@ export interface Props { } const ModuleNotLoadedMinimalized = (props: Props) => { + const history = useHistory() + const { + [FeatureFlags.cloudAds]: cloudAdsFeature, + } = useSelector(appFeatureFlagsFeaturesSelector) const { moduleName, source, onClose } = props const freeInstances = useSelector(freeInstancesSelector) || [] const sourceTutorial = getSourceTutorialByCapability(moduleName) - const moduleText = MODULE_CAPABILITY_TEXT_NOT_AVAILABLE[moduleName] + const moduleText = cloudAdsFeature?.flag + ? MODULE_CAPABILITY_TEXT_NOT_AVAILABLE[moduleName] + : MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE[moduleName] const freeDbWithModule = getDbWithModuleLoaded(freeInstances, moduleName) useCapability(sourceTutorial) @@ -38,56 +46,64 @@ const ModuleNotLoadedMinimalized = (props: Props) => {
{moduleText?.title}
- {!freeDbWithModule && ( - <> + {moduleText?.text} - - {(ssoCloudHandlerClick) => ( - { - ssoCloudHandlerClick(e, { - source, - action: OAuthSocialAction.Create - }, `${moduleName}_${source}`) - onClose?.() - }} - data-testid="tutorials-get-started-link" - > - Start with Cloud for free - - )} - - - <> - - - Start with Docker - - - - - )} - {!!freeDbWithModule && ( - <> - - Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. - - - - - )} + { + history.push(Pages.home) + }} + > + Redis Databases page + + } + > + {!freeDbWithModule ? ( + <> + + {moduleText?.text} + + + + {(ssoCloudHandlerClick) => ( + { + ssoCloudHandlerClick(e, { + source, + action: OAuthSocialAction.Create + }, `${moduleName}_${source}`) + onClose?.() + }} + data-testid="tutorials-get-started-link" + > + Start with Cloud for free + + )} + + + ) : ( + <> + + Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. + + + + + )} + () +const props: IProps = { + moduleName: RedisDefaultModules.Search, + type: 'browser', + id: 'id', + onClose: jest.fn(), +} + +const mockUseHistory = { push: jest.fn() } +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), +})) +jest.mock( + 'uiSrc/assets/img/icons/mobile_module_not_loaded.svg?react', + () => 'div', +) +jest.mock('uiSrc/assets/img/icons/module_not_loaded.svg?react', () => 'div') +jest.mock('uiSrc/assets/img/telescope-dark.svg?react', () => 'div') +jest.mock('uiSrc/assets/img/icons/cheer.svg?react', () => 'div') + +const mockGetDbWithModuleLoaded = (value?: boolean) => { + jest + .spyOn(utils, 'getDbWithModuleLoaded') + .mockImplementation(() => value as unknown as Instance) +} + +const TEST_IDS = { + ctaWrapper: 'module-not-loaded-cta-wrapper', +} describe('ModuleNotLoaded', () => { + beforeEach(() => { + jest.resetAllMocks() + mockFeatureFlags() + mockGetDbWithModuleLoaded() + }) + it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() + }) + + it('should render free trial text when cloudAds feature is enabled and no free db exists', () => { + const { queryByText } = render() + expect( + queryByText(/Create a free trial Redis Stack database/), + ).toBeInTheDocument() + }) + + it('should render free db text when cloudAds feature is enabled and free db exists', () => { + mockGetDbWithModuleLoaded(true) + const { queryByText } = render() + expect( + queryByText(/Use your free trial all-in-one Redis Cloud database/), + ).toBeInTheDocument() + }) + + it('should render expected text when cloudAds feature is disabled', () => { + mockFeatureFlags({ + cloudAds: { + flag: false, + }, + }) + mockGetDbWithModuleLoaded(true) // should not affect output + const { queryByText } = render() + expect( + queryByText(/Open a database with Redis Query Engine/), + ).toBeInTheDocument() + }) + + it('should not show CTA button when envDependant feature is disabled', () => { + mockFeatureFlags({ + envDependent: { + flag: false, + }, + }) + const { queryByTestId } = render() + expect(queryByTestId(TEST_IDS.ctaWrapper)).toBeEmptyDOMElement() + }) + + it('should show "Get Started For Free" button when envDependant feature is enabled and cloudAds feature is enabled', () => { + const { queryByText, getByText } = render() + expect(getByText(/Get Started For Free/)).toBeInTheDocument() + expect(queryByText(/Redis Databases page/)).not.toBeInTheDocument() + }) + + it('should show "Redis Databases page" button when envDependant feature is enabled and cloudAds feature is disabled', async () => { + mockFeatureFlags({ + cloudAds: { + flag: false, + }, + }) + jest + .spyOn(reactRouterDom, 'useHistory') + .mockImplementation(() => mockUseHistory as unknown as any) + + const { queryByText, getByText } = render() + const databasesButton = getByText(/Redis Databases page/) + expect(databasesButton).toBeInTheDocument() + expect(queryByText(/Get Started For Free/)).not.toBeInTheDocument() + + // click button + act(() => { + fireEvent.click(databasesButton) + }) + + // assert + expect(mockUseHistory.push).toHaveBeenCalledTimes(1) + expect(mockUseHistory.push).toHaveBeenCalledWith('/') + }) + + it('should show expected text when cloudAds feature is disabled', () => { + mockFeatureFlags({ + cloudAds: { + flag: false, + }, + }) + const { getByText } = render() + expect( + getByText(/Open a database with Redis Query Engine/), + ).toBeInTheDocument() + }) + + it('should show expected text when free db exists', () => { + mockGetDbWithModuleLoaded(true) + const { getByText } = render() + expect( + getByText(/Use your free trial all-in-one Redis Cloud database/), + ).toBeInTheDocument() + }) + + it('should show expected text when free db does not exist', () => { + const { getByText } = render() + expect( + getByText( + /Create a free trial Redis Stack database with Redis Query Engine/, + ), + ).toBeInTheDocument() + }) + + it('should uppercase first letter of module name in title - time series', () => { + const { getByText } = render( + , + ) + expect( + getByText( + /Time series data structure is not available for this database/, + ), + ).toBeInTheDocument() + }) + + it('should uppercase first letter of module name in title - bloom', () => { + const { getByText } = render( + , + ) + expect( + getByText( + /Probabilistic data structures are not available for this database/, + ), + ).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx index f959a310fb..a1de9a27dd 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -1,21 +1,25 @@ import React, { useCallback, useEffect, useState } from 'react' import cx from 'classnames' -import { EuiButton, EuiIcon, EuiLink, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui' +import { EuiIcon, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui' import { useSelector } from 'react-redux' import MobileIcon from 'uiSrc/assets/img/icons/mobile_module_not_loaded.svg?react' import DesktopIcon from 'uiSrc/assets/img/icons/module_not_loaded.svg?react' import TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg?react' import CheerIcon from 'uiSrc/assets/img/icons/cheer.svg?react' -import { FeatureFlags, MODULE_NOT_LOADED_CONTENT as CONTENT, MODULE_TEXT_VIEW } from 'uiSrc/constants' -import { OAuthSocialAction, OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces' -import { FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog } from 'uiSrc/components' +import { + FeatureFlags, + MODULE_NOT_LOADED_CONTENT as CONTENT, + MODULE_TEXT_VIEW, +} from 'uiSrc/constants' +import { OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces' +import { OAuthConnectFreeDb } from 'uiSrc/components' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' -import { getUtmExternalLink } from 'uiSrc/utils/links' -import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' import { getDbWithModuleLoaded } from 'uiSrc/utils' import { useCapability } from 'uiSrc/services' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' +import ModuleNotLoadedButton from './ModuleNotLoadedButton' import styles from './styles.module.scss' export const MODULE_OAUTH_SOURCE_MAP: { [key in RedisDefaultModules]?: String } = { @@ -38,7 +42,7 @@ const MAX_ELEMENT_WIDTH = 1440 const renderTitle = (width: number, moduleName?: string) => (

- {`${moduleName} ${[MODULE_TEXT_VIEW.redisgears, MODULE_TEXT_VIEW.bf].includes(moduleName) ? 'are' : 'is'} not available `} + {`${moduleName?.substring(0, 1).toUpperCase()}${moduleName?.substring(1)} ${[MODULE_TEXT_VIEW.redisgears, MODULE_TEXT_VIEW.bf].includes(moduleName) ? 'are' : 'is'} not available `} {width > MAX_ELEMENT_WIDTH &&
} for this database

@@ -57,11 +61,16 @@ const ListItem = ({ item }: { item: string }) => ( const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps) => { const [width, setWidth] = useState(0) const freeInstances = useSelector(freeInstancesSelector) || [] + const { + [FeatureFlags.cloudAds]: cloudAdsFeature, + } = useSelector(appFeatureFlagsFeaturesSelector) const module = MODULE_OAUTH_SOURCE_MAP[moduleName] const freeDbWithModule = getDbWithModuleLoaded(freeInstances, moduleName) - const source = type === 'browser' ? OAuthSocialSource.BrowserSearch : OAuthSocialSource[module] + const source = type === 'browser' + ? OAuthSocialSource.BrowserSearch + : OAuthSocialSource[module as keyof typeof OAuthSocialSource] useCapability(source) @@ -72,23 +81,25 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps } }) - const renderText = useCallback((moduleName?: string) => (!freeDbWithModule ? ( - - {`Create a free trial Redis Stack database with ${moduleName} which extends the core capabilities of your Redis`} - - ) : ( - - Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. - - )), [freeDbWithModule]) - - const onFreeDatabaseClick = () => { - onClose?.() - } + const renderText = useCallback((moduleName?: string) => { + if (!cloudAdsFeature?.flag) { + return ( + + Open a database with {moduleName?.toLowerCase()}. + + ) + } - const utmCampaign = type === 'browser' - ? UTM_CAMPAINGS[OAuthSocialSource.BrowserSearch] - : UTM_CAMPAINGS[OAuthSocialSource.Workbench] + return (!freeDbWithModule ? ( + + Create a free trial Redis Stack database with {moduleName} which extends the core capabilities of your Redis. + + ) : ( + + Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. + + )) + }, [freeDbWithModule]) return (
)}
-
+
{renderTitle(width, MODULE_TEXT_VIEW[moduleName])} {CONTENT[moduleName]?.text.map((item: string) => ( @@ -133,57 +144,19 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps {renderText(MODULE_TEXT_VIEW[moduleName])}
-
- {!!freeDbWithModule && ( +
+ {freeDbWithModule ? ( - )} - {!freeDbWithModule && ( - - <> - - Learn More - - - {(ssoCloudHandlerClick) => ( - { - ssoCloudHandlerClick( - e, - { - source: type === 'browser' ? OAuthSocialSource.BrowserSearch : OAuthSocialSource[module], - action: OAuthSocialAction.Create - } - ) - onFreeDatabaseClick() - }} - data-testid="get-started-link" - > - - Get Started For Free - - - )} - - - + ) : ( + )}
diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoadedButton.tsx b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoadedButton.tsx new file mode 100644 index 0000000000..96bde6a3c3 --- /dev/null +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoadedButton.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' +import { + EuiButton, + EuiLink, +} from '@elastic/eui' +import { useHistory } from 'react-router-dom' +import { + FeatureFlags, + MODULE_NOT_LOADED_CONTENT as CONTENT, + Pages, +} from 'uiSrc/constants' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' +import styles from 'uiSrc/components/messages/module-not-loaded/styles.module.scss' +import { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components' +import { getUtmExternalLink } from 'uiSrc/utils/links' +import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' +import { + OAuthSocialAction, + OAuthSocialSource, + RedisDefaultModules, +} from 'uiSrc/slices/interfaces' + +export interface IProps { + moduleName: RedisDefaultModules + module?: String; + onClose?: () => void + type?: 'workbench' | 'browser' +} + +const ModuleNotLoadedButton = ({ moduleName, type, onClose, module }: IProps) => { + const history = useHistory() + const { + [FeatureFlags.envDependent]: envDependentFeature, + } = useSelector(appFeatureFlagsFeaturesSelector) + + const utmCampaign = + type === 'browser' + ? UTM_CAMPAINGS[OAuthSocialSource.BrowserSearch] + : UTM_CAMPAINGS[OAuthSocialSource.Workbench] + + if (!envDependentFeature?.flag) { + return null + } + + return ( + <> + + Learn More + + { + e.preventDefault() + e.stopPropagation() + + history.push(Pages.home) + }} + data-testid="get-started-link" + > + + Redis Databases page + + + } + > + + {(ssoCloudHandlerClick) => ( + { + ssoCloudHandlerClick(e, { + source: + type === 'browser' + ? OAuthSocialSource.BrowserSearch + : OAuthSocialSource[module as keyof typeof OAuthSocialSource], + action: OAuthSocialAction.Create, + }) + onClose?.() + }} + data-testid="get-started-link" + > + + Get Started For Free + + + )} + + + + ) +} + +export default ModuleNotLoadedButton diff --git a/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.spec.tsx b/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.spec.tsx index bfe6a5b108..8348ecafbd 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.spec.tsx @@ -8,6 +8,7 @@ import { setSocialDialogState } from 'uiSrc/slices/oauth/cloud' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { sendEventTelemetry } from 'uiSrc/telemetry' import { HELP_LINKS } from 'uiSrc/pages/home/constants' +import * as appFeaturesSlice from 'uiSrc/slices/app/features' import CreateCloud from './CreateCloud' jest.mock('uiSrc/telemetry', () => ({ @@ -15,17 +16,20 @@ jest.mock('uiSrc/telemetry', () => ({ sendEventTelemetry: jest.fn(), })) -jest.mock('uiSrc/slices/app/features', () => ({ - ...jest.requireActual('uiSrc/slices/app/features'), - appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({ +const mockFeatureFlags = (cloudAds = true) => { + jest.spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector').mockReturnValue({ cloudSso: { flag: true + }, + cloudAds: { + flag: cloudAds } - }), -})) + }) +} let store: typeof mockedStore beforeEach(() => { + mockFeatureFlags() cleanup() store = cloneDeep(mockedStore) store.clearActions() @@ -68,4 +72,10 @@ describe('CreateCloud', () => { } }) }) + + it('should not render if cloud ads feature flag is disabled', () => { + mockFeatureFlags(false) + const { container } = render() + expect(container).toBeEmptyDOMElement() + }) }) diff --git a/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.tsx b/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.tsx index 710dd1ba72..0986d9a26d 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.tsx +++ b/redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.tsx @@ -2,7 +2,7 @@ import React from 'react' import cx from 'classnames' import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui' -import { OAuthSsoHandlerDialog } from 'uiSrc/components' +import { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components' import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import CloudIcon from 'uiSrc/assets/img/oauth/cloud_centered.svg?react' @@ -10,6 +10,7 @@ import CloudIcon from 'uiSrc/assets/img/oauth/cloud_centered.svg?react' import { getUtmExternalLink } from 'uiSrc/utils/links' import { sendEventTelemetry } from 'uiSrc/telemetry' import { HELP_LINKS } from 'uiSrc/pages/home/constants' +import { FeatureFlags } from 'uiSrc/constants' import styles from '../../styles.module.scss' const CreateCloud = () => { @@ -25,35 +26,37 @@ const CreateCloud = () => { } return ( - - - - {(ssoCloudHandlerClick, isSSOEnabled) => ( - { - onCLickLink(isSSOEnabled) - ssoCloudHandlerClick(e, - { source: OAuthSocialSource.NavigationMenu, action: OAuthSocialAction.Create }) - }} - className={styles.cloudLink} - href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, { campaign: 'navigation_menu' })} - target="_blank" - data-test-subj="create-cloud-nav-link" - > - - - )} - - - + + + + + {(ssoCloudHandlerClick, isSSOEnabled) => ( + { + onCLickLink(isSSOEnabled) + ssoCloudHandlerClick(e, + { source: OAuthSocialSource.NavigationMenu, action: OAuthSocialAction.Create }) + }} + className={styles.cloudLink} + href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, { campaign: 'navigation_menu' })} + target="_blank" + data-test-subj="create-cloud-nav-link" + > + + + )} + + + + ) } diff --git a/redisinsight/ui/src/components/navigation-menu/components/redis-logo/RedisLogo.spec.tsx b/redisinsight/ui/src/components/navigation-menu/components/redis-logo/RedisLogo.spec.tsx index 9405911bc3..dc77e7517a 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/redis-logo/RedisLogo.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/components/redis-logo/RedisLogo.spec.tsx @@ -16,7 +16,7 @@ beforeEach(() => { }) describe('RedisLogo', () => { - it('should have link if envDependant feature is on', () => { + it('should have link if envDependent feature is on', () => { const initialStoreState = set( cloneDeep(initialStateDefault), `app.features.featureFlags.features.${FeatureFlags.envDependent}`, @@ -32,7 +32,7 @@ describe('RedisLogo', () => { expect(screen.getByTestId('redis-logo-link')).toBeInTheDocument() }) - it('should not have link if envDependant feature is off', () => { + it('should not have link if envDependent feature is off', () => { const initialStoreState = set( cloneDeep(initialStateDefault), `app.features.featureFlags.features.${FeatureFlags.envDependent}`, diff --git a/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx index 370a8d4980..421e5de7fc 100644 --- a/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx @@ -13,7 +13,7 @@ import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { OAuthSocialSource } from 'uiSrc/slices/interfaces' import { appInfoSelector } from 'uiSrc/slices/app/info' import { PackageType } from 'uiSrc/constants/env' -import UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile' +import UserProfileBadge from 'uiSrc/components/instance-header/components/user-profile/UserProfileBadge' import styles from './styles.module.scss' @@ -82,11 +82,12 @@ const OAuthUserProfile = (props: Props) => { } return ( - ) } diff --git a/redisinsight/ui/src/components/page-header/PageHeader.tsx b/redisinsight/ui/src/components/page-header/PageHeader.tsx index 60a0f53831..f21e907459 100644 --- a/redisinsight/ui/src/components/page-header/PageHeader.tsx +++ b/redisinsight/ui/src/components/page-header/PageHeader.tsx @@ -69,7 +69,7 @@ const PageHeader = (props: Props) => { )} - + diff --git a/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx b/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx index 48ad291d4b..ccf57c7d4b 100644 --- a/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx +++ b/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx @@ -75,7 +75,7 @@ const RdiInstanceHeader = () => { - + diff --git a/redisinsight/ui/src/config/default.ts b/redisinsight/ui/src/config/default.ts index 1141b3dc31..ebd3596a48 100644 --- a/redisinsight/ui/src/config/default.ts +++ b/redisinsight/ui/src/config/default.ts @@ -70,6 +70,9 @@ export const defaultConfig = { features: { envDependent: { defaultFlag: booleanEnv('RI_FEATURES_ENV_DEPENDENT_DEFAULT_FLAG', true) + }, + cloudAds: { + defaultFlag: booleanEnv('RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG', true) } } } diff --git a/redisinsight/ui/src/constants/featureFlags.ts b/redisinsight/ui/src/constants/featureFlags.ts index d7d592b584..c72961eac8 100644 --- a/redisinsight/ui/src/constants/featureFlags.ts +++ b/redisinsight/ui/src/constants/featureFlags.ts @@ -8,5 +8,6 @@ export enum FeatureFlags { rdi = 'redisDataIntegration', hashFieldExpiration = 'hashFieldExpiration', enhancedCloudUI = 'enhancedCloudUI', + cloudAds = 'cloudAds', databaseManagement = 'databaseManagement', } diff --git a/redisinsight/ui/src/constants/help-texts.tsx b/redisinsight/ui/src/constants/help-texts.tsx index b5e6e3c66d..1778fe0efa 100644 --- a/redisinsight/ui/src/constants/help-texts.tsx +++ b/redisinsight/ui/src/constants/help-texts.tsx @@ -1,5 +1,7 @@ import React from 'react' import { EuiIcon, EuiText } from '@elastic/eui' +import { FeatureFlagComponent } from 'uiSrc/components' +import { FeatureFlags } from './featureFlags' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import styles from 'uiSrc/pages/browser/components/popover-delete/styles.module.scss' @@ -11,11 +13,14 @@ export default { {' '} here. {' '} - You can also create a - {' '} - free trial Redis Cloud database - {' '} - with built-in JSON support. + + <>You can also create a + {' '} + free trial Redis Cloud database + {' '} + with built-in JSON support. + + ), REMOVE_LAST_ELEMENT: (fieldType: string) => ( diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index db67184ab2..7b5d4ccf58 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -49,8 +49,8 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = } export const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = { - [RedisDefaultModules.Bloom]: 'Probabilistic data structures', + [RedisDefaultModules.Bloom]: 'probabilistic data structures', [RedisDefaultModules.ReJSON]: 'JSON data structure', [RedisDefaultModules.Search]: 'Redis Query Engine', - [RedisDefaultModules.TimeSeries]: 'Time series data structure', + [RedisDefaultModules.TimeSeries]: 'time series data structure', } diff --git a/redisinsight/ui/src/pages/home/HomePage.spec.tsx b/redisinsight/ui/src/pages/home/HomePage.spec.tsx index bbc6ded944..8f6c97d100 100644 --- a/redisinsight/ui/src/pages/home/HomePage.spec.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.spec.tsx @@ -50,10 +50,13 @@ describe('HomePage', () => { expect(screen.getByTestId('side-panels-insights')).toBeInTheDocument() }) - it('should not render free cloud db with feature flag disabled', async () => { + it('should not render free cloud db with enhanced cloud ui feature flag disabled', async () => { (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ enhancedCloudUI: { flag: false + }, + cloudAds: { + flag: true } }) await render() @@ -61,10 +64,27 @@ describe('HomePage', () => { expect(screen.queryByTestId('db-row_create-free-cloud-db')).not.toBeInTheDocument() }) - it('should render free cloud db with feature flag enabled', async () => { + it('should not render free cloud db with cloud ads feature flag disabled', async () => { (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ enhancedCloudUI: { flag: true + }, + cloudAds: { + flag: false + } + }) + await render() + + expect(screen.queryByTestId('db-row_create-free-cloud-db')).not.toBeInTheDocument() + }) + + it('should render free cloud db with feature flags enabled', async () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + enhancedCloudUI: { + flag: true + }, + cloudAds: { + flag: true } }) await render() diff --git a/redisinsight/ui/src/pages/home/HomePage.tsx b/redisinsight/ui/src/pages/home/HomePage.tsx index 2bd6150ad4..ad0ac511a8 100644 --- a/redisinsight/ui/src/pages/home/HomePage.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.tsx @@ -1,36 +1,56 @@ -import { - EuiPage, - EuiPageBody, - EuiPanel, -} from '@elastic/eui' +import { EuiPage, EuiPageBody, EuiPanel } from '@elastic/eui' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { clusterSelector, resetDataRedisCluster, resetInstancesRedisCluster, } from 'uiSrc/slices/instances/cluster' +import { + clusterSelector, + resetDataRedisCluster, + resetInstancesRedisCluster, +} from 'uiSrc/slices/instances/cluster' import { Nullable, setTitle } from 'uiSrc/utils' import { HomePageTemplate } from 'uiSrc/templates' import { BrowserStorageItem, FeatureFlags } from 'uiSrc/constants' import { resetKeys } from 'uiSrc/slices/browser/keys' -import { resetCliHelperSettings, resetCliSettingsAction } from 'uiSrc/slices/cli/cli-settings' +import { + resetCliHelperSettings, + resetCliSettingsAction, +} from 'uiSrc/slices/cli/cli-settings' import { resetRedisearchKeysData } from 'uiSrc/slices/browser/redisearch' -import { appContextSelector, setAppContextInitialState } from 'uiSrc/slices/app/context' +import { + appContextSelector, + setAppContextInitialState, +} from 'uiSrc/slices/app/context' import { Instance } from 'uiSrc/slices/interfaces' -import { cloudSelector, resetSubscriptionsRedisCloud } from 'uiSrc/slices/instances/cloud' +import { + cloudSelector, + resetSubscriptionsRedisCloud, +} from 'uiSrc/slices/instances/cloud' import { editedInstanceSelector, fetchEditedInstanceAction, fetchInstancesAction, instancesSelector, resetImportInstances, - setEditedInstance + setEditedInstance, } from 'uiSrc/slices/instances/instances' import { localStorageService } from 'uiSrc/services' -import { resetDataSentinel, sentinelSelector } from 'uiSrc/slices/instances/sentinel' +import { + resetDataSentinel, + sentinelSelector, +} from 'uiSrc/slices/instances/sentinel' import { contentSelector, - fetchContentAction as fetchCreateRedisButtonsAction + fetchContentAction as fetchCreateRedisButtonsAction, } from 'uiSrc/slices/content/create-redis-buttons' -import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' -import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' +import { + sendEventTelemetry, + sendPageViewTelemetry, + TelemetryEvent, + TelemetryPageView, +} from 'uiSrc/telemetry' +import { + appRedirectionSelector, + setUrlHandlingInitialState, +} from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' import { CREATE_CLOUD_DB_ID } from 'uiSrc/pages/home/constants' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' @@ -45,7 +65,7 @@ import styles from './styles.module.scss' enum OpenDialogName { AddDatabase = 'add', - EditDatabase = 'edit' + EditDatabase = 'edit', } const HomePage = () => { @@ -58,7 +78,10 @@ const HomePage = () => { const { instance: sentinelInstance } = useSelector(sentinelSelector) const { action, dbConnection } = useSelector(appRedirectionSelector) const { data: createDbContent } = useSelector(contentSelector) - const { [FeatureFlags.enhancedCloudUI]: enhancedCloudUIFeature } = useSelector(appFeatureFlagsFeaturesSelector) + const { + [FeatureFlags.enhancedCloudUI]: enhancedCloudUIFeature, + [FeatureFlags.cloudAds]: cloudAdsFeature, + } = useSelector(appFeatureFlagsFeaturesSelector) const { loading, @@ -68,16 +91,23 @@ const HomePage = () => { deletedSuccessfully: isDeletedInstance, } = useSelector(instancesSelector) - const { - data: editedInstance, - } = useSelector(editedInstanceSelector) + const { data: editedInstance } = useSelector(editedInstanceSelector) const { contextInstanceId } = useSelector(appContextSelector) - const predefinedInstances = enhancedCloudUIFeature?.flag && createDbContent?.cloud_list_of_databases ? [ - { id: CREATE_CLOUD_DB_ID, ...createDbContent.cloud_list_of_databases } as Instance - ] : [] - const isInstanceExists = instances.length > 0 || predefinedInstances.length > 0 + const predefinedInstances = + enhancedCloudUIFeature?.flag && + cloudAdsFeature?.flag && + createDbContent?.cloud_list_of_databases + ? [ + { + id: CREATE_CLOUD_DB_ID, + ...createDbContent.cloud_list_of_databases, + } as Instance, + ] + : [] + const isInstanceExists = + instances.length > 0 || predefinedInstances.length > 0 useEffect(() => { setTitle('Redis databases') @@ -87,9 +117,9 @@ const HomePage = () => { dispatch(resetSubscriptionsRedisCloud()) dispatch(fetchCreateRedisButtonsAction()) - return (() => { + return () => { dispatch(setEditedInstance(null)) - }) + } }, []) useEffect(() => { @@ -119,7 +149,9 @@ const HomePage = () => { useEffect(() => { if (editedInstance) { - const found = instances.find((item: Instance) => item.id === editedInstance.id) + const found = instances.find( + (item: Instance) => item.id === editedInstance.id, + ) if (found) { dispatch(fetchEditedInstanceAction(found)) } @@ -130,8 +162,8 @@ const HomePage = () => { sendPageViewTelemetry({ name: TelemetryPageView.DATABASES_LIST_PAGE, eventData: { - instancesCount: instances.length - } + instancesCount: instances.length, + }, }) } @@ -152,8 +184,8 @@ const HomePage = () => { sendEventTelemetry({ event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED, eventData: { - databaseId: editedInstance?.id - } + databaseId: editedInstance?.id, + }, }) } @@ -170,7 +202,7 @@ const HomePage = () => { } sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_ADD_FORM_DISMISSED + event: TelemetryEvent.CONFIG_DATABASES_ADD_FORM_DISMISSED, }) } @@ -187,8 +219,8 @@ const HomePage = () => { } const handleDeleteInstances = (instances: Instance[]) => { if ( - instances.find((instance) => instance.id === editedInstance?.id) - && openDialog === OpenDialogName.EditDatabase + instances.find((instance) => instance.id === editedInstance?.id) && + openDialog === OpenDialogName.EditDatabase ) { dispatch(setEditedInstance(null)) setOpenDialog(null) @@ -215,7 +247,7 @@ const HomePage = () => { editedInstance={ openDialog === OpenDialogName.EditDatabase ? editedInstance - : sentinelInstance ?? null + : (sentinelInstance ?? null) } onClose={ openDialog === OpenDialogName.EditDatabase @@ -226,7 +258,7 @@ const HomePage = () => { /> )}
- {(!isInstanceExists && !loading && !loadingChanging ? ( + {!isInstanceExists && !loading && !loadingChanging ? ( @@ -239,7 +271,7 @@ const HomePage = () => { onEditInstance={handleEditInstance} onDeleteInstances={handleDeleteInstances} /> - ))} + )}
diff --git a/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.spec.tsx b/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.spec.tsx index 576ab5f631..dfe34a9590 100644 --- a/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.spec.tsx @@ -76,4 +76,20 @@ describe('ConnectivityOptions', () => { ]) expect(onClose).toBeCalled() }) + + it('should not should create free db button if cloud ads feature flag is disabled', () => { + (appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({ + cloudSso: { + flag: true + }, + cloudAds: { + flag: false, + } + }) + + const onClose = jest.fn() + render() + + expect(screen.queryByTestId('create-free-db-btn')).not.toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.tsx b/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.tsx index cc08b09469..e689b38cff 100644 --- a/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.tsx +++ b/redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.tsx @@ -2,9 +2,10 @@ import React from 'react' import { EuiBadge, EuiButton, EuiFlexGrid, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui' import cx from 'classnames' import { AddDbType } from 'uiSrc/pages/home/constants' -import { OAuthSsoHandlerDialog } from 'uiSrc/components' +import { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components' import { getUtmExternalLink } from 'uiSrc/utils/links' import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' +import { FeatureFlags } from 'uiSrc/constants' import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' import CloudIcon from 'uiSrc/assets/img/oauth/cloud_centered.svg?react' @@ -43,32 +44,34 @@ const ConnectivityOptions = (props: Props) => { Add databases - - - {(ssoCloudHandlerClick, isSSOEnabled) => ( - { - ssoCloudHandlerClick(e, { - source: OAuthSocialSource.AddDbForm, - action: OAuthSocialAction.Create - }) - isSSOEnabled && onClose?.() - }} - data-testid="create-free-db-btn" - > - Free - - New database - - )} - - + + + + {(ssoCloudHandlerClick, isSSOEnabled) => ( + { + ssoCloudHandlerClick(e, { + source: OAuthSocialSource.AddDbForm, + action: OAuthSocialAction.Create + }) + isSSOEnabled && onClose?.() + }} + data-testid="create-free-db-btn" + > + Free + + New database + + )} + + + diff --git a/redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.tsx b/redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.tsx index 3b4cb42049..6bffbfc3f4 100644 --- a/redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.tsx +++ b/redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.tsx @@ -137,9 +137,11 @@ const DatabaseListHeader = ({ onAddInstance }: Props) => { {promoData && ( - - - + + + + + )} diff --git a/redisinsight/ui/src/pages/not-found-error/NotFoundErrorPage.spec.tsx b/redisinsight/ui/src/pages/not-found-error/NotFoundErrorPage.spec.tsx index 231536dc87..ca7e5e31e0 100644 --- a/redisinsight/ui/src/pages/not-found-error/NotFoundErrorPage.spec.tsx +++ b/redisinsight/ui/src/pages/not-found-error/NotFoundErrorPage.spec.tsx @@ -33,7 +33,7 @@ beforeEach(() => { }) describe('NotFoundErrorPage', () => { - it('should render the correct button when envDependant feature is on', async () => { + it('should render the correct button when envDependent feature is on', async () => { const pushMock = jest.fn() jest.spyOn(reactRouterDom, 'useHistory').mockReturnValue({ push: pushMock } as any) @@ -54,7 +54,7 @@ describe('NotFoundErrorPage', () => { expect(pushMock).toHaveBeenCalledWith('/') }) - it('should render the correct button when envDependant feature is off', () => { + it('should render the correct button when envDependent feature is off', () => { const initialStoreState = set( cloneDeep(initialStateDefault), `app.features.featureFlags.features.${FeatureFlags.envDependent}`, diff --git a/redisinsight/ui/src/slices/app/features.ts b/redisinsight/ui/src/slices/app/features.ts index 813f145426..f1c45c04ca 100644 --- a/redisinsight/ui/src/slices/app/features.ts +++ b/redisinsight/ui/src/slices/app/features.ts @@ -55,6 +55,9 @@ export const initialState: StateAppFeatures = { }, [FeatureFlags.envDependent]: { flag: riConfig.features.envDependent.defaultFlag + }, + [FeatureFlags.cloudAds]: { + flag: riConfig.features.cloudAds.defaultFlag } } } @@ -128,12 +131,17 @@ const appFeaturesSlice = createSlice({ getFeatureFlagsSuccess: (state, { payload }) => { state.featureFlags.loading = false - // make sure that feature was defined and enabled by default + // make sure certain features are defined and enabled by default if (!payload.features[FeatureFlags.envDependent]) { payload.features[FeatureFlags.envDependent] = { flag: riConfig.features.envDependent.defaultFlag } } + if (!payload.features[FeatureFlags.cloudAds]) { + payload.features[FeatureFlags.cloudAds] = { + flag: riConfig.features.cloudAds.defaultFlag + } + } state.featureFlags.features = payload.features }, diff --git a/redisinsight/ui/src/slices/app/init.ts b/redisinsight/ui/src/slices/app/init.ts index c0afc42585..1fbdd4dcbe 100644 --- a/redisinsight/ui/src/slices/app/init.ts +++ b/redisinsight/ui/src/slices/app/init.ts @@ -72,8 +72,8 @@ export function initializeAppAction( })) await dispatch(fetchFeatureFlags(async (flagsData) => { - const { [FeatureFlags.envDependent]: envDependant } = flagsData.features - if (!envDependant?.flag) { + const { [FeatureFlags.envDependent]: envDependent } = flagsData.features + if (!envDependent?.flag) { await dispatch(fetchCloudUserProfile(undefined, () => { throw new Error(FAILED_TO_FETCH_USER_PROFILE_ERROR) })) diff --git a/redisinsight/ui/src/slices/tests/app/init.spec.ts b/redisinsight/ui/src/slices/tests/app/init.spec.ts index 1997ec9ea3..b6c3ab877f 100644 --- a/redisinsight/ui/src/slices/tests/app/init.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/init.spec.ts @@ -156,7 +156,7 @@ describe('init slice', () => { expect(store.getActions()).toEqual(expectedActions) }) - it('fetches user profile if !envDependant', async () => { + it('fetches user profile if !envDependent', async () => { riConfig.api.csrfEndpoint = '' const newFeatureFlags = { diff --git a/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx b/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx index 9de09e5d27..5d2bcdff87 100644 --- a/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx +++ b/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx @@ -37,7 +37,7 @@ const HomePageTemplate = (props: Props) => { )} - + diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index 1dd3eb4cfc..9c0f055925 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -62,6 +62,7 @@ import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' import { apiService } from 'uiSrc/services' import { initialState as initialStateAppConnectivity } from 'uiSrc/slices/app/connectivity' import { initialState as initialStateAppInit } from 'uiSrc/slices/app/init' +import * as appFeaturesSlice from 'uiSrc/slices/app/features' interface Options { initialState?: RootState @@ -352,6 +353,17 @@ export const mockWindowLocation = (initialHref = '') => { return setHrefMock } +export const mockFeatureFlags = (overrides?: Partial) => { + const initialFlags = initialStateAppFeaturesReducer.featureFlags.features + + return jest + .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector') + .mockReturnValue({ + ...initialFlags, + ...(overrides || {}), + }) +} + // re-export everything export * from '@testing-library/react' // override render method