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: update org feature flags backend #10446

Merged
merged 24 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c2612ec
feat: add use ai to org
nickoferrall Oct 31, 2024
ed8e516
implement feature flag resolver and dataloaders to get all flags
nickoferrall Nov 1, 2024
c734ba1
implement toggle feature flag
nickoferrall Nov 1, 2024
5fff7ec
update dataloader
nickoferrall Nov 4, 2024
e943ab4
add checks in toggle feature flag mutation
nickoferrall Nov 4, 2024
ebbbb67
feature flag subscription working
nickoferrall Nov 4, 2024
412f30d
add lookup
nickoferrall Nov 5, 2024
dde53d2
Merge branch 'feat/add-use-ai' into feat/org-feature-flags-dynamic
nickoferrall Nov 5, 2024
f447d71
merge useAi
nickoferrall Nov 5, 2024
52d0507
clean up
nickoferrall Nov 5, 2024
74f684f
info icon centered
nickoferrall Nov 5, 2024
f0003d9
fix codegen error
nickoferrall Nov 5, 2024
ad7dc0e
Revert changes to yarn.lock
nickoferrall Nov 5, 2024
bb8f42e
fix ToggleFeatureFlagSuccess ts error
nickoferrall Nov 5, 2024
c2e559d
update enabled type
nickoferrall Nov 6, 2024
7e81e1c
Merge branch 'feat/feature-flags-ui' into feat/org-feature-flags-dynamic
nickoferrall Nov 6, 2024
85f77a6
only org admin can see ui
nickoferrall Nov 6, 2024
fd693f6
update err message
nickoferrall Nov 6, 2024
93fa3cf
update props
nickoferrall Nov 6, 2024
cf6af28
seperate owned feature flag and feature flag
nickoferrall Nov 15, 2024
947178a
state validScope type
nickoferrall Nov 15, 2024
0620a0d
chore: remove pg.d.ts from version control
nickoferrall Nov 18, 2024
0d2fdae
update featureFlag type in index.d.ts
nickoferrall Nov 18, 2024
f1dd5c4
feat: implement toggle ai features mutation (#10457)
nickoferrall Nov 20, 2024
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
69 changes: 35 additions & 34 deletions codegen.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const OrgDetails = (props: Props) => {
fragment OrgDetails_organization on Organization {
...OrgBillingDangerZone_organization
...EditableOrgName_organization
...OrgFeatureFlags_organization
orgId: id
isBillingLeader
createdAt
Expand Down Expand Up @@ -70,7 +71,7 @@ const OrgDetails = (props: Props) => {
</div>

<OrgFeatures />
<OrgFeatureFlags />
<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
Expand Up @@ -28,7 +28,10 @@ const FeatureRow = styled('div')({
const FeatureNameGroup = styled('div')({
display: 'flex',
alignItems: 'center',
gap: 4
gap: 4,
'& svg': {
display: 'block'
}
})

const OrgFeatures = () => {
Expand Down
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
7 changes: 7 additions & 0 deletions packages/client/subscriptions/NotificationSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ const subscription = graphql`
summary
descriptionHTML
}

ToggleFeatureFlagSuccess {
featureFlag {
featureName
enabled
}
}
}
}
`
Expand Down
6 changes: 5 additions & 1 deletion packages/server/database/types/Organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Input {
updatedAt?: Date
showConversionModal?: boolean
payLaterClickCount?: number
useAI?: boolean
}

export default class Organization {
Expand All @@ -37,6 +38,7 @@ export default class Organization {
trialStartDate?: Date | null
scheduledLockAt?: Date | null
lockedAt?: Date | null
useAI: boolean
updatedAt: Date
constructor(input: Input) {
const {
Expand All @@ -50,7 +52,8 @@ export default class Organization {
showConversionModal,
payLaterClickCount,
picture,
tier
tier,
useAI
} = input
this.id = id || generateUID()
this.activeDomain = activeDomain
Expand All @@ -63,5 +66,6 @@ export default class Organization {
this.picture = picture
this.showConversionModal = showConversionModal === null ? undefined : showConversionModal
this.payLaterClickCount = payLaterClickCount || 0
this.useAI = useAI ?? true
}
}
72 changes: 71 additions & 1 deletion packages/server/dataloader/customLoaderMakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ import {
selectTasks,
selectTeams
} from '../postgres/select'
import {Insight, MeetingSettings, OrganizationUser, Task, Team} from '../postgres/types'
import {
FeatureFlag,
Insight,
MeetingSettings,
OrganizationUser,
Task,
Team
} from '../postgres/types'
import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting'
import {TeamMeetingTemplate} from '../postgres/types/pg'
import {Logger} from '../utils/Logger'
Expand All @@ -37,6 +44,7 @@ import NullableDataLoader from './NullableDataLoader'
import RootDataLoader, {RegisterDependsOn} from './RootDataLoader'
import normalizeArrayResults from './normalizeArrayResults'
import normalizeResults from './normalizeResults'

export interface MeetingSettingsKey {
teamId: string
meetingType: MeetingTypeEnum
Expand Down Expand Up @@ -852,6 +860,7 @@ export const latestInsightByTeamId = (parent: RootDataLoader) => {
)
}

// whether a feature flag is enabled for a given owner (user, team, or org)
export const featureFlagByOwnerId = (parent: RootDataLoader) => {
return new DataLoader<{ownerId: string; featureName: string}, boolean, string>(
async (keys) => {
Expand Down Expand Up @@ -955,3 +964,64 @@ export const publicTemplatesByType = (parent: RootDataLoader) => {
}
)
}

export const allFeatureFlags = (parent: RootDataLoader) => {
nickoferrall marked this conversation as resolved.
Show resolved Hide resolved
return new DataLoader<'Organization' | 'Team' | 'User' | 'all', FeatureFlag[], string>(
async (scopes) => {
const pg = getKysely()
return await Promise.all(
scopes.map(async (scope) => {
const flags = await pg
.selectFrom('FeatureFlag')
.selectAll()
.where('expiresAt', '>', new Date())
.$if(scope !== 'all', (qb) => {
const validScope = scope as 'Organization' | 'Team' | 'User'
return qb.where('scope', '=', validScope)
})
.orderBy('featureName')
.execute()
return flags.map((flag) => ({...flag, isEnabled: true}))
})
)
},
{
...parent.dataLoaderOptions,
cacheKeyFn: (scope) => scope
}
)
}

export const allFeatureFlagsByOwner = (parent: RootDataLoader) => {
return new DataLoader<
{ownerId: string; scope: 'Organization' | 'Team' | 'User'},
FeatureFlag[],
string
>(
async (keys) => {
const flagsByOwnerId = await Promise.all(
keys.map(async ({ownerId, scope}) => {
const allFlags = await parent.get('allFeatureFlags').load(scope)
const flags = await Promise.all(
allFlags.map(async (flag) => {
const isEnabled = await parent
.get('featureFlagByOwnerId')
.load({ownerId, featureName: flag.featureName})
return {
...flag,
enabled: isEnabled
}
})
)
return flags
})
)

return flagsByOwnerId
},
{
...parent.dataLoaderOptions,
cacheKeyFn: (key) => `${key.ownerId}:${key.scope}`
}
)
}
Loading
Loading