Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
fix(IR-2799): fix feature flags state (#10453)
Browse files Browse the repository at this point in the history
* fix(IR-2799): fix feature flags state

* fix(IR-2799): fix remove type from flagName to decouple flags from the engine

* Added ability to toggle feature flags

* chore: Update license information in FeatureFlags.ts file

* Created getAllStringValueNodes file

---------

Co-authored-by: Hanzla Mateen <[email protected]>
  • Loading branch information
kattsushi and hanzlamateen authored Jul 1, 2024
1 parent 0f597a6 commit 7a70674
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,10 @@ import React, { forwardRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { HiMinus, HiPlusSmall } from 'react-icons/hi2'

import {
FeatureFlag,
FeatureFlagSettingType,
featureFlagSettingPath
} from '@etherealengine/common/src/schema.type.module'
import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags'
import { FeatureFlagSettingType, featureFlagSettingPath } from '@etherealengine/common/src/schema.type.module'
import { getAllStringValueNodes } from '@etherealengine/common/src/utils/getAllStringValueNodes'
import { useFind, useMutation } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
import Accordion from '@etherealengine/ui/src/primitives/tailwind/Accordion'
import { useHookstate } from '@hookstate/core'

Expand All @@ -46,24 +44,22 @@ const FeaturesTab = forwardRef(({ open }: { open: boolean }, ref: React.MutableR
const featureFlagSettings = useFind(featureFlagSettingPath)

useEffect(() => {
const defaultTypes = [
'ir.client.menu.social',
'ir.client.menu.emote',
'ir.client.menu.avaturn',
'ir.client.menu.readyPlayerMe'
]

if (featureFlagSettings.status === 'success') {
const defaultTypes = getAllStringValueNodes(FeatureFlags)
const missingTypes = defaultTypes.filter(
(type) =>
!featureFlagSettings.data.find(
(flag) => flag.flagName === type && !Object.keys(flag).filter((key) => !defaultProps.includes(key))
(flag) =>
flag.flagName === type &&
!Object.keys(flag)
.filter((key) => !defaultProps.includes(key))
.some((item) => !item)
)
)

const updatedFeatures: FeatureFlagSettingType[] = [
...missingTypes.map((type) => ({
flagName: type as FeatureFlag,
flagName: type,
flagValue: true,
id: '',
createdAt: '',
Expand All @@ -85,31 +81,47 @@ const FeaturesTab = forwardRef(({ open }: { open: boolean }, ref: React.MutableR
open={open}
>
<div className="mt-6 grid grid-cols-1 gap-6">
{displayedFeatures.value.map((feature) => (
<FeatureItem key={feature.id} feature={feature} />
))}
{displayedFeatures.value
.toSorted()
.sort((x, y) => (x.flagValue === y.flagValue ? 0 : x.flagValue ? -1 : 1)) // show enabled first https://stackoverflow.com/a/17387454/2077741
.map((feature) => (
<FeatureItem key={feature.id} feature={feature} />
))}
</div>
</Accordion>
)
})

const FeatureItem = ({ feature }: { feature: FeatureFlagSettingType }) => {
const featureFlagSettingMutation = useMutation(featureFlagSettingPath)
const additionalProps = Object.keys(feature).filter((key) => !defaultProps.includes(key))

const createOrUpdateFeatureFlag = async (feature: FeatureFlagSettingType, enabled: boolean) => {
if (feature.id) {
await featureFlagSettingMutation.patch(feature.id, { flagValue: enabled })
} else {
await featureFlagSettingMutation.create({
flagName: feature.flagName,
flagValue: enabled
})
}
}

return (
<div key={feature.id} className="flex items-center">
<Toggle
containerClassName="justify-start"
label={feature.flagName}
value={feature.flagValue}
disabled
onChange={(value) => {}}
onChange={(value) => createOrUpdateFeatureFlag(feature, value)}
/>
{additionalProps.map((key) => (
<div key={key} className="ml-6 text-sm text-gray-500">
{key}: {feature[key]}
</div>
))}
{additionalProps
.filter((key) => feature[key])
.map((key) => (
<div key={key} className="ml-6 text-sm text-gray-500">
{key}: {feature[key]}
</div>
))}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ import {
} from '@etherealengine/client-core/src/common/services/MediaInstanceConnectionService'
import { ChannelService, ChannelState } from '@etherealengine/client-core/src/social/services/ChannelService'
import { LocationState } from '@etherealengine/client-core/src/social/services/LocationService'
import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags'
import { InstanceID, LocationID, RoomCode } from '@etherealengine/common/src/schema.type.module'
import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { getMutableState, getState, none, useHookstate, useMutableState } from '@etherealengine/hyperflux'
import { NetworkState } from '@etherealengine/network'

import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { FriendService } from '../social/services/FriendService'
import { connectToInstance } from '../transports/SocketWebRTCClientFunctions'
import { PopupMenuState } from '../user/components/UserMenu/PopupMenuService'
Expand Down Expand Up @@ -247,7 +247,7 @@ export const SocialMenus = {
export const FriendMenus = () => {
const { t } = useTranslation()

const socialsEnabled = FeatureFlagsState.useEnabled('ir.client.menu.social')
const socialsEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Social)

useEffect(() => {
if (!socialsEnabled) return
Expand Down
7 changes: 4 additions & 3 deletions packages/client-core/src/user/UserUISystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions'
import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups'
import { getMutableState, none } from '@etherealengine/hyperflux'

import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags'
import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { InviteService } from '../social/services/InviteService'
import { PopupMenuState } from './components/UserMenu/PopupMenuService'
Expand Down Expand Up @@ -69,9 +70,9 @@ const reactor = () => {
const { t } = useTranslation()
InviteService.useAPIListeners()

const emotesEnabled = FeatureFlagsState.useEnabled('ir.client.menu.emote')
const avaturnEnabled = FeatureFlagsState.useEnabled('ir.client.menu.avaturn')
const rpmEnabled = FeatureFlagsState.useEnabled('ir.client.menu.readyPlayerMe')
const emotesEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Emote)
const avaturnEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Avaturn)
const rpmEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.ReadyPlayerMe)

useEffect(() => {
const FaceRetouchingNatural = lazy(() => import('@mui/icons-material/FaceRetouchingNatural'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import Grid from '@etherealengine/ui/src/primitives/mui/Grid'
import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton'

import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags'
import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { UserMenus } from '../../../UserUISystem'
import { AvatarService } from '../../../services/AvatarService'
Expand Down Expand Up @@ -84,8 +85,8 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => {
const [isSaving, setIsSaving] = useState(false)
const avatarRef = useRef<HTMLInputElement | null>(null)
const thumbnailRef = useRef<HTMLInputElement | null>(null)
const avaturnEnabled = FeatureFlagsState.useEnabled('ir.client.menu.avaturn')
const rpmEnabled = FeatureFlagsState.useEnabled('ir.client.menu.readyPlayerMe')
const avaturnEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Avaturn)
const rpmEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.ReadyPlayerMe)

let thumbnailSrc = state.thumbnailUrl
if (state.thumbnailFile) {
Expand Down
35 changes: 35 additions & 0 deletions packages/common/src/constants/FeatureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
CPAL-1.0 License
The contents of this file are subject to the Common Public Attribution License
Version 1.0. (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE.
The License is based on the Mozilla Public License Version 1.1, but Sections 14
and 15 have been added to cover use of software over a computer network and
provide for limited attribution for the Original Developer. In addition,
Exhibit A has been modified to be consistent with Exhibit B.
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License.
The Original Code is Ethereal Engine.
The Original Developer is the Initial Developer. The Initial Developer of the
Original Code is the Ethereal Engine team.
All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023
Ethereal Engine. All Rights Reserved.
*/

export const FeatureFlags = {
Client: {
Menu: {
Social: 'ir.client.menu.social',
Emote: 'ir.client.menu.emote',
Avaturn: 'ir.client.menu.avaturn',
ReadyPlayerMe: 'ir.client.menu.readyPlayerMe'
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ Ethereal Engine. All Rights Reserved.

// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import type { Static } from '@feathersjs/typebox'
import { getValidator, querySyntax, StringEnum, Type } from '@feathersjs/typebox'
import { getValidator, querySyntax, Type } from '@feathersjs/typebox'
import { dataValidator, queryValidator } from '../validators'

export type FeatureFlag = FeatureFlagSettingType['flagName']

export const featureFlagSettingPath = 'feature-flag-setting'

export const featureFlagSettingMethods = ['find', 'get', 'create', 'patch', 'remove'] as const
Expand All @@ -40,12 +38,7 @@ export const featureFlagSettingSchema = Type.Object(
id: Type.String({
format: 'uuid'
}),
flagName: StringEnum([
'ir.client.menu.social',
'ir.client.menu.emote',
'ir.client.menu.avaturn',
'ir.client.menu.readyPlayerMe'
]),
flagName: Type.String(),
flagValue: Type.Boolean(),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
Expand Down
45 changes: 45 additions & 0 deletions packages/common/src/utils/getAllStringValueNodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
CPAL-1.0 License
The contents of this file are subject to the Common Public Attribution License
Version 1.0. (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE.
The License is based on the Mozilla Public License Version 1.1, but Sections 14
and 15 have been added to cover use of software over a computer network and
provide for limited attribution for the Original Developer. In addition,
Exhibit A has been modified to be consistent with Exhibit B.
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License.
The Original Code is Ethereal Engine.
The Original Developer is the Initial Developer. The Initial Developer of the
Original Code is the Ethereal Engine team.
All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023
Ethereal Engine. All Rights Reserved.
*/

/**
* Method used to get all leaf node strings from an object.
* https://stackoverflow.com/a/63100031/2077741
* @param obj
* @returns
*/
export function getAllStringValueNodes(obj: any) {
if (typeof obj === 'string') {
return [obj]
}

// handle wrong types and null
if (typeof obj !== 'object' || !obj) {
return []
}

return Object.keys(obj).reduce((acc, key) => {
return [...acc, ...getAllStringValueNodes(obj[key])]
}, [])
}
8 changes: 4 additions & 4 deletions packages/engine/src/FeatureFlagsState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
Ethereal Engine. All Rights Reserved.
*/

import { FeatureFlag, featureFlagSettingPath } from '@etherealengine/common/src/schema.type.module'
import { featureFlagSettingPath } from '@etherealengine/common/src/schema.type.module'
import { defineState, getMutableState, useHookstate } from '@etherealengine/hyperflux/functions/StateFunctions'
import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
import { useEffect } from 'react'

export const FeatureFlagsState = defineState({
name: 'ee.engine.FeatureFlagsState',
initial: {} as Record<FeatureFlag, boolean>,
enabled(flagName: FeatureFlag) {
initial: {} as Record<string, boolean>,
enabled(flagName: string) {
const state = getMutableState(FeatureFlagsState)[flagName].value
return typeof state === 'boolean' ? state : true
},
useEnabled(flagName: FeatureFlag) {
useEnabled(flagName: string) {
const state = useHookstate(getMutableState(FeatureFlagsState)[flagName]).value
return typeof state === 'boolean' ? state : true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export const featureFlagSettingResolver = resolve<FeatureFlagSettingType, HookCo
updatedAt: virtual(async (featureFlagSettings) => fromDateTimeSql(featureFlagSettings.updatedAt))
})

export const featureFlagSettingExternalResolver = resolve<FeatureFlagSettingType, HookContext>({})
export const featureFlagSettingExternalResolver = resolve<FeatureFlagSettingType, HookContext>({
flagValue: async (_, setting) => !!setting.flagValue
})

export const featureFlagSettingDataResolver = resolve<FeatureFlagSettingType, HookContext>({
id: async () => {
Expand Down

0 comments on commit 7a70674

Please sign in to comment.