Skip to content

Commit 915ba4b

Browse files
alaisterjoshenlim
andauthored
feat: enable/disable dashboard features (supabase#37921)
* feat: enable/disable dashboard features * use the set directly * move enabled features to common * remove sneaky any --------- Co-authored-by: Joshen Lim <[email protected]>
1 parent e187e21 commit 915ba4b

File tree

8 files changed

+103
-35
lines changed

8 files changed

+103
-35
lines changed

apps/studio/components/interfaces/UserDropdown.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Link from 'next/link'
44
import { useRouter } from 'next/router'
55

66
import { ProfileImage } from 'components/ui/ProfileImage'
7+
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
78
import { useSignOut } from 'lib/auth'
89
import { IS_PLATFORM } from 'lib/constants'
910
import { useProfile } from 'lib/profile'
@@ -33,6 +34,7 @@ export function UserDropdown() {
3334
const appStateSnapshot = useAppStateSnapshot()
3435
const setCommandMenuOpen = useSetCommandMenuOpen()
3536
const { openFeaturePreviewModal } = useFeaturePreviewModal()
37+
const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email')
3638

3739
return (
3840
<DropdownMenu>
@@ -61,7 +63,7 @@ export function UserDropdown() {
6163
>
6264
{profile.username}
6365
</span>
64-
{profile.primary_email !== profile.username && (
66+
{profile.primary_email !== profile.username && profileShowEmailEnabled && (
6567
<span
6668
title={profile.primary_email}
6769
className="w-full text-left text-foreground-light text-xs truncate"

apps/studio/data/profile/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,3 @@ import type { components } from 'data/api'
33
export type Profile = components['schemas']['ProfileResponse'] & {
44
profileImageUrl?: string
55
}
6-
7-
export type Feature = Profile['disabled_features'][number]
Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,21 @@
1-
import type { Feature } from 'data/profile/types'
1+
import { isFeatureEnabled, type Feature } from 'common'
22
import { useProfile } from 'lib/profile'
33

4-
function checkFeature(feature: Feature, features?: Feature[]) {
5-
return !features?.includes(feature) ?? true
6-
}
7-
8-
type SnakeToCamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
9-
? `${First}${SnakeToCamelCase<Capitalize<Rest>>}`
10-
: S
11-
12-
type FeatureToCamelCase<S extends Feature> = S extends `${infer P}:${infer R}`
13-
? `${SnakeToCamelCase<P>}${Capitalize<SnakeToCamelCase<R>>}`
14-
: SnakeToCamelCase<S>
15-
16-
function featureToCamelCase(feature: Feature) {
17-
return feature
18-
.replace(/:/g, '_')
19-
.split('_')
20-
.map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))
21-
.join('') as FeatureToCamelCase<typeof feature>
22-
}
23-
244
function useIsFeatureEnabled<T extends Feature[]>(
255
features: T
26-
): { [key in FeatureToCamelCase<T[number]>]: boolean }
27-
function useIsFeatureEnabled(features: Feature): boolean
6+
): ReturnType<typeof isFeatureEnabled<T>>
7+
function useIsFeatureEnabled(features: Feature): ReturnType<typeof isFeatureEnabled>
288
function useIsFeatureEnabled<T extends Feature | Feature[]>(features: T) {
299
const { profile } = useProfile()
10+
const disabledFeatures = profile?.disabled_features
3011

12+
// This code branch is to make the type checker happy, it's intentionally
13+
// the same as the isFeatureEnabled function call below.
3114
if (Array.isArray(features)) {
32-
return Object.fromEntries(
33-
features.map((feature) => [
34-
featureToCamelCase(feature),
35-
checkFeature(feature, profile?.disabled_features),
36-
])
37-
)
15+
return isFeatureEnabled(features, disabledFeatures)
3816
}
3917

40-
return checkFeature(features, profile?.disabled_features)
18+
return isFeatureEnabled(features, disabledFeatures)
4119
}
4220

4321
export { useIsFeatureEnabled }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "./enabled-features.schema.json",
3+
4+
"profile:show_email": true,
5+
"profile:update": true
6+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"$schema": {
6+
"type": "string"
7+
},
8+
9+
"profile:show_email": {
10+
"type": "boolean",
11+
"description": "Show the user's email address in the toolbar"
12+
},
13+
"profile:update": {
14+
"type": "boolean",
15+
"description": "Allow the user to change their profile information (first name, last name)"
16+
}
17+
},
18+
"required": ["profile:show_email", "profile:update"],
19+
"additionalProperties": false
20+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { components } from 'api-types'
2+
import enabledFeaturesRaw from './enabled-features.json'
3+
4+
const enabledFeaturesStaticObj = enabledFeaturesRaw as Omit<typeof enabledFeaturesRaw, '$schema'>
5+
6+
type Profile = components['schemas']['ProfileResponse']
7+
8+
export type Feature = Profile['disabled_features'][number] | keyof typeof enabledFeaturesStaticObj
9+
10+
const disabledFeaturesStaticArray = Object.entries(enabledFeaturesStaticObj)
11+
.filter(([_, value]) => !value)
12+
.map(([key]) => key as Feature)
13+
14+
function checkFeature(feature: Feature, features: Set<Feature>) {
15+
return !features.has(feature)
16+
}
17+
18+
type SnakeToCamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
19+
? `${First}${SnakeToCamelCase<Capitalize<Rest>>}`
20+
: S
21+
22+
type FeatureToCamelCase<S extends Feature> = S extends `${infer P}:${infer R}`
23+
? `${SnakeToCamelCase<P>}${Capitalize<SnakeToCamelCase<R>>}`
24+
: SnakeToCamelCase<S>
25+
26+
function featureToCamelCase(feature: Feature) {
27+
return feature
28+
.replace(/:/g, '_')
29+
.split('_')
30+
.map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))
31+
.join('') as FeatureToCamelCase<typeof feature>
32+
}
33+
34+
function isFeatureEnabled<T extends Feature[]>(
35+
features: T,
36+
runtimeDisabledFeatures?: Feature[]
37+
): { [key in FeatureToCamelCase<T[number]>]: boolean }
38+
function isFeatureEnabled(features: Feature, runtimeDisabledFeatures?: Feature[]): boolean
39+
function isFeatureEnabled<T extends Feature | Feature[]>(
40+
features: T,
41+
runtimeDisabledFeatures?: Feature[]
42+
) {
43+
const disabledFeatures = new Set([
44+
...(runtimeDisabledFeatures ?? []),
45+
...disabledFeaturesStaticArray,
46+
])
47+
48+
if (Array.isArray(features)) {
49+
return Object.fromEntries(
50+
features.map((feature) => [
51+
featureToCamelCase(feature),
52+
checkFeature(feature, disabledFeatures),
53+
])
54+
)
55+
}
56+
57+
return checkFeature(features, disabledFeatures)
58+
}
59+
60+
export { isFeatureEnabled }

packages/common/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ export * from './auth'
22
export * from './consent-state'
33
export * from './constants'
44
export * from './database-types'
5+
export * from './enabled-features'
6+
export * from './feature-flags'
57
export * from './gotrue'
68
export * from './helpers'
79
export * from './hooks'
810
export * from './MetaFavicons/pages-router'
911
export * from './Providers'
1012
export * from './telemetry'
11-
export * from './feature-flags'

packages/common/tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"extends": "tsconfig/react-library.json",
33
"include": ["."],
4-
"exclude": ["dist", "build", "node_modules"]
4+
"exclude": ["dist", "build", "node_modules"],
5+
"compilerOptions": {
6+
"resolveJsonModule": true
7+
}
58
}

0 commit comments

Comments
 (0)