From 2fb7afdab9691681f7dee181c971a54c8f11e968 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Wed, 12 Mar 2025 15:52:51 -0500 Subject: [PATCH 01/13] RI-6910: Ability to hide ads --- .../ui/src/components/cloud-ad/CloudAd.tsx | 12 + redisinsight/ui/src/components/index.ts | 4 +- .../instance-header/InstanceHeader.tsx | 21 +- .../{ => user-profile}/CloudUserProfile.tsx | 4 +- .../components/user-profile/UserProfile.tsx | 233 ++---------------- .../user-profile/UserProfileBadge.tsx | 214 ++++++++++++++++ .../FilterNotAvailable.tsx | 86 +++---- .../ModuleNotLoadedMinimalized.tsx | 40 +-- .../module-not-loaded/ModuleNotLoaded.tsx | 70 +++--- .../components/create-cloud/CreateCloud.tsx | 62 ++--- .../oauth-user-profile/OAuthUserProfile.tsx | 4 +- redisinsight/ui/src/config/default.ts | 1 + redisinsight/ui/src/constants/cloud.ts | 5 + redisinsight/ui/src/constants/help-texts.tsx | 14 +- redisinsight/ui/src/pages/home/HomePage.tsx | 3 +- .../ConnectivityOptions.tsx | 56 +++-- 16 files changed, 444 insertions(+), 385 deletions(-) create mode 100644 redisinsight/ui/src/components/cloud-ad/CloudAd.tsx rename redisinsight/ui/src/components/instance-header/components/{ => user-profile}/CloudUserProfile.tsx (66%) create mode 100644 redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx create mode 100644 redisinsight/ui/src/constants/cloud.ts diff --git a/redisinsight/ui/src/components/cloud-ad/CloudAd.tsx b/redisinsight/ui/src/components/cloud-ad/CloudAd.tsx new file mode 100644 index 0000000000..5c0f8aa54e --- /dev/null +++ b/redisinsight/ui/src/components/cloud-ad/CloudAd.tsx @@ -0,0 +1,12 @@ +import { FC, ReactElement } from 'react' +import { HIDE_ADS } from 'uiSrc/constants/cloud' + +const CloudAd: FC<{ children: ReactElement }> = ({ children }) => { + if (HIDE_ADS) { + return null + } + + return children +} + +export default CloudAd diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts index d402cce966..2a6bcc2148 100644 --- a/redisinsight/ui/src/components/index.ts +++ b/redisinsight/ui/src/components/index.ts @@ -36,6 +36,7 @@ import { import { FormatedDate } from './formated-date' import { UploadWarning } from './upload-warning' import FormDialog from './form-dialog' +import CloudAd from './cloud-ad/CloudAd' export { FullScreen } from './full-screen' @@ -82,5 +83,6 @@ export { RecommendationBadgesLegend, FormatedDate, UploadWarning, - FormDialog + FormDialog, + CloudAd, } diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx index dab9afb323..0f66b9e0d7 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' @@ -30,9 +30,9 @@ import { isAnyFeatureEnabled } from 'uiSrc/utils/features' import { getConfig } from 'uiSrc/config' import { appReturnUrlSelector } from 'uiSrc/slices/app/url-handling' import { SmConsoleLink } from 'uiSrc/components/instance-header/components/SmConsoleLink' -import { CloudUserProfile } from 'uiSrc/components/instance-header/components/CloudUserProfile' import InstancesNavigationPopover from './components/instances-navigation-popover' import styles from './styles.module.scss' +import UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile' const riConfig = getConfig() const { returnUrlBase, returnUrlLabel, returnUrlTooltip } = riConfig.app @@ -271,20 +271,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 66% 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..5477698c2f 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.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.tsx index 1389463896..af10ab0777 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,213 +1,34 @@ -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 { FeatureFlagComponent, OAuthUserProfile } from 'uiSrc/components' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' +import { CloudUserProfile } from './CloudUserProfile' +import CloudAd from 'uiSrc/components/cloud-ad/CloudAd' + +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 + [FeatureFlags.envDependent]: envDependentFeature, + } = useSelector(appFeatureFlagsFeaturesSelector) + + if (!envDependentFeature?.flag) { + return ( + + + + ) } - 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 admin console - - - )} - > -
- Import Cloud databases - {isImportLoading ? ( - - ) : ( - - )} -
- -
- Cloud Console - - {name} - -
- -
-
- Logout - -
-
-
-
-
+ + + + + + + ) } 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..488c3b99fb --- /dev/null +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx @@ -0,0 +1,214 @@ +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 UserProfileBadge = (props: Props) => { + 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 + }) + } + )) + } + + 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 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..352fca482f 100644 --- a/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx +++ b/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx @@ -5,7 +5,7 @@ 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 { OAuthConnectFreeDb, OAuthSsoHandlerDialog, CloudAd } 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' @@ -44,47 +44,49 @@ 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. - - -
- - {(ssoCloudHandlerClick) => ( - { - ssoCloudHandlerClick(e, { - source: OAuthSocialSource.BrowserFiltering, - action: OAuthSocialAction.Create - }) - onFreeDatabaseClick() - }} - data-testid="get-started-link" - size="s" - > - Get Started For Free - - )} - - - - Learn More - -
- + + <> + + Create a free trial Redis Stack database that supports filtering and extends + the core capabilities of your Redis. + + +
+ + {(ssoCloudHandlerClick) => ( + { + ssoCloudHandlerClick(e, { + source: OAuthSocialSource.BrowserFiltering, + action: OAuthSocialAction.Create + }) + onFreeDatabaseClick() + }} + data-testid="get-started-link" + size="s" + > + Get Started For Free + + )} + + + + Learn More + +
+ +
)} ) 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..432e7b2f34 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 @@ -6,7 +6,7 @@ import TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg' import { OAuthSocialAction, OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' -import { ExternalLink, FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog } from 'uiSrc/components' +import { ExternalLink, FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog, CloudAd } from 'uiSrc/components' import { getUtmExternalLink } from 'uiSrc/utils/links' import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' import { getDbWithModuleLoaded, getSourceTutorialByCapability } from 'uiSrc/utils' @@ -44,24 +44,26 @@ const ModuleNotLoadedMinimalized = (props: Props) => { {moduleText?.text} - - {(ssoCloudHandlerClick) => ( - { - ssoCloudHandlerClick(e, { - source, - action: OAuthSocialAction.Create - }, `${moduleName}_${source}`) - onClose?.() - }} - data-testid="tutorials-get-started-link" - > - Start with Cloud for free - - )} - + + + {(ssoCloudHandlerClick) => ( + { + ssoCloudHandlerClick(e, { + source, + action: OAuthSocialAction.Create + }, `${moduleName}_${source}`) + onClose?.() + }} + data-testid="tutorials-get-started-link" + > + Start with Cloud for free + + )} + + <> 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..db9c76ea18 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -9,7 +9,7 @@ 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 { FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog, CloudAd } from 'uiSrc/components' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' import { getUtmExternalLink } from 'uiSrc/utils/links' @@ -73,9 +73,11 @@ 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`} - + + + {`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. @@ -152,36 +154,38 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps > Learn More - - {(ssoCloudHandlerClick) => ( - { - ssoCloudHandlerClick( - e, - { - source: type === 'browser' ? OAuthSocialSource.BrowserSearch : OAuthSocialSource[module], - action: OAuthSocialAction.Create - } - ) - onFreeDatabaseClick() - }} - data-testid="get-started-link" - > - + + {(ssoCloudHandlerClick) => ( + { + ssoCloudHandlerClick( + e, + { + source: type === 'browser' ? OAuthSocialSource.BrowserSearch : OAuthSocialSource[module], + action: OAuthSocialAction.Create + } + ) + onFreeDatabaseClick() + }} + data-testid="get-started-link" > - Get Started For Free - - - )} - + + Get Started For Free + + + )} + + )} 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..67b56f2652 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 { OAuthSsoHandlerDialog, CloudAd } 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' @@ -25,35 +25,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/oauth/oauth-user-profile/OAuthUserProfile.tsx b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx index 370a8d4980..0e7653a152 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,7 +82,7 @@ const OAuthUserProfile = (props: Props) => { } return ( - 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/pages/home/HomePage.tsx b/redisinsight/ui/src/pages/home/HomePage.tsx index 2bd6150ad4..4a20a4c4b0 100644 --- a/redisinsight/ui/src/pages/home/HomePage.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.tsx @@ -33,6 +33,7 @@ import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPag 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 { HIDE_ADS } from 'uiSrc/constants/cloud' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import DatabasesList from './components/database-list-component' @@ -74,7 +75,7 @@ const HomePage = () => { const { contextInstanceId } = useSelector(appContextSelector) - const predefinedInstances = enhancedCloudUIFeature?.flag && createDbContent?.cloud_list_of_databases ? [ + const predefinedInstances = enhancedCloudUIFeature?.flag && createDbContent?.cloud_list_of_databases && !HIDE_ADS ? [ { id: CREATE_CLOUD_DB_ID, ...createDbContent.cloud_list_of_databases } as Instance ] : [] const isInstanceExists = instances.length > 0 || predefinedInstances.length > 0 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..62f6b49b7b 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,7 +2,7 @@ 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 { OAuthSsoHandlerDialog, CloudAd } from 'uiSrc/components' import { getUtmExternalLink } from 'uiSrc/utils/links' import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' @@ -43,32 +43,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 + + )} + + + From dfe5553c9e4bfa232b6a2f1564d245df55a687fd Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Wed, 12 Mar 2025 16:44:07 -0500 Subject: [PATCH 02/13] RI-6910: Change to use feature flag --- .../ui/src/components/cloud-ad/CloudAd.tsx | 12 --- redisinsight/ui/src/components/index.ts | 4 +- .../components/user-profile/UserProfile.tsx | 23 ++--- .../FilterNotAvailable.tsx | 87 +++++++++---------- .../ModuleNotLoadedMinimalized.tsx | 6 +- .../module-not-loaded/ModuleNotLoaded.tsx | 10 +-- .../components/create-cloud/CreateCloud.tsx | 7 +- redisinsight/ui/src/config/default.ts | 4 +- redisinsight/ui/src/constants/cloud.ts | 5 -- redisinsight/ui/src/constants/featureFlags.ts | 1 + redisinsight/ui/src/constants/help-texts.tsx | 7 +- redisinsight/ui/src/pages/home/HomePage.tsx | 8 +- .../ConnectivityOptions.tsx | 7 +- redisinsight/ui/src/slices/app/features.ts | 10 ++- 14 files changed, 94 insertions(+), 97 deletions(-) delete mode 100644 redisinsight/ui/src/components/cloud-ad/CloudAd.tsx delete mode 100644 redisinsight/ui/src/constants/cloud.ts diff --git a/redisinsight/ui/src/components/cloud-ad/CloudAd.tsx b/redisinsight/ui/src/components/cloud-ad/CloudAd.tsx deleted file mode 100644 index 5c0f8aa54e..0000000000 --- a/redisinsight/ui/src/components/cloud-ad/CloudAd.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { FC, ReactElement } from 'react' -import { HIDE_ADS } from 'uiSrc/constants/cloud' - -const CloudAd: FC<{ children: ReactElement }> = ({ children }) => { - if (HIDE_ADS) { - return null - } - - return children -} - -export default CloudAd diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts index 2a6bcc2148..d402cce966 100644 --- a/redisinsight/ui/src/components/index.ts +++ b/redisinsight/ui/src/components/index.ts @@ -36,7 +36,6 @@ import { import { FormatedDate } from './formated-date' import { UploadWarning } from './upload-warning' import FormDialog from './form-dialog' -import CloudAd from './cloud-ad/CloudAd' export { FullScreen } from './full-screen' @@ -83,6 +82,5 @@ export { RecommendationBadgesLegend, FormatedDate, UploadWarning, - FormDialog, - CloudAd, + FormDialog } 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 af10ab0777..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 @@ -3,14 +3,15 @@ import { useSelector } from 'react-redux' import { EuiFlexItem } from '@elastic/eui' import { FeatureFlags } from 'uiSrc/constants' import { OAuthSocialSource } from 'uiSrc/slices/interfaces' -import { FeatureFlagComponent, OAuthUserProfile } from 'uiSrc/components' +import { OAuthUserProfile } from 'uiSrc/components' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { CloudUserProfile } from './CloudUserProfile' -import CloudAd from 'uiSrc/components/cloud-ad/CloudAd' const UserProfile = () => { const { [FeatureFlags.envDependent]: envDependentFeature, + [FeatureFlags.cloudAds]: cloudAds, + [FeatureFlags.cloudSso]: cloudSso, } = useSelector(appFeatureFlagsFeaturesSelector) if (!envDependentFeature?.flag) { @@ -21,15 +22,15 @@ const UserProfile = () => { ) } - return ( - - - - - - - - ) + if (cloudAds?.flag && cloudSso?.flag) { + return ( + + + + ) + } + + return null } export default UserProfile 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 352fca482f..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, CloudAd } 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,49 +45,47 @@ 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. - - -
- - {(ssoCloudHandlerClick) => ( - { - ssoCloudHandlerClick(e, { - source: OAuthSocialSource.BrowserFiltering, - action: OAuthSocialAction.Create - }) - onFreeDatabaseClick() - }} - data-testid="get-started-link" - size="s" - > - Get Started For Free - - )} - - - - Learn More - -
- -
+ + + Create a free trial Redis Stack database that supports filtering and extends + the core capabilities of your Redis. + + +
+ + {(ssoCloudHandlerClick) => ( + { + ssoCloudHandlerClick(e, { + source: OAuthSocialSource.BrowserFiltering, + action: OAuthSocialAction.Create + }) + onFreeDatabaseClick() + }} + data-testid="get-started-link" + size="s" + > + Get Started For Free + + )} + + + + Learn More + +
+
)} ) 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 432e7b2f34..23d6a9ef73 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 @@ -6,7 +6,7 @@ import TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg' import { OAuthSocialAction, OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' -import { ExternalLink, FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog, CloudAd } from 'uiSrc/components' +import { ExternalLink, FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog } from 'uiSrc/components' import { getUtmExternalLink } from 'uiSrc/utils/links' import { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links' import { getDbWithModuleLoaded, getSourceTutorialByCapability } from 'uiSrc/utils' @@ -44,7 +44,7 @@ const ModuleNotLoadedMinimalized = (props: Props) => { {moduleText?.text} - + {(ssoCloudHandlerClick) => ( { )} - + <> 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 db9c76ea18..762da51019 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -9,7 +9,7 @@ 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, CloudAd } from 'uiSrc/components' +import { FeatureFlagComponent, OAuthConnectFreeDb, OAuthSsoHandlerDialog } from 'uiSrc/components' import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' import { getUtmExternalLink } from 'uiSrc/utils/links' @@ -73,11 +73,11 @@ 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. @@ -154,7 +154,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps > Learn More - + {(ssoCloudHandlerClick) => ( )} - + )} 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 67b56f2652..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, CloudAd } 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,7 +26,7 @@ const CreateCloud = () => { } return ( - + { - + ) } diff --git a/redisinsight/ui/src/config/default.ts b/redisinsight/ui/src/config/default.ts index f18e41c619..91a2e158ce 100644 --- a/redisinsight/ui/src/config/default.ts +++ b/redisinsight/ui/src/config/default.ts @@ -56,7 +56,6 @@ export const defaultConfig = { useLocalResources: booleanEnv('RI_USE_LOCAL_RESOURCES', false), indexedDbName: process.env.RI_INDEXED_DB_NAME || 'RI_LOCAL_STORAGE', truncatedStringPrefix: process.env.RI_CLIENTS_TRUNCATED_STRING_PREFIX || '[Truncated due to length]', - hideAds: booleanEnv('RI_HIDE_CLOUD_ADS', false), }, workbench: { pipelineCountDefault: intEnv('PIPELINE_COUNT_DEFAULT', 5), @@ -71,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/cloud.ts b/redisinsight/ui/src/constants/cloud.ts deleted file mode 100644 index 1c3a5c3ae0..0000000000 --- a/redisinsight/ui/src/constants/cloud.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getConfig } from 'uiSrc/config' - -const riConfig = getConfig() - -export const HIDE_ADS = riConfig.app.hideAds diff --git a/redisinsight/ui/src/constants/featureFlags.ts b/redisinsight/ui/src/constants/featureFlags.ts index c506965f83..5298671bf8 100644 --- a/redisinsight/ui/src/constants/featureFlags.ts +++ b/redisinsight/ui/src/constants/featureFlags.ts @@ -8,4 +8,5 @@ export enum FeatureFlags { rdi = 'redisDataIntegration', hashFieldExpiration = 'hashFieldExpiration', enhancedCloudUI = 'enhancedCloudUI', + cloudAds = 'cloudAds', } diff --git a/redisinsight/ui/src/constants/help-texts.tsx b/redisinsight/ui/src/constants/help-texts.tsx index 74cdd61f73..1778fe0efa 100644 --- a/redisinsight/ui/src/constants/help-texts.tsx +++ b/redisinsight/ui/src/constants/help-texts.tsx @@ -1,6 +1,7 @@ import React from 'react' import { EuiIcon, EuiText } from '@elastic/eui' -import CloudAd from 'uiSrc/components/cloud-ad/CloudAd' +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' @@ -12,14 +13,14 @@ export default { {' '} here. {' '} - + <>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/pages/home/HomePage.tsx b/redisinsight/ui/src/pages/home/HomePage.tsx index 4a20a4c4b0..8e3f7d2392 100644 --- a/redisinsight/ui/src/pages/home/HomePage.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.tsx @@ -33,7 +33,6 @@ import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPag 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 { HIDE_ADS } from 'uiSrc/constants/cloud' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import DatabasesList from './components/database-list-component' @@ -59,7 +58,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, @@ -75,7 +77,7 @@ const HomePage = () => { const { contextInstanceId } = useSelector(appContextSelector) - const predefinedInstances = enhancedCloudUIFeature?.flag && createDbContent?.cloud_list_of_databases && !HIDE_ADS ? [ + 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 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 62f6b49b7b..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, CloudAd } 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,7 +44,7 @@ const ConnectivityOptions = (props: Props) => { Add databases
- + {(ssoCloudHandlerClick, isSSOEnabled) => ( @@ -70,7 +71,7 @@ const ConnectivityOptions = (props: Props) => { )} - + diff --git a/redisinsight/ui/src/slices/app/features.ts b/redisinsight/ui/src/slices/app/features.ts index 26fee70134..1ac4f3e854 100644 --- a/redisinsight/ui/src/slices/app/features.ts +++ b/redisinsight/ui/src/slices/app/features.ts @@ -52,6 +52,9 @@ export const initialState: StateAppFeatures = { }, [FeatureFlags.envDependent]: { flag: riConfig.features.envDependent.defaultFlag + }, + [FeatureFlags.cloudAds]: { + flag: riConfig.features.cloudAds.defaultFlag } } } @@ -125,12 +128,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 }, From 608695d6d292d90a3bd09203d39ff346e164ae0a Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Fri, 14 Mar 2025 10:57:18 -0500 Subject: [PATCH 03/13] RI-6910: UserProfile tests --- .../instance-header/InstanceHeader.spec.tsx | 14 +++ .../user-profile/CloudUserProfile.tsx | 2 +- .../user-profile/UserProfile.spec.tsx | 107 ++++++++++++++++++ .../user-profile/UserProfileBadge.tsx | 6 +- .../oauth-user-profile/OAuthUserProfile.tsx | 1 + 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx index 429949732e..fb7ffe702a 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx @@ -224,4 +224,18 @@ describe('InstanceHeader', () => { 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('user-profile-badge')).not.toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx index 5477698c2f..77667db297 100644 --- a/redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx @@ -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..4c6489a98d --- /dev/null +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx @@ -0,0 +1,107 @@ +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 = { + envDependant?: boolean; + cloudSso?: boolean; + cloudAds?: boolean; + mockUser?: CloudUser; +} + +const mockStoreStateWithFlags = ({ + envDependant = true, + cloudSso = true, + cloudAds = true, + mockUser = initialMockUser +}: MockStoreStateProps = {}) => { + const keys = ['envDependent', 'cloudSso', 'cloudAds'] + const values = [envDependant, 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({ envDependant: 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({ envDependant: 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({ envDependant: 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({ envDependant: 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/UserProfileBadge.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx index 488c3b99fb..24e72fa25a 100644 --- a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx @@ -22,6 +22,7 @@ import { CloudUser } from 'apiSrc/modules/cloud/user/models' import styles from './styles.module.scss' export interface Props { + "data-testid"?: string; error: Nullable; data: Nullable; handleClickSelectAccount?: (id: number) => void; @@ -37,7 +38,8 @@ const UserProfileBadge = (props: Props) => { data, handleClickSelectAccount, handleClickCloudAccount, - selectingAccountId + selectingAccountId, + 'data-testid': dataTestId, } = props const [isProfileOpen, setIsProfileOpen] = useState(false) @@ -96,7 +98,7 @@ const UserProfileBadge = (props: Props) => { const { accounts, currentAccountId, name } = data return ( -
+
{ data={data} handleClickCloudAccount={handleClickCloudAccount} handleClickSelectAccount={handleClickSelectAccount} + data-testid="oauth-user-profile-badge" /> ) } From 716694621597e1b0e1f6a2faf61a16a4fc602553 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Sat, 15 Mar 2025 08:18:54 -0500 Subject: [PATCH 04/13] RI-6910: Tests --- .../create-cloud/CreateCloud.spec.tsx | 20 ++++++++++++---- .../ui/src/pages/home/HomePage.spec.tsx | 24 +++++++++++++++++-- redisinsight/ui/src/pages/home/HomePage.tsx | 11 ++++++--- 3 files changed, 45 insertions(+), 10 deletions(-) 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/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 8e3f7d2392..cd3896502e 100644 --- a/redisinsight/ui/src/pages/home/HomePage.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.tsx @@ -77,9 +77,14 @@ const HomePage = () => { const { contextInstanceId } = useSelector(appContextSelector) - const predefinedInstances = enhancedCloudUIFeature?.flag && cloudAdsFeature?.flag && createDbContent?.cloud_list_of_databases ? [ - { id: CREATE_CLOUD_DB_ID, ...createDbContent.cloud_list_of_databases } as Instance - ] : [] + 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(() => { From 713cb19703786f9ca6a8bd2666b6a32d83a1d08b Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Sat, 15 Mar 2025 08:20:00 -0500 Subject: [PATCH 05/13] RI-6910: Prettier --- redisinsight/ui/src/pages/home/HomePage.tsx | 92 +++++++++++++-------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/redisinsight/ui/src/pages/home/HomePage.tsx b/redisinsight/ui/src/pages/home/HomePage.tsx index cd3896502e..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 = () => { @@ -71,9 +91,7 @@ const HomePage = () => { deletedSuccessfully: isDeletedInstance, } = useSelector(instancesSelector) - const { - data: editedInstance, - } = useSelector(editedInstanceSelector) + const { data: editedInstance } = useSelector(editedInstanceSelector) const { contextInstanceId } = useSelector(appContextSelector) @@ -82,10 +100,14 @@ const HomePage = () => { cloudAdsFeature?.flag && createDbContent?.cloud_list_of_databases ? [ - { id: CREATE_CLOUD_DB_ID, ...createDbContent.cloud_list_of_databases } as Instance - ] + { + id: CREATE_CLOUD_DB_ID, + ...createDbContent.cloud_list_of_databases, + } as Instance, + ] : [] - const isInstanceExists = instances.length > 0 || predefinedInstances.length > 0 + const isInstanceExists = + instances.length > 0 || predefinedInstances.length > 0 useEffect(() => { setTitle('Redis databases') @@ -95,9 +117,9 @@ const HomePage = () => { dispatch(resetSubscriptionsRedisCloud()) dispatch(fetchCreateRedisButtonsAction()) - return (() => { + return () => { dispatch(setEditedInstance(null)) - }) + } }, []) useEffect(() => { @@ -127,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)) } @@ -138,8 +162,8 @@ const HomePage = () => { sendPageViewTelemetry({ name: TelemetryPageView.DATABASES_LIST_PAGE, eventData: { - instancesCount: instances.length - } + instancesCount: instances.length, + }, }) } @@ -160,8 +184,8 @@ const HomePage = () => { sendEventTelemetry({ event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED, eventData: { - databaseId: editedInstance?.id - } + databaseId: editedInstance?.id, + }, }) } @@ -178,7 +202,7 @@ const HomePage = () => { } sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_ADD_FORM_DISMISSED + event: TelemetryEvent.CONFIG_DATABASES_ADD_FORM_DISMISSED, }) } @@ -195,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) @@ -223,7 +247,7 @@ const HomePage = () => { editedInstance={ openDialog === OpenDialogName.EditDatabase ? editedInstance - : sentinelInstance ?? null + : (sentinelInstance ?? null) } onClose={ openDialog === OpenDialogName.EditDatabase @@ -234,7 +258,7 @@ const HomePage = () => { /> )}
- {(!isInstanceExists && !loading && !loadingChanging ? ( + {!isInstanceExists && !loading && !loadingChanging ? ( @@ -247,7 +271,7 @@ const HomePage = () => { onEditInstance={handleEditInstance} onDeleteInstances={handleDeleteInstances} /> - ))} + )}
From 17dddfb14ca7becc30d51d7f3920dfb4b009a0f3 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Sat, 15 Mar 2025 08:27:24 -0500 Subject: [PATCH 06/13] RI-6910: Tests --- .../instance-header/InstanceHeader.spec.tsx | 7 ++++--- .../ConnectivityOptions.spec.tsx | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx index fb7ffe702a..27e3f81988 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx @@ -189,7 +189,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() @@ -218,7 +218,7 @@ 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() @@ -236,6 +236,7 @@ describe('InstanceHeader', () => { store: mockStore(initialStoreState) }) - expect(screen.queryByTestId('user-profile-badge')).not.toBeInTheDocument() + expect(screen.queryByTestId('oauth-user-profile-badge')).not.toBeInTheDocument() + expect(screen.queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() }) }) 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() + }) }) From 7fec9b5a2eb087ef9c0d2185f42301907ae7f384 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Sat, 15 Mar 2025 10:18:49 -0500 Subject: [PATCH 07/13] RI-6910: Add tests for UserProfileBadge --- .../ui/src/components/config/Config.spec.tsx | 2 +- .../user-profile/UserProfile.spec.tsx | 87 ++++--- .../user-profile/UserProfileBadge.spec.tsx | 230 ++++++++++++++++++ .../user-profile/UserProfileBadge.tsx | 16 +- .../components/redis-logo/RedisLogo.spec.tsx | 4 +- .../src/components/page-header/PageHeader.tsx | 8 +- .../rdi-instance-header/RdiInstanceHeader.tsx | 8 +- .../DatabaseListHeader.tsx | 10 +- .../NotFoundErrorPage.spec.tsx | 4 +- redisinsight/ui/src/slices/app/init.ts | 4 +- .../ui/src/slices/tests/app/init.spec.ts | 2 +- .../home-page-template/HomePageTemplate.tsx | 8 +- 12 files changed, 326 insertions(+), 57 deletions(-) create mode 100644 redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.spec.tsx 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/instance-header/components/user-profile/UserProfile.spec.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx index 4c6489a98d..fdadef4db5 100644 --- 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 @@ -1,36 +1,48 @@ import { cloneDeep, set } from 'lodash' import React from 'react' -import {CloudUser} from 'src/modules/cloud/user/models' +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 { + 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", + name: 'John Smith', currentAccountId: 45, accounts: [ - { id: 45, name: "Account 1" }, - { id: 46, name: "Account 2" }, + { + id: 45, + name: 'Account 1', + }, + { + id: 46, + name: 'Account 2', + }, ], - data: {} + data: {}, } type MockStoreStateProps = { - envDependant?: boolean; - cloudSso?: boolean; - cloudAds?: boolean; - mockUser?: CloudUser; + envDependent?: boolean + cloudSso?: boolean + cloudAds?: boolean + mockUser?: CloudUser } const mockStoreStateWithFlags = ({ - envDependant = true, + envDependent = true, cloudSso = true, cloudAds = true, - mockUser = initialMockUser + mockUser = initialMockUser, }: MockStoreStateProps = {}) => { const keys = ['envDependent', 'cloudSso', 'cloudAds'] - const values = [envDependant, cloudSso, cloudAds] + const values = [envDependent, cloudSso, cloudAds] const initialStoreState = cloneDeep(initialStateDefault) @@ -38,12 +50,20 @@ const mockStoreStateWithFlags = ({ set( initialStoreState, `app.features.featureFlags.features.${FeatureFlags[keys[i] as keyof typeof FeatureFlags]}`, - { flag: values[i] } + { flag: values[i] }, ) } - set(initialStoreState, 'user.cloudProfile', { error: '', data: mockUser }) - set(initialStoreState, 'oauth.cloud.user', { error: '', loading: false, initialLoading: false, data: mockUser }) + set(initialStoreState, 'user.cloudProfile', { + error: '', + data: mockUser, + }) + set(initialStoreState, 'oauth.cloud.user', { + error: '', + loading: false, + initialLoading: false, + data: mockUser, + }) return mockStore(initialStoreState) } @@ -56,18 +76,24 @@ describe('UserProfile', () => { }) it('should render CloudUserProfile if envDependentFeature is disabled', () => { - store = mockStoreStateWithFlags({ envDependant: false}) + store = mockStoreStateWithFlags({ envDependent: false }) const { getByTestId } = render(, { - store + store, }) expect(getByTestId('cloud-user-profile-badge')).toBeInTheDocument() }) it('should not show cloud user profile badge profile name is empty', () => { - store = mockStoreStateWithFlags({ envDependant: false, mockUser: { ...initialMockUser, name: '' } }) + store = mockStoreStateWithFlags({ + envDependent: false, + mockUser: { + ...initialMockUser, + name: '', + }, + }) const { queryByTestId } = render(, { - store + store, }) expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() @@ -76,16 +102,20 @@ describe('UserProfile', () => { it('should render OAuthUserProfile if envDependentFeature is enabled', () => { store = mockStoreStateWithFlags() const { queryByTestId } = render(, { - store + store, }) expect(queryByTestId('oauth-user-profile-badge')).toBeInTheDocument() }) it('should render nothing when envDependent=true, cloudAds=false, cloudSso=true', () => { - store = mockStoreStateWithFlags({ envDependant: true, cloudAds: false, cloudSso: true }) + store = mockStoreStateWithFlags({ + envDependent: true, + cloudAds: false, + cloudSso: true, + }) const { queryByTestId } = render(, { - store + store, }) expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument() @@ -93,15 +123,16 @@ describe('UserProfile', () => { }) it('should render nothing when envDependent=true, cloudAds=true, cloudSso=false', () => { - store = mockStoreStateWithFlags({ envDependant: true, cloudAds: true, cloudSso: false }) + store = mockStoreStateWithFlags({ + envDependent: true, + cloudAds: true, + cloudSso: false, + }) const { queryByTestId } = render(, { - store + 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/UserProfileBadge.spec.tsx b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.spec.tsx new file mode 100644 index 0000000000..5cf0f2959c --- /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 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 index 24e72fa25a..914326c56f 100644 --- a/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx +++ b/redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx @@ -21,7 +21,7 @@ import { getConfig } from 'uiSrc/config' import { CloudUser } from 'apiSrc/modules/cloud/user/models' import styles from './styles.module.scss' -export interface Props { +export interface UserProfileBadgeProps { "data-testid"?: string; error: Nullable; data: Nullable; @@ -32,7 +32,7 @@ export interface Props { const riConfig = getConfig() -const UserProfileBadge = (props: Props) => { +const UserProfileBadge = (props: UserProfileBadgeProps) => { const { error, data, @@ -98,7 +98,7 @@ const UserProfileBadge = (props: Props) => { const { accounts, currentAccountId, name } = data return ( -
+
{
Account} + otherwise={Account} > - Redis Cloud account + Redis Cloud account -
+
{accounts?.map(({ name, id }) => (
{ {name} #{id} - {id === currentAccountId && ()} - {id === selectingAccountId && ()} + {id === currentAccountId && ()} + {id === selectingAccountId && ()}
))}
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/page-header/PageHeader.tsx b/redisinsight/ui/src/components/page-header/PageHeader.tsx index 60a0f53831..d74b403f0a 100644 --- a/redisinsight/ui/src/components/page-header/PageHeader.tsx +++ b/redisinsight/ui/src/components/page-header/PageHeader.tsx @@ -70,9 +70,11 @@ 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..5cb8a73142 100644 --- a/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx +++ b/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx @@ -76,9 +76,11 @@ const RdiInstanceHeader = () => { - - - + + + + + ) 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 870139d579..e0659c17cd 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 @@ -13,7 +13,7 @@ import { instancesSelector } from 'uiSrc/slices/instances/instances' import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' import PromoLink from 'uiSrc/components/promo-link/PromoLink' -import { OAuthSsoHandlerDialog } from 'uiSrc/components' +import { FeatureFlagComponent, OAuthSsoHandlerDialog} from 'uiSrc/components' import { getPathToResource } from 'uiSrc/services/resourcesService' import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content' import { HELP_LINKS } from 'uiSrc/pages/home/constants' @@ -135,9 +135,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/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..67578f70b3 100644 --- a/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx +++ b/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx @@ -38,9 +38,11 @@ const HomePageTemplate = (props: Props) => { )} - - - + + + + +
From a0742868120df418a95d4bdffc18d23f0820715a Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Mon, 17 Mar 2025 13:00:59 -0500 Subject: [PATCH 08/13] RI-6910: add support for multiple feature flags to FeatureFlagComponent --- .../FeatureFlagComponent.spec.tsx | 182 ++++++++++++++---- .../FeatureFlagComponent.tsx | 14 +- .../src/components/page-header/PageHeader.tsx | 10 +- .../rdi-instance-header/RdiInstanceHeader.tsx | 10 +- .../home-page-template/HomePageTemplate.tsx | 10 +- 5 files changed, 163 insertions(+), 63 deletions(-) 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/page-header/PageHeader.tsx b/redisinsight/ui/src/components/page-header/PageHeader.tsx index d74b403f0a..f21e907459 100644 --- a/redisinsight/ui/src/components/page-header/PageHeader.tsx +++ b/redisinsight/ui/src/components/page-header/PageHeader.tsx @@ -69,12 +69,10 @@ 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 5cb8a73142..ccf57c7d4b 100644 --- a/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx +++ b/redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx @@ -75,12 +75,10 @@ const RdiInstanceHeader = () => { - - - - - - + + + + ) diff --git a/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx b/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx index 67578f70b3..5d2bcdff87 100644 --- a/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx +++ b/redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx @@ -37,12 +37,10 @@ const HomePageTemplate = (props: Props) => { )} - - - - - - + + + +
From 6887ccbde4c3ef5010b379654ea8893ba1e555ca Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Mon, 17 Mar 2025 16:18:01 -0500 Subject: [PATCH 09/13] RI-6910: Update screens for module not available --- .../ModuleNotLoadedMinimalized.tsx | 82 +++++++----- .../constants.ts | 22 ++++ .../module-not-loaded/ModuleNotLoaded.tsx | 113 ++++++---------- .../ModuleNotLoadedButton.tsx | 124 ++++++++++++++++++ 4 files changed, 235 insertions(+), 106 deletions(-) create mode 100644 redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoadedButton.tsx 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 23d6a9ef73..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,13 +46,32 @@ const ModuleNotLoadedMinimalized = (props: Props) => {
{moduleText?.title}
- {!freeDbWithModule && ( - <> + {moduleText?.text} - + { + history.push(Pages.home) + }} + > + Redis Databases page + + } + > + {!freeDbWithModule ? ( + <> + + {moduleText?.text} + + {(ssoCloudHandlerClick) => ( { )} - - - <> - - - Start with Docker - - - - - )} - {!!freeDbWithModule && ( - <> - - Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. - - - - - )} + + ) : ( + <> + + Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. + + + + + )} +
( 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,25 +81,25 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps } }) - const renderText = useCallback((moduleName?: string) => (!freeDbWithModule ? ( - + const renderText = useCallback((moduleName?: string) => { + if (!cloudAdsFeature?.flag) { + return ( + + Open a database with {moduleName}. + + ) + } + + return (!freeDbWithModule ? ( - {`Create a free trial Redis Stack database with ${moduleName} which extends the core capabilities of your Redis`} + 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 utmCampaign = type === 'browser' - ? UTM_CAMPAINGS[OAuthSocialSource.BrowserSearch] - : UTM_CAMPAINGS[OAuthSocialSource.Workbench] + ) : ( + + Use your free trial all-in-one Redis Cloud database to start exploring these capabilities. + + )) + }, [freeDbWithModule]) return (
- {!!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 From 7ec609a5ac3bd33b06d73880cd8f86cae6930e02 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Tue, 18 Mar 2025 13:28:02 -0500 Subject: [PATCH 10/13] RI-6910: Add tests --- .../ModuleNotLoadedMinimalized.spec.tsx | 25 +++- .../constants.ts | 4 +- .../ModuleNotLoaded.spec.tsx | 124 +++++++++++++++++- .../module-not-loaded/ModuleNotLoaded.tsx | 8 +- redisinsight/ui/src/utils/test-utils.tsx | 12 ++ 5 files changed, 160 insertions(+), 13 deletions(-) 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/constants.ts b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts index 8c1c629050..37cff77756 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts +++ b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts @@ -28,7 +28,7 @@ export const MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE: { [key in RedisDef } } = { [RedisDefaultModules.Bloom]: { title: 'Probabilistic data structures are not available', - text: 'Open a database with Probabilistic data structures.' + text: 'Open a database with probabilistic data structures.' }, [RedisDefaultModules.ReJSON]: { title: 'JSON data structure is not available', @@ -40,6 +40,6 @@ export const MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE: { [key in RedisDef }, [RedisDefaultModules.TimeSeries]: { title: 'Time series data structure is not available', - text: 'Open a database with Time series data structure.' + text: 'Open a database with time series data structure.' }, } diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx index 2d0e19e96a..c8570c85a2 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx @@ -1,12 +1,128 @@ import React from 'react' -import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import reactRouterDom from 'react-router-dom' +import { render, mockFeatureFlags, act, fireEvent} from 'uiSrc/utils/test-utils' +import * as utils from 'uiSrc/utils/modules' +import { Instance, RedisDefaultModules } from 'uiSrc/slices/interfaces' import ModuleNotLoaded, { IProps } from './ModuleNotLoaded' -const mockedProps = mock() +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() }) }) 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 edcb68f661..387a0c7c69 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -18,8 +18,8 @@ import { freeInstancesSelector } from 'uiSrc/slices/instances/instances' import { getDbWithModuleLoaded } from 'uiSrc/utils' import { useCapability } from 'uiSrc/services' -import {appFeatureFlagsFeaturesSelector} from 'uiSrc/slices/app/features' -import ModuleNotLoadedButton from 'uiSrc/components/messages/module-not-loaded/ModuleNotLoadedButton' +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 } = { @@ -122,7 +122,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps /> )}
-
+
{renderTitle(width, MODULE_TEXT_VIEW[moduleName])} {CONTENT[moduleName]?.text.map((item: string) => ( @@ -144,7 +144,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps {renderText(MODULE_TEXT_VIEW[moduleName])}
-
+
{freeDbWithModule ? ( { 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 From dfdf56e361e2797552d27447ae861f96e1631657 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Tue, 18 Mar 2025 13:40:26 -0500 Subject: [PATCH 11/13] RI-6910: updating manual build workflow --- .github/workflows/build.yml | 5 + .github/workflows/manual-build-enterprise.yml | 110 ++++++++++++++++++ .github/workflows/manual-build.yml | 1 + .github/workflows/pipeline-build-macos.yml | 5 + 4 files changed, 121 insertions(+) create mode 100644 .github/workflows/manual-build-enterprise.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e4071513c..6e449f3e13 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' @@ -36,6 +40,7 @@ jobs: environment: ${{ inputs.environment }} target: ${{ inputs.target }} debug: ${{ inputs.debug }} + enterprise: ${{ inputs.enterprise }} build-windows: if: contains(inputs.target, 'windows') || inputs.target == 'all' diff --git a/.github/workflows/manual-build-enterprise.yml b/.github/workflows/manual-build-enterprise.yml new file mode 100644 index 0000000000..f6963b7e46 --- /dev/null +++ b/.github/workflows/manual-build-enterprise.yml @@ -0,0 +1,110 @@ +name: 🚀 Manual build - Enterprise + +on: + push: + branches: [ 'RI-6910-hide-ads' ] + # 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 +# +# enterprise: +# description: Enterprise build +# 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: "build_macos_arm64" + debug: false + environment: development + 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-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' }} From a184dd71a07f06a42f854386a43554e429f11bd4 Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Tue, 18 Mar 2025 17:52:20 -0500 Subject: [PATCH 12/13] RI-6910: enterprise build workflow --- .github/workflows/build.yml | 3 + .github/workflows/manual-build-enterprise.yml | 170 +++++++++--------- .github/workflows/pipeline-build-docker.yml | 5 + .github/workflows/pipeline-build-linux.yml | 5 + .github/workflows/pipeline-build-windows.yml | 5 + 5 files changed, 100 insertions(+), 88 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e449f3e13..f66f169018 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,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' @@ -49,6 +50,7 @@ jobs: with: environment: ${{ inputs.environment }} debug: ${{ inputs.debug }} + enterprise: ${{ inputs.enterprise }} build-docker: if: contains(inputs.target, 'docker') || inputs.target == 'all' @@ -57,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 index f6963b7e46..ef087231fa 100644 --- a/.github/workflows/manual-build-enterprise.yml +++ b/.github/workflows/manual-build-enterprise.yml @@ -1,56 +1,50 @@ name: 🚀 Manual build - Enterprise on: - push: - branches: [ 'RI-6910-hide-ads' ] # 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 -# -# enterprise: -# description: Enterprise build -# type: boolean + 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: @@ -58,53 +52,53 @@ concurrency: 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 }} + 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 + needs: get-selected uses: ./.github/workflows/build.yml secrets: inherit with: - target: "build_macos_arm64" - debug: false - environment: development + 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 + 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/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-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' }} From 5dd6c142942f9b0e4d1aab2cb5cb439ba48abc3f Mon Sep 17 00:00:00 2001 From: seth-riedel Date: Thu, 20 Mar 2025 15:40:26 -0500 Subject: [PATCH 13/13] RI-6910: Change casing of module names --- .../ModuleNotLoaded.spec.tsx | 87 +++++++++++++++---- .../module-not-loaded/ModuleNotLoaded.tsx | 4 +- .../ui/src/constants/workbenchResults.ts | 4 +- 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx index c8570c85a2..33719c8181 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx @@ -1,6 +1,11 @@ import React from 'react' import reactRouterDom from 'react-router-dom' -import { render, mockFeatureFlags, act, fireEvent} from 'uiSrc/utils/test-utils' +import { + act, + fireEvent, + mockFeatureFlags, + render, +} from 'uiSrc/utils/test-utils' import * as utils from 'uiSrc/utils/modules' import { Instance, RedisDefaultModules } from 'uiSrc/slices/interfaces' import ModuleNotLoaded, { IProps } from './ModuleNotLoaded' @@ -9,7 +14,7 @@ const props: IProps = { moduleName: RedisDefaultModules.Search, type: 'browser', id: 'id', - onClose: jest.fn() + onClose: jest.fn(), } const mockUseHistory = { push: jest.fn() } @@ -19,18 +24,22 @@ jest.mock('react-router-dom', () => ({ push: jest.fn, }), })) -jest.mock('uiSrc/assets/img/icons/mobile_module_not_loaded.svg?react', () => 'div') +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) + jest + .spyOn(utils, 'getDbWithModuleLoaded') + .mockImplementation(() => value as unknown as Instance) } const TEST_IDS = { - ctaWrapper: 'module-not-loaded-cta-wrapper' + ctaWrapper: 'module-not-loaded-cta-wrapper', } describe('ModuleNotLoaded', () => { @@ -46,31 +55,37 @@ describe('ModuleNotLoaded', () => { 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() + 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() + 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() + 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() @@ -86,9 +101,11 @@ describe('ModuleNotLoaded', () => { mockFeatureFlags({ cloudAds: { flag: false, - } + }, }) - jest.spyOn(reactRouterDom, 'useHistory').mockImplementation(() => mockUseHistory as unknown as any) + jest + .spyOn(reactRouterDom, 'useHistory') + .mockImplementation(() => mockUseHistory as unknown as any) const { queryByText, getByText } = render() const databasesButton = getByText(/Redis Databases page/) @@ -109,20 +126,56 @@ describe('ModuleNotLoaded', () => { mockFeatureFlags({ cloudAds: { flag: false, - } + }, }) const { getByText } = render() - expect(getByText(/Open a database with Redis Query Engine/)).toBeInTheDocument() + 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() + 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() + 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 387a0c7c69..a1de9a27dd 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -42,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

@@ -85,7 +85,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps if (!cloudAdsFeature?.flag) { return ( - Open a database with {moduleName}. + Open a database with {moduleName?.toLowerCase()}. ) } 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', }