Skip to content

Commit

Permalink
feat: update org feature flags backend (#10446)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickoferrall authored Nov 20, 2024
1 parent 89fd195 commit 3b44d51
Show file tree
Hide file tree
Showing 37 changed files with 688 additions and 165 deletions.
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 @@ -24,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 @@ -69,8 +71,8 @@ const OrgDetails = (props: Props) => {
</div>
</div>

<OrgFeatures />
<OrgFeatureFlags />
<OrgFeatures organizationRef={organization} />
<OrgFeatureFlags organizationRef={organization} />
<OrgBillingDangerZone organization={organization} isWide />
</Suspense>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import styled from '@emotion/styled'
import {Info as InfoIcon} from '@mui/icons-material'
import React, {useState} from 'react'
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'
Expand All @@ -28,63 +34,73 @@ const FeatureRow = styled('div')({
const FeatureNameGroup = styled('div')({
display: 'flex',
alignItems: 'center',
gap: 4
gap: 4,
'& svg': {
display: 'block'
}
})

const features = [
{
id: 'suggestGroups',
name: 'Suggest Groups',
tooltip:
'Get AI-powered suggestions for creating new groups based on team activity and collaboration patterns'
},
{
id: 'publicTeams',
name: 'Public Teams',
tooltip: 'Allow teams to be discoverable by anyone in your organization'
},
{
id: 'standupAISummary',
name: 'Stand-Up AI Summary',
tooltip: 'Automatically generate summaries of your team standups using AI'
},
{
id: 'relatedDiscussions',
name: 'Related Discussions',
tooltip: 'See AI-suggested related discussions and threads across your organization'
}
]
// 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'
}

const OrgFeatureFlags = () => {
const [featureStates, setFeatureStates] = useState<Record<string, boolean>>({
suggestGroups: false,
publicTeams: false,
standupAISummary: false,
relatedDiscussions: false
})
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 = (featureId: string) => {
setFeatureStates((prev) => ({
...prev,
[featureId]: !prev[featureId]
}))
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>
{features.map((feature) => (
<FeatureRow key={feature.id}>
{organization.orgFeatureFlags.map((feature) => (
<FeatureRow key={feature.featureName}>
<FeatureNameGroup>
<span>{feature.name}</span>
<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.tooltip}</TooltipContent>
<TooltipContent>{feature.description}</TooltipContent>
</Tooltip>
</FeatureNameGroup>
<Toggle active={featureStates[feature.id]!} onClick={() => handleToggle(feature.id)} />
<Toggle active={!!feature.enabled} onClick={() => handleToggle(feature.featureName)} />
</FeatureRow>
))}
</PanelRow>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import styled from '@emotion/styled'
import {Info as InfoIcon} from '@mui/icons-material'
import React, {useState} from 'react'
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'
Expand All @@ -28,26 +34,55 @@ const FeatureRow = styled('div')({
const FeatureNameGroup = styled('div')({
display: 'flex',
alignItems: 'center',
gap: 4
gap: 4,
'& svg': {
display: 'block'
}
})

const OrgFeatures = () => {
const [showAIFeatures, setShowAIFeatures] = useState(false)
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>Show AI Features</span>
<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={showAIFeatures} onClick={() => setShowAIFeatures(!showAIFeatures)} />
<Toggle active={useAI} onClick={handleToggle} />
</FeatureRow>
</PanelRow>
</StyledPanel>
Expand Down
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
41 changes: 41 additions & 0 deletions packages/client/mutations/ToggleFeatureFlagMutation.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 {ToggleFeatureFlagMutation as TToggleFeatureFlagMutation} from '../__generated__/ToggleFeatureFlagMutation.graphql'
import {StandardMutation} from '../types/relayMutations'

graphql`
fragment ToggleFeatureFlagMutation_notification on ToggleFeatureFlagSuccess {
featureFlag {
featureName
enabled
}
}
`

const mutation = graphql`
mutation ToggleFeatureFlagMutation($featureName: String!, $orgId: ID, $teamId: ID, $userId: ID) {
toggleFeatureFlag(featureName: $featureName, orgId: $orgId, teamId: $teamId, userId: $userId) {
... on ErrorPayload {
error {
message
}
}
...ToggleFeatureFlagMutation_notification @relay(mask: false)
}
}
`

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

export default ToggleFeatureFlagMutation
Loading

0 comments on commit 3b44d51

Please sign in to comment.