1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

feat: e2e tests and mobile views (#348)

* fix: add sidebar button

* fix: set absolute positioned sidebar button

* feat: test setup

* fix: add tests for adding strategy

* fix: add delete  strategy test

* feat: add workflow

* feat: add vercel token

* fix: update project id

* fix: increase sleep

* fix: sleep

* fix: vercel

* fix: typo

* fix: vercel preview url action

* fix: yml formatting

* fix: steps

* fix: format

* fix: runs on

* fix: team id

* fix: teamid

* fix: add workflow

* fix: remove unused import

* fix: add token

* fix: add configuration

* fix: set env variables

* fix: use with

* feat: main navigation routes

* feat: mobile views

* fix: change spec name

* fix: update cypress project id

* fix: add record key

* fix: button positioning

* feat: permissions

* fix: custom strategy

* fix: remove unused action yml

* fix: update yarn lock

* fix: keys

* fix: remove videos and screenshots

* fix: add cyrpess folders to gitignore

* fix: env variable
This commit is contained in:
Fredrik Strand Oseberg 2021-09-30 11:44:30 +02:00 committed by GitHub
parent 35b218b75a
commit 87414c1c9c
62 changed files with 1635 additions and 246 deletions

24
frontend/.github/workflows/e2e.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: e2e-tests
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on: [deployment_status]
jobs:
e2e:
# only runs this job on successful deploy
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: |
echo "$GITHUB_CONTEXT"
- name: Checkout
uses: actions/checkout@v1
- name: Run Cypress
uses: cypress-io/github-action@v2
with:
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="default"
config: baseUrl=${{ github.event.deployment_status.target_url }}
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View File

@ -10,7 +10,6 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
@ -26,3 +25,6 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: yarn
- run: yarn run test

3
frontend/.gitignore vendored
View File

@ -48,3 +48,6 @@ build
.vscode/
.DS_Store
cypress/videos/*
cypress/screenshots/*

View File

@ -1 +1,2 @@
CHANGELOG.md
CHANGELOG.md
.github/*

3
frontend/cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"projectId": "tc2qff"
}

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,245 @@
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
let featureToggleName = '';
let enterprise = false;
let strategyId = '';
let defaultEnv = ':global:';
describe('feature toggle', () => {
before(() => {
featureToggleName = `unleash-e2e-${Math.floor(Math.random() * 100)}`;
enterprise = Boolean(Cypress.env('ENTERPRISE'));
const env = Cypress.env('DEFAULT_ENV');
if (env) {
defaultEnv = env;
}
});
after(() => {
const authToken = Cypress.env('AUTH_TOKEN');
cy.request({
method: 'DELETE',
url: `${
Cypress.config().baseUrl
}/api/admin/features/${featureToggleName}`,
headers: {
Authorization: authToken,
},
});
cy.request({
method: 'DELETE',
url: `${
Cypress.config().baseUrl
}/api/admin/archive/${featureToggleName}`,
headers: {
Authorization: authToken,
},
});
});
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
const passwordAuth = Cypress.env('PASSWORD_AUTH');
enterprise = Boolean(Cypress.env('ENTERPRISE'));
cy.visit('/');
if (passwordAuth) {
cy.get('[data-test="LOGIN_EMAIL_ID"]').type('test@test.com');
cy.get('[data-test="LOGIN_PASSWORD_ID"]').type('qY70$NDcJNXA');
cy.get("[data-test='LOGIN_BUTTON']").click();
} else {
cy.get('[data-test=LOGIN_EMAIL_ID]').type('test@unleash-e2e.com');
cy.get('[data-test=LOGIN_BUTTON]').click();
}
});
it('Creates a feature toggle', () => {
cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
cy.intercept('POST', '/api/admin/features').as('createFeature');
cy.get("[data-test='CF_NAME_ID'").type(featureToggleName);
cy.get("[data-test='CF_DESC_ID'").type('hellowrdada');
cy.get("[data-test='CF_CREATE_BTN_ID']").click();
cy.wait('@createFeature');
cy.url().should('include', featureToggleName);
});
it('Can add a gradual rollout strategy to the default environment', () => {
cy.wait(500);
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-1').click();
cy.get('[data-test=ROLLOUT_SLIDER_ID')
.click()
.type('{leftarrow}'.repeat(20));
if (enterprise) {
cy.get('[data-test=ADD_CONSTRAINT_ID]').click();
cy.get('[data-test=CONSTRAINT_AUTOCOMPLETE_ID]')
.type('{downArrow}'.repeat(1))
.type('{enter}');
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
}
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies`,
req => {
expect(req.body.name).to.equal('flexibleRollout');
expect(req.body.parameters.groupId).to.equal(featureToggleName);
expect(req.body.parameters.stickiness).to.equal('default');
expect(req.body.parameters.rollout).to.equal(30);
if (enterprise) {
expect(req.body.constraints.length).to.equal(1);
} else {
expect(req.body.constraints.length).to.equal(0);
}
req.continue(res => {
strategyId = res.body.id;
});
}
).as('addStrategyToFeature');
cy.get('[data-test=ADD_NEW_STRATEGY_SAVE_ID]').first().click();
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@addStrategyToFeature');
});
it('can update a strategy in the default environment', () => {
cy.wait(500);
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
cy.get('[data-test=STRATEGY_ACCORDION_ID-flexibleRollout').click();
cy.get('[data-test=ROLLOUT_SLIDER_ID')
.first()
.click()
.type('{rightArrow}'.repeat(10));
cy.get('[data-test=FLEXIBLE_STRATEGY_STICKINESS_ID]')
.first()
.click()
.get('[data-test=SELECT_ITEM_ID-sessionId')
.first()
.click();
let newGroupId = 'new-group-id';
cy.get('[data-test=FLEXIBLE_STRATEGY_GROUP_ID]')
.first()
.clear()
.type('new-group-id');
cy.intercept(
'PUT',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies/${strategyId}`,
req => {
expect(req.body.parameters.groupId).to.equal(newGroupId);
expect(req.body.parameters.stickiness).to.equal('sessionId');
expect(req.body.parameters.rollout).to.equal(60);
if (enterprise) {
expect(req.body.constraints.length).to.equal(1);
} else {
expect(req.body.constraints.length).to.equal(0);
}
req.continue(res => {
expect(res.statusCode).to.equal(200);
});
}
).as('updateStrategy');
cy.get('[data-test=UPDATE_STRATEGY_BUTTON_ID]').first().click();
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@updateStrategy');
});
it('can delete a strategy in the default environment', () => {
cy.wait(500);
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
cy.intercept(
'DELETE',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies/${strategyId}`,
req => {
req.continue(res => {
expect(res.statusCode).to.equal(200);
});
}
).as('deleteStrategy');
cy.get('[data-test=DELETE_STRATEGY_ID-flexibleRollout]').click();
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@deleteStrategy');
});
it('Can add a userid strategy to the default environment', () => {
cy.wait(500);
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-2').click();
if (enterprise) {
cy.get('[data-test=ADD_CONSTRAINT_ID]').click();
cy.get('[data-test=CONSTRAINT_AUTOCOMPLETE_ID]')
.type('{downArrow}'.repeat(1))
.type('{enter}');
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
}
cy.get('[data-test=STRATEGY_INPUT_LIST]')
.type('user1')
.type('{enter}')
.type('user2')
.type('{enter}');
cy.get('[data-test=ADD_TO_STRATEGY_INPUT_LIST]').click();
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies`,
req => {
expect(req.body.name).to.equal('userWithId');
expect(req.body.parameters.userIds.length).to.equal(11);
if (enterprise) {
expect(req.body.constraints.length).to.equal(1);
} else {
expect(req.body.constraints.length).to.equal(0);
}
req.continue(res => {
strategyId = res.body.id;
});
}
).as('addStrategyToFeature');
cy.get('[data-test=ADD_NEW_STRATEGY_SAVE_ID]').first().click();
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@addStrategyToFeature');
});
});

View File

@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -30,7 +30,9 @@
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com yarn run start",
"start:ea": "UNLEASH_API=https://unleash4.herokuapp.com yarn run start",
"test": "react-scripts test",
"prepare": "yarn run build"
"prepare": "yarn run build",
"e2e": "yarn run cypress open --config baseUrl='http://localhost:3000' --env PASSWORD_AUTH=true",
"e2e:enterprise": "yarn run cypress open --config baseUrl='http://localhost:3000' --env PASSWORD_AUTH=true,ENTERPRISE=true,AUTH_TOKEN=$AUTH_TOKEN"
},
"devDependencies": {
"@material-ui/core": "4.11.3",
@ -52,6 +54,7 @@
"classnames": "2.3.1",
"craco": "0.0.3",
"css-loader": "5.2.7",
"cypress": "^8.4.1",
"date-fns": "2.19.0",
"debounce": "1.2.1",
"enzyme": "3.11.0",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -9,6 +9,7 @@ import {
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
import { useStyles } from './Dialogue.styles';
import { DIALOGUE_CONFIRM_ID } from '../../../testIds';
interface IDialogue {
primaryButtonText?: string;
@ -65,6 +66,7 @@ const Dialogue: React.FC<IDialogue> = ({
onClick={onClick}
autoFocus
disabled={disabledPrimaryButton}
data-test={DIALOGUE_CONFIRM_ID}
>
{primaryButtonText || "Yes, I'm sure"}
</Button>

View File

@ -0,0 +1,32 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
display: 'flex',
width: '80%',
margin: '0 auto',
[theme.breakpoints.down(700)]: {
flexDirection: 'column',
alignItems: 'center',
},
},
textContainer: {
width: '50%',
[theme.breakpoints.down(700)]: {
width: '100%',
},
},
iconContainer: {
width: '50%',
[theme.breakpoints.down(700)]: {
width: '100%',
},
},
icon: {
width: '300px',
height: '200px',
[theme.breakpoints.down(700)]: {
marginTop: '2rem',
},
},
}));

View File

@ -0,0 +1,16 @@
import { ReactComponent as NoItemsIcon } from '../../../assets/icons/addfiles.svg';
import { useStyles } from './NoItems.styles';
const NoItems: React.FC = ({ children }) => {
const styles = useStyles();
return (
<div className={styles.container}>
<div className={styles.textContainer}>{children}</div>
<div className={styles.iconContainer}>
<NoItemsIcon className={styles.icon} />
</div>
</div>
);
};
export default NoItems;

View File

@ -14,6 +14,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
maxWidth,
tooltip,
children,
...rest
}) => {
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
@ -22,7 +23,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
condition={smallScreen}
show={
<Tooltip title={tooltip ? tooltip : ''}>
<IconButton onClick={onClick} data-loading>
<IconButton onClick={onClick} data-loading {...rest}>
<Icon />
</IconButton>
</Tooltip>
@ -33,6 +34,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
color="primary"
variant="contained"
data-loading
{...rest}
>
{children}
</Button>

View File

@ -1,10 +1,11 @@
import React from 'react';
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
import { SELECT_ITEM_ID } from '../../testIds';
export interface ISelectOption {
key: string;
title?: string;
label?: string
label?: string;
}
export interface ISelectMenuProps {
name: string;
@ -16,8 +17,8 @@ export interface ISelectMenuProps {
onChange?: (
event: React.ChangeEvent<{ name?: string; value: unknown }>,
child: React.ReactNode
) => void
disabled?: boolean
) => void;
disabled?: boolean;
className?: string;
classes?: any;
}
@ -36,7 +37,12 @@ const SelectMenu: React.FC<ISelectMenuProps> = ({
}) => {
const renderSelectItems = () =>
options.map(option => (
<MenuItem key={option.key} value={option.key} title={option.title || ''}>
<MenuItem
key={option.key}
value={option.key}
title={option.title || ''}
data-test={`${SELECT_ITEM_ID}-${option.label}`}
>
{option.label}
</MenuItem>
));
@ -62,6 +68,4 @@ const SelectMenu: React.FC<ISelectMenuProps> = ({
);
};
export default SelectMenu;

View File

@ -22,6 +22,7 @@ import AccessContext from '../../../contexts/AccessContext';
import { useStyles } from './styles';
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
import { NAVIGATE_TO_CREATE_FEATURE } from '../../../testIds';
const FeatureToggleList = ({
fetcher,
@ -161,7 +162,9 @@ const FeatureToggleList = ({
<IconButton
component={Link}
to={createURL}
data-test="add-feature-btn"
data-test={
NAVIGATE_TO_CREATE_FEATURE
}
disabled={
!hasAccess(
CREATE_FEATURE,
@ -176,10 +179,12 @@ const FeatureToggleList = ({
elseShow={
<Button
to={createURL}
data-test="add-feature-btn"
color="primary"
variant="contained"
component={Link}
data-test={
NAVIGATE_TO_CREATE_FEATURE
}
disabled={
!hasAccess(
CREATE_FEATURE,

View File

@ -197,7 +197,7 @@ exports[`renders correctly with one feature 1`] = `
<a
aria-disabled={true}
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
data-test="add-feature-btn"
data-test="NAVIGATE_TO_CREATE_FEATURE"
href="/projects/default/create-toggle?project=default"
onBlur={[Function]}
onClick={[Function]}
@ -454,7 +454,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
<a
aria-disabled={true}
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
data-test="add-feature-btn"
data-test="NAVIGATE_TO_CREATE_FEATURE"
href="/projects/default/create-toggle?project=default"
onBlur={[Function]}
onClick={[Function]}

View File

@ -1,5 +1,9 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: { borderRadius: '10px', boxShadow: 'none', display: 'flex' },
container: {
borderRadius: '10px',
boxShadow: 'none',
display: 'flex',
},
}));

View File

@ -3,13 +3,23 @@ import FeatureStrategiesList from './FeatureStrategiesList/FeatureStrategiesList
import { useStyles } from './FeatureStrategies.styles';
import FeatureStrategiesUIProvider from './FeatureStrategiesUIProvider';
import FeatureStrategiesEnvironments from './FeatureStrategiesEnvironments/FeatureStrategiesEnvironments';
import ConditionallyRender from '../../../common/ConditionallyRender';
import { UPDATE_FEATURE } from '../../../AccessProvider/permissions';
import { useContext } from 'react';
import AccessContext from '../../../../contexts/AccessContext';
const FeatureStrategies = () => {
const { hasAccess } = useContext(AccessContext);
const styles = useStyles();
return (
<Paper className={styles.container}>
<FeatureStrategiesUIProvider>
<FeatureStrategiesList />
<ConditionallyRender
condition={hasAccess(UPDATE_FEATURE)}
show={<FeatureStrategiesList />}
/>
<FeatureStrategiesEnvironments />
</FeatureStrategiesUIProvider>
</Paper>

View File

@ -5,7 +5,7 @@ export const useStyles = makeStyles(theme => ({
border: `1px solid ${theme.palette.grey[300]}`,
borderRadius: '5px',
width: '270px',
marginLeft: 'auto',
marginLeft: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',

View File

@ -41,6 +41,7 @@ const FeatureEnvironmentStrategyExecutionWrapper = ({
<FeatureStrategyExecution
constraints={strategy.constraints}
parameters={strategy.parameters}
strategy={strategy}
/>
</div>
);

View File

@ -18,9 +18,13 @@ export const useStyles = makeStyles(theme => ({
configureContainer: { display: 'flex', width: '100%' },
accordionContainer: {
width: '68%',
[theme.breakpoints.down(900)]: {
width: '100%',
},
},
executionContainer: {
width: '32%',
marginLeft: '1rem',
},
envWarning: {
marginBottom: '1rem',

View File

@ -1,4 +1,4 @@
import { Button } from '@material-ui/core';
import { Button, useMediaQuery } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { useContext, useState } from 'react';
import { getHumanReadbleStrategyName } from '../../../../../../utils/strategy-names';
@ -15,6 +15,7 @@ import { IFeatureViewParams } from '../../../../../../interfaces/params';
import cloneDeep from 'lodash.clonedeep';
import FeatureStrategyCreateExecution from '../../FeatureStrategyCreateExecution/FeatureStrategyCreateExecution';
import { PRODUCTION } from '../../../../../../constants/environmentTypes';
import { ADD_NEW_STRATEGY_SAVE_ID } from '../../../../../../testIds';
interface IFeatureStrategiesConfigure {
setToastData: React.Dispatch<React.SetStateAction<IToastType>>;
@ -22,6 +23,8 @@ interface IFeatureStrategiesConfigure {
const FeatureStrategiesConfigure = ({
setToastData,
}: IFeatureStrategiesConfigure) => {
const smallScreen = useMediaQuery('(max-width:900px)');
const { projectId, featureId } = useParams<IFeatureViewParams>();
const [productionGuard, setProductionGuard] = useState(false);
@ -132,12 +135,19 @@ const FeatureStrategiesConfigure = ({
setStrategyConstraints={setStrategyConstraints}
/>
</div>
<div className={styles.executionContainer}>
<FeatureStrategyCreateExecution
parameters={strategyParams}
constraints={strategyConstraints}
/>
</div>
<ConditionallyRender
condition={!smallScreen}
show={
<div className={styles.executionContainer}>
<FeatureStrategyCreateExecution
parameters={strategyParams}
constraints={strategyConstraints}
configureNewStrategy={configureNewStrategy}
/>
</div>
}
/>
</div>
<div className={styles.buttonContainer}>
@ -146,6 +156,7 @@ const FeatureStrategiesConfigure = ({
color="primary"
className={styles.btn}
onClick={resolveSubmit}
data-test={ADD_NEW_STRATEGY_SAVE_ID}
>
Save
</Button>

View File

@ -1,11 +1,7 @@
import {
IParameter,
IFeatureStrategy,
} from '../../../../../../interfaces/strategy';
import { IFeatureStrategy } from '../../../../../../interfaces/strategy';
import { FEATURE_STRATEGIES_DRAG_TYPE } from '../../FeatureStrategiesList/FeatureStrategyCard/FeatureStrategyCard';
import { DropTargetMonitor, useDrop } from 'react-dnd';
import { Fragment } from 'react';
import { resolveDefaultParamValue } from '../../../../strategy/AddStrategy/utils';
import useStrategies from '../../../../../../hooks/api/getters/useStrategies/useStrategies';
import { useStyles } from './FeatureStrategiesEnvironmentList.styles';
import classnames from 'classnames';
@ -18,6 +14,7 @@ import useDeleteStrategyMarkup from './useDeleteStrategyMarkup';
import useProductionGuardMarkup from './useProductionGuardMarkup';
import FeatureStrategyEditable from '../FeatureStrategyEditable/FeatureStrategyEditable';
import { PRODUCTION } from '../../../../../../constants/environmentTypes';
import { getStrategyObject } from '../../../../../../utils/get-strategy-object';
interface IFeatureStrategiesEnvironmentListProps {
strategies: IFeatureStrategy[];
@ -57,12 +54,12 @@ const FeatureStrategiesEnvironmentList = ({
};
},
drop(item: IFeatureDragItem, monitor: DropTargetMonitor) {
// const dragIndex = item.index;
// const hoverIndex = index;
const strategy = selectStrategy(item.name);
const strategy = getStrategyObject(
selectableStrategies,
item.name,
featureId
);
if (!strategy) return;
//addNewStrategy(strategy);
setConfigureNewStrategy(strategy);
setExpandedSidebar(false);
},
@ -97,19 +94,6 @@ const FeatureStrategiesEnvironmentList = ({
updateStrategy(strategy);
};
const selectStrategy = (name: string) => {
const selectedStrategy = selectableStrategies.find(
strategy => strategy.name === name
);
const parameters = {} as IParameter;
selectedStrategy?.parameters.forEach(({ name }: IParameter) => {
parameters[name] = resolveDefaultParamValue(name, featureId);
});
return { name, parameters, constraints: [] };
};
const renderStrategies = () => {
return strategies.map((strategy, index) => {
if (index !== strategies.length - 1) {
@ -119,6 +103,7 @@ const FeatureStrategiesEnvironmentList = ({
currentStrategy={strategy}
setDelDialog={setDelDialog}
updateStrategy={resolveUpdateStrategy}
index={index}
/>
<FeatureStrategiesSeparator text="OR" />
@ -131,6 +116,7 @@ const FeatureStrategiesEnvironmentList = ({
setDelDialog={setDelDialog}
currentStrategy={strategy}
updateStrategy={resolveUpdateStrategy}
index={index}
/>
);
}

View File

@ -7,7 +7,9 @@ import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { IFeatureStrategy } from '../../../../../../interfaces/strategy';
import cloneDeep from 'lodash.clonedeep';
const useFeatureStrategiesEnvironmentList = (strategies: IFeatureStrategy[]) => {
const useFeatureStrategiesEnvironmentList = (
strategies: IFeatureStrategy[]
) => {
const { projectId, featureId } = useParams<IFeatureViewParams>();
const { deleteStrategyFromFeature, updateStrategyOnFeature } =
@ -46,7 +48,7 @@ const useFeatureStrategiesEnvironmentList = (strategies: IFeatureStrategy[]) =>
await updateStrategyOnFeature(
projectId,
featureId,
activeEnvironment.id,
activeEnvironment.name,
updatedStrategy.id,
updateStrategyPayload
);

View File

@ -3,22 +3,45 @@ import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
width: '70%',
[theme.breakpoints.down(900)]: {
width: '50%',
},
[theme.breakpoints.down(700)]: {
width: '0%',
},
},
fullWidth: {
width: '90%',
[theme.breakpoints.down(700)]: {
width: '85%',
},
},
environmentsHeader: {
padding: '2rem 2rem 1rem 2rem',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
[theme.breakpoints.down(700)]: {
padding: '1.5rem',
},
},
tabContentContainer: {
padding: '1rem 2rem 2rem 2rem',
display: 'flex',
justifyContent: 'space-between',
[theme.breakpoints.down(700)]: {
padding: '1.5rem',
},
},
listContainerWithoutSidebar: {
width: '100%',
},
listContainer: {
width: '70%',
[theme.breakpoints.down(700)]: {
width: '100%',
},
},
listContainer: { width: '70%' },
listContainerFullWidth: { width: '100%' },
containerListView: {
display: 'none',
@ -41,4 +64,11 @@ export const useStyles = makeStyles(theme => ({
textTransform: 'none',
width: 'auto',
},
noItemsParagraph: {
margin: '1rem 0',
},
link: {
display: 'block',
margin: '1rem 0 0 0',
},
}));

View File

@ -1,7 +1,7 @@
import { useParams } from 'react-router-dom';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
import { useStyles } from './FeatureStrategiesEnvironments.styles';
import { Tabs, Tab, Button } from '@material-ui/core';
import { Tabs, Tab, Button, useMediaQuery } from '@material-ui/core';
import TabPanel from '../../../../common/TabNav/TabPanel';
import useTabs from '../../../../../hooks/useTabs';
import FeatureStrategiesEnvironmentList from './FeatureStrategiesEnvironmentList/FeatureStrategiesEnvironmentList';
@ -15,15 +15,24 @@ import { IFeatureViewParams } from '../../../../../interfaces/params';
import cloneDeep from 'lodash.clonedeep';
import FeatureStrategiesRefresh from './FeatureStrategiesRefresh/FeatureStrategiesRefresh';
import FeatureEnvironmentStrategyExecution from './FeatureEnvironmentStrategyExecution/FeatureEnvironmentStrategyExecution';
import { ADD_NEW_STRATEGY_ID } from '../../../../../testIds';
import NoItems from '../../../../common/NoItems/NoItems';
import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton';
import { Add } from '@material-ui/icons';
import AccessContext from '../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
const FeatureStrategiesEnvironments = () => {
const smallScreen = useMediaQuery('(max-width:700px)');
const { hasAccess } = useContext(AccessContext);
const startingTabId = 0;
const { projectId, featureId } = useParams<IFeatureViewParams>();
const { toast, setToastData } = useToast();
const [showRefreshPrompt, setShowRefreshPrompt] = useState(false);
const styles = useStyles();
const { a11yProps, activeTab, setActiveTab } = useTabs(startingTabId);
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(startingTabId);
const {
setActiveEnvironment,
configureNewStrategy,
@ -62,7 +71,8 @@ const FeatureStrategiesEnvironments = () => {
}, [feature]);
useEffect(() => {
setActiveEnvironment(feature?.environments[activeTab]);
if (!feature?.environments?.length > 0) return;
setActiveEnvironment(feature?.environments[activeTabIdx]);
/* eslint-disable-next-line */
}, [feature]);
@ -91,8 +101,8 @@ const FeatureStrategiesEnvironments = () => {
equal = false;
}
feature.environments.forEach(env => {
const cachedEnv = featureCache.environments.find(
feature?.environments?.forEach(env => {
const cachedEnv = featureCache?.environments?.find(
cacheEnv => cacheEnv.name === env.name
);
@ -100,8 +110,13 @@ const FeatureStrategiesEnvironments = () => {
equal = false;
return;
}
// If displayName is different
if (env?.displayName !== cachedEnv?.displayName) {
equal = false;
return;
}
// If the type of environments are different
if (env.type !== cachedEnv.type) {
if (env?.type !== cachedEnv?.type) {
equal = false;
return;
}
@ -109,8 +124,8 @@ const FeatureStrategiesEnvironments = () => {
if (!equal) return equal;
feature.environments.forEach(env => {
const cachedEnv = featureCache.environments.find(
feature?.environments?.forEach(env => {
const cachedEnv = featureCache?.environments?.find(
cachedEnv => cachedEnv.name === env.name
);
@ -121,8 +136,8 @@ const FeatureStrategiesEnvironments = () => {
return;
}
env.strategies.forEach(strategy => {
const cachedStrategy = cachedEnv.strategies.find(
env?.strategies?.forEach(strategy => {
const cachedStrategy = cachedEnv?.strategies?.find(
cachedStrategy => cachedStrategy.id === strategy.id
);
// Check stickiness
@ -199,29 +214,94 @@ const FeatureStrategiesEnvironments = () => {
const listContainerClasses = classNames(styles.listContainer, {
[styles.listContainerFullWidth]: expandedSidebar,
[styles.listContainerWithoutSidebar]: !hasAccess(UPDATE_FEATURE),
});
return featureCache?.environments?.map((env, index) => {
return (
<TabPanel
key={`tab_panel_${index}`}
value={activeTab}
value={activeTabIdx}
index={index}
>
<div className={tabContentClasses}>
<div className={listContainerClasses}>
<FeatureStrategiesEnvironmentList
strategies={env.strategies}
/>
</div>
<ConditionallyRender
condition={
!expandedSidebar && !configureNewStrategy
env.strategies.length > 0 || expandedSidebar
}
show={
<FeatureEnvironmentStrategyExecution
strategies={env.strategies}
env={env}
<>
<div className={listContainerClasses}>
<FeatureStrategiesEnvironmentList
strategies={env.strategies}
/>
</div>
<ConditionallyRender
condition={
!expandedSidebar &&
!configureNewStrategy &&
!smallScreen
}
show={
<FeatureEnvironmentStrategyExecution
strategies={env.strategies}
env={env}
/>
}
/>
</>
}
elseShow={
<ConditionallyRender
condition={!expandedSidebar}
show={
<NoItems>
<p
className={
styles.noItemsParagraph
}
>
No strategies added in the{' '}
{env.name} environment
</p>
<p
className={
styles.noItemsParagraph
}
>
Strategies added in this
environment will only be
executed if the SDK is using an
API key configured for this
environment.
<a
className={styles.link}
href="https://docs.getunleash.ai"
>
Read more here
</a>
</p>
<ConditionallyRender
condition={hasAccess(
UPDATE_FEATURE
)}
show={
<Button
variant="contained"
color="primary"
onClick={() =>
setExpandedSidebar(
prev => !prev
)
}
>
Add your first strategy
</Button>
}
/>
</NoItems>
}
/>
}
/>
@ -246,49 +326,67 @@ const FeatureStrategiesEnvironments = () => {
return (
<div className={classes}>
<div className={styles.environmentsHeader}>
<h2 className={styles.header}>Environments</h2>
<ConditionallyRender
condition={(!expandedSidebar && smallScreen) || !smallScreen}
show={
<>
<div className={styles.environmentsHeader}>
<h2 className={styles.header}>Environments</h2>
<FeatureStrategiesRefresh
show={showRefreshPrompt}
refresh={handleRefresh}
cancel={handleCancel}
/>
<Button
variant="contained"
color="primary"
onClick={() => setExpandedSidebar(prev => !prev)}
>
{expandedSidebar ? 'Hide sidebar' : 'Add new strategy'}
</Button>
</div>
<div className={styles.tabContainer}>
<Tabs
value={activeTab}
onChange={(_, tabId) => {
setActiveTab(tabId);
setActiveEnvironment(featureCache?.environments[tabId]);
}}
indicatorColor="primary"
textColor="primary"
className={styles.tabNavigation}
>
{renderTabs()}
</Tabs>
</div>
<FeatureStrategiesRefresh
show={showRefreshPrompt}
refresh={handleRefresh}
cancel={handleCancel}
/>
<ConditionallyRender
condition={!expandedSidebar}
show={
<ResponsiveButton
data-test={ADD_NEW_STRATEGY_ID}
onClick={() =>
setExpandedSidebar(prev => !prev)
}
Icon={Add}
maxWidth="700px"
disabled={!hasAccess(UPDATE_FEATURE)}
>
Add new strategy
</ResponsiveButton>
}
/>
</div>
<div className={styles.tabContainer}>
<Tabs
value={activeTabIdx}
onChange={(_, tabId) => {
setActiveTab(tabId);
setActiveEnvironment(
featureCache?.environments[tabId]
);
}}
indicatorColor="primary"
textColor="primary"
className={styles.tabNavigation}
>
{renderTabs()}
</Tabs>
</div>
<div>
{renderTabPanels()}
<ConditionallyRender
condition={configureNewStrategy}
show={
<FeatureStrategiesConfigure
setToastData={setToastData}
/>
}
/>
</div>
{toast}
<div>
{renderTabPanels()}
<ConditionallyRender
condition={configureNewStrategy}
show={
<FeatureStrategiesConfigure
setToastData={setToastData}
/>
}
/>
</div>
{toast}
</>
}
/>
</div>
);
};

View File

@ -14,10 +14,23 @@ export const useStyles = makeStyles(theme => ({
borderRadius: '3px',
fontSize: theme.fontSizes.smallerBody,
zIndex: 400,
[theme.breakpoints.down(500)]: {
right: 100,
},
},
buttonContainer: {
display: 'flex',
alignItems: 'center',
marginTop: '1rem',
[theme.breakpoints.down(500)]: {
flexDirection: 'column',
},
},
editButton: {
margin: '0 1rem 0 0',
[theme.breakpoints.down(500)]: {
width: '100%',
margin: '0.4rem 0',
},
},
}));

View File

@ -14,20 +14,31 @@ import cloneDeep from 'lodash.clonedeep';
import { Button, IconButton, Tooltip } from '@material-ui/core';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import { useStyles } from './FeatureStrategyEditable.styles';
import { Delete, FileCopy } from '@material-ui/icons';
import { Delete } from '@material-ui/icons';
import { PRODUCTION } from '../../../../../../constants/environmentTypes';
import {
DELETE_STRATEGY_ID,
STRATEGY_ACCORDION_ID,
UPDATE_STRATEGY_BUTTON_ID,
} from '../../../../../../testIds';
import AccessContext from '../../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../../AccessProvider/permissions';
interface IFeatureStrategyEditable {
currentStrategy: IFeatureStrategy;
setDelDialog?: React.Dispatch<React.SetStateAction<any>>;
updateStrategy: (strategy: IFeatureStrategy) => void;
index?: number;
}
const FeatureStrategyEditable = ({
currentStrategy,
updateStrategy,
setDelDialog,
index,
}: IFeatureStrategyEditable) => {
const { hasAccess } = useContext(AccessContext);
const { projectId, featureId } = useParams<IFeatureViewParams>();
const { activeEnvironment, featureCache, dirty, setDirty } = useContext(
FeatureStrategiesUIContext
@ -122,36 +133,31 @@ const FeatureStrategyEditable = ({
<FeatureStrategyAccordion
parameters={parameters}
constraints={constraints}
data-test={`${STRATEGY_ACCORDION_ID}-${strategy.name}`}
strategy={strategy}
setStrategyParams={setStrategyParams}
setStrategyConstraints={setStrategyConstraints}
dirty={dirty[strategy.id]}
actions={
<>
<Tooltip title="Delete strategy">
<IconButton
onClick={e => {
e.stopPropagation();
setDelDialog({
strategyId: strategy.id,
show: true,
});
}}
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title="Copy strategy">
<IconButton
onClick={e => {
e.stopPropagation();
}}
>
<FileCopy />
</IconButton>
</Tooltip>
</>
<ConditionallyRender
condition={hasAccess(UPDATE_FEATURE)}
show={
<Tooltip title="Delete strategy">
<IconButton
data-test={`${DELETE_STRATEGY_ID}-${strategy.name}`}
onClick={e => {
e.stopPropagation();
setDelDialog({
strategyId: strategy.id,
show: true,
});
}}
>
<Delete />
</IconButton>
</Tooltip>
}
/>
}
>
<ConditionallyRender
@ -162,12 +168,16 @@ const FeatureStrategyEditable = ({
<Button
variant="contained"
color="primary"
style={{ marginRight: '1rem' }}
className={styles.editButton}
onClick={updateFeatureStrategy}
data-test={UPDATE_STRATEGY_BUTTON_ID}
>
Save changes
</Button>
<Button onClick={discardChanges}>
<Button
onClick={discardChanges}
className={styles.editButton}
>
Discard changes
</Button>
</div>

View File

@ -6,8 +6,46 @@ export const useStyles = makeStyles(theme => ({
padding: '2rem',
borderRight: `1px solid ${theme.palette.grey[300]}`,
transition: 'width 0.3s ease',
position: 'relative',
minHeight: '400px',
[theme.breakpoints.down(900)]: {
width: '50%',
},
[theme.breakpoints.down(700)]: {
padding: '1rem',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
},
sidebarSmall: {
width: '10%',
[theme.breakpoints.down(700)]: {
width: '15%',
},
},
iconButton: {
position: 'absolute',
top: '300px',
right: '-25px',
backgroundColor: theme.palette.grey[300],
[theme.breakpoints.down(700)]: {
right: '-10px',
},
},
icon: {
transition: 'transform 0.4s ease',
transitionDelay: '0.4s',
},
expandedIcon: {
transform: 'rotate(180deg)',
},
mobileNavContainer: {
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: 'space-between',
padding: '1rem',
},
}));

View File

@ -5,31 +5,67 @@ import { useStyles } from './FeatureStrategiesList.styles';
import { useContext } from 'react';
import FeatureStrategiesUIContext from '../../../../../contexts/FeatureStrategiesUIContext';
import classnames from 'classnames';
import { Button, IconButton, useMediaQuery } from '@material-ui/core';
import { DoubleArrow } from '@material-ui/icons';
import ConditionallyRender from '../../../../common/ConditionallyRender';
const FeatureStrategiesList = () => {
const { expandedSidebar } = useContext(FeatureStrategiesUIContext);
const smallScreen = useMediaQuery('(max-width:700px)');
const { expandedSidebar, setExpandedSidebar } = useContext(
FeatureStrategiesUIContext
);
const styles = useStyles();
const { strategies } = useStrategies();
const DEFAULT_STRATEGY = 'default';
const renderStrategies = () => {
return strategies
.filter((strategy: IStrategy) => !strategy.deprecated)
.map((strategy: IStrategy) => (
.filter(
(strategy: IStrategy) =>
!strategy.deprecated && strategy.name !== DEFAULT_STRATEGY
)
.map((strategy: IStrategy, index: number) => (
<FeatureStrategyCard
key={strategy.name}
configureNewStrategy={!expandedSidebar}
name={strategy.name}
description={strategy.description}
index={index}
/>
));
};
const toggleSidebar = () => {
setExpandedSidebar(prev => !prev);
};
const classes = classnames(styles.sidebar, {
[styles.sidebarSmall]: !expandedSidebar,
});
return <section className={classes}>{renderStrategies()}</section>;
const iconClasses = classnames(styles.icon, {
[styles.expandedIcon]: expandedSidebar,
});
return (
<section className={classes}>
<ConditionallyRender
condition={smallScreen && expandedSidebar}
show={
<div className={styles.mobileNavContainer}>
<p>Select strategy</p>
<Button onClick={toggleSidebar}>Back</Button>
</div>
}
/>
<IconButton className={styles.iconButton} onClick={toggleSidebar}>
<DoubleArrow className={iconClasses} />
</IconButton>
{renderStrategies()}
</section>
);
};
export default FeatureStrategiesList;

View File

@ -8,6 +8,8 @@ export const useStyles = makeStyles(theme => ({
borderRadius: '3px',
margin: '0.5rem 0',
display: 'flex',
position: 'relative',
width: '100%',
'&:active': {
backgroundColor: theme.palette.primary.main,
color: '#fff',
@ -44,4 +46,9 @@ export const useStyles = makeStyles(theme => ({
isDragging: {
backgroundColor: theme.palette.primary.main,
},
addButton: {
position: 'absolute',
right: 0,
top: 0,
},
}));

View File

@ -1,7 +1,18 @@
import { IconButton, Tooltip } from '@material-ui/core';
import { Add } from '@material-ui/icons';
import classNames from 'classnames';
import { useContext } from 'react';
import { useDrag } from 'react-dnd';
import { getFeatureStrategyIcon, getHumanReadbleStrategyName } from '../../../../../../utils/strategy-names';
import { useParams } from 'react-router-dom';
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
import useStrategies from '../../../../../../hooks/api/getters/useStrategies/useStrategies';
import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { ADD_NEW_STRATEGY_CARD_BUTTON_ID } from '../../../../../../testIds';
import { getStrategyObject } from '../../../../../../utils/get-strategy-object';
import {
getFeatureStrategyIcon,
getHumanReadbleStrategyName,
} from '../../../../../../utils/strategy-names';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import { useStyles } from './FeatureStrategyCard.styles';
@ -9,6 +20,7 @@ interface IFeatureStrategyCardProps {
name: string;
description: string;
configureNewStrategy: boolean;
index?: number;
}
export const FEATURE_STRATEGIES_DRAG_TYPE = 'FEATURE_STRATEGIES_DRAG_TYPE';
@ -17,11 +29,26 @@ const FeatureStrategyCard = ({
name,
description,
configureNewStrategy,
index,
}: IFeatureStrategyCardProps) => {
const { featureId } = useParams<IFeatureViewParams>();
const { strategies } = useStrategies();
const { setConfigureNewStrategy, setExpandedSidebar } = useContext(
FeatureStrategiesUIContext
);
const handleClick = () => {
const strategy = getStrategyObject(strategies, name, featureId);
if (!strategy) return;
setConfigureNewStrategy(strategy);
setExpandedSidebar(false);
};
const styles = useStyles();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ _ , drag] = useDrag({
const [_, drag] = useDrag({
type: FEATURE_STRATEGIES_DRAG_TYPE,
item: () => {
return { name };
@ -48,6 +75,15 @@ const FeatureStrategyCard = ({
</div>
</div>
<div className={styles.rightSection}>
<IconButton
className={styles.addButton}
onClick={handleClick}
data-test={`${ADD_NEW_STRATEGY_CARD_BUTTON_ID}-${
index + 1
}`}
>
<Add />
</IconButton>
<Tooltip title={readableName}>
<p className={styles.title}>{readableName}</p>
</Tooltip>

View File

@ -27,9 +27,14 @@ export const useStyles = makeStyles(theme => ({
width: '100%',
},
accordionHeader: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
maxWidth: '200px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
[theme.breakpoints.down(700)]: {
maxWidth: '100px',
fontSize: theme.fontSizes.smallBody,
},
},
accordionActions: {
marginLeft: 'auto',
@ -43,4 +48,7 @@ export const useStyles = makeStyles(theme => ({
fontSize: theme.fontSizes.smallBody,
marginLeft: '0.5rem',
},
accordionDetails: {
width: '100%',
},
}));

View File

@ -1,9 +1,20 @@
import { IConstraint, IParameter, IFeatureStrategy } from '../../../../../interfaces/strategy';
import {
IConstraint,
IParameter,
IFeatureStrategy,
} from '../../../../../interfaces/strategy';
import Accordion from '@material-ui/core/Accordion';
import { AccordionDetails, AccordionSummary } from '@material-ui/core';
import {
AccordionDetails,
AccordionSummary,
useMediaQuery,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { getFeatureStrategyIcon, getHumanReadbleStrategyName } from '../../../../../utils/strategy-names';
import {
getFeatureStrategyIcon,
getHumanReadbleStrategyName,
} from '../../../../../utils/strategy-names';
import { useStyles } from './FeatureStrategyAccordion.styles';
import ConditionallyRender from '../../../../common/ConditionallyRender';
import FeatureStrategyAccordionBody from './FeatureStrategyAccordionBody/FeatureStrategyAccordionBody';
@ -29,7 +40,9 @@ const FeatureStrategyAccordion: React.FC<IFeatureStrategyAccordionProps> = ({
setStrategyConstraints,
actions,
children,
...rest
}) => {
const smallScreen = useMediaQuery('(max-width:500px)');
const styles = useStyles();
const strategyName = getHumanReadbleStrategyName(strategy.name);
const Icon = getFeatureStrategyIcon(strategy.name);
@ -43,7 +56,7 @@ const FeatureStrategyAccordion: React.FC<IFeatureStrategyAccordionProps> = ({
};
return (
<div className={styles.container}>
<div className={styles.container} {...rest}>
<Accordion className={styles.accordion} defaultExpanded={expanded}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
@ -51,12 +64,13 @@ const FeatureStrategyAccordion: React.FC<IFeatureStrategyAccordionProps> = ({
id={strategy.name}
>
<div className={styles.accordionSummary}>
<p className={styles.accordionHeader}>
<Icon className={styles.icon} /> {strategyName}
</p>
<Icon className={styles.icon} />
<p className={styles.accordionHeader}>{strategyName}</p>
<ConditionallyRender
condition={Boolean(parameters?.rollout)}
condition={
Boolean(parameters?.rollout) && !smallScreen
}
show={
<p className={styles.rollout}>
Rolling out to {parameters?.rollout}%
@ -67,7 +81,7 @@ const FeatureStrategyAccordion: React.FC<IFeatureStrategyAccordionProps> = ({
<div className={styles.accordionActions}>{actions}</div>
</div>
</AccordionSummary>
<AccordionDetails>
<AccordionDetails className={styles.accordionDetails}>
<FeatureStrategyAccordionBody
strategy={{ ...strategy, parameters }}
updateParameters={updateParameters}

View File

@ -6,6 +6,12 @@ export const useStyles = makeStyles(theme => ({
fontWeight: 'normal',
marginBottom: '0.5rem',
},
accordionContainer: {
width: '80%',
[theme.breakpoints.down(800)]: {
width: '100%',
},
},
constraintHeader: {
fontWeight: 'bold',
fontSize: theme.fontSizes.smallBody,

View File

@ -1,12 +1,14 @@
import DefaultStrategy from '../../../../strategy/EditStrategyModal/default-strategy';
import FlexibleStrategy from '../../common/FlexibleStrategy/FlexibleStrategy';
import { IConstraint, IFeatureStrategy } from '../../../../../../interfaces/strategy';
import {
IConstraint,
IFeatureStrategy,
} from '../../../../../../interfaces/strategy';
import useUnleashContext from '../../../../../../hooks/api/getters/useUnleashContext/useUnleashContext';
import useStrategies from '../../../../../../hooks/api/getters/useStrategies/useStrategies';
import GeneralStrategy from '../../common/GeneralStrategy/GeneralStrategy';
import UserWithIdStrategy from '../../common/UserWithIdStrategy/UserWithId';
import StrategyConstraints from '../../common/StrategyConstraints/StrategyConstraints';
import { useState } from 'react';
import { useContext, useState } from 'react';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import useUiConfig from '../../../../../../hooks/api/getters/useUiConfig/useUiConfig';
import { C } from '../../../../../common/flags';
@ -14,6 +16,10 @@ import { Button } from '@material-ui/core';
import { useStyles } from './FeatureStrategyAccordionBody.styles';
import Dialogue from '../../../../../common/Dialogue';
import FeatureStrategiesSeparator from '../../FeatureStrategiesEnvironments/FeatureStrategiesSeparator/FeatureStrategiesSeparator';
import DefaultStrategy from '../../common/DefaultStrategy/DefaultStrategy';
import { ADD_CONSTRAINT_ID } from '../../../../../../testIds';
import AccessContext from '../../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../../AccessProvider/permissions';
interface IFeatureStrategyAccordionBodyProps {
strategy: IFeatureStrategy;
@ -38,6 +44,7 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
const { strategies } = useStrategies();
const { uiConfig } = useUiConfig();
const [showConstraints, setShowConstraints] = useState(false);
const { hasAccess } = useContext(AccessContext);
const { context } = useUnleashContext();
@ -131,7 +138,7 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
const ON = uiConfig.flags[C];
return (
<div>
<div className={styles.accordionContainer}>
<ConditionallyRender
condition={ON}
show={
@ -140,12 +147,18 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
Constraints
</p>
{renderConstraints()}
<Button
className={styles.addConstraintBtn}
onClick={toggleConstraints}
>
+ Add constraint
</Button>
<ConditionallyRender
condition={hasAccess(UPDATE_FEATURE)}
show={
<Button
className={styles.addConstraintBtn}
onClick={toggleConstraints}
data-test={ADD_CONSTRAINT_ID}
>
+ Add constraint
</Button>
}
/>
</>
}
/>
@ -167,13 +180,20 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
setConstraintError={setConstraintError}
/>
</Dialogue>
<Type
parameters={parameters}
updateParameter={updateParameters}
strategyDefinition={definition}
context={context}
editable
<ConditionallyRender
condition={hasAccess(UPDATE_FEATURE)}
show={
<Type
parameters={parameters}
updateParameter={updateParameters}
strategyDefinition={definition}
context={context}
editable
/>
}
/>
{children}
</div>
);

View File

@ -1,15 +1,21 @@
import { IConstraint, IParameter } from '../../../../../interfaces/strategy';
import {
IConstraint,
IFeatureStrategy,
IParameter,
} from '../../../../../interfaces/strategy';
import FeatureStrategyExecution from '../FeatureStrategyExecution/FeatureStrategyExecution';
import { useStyles } from './FeatureStrategyCreateExecution.styles';
interface IFeatureStrategyCreateExecutionProps {
parameters: IParameter;
constraints: IConstraint[];
configureNewStrategy: IFeatureStrategy;
}
const FeatureStrategyCreateExecution = ({
parameters,
constraints,
configureNewStrategy,
}: IFeatureStrategyCreateExecutionProps) => {
const styles = useStyles();
return (
@ -18,6 +24,7 @@ const FeatureStrategyCreateExecution = ({
<FeatureStrategyExecution
parameters={parameters}
constraints={constraints}
strategy={configureNewStrategy}
/>
</div>
);

View File

@ -1,25 +1,37 @@
import { Fragment } from 'react';
import { IConstraint, IParameter } from '../../../../../interfaces/strategy';
import {
IConstraint,
IFeatureStrategy,
IParameter,
} from '../../../../../interfaces/strategy';
import ConditionallyRender from '../../../../common/ConditionallyRender';
import PercentageCircle from '../../../../common/PercentageCircle/PercentageCircle';
import FeatureStrategiesSeparator from '../FeatureStrategiesEnvironments/FeatureStrategiesSeparator/FeatureStrategiesSeparator';
import { useStyles } from './FeatureStrategyExecution.styles';
import FeatureStrategyExecutionConstraint from './FeatureStrategyExecutionConstraint/FeatureStrategyExecutionConstraint';
import FeatureStrategyExecutionChips from './FeatureStrategyExecutionChips/FeatureStrategyExecutionChips';
import useStrategies from '../../../../../hooks/api/getters/useStrategies/useStrategies';
interface IFeatureStrategiesExecutionProps {
parameters: IParameter;
constraints?: IConstraint[];
strategy: IFeatureStrategy;
}
const FeatureStrategyExecution = ({
parameters,
constraints = [],
strategy,
}: IFeatureStrategiesExecutionProps) => {
const styles = useStyles();
const { strategies } = useStrategies();
if (!parameters) return null;
const definition = strategies.find(strategyDefinition => {
return strategyDefinition.name === strategy.name;
});
const renderConstraints = () => {
return constraints.map((constraint, index) => {
if (index !== constraints.length - 1) {
@ -42,6 +54,8 @@ const FeatureStrategyExecution = ({
};
const renderParameters = () => {
if (definition?.editable) return null;
return Object.keys(parameters).map((key, index) => {
switch (key) {
case 'rollout':
@ -67,6 +81,7 @@ const FeatureStrategyExecution = ({
return (
<FeatureStrategyExecutionChips
key={key}
value={users}
text="user"
/>
@ -79,6 +94,7 @@ const FeatureStrategyExecution = ({
return (
<FeatureStrategyExecutionChips
key={key}
value={hosts}
text={'host'}
/>
@ -90,29 +106,152 @@ const FeatureStrategyExecution = ({
return (
<FeatureStrategyExecutionChips
key={key}
value={IPs}
text={'IP'}
/>
);
case 'stickiness':
case 'groupId':
return null;
default:
return null;
}
});
};
const renderCustomStrategy = () => {
if (!definition?.editable) return null;
return definition?.parameters.map((param: any, index: number) => {
const notLastItem = index !== definition?.parameters?.length - 1;
switch (param?.type) {
case 'list':
const values = strategy?.parameters[param.name]
.split(',')
.filter((val: string) => val);
return (
<Fragment key={param?.name}>
<FeatureStrategyExecutionChips
value={values}
text={param.name}
/>
<ConditionallyRender
condition={notLastItem}
show={<FeatureStrategiesSeparator text="AND" />}
/>
</Fragment>
);
case 'percentage':
return (
<Fragment key={param?.name}>
<p className={styles.text}>
{strategy?.parameters[param.name]}% of your user
base{' '}
{constraints?.length > 0
? 'who match constraints'
: ''}{' '}
are included.
</p>
<PercentageCircle
percentage={strategy.parameters[param.name]}
/>
<ConditionallyRender
condition={notLastItem}
show={<FeatureStrategiesSeparator text="AND" />}
/>
</Fragment>
);
case 'boolean':
return (
<Fragment key={param.name}>
<p className={styles.text} key={param.name}>
{param.name} must be{' '}
{strategy.parameters[param.name]}
</p>
<ConditionallyRender
condition={strategy.parameters[param.name]}
show={
<ConditionallyRender
condition={notLastItem}
show={
<FeatureStrategiesSeparator text="AND" />
}
/>
}
/>
</Fragment>
);
case 'string':
const numValue = strategy.parameters[param.name];
return (
<ConditionallyRender
condition={numValue !== undefined}
key={param.name}
show={
<>
<p className={styles.text}>
{param.name} is set to {numValue}
</p>
<ConditionallyRender
condition={notLastItem}
show={
<FeatureStrategiesSeparator text="AND" />
}
/>
</>
}
/>
);
case 'number':
const value = strategy.parameters[param.name];
return (
<ConditionallyRender
condition={value}
key={param.name}
show={
<>
<p className={styles.text}>
{param.name} is set to {value}
</p>
<ConditionallyRender
condition={notLastItem}
show={
<FeatureStrategiesSeparator text="AND" />
}
/>
</>
}
/>
);
case 'default':
return null;
}
return null;
});
};
return (
<>
<ConditionallyRender
condition={constraints.length > 0}
show={
<div className={styles.constraintsContainer}>
<p>Enabled for match:</p>
{renderConstraints()}
<FeatureStrategiesSeparator text="AND" />
</div>
<>
<div className={styles.constraintsContainer}>
<p>Enabled for match:</p>
{renderConstraints()}
<FeatureStrategiesSeparator text="AND" />
</div>
</>
}
/>
<ConditionallyRender
condition={strategy.name === 'default'}
show={<p>The standard strategy is on for all users.</p>}
/>
{renderParameters()}
{renderCustomStrategy()}
</>
);
};

View File

@ -7,5 +7,8 @@ export const useStyles = makeStyles(theme => ({
},
paragraph: {
margin: '0.25rem 0',
maxWidth: '95%',
textAlign: 'center',
wordBreak: 'break-word',
},
}));

View File

@ -0,0 +1,12 @@
import React from 'react';
import { IStrategy } from '../../../../../../interfaces/strategy';
interface IDefaultStrategyProps {
strategyDefinition: IStrategy;
}
const DefaultStrategy = ({ strategyDefinition }: IDefaultStrategyProps) => {
return <h6>{strategyDefinition?.description}</h6>;
};
export default DefaultStrategy;

View File

@ -6,6 +6,10 @@ import RolloutSlider from '../RolloutSlider/RolloutSlider';
import Select from '../../../../../common/select';
import React from 'react';
import Input from '../../../../../common/Input/Input';
import {
FLEXIBLE_STRATEGY_GROUP_ID,
FLEXIBLE_STRATEGY_STICKINESS_ID,
} from '../../../../../../testIds';
const builtInStickinessOptions = [
{ key: 'default', label: 'default' },
@ -64,6 +68,7 @@ const FlexibleStrategy = ({
value={1 * rollout}
onChange={updateRollout}
/>
<br />
<div>
<Tooltip title="Stickiness defines what parameter should be used to ensure that your users get consistency in features. By default unleash will use the first value present in the context in the order of userId, sessionId and random.">
@ -91,6 +96,7 @@ const FlexibleStrategy = ({
label="Stickiness"
options={stickinessOptions}
value={stickiness}
data-test={FLEXIBLE_STRATEGY_STICKINESS_ID}
onChange={e =>
onUpdate('stickiness')(e, e.target.value as number)
}
@ -121,6 +127,7 @@ const FlexibleStrategy = ({
label="groupId"
value={groupId || ''}
onChange={e => onUpdate('groupId')(e, e.target.value)}
data-test={FLEXIBLE_STRATEGY_GROUP_ID}
/>
</div>
</div>

View File

@ -100,7 +100,7 @@ const GeneralStrategy = ({
return (
<div key={name} className={styles.generalSection}>
<TextField
error={error !== undefined}
error={error}
helperText={error && `${name} is not a number!`}
variant="outlined"
size="small"

View File

@ -1,5 +1,6 @@
import { makeStyles, withStyles } from '@material-ui/core/styles';
import { Slider, Typography } from '@material-ui/core';
import { ROLLOUT_SLIDER_ID } from '../../../../../../testIds';
const StyledSlider = withStyles({
root: {
@ -32,7 +33,7 @@ const StyledSlider = withStyles({
const useStyles = makeStyles(theme => ({
slider: {
width: 450,
width: '100%',
maxWidth: '100%',
},
margin: {
@ -99,6 +100,7 @@ const RolloutSlider = ({
getAriaValueText={valuetext}
aria-labelledby="discrete-slider-always"
step={1}
data-test={ROLLOUT_SLIDER_ID}
marks={marks}
onChange={onChange}
valueLabelDisplay="on"

View File

@ -2,6 +2,10 @@ import React, { ChangeEvent, useState } from 'react';
import { Button, Chip, TextField, Typography } from '@material-ui/core';
import { Add } from '@material-ui/icons';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import {
ADD_TO_STRATEGY_INPUT_LIST,
STRATEGY_INPUT_LIST,
} from '../../../../../../testIds';
interface IStrategyInputList {
name: string;
@ -93,9 +97,11 @@ const StrategyInputList = ({
onBlur={onBlur}
onChange={onChange}
onKeyDown={onKeyDown}
data-test={STRATEGY_INPUT_LIST}
/>
<Button
onClick={setValue}
data-test={ADD_TO_STRATEGY_INPUT_LIST}
color="secondary"
startIcon={<Add />}
>

View File

@ -1,5 +1,6 @@
import { Tabs, Tab } from '@material-ui/core';
import { useParams } from 'react-router-dom';
import { useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
import useTabs from '../../../hooks/useTabs';
import { IFeatureViewParams } from '../../../interfaces/params';
@ -10,10 +11,19 @@ import FeatureViewEnvironment from './FeatureViewEnvironment/FeatureViewEnvironm
import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
const FeatureView2 = () => {
const { projectId, featureId } = useParams<IFeatureViewParams>();
const { projectId, featureId, activeTab } = useParams<IFeatureViewParams>();
const { feature } = useFeature(projectId, featureId);
const { a11yProps, activeTab, setActiveTab } = useTabs(0);
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(0);
const styles = useStyles();
const history = useHistory();
const basePath = `/projects/${projectId}/features2/${featureId}`;
useEffect(() => {
const tabIdx = tabData.findIndex(tab => tab.name === activeTab);
setActiveTab(tabIdx);
/* eslint-disable-next-line */
}, []);
const renderOverview = () => {
return (
@ -26,7 +36,7 @@ const FeatureView2 = () => {
width: '100%',
}}
>
{feature?.environments.map(env => {
{feature?.environments?.map(env => {
return (
<FeatureViewEnvironment env={env} key={env.name} />
);
@ -37,8 +47,18 @@ const FeatureView2 = () => {
};
const tabData = [
{ title: 'Overview', component: renderOverview() },
{ title: 'Strategies', component: <FeatureStrategies /> },
{
title: 'Overview',
component: renderOverview(),
path: `${basePath}/overview`,
name: 'overview',
},
{
title: 'Strategies',
component: <FeatureStrategies />,
path: `${basePath}/strategies`,
name: 'strategies',
},
];
const renderTabs = () => {
@ -48,7 +68,10 @@ const FeatureView2 = () => {
key={tab.title}
label={tab.title}
{...a11yProps(index)}
onClick={() => setActiveTab(index)}
onClick={() => {
setActiveTab(index);
history.push(tab.path);
}}
className={styles.tabButton}
/>
);
@ -58,7 +81,7 @@ const FeatureView2 = () => {
const renderTabContent = () => {
return tabData.map((tab, index) => {
return (
<TabPanel value={activeTab} index={index}>
<TabPanel value={activeTabIdx} index={index} key={tab.path}>
{tab.component}
</TabPanel>
);
@ -74,7 +97,7 @@ const FeatureView2 = () => {
<div className={styles.separator} />
<div className={styles.tabContainer}>
<Tabs
value={activeTab}
value={activeTabIdx}
onChange={(_, tabId) => {
setActiveTab(tabId);
}}

View File

@ -9,6 +9,7 @@ import InputListField from '../../../../common/input-list-field';
import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
import { useCommonStyles } from '../../../../../common.styles';
import { useStyles } from './StrategyConstraintInputField.styles';
import { CONSTRAINT_AUTOCOMPLETE_ID } from '../../../../../testIds';
const constraintOperators = [
{ key: 'IN', label: 'IN' },
@ -116,6 +117,7 @@ const StrategyConstraintInputField = ({
multiple
size="small"
options={options}
data-test={CONSTRAINT_AUTOCOMPLETE_ID}
value={values || []}
getOptionLabel={option => option.label}
onBlur={onBlur}

View File

@ -217,17 +217,7 @@ Array [
"layout": "main",
"menu": Object {},
"parent": "/projects",
"path": "/projects/:projectId/features2/:featureId/strategies",
"title": "FeatureView2",
"type": "protected",
},
Object {
"component": [Function],
"flags": "E",
"layout": "main",
"menu": Object {},
"parent": "/projects",
"path": "/projects/:projectId/features2/:featureId",
"path": "/projects/:projectId/features2/:featureId/:activeTab",
"title": "FeatureView2",
"type": "protected",
},

View File

@ -1,7 +1,7 @@
import { baseRoutes, getRoute } from '../routes';
test('returns all baseRoutes', () => {
expect(baseRoutes).toHaveLength(40);
expect(baseRoutes).toHaveLength(39);
expect(baseRoutes).toMatchSnapshot();
});

View File

@ -44,7 +44,6 @@ import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
import FeatureStrategies from '../feature/FeatureView2/FeatureStrategies/FeatureStrategies';
export const routes = [
// Features
@ -254,17 +253,7 @@ export const routes = [
menu: {},
},
{
path: '/projects/:projectId/features2/:featureId/strategies',
parent: '/projects',
title: 'FeatureView2',
component: FeatureStrategies,
type: 'protected',
layout: 'main',
flags: E,
menu: {},
},
{
path: '/projects/:projectId/features2/:featureId',
path: '/projects/:projectId/features2/:featureId/:activeTab',
parent: '/projects',
title: 'FeatureView2',
component: FeatureView2,

View File

@ -33,6 +33,7 @@ import HeaderTitle from '../../common/HeaderTitle';
import { useStyles } from './styles';
import AccessContext from '../../../contexts/AccessContext';
import Dialogue from '../../common/Dialogue';
import { ADD_NEW_STRATEGY_ID } from '../../../testIds';
const StrategiesList = ({
strategies,
@ -61,6 +62,7 @@ const StrategiesList = ({
show={
<Tooltip title="Add new strategy">
<IconButton
data-test={ADD_NEW_STRATEGY_ID}
onClick={() =>
history.push('/strategies/create')
}
@ -74,6 +76,7 @@ const StrategiesList = ({
onClick={() => history.push('/strategies/create')}
color="primary"
variant="contained"
data-test={ADD_NEW_STRATEGY_ID}
>
Add new strategy
</Button>

View File

@ -166,6 +166,7 @@ exports[`renders correctly with one strategy without permissions 1`] = `
>
<button
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-test="ADD_NEW_STRATEGY_ID"
disabled={false}
onBlur={[Function]}
onClick={[Function]}

View File

@ -5,6 +5,7 @@ import { Button, TextField } from '@material-ui/core';
import styles from './DemoAuth.module.scss';
import { ReactComponent as Logo } from '../../../assets/img/logo.svg';
import { LOGIN_BUTTON, LOGIN_EMAIL_ID } from '../../../testIds';
const DemoAuth = ({ demoLogin, history, authDetails }) => {
const [email, setEmail] = useState('');
@ -38,6 +39,7 @@ const DemoAuth = ({ demoLogin, history, authDetails }) => {
variant="outlined"
label="Email"
name="email"
data-test={LOGIN_EMAIL_ID}
required
type="email"
/>
@ -46,8 +48,8 @@ const DemoAuth = ({ demoLogin, history, authDetails }) => {
type="submit"
variant="contained"
color="primary"
data-test="login-submit"
className={styles.button}
data-test={LOGIN_BUTTON}
>
Sign in
</Button>

View File

@ -10,6 +10,11 @@ import useQueryParams from '../../../hooks/useQueryParams';
import AuthOptions from '../common/AuthOptions/AuthOptions';
import DividerText from '../../common/DividerText/DividerText';
import { Alert } from '@material-ui/lab';
import {
LOGIN_BUTTON,
LOGIN_PASSWORD_ID,
LOGIN_EMAIL_ID,
} from '../../../testIds';
const PasswordAuth = ({ authDetails, passwordLogin }) => {
const commonStyles = useCommonStyles();
@ -101,7 +106,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
variant="outlined"
autoComplete="true"
size="small"
data-test="LI_EMAIL_ID"
data-test={LOGIN_EMAIL_ID}
/>
<TextField
label="Password"
@ -114,14 +119,14 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
variant="outlined"
autoComplete="true"
size="small"
data-test="LI_PASSWORD_ID"
data-test={LOGIN_PASSWORD_ID}
/>
<Button
variant="contained"
color="primary"
type="submit"
style={{ width: '150px', margin: '1rem auto' }}
data-test="LI_BTN"
data-test={LOGIN_BUTTON}
>
Sign in
</Button>

View File

@ -1,14 +1,14 @@
import { useState } from 'react';
const useTabs = (startingIndex: number = 0) => {
const [activeTab, setActiveTab] = useState(startingIndex);
const [activeTabIdx, setActiveTab] = useState(startingIndex);
const a11yProps = (index: number) => ({
id: `tab-${index}`,
'aria-controls': `tabpanel-${index}`,
});
return { activeTab, setActiveTab, a11yProps };
return { activeTabIdx, setActiveTab, a11yProps };
};
export default useTabs;

View File

@ -1,4 +1,5 @@
export interface IFeatureViewParams {
projectId: string;
featureId: string;
activeTab: string;
}

View File

@ -11,6 +11,7 @@ export interface IStrategy {
editable: boolean;
deprecated: boolean;
description: string;
parameters: IParameter;
}
export interface IConstraint {
@ -31,5 +32,3 @@ export interface IStrategyPayload {
constraints: IConstraint[];
parameters: IParameter;
}

View File

@ -1,7 +1,34 @@
export const REPORTING_SELECT_ID = 'REPORTING_SELECT_ID';
/* NAVIGATION */
export const NAVIGATE_TO_CREATE_FEATURE = 'NAVIGATE_TO_CREATE_FEATURE';
/* CREATE FEATURE */
export const CF_NAME_ID = 'CF_NAME_ID';
export const CF_TYPE_ID = 'CF_TYPE_ID';
export const CF_DESC_ID = 'CF_DESC_ID';
export const CF_CREATE_BTN_ID = 'CF_CREATE_BTN_ID';
/* LOGIN */
export const LOGIN_EMAIL_ID = 'LOGIN_EMAIL_ID';
export const LOGIN_BUTTON = 'LOGIN_BUTTON';
export const LOGIN_PASSWORD_ID = 'LOGIN_PASSWORD_ID';
/* STRATEGY */
export const ADD_NEW_STRATEGY_ID = 'ADD_NEW_STRATEGY_ID';
export const ADD_NEW_STRATEGY_CARD_BUTTON_ID =
'ADD_NEW_STRATEGY_CARD_BUTTON_ID';
export const ROLLOUT_SLIDER_ID = 'ROLLOUT_SLIDER_ID';
export const ADD_NEW_STRATEGY_SAVE_ID = 'ADD_NEW_STRATEGY_SAVE_ID';
export const DIALOGUE_CONFIRM_ID = 'DIALOGUE_CONFIRM_ID';
export const ADD_CONSTRAINT_ID = 'ADD_CONSTRAINT_ID';
export const CONSTRAINT_AUTOCOMPLETE_ID = 'CONSTRAINT_AUTOCOMPLETE_ID';
export const STRATEGY_ACCORDION_ID = 'STRATEGY_ACCORDION_ID';
export const FLEXIBLE_STRATEGY_STICKINESS_ID =
'FLEXIBLE_STRATEGY_STICKINESS_ID';
export const FLEXIBLE_STRATEGY_GROUP_ID = 'FLEXIBLE_STRATEGY_GROUP_ID';
export const SELECT_ITEM_ID = 'SELECT_ITEM_ID';
export const UPDATE_STRATEGY_BUTTON_ID = 'UPDATE_STRATEGY_BUTTON_ID';
export const DELETE_STRATEGY_ID = 'DELETE_STRATEGY_ID';
export const STRATEGY_INPUT_LIST = 'STRATEGY_INPUT_LIST';
export const ADD_TO_STRATEGY_INPUT_LIST = 'ADD_TO_STRATEGY_INPUT_LIST';

View File

@ -0,0 +1,19 @@
import { resolveDefaultParamValue } from '../component/feature/strategy/AddStrategy/utils';
import { IStrategy, IParameter } from '../interfaces/strategy';
export const getStrategyObject = (
selectableStrategies: IStrategy[],
name: string,
featureId: string
) => {
const selectedStrategy = selectableStrategies.find(
strategy => strategy.name === name
);
const parameters = {} as IParameter;
selectedStrategy?.parameters.forEach(({ name }: IParameter) => {
parameters[name] = resolveDefaultParamValue(name, featureId);
});
return { name, parameters, constraints: [] };
};

View File

@ -1291,6 +1291,39 @@
resolved "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
"@cypress/request@^2.88.6":
version "2.88.6"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9"
integrity sha512-z0UxBE/+qaESAHY9p9sM2h8Y4XqtsbDCt0/DPOrqA/RZgKi4PkxdpXyK4wCCnSk1xHqWHZZAE+gV6aDAR6+caQ==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.5.0"
tunnel-agent "^0.6.0"
uuid "^8.3.2"
"@cypress/xvfb@^1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a"
integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==
dependencies:
debug "^3.1.0"
lodash.once "^4.1.1"
"@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
@ -2100,6 +2133,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.27.tgz#4141fcad57c332a120591de883e26fe4bb14aaea"
integrity sha512-qZdePUDSLAZRXXV234bLBEUM0nAQjoxbcSwp1rqSMUe1rZ47mwU6OjciR/JvF1Oo8mc0ys6GE0ks0HGgqAZoGg==
"@types/node@^14.14.31":
version "14.17.19"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.19.tgz#7341e9ac1b5d748d7a3ddc04336ed536a6f91c31"
integrity sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
@ -2196,6 +2234,16 @@
resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
"@types/sinonjs__fake-timers@^6.0.2":
version "6.0.4"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
"@types/sizzle@^2.3.2":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz"
@ -2277,6 +2325,13 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yauzl@^2.9.1":
version "2.9.2"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@^4.5.0":
version "4.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz"
@ -2673,7 +2728,7 @@ ansi-colors@^4.1.1:
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
ansi-escapes@^4.2.1, ansi-escapes@^4.3.1:
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1:
version "4.3.2"
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
@ -2753,6 +2808,11 @@ aproba@^1.1.1:
resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz"
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
arch@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz"
@ -2940,6 +3000,11 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
async@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
@ -3243,7 +3308,12 @@ bindings@^1.5.0:
dependencies:
file-uri-to-path "1.0.0"
bluebird@^3.5.5:
blob-util@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
bluebird@^3.5.5, bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@ -3421,6 +3491,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz"
@ -3524,6 +3599,11 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
cachedir@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
@ -3638,6 +3718,11 @@ char-regex@^1.0.2:
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
check-more-types@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
check-types@^11.1.1:
version "11.1.2"
resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz"
@ -3738,6 +3823,11 @@ ci-info@^2.0.0:
resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
ci-info@^3.1.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz"
@ -3778,6 +3868,31 @@ clean-stack@^2.0.0:
resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
dependencies:
restore-cursor "^3.1.0"
cli-table3@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
dependencies:
object-assign "^4.1.0"
string-width "^4.2.0"
optionalDependencies:
colors "^1.1.2"
cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
dependencies:
slice-ansi "^3.0.0"
string-width "^4.2.0"
cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz"
@ -3873,6 +3988,16 @@ colorette@^1.2.1, colorette@^1.2.2:
resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
colorette@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
colors@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
@ -3890,6 +4015,11 @@ commander@^4.1.1:
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
common-tags@^1.8.0:
version "1.8.0"
resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz"
@ -4447,6 +4577,53 @@ cyclist@^1.0.1:
resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@^8.4.1:
version "8.4.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.4.1.tgz#8b5898bf49359cadc28f02ba05d51f63b8e3a717"
integrity sha512-itJXq0Vx3sXCUrDyBi2IUrkxVu/gTTp1VhjB5tzGgkeCR8Ae+/T8WV63rsZ7fS8Tpq7LPPXiyoM/sEdOX7cR6A==
dependencies:
"@cypress/request" "^2.88.6"
"@cypress/xvfb" "^1.2.4"
"@types/node" "^14.14.31"
"@types/sinonjs__fake-timers" "^6.0.2"
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
blob-util "^2.0.2"
bluebird "^3.7.2"
cachedir "^2.3.0"
chalk "^4.1.0"
check-more-types "^2.24.0"
cli-cursor "^3.1.0"
cli-table3 "~0.6.0"
commander "^5.1.0"
common-tags "^1.8.0"
dayjs "^1.10.4"
debug "^4.3.2"
enquirer "^2.3.6"
eventemitter2 "^6.4.3"
execa "4.1.0"
executable "^4.1.1"
extract-zip "2.0.1"
figures "^3.2.0"
fs-extra "^9.1.0"
getos "^3.2.1"
is-ci "^3.0.0"
is-installed-globally "~0.4.0"
lazy-ass "^1.6.0"
listr2 "^3.8.3"
lodash "^4.17.21"
log-symbols "^4.0.0"
minimist "^1.2.5"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
ramda "~0.27.1"
request-progress "^3.0.0"
supports-color "^8.1.1"
tmp "~0.2.1"
untildify "^4.0.0"
url "^0.11.0"
yauzl "^2.10.0"
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz"
@ -4481,6 +4658,11 @@ date-fns@2.19.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
dayjs@^1.10.4:
version "1.10.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
debounce@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
@ -4493,7 +4675,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@^3.1.1, debug@^3.2.6:
debug@^3.1.0, debug@^3.1.1, debug@^3.2.6:
version "3.2.7"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@ -4507,6 +4689,13 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "2.1.2"
debug@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
@ -4946,7 +5135,7 @@ enhanced-resolve@^4.3.0, enhanced-resolve@^4.5.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enquirer@^2.3.5:
enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@ -5451,6 +5640,11 @@ etag@~1.8.1:
resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter2@^6.4.3:
version "6.4.4"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b"
integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==
eventemitter3@^4.0.0:
version "4.0.7"
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
@ -5481,20 +5675,7 @@ exec-sh@^0.3.2:
resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz"
integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz"
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
dependencies:
cross-spawn "^6.0.0"
get-stream "^4.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^4.0.0:
execa@4.1.0, execa@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz"
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
@ -5509,6 +5690,26 @@ execa@^4.0.0:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz"
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
dependencies:
cross-spawn "^6.0.0"
get-stream "^4.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
executable@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==
dependencies:
pify "^2.2.0"
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
@ -5616,6 +5817,17 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-zip@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
dependencies:
debug "^4.1.1"
get-stream "^5.1.0"
yauzl "^2.10.0"
optionalDependencies:
"@types/yauzl" "^2.9.1"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz"
@ -5674,6 +5886,13 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
fetch-mock@9.11.0:
version "9.11.0"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f"
@ -5695,6 +5914,13 @@ figgy-pudding@^3.5.1:
resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
figures@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
@ -5896,7 +6122,7 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^9.0.1:
fs-extra@^9.0.1, fs-extra@^9.1.0:
version "9.1.0"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
@ -6002,7 +6228,7 @@ get-stream@^4.0.0:
dependencies:
pump "^3.0.0"
get-stream@^5.0.0:
get-stream@^5.0.0, get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
@ -6014,6 +6240,13 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
getos@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==
dependencies:
async "^3.2.0"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz"
@ -6053,6 +6286,13 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
global-dirs@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==
dependencies:
ini "2.0.0"
global-modules@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz"
@ -6642,6 +6882,11 @@ inherits@2.0.3:
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
ini@^1.3.5:
version "1.3.8"
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
@ -6763,6 +7008,13 @@ is-ci@^2.0.0:
dependencies:
ci-info "^2.0.0"
is-ci@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994"
integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==
dependencies:
ci-info "^3.1.1"
is-color-stop@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz"
@ -6880,6 +7132,14 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3:
resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz"
integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=
is-installed-globally@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==
dependencies:
global-dirs "^3.0.0"
is-path-inside "^3.0.2"
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz"
@ -6936,6 +7196,11 @@ is-path-inside@^2.1.0:
dependencies:
path-is-inside "^1.0.2"
is-path-inside@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz"
@ -7020,6 +7285,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-unicode-supported@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz"
@ -7823,6 +8093,11 @@ last-call-webpack-plugin@^3.0.0:
lodash "^4.17.5"
webpack-sources "^1.1.0"
lazy-ass@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM=
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz"
@ -7849,6 +8124,19 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
listr2@^3.8.3:
version "3.12.2"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.12.2.tgz#2d55cc627111603ad4768a9e87c9c7bb9b49997e"
integrity sha512-64xC2CJ/As/xgVI3wbhlPWVPx0wfTqbUAkpb7bjDi0thSWMqrf07UFhrfsGoo8YSXmF049Rp9C0cjLC8rZxK9A==
dependencies:
cli-truncate "^2.1.0"
colorette "^1.4.0"
log-update "^4.0.0"
p-map "^4.0.0"
rxjs "^6.6.7"
through "^2.3.8"
wrap-ansi "^7.0.0"
load-json-file@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz"
@ -7969,6 +8257,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz"
@ -8004,6 +8297,24 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
dependencies:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
dependencies:
ansi-escapes "^4.3.0"
cli-cursor "^3.1.0"
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
loglevel@^1.6.8:
version "1.7.1"
resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz"
@ -8807,6 +9118,11 @@ os-browserify@^0.3.0:
resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
ospath@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
p-each-series@^2.1.0:
version "2.2.0"
resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz"
@ -9068,6 +9384,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
@ -9078,7 +9399,7 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
pify@^2.0.0:
pify@^2.0.0, pify@^2.2.0:
version "2.3.0"
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
@ -9884,7 +10205,7 @@ prepend-http@^1.0.0:
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
pretty-bytes@^5.3.0:
pretty-bytes@^5.3.0, pretty-bytes@^5.6.0:
version "5.6.0"
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
@ -10108,6 +10429,11 @@ railroad-diagrams@^1.0.0:
resolved "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz"
integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
ramda@~0.27.1:
version "0.27.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz"
@ -10631,6 +10957,13 @@ repeat-string@^1.6.1:
resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
request-progress@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
dependencies:
throttleit "^1.0.0"
request-promise-core@1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz"
@ -10772,6 +11105,14 @@ resolve@^2.0.0-next.3:
is-core-module "^2.2.0"
path-parse "^1.0.6"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
dependencies:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz"
@ -10894,6 +11235,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rxjs@^6.6.7:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
@ -11207,6 +11555,15 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz"
@ -11689,6 +12046,13 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
dependencies:
has-flag "^4.0.0"
supports-hyperlinks@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz"
@ -11871,6 +12235,11 @@ throat@^5.0.0:
resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
through2@^2.0.0:
version "2.0.5"
resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz"
@ -11879,6 +12248,11 @@ through2@^2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
thunky@^1.0.2:
version "1.1.0"
resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz"
@ -11906,6 +12280,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
dependencies:
rimraf "^3.0.0"
tmpl@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@ -12229,6 +12610,11 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
untildify@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
upath@^1.1.1, upath@^1.1.2, upath@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz"
@ -12328,7 +12714,7 @@ uuid@^3.3.2, uuid@^3.4.0:
resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.0:
uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
@ -12873,6 +13259,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
@ -12984,6 +13379,14 @@ yargs@^15.4.1:
y18n "^4.0.0"
yargs-parser "^18.1.2"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"