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

chore: remove flagsReleaseManagementUI and flagsOverviewSearch flags (#10011)

Removing the `flagsReleaseManagementUI` and `flagsOverviewSearch`
feature flags - we're keeping these enabled.
This commit is contained in:
Tymoteusz Czech 2025-05-16 15:13:32 +02:00 committed by GitHub
parent 8afaf7e88b
commit b0954f213c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 168 additions and 409 deletions

View File

@ -18,15 +18,6 @@ type APIProject = {
id: string;
};
type UIFeature = {
name: string;
segments: string;
tags: string;
createdAt: string;
project: string;
state: string;
};
const server = testServerSetup();
const setupNoFeaturesReturned = () =>
@ -74,27 +65,6 @@ const setupApi = (features: APIFeature[], projects: APIProject[]) => {
});
};
const verifyTableFeature = async (feature: Partial<UIFeature>) => {
await screen.findByText('Search');
await screen.findByText('Filter');
await Promise.all(
Object.values(feature).map((value) => screen.findByText(value)),
);
};
const filterFeaturesByProject = async (projectName: string) => {
const addFilterButton = screen.getByText('Filter');
addFilterButton.click();
const projectItem = await screen.findByText('Project');
fireEvent.click(projectItem);
await screen.findByPlaceholderText('Search');
const anotherProjectCheckbox = await screen.findByText(projectName);
anotherProjectCheckbox.click();
};
test('Filter table by project', async () => {
setupApi(
[
@ -124,17 +94,33 @@ test('Filter table by project', async () => {
</FeedbackProvider>,
);
await verifyTableFeature({
name: 'Operational Feature',
createdAt: '11/03/2023',
segments: '1 segment',
tags: '1 tag',
project: 'project-a',
state: 'Active',
await screen.findByPlaceholderText(/Search/);
await screen.getByRole('button', {
name: /Filter/i,
});
await Promise.all(
Object.values({
name: 'Operational Feature',
createdAt: '11/03/2023',
project: 'project-a',
}).map((value) => screen.findByText(value)),
);
setupNoFeaturesReturned();
await filterFeaturesByProject('Project B');
const addFilterButton = screen.getByText('Filter');
addFilterButton.click();
const projectItem = await screen.findByRole('menuitem', {
name: 'Project',
});
fireEvent.click(projectItem);
await screen.findByPlaceholderText('Search');
const anotherProjectCheckbox = await screen.findByText('Project B');
anotherProjectCheckbox.click();
await screen.findByText('No feature flags found matching your criteria.');
expect(window.location.href).toContain(

View File

@ -6,31 +6,22 @@ import { PaginatedTable, TablePlaceholder } from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import type { FeatureSchema, FeatureSearchResponseSchema } from 'openapi';
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell.tsx';
import { Search } from 'component/common/Search/Search';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { ExportDialog } from './ExportDialog.tsx';
import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { focusable } from 'themes/themeStyles';
import {
FeatureEnvironmentSeenCell,
FeatureLifecycleCell,
} from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import { FeatureLifecycleCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useToast from 'hooks/useToast';
import { FeatureToggleFilters } from './FeatureToggleFilters/FeatureToggleFilters.tsx';
import { withTableState } from 'utils/withTableState';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell';
import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions.tsx';
import useLoading from 'hooks/useLoading';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import {
@ -80,9 +71,6 @@ export const FeatureToggleListTable: FC = () => {
const [showExportDialog, setShowExportDialog] = useState(false);
const { setToastApiError } = useToast();
const flagsReleaseManagementUIEnabled = useUiFlag(
'flagsReleaseManagementUI',
);
const {
features,
@ -126,228 +114,94 @@ export const FeatureToggleListTable: FC = () => {
[favorite, refetchFeatures, unfavorite, setToastApiError],
);
const columns = useMemo(
() =>
flagsReleaseManagementUIEnabled
? [
columnHelper.accessor('favorite', {
header: () => (
<FavoriteIconHeader
isActive={tableState.favoritesFirst}
onClick={() =>
setTableState({
favoritesFirst:
!tableState.favoritesFirst,
})
}
/>
),
cell: ({ getValue, row }) => (
<FavoriteIconCell
value={getValue()}
onClick={() => onFavorite(row.original)}
/>
),
enableSorting: false,
meta: { width: 48 },
}),
columnHelper.accessor('name', {
header: 'Name',
cell: createFeatureOverviewCell(
onTagClick,
onFlagTypeClick,
),
meta: { width: '40%' },
}),
columnHelper.accessor('createdAt', {
header: 'Created',
cell: ({ getValue }) => (
<DateCell value={getValue()} />
),
meta: { width: '1%' },
}),
columnHelper.accessor('createdBy', {
id: 'createdBy',
header: 'By',
cell: AvatarCell(),
meta: { width: '1%', align: 'center' },
enableSorting: false,
}),
columnHelper.accessor('lifecycle', {
id: 'lifecycle',
header: 'Lifecycle',
cell: ({ row: { original } }) => (
<FeatureLifecycleCell
feature={original}
expanded
data-loading
/>
),
enableSorting: false,
size: 50,
meta: { width: '1%' },
}),
...(!isOss()
? [
columnHelper.accessor('environments', {
id: 'status',
header: 'Status',
cell: ({ row: { original } }) => (
<StatusCell {...original} />
),
enableSorting: false,
size: 350,
}),
]
: []),
columnHelper.accessor('project', {
header: 'Project',
cell: ({ getValue }) => {
const projectId = getValue();
const projectName = projects.find(
(project) => project.id === projectId,
)?.name;
const showStatusColumn = !isOss();
return (
<Box sx={{ minWidth: '180px' }}>
<LinkCell
title={projectName || projectId}
to={`/projects/${projectId}`}
/>
</Box>
);
},
const columns = useMemo(
() => [
columnHelper.accessor('favorite', {
header: () => (
<FavoriteIconHeader
isActive={tableState.favoritesFirst}
onClick={() =>
setTableState({
favoritesFirst: !tableState.favoritesFirst,
})
}
/>
),
cell: ({ getValue, row }) => (
<FavoriteIconCell
value={getValue()}
onClick={() => onFavorite(row.original)}
/>
),
enableSorting: false,
meta: { width: 48 },
}),
columnHelper.accessor('name', {
header: 'Name',
cell: createFeatureOverviewCell(onTagClick, onFlagTypeClick),
meta: { width: '40%' },
}),
columnHelper.accessor('createdAt', {
header: 'Created',
cell: ({ getValue }) => <DateCell value={getValue()} />,
meta: { width: '1%' },
}),
columnHelper.accessor('createdBy', {
id: 'createdBy',
header: 'By',
cell: AvatarCell(),
meta: { width: '1%', align: 'center' },
enableSorting: false,
}),
columnHelper.accessor('lifecycle', {
id: 'lifecycle',
header: 'Lifecycle',
cell: ({ row: { original } }) => (
<FeatureLifecycleCell
feature={original}
expanded
data-loading
/>
),
enableSorting: false,
size: 50,
meta: { width: '1%' },
}),
...(showStatusColumn
? [
columnHelper.accessor('environments', {
id: 'status',
header: 'Status',
cell: ({ row: { original } }) => (
<StatusCell {...original} />
),
enableSorting: false,
size: 350,
}),
]
: [
columnHelper.accessor('favorite', {
header: () => (
<FavoriteIconHeader
isActive={tableState.favoritesFirst}
onClick={() =>
setTableState({
favoritesFirst:
!tableState.favoritesFirst,
})
}
/>
),
cell: ({ getValue, row }) => (
<>
<FavoriteIconCell
value={getValue()}
onClick={() => onFavorite(row.original)}
/>
</>
),
enableSorting: false,
meta: {
width: '1%',
},
}),
columnHelper.accessor('lastSeenAt', {
header: 'Seen',
cell: ({ row }) => (
<FeatureEnvironmentSeenCell
feature={row.original}
/>
),
meta: {
align: 'center',
width: '1%',
},
}),
columnHelper.accessor('type', {
header: 'Type',
cell: ({ getValue }) => (
<FeatureTypeCell value={getValue()} />
),
meta: {
align: 'center',
width: '1%',
},
}),
: []),
columnHelper.accessor('project', {
header: 'Project',
cell: ({ getValue }) => {
const projectId = getValue();
const projectName = projects.find(
(project) => project.id === projectId,
)?.name;
columnHelper.accessor('name', {
header: 'Name',
cell: ({ row }) => (
<LinkCell
title={row.original.name}
subtitle={
row.original.description || undefined
}
to={`/projects/${row.original.project}/features/${row.original.name}`}
/>
),
meta: {
width: '50%',
},
}),
columnHelper.accessor(
(row) => row.segments?.join('\n') || '',
{
header: 'Segments',
cell: ({ getValue, row }) => (
<FeatureSegmentCell
value={getValue()}
row={row}
/>
),
enableSorting: false,
meta: {
width: '1%',
},
},
),
columnHelper.accessor(
(row) =>
row.tags
?.map(({ type, value }) => `${type}:${value}`)
.join('\n') || '',
{
header: 'Tags',
cell: FeatureTagCell,
enableSorting: false,
meta: {
width: '1%',
},
},
),
columnHelper.accessor('createdAt', {
header: 'Created',
cell: ({ getValue }) => (
<DateCell value={getValue()} />
),
meta: {
width: '1%',
},
}),
columnHelper.accessor('project', {
header: 'Project ID',
cell: ({ getValue }) => {
const value = getValue();
return (
<LinkCell
title={value}
to={`/projects/${value}`}
/>
);
},
meta: {
width: '1%',
},
}),
columnHelper.accessor('stale', {
header: 'State',
cell: ({ getValue }) => (
<FeatureStaleCell value={getValue()} />
),
meta: {
width: '1%',
},
}),
],
[tableState.favoritesFirst],
return (
<Box sx={{ minWidth: '180px' }}>
<LinkCell
title={projectName || projectId}
to={`/projects/${projectId}`}
/>
</Box>
);
},
}),
],
[tableState.favoritesFirst, showStatusColumn],
);
const data = useMemo<FeatureSearchResponseSchema[]>(
() =>
@ -402,26 +256,9 @@ export const FeatureToggleListTable: FC = () => {
bodyClass='no-padding'
header={
<PageHeader
title={
flagsReleaseManagementUIEnabled
? 'Flags overview'
: 'Search'
}
title='Flags overview'
actions={
<>
{!flagsReleaseManagementUIEnabled &&
!isSmallScreen ? (
<>
<Search
placeholder='Search'
expandable
initialValue={tableState.query || ''}
onChange={setSearchValue}
id='globalFeatureFlags'
/>
<PageHeader.Divider />
</>
) : null}
<Link
component={RouterLink}
to='/archive'
@ -437,51 +274,28 @@ export const FeatureToggleListTable: FC = () => {
>
View archive
</Link>
{flagsReleaseManagementUIEnabled ? (
<ExportFlags
onClick={() => setShowExportDialog(true)}
/>
) : (
<FeatureToggleListActions
onExportClick={() =>
setShowExportDialog(true)
}
/>
)}
<ExportFlags
onClick={() => setShowExportDialog(true)}
/>
</>
}
>
<ConditionallyRender
condition={
isSmallScreen && !flagsReleaseManagementUIEnabled
}
show={
<Search
initialValue={tableState.query || ''}
onChange={setSearchValue}
id='globalFeatureFlags'
/>
}
/>
</PageHeader>
/>
}
>
{flagsReleaseManagementUIEnabled ? (
<LifecycleFilters
state={filterState}
onChange={setTableState}
total={loading ? undefined : total}
>
{!isSmallScreen ? (
<Search
placeholder='Search'
initialValue={tableState.query || ''}
onChange={setSearchValue}
id='globalFeatureFlags'
/>
) : null}
</LifecycleFilters>
) : null}
<LifecycleFilters
state={filterState}
onChange={setTableState}
total={loading ? undefined : total}
>
{!isSmallScreen ? (
<Search
placeholder='Search'
initialValue={tableState.query || ''}
onChange={setSearchValue}
id='globalFeatureFlags'
/>
) : null}
</LifecycleFilters>
<FeatureToggleFilters
onChange={setTableState}
state={filterState}

View File

@ -14,7 +14,6 @@ import ServiceAccountIcon from '@mui/icons-material/Computer';
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
import SettingsIcon from '@mui/icons-material/Settings';
import SearchIcon from '@mui/icons-material/Search';
import InsightsIcon from '@mui/icons-material/Insights';
import ApiAccessIcon from '@mui/icons-material/KeyOutlined';
import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined';
@ -35,14 +34,13 @@ import PersonalDashboardIcon from '@mui/icons-material/DashboardOutlined';
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
import PlaygroundIcon from '@mui/icons-material/AutoFixNormal';
import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined';
import { useUiFlag } from 'hooks/useUiFlag';
// TODO: move to routes
const icons: Record<
string,
typeof SvgIcon | FC<ComponentProps<typeof SvgIcon>>
> = {
'/search': SearchIcon,
'/search': FlagOutlinedIcon,
'/insights': InsightsIcon,
'/applications': ApplicationsIcon,
'/context': ContextFieldsIcon,
@ -89,12 +87,7 @@ const icons: Record<
};
export const IconRenderer: FC<{ path: string }> = ({ path }) => {
const flagsReleaseManagementUI = useUiFlag('flagsReleaseManagementUI');
const IconComponent = useMemo(() => icons[path] || EmptyIcon, [path]); // Fallback to 'default' if the type is not found
if (flagsReleaseManagementUI && path === '/search') {
return <FlagOutlinedIcon />;
}
return <IconComponent />;
};

View File

@ -9,7 +9,6 @@ import {
SignOutItem,
} from './ListItems.tsx';
import { Box, List, Typography } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
import { IconRenderer } from './IconRenderer.tsx';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import Accordion from '@mui/material/Accordion';
@ -127,7 +126,6 @@ export const PrimaryNavigationList: FC<{
}> = ({ mode, onClick, activeItem }) => {
const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem;
const { isOss } = useUiConfig();
const flagsReleaseManagementUI = useUiFlag('flagsReleaseManagementUI');
return (
<List>
@ -150,7 +148,7 @@ export const PrimaryNavigationList: FC<{
</DynamicListItem>
<DynamicListItem
href='/search'
text={flagsReleaseManagementUI ? 'Flags overview' : 'Search'}
text='Flags overview'
onClick={() => onClick('/search')}
selected={activeItem === '/search'}
>

View File

@ -69,7 +69,7 @@ test('select active item', async () => {
{ route: '/search' },
);
const searchLink = screen.getByRole('link', { name: 'Search' });
const searchLink = screen.getByRole('link', { name: 'Flags overview' });
expect(searchLink).toHaveClass(classes.selected);
});

View File

@ -15,8 +15,8 @@ exports[`order of items in navigation > menu for enterprise plan 1`] = `
"text": "Projects",
},
{
"icon": "SearchIcon",
"text": "Search",
"icon": "FlagOutlinedIcon",
"text": "Flags overview",
},
{
"icon": "AutoFixNormalIcon",
@ -80,8 +80,8 @@ exports[`order of items in navigation > menu for open-source 1`] = `
"text": "Projects",
},
{
"icon": "SearchIcon",
"text": "Search",
"icon": "FlagOutlinedIcon",
"text": "Flags overview",
},
{
"icon": "AutoFixNormalIcon",
@ -137,8 +137,8 @@ exports[`order of items in navigation > menu for pro plan 1`] = `
"text": "Projects",
},
{
"icon": "SearchIcon",
"text": "Search",
"icon": "FlagOutlinedIcon",
"text": "Flags overview",
},
{
"icon": "AutoFixNormalIcon",

View File

@ -104,17 +104,6 @@ exports[`returns all baseRoutes 1`] = `
"menu": {
"primary": true,
},
"notFlag": "flagsReleaseManagementUI",
"path": "/search",
"title": "Search",
"type": "protected",
},
{
"component": [Function],
"flag": "flagsReleaseManagementUI",
"menu": {
"primary": true,
},
"path": "/search",
"title": "Flags overview",
"type": "protected",

View File

@ -130,21 +130,12 @@ export const routes: IRoute[] = [
},
// Flags overview
{
path: '/search',
title: 'Search',
component: FeatureToggleListTable,
type: 'protected',
menu: { primary: true },
notFlag: 'flagsReleaseManagementUI',
},
{
path: '/search',
title: 'Flags overview',
component: FeatureToggleListTable,
type: 'protected',
menu: { primary: true },
flag: 'flagsReleaseManagementUI',
},
// Playground

View File

@ -5,11 +5,11 @@ import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
type AvatarCellProps = {
row: {
original: {
createdBy: {
id: number;
name: string;
row?: {
original?: {
createdBy?: {
id?: number;
name?: string;
imageUrl?: string;
};
};
@ -42,35 +42,39 @@ const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
export const AvatarCell =
(onAvatarClick?: (userId: number) => void): FC<AvatarCellProps> =>
({ row: { original } }) => {
const ariaDisabled = original.createdBy.id === 0;
({ row }) => {
const createdBy = {
id: 0,
name: '',
imageUrl: '',
...row?.original?.createdBy,
};
const ariaDisabled = createdBy.id === 0;
const clickAction = ariaDisabled
? () => {}
: () => onAvatarClick?.(original.createdBy.id);
: () => onAvatarClick?.(createdBy.id);
const tooltipContent = ariaDisabled ? (
<>
<p>{original.createdBy.name}</p>
<p>{createdBy.name}</p>
<StyledSecondaryText>
You can't filter by unknown users.
</StyledSecondaryText>
</>
) : (
<p>{original.createdBy.name}</p>
<p>{createdBy.name}</p>
);
const content = (
<>
<ScreenReaderOnly>
<span>
Show only flags created by {original.createdBy.name}
</span>
<span>Show only flags created by {createdBy.name}</span>
</ScreenReaderOnly>
<StyledAvatar
disableTooltip
user={{
id: original.createdBy.id,
name: original.createdBy.name,
imageUrl: original.createdBy.imageUrl,
id: createdBy.id,
name: createdBy.name,
imageUrl: createdBy.imageUrl,
}}
/>
</>

View File

@ -87,7 +87,6 @@ export type UiFlags = {
consumptionModel?: boolean;
edgeObservability?: boolean;
addEditStrategy?: boolean;
flagsReleaseManagementUI?: boolean;
cleanupReminder?: boolean;
registerFrontendClient?: boolean;
featureLinks?: boolean;

View File

@ -268,16 +268,14 @@ class FeatureSearchStore implements IFeatureSearchStore {
'lifecycle.stage_feature',
);
if (this.flagResolver.isEnabled('flagsOverviewSearch')) {
const parsedLifecycle = lifecycle
? parseSearchOperatorValue(
'lifecycle.latest_stage',
lifecycle,
)
: null;
if (parsedLifecycle) {
applyGenericQueryParams(query, [parsedLifecycle]);
}
const parsedLifecycle = lifecycle
? parseSearchOperatorValue(
'lifecycle.latest_stage',
lifecycle,
)
: null;
if (parsedLifecycle) {
applyGenericQueryParams(query, [parsedLifecycle]);
}
const rankingSql = this.buildRankingSql(
@ -342,10 +340,8 @@ class FeatureSearchStore implements IFeatureSearchStore {
.whereBetween('final_rank', [offset + 1, offset + limit])
.orderBy('final_rank');
if (this.flagResolver.isEnabled('flagsOverviewSearch')) {
this.buildChangeRequestSql(finalQuery);
this.buildReleasePlanSql(finalQuery);
}
this.buildChangeRequestSql(finalQuery);
this.buildReleasePlanSql(finalQuery);
this.queryExtraData(finalQuery);
const rows = await finalQuery;

View File

@ -31,7 +31,6 @@ beforeAll(async () => {
flags: {
strictSchemaValidation: true,
anonymiseEventLog: true,
flagsOverviewSearch: true,
},
},
},

View File

@ -58,8 +58,6 @@ export type IFlagKey =
| 'teamsIntegrationChangeRequests'
| 'edgeObservability'
| 'addEditStrategy'
| 'flagsOverviewSearch'
| 'flagsReleaseManagementUI'
| 'cleanupReminder'
| 'removeInactiveApplications'
| 'registerFrontendClient'
@ -280,14 +278,7 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_ADD_EDIT_STRATEGY,
false,
),
flagsOverviewSearch: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FLAGS_OVERVIEW_SEARCH,
false,
),
flagsReleaseManagementUI: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FLAGS_RELEASE_MANAGEMENT_UI,
false,
),
cleanupReminder: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_CLEANUP_REMINDER,
false,

View File

@ -52,7 +52,6 @@ process.nextTick(async () => {
uniqueSdkTracking: true,
teamsIntegrationChangeRequests: true,
addEditStrategy: true,
flagsOverviewSearch: true,
cleanupReminder: true,
strictSchemaValidation: true,
registerFrontendClient: true,