mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-09 01:17:06 +02:00
Feature toggles: Filtering by tags (#2396)
https://linear.app/unleash/issue/UNL-140/experiment-with-filtering-feature-toggles-by-tags-on-the-ui Going with a naïve approach for now, tags can be searchable the same way we search for text. The tags column only shows up if at least one toggle has tags set. There's a simple highlightable component that lets us know a match was found and then shows all the tags on a tooltip: <img width="1289" alt="image" src="https://user-images.githubusercontent.com/14320932/201155093-b8605ff2-5bf7-45c5-b240-a33da254c278.png">
This commit is contained in:
parent
b891d1ec4f
commit
1ddc46011c
@ -0,0 +1,60 @@
|
|||||||
|
import { VFC } from 'react';
|
||||||
|
import { FeatureSchema } from 'openapi';
|
||||||
|
import { Link, styled, Typography } from '@mui/material';
|
||||||
|
import { TextCell } from '../TextCell/TextCell';
|
||||||
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
|
||||||
|
const StyledTag = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledLink = styled(Link, {
|
||||||
|
shouldForwardProp: prop => prop !== 'highlighted',
|
||||||
|
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
|
||||||
|
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IFeatureTagCellProps {
|
||||||
|
row: {
|
||||||
|
original: FeatureSchema;
|
||||||
|
};
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
|
||||||
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
|
if (!row.original.tags || row.original.tags.length === 0)
|
||||||
|
return <TextCell />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
<HtmlTooltip
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{row.original.tags?.map(tag => (
|
||||||
|
<StyledTag key={tag.type + tag.value}>
|
||||||
|
<Highlighter search={searchQuery}>
|
||||||
|
{`${tag.type}:${tag.value}`}
|
||||||
|
</Highlighter>
|
||||||
|
</StyledTag>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledLink
|
||||||
|
underline="always"
|
||||||
|
highlighted={
|
||||||
|
searchQuery.length > 0 && value.includes(searchQuery)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.original.tags?.length === 1
|
||||||
|
? '1 tag'
|
||||||
|
: `${row.original.tags?.length} tags`}
|
||||||
|
</StyledLink>
|
||||||
|
</HtmlTooltip>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
};
|
@ -20,6 +20,7 @@ import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'
|
|||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
|
|
||||||
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||||
name: 'Name of the feature',
|
name: 'Name of the feature',
|
||||||
@ -57,6 +58,15 @@ const columns = [
|
|||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
Header: 'Tags',
|
||||||
|
accessor: (row: FeatureSchema) =>
|
||||||
|
row.tags?.map(({ type, value }) => `${type}:${value}`).join('\n') ||
|
||||||
|
'',
|
||||||
|
Cell: FeatureTagCell,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: 'Created',
|
Header: 'Created',
|
||||||
accessor: 'createdAt',
|
accessor: 'createdAt',
|
||||||
@ -139,7 +149,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns,
|
columns: columns as any[],
|
||||||
data,
|
data,
|
||||||
initialState,
|
initialState,
|
||||||
sortTypes,
|
sortTypes,
|
||||||
@ -153,14 +163,17 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hiddenColumns = ['description'];
|
const hiddenColumns = ['description'];
|
||||||
|
if (!features.some(({ tags }) => tags?.length)) {
|
||||||
|
hiddenColumns.push('tags');
|
||||||
|
}
|
||||||
if (isMediumScreen) {
|
if (isMediumScreen) {
|
||||||
hiddenColumns.push('lastSeenAt', 'stale');
|
hiddenColumns.push('lastSeenAt', 'stale');
|
||||||
}
|
}
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
hiddenColumns.push('type', 'createdAt');
|
hiddenColumns.push('type', 'createdAt', 'tags');
|
||||||
}
|
}
|
||||||
setHiddenColumns(hiddenColumns);
|
setHiddenColumns(hiddenColumns);
|
||||||
}, [setHiddenColumns, isSmallScreen, isMediumScreen]);
|
}, [setHiddenColumns, isSmallScreen, isMediumScreen, features]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tableState: PageQueryType = {};
|
const tableState: PageQueryType = {};
|
||||||
|
@ -25,6 +25,7 @@ interface IColumnsMenuProps {
|
|||||||
id: string;
|
id: string;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
toggleHidden: (state: boolean) => void;
|
toggleHidden: (state: boolean) => void;
|
||||||
|
hideInMenu?: boolean;
|
||||||
}[];
|
}[];
|
||||||
staticColumns?: string[];
|
staticColumns?: string[];
|
||||||
dividerBefore?: string[];
|
dividerBefore?: string[];
|
||||||
@ -143,51 +144,56 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
{allColumns.map(column => [
|
{allColumns
|
||||||
<ConditionallyRender
|
.filter(({ hideInMenu }) => !hideInMenu)
|
||||||
condition={dividerBefore.includes(column.id)}
|
.map(column => [
|
||||||
show={<Divider className={classes.divider} />}
|
<ConditionallyRender
|
||||||
/>,
|
condition={dividerBefore.includes(column.id)}
|
||||||
<MenuItem
|
show={<Divider className={classes.divider} />}
|
||||||
onClick={() =>
|
/>,
|
||||||
column.toggleHidden(column.isVisible)
|
<MenuItem
|
||||||
}
|
onClick={() =>
|
||||||
disabled={staticColumns.includes(column.id)}
|
column.toggleHidden(column.isVisible)
|
||||||
className={classes.menuItem}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Checkbox
|
|
||||||
edge="start"
|
|
||||||
checked={column.isVisible}
|
|
||||||
disableRipple
|
|
||||||
inputProps={{
|
|
||||||
'aria-labelledby': column.id,
|
|
||||||
}}
|
|
||||||
size="medium"
|
|
||||||
className={classes.checkbox}
|
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
id={column.id}
|
|
||||||
primary={
|
|
||||||
<Typography variant="body2">
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
typeof column.Header ===
|
|
||||||
'string' && column.Header
|
|
||||||
)}
|
|
||||||
show={() => <>{column.Header}</>}
|
|
||||||
elseShow={() => column.id}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
}
|
}
|
||||||
/>
|
disabled={staticColumns.includes(column.id)}
|
||||||
</MenuItem>,
|
className={classes.menuItem}
|
||||||
<ConditionallyRender
|
>
|
||||||
condition={dividerAfter.includes(column.id)}
|
<ListItemIcon>
|
||||||
show={<Divider className={classes.divider} />}
|
<Checkbox
|
||||||
/>,
|
edge="start"
|
||||||
])}
|
checked={column.isVisible}
|
||||||
|
disableRipple
|
||||||
|
inputProps={{
|
||||||
|
'aria-labelledby': column.id,
|
||||||
|
}}
|
||||||
|
size="medium"
|
||||||
|
className={classes.checkbox}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
id={column.id}
|
||||||
|
primary={
|
||||||
|
<Typography variant="body2">
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
typeof column.Header ===
|
||||||
|
'string' &&
|
||||||
|
column.Header
|
||||||
|
)}
|
||||||
|
show={() => (
|
||||||
|
<>{column.Header}</>
|
||||||
|
)}
|
||||||
|
elseShow={() => column.id}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuItem>,
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={dividerAfter.includes(column.id)}
|
||||||
|
show={<Divider className={classes.divider} />}
|
||||||
|
/>,
|
||||||
|
])}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -41,6 +41,8 @@ import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConf
|
|||||||
import { CopyStrategyMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
|
import { CopyStrategyMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
|
||||||
import { UpdateEnabledMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
import { UpdateEnabledMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||||
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
interface IProjectFeatureTogglesProps {
|
||||||
features: IProject['features'];
|
features: IProject['features'];
|
||||||
@ -192,6 +194,17 @@ export const ProjectFeatureToggles = ({
|
|||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
Header: 'Tags',
|
||||||
|
accessor: (row: IFeatureToggleListItem) =>
|
||||||
|
row.tags
|
||||||
|
?.map(({ type, value }) => `${type}:${value}`)
|
||||||
|
.join('\n') || '',
|
||||||
|
Cell: FeatureTagCell,
|
||||||
|
hideInMenu: true,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: 'Created',
|
Header: 'Created',
|
||||||
accessor: 'createdAt',
|
accessor: 'createdAt',
|
||||||
@ -259,6 +272,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
createdAt,
|
createdAt,
|
||||||
type,
|
type,
|
||||||
stale,
|
stale,
|
||||||
|
tags,
|
||||||
environments: featureEnvironments,
|
environments: featureEnvironments,
|
||||||
}) => ({
|
}) => ({
|
||||||
name,
|
name,
|
||||||
@ -266,6 +280,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
createdAt,
|
createdAt,
|
||||||
type,
|
type,
|
||||||
stale,
|
stale,
|
||||||
|
tags,
|
||||||
environments: Object.fromEntries(
|
environments: Object.fromEntries(
|
||||||
environments.map(env => [
|
environments.map(env => [
|
||||||
env,
|
env,
|
||||||
@ -370,6 +385,16 @@ export const ProjectFeatureToggles = ({
|
|||||||
useSortBy
|
useSortBy
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!features.some(({ tags }) => tags?.length)) {
|
||||||
|
setHiddenColumns(hiddenColumns => [...hiddenColumns, 'tags']);
|
||||||
|
} else {
|
||||||
|
setHiddenColumns(hiddenColumns =>
|
||||||
|
hiddenColumns.filter(column => column !== 'tags')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [setHiddenColumns, features]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { IFeatureStrategy } from './strategy';
|
import { IFeatureStrategy } from './strategy';
|
||||||
|
import { ITag } from './tags';
|
||||||
|
|
||||||
export interface IFeatureToggleListItem {
|
export interface IFeatureToggleListItem {
|
||||||
type: string;
|
type: string;
|
||||||
@ -7,6 +8,7 @@ export interface IFeatureToggleListItem {
|
|||||||
lastSeenAt?: string;
|
lastSeenAt?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
environments: IEnvironments[];
|
environments: IEnvironments[];
|
||||||
|
tags?: ITag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEnvironments {
|
export interface IEnvironments {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 4.11.0-beta.2
|
* The version of the OpenAPI document: 4.11.0-beta.2
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
* https://openapi-generator.tech
|
* https://openapi-generator.tech
|
||||||
@ -25,6 +25,12 @@ import {
|
|||||||
StrategySchemaFromJSONTyped,
|
StrategySchemaFromJSONTyped,
|
||||||
StrategySchemaToJSON,
|
StrategySchemaToJSON,
|
||||||
} from './StrategySchema';
|
} from './StrategySchema';
|
||||||
|
import {
|
||||||
|
TagSchema,
|
||||||
|
TagSchemaFromJSON,
|
||||||
|
TagSchemaFromJSONTyped,
|
||||||
|
TagSchemaToJSON,
|
||||||
|
} from './TagSchema';
|
||||||
import {
|
import {
|
||||||
VariantSchema,
|
VariantSchema,
|
||||||
VariantSchemaFromJSON,
|
VariantSchemaFromJSON,
|
||||||
@ -33,121 +39,158 @@ import {
|
|||||||
} from './VariantSchema';
|
} from './VariantSchema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @interface FeatureSchema
|
* @interface FeatureSchema
|
||||||
*/
|
*/
|
||||||
export interface FeatureSchema {
|
export interface FeatureSchema {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
type?: string;
|
type?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
project?: string;
|
project?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
stale?: boolean;
|
stale?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
impressionData?: boolean;
|
impressionData?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
createdAt?: Date | null;
|
createdAt?: Date | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
archivedAt?: Date | null;
|
archivedAt?: Date | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
lastSeenAt?: Date | null;
|
lastSeenAt?: Date | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<FeatureEnvironmentSchema>}
|
* @type {Array<FeatureEnvironmentSchema>}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
environments?: Array<FeatureEnvironmentSchema>;
|
environments?: Array<FeatureEnvironmentSchema>;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<StrategySchema>}
|
* @type {Array<StrategySchema>}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
strategies?: Array<StrategySchema>;
|
strategies?: Array<StrategySchema>;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<VariantSchema>}
|
* @type {Array<VariantSchema>}
|
||||||
* @memberof FeatureSchema
|
* @memberof FeatureSchema
|
||||||
*/
|
*/
|
||||||
variants?: Array<VariantSchema>;
|
variants?: Array<VariantSchema>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<TagSchema>}
|
||||||
|
* @memberof FeatureSchema
|
||||||
|
*/
|
||||||
|
tags?: Array<TagSchema> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeatureSchemaFromJSON(json: any): FeatureSchema {
|
export function FeatureSchemaFromJSON(json: any): FeatureSchema {
|
||||||
return FeatureSchemaFromJSONTyped(json, false);
|
return FeatureSchemaFromJSONTyped(json, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeatureSchemaFromJSONTyped(json: any, ignoreDiscriminator: boolean): FeatureSchema {
|
export function FeatureSchemaFromJSONTyped(
|
||||||
if ((json === undefined) || (json === null)) {
|
json: any,
|
||||||
|
ignoreDiscriminator: boolean
|
||||||
|
): FeatureSchema {
|
||||||
|
if (json === undefined || json === null) {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
name: json['name'],
|
||||||
'name': json['name'],
|
type: !exists(json, 'type') ? undefined : json['type'],
|
||||||
'type': !exists(json, 'type') ? undefined : json['type'],
|
description: !exists(json, 'description')
|
||||||
'description': !exists(json, 'description') ? undefined : json['description'],
|
? undefined
|
||||||
'archived': !exists(json, 'archived') ? undefined : json['archived'],
|
: json['description'],
|
||||||
'project': !exists(json, 'project') ? undefined : json['project'],
|
archived: !exists(json, 'archived') ? undefined : json['archived'],
|
||||||
'enabled': !exists(json, 'enabled') ? undefined : json['enabled'],
|
project: !exists(json, 'project') ? undefined : json['project'],
|
||||||
'stale': !exists(json, 'stale') ? undefined : json['stale'],
|
enabled: !exists(json, 'enabled') ? undefined : json['enabled'],
|
||||||
'impressionData': !exists(json, 'impressionData') ? undefined : json['impressionData'],
|
stale: !exists(json, 'stale') ? undefined : json['stale'],
|
||||||
'createdAt': !exists(json, 'createdAt') ? undefined : (json['createdAt'] === null ? null : new Date(json['createdAt'])),
|
impressionData: !exists(json, 'impressionData')
|
||||||
'archivedAt': !exists(json, 'archivedAt') ? undefined : (json['archivedAt'] === null ? null : new Date(json['archivedAt'])),
|
? undefined
|
||||||
'lastSeenAt': !exists(json, 'lastSeenAt') ? undefined : (json['lastSeenAt'] === null ? null : new Date(json['lastSeenAt'])),
|
: json['impressionData'],
|
||||||
'environments': !exists(json, 'environments') ? undefined : ((json['environments'] as Array<any>).map(FeatureEnvironmentSchemaFromJSON)),
|
createdAt: !exists(json, 'createdAt')
|
||||||
'strategies': !exists(json, 'strategies') ? undefined : ((json['strategies'] as Array<any>).map(StrategySchemaFromJSON)),
|
? undefined
|
||||||
'variants': !exists(json, 'variants') ? undefined : ((json['variants'] as Array<any>).map(VariantSchemaFromJSON)),
|
: json['createdAt'] === null
|
||||||
|
? null
|
||||||
|
: new Date(json['createdAt']),
|
||||||
|
archivedAt: !exists(json, 'archivedAt')
|
||||||
|
? undefined
|
||||||
|
: json['archivedAt'] === null
|
||||||
|
? null
|
||||||
|
: new Date(json['archivedAt']),
|
||||||
|
lastSeenAt: !exists(json, 'lastSeenAt')
|
||||||
|
? undefined
|
||||||
|
: json['lastSeenAt'] === null
|
||||||
|
? null
|
||||||
|
: new Date(json['lastSeenAt']),
|
||||||
|
environments: !exists(json, 'environments')
|
||||||
|
? undefined
|
||||||
|
: (json['environments'] as Array<any>).map(
|
||||||
|
FeatureEnvironmentSchemaFromJSON
|
||||||
|
),
|
||||||
|
strategies: !exists(json, 'strategies')
|
||||||
|
? undefined
|
||||||
|
: (json['strategies'] as Array<any>).map(StrategySchemaFromJSON),
|
||||||
|
variants: !exists(json, 'variants')
|
||||||
|
? undefined
|
||||||
|
: (json['variants'] as Array<any>).map(VariantSchemaFromJSON),
|
||||||
|
tags: !exists(json, 'tags')
|
||||||
|
? undefined
|
||||||
|
: json['tags'] === null
|
||||||
|
? null
|
||||||
|
: (json['tags'] as Array<any>).map(TagSchemaFromJSON),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,21 +202,51 @@ export function FeatureSchemaToJSON(value?: FeatureSchema | null): any {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
name: value.name,
|
||||||
'name': value.name,
|
type: value.type,
|
||||||
'type': value.type,
|
description: value.description,
|
||||||
'description': value.description,
|
archived: value.archived,
|
||||||
'archived': value.archived,
|
project: value.project,
|
||||||
'project': value.project,
|
enabled: value.enabled,
|
||||||
'enabled': value.enabled,
|
stale: value.stale,
|
||||||
'stale': value.stale,
|
impressionData: value.impressionData,
|
||||||
'impressionData': value.impressionData,
|
createdAt:
|
||||||
'createdAt': value.createdAt === undefined ? undefined : (value.createdAt === null ? null : value.createdAt.toISOString().substr(0,10)),
|
value.createdAt === undefined
|
||||||
'archivedAt': value.archivedAt === undefined ? undefined : (value.archivedAt === null ? null : value.archivedAt.toISOString().substr(0,10)),
|
? undefined
|
||||||
'lastSeenAt': value.lastSeenAt === undefined ? undefined : (value.lastSeenAt === null ? null : value.lastSeenAt.toISOString().substr(0,10)),
|
: value.createdAt === null
|
||||||
'environments': value.environments === undefined ? undefined : ((value.environments as Array<any>).map(FeatureEnvironmentSchemaToJSON)),
|
? null
|
||||||
'strategies': value.strategies === undefined ? undefined : ((value.strategies as Array<any>).map(StrategySchemaToJSON)),
|
: value.createdAt.toISOString().substr(0, 10),
|
||||||
'variants': value.variants === undefined ? undefined : ((value.variants as Array<any>).map(VariantSchemaToJSON)),
|
archivedAt:
|
||||||
|
value.archivedAt === undefined
|
||||||
|
? undefined
|
||||||
|
: value.archivedAt === null
|
||||||
|
? null
|
||||||
|
: value.archivedAt.toISOString().substr(0, 10),
|
||||||
|
lastSeenAt:
|
||||||
|
value.lastSeenAt === undefined
|
||||||
|
? undefined
|
||||||
|
: value.lastSeenAt === null
|
||||||
|
? null
|
||||||
|
: value.lastSeenAt.toISOString().substr(0, 10),
|
||||||
|
environments:
|
||||||
|
value.environments === undefined
|
||||||
|
? undefined
|
||||||
|
: (value.environments as Array<any>).map(
|
||||||
|
FeatureEnvironmentSchemaToJSON
|
||||||
|
),
|
||||||
|
strategies:
|
||||||
|
value.strategies === undefined
|
||||||
|
? undefined
|
||||||
|
: (value.strategies as Array<any>).map(StrategySchemaToJSON),
|
||||||
|
variants:
|
||||||
|
value.variants === undefined
|
||||||
|
? undefined
|
||||||
|
: (value.variants as Array<any>).map(VariantSchemaToJSON),
|
||||||
|
tags:
|
||||||
|
value.tags === undefined
|
||||||
|
? undefined
|
||||||
|
: value.tags === null
|
||||||
|
? null
|
||||||
|
: (value.tags as Array<any>).map(TagSchemaToJSON),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +75,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxyFrontend": false,
|
"embedProxyFrontend": false,
|
||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
"syncSSOGroups": false,
|
"syncSSOGroups": false,
|
||||||
|
"toggleTagFiltering": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"flagResolver": FlagResolver {
|
"flagResolver": FlagResolver {
|
||||||
@ -88,6 +89,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxyFrontend": false,
|
"embedProxyFrontend": false,
|
||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
"syncSSOGroups": false,
|
"syncSSOGroups": false,
|
||||||
|
"toggleTagFiltering": false,
|
||||||
},
|
},
|
||||||
"externalResolver": {
|
"externalResolver": {
|
||||||
"isEnabled": [Function],
|
"isEnabled": [Function],
|
||||||
|
@ -12,12 +12,14 @@ import {
|
|||||||
IFeatureOverview,
|
IFeatureOverview,
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
|
ITag,
|
||||||
} from '../types/model';
|
} from '../types/model';
|
||||||
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
||||||
import { PartialSome } from '../types/partial';
|
import { PartialSome } from '../types/partial';
|
||||||
import FeatureToggleStore from './feature-toggle-store';
|
import FeatureToggleStore from './feature-toggle-store';
|
||||||
import { ensureStringValue } from '../util/ensureStringValue';
|
import { ensureStringValue } from '../util/ensureStringValue';
|
||||||
import { mapValues } from '../util/map-values';
|
import { mapValues } from '../util/map-values';
|
||||||
|
import { IFlagResolver } from '../types/experimental';
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
'id',
|
'id',
|
||||||
@ -111,7 +113,14 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
|
|
||||||
private readonly timer: Function;
|
private readonly timer: Function;
|
||||||
|
|
||||||
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
db: Knex,
|
||||||
|
eventBus: EventEmitter,
|
||||||
|
getLogger: LogProvider,
|
||||||
|
flagResolver: IFlagResolver,
|
||||||
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('feature-toggle-store.ts');
|
this.logger = getLogger('feature-toggle-store.ts');
|
||||||
this.timer = (action) =>
|
this.timer = (action) =>
|
||||||
@ -119,6 +128,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
store: 'feature-toggle-strategies',
|
store: 'feature-toggle-strategies',
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(key: string): Promise<void> {
|
async delete(key: string): Promise<void> {
|
||||||
@ -317,23 +327,63 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addTag(
|
||||||
|
feature: Record<string, any>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
): void {
|
||||||
|
const tags = feature.tags || [];
|
||||||
|
const newTag = FeatureStrategiesStore.rowToTag(row);
|
||||||
|
feature.tags = [...tags, newTag];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNewTag(
|
||||||
|
feature: Record<string, any>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
row.tag_type &&
|
||||||
|
row.tag_value &&
|
||||||
|
!feature.tags?.some(
|
||||||
|
(tag) =>
|
||||||
|
tag.type === row.tag_type && tag.value === row.tag_value,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static rowToTag(r: any): ITag {
|
||||||
|
return {
|
||||||
|
value: r.tag_value,
|
||||||
|
type: r.tag_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getFeatureOverview(
|
async getFeatureOverview(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
): Promise<IFeatureOverview[]> {
|
): Promise<IFeatureOverview[]> {
|
||||||
const rows = await this.db('features')
|
let selectColumns = [
|
||||||
|
'features.name as feature_name',
|
||||||
|
'features.type as type',
|
||||||
|
'features.created_at as created_at',
|
||||||
|
'features.last_seen_at as last_seen_at',
|
||||||
|
'features.stale as stale',
|
||||||
|
'feature_environments.enabled as enabled',
|
||||||
|
'feature_environments.environment as environment',
|
||||||
|
'environments.type as environment_type',
|
||||||
|
'environments.sort_order as environment_sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.flagResolver.isEnabled('toggleTagFiltering')) {
|
||||||
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
'ft.tag_value as tag_value',
|
||||||
|
'ft.tag_type as tag_type',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = this.db('features')
|
||||||
.where({ project: projectId })
|
.where({ project: projectId })
|
||||||
.select(
|
.select(selectColumns)
|
||||||
'features.name as feature_name',
|
|
||||||
'features.type as type',
|
|
||||||
'features.created_at as created_at',
|
|
||||||
'features.last_seen_at as last_seen_at',
|
|
||||||
'features.stale as stale',
|
|
||||||
'feature_environments.enabled as enabled',
|
|
||||||
'feature_environments.environment as environment',
|
|
||||||
'environments.type as environment_type',
|
|
||||||
'environments.sort_order as environment_sort_order',
|
|
||||||
)
|
|
||||||
.modify(FeatureToggleStore.filterByArchived, archived)
|
.modify(FeatureToggleStore.filterByArchived, archived)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
'feature_environments',
|
'feature_environments',
|
||||||
@ -345,12 +395,26 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'feature_environments.environment',
|
'feature_environments.environment',
|
||||||
'environments.name',
|
'environments.name',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.flagResolver.isEnabled('toggleTagFiltering')) {
|
||||||
|
query = query.leftJoin(
|
||||||
|
'feature_tag as ft',
|
||||||
|
'ft.feature_name',
|
||||||
|
'features.name',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await query;
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const overview = rows.reduce((acc, r) => {
|
const overview = rows.reduce((acc, r) => {
|
||||||
if (acc[r.feature_name] !== undefined) {
|
if (acc[r.feature_name] !== undefined) {
|
||||||
acc[r.feature_name].environments.push(
|
acc[r.feature_name].environments.push(
|
||||||
FeatureStrategiesStore.getEnvironment(r),
|
FeatureStrategiesStore.getEnvironment(r),
|
||||||
);
|
);
|
||||||
|
if (this.isNewTag(acc[r.feature_name], r)) {
|
||||||
|
this.addTag(acc[r.feature_name], r);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
acc[r.feature_name] = {
|
acc[r.feature_name] = {
|
||||||
type: r.type,
|
type: r.type,
|
||||||
@ -362,9 +426,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
FeatureStrategiesStore.getEnvironment(r),
|
FeatureStrategiesStore.getEnvironment(r),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
if (this.isNewTag(acc[r.feature_name], r)) {
|
||||||
|
this.addTag(acc[r.feature_name], r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return Object.values(overview).map((o: IFeatureOverview) => ({
|
return Object.values(overview).map((o: IFeatureOverview) => ({
|
||||||
...o,
|
...o,
|
||||||
environments: o.environments
|
environments: o.environments
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
IFeatureToggleQuery,
|
IFeatureToggleQuery,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
|
ITag,
|
||||||
} from '../types/model';
|
} from '../types/model';
|
||||||
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
||||||
import { DEFAULT_ENV } from '../util/constants';
|
import { DEFAULT_ENV } from '../util/constants';
|
||||||
@ -14,6 +15,7 @@ import EventEmitter from 'events';
|
|||||||
import FeatureToggleStore from './feature-toggle-store';
|
import FeatureToggleStore from './feature-toggle-store';
|
||||||
import { ensureStringValue } from '../util/ensureStringValue';
|
import { ensureStringValue } from '../util/ensureStringValue';
|
||||||
import { mapValues } from '../util/map-values';
|
import { mapValues } from '../util/map-values';
|
||||||
|
import { IFlagResolver } from '../types/experimental';
|
||||||
|
|
||||||
export interface FeaturesTable {
|
export interface FeaturesTable {
|
||||||
name: string;
|
name: string;
|
||||||
@ -37,11 +39,14 @@ export default class FeatureToggleClientStore
|
|||||||
|
|
||||||
private timer: Function;
|
private timer: Function;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
db: Knex,
|
db: Knex,
|
||||||
eventBus: EventEmitter,
|
eventBus: EventEmitter,
|
||||||
getLogger: LogProvider,
|
getLogger: LogProvider,
|
||||||
inlineSegmentConstraints: boolean,
|
inlineSegmentConstraints: boolean,
|
||||||
|
flagResolver: IFlagResolver,
|
||||||
) {
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('feature-toggle-client-store.ts');
|
this.logger = getLogger('feature-toggle-client-store.ts');
|
||||||
@ -51,6 +56,7 @@ export default class FeatureToggleClientStore
|
|||||||
store: 'feature-toggle',
|
store: 'feature-toggle',
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAll(
|
private async getAll(
|
||||||
@ -82,6 +88,14 @@ export default class FeatureToggleClientStore
|
|||||||
'segments.constraints as segment_constraints',
|
'segments.constraints as segment_constraints',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isAdmin && this.flagResolver.isEnabled('toggleTagFiltering')) {
|
||||||
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
'ft.tag_value as tag_value',
|
||||||
|
'ft.tag_type as tag_type',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
let query = this.db('features')
|
let query = this.db('features')
|
||||||
.select(selectColumns)
|
.select(selectColumns)
|
||||||
.modify(FeatureToggleStore.filterByArchived, archived)
|
.modify(FeatureToggleStore.filterByArchived, archived)
|
||||||
@ -108,6 +122,14 @@ export default class FeatureToggleClientStore
|
|||||||
)
|
)
|
||||||
.leftJoin('segments', `segments.id`, `fss.segment_id`);
|
.leftJoin('segments', `segments.id`, `fss.segment_id`);
|
||||||
|
|
||||||
|
if (isAdmin && this.flagResolver.isEnabled('toggleTagFiltering')) {
|
||||||
|
query = query.leftJoin(
|
||||||
|
'feature_tag as ft',
|
||||||
|
'ft.feature_name',
|
||||||
|
'features.name',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (featureQuery) {
|
if (featureQuery) {
|
||||||
if (featureQuery.tag) {
|
if (featureQuery.tag) {
|
||||||
const tagQuery = this.db
|
const tagQuery = this.db
|
||||||
@ -140,6 +162,9 @@ export default class FeatureToggleClientStore
|
|||||||
FeatureToggleClientStore.rowToStrategy(r),
|
FeatureToggleClientStore.rowToStrategy(r),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.isNewTag(feature, r)) {
|
||||||
|
this.addTag(feature, r);
|
||||||
|
}
|
||||||
if (featureQuery?.inlineSegmentConstraints && r.segment_id) {
|
if (featureQuery?.inlineSegmentConstraints && r.segment_id) {
|
||||||
this.addSegmentToStrategy(feature, r);
|
this.addSegmentToStrategy(feature, r);
|
||||||
} else if (
|
} else if (
|
||||||
@ -185,6 +210,13 @@ export default class FeatureToggleClientStore
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static rowToTag(row: Record<string, any>): ITag {
|
||||||
|
return {
|
||||||
|
value: row.tag_value,
|
||||||
|
type: row.tag_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static removeIdsFromStrategies(features: IFeatureToggleClient[]) {
|
private static removeIdsFromStrategies(features: IFeatureToggleClient[]) {
|
||||||
features.forEach((feature) => {
|
features.forEach((feature) => {
|
||||||
feature.strategies.forEach((strategy) => {
|
feature.strategies.forEach((strategy) => {
|
||||||
@ -203,6 +235,29 @@ export default class FeatureToggleClientStore
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addTag(
|
||||||
|
feature: Record<string, any>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
): void {
|
||||||
|
const tags = feature.tags || [];
|
||||||
|
const newTag = FeatureToggleClientStore.rowToTag(row);
|
||||||
|
feature.tags = [...tags, newTag];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNewTag(
|
||||||
|
feature: PartialDeep<IFeatureToggleClient>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
row.tag_type &&
|
||||||
|
row.tag_value &&
|
||||||
|
!feature.tags?.some(
|
||||||
|
(tag) =>
|
||||||
|
tag.type === row.tag_type && tag.value === row.tag_value,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private addSegmentToStrategy(
|
private addSegmentToStrategy(
|
||||||
feature: PartialDeep<IFeatureToggleClient>,
|
feature: PartialDeep<IFeatureToggleClient>,
|
||||||
row: Record<string, any>,
|
row: Record<string, any>,
|
||||||
|
@ -68,12 +68,14 @@ export const createStores = (
|
|||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
|
config.flagResolver,
|
||||||
),
|
),
|
||||||
featureToggleClientStore: new FeatureToggleClientStore(
|
featureToggleClientStore: new FeatureToggleClientStore(
|
||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
config.inlineSegmentConstraints,
|
config.inlineSegmentConstraints,
|
||||||
|
config.flagResolver,
|
||||||
),
|
),
|
||||||
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
|
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
|
||||||
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
||||||
|
@ -5,6 +5,7 @@ import { overrideSchema } from './override-schema';
|
|||||||
import { parametersSchema } from './parameters-schema';
|
import { parametersSchema } from './parameters-schema';
|
||||||
import { environmentSchema } from './environment-schema';
|
import { environmentSchema } from './environment-schema';
|
||||||
import { featureStrategySchema } from './feature-strategy-schema';
|
import { featureStrategySchema } from './feature-strategy-schema';
|
||||||
|
import { tagSchema } from './tag-schema';
|
||||||
|
|
||||||
export const featureSchema = {
|
export const featureSchema = {
|
||||||
$id: '#/components/schemas/featureSchema',
|
$id: '#/components/schemas/featureSchema',
|
||||||
@ -69,6 +70,13 @@ export const featureSchema = {
|
|||||||
$ref: '#/components/schemas/variantSchema',
|
$ref: '#/components/schemas/variantSchema',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/tagSchema',
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
@ -78,6 +86,7 @@ export const featureSchema = {
|
|||||||
parametersSchema,
|
parametersSchema,
|
||||||
featureStrategySchema,
|
featureStrategySchema,
|
||||||
variantSchema,
|
variantSchema,
|
||||||
|
tagSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -34,6 +34,10 @@ export const defaultExperimentalOptions = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_CLONE_ENVIRONMENT,
|
process.env.UNLEASH_EXPERIMENTAL_CLONE_ENVIRONMENT,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
toggleTagFiltering: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_TOGGLE_TAG_FILTERING,
|
||||||
|
false,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
externalResolver: { isEnabled: (): boolean => false },
|
externalResolver: { isEnabled: (): boolean => false },
|
||||||
};
|
};
|
||||||
|
@ -68,6 +68,7 @@ export interface IFeatureToggleClient {
|
|||||||
impressionData?: boolean;
|
impressionData?: boolean;
|
||||||
lastSeenAt?: Date;
|
lastSeenAt?: Date;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
|
tags?: ITag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureEnvironmentInfo {
|
export interface IFeatureEnvironmentInfo {
|
||||||
|
@ -41,6 +41,7 @@ process.nextTick(async () => {
|
|||||||
syncSSOGroups: true,
|
syncSSOGroups: true,
|
||||||
changeRequests: true,
|
changeRequests: true,
|
||||||
cloneEnvironment: true,
|
cloneEnvironment: true,
|
||||||
|
toggleTagFiltering: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
@ -1212,6 +1212,13 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"tags": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/tagSchema",
|
||||||
|
},
|
||||||
|
"nullable": true,
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
@ -193,6 +193,7 @@ export default class FakeFeatureStrategiesStore
|
|||||||
type: t.type || 'Release',
|
type: t.type || 'Release',
|
||||||
stale: t.stale || false,
|
stale: t.stale || false,
|
||||||
variants: [],
|
variants: [],
|
||||||
|
tags: [],
|
||||||
}));
|
}));
|
||||||
return Promise.resolve(clientRows);
|
return Promise.resolve(clientRows);
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ export default class FakeFeatureToggleClientStore
|
|||||||
type: t.type || 'Release',
|
type: t.type || 'Release',
|
||||||
stale: t.stale || false,
|
stale: t.stale || false,
|
||||||
variants: [],
|
variants: [],
|
||||||
|
tags: [],
|
||||||
}));
|
}));
|
||||||
return Promise.resolve(clientRows);
|
return Promise.resolve(clientRows);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user