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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(
<EnvironmentBadge environment={Environment.Production} />,
withDevProdFlag(true),
)

expect(
screen.getByTestId(`environment-badge-${Environment.Production}`),
).toBeInTheDocument()
expect(screen.getByText('PROD')).toBeInTheDocument()
})

it('renders the DEV label for development environment', () => {
render(
<EnvironmentBadge environment={Environment.Development} />,
withDevProdFlag(true),
)

expect(
screen.getByTestId(`environment-badge-${Environment.Development}`),
).toBeInTheDocument()
expect(screen.getByText('DEV')).toBeInTheDocument()
})

it('renders nothing for unspecified environment', () => {
const { container } = render(
<EnvironmentBadge environment={Environment.Unspecified} />,
withDevProdFlag(true),
)

expect(container).toBeEmptyDOMElement()
})

it('renders nothing when environment is undefined', () => {
const { container } = render(
<EnvironmentBadge environment={undefined} />,
withDevProdFlag(true),
)

expect(container).toBeEmptyDOMElement()
})

it('renders nothing when the dev-prodMode feature flag is off', () => {
const { container } = render(
<EnvironmentBadge environment={Environment.Production} />,
withDevProdFlag(false),
)

expect(container).toBeEmptyDOMElement()
})

it('uses the provided dataTestId override', () => {
render(
<EnvironmentBadge
environment={Environment.Production}
dataTestId="custom-badge"
/>,
withDevProdFlag(true),
)

expect(screen.getByTestId('custom-badge')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
valkirilov marked this conversation as resolved.
Outdated
label: string
variant: BadgeVariants
tooltip: React.ReactNode
}

const PRODUCTION_TOOLTIP =
Comment thread
valkirilov marked this conversation as resolved.
Outdated
'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<Record<Environment, EnvironmentBadgeConfig>> = {
[Environment.Production]: {
label: 'PROD',
variant: 'danger',
tooltip: PRODUCTION_TOOLTIP,
},
[Environment.Development]: {
label: 'DEV',
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 (
<RiTooltip content={config.tooltip} position="bottom">
<RiBadge
label={config.label}
variant={config.variant}
data-testid={dataTestId ?? `environment-badge-${environment}`}
/>
</RiTooltip>
)
}

export default EnvironmentBadge
2 changes: 2 additions & 0 deletions redisinsight/ui/src/components/environment-badge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EnvironmentBadge } from './EnvironmentBadge'
export type { EnvironmentBadgeProps } from './EnvironmentBadge'
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -66,13 +67,18 @@ 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(<InstanceHeader {...instance(mockedProps)} />)).toBeTruthy()
})

it('should render change index button with databases = 1', () => {
;(connectedInstanceInfoSelector as jest.Mock).mockReturnValueOnce({
mockedConnectedInstanceInfoSelector.mockReturnValueOnce({
databases: 1,
})

Expand Down Expand Up @@ -114,7 +120,7 @@ describe('InstanceHeader', () => {
})

it('should be disabled db index button with loading state', () => {
;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({
mockedConnectedInstanceSelector.mockReturnValueOnce({
loading: true,
})

Expand All @@ -124,7 +130,7 @@ describe('InstanceHeader', () => {
})

it('should be disabled db index button with disabled state', () => {
;(appContextDbIndex as jest.Mock).mockReturnValueOnce({
mockedAppContextDbIndex.mockReturnValueOnce({
disabled: true,
})

Expand Down Expand Up @@ -256,6 +262,94 @@ describe('InstanceHeader', () => {
).toHaveTextContent('Test account #40')
})

describe('environment indicator', () => {
const renderWithEnv = (env: Environment) => {
mockedConnectedInstanceSelector.mockReturnValue({
username: 'username',
id: 'instanceId',
loading: false,
environment: env,
})

const state = set(
cloneDeep(initialStateDefault),
`app.features.featureFlags.features.${FeatureFlags.devProdMode}`,
{ flag: true },
)

return render(<InstanceHeader {...instance(mockedProps)} />, {
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', () => {
mockedConnectedInstanceSelector.mockReturnValue({
username: 'username',
id: 'instanceId',
loading: false,
environment: Environment.Production,
})

const state = set(
cloneDeep(initialStateDefault),
`app.features.featureFlags.features.${FeatureFlags.devProdMode}`,
{ flag: false },
)

render(<InstanceHeader {...instance(mockedProps)} />, {
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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import styled, { css } from 'styled-components'

interface InstanceHeaderContainerProps
extends React.HTMLAttributes<HTMLDivElement> {
$isProductionEnv?: boolean
}

const productionTint = css`
background-color: ${({ theme }) => theme.semantic.color.background.danger100};
`

export const InstanceHeaderContainer = styled.div<InstanceHeaderContainerProps>`
${({ $isProductionEnv }) => $isProductionEnv && productionTint}
`
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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>(String(db || 0))
Expand Down Expand Up @@ -133,11 +139,14 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => {
}

return (
<div
<InstanceHeaderContainer
className={cx(styles.container)}
style={{
borderBottom: theme.components.sideBar.collapsed.borderRight,
}}
data-testid="instance-header"
data-environment={environment}
$isProductionEnv={isProductionEnv}
>
<Row
responsive
Expand Down Expand Up @@ -217,6 +226,12 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => {
<InstancesNavigationPopover name={name} />
)}
</FlexItem>
<FlexItem
style={{ paddingLeft: 8 }}
Comment thread
valkirilov marked this conversation as resolved.
Outdated
data-testid="instance-header-environment"
>
<EnvironmentBadge environment={environment} />
</FlexItem>
{databases > 1 && (
<FlexItem style={{ paddingLeft: 12 }}>
<div
Expand Down Expand Up @@ -327,7 +342,7 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => {
</Row>
</FlexItem>
</Row>
</div>
</InstanceHeaderContainer>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const InstancesList = ({
databaseId: instance.id,
source: 'navigation_panel',
provider: instance.provider,
environment: instance.environment,
...modulesSummary,
...infoData,
},
Expand Down
Loading
Loading