From f73d36fda3a967ccad4b3e713dfa1f38f110874c Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Fri, 26 May 2023 14:37:00 +0300 Subject: [PATCH] feat: add usage of segment in list (#3853) --- .../src/component/segments/SegmentTable.tsx | 56 ++++++++++++++-- frontend/src/interfaces/uiConfig.ts | 1 + .../__snapshots__/create-config.test.ts.snap | 2 + src/lib/db/index.ts | 7 +- src/lib/db/segment-store.ts | 65 ++++++++++++++++--- .../createExportImportService.ts | 7 +- .../createFeatureToggleService.ts | 7 +- src/lib/types/experimental.ts | 7 +- src/lib/types/model.ts | 2 + src/server-dev.ts | 1 + 10 files changed, 137 insertions(+), 18 deletions(-) diff --git a/frontend/src/component/segments/SegmentTable.tsx b/frontend/src/component/segments/SegmentTable.tsx index b469d07cde..24ee0ef9ba 100644 --- a/frontend/src/component/segments/SegmentTable.tsx +++ b/frontend/src/component/segments/SegmentTable.tsx @@ -11,7 +11,13 @@ import { import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import { CreateSegmentButton } from 'component/segments/CreateSegmentButton/CreateSegmentButton'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; -import { useMediaQuery } from '@mui/material'; +import { + Box, + Checkbox, + styled, + Typography, + useMediaQuery, +} from '@mui/material'; import { sortTypes } from 'utils/sortTypes'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useMemo, useState } from 'react'; @@ -28,10 +34,13 @@ import { Search } from 'component/common/Search/Search'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; +import { RowSelectCell } from '../project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell'; +import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig'; export const SegmentTable = () => { const projectId = useOptionalPathParam('projectId'); const { segments, loading } = useSegments(); + const { uiConfig } = useUiConfig(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const [initialState] = useState({ sortBy: [{ id: 'createdAt' }], @@ -56,6 +65,10 @@ export const SegmentTable = () => { return segments; }, [segments, projectId]); + const columns = useMemo( + () => getColumns(uiConfig.flags.segmentContextFieldUsage), + [uiConfig.flags.segmentContextFieldUsage] + ); const { getTableProps, getTableBodyProps, @@ -68,7 +81,7 @@ export const SegmentTable = () => { } = useTable( { initialState, - columns: COLUMNS as any, + columns: columns as any, data: data as any, sortTypes, autoResetGlobalFilter: false, @@ -95,7 +108,7 @@ export const SegmentTable = () => { }, ], setHiddenColumns, - COLUMNS + columns ); return ( @@ -170,7 +183,7 @@ export const SegmentTable = () => { ); }; -const COLUMNS = [ +const getColumns = (segmentContextFieldUsage?: boolean) => [ { id: 'Icon', width: '1%', @@ -182,10 +195,41 @@ const COLUMNS = [ Header: 'Name', accessor: 'name', width: '60%', - Cell: ({ value, row: { original } }: any) => ( - + Cell: ({ + row: { + original: { name, description, id }, + }, + }: any) => ( + ), }, + ...(segmentContextFieldUsage + ? [ + { + Header: 'Used in', + width: '60%', + Cell: ({ value, row: { original } }: any) => ( + + {original.usedInProjects} projects + {original.usedInFeatures} feature toggles + + ), + }, + ] + : []), + { Header: 'Project', accessor: 'project', diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 36d2cbd464..2c002499e8 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -51,6 +51,7 @@ export interface IFlags { variantMetrics?: boolean; strategyImprovements?: boolean; disableBulkToggle?: boolean; + segmentContextFieldUsage?: boolean; } export interface IVersionInfo { diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 22c257eae2..6779d0a14e 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -90,6 +90,7 @@ exports[`should create default config 1`] = ` "personalAccessTokensKillSwitch": false, "proPlanAutoCharge": false, "responseTimeWithAppNameKillSwitch": false, + "segmentContextFieldUsage": false, "strategyImprovements": false, "strictSchemaValidation": false, "variantMetrics": false, @@ -120,6 +121,7 @@ exports[`should create default config 1`] = ` "personalAccessTokensKillSwitch": false, "proPlanAutoCharge": false, "responseTimeWithAppNameKillSwitch": false, + "segmentContextFieldUsage": false, "strategyImprovements": false, "strictSchemaValidation": false, "variantMetrics": false, diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index f48557d450..046bcca2b0 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -99,7 +99,12 @@ export const createStores = ( ), userSplashStore: new UserSplashStore(db, eventBus, getLogger), roleStore: new RoleStore(db, eventBus, getLogger), - segmentStore: new SegmentStore(db, eventBus, getLogger), + segmentStore: new SegmentStore( + db, + eventBus, + getLogger, + config.flagResolver, + ), groupStore: new GroupStore(db), publicSignupTokenStore: new PublicSignupTokenStore( db, diff --git a/src/lib/db/segment-store.ts b/src/lib/db/segment-store.ts index 28899f1ca0..aa2ae88e18 100644 --- a/src/lib/db/segment-store.ts +++ b/src/lib/db/segment-store.ts @@ -6,6 +6,7 @@ import NotFoundError from '../error/notfound-error'; import { PartialSome } from '../types/partial'; import User from '../types/user'; import { Db } from './db'; +import { IFlagResolver } from '../types'; const T = { segments: 'segments', @@ -29,7 +30,9 @@ interface ISegmentRow { description?: string; segment_project_id?: string; created_by?: string; - created_at?: Date; + created_at: Date; + used_in_projects?: number; + used_in_features?: number; constraints: IConstraint[]; } @@ -46,9 +49,17 @@ export default class SegmentStore implements ISegmentStore { private db: Db; - constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { + private flagResolver: IFlagResolver; + + constructor( + db: Db, + eventBus: EventEmitter, + getLogger: LogProvider, + flagResolver: IFlagResolver, + ) { this.db = db; this.eventBus = eventBus; + this.flagResolver = flagResolver; this.logger = getLogger('lib/db/segment-store.ts'); } @@ -96,12 +107,42 @@ export default class SegmentStore implements ISegmentStore { } async getAll(): Promise { - const rows: ISegmentRow[] = await this.db - .select(this.prefixColumns()) - .from(T.segments) - .orderBy('name', 'asc'); + if (this.flagResolver.isEnabled('segmentContextFieldUsage')) { + const rows: ISegmentRow[] = await this.db + .select( + this.prefixColumns(), + 'used_in_projects', + 'used_in_features', + ) + .countDistinct( + `${T.featureStrategies}.project_name AS used_in_projects`, + ) + .countDistinct( + `${T.featureStrategies}.feature_name AS used_in_features`, + ) + .from(T.segments) + .leftJoin( + T.featureStrategySegment, + `${T.segments}.id`, + `${T.featureStrategySegment}.segment_id`, + ) + .leftJoin( + T.featureStrategies, + `${T.featureStrategies}.id`, + `${T.featureStrategySegment}.feature_strategy_id`, + ) + .groupBy(this.prefixColumns()) + .orderBy('name', 'asc'); - return rows.map(this.mapRow); + return rows.map(this.mapRow); + } else { + const rows: ISegmentRow[] = await this.db + .select(this.prefixColumns()) + .from(T.segments) + .orderBy('name', 'asc'); + + return rows.map(this.mapRow); + } } async getActive(): Promise { @@ -199,7 +240,7 @@ export default class SegmentStore implements ISegmentStore { throw new NotFoundError('No row'); } - return { + const segment: ISegment = { id: row.id, name: row.name, description: row.description, @@ -207,7 +248,15 @@ export default class SegmentStore implements ISegmentStore { constraints: row.constraints, createdBy: row.created_by, createdAt: row.created_at, + usedInProjects: row.used_in_projects + ? Number(row.used_in_projects) + : 0, + usedInFeatures: row.used_in_projects + ? Number(row.used_in_features) + : 0, }; + + return segment; } destroy(): void {} diff --git a/src/lib/features/export-import-toggles/createExportImportService.ts b/src/lib/features/export-import-toggles/createExportImportService.ts index fe04184cef..ce39eabb0c 100644 --- a/src/lib/features/export-import-toggles/createExportImportService.ts +++ b/src/lib/features/export-import-toggles/createExportImportService.ts @@ -118,7 +118,12 @@ export const createExportImportTogglesService = ( const featureToggleStore = new FeatureToggleStore(db, eventBus, getLogger); const tagStore = new TagStore(db, eventBus, getLogger); const tagTypeStore = new TagTypeStore(db, eventBus, getLogger); - const segmentStore = new SegmentStore(db, eventBus, getLogger); + const segmentStore = new SegmentStore( + db, + eventBus, + getLogger, + flagResolver, + ); const projectStore = new ProjectStore( db, eventBus, diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index 3323aac976..98b2eb6060 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -68,7 +68,12 @@ export const createFeatureToggleService = ( eventBus, getLogger, ); - const segmentStore = new SegmentStore(db, eventBus, getLogger); + const segmentStore = new SegmentStore( + db, + eventBus, + getLogger, + flagResolver, + ); const contextFieldStore = new ContextFieldStore(db, getLogger); const groupStore = new GroupStore(db); const accountStore = new AccountStore(db, getLogger); diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index d9f04590b3..0e031d5038 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -21,7 +21,8 @@ export type IFlagKey = | 'strategyImprovements' | 'googleAuthEnabled' | 'variantMetrics' - | 'disableBulkToggle'; + | 'disableBulkToggle' + | 'segmentContextFieldUsage'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -98,6 +99,10 @@ const flags: IFlags = { process.env.DISABLE_BULK_TOGGLE, false, ), + segmentContextFieldUsage: parseEnvVarBoolean( + process.env.UNLEASH_SSEGMENT_CONTEXT_FIELD_USAGE, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 3486563c8b..3a24ebd9f7 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -402,6 +402,8 @@ export interface ISegment { description?: string; project?: string; constraints: IConstraint[]; + usedInProjects?: number; + usedInFeatures?: number; createdBy?: string; createdAt: Date; } diff --git a/src/server-dev.ts b/src/server-dev.ts index 44bc204350..20895a4019 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -40,6 +40,7 @@ process.nextTick(async () => { responseTimeWithAppNameKillSwitch: false, variantMetrics: true, strategyImprovements: true, + segmentContextFieldUsage: true, }, }, authentication: {