From 20d42b8c8fddbd0cff5307aff80ccf884d1f9d7f Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 22 May 2026 15:52:28 +0300 Subject: [PATCH 01/13] feat(ui): visual environment indicators in database list and header Adds PROD badge and Development label, gated by the dev-prodMode feature flag, in the databases-list rows and the top instance header. Renders a subtle red gradient on the InstanceHeader container when the active connection is classified as Production. Extends the connect-to-database telemetry event with the database environment. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../EnvironmentBadge.spec.tsx | 87 ++++++++++++ .../environment-badge/EnvironmentBadge.tsx | 64 +++++++++ .../src/components/environment-badge/index.ts | 2 + .../instance-header/InstanceHeader.spec.tsx | 89 ++++++++++++ .../instance-header/InstanceHeader.styles.ts | 18 +++ .../instance-header/InstanceHeader.tsx | 19 ++- .../instances-list/InstancesList.tsx | 1 + .../OAuthConnectFreeDb.spec.tsx | 1 + .../OAuthConnectFreeDb.tsx | 4 +- .../DatabasesListCellName.spec.tsx | 129 ++++++++++++++++++ .../DatabasesListCellName.tsx | 8 ++ .../databases-list/methods/handlers.spec.ts | 2 + .../databases-list/methods/handlers.ts | 3 +- .../components/db-status/DbStatus.spec.tsx | 48 +++++++ .../home/components/db-status/DbStatus.tsx | 23 +++- 15 files changed, 489 insertions(+), 9 deletions(-) create mode 100644 redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx create mode 100644 redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx create mode 100644 redisinsight/ui/src/components/environment-badge/index.ts create mode 100644 redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts create mode 100644 redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx new file mode 100644 index 0000000000..a4a430392f --- /dev/null +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx @@ -0,0 +1,87 @@ +import { cloneDeep, set } from 'lodash' +import React from 'react' + +import { Environment } from 'apiClient' +import { FeatureFlags } from 'uiSrc/constants' +import { + initialStateDefault, + mockStore, + render, + screen, +} from 'uiSrc/utils/test-utils' + +import { EnvironmentBadge } from './EnvironmentBadge' + +const withDevProdFlag = (flag: boolean) => { + const state = set( + cloneDeep(initialStateDefault), + `app.features.featureFlags.features.${FeatureFlags.devProdMode}`, + { flag }, + ) + return { store: mockStore(state) } +} + +describe('EnvironmentBadge', () => { + it('renders the PROD badge for production environment', () => { + render( + , + withDevProdFlag(true), + ) + + expect( + screen.getByTestId(`environment-badge-${Environment.Production}`), + ).toBeInTheDocument() + expect(screen.getByText('PROD')).toBeInTheDocument() + }) + + it('renders the Development label for development environment', () => { + render( + , + withDevProdFlag(true), + ) + + expect( + screen.getByTestId(`environment-badge-${Environment.Development}`), + ).toBeInTheDocument() + expect(screen.getByText('Development')).toBeInTheDocument() + }) + + it('renders nothing for unspecified environment', () => { + const { container } = render( + , + withDevProdFlag(true), + ) + + expect(container).toBeEmptyDOMElement() + }) + + it('renders nothing when environment is undefined', () => { + const { container } = render( + , + withDevProdFlag(true), + ) + + expect(container).toBeEmptyDOMElement() + }) + + it('renders nothing when the dev-prodMode feature flag is off', () => { + const { container } = render( + , + withDevProdFlag(false), + ) + + expect(container).toBeEmptyDOMElement() + }) + + it('uses the provided dataTestId override', () => { + render( + , + withDevProdFlag(true), + ) + + expect(screen.getByTestId('custom-badge')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx new file mode 100644 index 0000000000..fb3174c6b2 --- /dev/null +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { Environment } from 'apiClient' + +import { appFeatureFlagDevProdModeSelector } from 'uiSrc/slices/app/features' +import { + RiBadge, + BadgeVariants, +} from 'uiSrc/components/base/display/badge/RiBadge' +import { RiTooltip } from 'uiSrc/components/base/tooltip/RITooltip' + +export interface EnvironmentBadgeProps { + environment?: Environment + dataTestId?: string +} + +interface EnvironmentBadgeConfig { + label: string + variant: BadgeVariants + tooltip: React.ReactNode +} + +const PRODUCTION_TOOLTIP = + 'Production — Adds an extra layer of protection to prevent unintended changes. Includes additional confirmation dialogs before modifying data and stronger friction before running dangerous commands.' + +const DEVELOPMENT_TOOLTIP = + 'Development — Skips standard confirmation dialogs when modifying data, for faster work on development and test databases.' + +const BADGE_CONFIG: Partial> = { + [Environment.Production]: { + label: 'PROD', + variant: 'danger', + tooltip: PRODUCTION_TOOLTIP, + }, + [Environment.Development]: { + label: 'Development', + variant: 'default', + tooltip: DEVELOPMENT_TOOLTIP, + }, +} + +export const EnvironmentBadge = ({ + environment, + dataTestId, +}: EnvironmentBadgeProps) => { + const flagEnabled = useSelector(appFeatureFlagDevProdModeSelector) + + if (!flagEnabled || !environment) return null + + const config = BADGE_CONFIG[environment] + if (!config) return null + + return ( + + + + ) +} + +export default EnvironmentBadge diff --git a/redisinsight/ui/src/components/environment-badge/index.ts b/redisinsight/ui/src/components/environment-badge/index.ts new file mode 100644 index 0000000000..9ae4db02fb --- /dev/null +++ b/redisinsight/ui/src/components/environment-badge/index.ts @@ -0,0 +1,2 @@ +export { EnvironmentBadge } from './EnvironmentBadge' +export type { EnvironmentBadgeProps } from './EnvironmentBadge' diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx index 824e995b71..2f207891ac 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx @@ -2,6 +2,7 @@ import { cloneDeep, set } from 'lodash' import React from 'react' import reactRouterDom from 'react-router-dom' import { instance, mock } from 'ts-mockito' +import { Environment } from 'apiClient' import { cleanup, act, @@ -256,6 +257,94 @@ describe('InstanceHeader', () => { ).toHaveTextContent('Test account #40') }) + describe('environment indicator', () => { + const renderWithEnv = (env: Environment) => { + ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + username: 'username', + id: 'instanceId', + loading: false, + environment: env, + }) + + const state = set( + cloneDeep(initialStateDefault), + `app.features.featureFlags.features.${FeatureFlags.devProdMode}`, + { flag: true }, + ) + + return render(, { + store: mockStore(state), + }) + } + + it('renders the PROD badge and marks the header as production', () => { + renderWithEnv(Environment.Production) + + expect(screen.getByTestId('instance-header')).toHaveAttribute( + 'data-environment', + Environment.Production, + ) + expect( + screen.getByTestId(`environment-badge-${Environment.Production}`), + ).toBeInTheDocument() + }) + + it('renders the Development label without the production data attribute', () => { + renderWithEnv(Environment.Development) + + expect(screen.getByTestId('instance-header')).toHaveAttribute( + 'data-environment', + Environment.Development, + ) + expect( + screen.getByTestId(`environment-badge-${Environment.Development}`), + ).toBeInTheDocument() + }) + + it('renders no environment badge for Unspecified', () => { + renderWithEnv(Environment.Unspecified) + + expect(screen.getByTestId('instance-header')).toHaveAttribute( + 'data-environment', + Environment.Unspecified, + ) + expect( + screen.queryByTestId(`environment-badge-${Environment.Production}`), + ).not.toBeInTheDocument() + expect( + screen.queryByTestId(`environment-badge-${Environment.Development}`), + ).not.toBeInTheDocument() + }) + + it('does not render the badge when the dev-prodMode flag is off', () => { + ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + username: 'username', + id: 'instanceId', + loading: false, + environment: Environment.Production, + }) + + const state = set( + cloneDeep(initialStateDefault), + `app.features.featureFlags.features.${FeatureFlags.devProdMode}`, + { flag: false }, + ) + + render(, { + store: mockStore(state), + }) + + expect( + screen.queryByTestId(`environment-badge-${Environment.Production}`), + ).not.toBeInTheDocument() + // hook returns Unspecified when the flag is off, even if the stored env is Production + expect(screen.getByTestId('instance-header')).toHaveAttribute( + 'data-environment', + Environment.Unspecified, + ) + }) + }) + it('should not show sso user profile if cloud ads feature is off', async () => { const initialStoreState = set( cloneDeep(initialStateDefault), diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts new file mode 100644 index 0000000000..11bf67f30a --- /dev/null +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts @@ -0,0 +1,18 @@ +import styled, { css } from 'styled-components' + +interface InstanceHeaderContainerProps { + $isProductionEnv?: boolean +} + +const productionGradient = css` + background: linear-gradient( + 90deg, + ${({ theme }) => theme.semantic.color.background.danger200} 0%, + ${({ theme }) => theme.semantic.color.background.danger100} 25%, + transparent 60% + ); +` + +export const InstanceHeaderContainer = styled.div` + ${({ $isProductionEnv }) => $isProductionEnv && productionGradient} +` diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx index f20d844234..0dcf70460b 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx @@ -3,8 +3,11 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import cx from 'classnames' import { useTheme } from '@redis-ui/styles' +import { Environment } from 'apiClient' import { FeatureFlags, Pages } from 'uiSrc/constants' +import { useDatabaseEnvironment } from 'uiSrc/components/hooks/useDatabaseEnvironment' +import { EnvironmentBadge } from 'uiSrc/components/environment-badge' import { selectOnFocus } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { BuildType } from 'uiSrc/constants/env' @@ -46,6 +49,7 @@ import { NumericInput } from 'uiSrc/components/base/inputs' import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' import { Link } from 'uiSrc/components/base/link/Link' import InstancesNavigationPopover from './components/instances-navigation-popover' +import { InstanceHeaderContainer } from './InstanceHeader.styles' import styles from './styles.module.scss' const riConfig = getConfig() @@ -82,6 +86,8 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => { databaseChatFeature, documentationChatFeature, ]) + const { environment } = useDatabaseEnvironment() + const isProductionEnv = environment === Environment.Production const history = useHistory() const [dbIndex, setDbIndex] = useState(String(db || 0)) @@ -133,11 +139,14 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => { } return ( -
{ )} + + + {databases > 1 && (
{ -
+ ) } diff --git a/redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/InstancesList.tsx b/redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/InstancesList.tsx index e7c602c5d5..dc35bc3cc1 100644 --- a/redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/InstancesList.tsx +++ b/redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/InstancesList.tsx @@ -72,6 +72,7 @@ const InstancesList = ({ databaseId: instance.id, source: 'navigation_panel', provider: instance.provider, + environment: instance.environment, ...modulesSummary, ...infoData, }, diff --git a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx index 8ac9ac0fe9..d763df6acf 100644 --- a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx @@ -68,6 +68,7 @@ describe('OAuthConnectFreeDb', () => { databaseId: 'providedId', provider: undefined, source: OAuthSocialSource.ListOfDatabases, + environment: 'unspecified', ...getRedisModulesSummary(), ...MOCK_ADDITIONAL_INFO, }, diff --git a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx index c2426dc252..de177eb1f5 100644 --- a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx @@ -37,7 +37,8 @@ const OAuthConnectFreeDb = ({ className, }: Props) => { const { loading } = useSelector(instancesSelector) ?? {} - const { modules, provider } = useSelector(connectedInstanceSelector) ?? {} + const { modules, provider, environment } = + useSelector(connectedInstanceSelector) ?? {} const [firstFreeInstance] = useSelector(freeInstancesSelector) ?? [] const targetDatabaseId = id || firstFreeInstance?.id @@ -58,6 +59,7 @@ const OAuthConnectFreeDb = ({ databaseId: targetDatabaseId, provider, source, + environment, ...modulesSummary, ...infoData, }, diff --git a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx new file mode 100644 index 0000000000..63e7f13332 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx @@ -0,0 +1,129 @@ +import { cloneDeep, set } from 'lodash' +import React from 'react' + +import { Environment } from 'apiClient' +import { Instance } from 'uiSrc/slices/interfaces' +import { FeatureFlags } from 'uiSrc/constants' +import { + initialStateDefault, + mockStore, + render, + screen, +} from 'uiSrc/utils/test-utils' + +import DatabasesListCellName from './DatabasesListCellName' + +const renderCell = ( + instance: Partial, + { devProdMode = true }: { devProdMode?: boolean } = {}, +) => { + const state = set( + cloneDeep(initialStateDefault), + `app.features.featureFlags.features.${FeatureFlags.devProdMode}`, + { flag: devProdMode }, + ) + + const cellProps = { row: { original: instance as Instance } } as any + return render(, { + store: mockStore(state), + }) +} + +describe('DatabasesListCellName', () => { + const baseInstance: Partial = { + id: 'db-1', + name: 'My Database', + db: 0, + new: false, + cloudDetails: undefined, + } + + describe('environment badge', () => { + it('renders the PROD badge when environment is Production', () => { + renderCell({ ...baseInstance, environment: Environment.Production }) + + expect(screen.getByTestId('environment-badge-db-1')).toBeInTheDocument() + expect(screen.getByText('PROD')).toBeInTheDocument() + }) + + it('renders the Development label when environment is Development', () => { + renderCell({ ...baseInstance, environment: Environment.Development }) + + expect(screen.getByTestId('environment-badge-db-1')).toBeInTheDocument() + expect(screen.getByText('Development')).toBeInTheDocument() + }) + + it('renders no badge when environment is Unspecified', () => { + renderCell({ ...baseInstance, environment: Environment.Unspecified }) + + expect( + screen.queryByTestId('environment-badge-db-1'), + ).not.toBeInTheDocument() + expect(screen.queryByText('PROD')).not.toBeInTheDocument() + expect(screen.queryByText('Development')).not.toBeInTheDocument() + }) + + it('does not render the badge when the dev-prodMode flag is off', () => { + renderCell( + { ...baseInstance, environment: Environment.Production }, + { devProdMode: false }, + ) + + expect( + screen.queryByTestId('environment-badge-db-1'), + ).not.toBeInTheDocument() + }) + }) + + describe('temporary indicator precedence', () => { + it('shows the "new" temp indicator instead of the PROD badge', () => { + renderCell({ + ...baseInstance, + new: true, + environment: Environment.Production, + }) + + expect(screen.getByTestId('database-status-new-db-1')).toBeInTheDocument() + expect( + screen.queryByTestId('environment-badge-db-1'), + ).not.toBeInTheDocument() + }) + + it('shows the cloud warning indicator instead of the Development label', () => { + const fiveDaysAgo = new Date() + fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5) + + renderCell({ + ...baseInstance, + lastConnection: fiveDaysAgo, + cloudDetails: { free: true } as Instance['cloudDetails'], + environment: Environment.Development, + }) + + expect( + screen.queryByTestId('environment-badge-db-1'), + ).not.toBeInTheDocument() + }) + + it('falls back to the env badge once the temp indicator clears', () => { + renderCell({ + ...baseInstance, + new: false, + environment: Environment.Production, + }) + + expect( + screen.queryByTestId('database-status-new-db-1'), + ).not.toBeInTheDocument() + expect(screen.getByTestId('environment-badge-db-1')).toBeInTheDocument() + }) + }) + + it('renders the database name', () => { + renderCell({ ...baseInstance, name: 'production-cache', db: 3 }) + + expect(screen.getByTestId('instance-name-db-1')).toHaveTextContent( + 'production-cache', + ) + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx index f7ffecc3de..3150f1eb5e 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx @@ -5,6 +5,7 @@ import { RiTooltip } from 'uiSrc/components' import { replaceSpaces, formatLongName, getDbIndex } from 'uiSrc/utils' import DbStatus from 'uiSrc/pages/home/components/db-status' import { Text } from 'uiSrc/components/base/text' +import { EnvironmentBadge } from 'uiSrc/components/environment-badge' import { StyledCellNameWrapper } from './DatabasesListCellName.styles' @@ -18,6 +19,7 @@ const DatabasesListCellName: IDatabaseListCell = ({ row }) => { lastConnection, createdAt, cloudDetails, + environment, } = instance const cellContent = replaceSpaces(name.substring(0, 200)) @@ -30,6 +32,12 @@ const DatabasesListCellName: IDatabaseListCell = ({ row }) => { lastConnection={lastConnection} createdAt={createdAt} isFree={cloudDetails?.free} + fallback={ + + } />
diff --git a/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts b/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts index 0aa665bd27..d3397795fc 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts +++ b/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts @@ -73,6 +73,7 @@ const instance = { id: '1', provider: 'LOCALHOST', modules: [], + environment: 'production', } describe('databases-list methods/handlers', () => { @@ -97,6 +98,7 @@ describe('databases-list methods/handlers', () => { eventData: expect.objectContaining({ databaseId: '1', provider: 'LOCALHOST', + environment: 'production', }), }), ) diff --git a/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.ts b/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.ts index 4cd2d87086..b080e7646a 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.ts +++ b/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.ts @@ -32,7 +32,7 @@ const connectToInstance = (id: string) => { } export const handleCheckConnectToInstance = async (instance: Instance) => { - const { id, provider, modules } = instance + const { id, provider, modules, environment } = instance const { contextInstanceId } = appContextSelector(store.getState()) dispatch( @@ -53,6 +53,7 @@ export const handleCheckConnectToInstance = async (instance: Instance) => { databaseId: id, provider, source: 'db_list', + environment, ...modulesSummary, ...infoData, }, diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx index a93689d4ab..f872ca66db 100644 --- a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx @@ -98,6 +98,54 @@ describe('DbStatus', () => { ).not.toBeInTheDocument() }) + describe('fallback', () => { + it('renders the fallback when no temporary indicator applies', () => { + render( + } + />, + ) + + expect(screen.getByTestId('env-fallback')).toBeInTheDocument() + }) + + it('renders the temporary indicator instead of the fallback when "isNew" is true', () => { + render( + } + />, + ) + + expect(screen.getByTestId('database-status-new-1')).toBeInTheDocument() + expect(screen.queryByTestId('env-fallback')).not.toBeInTheDocument() + }) + + it('renders the temporary indicator instead of the fallback for an unused cloud db', () => { + const lastConnection = new Date(Date.now() - daysToMs(5)) + render( + } + />, + ) + + expect( + screen.getByTestId(`database-status-${WarningTypes.TryDatabase}-1`), + ).toBeInTheDocument() + expect(screen.queryByTestId('env-fallback')).not.toBeInTheDocument() + }) + }) + it('should render new status', async () => { const sendEventTelemetryMock = jest.fn() ;(sendEventTelemetry as jest.Mock).mockImplementation( diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx index f499fcc901..714800a7fe 100644 --- a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx +++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx @@ -21,12 +21,21 @@ import { } from './texts' import { IconWrapper, InfoIcon } from './DbStatus.styles' +const LAST_CONNECTION_SM = 3 +const LAST_CONNECTION_L = 16 + export interface Props { id: string lastConnection: Maybe createdAt: Maybe isNew: boolean isFree?: boolean + /** + * Rendered when no temporary indicator (cloud warning / "new" badge) applies. + * Lets the caller declare a precedence chain without duplicating DbStatus's + * own decision logic. + */ + fallback?: React.ReactNode } export enum WarningTypes { @@ -42,11 +51,15 @@ interface WarningTooltipProps { isCapabilityNotShown?: boolean } -const LAST_CONNECTION_SM = 3 -const LAST_CONNECTION_L = 16 - const DbStatus = (props: Props) => { - const { id, lastConnection, createdAt, isNew, isFree } = props + const { + id, + lastConnection, + createdAt, + isNew, + isFree, + fallback = null, + } = props const { source } = useSelector(appContextCapability) const capability = getTutorialCapability(source!) @@ -108,7 +121,7 @@ const DbStatus = (props: Props) => { ) } - return null + return <>{fallback} } // separated to send event when content is displayed From 0ba7b1978d283e3b3fe331c5d40bf93892569a41 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 22 May 2026 16:02:42 +0300 Subject: [PATCH 02/13] fix(ui): allow children on InstanceHeaderContainer styled component The styled.div

generic in styled-components v5 needs the props type to extend HTMLAttributes for children to be allowed. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../src/components/instance-header/InstanceHeader.styles.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts index 11bf67f30a..0532005eed 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts @@ -1,6 +1,8 @@ +import React from 'react' import styled, { css } from 'styled-components' -interface InstanceHeaderContainerProps { +interface InstanceHeaderContainerProps + extends React.HTMLAttributes { $isProductionEnv?: boolean } From dd3f936a7f84d208f907e150e573b518be9225ae Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 22 May 2026 16:04:13 +0300 Subject: [PATCH 03/13] refactor(ui): hoist jest.Mock casts into named constants in InstanceHeader spec Avoids the leading-semicolon ;(... as jest.Mock) pattern at each call site by typing the mocked selectors once at the top of the file. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../instance-header/InstanceHeader.spec.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx index 2f207891ac..c7f7ddce1d 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx @@ -67,13 +67,19 @@ jest.mock('react-router-dom', () => ({ }), })) +const mockedConnectedInstanceSelector = + connectedInstanceSelector as jest.Mock +const mockedConnectedInstanceInfoSelector = + connectedInstanceInfoSelector as jest.Mock +const mockedAppContextDbIndex = appContextDbIndex as jest.Mock + describe('InstanceHeader', () => { it('should render', () => { expect(render()).toBeTruthy() }) it('should render change index button with databases = 1', () => { - ;(connectedInstanceInfoSelector as jest.Mock).mockReturnValueOnce({ + mockedConnectedInstanceInfoSelector.mockReturnValueOnce({ databases: 1, }) @@ -115,7 +121,7 @@ describe('InstanceHeader', () => { }) it('should be disabled db index button with loading state', () => { - ;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({ + mockedConnectedInstanceSelector.mockReturnValueOnce({ loading: true, }) @@ -125,7 +131,7 @@ describe('InstanceHeader', () => { }) it('should be disabled db index button with disabled state', () => { - ;(appContextDbIndex as jest.Mock).mockReturnValueOnce({ + mockedAppContextDbIndex.mockReturnValueOnce({ disabled: true, }) @@ -259,7 +265,7 @@ describe('InstanceHeader', () => { describe('environment indicator', () => { const renderWithEnv = (env: Environment) => { - ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + mockedConnectedInstanceSelector.mockReturnValue({ username: 'username', id: 'instanceId', loading: false, @@ -317,7 +323,7 @@ describe('InstanceHeader', () => { }) it('does not render the badge when the dev-prodMode flag is off', () => { - ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + mockedConnectedInstanceSelector.mockReturnValue({ username: 'username', id: 'instanceId', loading: false, From b043b47f4778fcc7ded29b1f27f48d6ec7f69835 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 22 May 2026 16:04:33 +0300 Subject: [PATCH 04/13] refactor(ui): drop verbose fallback prop comment on DbStatus References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../ui/src/pages/home/components/db-status/DbStatus.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx index 714800a7fe..814022ff6a 100644 --- a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx +++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx @@ -30,11 +30,6 @@ export interface Props { createdAt: Maybe isNew: boolean isFree?: boolean - /** - * Rendered when no temporary indicator (cloud warning / "new" badge) applies. - * Lets the caller declare a precedence chain without duplicating DbStatus's - * own decision logic. - */ fallback?: React.ReactNode } From c95f17ed4ddc7225383d898d50dda3f404dd4ea6 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 22 May 2026 16:08:01 +0300 Subject: [PATCH 05/13] style(ui): apply prettier formatting on InstanceHeader spec constants Co-Authored-By: Claude Opus 4.7 --- .../ui/src/components/instance-header/InstanceHeader.spec.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx index c7f7ddce1d..4b7fd08090 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx @@ -67,8 +67,7 @@ jest.mock('react-router-dom', () => ({ }), })) -const mockedConnectedInstanceSelector = - connectedInstanceSelector as jest.Mock +const mockedConnectedInstanceSelector = connectedInstanceSelector as jest.Mock const mockedConnectedInstanceInfoSelector = connectedInstanceInfoSelector as jest.Mock const mockedAppContextDbIndex = appContextDbIndex as jest.Mock From c71e8e73946baab61e67e3bb0fd09284b647c0a1 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 22 May 2026 16:33:32 +0300 Subject: [PATCH 06/13] refactor(ui): shorten development badge label to DEV References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../components/environment-badge/EnvironmentBadge.spec.tsx | 4 ++-- .../src/components/environment-badge/EnvironmentBadge.tsx | 2 +- .../DatabasesListCellName/DatabasesListCellName.spec.tsx | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx index a4a430392f..c0726b6d1f 100644 --- a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.spec.tsx @@ -34,7 +34,7 @@ describe('EnvironmentBadge', () => { expect(screen.getByText('PROD')).toBeInTheDocument() }) - it('renders the Development label for development environment', () => { + it('renders the DEV label for development environment', () => { render( , withDevProdFlag(true), @@ -43,7 +43,7 @@ describe('EnvironmentBadge', () => { expect( screen.getByTestId(`environment-badge-${Environment.Development}`), ).toBeInTheDocument() - expect(screen.getByText('Development')).toBeInTheDocument() + expect(screen.getByText('DEV')).toBeInTheDocument() }) it('renders nothing for unspecified environment', () => { diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx index fb3174c6b2..57f2f51bfa 100644 --- a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx @@ -33,7 +33,7 @@ const BADGE_CONFIG: Partial> = { tooltip: PRODUCTION_TOOLTIP, }, [Environment.Development]: { - label: 'Development', + label: 'DEV', variant: 'default', tooltip: DEVELOPMENT_TOOLTIP, }, diff --git a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx index 63e7f13332..ab8217acab 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx @@ -46,11 +46,11 @@ describe('DatabasesListCellName', () => { expect(screen.getByText('PROD')).toBeInTheDocument() }) - it('renders the Development label when environment is Development', () => { + it('renders the DEV label when environment is Development', () => { renderCell({ ...baseInstance, environment: Environment.Development }) expect(screen.getByTestId('environment-badge-db-1')).toBeInTheDocument() - expect(screen.getByText('Development')).toBeInTheDocument() + expect(screen.getByText('DEV')).toBeInTheDocument() }) it('renders no badge when environment is Unspecified', () => { @@ -60,7 +60,7 @@ describe('DatabasesListCellName', () => { screen.queryByTestId('environment-badge-db-1'), ).not.toBeInTheDocument() expect(screen.queryByText('PROD')).not.toBeInTheDocument() - expect(screen.queryByText('Development')).not.toBeInTheDocument() + expect(screen.queryByText('DEV')).not.toBeInTheDocument() }) it('does not render the badge when the dev-prodMode flag is off', () => { From d92844a98f06175d657b5bf78d3c87148b7f5d19 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Tue, 26 May 2026 14:36:49 +0300 Subject: [PATCH 07/13] refactor(ui): flat tint top bar and render env badge alongside DbStatus Replaces the InstanceHeader linear gradient with a flat danger100 tint across the entire top bar for the production environment. Drops the fallback-prop precedence between DbStatus and EnvironmentBadge so both indicators render together in the list cell. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../instance-header/InstanceHeader.styles.ts | 11 ++--- .../DatabasesListCellName.spec.tsx | 25 ++-------- .../DatabasesListCellName.tsx | 10 ++-- .../components/db-status/DbStatus.spec.tsx | 48 ------------------- .../home/components/db-status/DbStatus.tsx | 12 +---- 5 files changed, 13 insertions(+), 93 deletions(-) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts index 0532005eed..7647140826 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts @@ -6,15 +6,10 @@ interface InstanceHeaderContainerProps $isProductionEnv?: boolean } -const productionGradient = css` - background: linear-gradient( - 90deg, - ${({ theme }) => theme.semantic.color.background.danger200} 0%, - ${({ theme }) => theme.semantic.color.background.danger100} 25%, - transparent 60% - ); +const productionTint = css` + background-color: ${({ theme }) => theme.semantic.color.background.danger100}; ` export const InstanceHeaderContainer = styled.div` - ${({ $isProductionEnv }) => $isProductionEnv && productionGradient} + ${({ $isProductionEnv }) => $isProductionEnv && productionTint} ` diff --git a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx index ab8217acab..e77d993fd6 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.spec.tsx @@ -75,8 +75,8 @@ describe('DatabasesListCellName', () => { }) }) - describe('temporary indicator precedence', () => { - it('shows the "new" temp indicator instead of the PROD badge', () => { + describe('with a temporary indicator', () => { + it('renders the env badge alongside the "new" temp indicator', () => { renderCell({ ...baseInstance, new: true, @@ -84,12 +84,10 @@ describe('DatabasesListCellName', () => { }) expect(screen.getByTestId('database-status-new-db-1')).toBeInTheDocument() - expect( - screen.queryByTestId('environment-badge-db-1'), - ).not.toBeInTheDocument() + expect(screen.getByTestId('environment-badge-db-1')).toBeInTheDocument() }) - it('shows the cloud warning indicator instead of the Development label', () => { + it('renders the env badge alongside the cloud warning indicator', () => { const fiveDaysAgo = new Date() fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5) @@ -100,21 +98,6 @@ describe('DatabasesListCellName', () => { environment: Environment.Development, }) - expect( - screen.queryByTestId('environment-badge-db-1'), - ).not.toBeInTheDocument() - }) - - it('falls back to the env badge once the temp indicator clears', () => { - renderCell({ - ...baseInstance, - new: false, - environment: Environment.Production, - }) - - expect( - screen.queryByTestId('database-status-new-db-1'), - ).not.toBeInTheDocument() expect(screen.getByTestId('environment-badge-db-1')).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx index 3150f1eb5e..1870b3bfeb 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx @@ -32,14 +32,12 @@ const DatabasesListCellName: IDatabaseListCell = ({ row }) => { lastConnection={lastConnection} createdAt={createdAt} isFree={cloudDetails?.free} - fallback={ - - } /> + { ).not.toBeInTheDocument() }) - describe('fallback', () => { - it('renders the fallback when no temporary indicator applies', () => { - render( - } - />, - ) - - expect(screen.getByTestId('env-fallback')).toBeInTheDocument() - }) - - it('renders the temporary indicator instead of the fallback when "isNew" is true', () => { - render( - } - />, - ) - - expect(screen.getByTestId('database-status-new-1')).toBeInTheDocument() - expect(screen.queryByTestId('env-fallback')).not.toBeInTheDocument() - }) - - it('renders the temporary indicator instead of the fallback for an unused cloud db', () => { - const lastConnection = new Date(Date.now() - daysToMs(5)) - render( - } - />, - ) - - expect( - screen.getByTestId(`database-status-${WarningTypes.TryDatabase}-1`), - ).toBeInTheDocument() - expect(screen.queryByTestId('env-fallback')).not.toBeInTheDocument() - }) - }) - it('should render new status', async () => { const sendEventTelemetryMock = jest.fn() ;(sendEventTelemetry as jest.Mock).mockImplementation( diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx index 814022ff6a..d595a1db1c 100644 --- a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx +++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx @@ -30,7 +30,6 @@ export interface Props { createdAt: Maybe isNew: boolean isFree?: boolean - fallback?: React.ReactNode } export enum WarningTypes { @@ -47,14 +46,7 @@ interface WarningTooltipProps { } const DbStatus = (props: Props) => { - const { - id, - lastConnection, - createdAt, - isNew, - isFree, - fallback = null, - } = props + const { id, lastConnection, createdAt, isNew, isFree } = props const { source } = useSelector(appContextCapability) const capability = getTutorialCapability(source!) @@ -116,7 +108,7 @@ const DbStatus = (props: Props) => { ) } - return <>{fallback} + return null } // separated to send event when content is displayed From a254a170ec4c460abafc9d8eaf1febf0f43e9fc4 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Tue, 26 May 2026 14:38:53 +0300 Subject: [PATCH 08/13] chore(ui): drop no-op edit to DbStatus.tsx References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../ui/src/pages/home/components/db-status/DbStatus.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx index d595a1db1c..f499fcc901 100644 --- a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx +++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx @@ -21,9 +21,6 @@ import { } from './texts' import { IconWrapper, InfoIcon } from './DbStatus.styles' -const LAST_CONNECTION_SM = 3 -const LAST_CONNECTION_L = 16 - export interface Props { id: string lastConnection: Maybe @@ -45,6 +42,9 @@ interface WarningTooltipProps { isCapabilityNotShown?: boolean } +const LAST_CONNECTION_SM = 3 +const LAST_CONNECTION_L = 16 + const DbStatus = (props: Props) => { const { id, lastConnection, createdAt, isNew, isFree } = props From ce119e83d0313cedf2e4a1a577426e05f8238db8 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Tue, 26 May 2026 14:49:28 +0300 Subject: [PATCH 09/13] ci: retrigger workflow From 320a917475b7da4058d0098140196b52420cd743 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Tue, 26 May 2026 15:16:15 +0300 Subject: [PATCH 10/13] fix(ui): source environment telemetry from launch target in free DB flow OAuthConnectFreeDb was pulling environment from the currently connected instance, but the telemetry event reports the opening of a different (target) database. Source environment from firstFreeInstance so analytics reflect the actual classification of the free DB being launched. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx | 5 ++++- .../oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx index d763df6acf..73c757419a 100644 --- a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx @@ -33,6 +33,7 @@ jest.mock('uiSrc/slices/instances/instances', () => ({ freeInstancesSelector: jest.fn().mockReturnValue([ { id: 'instanceId', + environment: 'production', }, ]), })) @@ -68,7 +69,9 @@ describe('OAuthConnectFreeDb', () => { databaseId: 'providedId', provider: undefined, source: OAuthSocialSource.ListOfDatabases, - environment: 'unspecified', + // sourced from firstFreeInstance (the launch target), not the + // currently connected instance + environment: 'production', ...getRedisModulesSummary(), ...MOCK_ADDITIONAL_INFO, }, diff --git a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx index de177eb1f5..218f043d02 100644 --- a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx @@ -37,11 +37,14 @@ const OAuthConnectFreeDb = ({ className, }: Props) => { const { loading } = useSelector(instancesSelector) ?? {} - const { modules, provider, environment } = - useSelector(connectedInstanceSelector) ?? {} + const { modules, provider } = useSelector(connectedInstanceSelector) ?? {} const [firstFreeInstance] = useSelector(freeInstancesSelector) ?? [] const targetDatabaseId = id || firstFreeInstance?.id + // environment should describe the database we're about to launch (the + // target), not the one currently connected — sourced from the target so + // analytics reflect the actual classification of the free DB. + const targetEnvironment = firstFreeInstance?.environment const dispatch = useDispatch() const { search } = useLocation() @@ -59,7 +62,7 @@ const OAuthConnectFreeDb = ({ databaseId: targetDatabaseId, provider, source, - environment, + environment: targetEnvironment, ...modulesSummary, ...infoData, }, From 1817b5b90a6e71d4995336079071d0a96d04add1 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Tue, 26 May 2026 15:23:51 +0300 Subject: [PATCH 11/13] chore(ui): drop explanatory comments from OAuthConnectFreeDb env fix References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx | 2 -- .../oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx | 3 --- 2 files changed, 5 deletions(-) diff --git a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx index 73c757419a..1d25e10fd0 100644 --- a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx @@ -69,8 +69,6 @@ describe('OAuthConnectFreeDb', () => { databaseId: 'providedId', provider: undefined, source: OAuthSocialSource.ListOfDatabases, - // sourced from firstFreeInstance (the launch target), not the - // currently connected instance environment: 'production', ...getRedisModulesSummary(), ...MOCK_ADDITIONAL_INFO, diff --git a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx index 218f043d02..14a11fa88e 100644 --- a/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx @@ -41,9 +41,6 @@ const OAuthConnectFreeDb = ({ const [firstFreeInstance] = useSelector(freeInstancesSelector) ?? [] const targetDatabaseId = id || firstFreeInstance?.id - // environment should describe the database we're about to launch (the - // target), not the one currently connected — sourced from the target so - // analytics reflect the actual classification of the free DB. const targetEnvironment = firstFreeInstance?.environment const dispatch = useDispatch() From 3b596e8effd211f64c925bec4ca4add44c2b73ef Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Wed, 27 May 2026 10:46:24 +0300 Subject: [PATCH 12/13] =?UTF-8?q?refactor(ui):=20apply=20PR=20review=20nit?= =?UTF-8?q?s=20=E2=80=94=20split=20types/constants,=20use=20enum,=20drop?= =?UTF-8?q?=20inline=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move EnvironmentBadgeProps and EnvironmentBadgeConfig to EnvironmentBadge.types.ts; tooltip strings and BADGE_CONFIG to EnvironmentBadge.constants.ts, per the style guide. - Replace hardcoded 'production' strings with Environment.Production in the OAuth and handlers specs. - Replace the inline paddingLeft on the InstanceHeader env-badge slot with a styled FlexItem in InstanceHeader.styles.ts. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../EnvironmentBadge.constants.ts | 24 ++++++++++++ .../environment-badge/EnvironmentBadge.tsx | 37 ++----------------- .../EnvironmentBadge.types.ts | 15 ++++++++ .../src/components/environment-badge/index.ts | 2 +- .../instance-header/InstanceHeader.styles.ts | 6 +++ .../instance-header/InstanceHeader.tsx | 12 +++--- .../OAuthConnectFreeDb.spec.tsx | 25 ++++++++----- .../databases-list/methods/handlers.spec.ts | 6 ++- 8 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 redisinsight/ui/src/components/environment-badge/EnvironmentBadge.constants.ts create mode 100644 redisinsight/ui/src/components/environment-badge/EnvironmentBadge.types.ts diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.constants.ts b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.constants.ts new file mode 100644 index 0000000000..55dc3b1dc4 --- /dev/null +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.constants.ts @@ -0,0 +1,24 @@ +import { Environment } from 'apiClient' + +import { EnvironmentBadgeConfig } from './EnvironmentBadge.types' + +export const PRODUCTION_TOOLTIP = + 'Production — Adds an extra layer of protection to prevent unintended changes. Includes additional confirmation dialogs before modifying data and stronger friction before running dangerous commands.' + +export const DEVELOPMENT_TOOLTIP = + 'Development — Skips standard confirmation dialogs when modifying data, for faster work on development and test databases.' + +export const BADGE_CONFIG: Partial< + Record +> = { + [Environment.Production]: { + label: 'PROD', + variant: 'danger', + tooltip: PRODUCTION_TOOLTIP, + }, + [Environment.Development]: { + label: 'DEV', + variant: 'default', + tooltip: DEVELOPMENT_TOOLTIP, + }, +} diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx index 57f2f51bfa..6e77cce6a0 100644 --- a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.tsx @@ -1,43 +1,12 @@ import React from 'react' import { useSelector } from 'react-redux' -import { Environment } from 'apiClient' import { appFeatureFlagDevProdModeSelector } from 'uiSrc/slices/app/features' -import { - RiBadge, - BadgeVariants, -} from 'uiSrc/components/base/display/badge/RiBadge' +import { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge' import { RiTooltip } from 'uiSrc/components/base/tooltip/RITooltip' -export interface EnvironmentBadgeProps { - environment?: Environment - dataTestId?: string -} - -interface EnvironmentBadgeConfig { - label: string - variant: BadgeVariants - tooltip: React.ReactNode -} - -const PRODUCTION_TOOLTIP = - 'Production — Adds an extra layer of protection to prevent unintended changes. Includes additional confirmation dialogs before modifying data and stronger friction before running dangerous commands.' - -const DEVELOPMENT_TOOLTIP = - 'Development — Skips standard confirmation dialogs when modifying data, for faster work on development and test databases.' - -const BADGE_CONFIG: Partial> = { - [Environment.Production]: { - label: 'PROD', - variant: 'danger', - tooltip: PRODUCTION_TOOLTIP, - }, - [Environment.Development]: { - label: 'DEV', - variant: 'default', - tooltip: DEVELOPMENT_TOOLTIP, - }, -} +import { EnvironmentBadgeProps } from './EnvironmentBadge.types' +import { BADGE_CONFIG } from './EnvironmentBadge.constants' export const EnvironmentBadge = ({ environment, diff --git a/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.types.ts b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.types.ts new file mode 100644 index 0000000000..747a509743 --- /dev/null +++ b/redisinsight/ui/src/components/environment-badge/EnvironmentBadge.types.ts @@ -0,0 +1,15 @@ +import React from 'react' +import { Environment } from 'apiClient' + +import { BadgeVariants } from 'uiSrc/components/base/display/badge/RiBadge' + +export interface EnvironmentBadgeProps { + environment?: Environment + dataTestId?: string +} + +export interface EnvironmentBadgeConfig { + label: string + variant: BadgeVariants + tooltip: React.ReactNode +} diff --git a/redisinsight/ui/src/components/environment-badge/index.ts b/redisinsight/ui/src/components/environment-badge/index.ts index 9ae4db02fb..f923f87d48 100644 --- a/redisinsight/ui/src/components/environment-badge/index.ts +++ b/redisinsight/ui/src/components/environment-badge/index.ts @@ -1,2 +1,2 @@ export { EnvironmentBadge } from './EnvironmentBadge' -export type { EnvironmentBadgeProps } from './EnvironmentBadge' +export type { EnvironmentBadgeProps } from './EnvironmentBadge.types' diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts index 7647140826..a3de656e23 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts @@ -1,6 +1,8 @@ import React from 'react' import styled, { css } from 'styled-components' +import { FlexItem } from 'uiSrc/components/base/layout/flex' + interface InstanceHeaderContainerProps extends React.HTMLAttributes { $isProductionEnv?: boolean @@ -13,3 +15,7 @@ const productionTint = css` export const InstanceHeaderContainer = styled.div` ${({ $isProductionEnv }) => $isProductionEnv && productionTint} ` + +export const EnvironmentBadgeSlot = styled(FlexItem)` + padding-left: 8px; +` diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx index 0dcf70460b..d67c5023b8 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.tsx @@ -49,7 +49,10 @@ import { NumericInput } from 'uiSrc/components/base/inputs' import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' import { Link } from 'uiSrc/components/base/link/Link' import InstancesNavigationPopover from './components/instances-navigation-popover' -import { InstanceHeaderContainer } from './InstanceHeader.styles' +import { + EnvironmentBadgeSlot, + InstanceHeaderContainer, +} from './InstanceHeader.styles' import styles from './styles.module.scss' const riConfig = getConfig() @@ -226,12 +229,9 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => { )} - + - + {databases > 1 && (

({ }), })) -jest.mock('uiSrc/slices/instances/instances', () => ({ - ...jest.requireActual('uiSrc/slices/instances/instances'), - freeInstancesSelector: jest.fn().mockReturnValue([ - { - id: 'instanceId', - environment: 'production', - }, - ]), -})) +jest.mock('uiSrc/slices/instances/instances', () => { + // eslint-disable-next-line global-require + const { Environment: MockEnvironment } = require('apiClient') + return { + ...jest.requireActual('uiSrc/slices/instances/instances'), + freeInstancesSelector: jest.fn().mockReturnValue([ + { + id: 'instanceId', + environment: MockEnvironment.Production, + }, + ]), + } +}) let store: typeof mockedStore beforeEach(() => { @@ -69,7 +74,7 @@ describe('OAuthConnectFreeDb', () => { databaseId: 'providedId', provider: undefined, source: OAuthSocialSource.ListOfDatabases, - environment: 'production', + environment: Environment.Production, ...getRedisModulesSummary(), ...MOCK_ADDITIONAL_INFO, }, diff --git a/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts b/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts index d3397795fc..62118c086c 100644 --- a/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts +++ b/redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts @@ -1,3 +1,5 @@ +import { Environment } from 'apiClient' + import { TelemetryEvent, sendEventTelemetry, @@ -73,7 +75,7 @@ const instance = { id: '1', provider: 'LOCALHOST', modules: [], - environment: 'production', + environment: Environment.Production, } describe('databases-list methods/handlers', () => { @@ -98,7 +100,7 @@ describe('databases-list methods/handlers', () => { eventData: expect.objectContaining({ databaseId: '1', provider: 'LOCALHOST', - environment: 'production', + environment: Environment.Production, }), }), ) From c94afde044a8e3719b1693c03c3ec3d0ae5b7720 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Wed, 27 May 2026 11:01:09 +0300 Subject: [PATCH 13/13] fix(ui): collapse empty env badge slot in InstanceHeader EnvironmentBadgeSlot now hides via :empty when EnvironmentBadge returns null (feature flag off, environment unspecified), so the 8px padding no longer reserves space in the header for users without a badge. References: #RI-8181 Co-Authored-By: Claude Opus 4.7 --- .../src/components/instance-header/InstanceHeader.styles.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts index a3de656e23..e95653c03e 100644 --- a/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts +++ b/redisinsight/ui/src/components/instance-header/InstanceHeader.styles.ts @@ -18,4 +18,8 @@ export const InstanceHeaderContainer = styled.div` export const EnvironmentBadgeSlot = styled(FlexItem)` padding-left: 8px; + + &:empty { + display: none; + } `