1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

Merge branch 'master' into fix/breadcrumb-createToggle2

This commit is contained in:
Youssef Khedher 2022-01-07 08:20:52 +01:00 committed by GitHub
commit 90af5a40d6
19 changed files with 416 additions and 144 deletions

View File

@ -0,0 +1,34 @@
name: 'Release static assets to CDN'
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: yarn
- uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Deploy static assets to S3
run: |
aws s3 cp build/ s3://getunleash-static/unleash/${{ steps.get_version.outputs.VERSION }} --recursive

View File

@ -1,7 +1,7 @@
{ {
"name": "unleash-frontend", "name": "unleash-frontend",
"description": "unleash your features", "description": "unleash your features",
"version": "4.4.0", "version": "4.4.1",
"keywords": [ "keywords": [
"unleash", "unleash",
"feature toggle", "feature toggle",
@ -45,11 +45,11 @@
"@testing-library/user-event": "13.5.0", "@testing-library/user-event": "13.5.0",
"@types/debounce": "1.2.1", "@types/debounce": "1.2.1",
"@types/deep-diff": "1.0.1", "@types/deep-diff": "1.0.1",
"@types/enzyme": "3.10.10", "@types/enzyme": "3.10.11",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/jest": "27.0.3", "@types/jest": "27.4.0",
"@types/node": "14.18.1", "@types/node": "14.18.5",
"@types/react": "17.0.37", "@types/react": "17.0.38",
"@types/react-dom": "17.0.11", "@types/react-dom": "17.0.11",
"@types/react-router-dom": "5.3.2", "@types/react-router-dom": "5.3.2",
"@types/react-timeago": "4.1.3", "@types/react-timeago": "4.1.3",
@ -60,7 +60,7 @@
"craco": "0.0.3", "craco": "0.0.3",
"css-loader": "6.5.1", "css-loader": "6.5.1",
"cypress": "8.7.0", "cypress": "8.7.0",
"date-fns": "2.27.0", "date-fns": "2.28.0",
"debounce": "1.2.1", "debounce": "1.2.1",
"deep-diff": "1.0.2", "deep-diff": "1.0.2",
"enzyme": "3.11.0", "enzyme": "3.11.0",
@ -75,8 +75,8 @@
"node-fetch": "2.6.6", "node-fetch": "2.6.6",
"prettier": "2.5.1", "prettier": "2.5.1",
"react": "17.0.2", "react": "17.0.2",
"react-dnd": "14.0.4", "react-dnd": "14.0.5",
"react-dnd-html5-backend": "14.0.2", "react-dnd-html5-backend": "14.1.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-outside-click-handler": "1.3.0", "react-outside-click-handler": "1.3.0",
"react-redux": "7.2.6", "react-redux": "7.2.6",
@ -87,7 +87,7 @@
"redux-devtools-extension": "2.13.9", "redux-devtools-extension": "2.13.9",
"redux-mock-store": "1.5.4", "redux-mock-store": "1.5.4",
"redux-thunk": "2.4.1", "redux-thunk": "2.4.1",
"sass": "1.44.0", "sass": "1.46.0",
"swr": "1.0.1", "swr": "1.0.1",
"typescript": "4.5.4", "typescript": "4.5.4",
"web-vitals": "2.1.2" "web-vitals": "2.1.2"

View File

@ -5,6 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="baseUriPath" content="::baseUriPath::" /> <meta name="baseUriPath" content="::baseUriPath::" />
<meta name="cdnPrefix" content="::cdnPrefix::" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="unleash" /> <meta name="description" content="unleash" />
<title>Unleash - Enterprise ready feature toggles</title> <title>Unleash - Enterprise ready feature toggles</title>

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { TextField, Typography, Avatar } from '@material-ui/core'; import { Typography, Avatar } from '@material-ui/core';
import { trim } from '../../common/util'; import { trim } from '../../common/util';
import { modalStyles } from './util'; import { modalStyles } from './util';
import Dialogue from '../../common/Dialogue/Dialogue'; import Dialogue from '../../common/Dialogue/Dialogue';
@ -10,6 +10,7 @@ import { useCommonStyles } from '../../../common.styles';
import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher'; import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import PasswordField from '../../common/PasswordField/PasswordField';
function ChangePassword({ function ChangePassword({
showDialog, showDialog,
@ -109,26 +110,20 @@ function ChangePassword({
/> />
<p style={{ color: 'red' }}>{error.general}</p> <p style={{ color: 'red' }}>{error.general}</p>
<TextField <PasswordField
label="New password" label="New password"
name="password" name="password"
type="password"
value={data.password} value={data.password}
helperText={error.password} helperText={error.password}
onChange={updateField} onChange={updateField}
variant="outlined"
size="small"
/> />
<TextField <PasswordField
label="Confirm password" label="Confirm password"
name="confirm" name="confirm"
type="password"
value={data.confirm} value={data.confirm}
error={error.confirm !== undefined} error={error.confirm !== undefined}
helperText={error.confirm} helperText={error.confirm}
onChange={updateField} onChange={updateField}
variant="outlined"
size="small"
/> />
<PasswordMatcher <PasswordMatcher
started={data.password && data.confirm} started={data.password && data.confirm}

View File

@ -0,0 +1,44 @@
import { IconButton, InputAdornment, TextField } from '@material-ui/core';
import { Visibility, VisibilityOff } from '@material-ui/icons';
import { useState } from 'react';
const PasswordField = ({ ...rest }) => {
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleMouseDownPassword = (
e: React.MouseEvent<HTMLButtonElement>
) => {
e.preventDefault();
};
return (
<TextField
variant="outlined"
size="small"
type={showPassword ? 'text' : 'password'}
InputProps={{
style: {
paddingRight: '0px !important',
},
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
{...rest}
/>
);
};
export default PasswordField;

View File

@ -7,6 +7,7 @@ export const useStyles = makeStyles(theme => ({
position: 'relative', position: 'relative',
width: '50px', width: '50px',
height: '100%', height: '100%',
padding: '15px 0px',
}, },
vertical: { vertical: {
borderRadius: '1px', borderRadius: '1px',

View File

@ -3,7 +3,6 @@ import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
splashMainContainer: { splashMainContainer: {
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
height: '100%',
width: '100%', width: '100%',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',

View File

@ -19,6 +19,13 @@ export const useStyles = makeStyles(theme => ({
position: 'relative', position: 'relative',
paddingBottom: '1rem', paddingBottom: '1rem',
}, },
header: {
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
paddingTop: '1.5rem',
},
headerTitle: { headerTitle: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -70,8 +77,8 @@ export const useStyles = makeStyles(theme => ({
alignItems: 'center', alignItems: 'center',
}, },
percentageContainer: { percentageContainer: {
width: '50px', width: '90px',
height: '50px', height: '90px',
border: `2px solid ${theme.palette.primary.light}`, border: `2px solid ${theme.palette.primary.light}`,
borderRadius: '50%', borderRadius: '50%',
display: 'flex', display: 'flex',
@ -95,6 +102,7 @@ export const useStyles = makeStyles(theme => ({
requestText: { requestText: {
textAlign: 'center', textAlign: 'center',
marginTop: '1rem', marginTop: '1rem',
fontSize: theme.fontSizes.smallBody,
}, },
linkContainer: { linkContainer: {
display: 'flex', display: 'flex',
@ -114,6 +122,29 @@ export const useStyles = makeStyles(theme => ({
fill: theme.palette.grey[400], fill: theme.palette.grey[400],
marginBottom: '1rem', marginBottom: '1rem',
}, },
strategiesText: {
fontSize: '14px',
color: theme.palette.grey[700],
},
stratigiesInfoContainer: {
display: 'flex',
},
noStratigiesInfoContainer: {
top: '1px',
position: 'relative',
},
stratigiesIconsContainer: {
display: 'flex',
alignItems: 'center',
transform: 'scale(0.8)',
top: '3px',
left: '-10px',
position: 'relative',
[theme.breakpoints.down(560)]: {
marginLeft: '0px',
top: '5px',
},
},
[theme.breakpoints.down(750)]: { [theme.breakpoints.down(750)]: {
accordionBodyFooter: { accordionBodyFooter: {
flexDirection: 'column', flexDirection: 'column',
@ -124,7 +155,7 @@ export const useStyles = makeStyles(theme => ({
}, },
[theme.breakpoints.down(560)]: { [theme.breakpoints.down(560)]: {
disabledIndicatorPos: { disabledIndicatorPos: {
top: '-8px', top: '13px',
}, },
headerTitle: { headerTitle: {
flexDirection: 'column', flexDirection: 'column',
@ -135,6 +166,9 @@ export const useStyles = makeStyles(theme => ({
truncator: { truncator: {
textAlign: 'center', textAlign: 'center',
}, },
resultContainer: {
flexWrap: 'wrap',
},
}, },
[theme.breakpoints.down(400)]: { [theme.breakpoints.down(400)]: {
accordionHeader: { accordionHeader: {
@ -145,4 +179,60 @@ export const useStyles = makeStyles(theme => ({
padding: '0.5rem', padding: '0.5rem',
}, },
}, },
strategyIconContainer: {
marginRight: '5px',
},
strategyIcon: {
fill: theme.palette.grey[600],
},
container: {
display: 'flex',
alignItems: 'center',
marginLeft: '1.3rem',
[theme.breakpoints.down(560)]: {
flexDirection: 'column',
marginLeft: '0',
},
},
addStrategyButton: {
background: 'none',
textDecoration: 'none',
boxShadow: 'none',
color: theme.palette.primary.main,
fontWeight: 'normal',
'&:hover': {
background: 'none',
textDecoration: 'none',
boxShadow: 'none',
color: theme.palette.primary.main,
fontWeight: 'normal',
},
'&:disabled': {
margin: '0px 16px',
height: '35px'
},
},
separtor: {
marginLeft: '-10px',
marginRight: '9px',
[theme.breakpoints.down(560)]: {
display: 'none',
},
},
resultContainer: {
display: 'flex',
width: '100%',
justifyContent: 'space-around',
},
dataContainer: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
padding: '0px 15px',
},
resultTitle: {
color: theme.palette.primary.main,
},
})); }));

View File

@ -2,24 +2,38 @@ import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Tooltip,
} from '@material-ui/core'; } from '@material-ui/core';
import { ExpandMore } from '@material-ui/icons'; import { ExpandMore } from '@material-ui/icons';
import React from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useHistory } from 'react-router-dom';
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature';
import useFeatureMetrics from '../../../../../../hooks/api/getters/useFeatureMetrics/useFeatureMetrics'; import useFeatureMetrics from '../../../../../../hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle'; import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle';
import { IFeatureViewParams } from '../../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { getFeatureMetrics } from '../../../../../../utils/get-feature-metrics'; import { getFeatureMetrics } from '../../../../../../utils/get-feature-metrics';
import {
getFeatureStrategyIcon,
getHumanReadableStrategyName,
} from '../../../../../../utils/strategy-names';
import ConditionallyRender from '../../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../../common/ConditionallyRender';
import DisabledIndicator from '../../../../../common/DisabledIndicator/DisabledIndicator'; import DisabledIndicator from '../../../../../common/DisabledIndicator/DisabledIndicator';
import EnvironmentIcon from '../../../../../common/EnvironmentIcon/EnvironmentIcon'; import EnvironmentIcon from '../../../../../common/EnvironmentIcon/EnvironmentIcon';
import PermissionButton from '../../../../../common/PermissionButton/PermissionButton';
import StringTruncator from '../../../../../common/StringTruncator/StringTruncator'; import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
import { useStyles } from './FeatureOverviewEnvironment.styles'; import { useStyles } from './FeatureOverviewEnvironment.styles';
import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody'; import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody';
import FeatureOverviewEnvironmentFooter from './FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter'; import FeatureOverviewEnvironmentFooter from './FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter';
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics'; import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
interface IStrategyIconObject {
count: number;
Icon: React.ReactElement;
name: string;
}
interface IFeatureOverviewEnvironmentProps { interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment; env: IFeatureEnvironment;
} }
@ -31,6 +45,7 @@ const FeatureOverviewEnvironment = ({
const { projectId, featureId } = useParams<IFeatureViewParams>(); const { projectId, featureId } = useParams<IFeatureViewParams>();
const { metrics } = useFeatureMetrics(projectId, featureId); const { metrics } = useFeatureMetrics(projectId, featureId);
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
const history = useHistory();
const featureMetrics = getFeatureMetrics(feature?.environments, metrics); const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
const environmentMetric = featureMetrics.find( const environmentMetric = featureMetrics.find(
@ -48,6 +63,29 @@ const FeatureOverviewEnvironment = ({
return `This environment is disabled, which means that none of your strategies are executing`; return `This environment is disabled, which means that none of your strategies are executing`;
}; };
const strategiesLink = `/projects/${projectId}/features2/${featureId}/strategies?environment=${featureEnvironment?.name}&addStrategy=true`;
const getStrategyIcons = () => {
const strategyObjects = featureEnvironment?.strategies.reduce(
(acc, current) => {
if (acc[current.name]) {
acc[current.name].count = acc[current.name].count + 1;
} else {
acc[current.name] = {
count: 1,
Icon: getFeatureStrategyIcon(current.name),
};
}
return acc;
},
{} as { [key: string]: IStrategyIconObject }
);
return Object.keys(strategyObjects).map(strategyName => {
return { ...strategyObjects[strategyName], name: strategyName };
});
};
return ( return (
<div className={styles.featureOverviewEnvironment}> <div className={styles.featureOverviewEnvironment}>
<Accordion style={{ boxShadow: 'none' }}> <Accordion style={{ boxShadow: 'none' }}>
@ -55,17 +93,76 @@ const FeatureOverviewEnvironment = ({
className={styles.accordionHeader} className={styles.accordionHeader}
expandIcon={<ExpandMore />} expandIcon={<ExpandMore />}
> >
<div className={styles.headerTitle} data-loading> <div className={styles.header} data-loading>
<EnvironmentIcon <div className={styles.headerTitle}>
enabled={env.enabled} <EnvironmentIcon
className={styles.headerIcon} enabled={env.enabled}
/> className={styles.headerIcon}
Feature toggle execution for&nbsp; />
<StringTruncator Feature toggle execution for&nbsp;
text={env.name} <StringTruncator
className={styles.truncator} text={env.name}
maxWidth="100" className={styles.truncator}
/> maxWidth="100"
/>
</div>
<div className={styles.container}>
<PermissionButton
permission={UPDATE_FEATURE}
onClick={() => history.push(strategiesLink)}
className={styles.addStrategyButton}
>
Add strategy
</PermissionButton>
<span className={styles.separtor}>|</span>
<ConditionallyRender
condition={
featureEnvironment?.strategies.length !== 0
}
show={
<div
className={
styles.stratigiesIconsContainer
}
>
{getStrategyIcons()?.map(
({ name, Icon }) => (
<Tooltip
title={getHumanReadableStrategyName(
name
)}
arrow
>
<div
className={
styles.strategyIconContainer
}
>
<Icon
className={
styles.strategyIcon
}
/>
</div>
</Tooltip>
)
)}
</div>
}
elseShow={
<div
className={
styles.noStratigiesInfoContainer
}
>
<p className={styles.strategiesText}>
No strategies defined on this toggle
</p>
</div>
}
/>
</div>
<ConditionallyRender <ConditionallyRender
condition={!env.enabled} condition={!env.enabled}
show={ show={

View File

@ -1,4 +1,4 @@
import { Link, useParams, useHistory } from 'react-router-dom'; import { useParams, useHistory } from 'react-router-dom';
import { IFeatureViewParams } from '../../../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../../../interfaces/params';
import ConditionallyRender from '../../../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../../../common/ConditionallyRender';
import NoItemsStrategies from '../../../../../../common/NoItems/NoItemsStrategies/NoItemsStrategies'; import NoItemsStrategies from '../../../../../../common/NoItems/NoItemsStrategies/NoItemsStrategies';
@ -6,6 +6,9 @@ import FeatureOverviewEnvironmentStrategies from '../FeatureOverviewEnvironmentS
import { useStyles } from '../FeatureOverviewEnvironment.styles'; import { useStyles } from '../FeatureOverviewEnvironment.styles';
import { IFeatureEnvironment } from '../../../../../../../interfaces/featureToggle'; import { IFeatureEnvironment } from '../../../../../../../interfaces/featureToggle';
import { UPDATE_FEATURE } from '../../../../../../providers/AccessProvider/permissions';
import ResponsiveButton from '../../../../../../common/ResponsiveButton/ResponsiveButton';
import { Add } from '@material-ui/icons';
interface IFeatureOverviewEnvironmentBodyProps { interface IFeatureOverviewEnvironmentBodyProps {
getOverviewText: () => string; getOverviewText: () => string;
@ -39,7 +42,14 @@ const FeatureOverviewEnvironmentBody = ({
show={ show={
<> <>
<div className={styles.linkContainer}> <div className={styles.linkContainer}>
<Link to={strategiesLink}>Edit strategies</Link> <ResponsiveButton
Icon={Add}
onClick={() => history.push(strategiesLink)}
maxWidth="700px"
permission={UPDATE_FEATURE}
>
Add strategy
</ResponsiveButton>
</div> </div>
<FeatureOverviewEnvironmentStrategies <FeatureOverviewEnvironmentStrategies
strategies={featureEnvironment?.strategies} strategies={featureEnvironment?.strategies}

View File

@ -1,12 +1,8 @@
import { Warning } from '@material-ui/icons';
import { import {
IFeatureEnvironment, IFeatureEnvironment,
IFeatureEnvironmentMetrics, IFeatureEnvironmentMetrics,
} from '../../../../../../../interfaces/featureToggle'; } from '../../../../../../../interfaces/featureToggle';
import { calculatePercentage } from '../../../../../../../utils/calculate-percentage'; import { calculatePercentage } from '../../../../../../../utils/calculate-percentage';
import ConditionallyRender from '../../../../../../common/ConditionallyRender';
import FeatureEnvironmentMetrics from '../../../FeatureEnvironmentMetrics/FeatureEnvironmentMetrics';
import { useStyles } from '../FeatureOverviewEnvironment.styles'; import { useStyles } from '../FeatureOverviewEnvironment.styles';
interface IFeatureOverviewEnvironmentFooterProps { interface IFeatureOverviewEnvironmentFooterProps {
@ -32,37 +28,41 @@ const FeatureOverviewEnvironmentFooter = ({
</div> </div>
<div className={styles.accordionBodyFooter}> <div className={styles.accordionBodyFooter}>
<ConditionallyRender <div className={styles.resultContainer}>
condition={env.enabled} <div className={styles.dataContainer}>
show={ <h3 className={styles.resultTitle}>Exposure</h3>
<FeatureEnvironmentMetrics metric={environmentMetric} /> <div className={styles.percentageContainer}>
} {environmentMetric?.yes}
elseShow={
<div className={styles.disabledInfo}>
<Warning className={styles.disabledIcon} />
<p>
As long as the environment is disabled, all
requests made for this feature toggle will
return false. Add a strategy and turn on the
environment to enable it for your users.
</p>
</div> </div>
} <p className={styles.requestText}>
/> Total exposure of the feature in the environment in
the last hour
<div className={styles.requestContainer}> </p>
Total requests {totalTraffic} </div>
<div className={styles.percentageContainer}> <div className={styles.dataContainer}>
{calculatePercentage( <h3 className={styles.resultTitle}>% exposure</h3>
totalTraffic, <div className={styles.percentageContainer}>
environmentMetric?.yes {calculatePercentage(
)} totalTraffic,
% environmentMetric?.yes
)}
%
</div>
<p className={styles.requestText}>
Total exposure of the feature in the environment in
the last hour
</p>
</div>
<div className={styles.dataContainer}>
<h3 className={styles.resultTitle}>Total requests</h3>
<div className={styles.percentageContainer}>
{environmentMetric?.yes + environmentMetric?.no}
</div>
<p className={styles.requestText}>
The total request of the feature in the environment
in the last hour
</p>
</div> </div>
<p className={styles.requestText}>
Received enabled for this feature in this environment in
the last hour.
</p>
</div> </div>
</div> </div>
</> </>

View File

@ -17,15 +17,18 @@ export const useStyles = makeStyles(theme => ({
width: '75px', width: '75px',
}, },
infoParagraph: { infoParagraph: {
maxWidth: '150px', maxWidth: '215px',
marginTop: '0.25rem', marginTop: '0.25rem',
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
}, },
percentage: { percentage: {
color: theme.palette.primary.light, color: theme.palette.primary.light,
textAlign: 'center', textAlign: 'right',
fontSize: theme.fontSizes.subHeader, fontSize: theme.fontSizes.subHeader,
}, },
percentageCircle: {
transform: 'scale(0.85)',
},
[theme.breakpoints.down(700)]: { [theme.breakpoints.down(700)]: {
infoParagraph: { infoParagraph: {
display: 'none', display: 'none',
@ -35,8 +38,6 @@ export const useStyles = makeStyles(theme => ({
icon: { icon: {
display: 'none', display: 'none',
}, },
},
[theme.breakpoints.down(400)]: {
percentageCircle: { percentageCircle: {
display: 'none', display: 'none',
}, },

View File

@ -27,7 +27,8 @@ const FeatureOverviewEnvironmentMetrics = ({
{percentage}% {percentage}%
</p> </p>
<p className={styles.infoParagraph} data-loading> <p className={styles.infoParagraph} data-loading>
No one has received this feature in the last hour. The feature has been requested <b>0 times</b> and
exposed<b> 0 times</b> in the last hour
</p> </p>
</div> </div>
<FiberManualRecord <FiberManualRecord
@ -44,8 +45,10 @@ const FeatureOverviewEnvironmentMetrics = ({
<div className={styles.info}> <div className={styles.info}>
<p className={styles.percentage}>{percentage}%</p> <p className={styles.percentage}>{percentage}%</p>
<p className={styles.infoParagraph}> <p className={styles.infoParagraph}>
{environmentMetric.yes} users have received the feature in The feature has been requested{' '}
the last hour. <b>{environmentMetric.yes + environmentMetric.no} times</b>{' '}
and exposed <b>{environmentMetric.yes} times</b> in the last
hour
</p> </p>
</div> </div>
<PercentageCircle <PercentageCircle

View File

@ -63,7 +63,7 @@ const FeatureStrategyExecution = ({
return ( return (
<Fragment key={key}> <Fragment key={key}>
<p className={styles.text}> <p className={styles.text}>
{parameters[key]}% of your user base{' '} {parameters[key]}% of your base{' '}
{constraints.length > 0 {constraints.length > 0
? 'who match constraints' ? 'who match constraints'
: ''}{' '} : ''}{' '}
@ -146,8 +146,7 @@ const FeatureStrategyExecution = ({
return ( return (
<Fragment key={param?.name}> <Fragment key={param?.name}>
<p className={styles.text}> <p className={styles.text}>
{strategy?.parameters[param.name]}% of your user {strategy?.parameters[param.name]}% of your base{' '}
base{' '}
{constraints?.length > 0 {constraints?.length > 0
? 'who match constraints' ? 'who match constraints'
: ''}{' '} : ''}{' '}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Grid, TextField, Typography } from '@material-ui/core'; import { Button, Grid, TextField, Typography } from '@material-ui/core';
@ -10,6 +10,7 @@ import AuthOptions from '../common/AuthOptions/AuthOptions';
import DividerText from '../../common/DividerText/DividerText'; import DividerText from '../../common/DividerText/DividerText';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import useUser from '../../../hooks/api/getters/useUser/useUser'; import useUser from '../../../hooks/api/getters/useUser/useUser';
import PasswordField from '../../common/PasswordField/PasswordField';
const HostedAuth = ({ authDetails, passwordLogin }) => { const HostedAuth = ({ authDetails, passwordLogin }) => {
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
@ -109,16 +110,13 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
variant="outlined" variant="outlined"
size="small" size="small"
/> />
<TextField <PasswordField
label="Password" label="Password"
onChange={evt => setPassword(evt.target.value)} onChange={evt => setPassword(evt.target.value)}
name="password" name="password"
type="password"
value={password} value={password}
error={!!passwordError} error={!!passwordError}
helperText={passwordError} helperText={passwordError}
variant="outlined"
size="small"
/> />
<Grid container> <Grid container>
<Button <Button

View File

@ -16,6 +16,7 @@ import {
LOGIN_EMAIL_ID, LOGIN_EMAIL_ID,
} from '../../../testIds'; } from '../../../testIds';
import useUser from '../../../hooks/api/getters/useUser/useUser'; import useUser from '../../../hooks/api/getters/useUser/useUser';
import PasswordField from '../../common/PasswordField/PasswordField';
const PasswordAuth = ({ authDetails, passwordLogin }) => { const PasswordAuth = ({ authDetails, passwordLogin }) => {
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
@ -111,22 +112,17 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
value={username} value={username}
error={!!usernameError} error={!!usernameError}
helperText={usernameError} helperText={usernameError}
variant="outlined"
autoComplete="true" autoComplete="true"
size="small"
data-test={LOGIN_EMAIL_ID} data-test={LOGIN_EMAIL_ID}
/> />
<TextField <PasswordField
label="Password" label="Password"
onChange={evt => setPassword(evt.target.value)} onChange={evt => setPassword(evt.target.value)}
name="password" name="password"
type="password"
value={password} value={password}
error={!!passwordError} error={!!passwordError}
helperText={passwordError} helperText={passwordError}
variant="outlined"
autoComplete="true" autoComplete="true"
size="small"
data-test={LOGIN_PASSWORD_ID} data-test={LOGIN_PASSWORD_ID}
/> />
<Button <Button

View File

@ -1,5 +1,5 @@
import { SyntheticEvent, useState } from 'react'; import { SyntheticEvent, useState } from 'react';
import { Button, TextField, Typography } from '@material-ui/core'; import { Button, Typography } from '@material-ui/core';
import classnames from 'classnames'; import classnames from 'classnames';
import { useStyles } from './EditProfile.styles'; import { useStyles } from './EditProfile.styles';
import { useCommonStyles } from '../../../../common.styles'; import { useCommonStyles } from '../../../../common.styles';
@ -16,6 +16,7 @@ import {
UNAUTHORIZED, UNAUTHORIZED,
} from '../../../../constants/statusCodes'; } from '../../../../constants/statusCodes';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import PasswordField from '../../../common/PasswordField/PasswordField';
interface IEditProfileProps { interface IEditProfileProps {
setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>; setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>;
@ -54,7 +55,7 @@ const EditProfile = ({
credentials: 'include', credentials: 'include',
}); });
handleResponse(res); handleResponse(res);
} catch (e) { } catch (e: any) {
setError(e); setError(e);
} }
} }
@ -112,27 +113,25 @@ const EditProfile = ({
callback={setValidPassword} callback={setValidPassword}
data-loading data-loading
/> />
<TextField <PasswordField
data-loading data-loading
variant="outlined"
size="small"
label="Password" label="Password"
type="password"
name="password" name="password"
value={password} value={password}
autoComplete="on" autoComplete="on"
onChange={e => setPassword(e.target.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
/> />
<TextField <PasswordField
data-loading data-loading
variant="outlined"
size="small"
label="Confirm password" label="Confirm password"
type="password"
name="confirmPassword" name="confirmPassword"
value={confirmPassword} value={confirmPassword}
autoComplete="on" autoComplete="on"
onChange={e => setConfirmPassword(e.target.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setConfirmPassword(e.target.value)
}
/> />
<PasswordMatcher <PasswordMatcher
data-loading data-loading

View File

@ -1,4 +1,4 @@
import { Button, TextField } from '@material-ui/core'; import { Button } from '@material-ui/core';
import classnames from 'classnames'; import classnames from 'classnames';
import { import {
SyntheticEvent, SyntheticEvent,
@ -17,6 +17,7 @@ import PasswordMatcher from './PasswordMatcher/PasswordMatcher';
import { useStyles } from './ResetPasswordForm.styles'; import { useStyles } from './ResetPasswordForm.styles';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import PasswordField from '../../../common/PasswordField/PasswordField';
interface IResetPasswordProps { interface IResetPasswordProps {
token: string; token: string;
@ -107,24 +108,22 @@ const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
styles.container styles.container
)} )}
> >
<TextField <PasswordField
variant="outlined"
size="small"
type="password"
placeholder="Password" placeholder="Password"
value={password || ''} value={password || ''}
onChange={e => setPassword(e.target.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
onFocus={() => setShowPasswordChecker(true)} onFocus={() => setShowPasswordChecker(true)}
autoComplete="password" autoComplete="password"
data-loading data-loading
/> />
<TextField <PasswordField
variant="outlined"
size="small"
type="password"
value={confirmPassword || ''} value={confirmPassword || ''}
placeholder="Confirm password" placeholder="Confirm password"
onChange={e => setConfirmPassword(e.target.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setConfirmPassword(e.target.value)
}
autoComplete="confirm-password" autoComplete="confirm-password"
data-loading data-loading
/> />

View File

@ -1991,10 +1991,10 @@
"@types/cheerio" "*" "@types/cheerio" "*"
"@types/react" "*" "@types/react" "*"
"@types/enzyme@3.10.10": "@types/enzyme@3.10.11":
version "3.10.10" version "3.10.11"
resolved "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.10.tgz" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.11.tgz#8924bd92cc63ac1843e215225dfa8f71555fe814"
integrity sha512-/D4wFhiEjUDfPu+j5FVK0g/jf7rqeEIpNfAI+kyxzLpw5CKO0drnW3W5NC38alIjsWgnyQ8pbuPF5+UD+vhVyg== integrity sha512-LEtC7zXsQlbGXWGcnnmOI7rTyP+i1QzQv4Va91RKXDEukLDaNyxu0rXlfMiGEhJwfgTPCTb0R+Pnlj//oM9e/w==
dependencies: dependencies:
"@types/cheerio" "*" "@types/cheerio" "*"
"@types/react" "*" "@types/react" "*"
@ -2084,10 +2084,10 @@
jest-diff "^26.0.0" jest-diff "^26.0.0"
pretty-format "^26.0.0" pretty-format "^26.0.0"
"@types/jest@27.0.3": "@types/jest@27.4.0":
version "27.0.3" version "27.4.0"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed"
integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg== integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==
dependencies: dependencies:
jest-diff "^27.0.0" jest-diff "^27.0.0"
pretty-format "^27.0.0" pretty-format "^27.0.0"
@ -2112,10 +2112,10 @@
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
"@types/node@14.18.1": "@types/node@14.18.5":
version "14.18.1" version "14.18.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.1.tgz#459886b51f52aa923dc06b9ea81cb8b1d733e9d3" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.5.tgz#0dd636fe7b2c6055cbed0d4ca3b7fb540f130a96"
integrity sha512-fTFWOFrgAkj737w1o0HLTIgisgYHnsZfeiqhG1Ltrf/iJjudEbUwetQAsfrtVE49JGwvpEzQR+EbMkIqG4227g== integrity sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A==
"@types/node@^14.14.31": "@types/node@^14.14.31":
version "14.17.19" version "14.17.19"
@ -2204,10 +2204,10 @@
"@types/scheduler" "*" "@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/react@17.0.37": "@types/react@17.0.38":
version "17.0.37" version "17.0.38"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/scheduler" "*" "@types/scheduler" "*"
@ -4655,10 +4655,10 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0" whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0" whatwg-url "^8.0.0"
date-fns@2.27.0: date-fns@2.28.0:
version "2.27.0" version "2.28.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.27.0.tgz#e1ff3c3ddbbab8a2eaadbb6106be2929a5a2d92b" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
integrity sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q== integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
dayjs@^1.10.4: dayjs@^1.10.4:
version "1.10.7" version "1.10.7"
@ -10479,17 +10479,17 @@ react-dev-utils@^11.0.3:
strip-ansi "6.0.0" strip-ansi "6.0.0"
text-table "0.2.0" text-table "0.2.0"
react-dnd-html5-backend@14.0.2: react-dnd-html5-backend@14.1.0:
version "14.0.2" version "14.1.0"
resolved "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
integrity sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw== integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==
dependencies: dependencies:
dnd-core "14.0.1" dnd-core "14.0.1"
react-dnd@14.0.4: react-dnd@14.0.5:
version "14.0.4" version "14.0.5"
resolved "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.4.tgz" resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed"
integrity sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg== integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==
dependencies: dependencies:
"@react-dnd/invariant" "^2.0.0" "@react-dnd/invariant" "^2.0.0"
"@react-dnd/shallowequal" "^2.0.0" "@react-dnd/shallowequal" "^2.0.0"
@ -11265,13 +11265,14 @@ sass-loader@^10.0.5:
schema-utils "^3.0.0" schema-utils "^3.0.0"
semver "^7.3.2" semver "^7.3.2"
sass@1.44.0: sass@1.46.0:
version "1.44.0" version "1.46.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.44.0.tgz#619aa0a2275c097f9af5e6b8fe8a95e3056430fb" resolved "https://registry.yarnpkg.com/sass/-/sass-1.46.0.tgz#923117049525236026a7ede69715580eb0fac751"
integrity sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw== integrity sha512-Z4BYTgioAOlMmo4LU3Ky2txR8KR0GRPLXxO38kklaYxgo7qMTgy+mpNN4eKsrXDTFlwS5vdruvazG4cihxHRVQ==
dependencies: dependencies:
chokidar ">=3.0.0 <4.0.0" chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0" immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@~1.2.4: sax@~1.2.4:
version "1.2.4" version "1.2.4"
@ -11607,6 +11608,11 @@ source-list-map@^2.0.0:
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
source-map-js@^0.6.2: source-map-js@^0.6.2:
version "0.6.2" version "0.6.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz"