Skip to content

feat(opentrons-ai-client): add hidden feature flags #18198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 29, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 0 additions & 35 deletions components/src/controls/LabeledToggle.tsx

This file was deleted.

79 changes: 61 additions & 18 deletions components/src/controls/ToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,69 @@
// reusable toggle button with on off styling for connect to robot and opt in/out
import cx from 'classnames'
import { css } from 'styled-components'

import { IconButton } from '../buttons'
import styles from './styles.module.css'
import { COLORS } from '../helix-design-system'
import { Icon } from '../icons'
import { Btn, Flex } from '../primitives'

import type { ButtonProps } from '../buttons'
import type { MouseEvent } from 'react'
import type { StyleProps } from '../primitives'

export interface ToggleButtonProps extends ButtonProps {
const TOGGLE_DISABLED_STYLES = css`
color: ${COLORS.grey50};

&:hover {
color: ${COLORS.grey55};
}

&:focus-visible {
box-shadow: 0 0 0 3px ${COLORS.yellow50};
}

&:disabled {
color: ${COLORS.grey30};
}
`

const TOGGLE_ENABLED_STYLES = css`
color: ${COLORS.blue50};

&:hover {
color: ${COLORS.blue55};
}

&:focus-visible {
box-shadow: 0 0 0 3px ${COLORS.yellow50};
}

&:disabled {
color: ${COLORS.grey30};
}
`

interface ToggleButtonProps extends StyleProps {
toggledOn: boolean
label?: string | null
disabled?: boolean | null
id?: string
onClick?: (e: MouseEvent) => void
}

export function ToggleButton(props: ToggleButtonProps): JSX.Element {
// TODO(mc, 2020-02-04): destructuring `name` to avoid flow error
// ButtonProps::name conflicts with IconProps::name, and IconButton
// has `name` prop to pass to Icon. IconButton will need to be redone
const { toggledOn, name, ...buttonProps } = props
const className = cx(styles.robot_item_icon, props.className, {
[styles.toggled_on]: toggledOn,
[styles.toggled_off]: !toggledOn,
})

const toggleIcon = toggledOn ? 'ot-toggle-switch-on' : 'ot-toggle-switch-off'

return <IconButton {...buttonProps} name={toggleIcon} className={className} />
const { label, toggledOn, disabled, size, ...buttonProps } = props
const iconName = toggledOn ? 'ot-toggle-input-on' : 'ot-toggle-input-off'

Check warning on line 52 in components/src/controls/ToggleButton.tsx

Codecov / codecov/patch

components/src/controls/ToggleButton.tsx#L51-L52

Added lines #L51 - L52 were not covered by tests

return (
<Btn
disabled={disabled ?? false}
role="switch"
aria-label={label}
aria-checked={toggledOn}
size={size ?? '2rem'}
css={props.toggledOn ? TOGGLE_ENABLED_STYLES : TOGGLE_DISABLED_STYLES}
{...buttonProps}

Check warning on line 62 in components/src/controls/ToggleButton.tsx

Codecov / codecov/patch

components/src/controls/ToggleButton.tsx#L54-L62

Added lines #L54 - L62 were not covered by tests
>
<Flex>
<Icon name={iconName} size="2rem" />
</Flex>
</Btn>

Check warning on line 67 in components/src/controls/ToggleButton.tsx

Codecov / codecov/patch

components/src/controls/ToggleButton.tsx#L64-L67

Added lines #L64 - L67 were not covered by tests
)
}
1 change: 0 additions & 1 deletion components/src/controls/index.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
// belong in the please-refactor-these-to-use-primitives-and-styled-components
// category). We should move them all.
export * from './ToggleButton'
export * from './LabeledToggle'
export * from './LabeledButton'
export * from './LabeledCheckbox'
export * from './LabeledRadioGroup'
19 changes: 18 additions & 1 deletion opentrons-ai-client/src/OpentronsAI.tsx
Original file line number Diff line number Diff line change
@@ -19,7 +19,14 @@ import { Header } from './molecules/Header'
import { HeaderWithMeter } from './molecules/HeaderWithMeter'
import { Loading } from './molecules/Loading'
import { OpentronsAIRoutes } from './OpentronsAIRoutes'
import { headerWithMeterAtom, mixpanelAtom, tokenAtom } from './resources/atoms'
import { FeatureFlagsModal } from './organisms/FeatureFlagsModal'
import {
displayFeatureFlagsModalAtom,
featureFlagsAtom,
headerWithMeterAtom,
mixpanelAtom,
tokenAtom,
} from './resources/atoms'
import { CLIENT_MAX_WIDTH } from './resources/constants'
import { useGetAccessToken } from './resources/hooks'
import { useTrackEvent } from './resources/hooks/useTrackEvent'
@@ -30,6 +37,9 @@ export function OpentronsAI(): JSX.Element | null {
const [{ displayHeaderWithMeter, progress }] = useAtom(headerWithMeterAtom)
const [mixpanelState, setMixpanelState] = useAtom(mixpanelAtom)
const { getAccessToken } = useGetAccessToken()
const [featureFlags, setFeatureFlags] = useAtom(featureFlagsAtom)
const [displayFeatureFlagsModal] = useAtom(displayFeatureFlagsModalAtom)

const trackEvent = useTrackEvent()

const fetchAccessToken = async (): Promise<void> => {
@@ -69,13 +79,20 @@ export function OpentronsAI(): JSX.Element | null {
return null
}

global.enablePrereleaseMode = () => {
setFeatureFlags({ enablePrereleaseMode: true })
}

return (
<Flex
id="opentrons-ai"
width={'100%'}
height={'100vh'}
flexDirection={DIRECTION_COLUMN}
>
{displayFeatureFlagsModal && featureFlags.enablePrereleaseMode && (
<FeatureFlagsModal />
)}
<StickyHeader>
{displayHeaderWithMeter ? (
<HeaderWithMeter progressPercentage={progress} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"enablePDProtocolGeneration": "Enable Real PD Protocol Generation",
"enablePrereleaseMode": "Enable Prerelease Mode",
"feature_flags_title": "Feature Flags",
"close": "Close",
"feature_flags_body": "Internal Feature Flags"
}
2 changes: 2 additions & 0 deletions opentrons-ai-client/src/assets/localization/en/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import create_protocol from './create_protocol.json'
import feature_flags from './feature_flags.json'
import protocol_generator from './protocol_generator.json'
import shared from './shared.json'

export const en = {
feature_flags,
shared,
protocol_generator,
create_protocol,
31 changes: 27 additions & 4 deletions opentrons-ai-client/src/molecules/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -5,20 +5,27 @@ import styled from 'styled-components'

import {
ALIGN_CENTER,
Box,
COLORS,
Flex,
JUSTIFY_CENTER,
JUSTIFY_SPACE_BETWEEN,
Link as LinkButton,
POSITION_ABSOLUTE,
POSITION_RELATIVE,
SPACING,
StyledText,
TYPOGRAPHY,
} from '@opentrons/components'

import { displayExitConfirmModalAtom } from '../../resources/atoms'
import {
displayExitConfirmModalAtom,
displayFeatureFlagsModalAtom,
featureFlagsAtom,
} from '../../resources/atoms'
import { CLIENT_MAX_WIDTH } from '../../resources/constants'
import { useTrackEvent } from '../../resources/hooks/useTrackEvent'
import { SettingsButton } from '../SettingsButton'

const HeaderBar = styled(Flex)`
position: ${POSITION_RELATIVE};
@@ -62,6 +69,10 @@ export function Header({ isExitButton = false }: HeaderProps): JSX.Element {
const { logout } = useAuth0()
const trackEvent = useTrackEvent()
const [, setDisplayExitConfirmModal] = useAtom(displayExitConfirmModalAtom)
const [featureFlags] = useAtom(featureFlagsAtom)
const [, setDisplayFeatureFlagsModalAtom] = useAtom(
displayFeatureFlagsModalAtom
)

async function handleLoginOrExitClick(): Promise<void> {
if (isExitButton) {
@@ -80,9 +91,21 @@ export function Header({ isExitButton = false }: HeaderProps): JSX.Element {
<HeaderTitle>{t('opentrons')}</HeaderTitle>
<HeaderGradientTitle>{t('ai')}</HeaderGradientTitle>
</Flex>
<LogoutOrExitButton onClick={handleLoginOrExitClick}>
{isExitButton ? t('exit') : t('logout')}
</LogoutOrExitButton>
<Flex>
<LogoutOrExitButton onClick={handleLoginOrExitClick}>
{isExitButton ? t('exit') : t('logout')}
</LogoutOrExitButton>

{featureFlags.enablePrereleaseMode && (
<Box marginLeft={SPACING.spacing32} marginTop="-.5rem">
<SettingsButton
onClick={() => {
setDisplayFeatureFlagsModalAtom(true)
}}
/>
</Box>
)}
</Flex>
</HeaderBarContent>
</HeaderBar>
)
58 changes: 58 additions & 0 deletions opentrons-ai-client/src/molecules/SettingsButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { css } from 'styled-components'

import { Btn, COLORS, Flex, Icon, JUSTIFY_CENTER } from '@opentrons/components'

const BUTTON_NAME = 'SettingsIconButton'

export const SettingsButton = (props: { onClick: () => void }): JSX.Element => {
const { onClick } = props
return (
<Btn
onClick={onClick}
css={GEAR_ICON_STYLE}
data-testid={BUTTON_NAME}
aria-label={BUTTON_NAME}
>
<Flex justifyContent={JUSTIFY_CENTER}>
<Icon size="1rem" name="gear" />
</Flex>
</Btn>
)
}

const GEAR_ICON_STYLE = css`
width: 2rem;
height: 2rem;
border-radius: 50%;
color: ${COLORS.grey60};

&:hover {
background-color: ${COLORS.grey30};
}

&:active {
color: ${COLORS.grey60};
background-color: ${COLORS.grey35};
}

&:focus-visible {
position: relative;
outline: none;

/* blue ring */
&::after {
content: '';
position: absolute;
top: -0.5rem;
left: -0.5rem;
right: -0.5rem;
bottom: -0.5rem;

border: 3px solid ${COLORS.blue50};
border-radius: 50%;
pointer-events: none;
box-sizing: content-box;
}
background-color: ${COLORS.grey35};
}
`
40 changes: 40 additions & 0 deletions opentrons-ai-client/src/organisms/FeatureFlags/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useTranslation } from 'react-i18next'
import { useAtom } from 'jotai'
import map from 'lodash/map'

import {
DIRECTION_COLUMN,
Flex,
JUSTIFY_SPACE_BETWEEN,
SPACING,
StyledText,
ToggleButton,
} from '@opentrons/components'

import { featureFlagsAtom } from '../../resources/atoms'

export const FeatureFlags = (): JSX.Element | null => {
const [featureFlags, setFeatureFlags] = useAtom(featureFlagsAtom)
const { t } = useTranslation('feature_flags')

if (featureFlags.enablePrereleaseMode) {
return (
<Flex flexDirection={DIRECTION_COLUMN} gap={SPACING.spacing6}>
{map(featureFlags, (flagValue, flagKey) => {
return (
<Flex justifyContent={JUSTIFY_SPACE_BETWEEN} key={flagKey}>
<StyledText> {t(flagKey)} </StyledText>
<ToggleButton
label={t(flagKey)}
toggledOn={flagValue}
onClick={() => {
setFeatureFlags({ [flagKey]: !flagValue })
}}
></ToggleButton>
</Flex>
)
})}
</Flex>
)
}
}
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.