mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
refactor: remove variants per environment feature flag (#3102)
https://linear.app/unleash/issue/2-428/clean-up-feature-flag-once-were-done-with-the-migration Cleans up the variants per environment feature flag due to GA.
This commit is contained in:
parent
0a32647d75
commit
8729f082d2
@ -232,36 +232,29 @@ describe('feature', () => {
|
|||||||
cy.wait('@addStrategyToFeature');
|
cy.wait('@addStrategyToFeature');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add two variant to the feature', () => {
|
it('can add two variants to the development environment', () => {
|
||||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
`/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`,
|
||||||
req => {
|
req => {
|
||||||
if (req.body.length === 1) {
|
expect(req.body[0].op).to.equal('add');
|
||||||
expect(req.body[0].op).to.equal('add');
|
expect(req.body[0].path).to.equal('/0');
|
||||||
expect(req.body[0].path).to.match(/\//);
|
expect(req.body[0].value.name).to.equal(variant1);
|
||||||
expect(req.body[0].value.name).to.equal(variant1);
|
expect(req.body[0].value.weight).to.equal(500);
|
||||||
} else if (req.body.length === 2) {
|
expect(req.body[1].op).to.equal('add');
|
||||||
expect(req.body[0].op).to.equal('replace');
|
expect(req.body[1].path).to.equal('/1');
|
||||||
expect(req.body[0].path).to.match(/weight/);
|
expect(req.body[1].value.name).to.equal(variant2);
|
||||||
expect(req.body[0].value).to.equal(500);
|
expect(req.body[1].value.weight).to.equal(500);
|
||||||
expect(req.body[1].op).to.equal('add');
|
|
||||||
expect(req.body[1].path).to.match(/\//);
|
|
||||||
expect(req.body[1].value.name).to.equal(variant2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).as('variantCreation');
|
).as('variantCreation');
|
||||||
|
|
||||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
cy.get('[data-testid=ADD_VARIANT_BUTTON]').first().click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant1);
|
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant1);
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=MODAL_ADD_VARIANT_BUTTON]').click();
|
||||||
cy.wait('@variantCreation');
|
cy.get('[data-testid=VARIANT_NAME_INPUT]').last().type(variant2);
|
||||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant2);
|
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.wait('@variantCreation');
|
cy.wait('@variantCreation');
|
||||||
});
|
});
|
||||||
@ -269,60 +262,61 @@ describe('feature', () => {
|
|||||||
it('can set weight to fixed value for one of the variants', () => {
|
it('can set weight to fixed value for one of the variants', () => {
|
||||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
|
|
||||||
cy.get(`[data-testid=VARIANT_EDIT_BUTTON_${variant1}]`).click();
|
cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]')
|
cy.get('[data-testid=VARIANT_NAME_INPUT]')
|
||||||
|
.last()
|
||||||
.children()
|
.children()
|
||||||
.find('input')
|
.find('input')
|
||||||
.should('have.attr', 'disabled');
|
.should('have.attr', 'disabled');
|
||||||
cy.get('[data-testid=VARIANT_WEIGHT_CHECK]').find('input').check();
|
cy.get('[data-testid=VARIANT_WEIGHT_CHECK]')
|
||||||
cy.get('[data-testid=VARIANT_WEIGHT_INPUT]').clear().type('15');
|
.last()
|
||||||
|
.find('input')
|
||||||
|
.check();
|
||||||
|
cy.get('[data-testid=VARIANT_WEIGHT_INPUT]').last().clear().type('15');
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
`/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`,
|
||||||
req => {
|
req => {
|
||||||
expect(req.body[0].op).to.equal('replace');
|
expect(req.body[0].op).to.equal('replace');
|
||||||
expect(req.body[0].path).to.match(/weight/);
|
expect(req.body[0].path).to.equal('/1/weightType');
|
||||||
expect(req.body[0].value).to.equal(850);
|
expect(req.body[0].value).to.equal('fix');
|
||||||
expect(req.body[1].op).to.equal('replace');
|
expect(req.body[1].op).to.equal('replace');
|
||||||
expect(req.body[1].path).to.match(/weightType/);
|
expect(req.body[1].path).to.equal('/1/weight');
|
||||||
expect(req.body[1].value).to.equal('fix');
|
expect(req.body[1].value).to.equal(150);
|
||||||
expect(req.body[2].op).to.equal('replace');
|
expect(req.body[2].op).to.equal('replace');
|
||||||
expect(req.body[2].path).to.match(/weight/);
|
expect(req.body[2].path).to.equal('/0/weight');
|
||||||
expect(req.body[2].value).to.equal(150);
|
expect(req.body[2].value).to.equal(850);
|
||||||
}
|
}
|
||||||
).as('variantUpdate');
|
).as('variantUpdate');
|
||||||
|
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.wait('@variantUpdate');
|
cy.wait('@variantUpdate');
|
||||||
cy.get(`[data-testid=VARIANT_WEIGHT_${variant1}]`).should(
|
cy.get(`[data-testid=VARIANT_WEIGHT_${variant2}]`).should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'15 %'
|
'15 %'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can delete variant', () => {
|
it('can delete variant', () => {
|
||||||
const variantName = 'to-be-deleted';
|
|
||||||
|
|
||||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variantName);
|
cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variant2}]`).click();
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
`/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`,
|
||||||
req => {
|
req => {
|
||||||
const patch = req.body.find(
|
expect(req.body[0].op).to.equal('remove');
|
||||||
(patch: Record<string, string>) => patch.op === 'remove'
|
expect(req.body[0].path).to.equal('/1');
|
||||||
);
|
expect(req.body[1].op).to.equal('replace');
|
||||||
expect(patch.path).to.match(/\//);
|
expect(req.body[1].path).to.equal('/0/weight');
|
||||||
|
expect(req.body[1].value).to.equal(1000);
|
||||||
}
|
}
|
||||||
).as('delete');
|
).as('delete');
|
||||||
|
|
||||||
cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variantName}]`).click();
|
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.wait('@delete');
|
cy.wait('@delete');
|
||||||
});
|
});
|
||||||
|
@ -119,7 +119,11 @@ describe('imports', () => {
|
|||||||
// cy.contains('Import completed');
|
// cy.contains('Import completed');
|
||||||
|
|
||||||
cy.visit(`/projects/default/features/${randomFeatureName}`);
|
cy.visit(`/projects/default/features/${randomFeatureName}`);
|
||||||
cy.contains('enabled in development');
|
cy.get(
|
||||||
|
"[data-testid='feature-toggle-status'] input[type='checkbox']:checked"
|
||||||
|
)
|
||||||
|
.closest('div')
|
||||||
|
.contains('development');
|
||||||
cy.contains('50%');
|
cy.contains('50%');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
|
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
|
||||||
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||||
import FeatureOverviewEnvSwitches from './FeatureOverviewEnvSwitches/FeatureOverviewEnvSwitches';
|
|
||||||
import { Routes, Route, useNavigate } from 'react-router-dom';
|
import { Routes, Route, useNavigate } from 'react-router-dom';
|
||||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
@ -10,8 +9,6 @@ import {
|
|||||||
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
||||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
@ -34,7 +31,6 @@ const StyledMainContent = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const FeatureOverview = () => {
|
const FeatureOverview = () => {
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
@ -48,20 +44,9 @@ const FeatureOverview = () => {
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<div>
|
<div>
|
||||||
<FeatureOverviewMetaData />
|
<FeatureOverviewMetaData />
|
||||||
<ConditionallyRender
|
<FeatureOverviewSidePanel
|
||||||
condition={Boolean(uiConfig.flags.variantsPerEnvironment)}
|
hiddenEnvironments={hiddenEnvironments}
|
||||||
show={
|
setHiddenEnvironments={setHiddenEnvironments}
|
||||||
<FeatureOverviewSidePanel
|
|
||||||
hiddenEnvironments={hiddenEnvironments}
|
|
||||||
setHiddenEnvironments={setHiddenEnvironments}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<FeatureOverviewEnvSwitches
|
|
||||||
hiddenEnvironments={hiddenEnvironments}
|
|
||||||
setHiddenEnvironments={setHiddenEnvironments}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<StyledMainContent>
|
<StyledMainContent>
|
||||||
|
@ -145,17 +145,6 @@ const FeatureOverviewMetaData = () => {
|
|||||||
/>
|
/>
|
||||||
</StyledBody>
|
</StyledBody>
|
||||||
</StyledPaddingContainerTop>
|
</StyledPaddingContainerTop>
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
tags.length > 0 &&
|
|
||||||
!Boolean(uiConfig.flags.variantsPerEnvironment)
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<StyledPaddingContainerBottom>
|
|
||||||
<FeatureOverviewTags projectId={projectId} />
|
|
||||||
</StyledPaddingContainerBottom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -59,7 +59,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
|||||||
environment => environment.enabled && environment.variants?.length
|
environment => environment.enabled && environment.variants?.length
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer data-testid="feature-toggle-status">
|
||||||
{header}
|
{header}
|
||||||
{feature.environments.map(environment => {
|
{feature.environments.map(environment => {
|
||||||
const strategiesLabel =
|
const strategiesLabel =
|
||||||
|
@ -284,6 +284,7 @@ export const EnvironmentVariantsModal = ({
|
|||||||
</StyledName>
|
</StyledName>
|
||||||
</div>
|
</div>
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
|
data-testid="MODAL_ADD_VARIANT_BUTTON"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setVariantsEdit(variantsEdit => [
|
setVariantsEdit(variantsEdit => [
|
||||||
...variantsEdit,
|
...variantsEdit,
|
||||||
@ -400,6 +401,7 @@ export const EnvironmentVariantsModal = ({
|
|||||||
|
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="DIALOGUE_CONFIRM_ID"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -304,6 +304,7 @@ export const VariantForm = ({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
data-testid={`VARIANT_DELETE_BUTTON_${variant.name}`}
|
||||||
onClick={() => removeVariant(variant.id)}
|
onClick={() => removeVariant(variant.id)}
|
||||||
disabled={isProtectedVariant(variant)}
|
disabled={isProtectedVariant(variant)}
|
||||||
>
|
>
|
||||||
@ -318,6 +319,7 @@ export const VariantForm = ({
|
|||||||
This will be used to identify the variant in your code
|
This will be used to identify the variant in your code
|
||||||
</StyledSubLabel>
|
</StyledSubLabel>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
|
data-testid="VARIANT_NAME_INPUT"
|
||||||
autoFocus
|
autoFocus
|
||||||
label="Variant name"
|
label="Variant name"
|
||||||
error={Boolean(errors.name)}
|
error={Boolean(errors.name)}
|
||||||
@ -336,6 +338,7 @@ export const VariantForm = ({
|
|||||||
label="Custom percentage"
|
label="Custom percentage"
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="VARIANT_WEIGHT_CHECK"
|
||||||
checked={customPercentage}
|
checked={customPercentage}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
setCustomPercentage(
|
setCustomPercentage(
|
||||||
@ -346,6 +349,7 @@ export const VariantForm = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StyledWeightInput
|
<StyledWeightInput
|
||||||
|
data-testid="VARIANT_WEIGHT_INPUT"
|
||||||
type="number"
|
type="number"
|
||||||
label="Variant weight"
|
label="Variant weight"
|
||||||
error={Boolean(errors.percentage)}
|
error={Boolean(errors.percentage)}
|
||||||
|
@ -318,6 +318,7 @@ export const FeatureEnvironmentVariants = () => {
|
|||||||
)}
|
)}
|
||||||
show={
|
show={
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
|
data-testid="EDIT_VARIANTS_BUTTON"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
editVariants(environment)
|
editVariants(environment)
|
||||||
}
|
}
|
||||||
@ -332,6 +333,7 @@ export const FeatureEnvironmentVariants = () => {
|
|||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
|
data-testid="ADD_VARIANT_BUTTON"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
editVariants(environment)
|
editVariants(environment)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,10 @@
|
|||||||
import { FeatureVariantsList } from './FeatureVariantsList/FeatureVariantsList';
|
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { FeatureEnvironmentVariants } from './FeatureEnvironmentVariants/FeatureEnvironmentVariants';
|
import { FeatureEnvironmentVariants } from './FeatureEnvironmentVariants/FeatureEnvironmentVariants';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
|
|
||||||
const FeatureVariants = () => {
|
const FeatureVariants = () => {
|
||||||
usePageTitle('Variants');
|
usePageTitle('Variants');
|
||||||
|
|
||||||
const { uiConfig } = useUiConfig();
|
return <FeatureEnvironmentVariants />;
|
||||||
|
|
||||||
if (uiConfig.flags.variantsPerEnvironment) {
|
|
||||||
return <FeatureEnvironmentVariants />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <FeatureVariantsList />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureVariants;
|
export default FeatureVariants;
|
||||||
|
@ -4,7 +4,6 @@ import { emptyFeature } from './emptyFeature';
|
|||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import { IFeatureToggle } from 'interfaces/featureToggle';
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
|
|
||||||
export interface IUseFeatureOutput {
|
export interface IUseFeatureOutput {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
@ -26,14 +25,9 @@ export const useFeature = (
|
|||||||
): IUseFeatureOutput => {
|
): IUseFeatureOutput => {
|
||||||
const path = formatFeatureApiPath(projectId, featureId);
|
const path = formatFeatureApiPath(projectId, featureId);
|
||||||
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const {
|
|
||||||
flags: { variantsPerEnvironment },
|
|
||||||
} = uiConfig;
|
|
||||||
|
|
||||||
const { data, error, mutate } = useSWR<IFeatureResponse>(
|
const { data, error, mutate } = useSWR<IFeatureResponse>(
|
||||||
['useFeature', path, variantsPerEnvironment],
|
['useFeature', path],
|
||||||
() => featureFetcher(path, variantsPerEnvironment),
|
() => featureFetcher(path),
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -51,14 +45,9 @@ export const useFeature = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const featureFetcher = async (
|
export const featureFetcher = async (
|
||||||
path: string,
|
path: string
|
||||||
variantsPerEnvironment?: boolean
|
|
||||||
): Promise<IFeatureResponse> => {
|
): Promise<IFeatureResponse> => {
|
||||||
const variantEnvironments = variantsPerEnvironment
|
const res = await fetch(path);
|
||||||
? '?variantEnvironments=true'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const res = await fetch(path + variantEnvironments);
|
|
||||||
|
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
return { status: 404 };
|
return { status: 404 };
|
||||||
@ -79,6 +68,6 @@ export const formatFeatureApiPath = (
|
|||||||
featureId: string
|
featureId: string
|
||||||
): string => {
|
): string => {
|
||||||
return formatApiPath(
|
return formatApiPath(
|
||||||
`api/admin/projects/${projectId}/features/${featureId}`
|
`api/admin/projects/${projectId}/features/${featureId}?variantEnvironments=true`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -38,7 +38,6 @@ export interface IFlags {
|
|||||||
UG?: boolean;
|
UG?: boolean;
|
||||||
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
||||||
embedProxyFrontend?: boolean;
|
embedProxyFrontend?: boolean;
|
||||||
variantsPerEnvironment?: boolean;
|
|
||||||
networkView?: boolean;
|
networkView?: boolean;
|
||||||
maintenance?: boolean;
|
maintenance?: boolean;
|
||||||
maintenanceMode?: boolean;
|
maintenanceMode?: boolean;
|
||||||
|
@ -84,7 +84,6 @@ exports[`should create default config 1`] = `
|
|||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
"showProjectApiAccess": false,
|
"showProjectApiAccess": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
"variantsPerEnvironment": false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"flagResolver": FlagResolver {
|
"flagResolver": FlagResolver {
|
||||||
@ -106,7 +105,6 @@ exports[`should create default config 1`] = `
|
|||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
"showProjectApiAccess": false,
|
"showProjectApiAccess": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
"variantsPerEnvironment": false,
|
|
||||||
},
|
},
|
||||||
"externalResolver": {
|
"externalResolver": {
|
||||||
"isEnabled": [Function],
|
"isEnabled": [Function],
|
||||||
|
@ -97,13 +97,6 @@ export default class EnvironmentService {
|
|||||||
environment,
|
environment,
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.flagResolver.isEnabled('variantsPerEnvironment')) {
|
|
||||||
await this.featureEnvironmentStore.clonePreviousVariants(
|
|
||||||
environment,
|
|
||||||
projectId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === UNIQUE_CONSTRAINT_VIOLATION) {
|
if (e.code === UNIQUE_CONSTRAINT_VIOLATION) {
|
||||||
throw new NameExistsError(
|
throw new NameExistsError(
|
||||||
|
@ -27,7 +27,6 @@ function getSetup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupV3VariantsCompatibilityScenario(
|
async function setupV3VariantsCompatibilityScenario(
|
||||||
variantsPerEnvironment: boolean,
|
|
||||||
envs = [
|
envs = [
|
||||||
{ name: 'env-2', enabled: true },
|
{ name: 'env-2', enabled: true },
|
||||||
{ name: 'env-3', enabled: true },
|
{ name: 'env-3', enabled: true },
|
||||||
@ -55,9 +54,7 @@ async function setupV3VariantsCompatibilityScenario(
|
|||||||
env.name,
|
env.name,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: variantsPerEnvironment
|
name: `${env.name}-variant`,
|
||||||
? `${env.name}-variant`
|
|
||||||
: 'variant-name',
|
|
||||||
stickiness: 'default',
|
stickiness: 'default',
|
||||||
weight: 1000,
|
weight: 1000,
|
||||||
weightType: 'variable',
|
weightType: 'variable',
|
||||||
@ -69,7 +66,7 @@ async function setupV3VariantsCompatibilityScenario(
|
|||||||
stateService: new StateService(stores, {
|
stateService: new StateService(stores, {
|
||||||
getLogger,
|
getLogger,
|
||||||
flagResolver: {
|
flagResolver: {
|
||||||
isEnabled: () => variantsPerEnvironment,
|
isEnabled: () => true,
|
||||||
getAll: () => ({}),
|
getAll: () => ({}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -622,88 +619,8 @@ test('exporting to new format works', async () => {
|
|||||||
expect(exported.featureStrategies).toHaveLength(1);
|
expect(exported.featureStrategies).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('exporting with no environments should fail', async () => {
|
|
||||||
const { stateService, stores } = await setupV3VariantsCompatibilityScenario(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
await stores.environmentStore.deleteAll();
|
|
||||||
|
|
||||||
expect(stateService.export({})).rejects.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
// This test should be removed as soon as variants per environment is GA
|
|
||||||
test('exporting variants to v3 format should pick variants from the correct env', async () => {
|
|
||||||
const { stateService } = await setupV3VariantsCompatibilityScenario(false);
|
|
||||||
|
|
||||||
const exported = await stateService.export({});
|
|
||||||
expect(exported.features).toHaveLength(1);
|
|
||||||
|
|
||||||
expect(exported.features[0].variants).toStrictEqual([
|
|
||||||
{
|
|
||||||
name: 'variant-name',
|
|
||||||
stickiness: 'default',
|
|
||||||
weight: 1000,
|
|
||||||
weightType: 'variable',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
exported.featureEnvironments.forEach((fe) =>
|
|
||||||
expect(fe.variants).toBeUndefined(),
|
|
||||||
);
|
|
||||||
expect(exported.environments).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
// This test should be removed as soon as variants per environment is GA
|
|
||||||
test('exporting variants to v3 format when some envs are disabled should export variants', async () => {
|
|
||||||
const { stateService } = await setupV3VariantsCompatibilityScenario(false, [
|
|
||||||
{ name: 'env-2', enabled: false },
|
|
||||||
{ name: 'env-3', enabled: false },
|
|
||||||
{ name: 'env-1', enabled: true },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const exported = await stateService.export({});
|
|
||||||
expect(exported.features).toHaveLength(1);
|
|
||||||
|
|
||||||
expect(exported.features[0].variants).toStrictEqual([
|
|
||||||
{
|
|
||||||
name: 'variant-name',
|
|
||||||
stickiness: 'default',
|
|
||||||
weight: 1000,
|
|
||||||
weightType: 'variable',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
exported.featureEnvironments.forEach((fe) =>
|
|
||||||
expect(fe.variants).toBeUndefined(),
|
|
||||||
);
|
|
||||||
expect(exported.environments).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
// This test should be removed as soon as variants per environment is GA
|
|
||||||
test('exporting variants to v3 format when all envs are disabled should export variants', async () => {
|
|
||||||
const { stateService } = await setupV3VariantsCompatibilityScenario(false, [
|
|
||||||
{ name: 'env-2', enabled: false },
|
|
||||||
{ name: 'env-3', enabled: false },
|
|
||||||
{ name: 'env-1', enabled: false },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const exported = await stateService.export({});
|
|
||||||
expect(exported.features).toHaveLength(1);
|
|
||||||
|
|
||||||
expect(exported.features[0].variants).toStrictEqual([
|
|
||||||
{
|
|
||||||
name: 'variant-name',
|
|
||||||
stickiness: 'default',
|
|
||||||
weight: 1000,
|
|
||||||
weightType: 'variable',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
exported.featureEnvironments.forEach((fe) =>
|
|
||||||
expect(fe.variants).toBeUndefined(),
|
|
||||||
);
|
|
||||||
expect(exported.environments).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exporting variants to v4 format should not include variants in features', async () => {
|
test('exporting variants to v4 format should not include variants in features', async () => {
|
||||||
const { stateService } = await setupV3VariantsCompatibilityScenario(true);
|
const { stateService } = await setupV3VariantsCompatibilityScenario();
|
||||||
const exported = await stateService.export({});
|
const exported = await stateService.export({});
|
||||||
|
|
||||||
expect(exported.features).toHaveLength(1);
|
expect(exported.features).toHaveLength(1);
|
||||||
|
@ -703,61 +703,7 @@ export default class StateService {
|
|||||||
environments: IEnvironment[];
|
environments: IEnvironment[];
|
||||||
featureEnvironments: IFeatureEnvironment[];
|
featureEnvironments: IFeatureEnvironment[];
|
||||||
}> {
|
}> {
|
||||||
if (this.flagResolver.isEnabled('variantsPerEnvironment')) {
|
return this.exportV4(opts);
|
||||||
return this.exportV4(opts);
|
|
||||||
}
|
|
||||||
// adapt v4 to v3. We need includeEnvironments set to true to filter the
|
|
||||||
// best environment from where we'll pick variants (cause now they are stored
|
|
||||||
// per environment despite being displayed as if they belong to the feature)
|
|
||||||
const v4 = await this.exportV4({ ...opts, includeEnvironments: true });
|
|
||||||
// undefined defaults to true
|
|
||||||
if (opts.includeFeatureToggles !== false) {
|
|
||||||
const enabledEnvironments = v4.environments.filter(
|
|
||||||
(env) => env.enabled !== false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const featureAndEnvs = v4.featureEnvironments.map((fE) => {
|
|
||||||
const envDetails = enabledEnvironments.find(
|
|
||||||
(env) => fE.environment === env.name,
|
|
||||||
);
|
|
||||||
return { ...fE, ...envDetails, active: fE.enabled };
|
|
||||||
});
|
|
||||||
v4.features = v4.features.map((f) => {
|
|
||||||
const variants = featureAndEnvs
|
|
||||||
.sort((e1, e2) => {
|
|
||||||
if (e1.active !== e2.active) {
|
|
||||||
return e1.active ? -1 : 1;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
e1.type !== 'production' ||
|
|
||||||
e2.type !== 'production'
|
|
||||||
) {
|
|
||||||
if (e1.type === 'production') {
|
|
||||||
return -1;
|
|
||||||
} else if (e2.type === 'production') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e1.sortOrder - e2.sortOrder;
|
|
||||||
})
|
|
||||||
.find((fe) => fe.featureName === f.name)?.variants;
|
|
||||||
return { ...f, variants };
|
|
||||||
});
|
|
||||||
v4.featureEnvironments = v4.featureEnvironments.map((fe) => {
|
|
||||||
delete fe.variants;
|
|
||||||
return fe;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// only if explicitly set to false (i.e. undefined defaults to true)
|
|
||||||
if (opts.includeEnvironments === false) {
|
|
||||||
delete v4.environments;
|
|
||||||
} else {
|
|
||||||
if (v4.environments.length === 0) {
|
|
||||||
throw Error('Unable to export without enabled environments');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v4.version = 3;
|
|
||||||
return v4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportV4({
|
async exportV4({
|
||||||
|
@ -30,10 +30,6 @@ const flags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_PROXY_RETURN_ALL_TOGGLES,
|
process.env.UNLEASH_EXPERIMENTAL_PROXY_RETURN_ALL_TOGGLES,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
variantsPerEnvironment: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_VARIANTS_PER_ENVIRONMENT,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
networkView: parseEnvVarBoolean(
|
networkView: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_NETWORK_VIEW,
|
process.env.UNLEASH_EXPERIMENTAL_NETWORK_VIEW,
|
||||||
false,
|
false,
|
||||||
|
@ -38,7 +38,6 @@ process.nextTick(async () => {
|
|||||||
embedProxyFrontend: true,
|
embedProxyFrontend: true,
|
||||||
anonymiseEventLog: false,
|
anonymiseEventLog: false,
|
||||||
responseTimeWithAppName: true,
|
responseTimeWithAppName: true,
|
||||||
variantsPerEnvironment: true,
|
|
||||||
maintenance: true,
|
maintenance: true,
|
||||||
featuresExportImport: true,
|
featuresExportImport: true,
|
||||||
newProjectOverview: true,
|
newProjectOverview: true,
|
||||||
|
@ -26,7 +26,6 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
|
|||||||
flags: {
|
flags: {
|
||||||
embedProxy: true,
|
embedProxy: true,
|
||||||
embedProxyFrontend: true,
|
embedProxyFrontend: true,
|
||||||
variantsPerEnvironment: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -4,9 +4,6 @@ import getLogger from '../../../fixtures/no-logger';
|
|||||||
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
||||||
import { collectIds } from '../../../../lib/util/collect-ids';
|
import { collectIds } from '../../../../lib/util/collect-ids';
|
||||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||||
import variantsv3 from '../../../examples/variantsexport_v3.json';
|
|
||||||
import v3WithDefaultDisabled from '../../../examples/exported3-with-default-disabled.json';
|
|
||||||
import { StateService } from '../../../../lib/services';
|
|
||||||
|
|
||||||
const importData = require('../../../examples/import.json');
|
const importData = require('../../../examples/import.json');
|
||||||
|
|
||||||
@ -435,66 +432,3 @@ test(`should not show environment on feature toggle, when environment is disable
|
|||||||
expect(result[1].name).toBe('production');
|
expect(result[1].name).toBe('production');
|
||||||
expect(result[1].enabled).toBeFalsy();
|
expect(result[1].enabled).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`should handle v3 export with variants in features`, async () => {
|
|
||||||
app.services.stateService = new StateService(db.stores, {
|
|
||||||
getLogger,
|
|
||||||
flagResolver: {
|
|
||||||
isEnabled: () => false,
|
|
||||||
getAll: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/state/import?drop=true')
|
|
||||||
.attach('file', 'src/test/examples/variantsexport_v3.json')
|
|
||||||
.expect(202);
|
|
||||||
|
|
||||||
const exported = await app.services.stateService.export({});
|
|
||||||
let exportedFeatures = exported.features
|
|
||||||
.map((f) => {
|
|
||||||
delete f.createdAt;
|
|
||||||
return f;
|
|
||||||
})
|
|
||||||
.sort();
|
|
||||||
let importedFeatures = variantsv3.features
|
|
||||||
.map((f) => {
|
|
||||||
delete f.createdAt;
|
|
||||||
return f;
|
|
||||||
})
|
|
||||||
.sort();
|
|
||||||
expect(exportedFeatures).toStrictEqual(importedFeatures);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`should handle v3 export with variants in features and only 1 env`, async () => {
|
|
||||||
app.services.stateService = new StateService(db.stores, {
|
|
||||||
getLogger,
|
|
||||||
flagResolver: {
|
|
||||||
isEnabled: () => false,
|
|
||||||
getAll: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/state/import?drop=true')
|
|
||||||
.attach(
|
|
||||||
'file',
|
|
||||||
'src/test/examples/exported3-with-default-disabled.json',
|
|
||||||
)
|
|
||||||
.expect(202);
|
|
||||||
|
|
||||||
const exported = await app.services.stateService.export({});
|
|
||||||
let exportedFeatures = exported.features
|
|
||||||
.map((f) => {
|
|
||||||
delete f.createdAt;
|
|
||||||
return f;
|
|
||||||
})
|
|
||||||
.sort();
|
|
||||||
let importedFeatures = v3WithDefaultDisabled.features
|
|
||||||
.map((f) => {
|
|
||||||
delete f.createdAt;
|
|
||||||
return f;
|
|
||||||
})
|
|
||||||
.sort();
|
|
||||||
expect(exportedFeatures).toStrictEqual(importedFeatures);
|
|
||||||
});
|
|
||||||
|
Loading…
Reference in New Issue
Block a user