Skip to content
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

feat: org feature flags UI #10436

Merged
merged 5 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
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
76 changes: 39 additions & 37 deletions codegen.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Props {
}

const isServer = typeof window === 'undefined'
const hasAI = isServer
const hasAiApiKey = isServer
? !!process.env.OPEN_AI_API_KEY
: !!window.__ACTION__ && !!window.__ACTION__.hasOpenAI

Expand All @@ -25,7 +25,7 @@ const WholeMeetingSummary = (props: Props) => {
summary
organization {
hasStandupAISummaryFlag: featureFlag(featureName: "standupAISummary")
hasNoAISummaryFlag: featureFlag(featureName: "noAISummary")
useAI
}
... on RetrospectiveMeeting {
reflectionGroups(sortBy: voteCount) {
Expand All @@ -47,15 +47,16 @@ const WholeMeetingSummary = (props: Props) => {
const {summary: wholeMeetingSummary, reflectionGroups, organization} = meeting
const reflections = reflectionGroups?.flatMap((group) => group.reflections) // reflectionCount hasn't been calculated yet so check reflections length
const hasMoreThanOneReflection = reflections?.length && reflections.length > 1
if (!hasMoreThanOneReflection || organization.hasNoAISummaryFlag || !hasAI) return null
if (!hasMoreThanOneReflection || !organization.useAI || !hasAiApiKey) return null
if (!wholeMeetingSummary) return <WholeMeetingSummaryLoading />
return <WholeMeetingSummaryResult meetingRef={meeting} />
} else if (meeting.__typename === 'TeamPromptMeeting') {
const {summary: wholeMeetingSummary, responses, organization} = meeting
const {hasStandupAISummaryFlag, useAI} = organization
if (
!organization.hasStandupAISummaryFlag ||
organization.hasNoAISummaryFlag ||
!hasAI ||
!hasStandupAISummaryFlag ||
!useAI ||
!hasAiApiKey ||
!responses ||
responses.length === 0
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const NewCheckInQuestion = (props: Props) => {
}
team {
organization {
hasNoAISummaryFlag: featureFlag(featureName: "noAISummary")
useAI
}
}
}
Expand All @@ -101,7 +101,7 @@ const NewCheckInQuestion = (props: Props) => {
localPhase,
facilitatorUserId,
team: {
organization: {hasNoAISummaryFlag}
organization: {useAI}
}
} = meeting
const {checkInQuestion} = localPhase
Expand Down Expand Up @@ -226,7 +226,7 @@ const NewCheckInQuestion = (props: Props) => {
}
})
}
const showAiIcebreaker = !hasNoAISummaryFlag && isFacilitating && window.__ACTION__.hasOpenAI
const showAiIcebreaker = useAI && isFacilitating && window.__ACTION__.hasOpenAI

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import useModal from '../../../../hooks/useModal'
import defaultOrgAvatar from '../../../../styles/theme/images/avatar-organization.svg'
import OrganizationDetails from '../Organization/OrganizationDetails'
import OrgBillingDangerZone from './OrgBillingDangerZone'
import OrgFeatureFlags from './OrgFeatureFlags'
import OrgFeatures from './OrgFeatures'

type Props = {
organizationRef: OrgDetails_organization$key
Expand All @@ -22,6 +24,8 @@ const OrgDetails = (props: Props) => {
fragment OrgDetails_organization on Organization {
...OrgBillingDangerZone_organization
...EditableOrgName_organization
...OrgFeatureFlags_organization
...OrgFeatures_organization
orgId: id
isBillingLeader
createdAt
Expand Down Expand Up @@ -66,6 +70,9 @@ const OrgDetails = (props: Props) => {
<OrganizationDetails createdAt={createdAt} billingTier={billingTier} tier={tier} />
</div>
</div>

<OrgFeatures organizationRef={organization} />
<OrgFeatureFlags organizationRef={organization} />
<OrgBillingDangerZone organization={organization} isWide />
</Suspense>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import styled from '@emotion/styled'
import {Info as InfoIcon} from '@mui/icons-material'
import graphql from 'babel-plugin-relay/macro'
import React from 'react'
import {useFragment} from 'react-relay'
import {OrgFeatureFlags_organization$key} from '../../../../__generated__/OrgFeatureFlags_organization.graphql'
import Panel from '../../../../components/Panel/Panel'
import Toggle from '../../../../components/Toggle/Toggle'
import useAtmosphere from '../../../../hooks/useAtmosphere'
import useMutationProps from '../../../../hooks/useMutationProps'
import ToggleFeatureFlagMutation from '../../../../mutations/ToggleFeatureFlagMutation'
import {PALETTE} from '../../../../styles/paletteV3'
import {ElementWidth, Layout} from '../../../../types/constEnums'
import {Tooltip} from '../../../../ui/Tooltip/Tooltip'
import {TooltipContent} from '../../../../ui/Tooltip/TooltipContent'
import {TooltipTrigger} from '../../../../ui/Tooltip/TooltipTrigger'

const StyledPanel = styled(Panel)<{isWide: boolean}>(({isWide}) => ({
maxWidth: isWide ? ElementWidth.PANEL_WIDTH : 'inherit'
}))

const PanelRow = styled('div')({
borderTop: `1px solid ${PALETTE.SLATE_300}`,
padding: Layout.ROW_GUTTER
})

const FeatureRow = styled('div')({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8
})

const FeatureNameGroup = styled('div')({
display: 'flex',
alignItems: 'center',
gap: 4,
'& svg': {
display: 'block'
}
})

// TODO: create a migration that updates featureName to be a readable string
// then update the references throughout the app and remove this
const FEATURE_NAME_LOOKUP: Record<string, string> = {
insights: 'Team Insights',
publicTeams: 'Public Teams',
relatedDiscussions: 'Related Discussions',
standupAISummary: 'Standup AI Summary',
suggestGroups: 'AI Reflection Group Suggestions'
}

interface Props {
organizationRef: OrgFeatureFlags_organization$key
}

const OrgFeatureFlags = (props: Props) => {
const {organizationRef} = props
const atmosphere = useAtmosphere()
const {onError, onCompleted} = useMutationProps()
const organization = useFragment(
graphql`
fragment OrgFeatureFlags_organization on Organization {
id
isOrgAdmin
orgFeatureFlags {
featureName
description
enabled
}
}
`,
organizationRef
)
const {isOrgAdmin} = organization

const handleToggle = async (featureName: string) => {
const variables = {
featureName,
orgId: organization.id
}
ToggleFeatureFlagMutation(atmosphere, variables, {
onError,
onCompleted
})
}

if (!isOrgAdmin) return null
return (
<StyledPanel isWide label='Organization Feature Flags'>
<PanelRow>
{organization.orgFeatureFlags.map((feature) => (
<FeatureRow key={feature.featureName}>
<FeatureNameGroup>
<span>{FEATURE_NAME_LOOKUP[feature.featureName] || feature.featureName}</span>
<Tooltip>
<TooltipTrigger className='bg-transparent hover:cursor-pointer'>
<InfoIcon className='h-4 w-4 text-slate-600' />
</TooltipTrigger>
<TooltipContent>{feature.description}</TooltipContent>
</Tooltip>
</FeatureNameGroup>
<Toggle active={!!feature.enabled} onClick={() => handleToggle(feature.featureName)} />
</FeatureRow>
))}
</PanelRow>
</StyledPanel>
)
}

export default OrgFeatureFlags
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import styled from '@emotion/styled'
import {Info as InfoIcon} from '@mui/icons-material'
import graphql from 'babel-plugin-relay/macro'
import React from 'react'
import {useFragment} from 'react-relay'
import {OrgFeatures_organization$key} from '../../../../__generated__/OrgFeatures_organization.graphql'
import Panel from '../../../../components/Panel/Panel'
import Toggle from '../../../../components/Toggle/Toggle'
import useAtmosphere from '../../../../hooks/useAtmosphere'
import useMutationProps from '../../../../hooks/useMutationProps'
import ToggleAIFeaturesMutation from '../../../../mutations/ToggleAIFeaturesMutation'
import {PALETTE} from '../../../../styles/paletteV3'
import {ElementWidth, Layout} from '../../../../types/constEnums'
import {Tooltip} from '../../../../ui/Tooltip/Tooltip'
import {TooltipContent} from '../../../../ui/Tooltip/TooltipContent'
import {TooltipTrigger} from '../../../../ui/Tooltip/TooltipTrigger'

const StyledPanel = styled(Panel)<{isWide: boolean}>(({isWide}) => ({
maxWidth: isWide ? ElementWidth.PANEL_WIDTH : 'inherit'
}))

const PanelRow = styled('div')({
borderTop: `1px solid ${PALETTE.SLATE_300}`,
padding: Layout.ROW_GUTTER
})

const FeatureRow = styled('div')({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8
})

const FeatureNameGroup = styled('div')({
display: 'flex',
alignItems: 'center',
gap: 4,
'& svg': {
display: 'block'
}
})

interface Props {
organizationRef: OrgFeatures_organization$key
}

const OrgFeatures = (props: Props) => {
const {organizationRef} = props
const atmosphere = useAtmosphere()
const {onError, onCompleted} = useMutationProps()
const organization = useFragment(
graphql`
fragment OrgFeatures_organization on Organization {
id
isOrgAdmin
useAI
}
`,
organizationRef
)
const {id: orgId, isOrgAdmin, useAI} = organization

const handleToggle = () => {
const variables = {orgId}
ToggleAIFeaturesMutation(atmosphere, variables, {
onError,
onCompleted
})
}

if (!isOrgAdmin) return null
return (
<StyledPanel isWide label='AI Features'>
<PanelRow>
<FeatureRow>
<FeatureNameGroup>
<span>Enable AI Features</span>
<Tooltip>
<TooltipTrigger className='bg-transparent hover:cursor-pointer'>
<InfoIcon className='h-4 w-4 text-slate-600' />
</TooltipTrigger>
<TooltipContent>Enable AI-powered features across your organization</TooltipContent>
</Tooltip>
</FeatureNameGroup>
<Toggle active={useAI} onClick={handleToggle} />
</FeatureRow>
</PanelRow>
</StyledPanel>
)
}

export default OrgFeatures
4 changes: 2 additions & 2 deletions packages/client/mutations/EndRetrospectiveMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ graphql`
groupTitle
}
organization {
hasNoAISummaryFlag: featureFlag(featureName: "noAISummary")
useAI
}
reflectionGroups(sortBy: voteCount) {
reflections {
Expand Down Expand Up @@ -125,7 +125,7 @@ export const endRetrospectiveTeamOnNext: OnNextHandler<
const reflections = reflectionGroups.flatMap((group) => group.reflections) // reflectionCount hasn't been calculated yet so check reflections length
const hasMoreThanOneReflection = reflections.length > 1
const hasOpenAISummary =
hasMoreThanOneReflection && !organization.hasNoAISummaryFlag && window.__ACTION__.hasOpenAI
hasMoreThanOneReflection && organization.useAI && window.__ACTION__.hasOpenAI
const hasTeamHealth = phases.some((phase) => phase.phaseType === 'TEAM_HEALTH')
const pathname = `/new-summary/${meetingId}`
const search = new URLSearchParams()
Expand Down
41 changes: 41 additions & 0 deletions packages/client/mutations/ToggleAIFeaturesMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import graphql from 'babel-plugin-relay/macro'
import {commitMutation} from 'react-relay'
import {ToggleAIFeaturesMutation as TToggleAIFeaturesMutation} from '../__generated__/ToggleAIFeaturesMutation.graphql'
import {StandardMutation} from '../types/relayMutations'

graphql`
fragment ToggleAIFeaturesMutation_organization on ToggleAIFeaturesSuccess {
organization {
id
useAI
}
}
`

const mutation = graphql`
mutation ToggleAIFeaturesMutation($orgId: ID!) {
toggleAIFeatures(orgId: $orgId) {
... on ErrorPayload {
error {
message
}
}
...ToggleAIFeaturesMutation_organization @relay(mask: false)
}
}
`

const ToggleAIFeaturesMutation: StandardMutation<TToggleAIFeaturesMutation> = (
atmosphere,
variables,
{onError, onCompleted}
) => {
return commitMutation<TToggleAIFeaturesMutation>(atmosphere, {
mutation,
variables,
onCompleted,
onError
})
}

export default ToggleAIFeaturesMutation
Loading
Loading