diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx index dbad8a44a0a6..481310021ccd 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx @@ -13,8 +13,13 @@ const setupApi = () => { name: 'featureA', tags: [{ type: 'backend', value: 'sdk' }], type: 'operational', + createdBy: { id: 1, name: 'author' }, + }, + { + name: 'featureB', + type: 'release', + createdBy: { id: 1, name: 'author' }, }, - { name: 'featureB', type: 'release' }, ]; testServerRoute(server, '/api/admin/search/features', { features, diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index 81ddb798e77d..7dbb5c5868cc 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -39,6 +39,7 @@ import { useProjectFeatureSearch, useProjectFeatureSearchActions, } from './useProjectFeatureSearch'; +import { UserAvatar } from '../../../common/UserAvatar/UserAvatar'; interface IPaginatedProjectFeatureTogglesProps { environments: string[]; @@ -101,6 +102,7 @@ export const ProjectFeatureToggles = ({ const isPlaceholder = Boolean(initialLoad || (loading && total)); const featureLifecycleEnabled = useUiFlag('featureLifecycle'); + const flagCreatorEnabled = useUiFlag('flagCreator'); const columns = useMemo( () => [ @@ -167,6 +169,30 @@ export const ProjectFeatureToggles = ({ width: '1%', }, }), + ...(flagCreatorEnabled + ? [ + columnHelper.accessor('createdBy', { + id: 'createdBy', + header: 'By', + cell: ({ row: { original } }) => { + return ( + + ); + }, + enableSorting: false, + meta: { + width: '1%', + align: 'center', + }, + }), + ] + : []), columnHelper.accessor('lastSeenAt', { id: 'lastSeenAt', header: 'Last seen', @@ -305,6 +331,11 @@ export const ProjectFeatureToggles = ({ type: '-', name: `Feature name ${index}`, createdAt: new Date().toISOString(), + createdBy: { + id: 0, + name: '', + imageUrl: '', + }, dependencyType: null, favorite: false, impressionData: false, @@ -404,6 +435,16 @@ export const ProjectFeatureToggles = ({ id: 'createdAt', isVisible: columnVisibility.createdAt, }, + ...(flagCreatorEnabled + ? [ + { + header: 'By', + id: 'createdBy', + isVisible: + columnVisibility.createdBy, + }, + ] + : []), { header: 'Last seen', id: 'lastSeenAt', diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts index afea38fbd75f..2bdc8800253b 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts @@ -57,6 +57,7 @@ export const useDefaultColumnVisibility = (allColumnIds: string[]) => { 'lastSeenAt', ...(featureLifecycleEnabled ? ['lifecycle'] : []), 'createdAt', + 'createdBy', 'type', 'tags', ...showEnvironments(3), diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index bb252c302af7..47a70528a907 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -87,6 +87,7 @@ export type UiFlags = { enableLegacyVariants?: boolean; navigationSidebar?: boolean; commandBarUI?: boolean; + flagCreator?: boolean; }; export interface IVersionInfo { diff --git a/frontend/src/openapi/models/featureSearchResponseSchema.ts b/frontend/src/openapi/models/featureSearchResponseSchema.ts index f53a31a86994..6a976cfd7759 100644 --- a/frontend/src/openapi/models/featureSearchResponseSchema.ts +++ b/frontend/src/openapi/models/featureSearchResponseSchema.ts @@ -28,7 +28,7 @@ export interface FeatureSearchResponseSchema { */ createdAt: string | null; /** User who created the feature flag */ - createdBy?: FeatureSearchResponseSchemaCreatedBy; + createdBy: FeatureSearchResponseSchemaCreatedBy; /** * The type of dependency. 'parent' means that the feature is a parent feature, 'child' means that the feature is a child feature. * @nullable diff --git a/frontend/src/openapi/models/featureSearchResponseSchemaCreatedBy.ts b/frontend/src/openapi/models/featureSearchResponseSchemaCreatedBy.ts index 2e0971151e35..3f4011dc8fc7 100644 --- a/frontend/src/openapi/models/featureSearchResponseSchemaCreatedBy.ts +++ b/frontend/src/openapi/models/featureSearchResponseSchemaCreatedBy.ts @@ -14,7 +14,7 @@ export type FeatureSearchResponseSchemaCreatedBy = { * URL used for the user profile image * @nullable */ - imageUrl: string | null; + imageUrl: string; /** Name of the user */ name: string; }; diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index b7270494e556..0e220d6b2b29 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -122,6 +122,7 @@ exports[`should create default config 1`] = ` }, }, "filterInvalidClientMetrics": false, + "flagCreator": false, "googleAuthEnabled": false, "killInsightsUI": false, "killScheduledChangeRequestCache": false, diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index 4dbfef72702f..e2cb15d0bf88 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -19,6 +19,7 @@ import type { } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import { applyGenericQueryParams, applySearchFilters } from './search-utils'; import type { FeatureSearchEnvironmentSchema } from '../../openapi/spec/feature-search-environment-schema'; +import { generateImageUrl } from '../../util'; const sortEnvironments = (overview: IFeatureOverview[]) => { return overview.map((data: IFeatureOverview) => ({ @@ -404,6 +405,11 @@ class FeatureSearchStore implements IFeatureSearchStore { if (!entry) { // Create a new entry + const name = + row.user_name || + row.user_username || + row.user_email || + 'unknown'; entry = { type: row.type, description: row.description, @@ -419,12 +425,12 @@ class FeatureSearchStore implements IFeatureSearchStore { segments: row.segment_name ? [row.segment_name] : [], createdBy: { id: Number(row.user_id), - name: - row.user_name || - row.user_username || - row.user_email || - 'unknown', - imageUrl: row.user_image_url, + name: name, + imageUrl: generateImageUrl({ + id: row.user_id, + email: row.user_email, + username: name, + }), }, }; if (featureLifecycleEnabled) { diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts index 18402b79d83d..5bd2f6c05b38 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -175,11 +175,21 @@ test('should search matching features by name', async () => { features: [ { name: 'my_feature_a', - createdBy: { id: 1, name: 'user@getunleash.io' }, + createdBy: { + id: 1, + name: 'user@getunleash.io', + imageUrl: + 'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g', + }, }, { name: 'my_feature_b', - createdBy: { id: 1, name: 'user@getunleash.io' }, + createdBy: { + id: 1, + name: 'user@getunleash.io', + imageUrl: + 'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g', + }, }, ], total: 2, diff --git a/src/lib/openapi/spec/feature-search-response-schema.ts b/src/lib/openapi/spec/feature-search-response-schema.ts index 0c7a31b36eec..e951452d8f68 100644 --- a/src/lib/openapi/spec/feature-search-response-schema.ts +++ b/src/lib/openapi/spec/feature-search-response-schema.ts @@ -21,6 +21,7 @@ export const featureSearchResponseSchema = { 'favorite', 'impressionData', 'createdAt', + 'createdBy', 'environments', 'segments', ], @@ -197,7 +198,6 @@ export const featureSearchResponseSchema = { description: `URL used for the user profile image`, type: 'string', example: 'https://example.com/242x200.png', - nullable: true, }, }, }, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index ed48aa4bcaf7..fee61e44f1ba 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -62,7 +62,8 @@ export type IFlagKey = | 'enableLegacyVariants' | 'debugMetrics' | 'navigationSidebar' - | 'commandBarUI'; + | 'commandBarUI' + | 'flagCreator'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -299,6 +300,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_COMMAND_BAR_UI, false, ), + flagCreator: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_FLAG_CREATOR, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 44f6b0e5dd4c..789163bd44d9 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -53,6 +53,7 @@ process.nextTick(async () => { createProjectWithEnvironmentConfig: true, manyStrategiesPagination: true, enableLegacyVariants: false, + flagCreator: true, }, }, authentication: {