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:
parent
35b218b75a
commit
87414c1c9c
24
frontend/.github/workflows/e2e.yml
vendored
Normal file
24
frontend/.github/workflows/e2e.yml
vendored
Normal 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 }}
|
4
frontend/.github/workflows/node.js.yml
vendored
4
frontend/.github/workflows/node.js.yml
vendored
@ -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
3
frontend/.gitignore
vendored
@ -48,3 +48,6 @@ build
|
||||
.vscode/
|
||||
|
||||
.DS_Store
|
||||
|
||||
cypress/videos/*
|
||||
cypress/screenshots/*
|
@ -1 +1,2 @@
|
||||
CHANGELOG.md
|
||||
CHANGELOG.md
|
||||
.github/*
|
3
frontend/cypress.json
Normal file
3
frontend/cypress.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"projectId": "tc2qff"
|
||||
}
|
5
frontend/cypress/fixtures/example.json
Normal file
5
frontend/cypress/fixtures/example.json
Normal 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"
|
||||
}
|
245
frontend/cypress/integration/feature-toggle/feature.spec.js
Normal file
245
frontend/cypress/integration/feature-toggle/feature.spec.js
Normal 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');
|
||||
});
|
||||
});
|
22
frontend/cypress/plugins/index.js
Normal file
22
frontend/cypress/plugins/index.js
Normal 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
|
||||
}
|
25
frontend/cypress/support/commands.js
Normal file
25
frontend/cypress/support/commands.js
Normal 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) => { ... })
|
20
frontend/cypress/support/index.js
Normal file
20
frontend/cypress/support/index.js
Normal 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')
|
@ -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",
|
||||
|
1
frontend/src/assets/icons/addfiles.svg
Normal file
1
frontend/src/assets/icons/addfiles.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
@ -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>
|
||||
|
32
frontend/src/component/common/NoItems/NoItems.styles.ts
Normal file
32
frontend/src/component/common/NoItems/NoItems.styles.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}));
|
16
frontend/src/component/common/NoItems/NoItems.tsx
Normal file
16
frontend/src/component/common/NoItems/NoItems.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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]}
|
||||
|
@ -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',
|
||||
},
|
||||
}));
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -41,6 +41,7 @@ const FeatureEnvironmentStrategyExecutionWrapper = ({
|
||||
<FeatureStrategyExecution
|
||||
constraints={strategy.constraints}
|
||||
parameters={strategy.parameters}
|
||||
strategy={strategy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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',
|
||||
},
|
||||
}));
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
},
|
||||
}));
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
}));
|
||||
|
@ -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>
|
||||
|
@ -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%',
|
||||
},
|
||||
}));
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -7,5 +7,8 @@ export const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
paragraph: {
|
||||
margin: '0.25rem 0',
|
||||
maxWidth: '95%',
|
||||
textAlign: 'center',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
}));
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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 />}
|
||||
>
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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}
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { baseRoutes, getRoute } from '../routes';
|
||||
|
||||
test('returns all baseRoutes', () => {
|
||||
expect(baseRoutes).toHaveLength(40);
|
||||
expect(baseRoutes).toHaveLength(39);
|
||||
expect(baseRoutes).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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]}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface IFeatureViewParams {
|
||||
projectId: string;
|
||||
featureId: string;
|
||||
activeTab: string;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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';
|
||||
|
19
frontend/src/utils/get-strategy-object.ts
Normal file
19
frontend/src/utils/get-strategy-object.ts
Normal 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: [] };
|
||||
};
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user