1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

feat: add usage of segment in list (#3853)

This commit is contained in:
Jaanus Sellin 2023-05-26 14:37:00 +03:00 committed by GitHub
parent 49722d5c48
commit f73d36fda3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 137 additions and 18 deletions

View File

@ -11,7 +11,13 @@ import {
import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import { useTable, useGlobalFilter, useSortBy } from 'react-table';
import { CreateSegmentButton } from 'component/segments/CreateSegmentButton/CreateSegmentButton'; import { CreateSegmentButton } from 'component/segments/CreateSegmentButton/CreateSegmentButton';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; 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 { sortTypes } from 'utils/sortTypes';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
@ -28,10 +34,13 @@ import { Search } from 'component/common/Search/Search';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import { RowSelectCell } from '../project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell';
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
export const SegmentTable = () => { export const SegmentTable = () => {
const projectId = useOptionalPathParam('projectId'); const projectId = useOptionalPathParam('projectId');
const { segments, loading } = useSegments(); const { segments, loading } = useSegments();
const { uiConfig } = useUiConfig();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const [initialState] = useState({ const [initialState] = useState({
sortBy: [{ id: 'createdAt' }], sortBy: [{ id: 'createdAt' }],
@ -56,6 +65,10 @@ export const SegmentTable = () => {
return segments; return segments;
}, [segments, projectId]); }, [segments, projectId]);
const columns = useMemo(
() => getColumns(uiConfig.flags.segmentContextFieldUsage),
[uiConfig.flags.segmentContextFieldUsage]
);
const { const {
getTableProps, getTableProps,
getTableBodyProps, getTableBodyProps,
@ -68,7 +81,7 @@ export const SegmentTable = () => {
} = useTable( } = useTable(
{ {
initialState, initialState,
columns: COLUMNS as any, columns: columns as any,
data: data as any, data: data as any,
sortTypes, sortTypes,
autoResetGlobalFilter: false, autoResetGlobalFilter: false,
@ -95,7 +108,7 @@ export const SegmentTable = () => {
}, },
], ],
setHiddenColumns, setHiddenColumns,
COLUMNS columns
); );
return ( return (
@ -170,7 +183,7 @@ export const SegmentTable = () => {
); );
}; };
const COLUMNS = [ const getColumns = (segmentContextFieldUsage?: boolean) => [
{ {
id: 'Icon', id: 'Icon',
width: '1%', width: '1%',
@ -182,10 +195,41 @@ const COLUMNS = [
Header: 'Name', Header: 'Name',
accessor: 'name', accessor: 'name',
width: '60%', width: '60%',
Cell: ({ value, row: { original } }: any) => ( Cell: ({
<HighlightCell value={value} subtitle={original.description} /> row: {
original: { name, description, id },
},
}: any) => (
<LinkCell
title={name}
to={`/segments/edit/${id}`}
subtitle={description}
/>
), ),
}, },
...(segmentContextFieldUsage
? [
{
Header: 'Used in',
width: '60%',
Cell: ({ value, row: { original } }: any) => (
<TextCell
sx={{
color:
original.usedInProjects === 0 &&
original.usedInFeatures === 0
? theme.palette.text.disabled
: 'inherit',
}}
>
<Box>{original.usedInProjects} projects</Box>
<Box>{original.usedInFeatures} feature toggles</Box>
</TextCell>
),
},
]
: []),
{ {
Header: 'Project', Header: 'Project',
accessor: 'project', accessor: 'project',

View File

@ -51,6 +51,7 @@ export interface IFlags {
variantMetrics?: boolean; variantMetrics?: boolean;
strategyImprovements?: boolean; strategyImprovements?: boolean;
disableBulkToggle?: boolean; disableBulkToggle?: boolean;
segmentContextFieldUsage?: boolean;
} }
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -90,6 +90,7 @@ exports[`should create default config 1`] = `
"personalAccessTokensKillSwitch": false, "personalAccessTokensKillSwitch": false,
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,
"segmentContextFieldUsage": false,
"strategyImprovements": false, "strategyImprovements": false,
"strictSchemaValidation": false, "strictSchemaValidation": false,
"variantMetrics": false, "variantMetrics": false,
@ -120,6 +121,7 @@ exports[`should create default config 1`] = `
"personalAccessTokensKillSwitch": false, "personalAccessTokensKillSwitch": false,
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,
"segmentContextFieldUsage": false,
"strategyImprovements": false, "strategyImprovements": false,
"strictSchemaValidation": false, "strictSchemaValidation": false,
"variantMetrics": false, "variantMetrics": false,

View File

@ -99,7 +99,12 @@ export const createStores = (
), ),
userSplashStore: new UserSplashStore(db, eventBus, getLogger), userSplashStore: new UserSplashStore(db, eventBus, getLogger),
roleStore: new RoleStore(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), groupStore: new GroupStore(db),
publicSignupTokenStore: new PublicSignupTokenStore( publicSignupTokenStore: new PublicSignupTokenStore(
db, db,

View File

@ -6,6 +6,7 @@ import NotFoundError from '../error/notfound-error';
import { PartialSome } from '../types/partial'; import { PartialSome } from '../types/partial';
import User from '../types/user'; import User from '../types/user';
import { Db } from './db'; import { Db } from './db';
import { IFlagResolver } from '../types';
const T = { const T = {
segments: 'segments', segments: 'segments',
@ -29,7 +30,9 @@ interface ISegmentRow {
description?: string; description?: string;
segment_project_id?: string; segment_project_id?: string;
created_by?: string; created_by?: string;
created_at?: Date; created_at: Date;
used_in_projects?: number;
used_in_features?: number;
constraints: IConstraint[]; constraints: IConstraint[];
} }
@ -46,9 +49,17 @@ export default class SegmentStore implements ISegmentStore {
private db: Db; 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.db = db;
this.eventBus = eventBus; this.eventBus = eventBus;
this.flagResolver = flagResolver;
this.logger = getLogger('lib/db/segment-store.ts'); this.logger = getLogger('lib/db/segment-store.ts');
} }
@ -96,6 +107,35 @@ export default class SegmentStore implements ISegmentStore {
} }
async getAll(): Promise<ISegment[]> { async getAll(): Promise<ISegment[]> {
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);
} else {
const rows: ISegmentRow[] = await this.db const rows: ISegmentRow[] = await this.db
.select(this.prefixColumns()) .select(this.prefixColumns())
.from(T.segments) .from(T.segments)
@ -103,6 +143,7 @@ export default class SegmentStore implements ISegmentStore {
return rows.map(this.mapRow); return rows.map(this.mapRow);
} }
}
async getActive(): Promise<ISegment[]> { async getActive(): Promise<ISegment[]> {
const rows: ISegmentRow[] = await this.db const rows: ISegmentRow[] = await this.db
@ -199,7 +240,7 @@ export default class SegmentStore implements ISegmentStore {
throw new NotFoundError('No row'); throw new NotFoundError('No row');
} }
return { const segment: ISegment = {
id: row.id, id: row.id,
name: row.name, name: row.name,
description: row.description, description: row.description,
@ -207,7 +248,15 @@ export default class SegmentStore implements ISegmentStore {
constraints: row.constraints, constraints: row.constraints,
createdBy: row.created_by, createdBy: row.created_by,
createdAt: row.created_at, 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 {} destroy(): void {}

View File

@ -118,7 +118,12 @@ export const createExportImportTogglesService = (
const featureToggleStore = new FeatureToggleStore(db, eventBus, getLogger); const featureToggleStore = new FeatureToggleStore(db, eventBus, getLogger);
const tagStore = new TagStore(db, eventBus, getLogger); const tagStore = new TagStore(db, eventBus, getLogger);
const tagTypeStore = new TagTypeStore(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( const projectStore = new ProjectStore(
db, db,
eventBus, eventBus,

View File

@ -68,7 +68,12 @@ export const createFeatureToggleService = (
eventBus, eventBus,
getLogger, getLogger,
); );
const segmentStore = new SegmentStore(db, eventBus, getLogger); const segmentStore = new SegmentStore(
db,
eventBus,
getLogger,
flagResolver,
);
const contextFieldStore = new ContextFieldStore(db, getLogger); const contextFieldStore = new ContextFieldStore(db, getLogger);
const groupStore = new GroupStore(db); const groupStore = new GroupStore(db);
const accountStore = new AccountStore(db, getLogger); const accountStore = new AccountStore(db, getLogger);

View File

@ -21,7 +21,8 @@ export type IFlagKey =
| 'strategyImprovements' | 'strategyImprovements'
| 'googleAuthEnabled' | 'googleAuthEnabled'
| 'variantMetrics' | 'variantMetrics'
| 'disableBulkToggle'; | 'disableBulkToggle'
| 'segmentContextFieldUsage';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -98,6 +99,10 @@ const flags: IFlags = {
process.env.DISABLE_BULK_TOGGLE, process.env.DISABLE_BULK_TOGGLE,
false, false,
), ),
segmentContextFieldUsage: parseEnvVarBoolean(
process.env.UNLEASH_SSEGMENT_CONTEXT_FIELD_USAGE,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -402,6 +402,8 @@ export interface ISegment {
description?: string; description?: string;
project?: string; project?: string;
constraints: IConstraint[]; constraints: IConstraint[];
usedInProjects?: number;
usedInFeatures?: number;
createdBy?: string; createdBy?: string;
createdAt: Date; createdAt: Date;
} }

View File

@ -40,6 +40,7 @@ process.nextTick(async () => {
responseTimeWithAppNameKillSwitch: false, responseTimeWithAppNameKillSwitch: false,
variantMetrics: true, variantMetrics: true,
strategyImprovements: true, strategyImprovements: true,
segmentContextFieldUsage: true,
}, },
}, },
authentication: { authentication: {