mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
Merge branch 'main' into refactor/applications
This commit is contained in:
commit
f72756db00
@ -96,7 +96,7 @@ describe('feature toggle', () => {
|
|||||||
|
|
||||||
it('Can add a gradual rollout strategy to the development environment', () => {
|
it('Can add a gradual rollout strategy to the development environment', () => {
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
|
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
|
||||||
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
|
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
|
||||||
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-2').click();
|
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-2').click();
|
||||||
cy.get('[data-test=ROLLOUT_SLIDER_ID')
|
cy.get('[data-test=ROLLOUT_SLIDER_ID')
|
||||||
@ -139,7 +139,7 @@ describe('feature toggle', () => {
|
|||||||
|
|
||||||
it('can update a strategy in the development environment', () => {
|
it('can update a strategy in the development environment', () => {
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
|
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
|
||||||
cy.get('[data-test=STRATEGY_ACCORDION_ID-flexibleRollout').click();
|
cy.get('[data-test=STRATEGY_ACCORDION_ID-flexibleRollout').click();
|
||||||
|
|
||||||
cy.get('[data-test=ROLLOUT_SLIDER_ID')
|
cy.get('[data-test=ROLLOUT_SLIDER_ID')
|
||||||
@ -186,7 +186,7 @@ describe('feature toggle', () => {
|
|||||||
|
|
||||||
it('can delete a strategy in the development environment', () => {
|
it('can delete a strategy in the development environment', () => {
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
|
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
@ -205,7 +205,7 @@ describe('feature toggle', () => {
|
|||||||
|
|
||||||
it('Can add a userid strategy to the development environment', () => {
|
it('Can add a userid strategy to the development environment', () => {
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/strategies`);
|
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
|
||||||
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
|
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
|
||||||
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-3').click();
|
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-3').click();
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ describe('feature toggle', () => {
|
|||||||
const variantName = 'my-new-variant';
|
const variantName = 'my-new-variant';
|
||||||
const secondVariantName = 'my-second-variant';
|
const secondVariantName = 'my-second-variant';
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||||
@ -283,7 +283,7 @@ describe('feature toggle', () => {
|
|||||||
it('Can set weight to fixed value for one of the variants', () => {
|
it('Can set weight to fixed value for one of the variants', () => {
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
|
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
cy.get('[data-test=VARIANT_EDIT_BUTTON]').first().click();
|
cy.get('[data-test=VARIANT_EDIT_BUTTON]').first().click();
|
||||||
cy.get('[data-test=VARIANT_NAME_INPUT]')
|
cy.get('[data-test=VARIANT_NAME_INPUT]')
|
||||||
.children()
|
.children()
|
||||||
@ -319,7 +319,7 @@ describe('feature toggle', () => {
|
|||||||
it(`can delete variant`, () => {
|
it(`can delete variant`, () => {
|
||||||
const variantName = 'to-be-deleted';
|
const variantName = 'to-be-deleted';
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.visit(`/projects/default/features2/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
|
cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
|
||||||
cy.get('[data-test=VARIANT_NAME_INPUT]').type(variantName);
|
cy.get('[data-test=VARIANT_NAME_INPUT]').type(variantName);
|
||||||
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-frontend",
|
"name": "unleash-frontend",
|
||||||
"description": "unleash your features",
|
"description": "unleash your features",
|
||||||
"version": "4.7.0",
|
"version": "4.7.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
@ -45,12 +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.11",
|
|
||||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/node": "14.18.10",
|
"@types/node": "14.18.10",
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "17.0.39",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.11",
|
||||||
|
"@types/react-outside-click-handler": "^1.3.1",
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
"@types/react-test-renderer": "17.0.1",
|
"@types/react-test-renderer": "17.0.1",
|
||||||
"@types/react-timeago": "4.1.3",
|
"@types/react-timeago": "4.1.3",
|
||||||
@ -64,9 +63,6 @@
|
|||||||
"date-fns": "2.28.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-adapter-react-16": "1.15.6",
|
|
||||||
"enzyme-to-json": "3.6.2",
|
|
||||||
"fast-json-patch": "3.1.0",
|
"fast-json-patch": "3.1.0",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"http-proxy-middleware": "2.0.2",
|
"http-proxy-middleware": "2.0.2",
|
||||||
@ -75,6 +71,7 @@
|
|||||||
"lodash.flow": "3.5.0",
|
"lodash.flow": "3.5.0",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dnd": "14.0.5",
|
"react-dnd": "14.0.5",
|
||||||
"react-dnd-html5-backend": "14.1.0",
|
"react-dnd-html5-backend": "14.1.0",
|
||||||
@ -83,6 +80,7 @@
|
|||||||
"react-redux": "7.2.6",
|
"react-redux": "7.2.6",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "5.3.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
"react-test-renderer": "^16.14.0",
|
||||||
"react-timeago": "6.2.1",
|
"react-timeago": "6.2.1",
|
||||||
"redux": "4.1.2",
|
"redux": "4.1.2",
|
||||||
"redux-devtools-extension": "2.13.9",
|
"redux-devtools-extension": "2.13.9",
|
||||||
@ -95,12 +93,10 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js",
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js",
|
||||||
|
"\\.svg": "<rootDir>/src/__mocks__/svgMock.js",
|
||||||
"\\.(css|scss)$": "identity-obj-proxy"
|
"\\.(css|scss)$": "identity-obj-proxy"
|
||||||
},
|
}
|
||||||
"snapshotSerializers": [
|
|
||||||
"enzyme-to-json/serializer"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
2
frontend/src/__mocks__/svgMock.js
Normal file
2
frontend/src/__mocks__/svgMock.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export default 'SvgrURL'
|
||||||
|
export const ReactComponent = 'div'
|
@ -1,23 +1,19 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Paper } from '@material-ui/core';
|
import { Paper } from '@material-ui/core';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import CheckIcon from '@material-ui/icons/Check';
|
import CheckIcon from '@material-ui/icons/Check';
|
||||||
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import styles from './ReportCard.module.scss';
|
import styles from './ReportCard.module.scss';
|
||||||
import ReactTimeAgo from 'react-timeago';
|
import ReactTimeAgo from 'react-timeago';
|
||||||
|
import { IProjectHealthReport } from "../../../interfaces/project";
|
||||||
|
|
||||||
const ReportCard = ({
|
interface IReportCardProps {
|
||||||
health,
|
healthReport: IProjectHealthReport
|
||||||
activeCount,
|
}
|
||||||
staleCount,
|
|
||||||
potentiallyStaleCount,
|
export const ReportCard = ({ healthReport }: IReportCardProps) => {
|
||||||
lastUpdate,
|
const healthLessThan50 = healthReport.health < 50;
|
||||||
}) => {
|
const healthLessThan75 = healthReport.health < 75;
|
||||||
const healthLessThan50 = health < 50;
|
|
||||||
const healthLessThan75 = health < 75;
|
|
||||||
|
|
||||||
const healthClasses = classnames(styles.reportCardHealthRating, {
|
const healthClasses = classnames(styles.reportCardHealthRating, {
|
||||||
[styles.healthWarning]: healthLessThan75,
|
[styles.healthWarning]: healthLessThan75,
|
||||||
@ -27,21 +23,21 @@ const ReportCard = ({
|
|||||||
const renderActiveToggles = () => (
|
const renderActiveToggles = () => (
|
||||||
<>
|
<>
|
||||||
<CheckIcon className={styles.check} />
|
<CheckIcon className={styles.check} />
|
||||||
<span>{activeCount} active toggles</span>
|
<span>{healthReport.activeCount} active toggles</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderStaleToggles = () => (
|
const renderStaleToggles = () => (
|
||||||
<>
|
<>
|
||||||
<ReportProblemOutlinedIcon className={styles.danger} />
|
<ReportProblemOutlinedIcon className={styles.danger} />
|
||||||
<span>{staleCount} stale toggles</span>
|
<span>{healthReport.staleCount} stale toggles</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPotentiallyStaleToggles = () => (
|
const renderPotentiallyStaleToggles = () => (
|
||||||
<>
|
<>
|
||||||
<ReportProblemOutlinedIcon className={styles.danger} />
|
<ReportProblemOutlinedIcon className={styles.danger} />
|
||||||
<span>{potentiallyStaleCount} potentially stale toggles</span>
|
<span>{healthReport.potentiallyStaleCount} potentially stale toggles</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -52,14 +48,14 @@ const ReportCard = ({
|
|||||||
<h2 className={styles.header}>Health rating</h2>
|
<h2 className={styles.header}>Health rating</h2>
|
||||||
<div className={styles.reportCardHealthInnerContainer}>
|
<div className={styles.reportCardHealthInnerContainer}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={health > -1}
|
condition={healthReport.health > -1}
|
||||||
show={
|
show={
|
||||||
<div>
|
<div>
|
||||||
<p className={healthClasses}>{health}%</p>
|
<p className={healthClasses}>{healthReport.health}%</p>
|
||||||
<p className={styles.lastUpdate}>
|
<p className={styles.lastUpdate}>
|
||||||
Last updated:{' '}
|
Last updated:{' '}
|
||||||
<ReactTimeAgo
|
<ReactTimeAgo
|
||||||
date={lastUpdate}
|
date={healthReport.updatedAt}
|
||||||
live={false}
|
live={false}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
@ -73,12 +69,12 @@ const ReportCard = ({
|
|||||||
<ul className={styles.reportCardList}>
|
<ul className={styles.reportCardList}>
|
||||||
<li>
|
<li>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={activeCount}
|
condition={Boolean(healthReport.activeCount)}
|
||||||
show={renderActiveToggles}
|
show={renderActiveToggles}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={activeCount}
|
condition={Boolean(healthReport.activeCount)}
|
||||||
show={
|
show={
|
||||||
<p className={styles.reportCardActionText}>
|
<p className={styles.reportCardActionText}>
|
||||||
Also includes potentially stale toggles.
|
Also includes potentially stale toggles.
|
||||||
@ -88,7 +84,7 @@ const ReportCard = ({
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={staleCount}
|
condition={Boolean(healthReport.staleCount)}
|
||||||
show={renderStaleToggles}
|
show={renderStaleToggles}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
@ -101,13 +97,13 @@ const ReportCard = ({
|
|||||||
<ul className={styles.reportCardList}>
|
<ul className={styles.reportCardList}>
|
||||||
<li>
|
<li>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={potentiallyStaleCount}
|
condition={Boolean(healthReport.potentiallyStaleCount)}
|
||||||
show={renderPotentiallyStaleToggles}
|
show={renderPotentiallyStaleToggles}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={potentiallyStaleCount}
|
condition={Boolean(healthReport.potentiallyStaleCount)}
|
||||||
show={
|
show={
|
||||||
<p className={styles.reportCardActionText}>
|
<p className={styles.reportCardActionText}>
|
||||||
Review your feature toggles and delete
|
Review your feature toggles and delete
|
||||||
@ -126,9 +122,3 @@ const ReportCard = ({
|
|||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ReportCard.propTypes = {
|
|
||||||
features: PropTypes.array.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReportCard;
|
|
@ -1,19 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import ReportCard from './ReportCard';
|
|
||||||
import { filterByProject } from '../utils';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
const features = state.features.toJS();
|
|
||||||
|
|
||||||
const sameProject = filterByProject(ownProps.selectedProject);
|
|
||||||
const featuresByProject = features.filter(sameProject);
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: featuresByProject,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReportCardContainer = connect(mapStateToProps, null)(ReportCard);
|
|
||||||
|
|
||||||
export default ReportCardContainer;
|
|
@ -7,7 +7,7 @@ import { Checkbox } from '@material-ui/core';
|
|||||||
import CheckIcon from '@material-ui/icons/Check';
|
import CheckIcon from '@material-ui/icons/Check';
|
||||||
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import FeatureStatus from '../../../feature/FeatureView2/FeatureStatus/FeatureStatus';
|
import FeatureStatus from '../../../feature/FeatureView/FeatureStatus/FeatureStatus';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
pluralize,
|
pluralize,
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Select from '../common/select';
|
|
||||||
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
|
||||||
import ReportToggleList from './ReportToggleList/ReportToggleList';
|
|
||||||
|
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
|
|
||||||
import { formatProjectOptions } from './utils';
|
|
||||||
import { REPORTING_SELECT_ID } from '../../testIds';
|
|
||||||
|
|
||||||
import styles from './Reporting.module.scss';
|
|
||||||
import useHealthReport from '../../hooks/api/getters/useHealthReport/useHealthReport';
|
|
||||||
import ApiError from '../common/ApiError/ApiError';
|
|
||||||
import useQueryParams from '../../hooks/useQueryParams';
|
|
||||||
|
|
||||||
const Reporting = ({ projects }) => {
|
|
||||||
const [projectOptions, setProjectOptions] = useState([
|
|
||||||
{ key: 'default', label: 'Default' },
|
|
||||||
]);
|
|
||||||
const [selectedProject, setSelectedProject] = useState('default');
|
|
||||||
const { project, error, refetch } = useHealthReport(selectedProject);
|
|
||||||
|
|
||||||
const params = useQueryParams();
|
|
||||||
const projectId = params.get('project');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectId) {
|
|
||||||
return setSelectedProject(projectId);
|
|
||||||
}
|
|
||||||
setSelectedProject(projects[0].id);
|
|
||||||
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setProjectOptions(formatProjectOptions(projects));
|
|
||||||
}, [projects]);
|
|
||||||
|
|
||||||
const onChange = e => {
|
|
||||||
const { value } = e.target;
|
|
||||||
|
|
||||||
const selectedProject = projectOptions.find(
|
|
||||||
option => option.key === value
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedProject(selectedProject.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSelect = () => (
|
|
||||||
<div className={styles.projectSelector}>
|
|
||||||
<h1 className={styles.header}>Project</h1>
|
|
||||||
<Select
|
|
||||||
name="project"
|
|
||||||
className={styles.select}
|
|
||||||
options={projectOptions}
|
|
||||||
value={selectedProject}
|
|
||||||
onChange={onChange}
|
|
||||||
inputProps={{ ['data-testid']: REPORTING_SELECT_ID }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const multipleProjects = projects.length > 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={multipleProjects}
|
|
||||||
show={renderSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={error}
|
|
||||||
show={
|
|
||||||
<ApiError
|
|
||||||
data-loading
|
|
||||||
style={{ maxWidth: '500px', marginTop: '1rem' }}
|
|
||||||
onClick={refetch}
|
|
||||||
text={`Could not fetch health rating for ${selectedProject}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ReportCardContainer
|
|
||||||
health={project?.health}
|
|
||||||
staleCount={project?.staleCount}
|
|
||||||
activeCount={project?.activeCount}
|
|
||||||
potentiallyStaleCount={project?.potentiallyStaleCount}
|
|
||||||
selectedProject={selectedProject}
|
|
||||||
/>
|
|
||||||
<ReportToggleList
|
|
||||||
features={project.features}
|
|
||||||
selectedProject={selectedProject}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Reporting.propTypes = {
|
|
||||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
|
||||||
projects: PropTypes.array.isRequired,
|
|
||||||
features: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Reporting;
|
|
@ -1,176 +0,0 @@
|
|||||||
.header {
|
|
||||||
font-size: var(--h1-size);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--card-padding);
|
|
||||||
margin: var(--card-margin-y) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projectSelector {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
background-color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select select {
|
|
||||||
border: none;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select select:active {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ReportCard **/
|
|
||||||
|
|
||||||
.reportCardContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardHealthInnerContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardHealthRating {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardList {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardList li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardList li span {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
font-size: var(--p-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.check,
|
|
||||||
.danger {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardActionContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardActionText {
|
|
||||||
max-width: 300px;
|
|
||||||
font-size: var(--p-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportCardBtn {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.healthDanger {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.healthWarning {
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ReportToggleList **/
|
|
||||||
.reportToggleList {
|
|
||||||
width: 100%;
|
|
||||||
margin: var(--card-margin-y) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulkAction {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
font-size: var(--p-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortIcon {
|
|
||||||
margin-left: 8px;
|
|
||||||
transform: translateY(6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportToggleListHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: var(--default-border);
|
|
||||||
padding: 1rem var(--card-padding-x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportToggleListInnerContainer {
|
|
||||||
padding: var(--card-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportToggleListHeading {
|
|
||||||
font-size: var(--h1-size);
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 'bold';
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportingToggleTable {
|
|
||||||
width: 100%;
|
|
||||||
border-spacing: 0 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportingToggleTable th {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expired {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stale {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportStatus {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reportIcon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableRow {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { fetchFeatureToggles } from '../../store/feature-toggle/actions';
|
|
||||||
|
|
||||||
import Reporting from './Reporting.jsx';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
projects: state.projects.toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchFeatureToggles,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReportingContainer = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Reporting);
|
|
||||||
|
|
||||||
export default ReportingContainer;
|
|
@ -177,10 +177,3 @@ export const getDates = dateString => {
|
|||||||
|
|
||||||
export const filterByProject = selectedProject => feature =>
|
export const filterByProject = selectedProject => feature =>
|
||||||
feature.project === selectedProject;
|
feature.project === selectedProject;
|
||||||
|
|
||||||
export const isFeatureExpired = feature => {
|
|
||||||
const [date, now] = getDates(feature.createdAt);
|
|
||||||
const diff = getDiffInDays(date, now);
|
|
||||||
|
|
||||||
return expired(diff, feature.type);
|
|
||||||
};
|
|
||||||
|
@ -86,7 +86,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
|
|||||||
refetch();
|
refetch();
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
show: true,
|
title: 'Deleted successfully',
|
||||||
text: 'Successfully deleted API token.',
|
text: 'Successfully deleted API token.',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ApiTokenList from '../api-token/ApiTokenList/ApiTokenList';
|
import ApiTokenList from '../api-token/ApiTokenList/ApiTokenList';
|
||||||
|
|
||||||
import AdminMenu from '../admin-menu';
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
import usePermissions from '../../../hooks/usePermissions';
|
import usePermissions from '../../../hooks/usePermissions';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@ import useToast from '../../../hooks/useToast';
|
|||||||
|
|
||||||
const PasswordAuthSettings = () => {
|
const PasswordAuthSettings = () => {
|
||||||
|
|
||||||
const { setToastData } = useToast();
|
const { setToastData } = useToast();
|
||||||
const { config } = useAuthSettings('simple');
|
const { config } = useAuthSettings('simple');
|
||||||
const [disablePasswordAuth, setDisablePasswordAuth] = useState<boolean>(false);
|
const [disablePasswordAuth, setDisablePasswordAuth] = useState<boolean>(false);
|
||||||
const { updateSettings, errors, loading } = useAuthSettingsApi<ISimpleAuthSettings>('simple')
|
const { updateSettings, errors, loading } = useAuthSettingsApi<ISimpleAuthSettings>('simple')
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
@ -24,7 +24,7 @@ const PasswordAuthSettings = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisablePasswordAuth(!!config.disabled);
|
setDisablePasswordAuth(!!config.disabled);
|
||||||
}, [ config.disabled ]);
|
}, [ config.disabled ]);
|
||||||
|
|
||||||
if (!hasAccess(ADMIN)) {
|
if (!hasAccess(ADMIN)) {
|
||||||
return (
|
return (
|
||||||
@ -38,10 +38,10 @@ const PasswordAuthSettings = () => {
|
|||||||
setDisablePasswordAuth(!disablePasswordAuth);
|
setDisablePasswordAuth(!disablePasswordAuth);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = async evt => {
|
const onSubmit = async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings: ISimpleAuthSettings = { disabled: disablePasswordAuth };
|
const settings: ISimpleAuthSettings = { disabled: disablePasswordAuth };
|
||||||
await updateSettings(settings);
|
await updateSettings(settings);
|
||||||
@ -62,7 +62,7 @@ const PasswordAuthSettings = () => {
|
|||||||
});
|
});
|
||||||
setDisablePasswordAuth(config.disabled)
|
setDisablePasswordAuth(config.disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent=''>
|
<PageContent headerContent=''>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import AdminMenu from '../admin-menu';
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import GoogleAuth from './google-auth-container';
|
import GoogleAuth from './google-auth-container';
|
||||||
import SamlAuth from './saml-auth-container';
|
import SamlAuth from './saml-auth-container';
|
||||||
@ -22,7 +22,7 @@ function AdminAuthPage({ authenticationType, history }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
component: <PasswordAuthSettings />
|
component: <PasswordAuthSettings />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Google',
|
label: 'Google',
|
||||||
@ -34,34 +34,47 @@ function AdminAuthPage({ authenticationType, history }) {
|
|||||||
<div>
|
<div>
|
||||||
<AdminMenu history={history} />
|
<AdminMenu history={history} />
|
||||||
<PageContent headerContent="Single Sign-On">
|
<PageContent headerContent="Single Sign-On">
|
||||||
<ConditionallyRender condition={authenticationType === 'enterprise'}
|
<ConditionallyRender
|
||||||
show={
|
condition={authenticationType === 'enterprise'}
|
||||||
<TabNav tabData={tabs} />
|
show={<TabNav tabData={tabs} />}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender condition={authenticationType === 'open-source'}
|
<ConditionallyRender
|
||||||
|
condition={authenticationType === 'open-source'}
|
||||||
show={
|
show={
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
You are running the open-source version of Unleash. You have to use the Enterprise edition
|
You are running the open-source version of Unleash.
|
||||||
in order configure Single Sign-on.</Alert>
|
You have to use the Enterprise edition in order
|
||||||
|
configure Single Sign-on.
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender condition={authenticationType === 'demo'}
|
<ConditionallyRender
|
||||||
|
condition={authenticationType === 'demo'}
|
||||||
show={
|
show={
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
You are running Unleash in demo mode. You have to use the Enterprise edition
|
You are running Unleash in demo mode. You have to
|
||||||
in order configure Single Sign-on.</Alert>
|
use the Enterprise edition in order configure Single
|
||||||
|
Sign-on.
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender condition={authenticationType === 'custom'}
|
<ConditionallyRender
|
||||||
|
condition={authenticationType === 'custom'}
|
||||||
show={
|
show={
|
||||||
<Alert severity="warning">You have decided to use custom authentication type. You have to use the Enterprise edition
|
<Alert severity="warning">
|
||||||
in order configure Single Sign-on from the user interface.</Alert>
|
You have decided to use custom authentication type.
|
||||||
|
You have to use the Enterprise edition in order
|
||||||
|
configure Single Sign-on from the user interface.
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender condition={authenticationType === 'hosted'}
|
<ConditionallyRender
|
||||||
|
condition={authenticationType === 'hosted'}
|
||||||
show={
|
show={
|
||||||
<Alert severity="info">Your Unleash instance is managed by the Unleash team.</Alert>
|
<Alert severity="info">
|
||||||
|
Your Unleash instance is managed by the Unleash
|
||||||
|
team.
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import InvoiceList from './InvoiceList';
|
||||||
import InvoiceList from './invoice-container';
|
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
|
|
||||||
const InvoiceAdminPage = ({ history }) => {
|
const InvoiceAdminPage = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -24,9 +22,4 @@ const InvoiceAdminPage = ({ history }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
InvoiceAdminPage.propTypes = {
|
|
||||||
match: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InvoiceAdminPage;
|
export default InvoiceAdminPage;
|
122
frontend/src/component/admin/invoice/InvoiceList.tsx
Normal file
122
frontend/src/component/admin/invoice/InvoiceList.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Button,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew';
|
||||||
|
import { formatDateWithLocale } from '../../common/util';
|
||||||
|
import PageContent from '../../common/PageContent';
|
||||||
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import { formatApiPath } from '../../../utils/format-path';
|
||||||
|
import useInvoices from '../../../hooks/api/getters/useInvoices/useInvoices';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { IInvoice } from '../../../interfaces/invoice';
|
||||||
|
|
||||||
|
const PORTAL_URL = formatApiPath('api/admin/invoices/portal');
|
||||||
|
|
||||||
|
const InvoiceList = () => {
|
||||||
|
const { refetchInvoices, invoices } = useInvoices();
|
||||||
|
const [isLoaded, setLoaded] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetchInvoices();
|
||||||
|
setLoaded(true);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={invoices.length > 0}
|
||||||
|
show={
|
||||||
|
<PageContent
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
title="Invoices"
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
href={PORTAL_URL}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
endIcon={<OpenInNew />}
|
||||||
|
>
|
||||||
|
Billing portal
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Amount</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell>Due date</TableCell>
|
||||||
|
<TableCell>PDF</TableCell>
|
||||||
|
<TableCell>Link</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{invoices.map((item: IInvoice) => (
|
||||||
|
<TableRow
|
||||||
|
key={item.invoiceURL}
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
item.status === 'past-due'
|
||||||
|
? '#ff9194'
|
||||||
|
: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
{item.amountFomratted}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
{item.status}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
{item.dueDate &&
|
||||||
|
formatDateWithLocale(
|
||||||
|
item.dueDate,
|
||||||
|
location.locale
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
<a href={item.invoicePDF}>PDF</a>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={item.invoiceURL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Payment link
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
}
|
||||||
|
elseShow={<div>{isLoaded && 'No invoices to show.'}</div>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default InvoiceList;
|
@ -1,11 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Component from './invoice-list';
|
|
||||||
import { fetchInvoices } from '../../../store/e-admin-invoice/actions';
|
|
||||||
export default connect(
|
|
||||||
state => ({
|
|
||||||
location: state.settings.toJS().location || {},
|
|
||||||
invoices: state.invoiceAdmin.toJS(),
|
|
||||||
}),
|
|
||||||
{ fetchInvoices }
|
|
||||||
)(Component);
|
|
@ -1,88 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableHead,
|
|
||||||
TableBody,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
Button,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew';
|
|
||||||
import { formatDateWithLocale } from '../../common/util';
|
|
||||||
import PageContent from '../../common/PageContent';
|
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
|
||||||
import { formatApiPath } from '../../../utils/format-path';
|
|
||||||
|
|
||||||
const PORTAL_URL = formatApiPath('api/admin/invoices/portal');
|
|
||||||
|
|
||||||
function InvoiceList({
|
|
||||||
location,
|
|
||||||
fetchInvoices,
|
|
||||||
invoices,
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const [isLoaded, setLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchInvoices().finally(() => setLoaded(true));
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConditionallyRender condition={invoices.length > 0}
|
|
||||||
show={
|
|
||||||
<PageContent headerContent={<HeaderTitle title="Invoices" actions={
|
|
||||||
<Button href={PORTAL_URL} rel="noreferrer" target="_blank" endIcon={<OpenInNew />}>
|
|
||||||
Billing portal
|
|
||||||
</Button>} />}>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Amount</TableCell>
|
|
||||||
<TableCell>Status</TableCell>
|
|
||||||
<TableCell>Due date</TableCell>
|
|
||||||
<TableCell>PDF</TableCell>
|
|
||||||
<TableCell>Link</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{invoices.map(item => (
|
|
||||||
<TableRow key={item.invoiceURL} style={{backgroundColor: item.status === 'past-due' ? '#ff9194' : 'inherit'}}>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
{item.amountFomratted}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
{item.status}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
{ item.dueDate && formatDateWithLocale(
|
|
||||||
item.dueDate,
|
|
||||||
location.locale
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
<a href={item.invoicePDF}>PDF</a>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
<a href={item.invoiceURL} target="_blank" rel="noreferrer">Payment link</a>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</PageContent>} elseShow={<div>{isLoaded && "No invoices to show."}</div>} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InvoiceList.propTypes = {
|
|
||||||
location: PropTypes.object,
|
|
||||||
fetchInvoices: PropTypes.func.isRequired,
|
|
||||||
invoices: PropTypes.array.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InvoiceList;
|
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { Paper, Tabs, Tab } from '@material-ui/core';
|
import { Paper, Tabs, Tab } from '@material-ui/core';
|
||||||
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const navLinkStyle = {
|
const navLinkStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
@ -7,7 +7,7 @@ import ConditionallyRender from '../../../common/ConditionallyRender';
|
|||||||
import HeaderTitle from '../../../common/HeaderTitle';
|
import HeaderTitle from '../../../common/HeaderTitle';
|
||||||
import PageContent from '../../../common/PageContent';
|
import PageContent from '../../../common/PageContent';
|
||||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
import AdminMenu from '../../admin-menu';
|
import AdminMenu from '../../menu/AdminMenu';
|
||||||
import { useStyles } from './ProjectRoles.styles';
|
import { useStyles } from './ProjectRoles.styles';
|
||||||
import ProjectRoleList from './ProjectRoleList/ProjectRoleList';
|
import ProjectRoleList from './ProjectRoleList/ProjectRoleList';
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import UsersList from './UsersList/UsersList';
|
import UsersList from './UsersList/UsersList';
|
||||||
import AdminMenu from '../admin-menu';
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
import PageContent from '../../../component/common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
import ConditionallyRender from '../../../component/common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
|
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import HeaderTitle from '../../../component/common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import { Button } from '@material-ui/core';
|
import { Button } from '@material-ui/core';
|
||||||
import { useStyles } from './index.styles';
|
import { useStyles } from './UserAdmin.styles';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
const UsersAdmin = () => {
|
const UsersAdmin = () => {
|
@ -1,22 +1,29 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { TextField, Typography, Avatar } from '@material-ui/core';
|
import { TextField, 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';
|
||||||
import PasswordChecker from '../../user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
|
import PasswordChecker from '../../../../user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
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 { IUser } from '../../../../../interfaces/user';
|
||||||
|
|
||||||
function ChangePassword({
|
interface IChangePasswordProps {
|
||||||
|
showDialog: () => void;
|
||||||
|
closeDialog: () => void;
|
||||||
|
changePassword: () => void;
|
||||||
|
user: IUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangePassword = ({
|
||||||
showDialog,
|
showDialog,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
changePassword,
|
changePassword,
|
||||||
user = {},
|
user = {},
|
||||||
}) {
|
}: IChangePasswordProps) => {
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
const [error, setError] = useState({});
|
const [error, setError] = useState({});
|
||||||
const [validPassword, setValidPassword] = useState(false);
|
const [validPassword, setValidPassword] = useState(false);
|
||||||
@ -137,14 +144,6 @@ function ChangePassword({
|
|||||||
</form>
|
</form>
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
ChangePassword.propTypes = {
|
|
||||||
showDialog: PropTypes.bool.isRequired,
|
|
||||||
closeDialog: PropTypes.func.isRequired,
|
|
||||||
changePassword: PropTypes.func.isRequired,
|
|
||||||
validatePassword: PropTypes.func.isRequired,
|
|
||||||
user: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChangePassword;
|
export default ChangePassword;
|
@ -1,21 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
import Dialogue from '../../../../common/Dialogue/Dialogue';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import propTypes from 'prop-types';
|
import { REMOVE_USER_ERROR } from '../../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
||||||
import { REMOVE_USER_ERROR } from '../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import useLoading from '../../../hooks/useLoading';
|
import useLoading from '../../../../../hooks/useLoading';
|
||||||
import { Avatar, Typography } from '@material-ui/core';
|
import { Avatar, Typography } from '@material-ui/core';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../../../common.styles';
|
||||||
|
import { IUser } from '../../../../../interfaces/user';
|
||||||
|
|
||||||
const DelUserComponent = ({
|
interface IDeleteUserProps {
|
||||||
|
showDialog: () => void;
|
||||||
|
closeDialog: () => void;
|
||||||
|
user: IUser;
|
||||||
|
userLoading: boolean;
|
||||||
|
removeUser: () => void;
|
||||||
|
userApiErrors: Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteUser = ({
|
||||||
showDialog,
|
showDialog,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
user,
|
user,
|
||||||
userLoading,
|
userLoading,
|
||||||
removeUser,
|
removeUser,
|
||||||
userApiErrors,
|
userApiErrors,
|
||||||
}) => {
|
}: IDeleteUserProps) => {
|
||||||
const ref = useLoading(userLoading);
|
const ref = useLoading(userLoading);
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
|
|
||||||
@ -75,11 +84,4 @@ const DelUserComponent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DelUserComponent.propTypes = {
|
export default DeleteUser;
|
||||||
showDialog: propTypes.bool.isRequired,
|
|
||||||
closeDialog: propTypes.func.isRequired,
|
|
||||||
user: propTypes.object,
|
|
||||||
removeUser: propTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DelUserComponent;
|
|
@ -7,8 +7,8 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import ChangePassword from '../change-password-component';
|
import ChangePassword from './ChangePassword/ChangePassword';
|
||||||
import DelUser from '../del-user-component';
|
import DeleteUser from './DeleteUser/DeleteUser';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import AccessContext from '../../../../contexts/AccessContext';
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
@ -170,7 +170,7 @@ const UsersList = () => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(delUser)}
|
condition={Boolean(delUser)}
|
||||||
show={
|
show={
|
||||||
<DelUser
|
<DeleteUser
|
||||||
showDialog={delDialog}
|
showDialog={delDialog}
|
||||||
closeDialog={closeDelDialog}
|
closeDialog={closeDelDialog}
|
||||||
user={delUser}
|
user={delUser}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import UsersList from './UsersList';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
location: state.settings.toJS().location || {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Container = connect(mapStateToProps)(UsersList);
|
|
||||||
|
|
||||||
export default Container;
|
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CircularProgress } from '@material-ui/core';
|
import { CircularProgress } from '@material-ui/core';
|
||||||
import { Warning } from '@material-ui/icons';
|
import { Warning } from '@material-ui/icons';
|
||||||
@ -25,49 +25,45 @@ const Empty = () => (
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
class ClientStrategies extends Component {
|
const ClientStrategies = ({ fetchAll, applications }) => {
|
||||||
static propTypes = {
|
const [filter, setFilter] = useState('');
|
||||||
applications: PropTypes.array,
|
|
||||||
fetchAll: PropTypes.func.isRequired,
|
|
||||||
settings: PropTypes.object.isRequired,
|
|
||||||
updateSetting: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
this.props.fetchAll();
|
fetchAll();
|
||||||
|
}, [fetchAll]);
|
||||||
|
|
||||||
|
const filteredApplications = useMemo(() => {
|
||||||
|
const regExp = new RegExp(filter, 'i');
|
||||||
|
return filter
|
||||||
|
? applications?.filter(a => regExp.test(a.appName))
|
||||||
|
: applications;
|
||||||
|
}, [applications, filter]);
|
||||||
|
|
||||||
|
if (!filteredApplications) {
|
||||||
|
return <CircularProgress variant="indeterminate" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { applications } = this.props;
|
<>
|
||||||
|
<div className={commonStyles.searchField}>
|
||||||
if (!applications) {
|
<SearchField value={filter} updateValue={setFilter} />
|
||||||
return <CircularProgress variant="indeterminate" />;
|
</div>
|
||||||
}
|
<PageContent headerContent={<HeaderTitle title="Applications" />}>
|
||||||
return (
|
<div className={commonStyles.fullwidth}>
|
||||||
<>
|
{filteredApplications.length > 0 ? (
|
||||||
<div className={commonStyles.searchField}>
|
<AppsLinkList apps={filteredApplications} />
|
||||||
<SearchField
|
) : (
|
||||||
value={this.props.settings.filter}
|
<Empty />
|
||||||
updateValue={this.props.updateSetting.bind(
|
)}
|
||||||
this,
|
|
||||||
'filter'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<PageContent
|
</PageContent>
|
||||||
headerContent={<HeaderTitle title="Applications" />}
|
</>
|
||||||
>
|
);
|
||||||
<div className={commonStyles.fullwidth}>
|
};
|
||||||
{applications.length > 0 ? (
|
|
||||||
<AppsLinkList apps={applications} />
|
ClientStrategies.propTypes = {
|
||||||
) : (
|
applications: PropTypes.array,
|
||||||
<Empty />
|
fetchAll: PropTypes.func.isRequired,
|
||||||
)}
|
};
|
||||||
</div>
|
|
||||||
</PageContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ClientStrategies;
|
export default ClientStrategies;
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ApplicationList from './application-list-component';
|
import ApplicationList from './application-list-component';
|
||||||
import { fetchAll } from './../../store/application/actions';
|
import { fetchAll } from '../../store/application/actions';
|
||||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => ({
|
||||||
const applications = state.applications.get('list').toJS();
|
applications: state.applications.get('list').toJS(),
|
||||||
const settings = state.settings.toJS().application || {};
|
});
|
||||||
|
|
||||||
const regex = new RegExp(settings.filter, 'i');
|
const mapDispatchToProps = {
|
||||||
|
fetchAll,
|
||||||
return {
|
|
||||||
applications: settings.filter ? applications.filter(a => regex.test(a.appName)) : applications,
|
|
||||||
settings,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = { fetchAll, updateSetting: updateSettingForGroup('application') };
|
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, mapDispatchToProps)(ApplicationList);
|
const Container = connect(mapStateToProps, mapDispatchToProps)(ApplicationList);
|
||||||
|
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { fetchArchive, revive } from './../../store/archive/actions';
|
|
||||||
import ViewToggleComponent from '../feature/FeatureView/FeatureView';
|
|
||||||
import { fetchTags } from '../../store/feature-tags/actions';
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
(state, props) => ({
|
|
||||||
features: state.archive.get('list').toArray(),
|
|
||||||
featureToggle: state.archive
|
|
||||||
.get('list')
|
|
||||||
.toArray()
|
|
||||||
.find(toggle => toggle.name === props.featureToggleName),
|
|
||||||
tagTypes: state.tagTypes.toJS(),
|
|
||||||
user: state.user.toJS(),
|
|
||||||
featureTags: state.featureTags.toJS(),
|
|
||||||
activeTab: props.activeTab,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
fetchArchive,
|
|
||||||
revive,
|
|
||||||
fetchTags,
|
|
||||||
}
|
|
||||||
)(ViewToggleComponent);
|
|
@ -3,7 +3,7 @@ import classnames from 'classnames';
|
|||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
import { IFeatureViewParams } from '../../../interfaces/params';
|
||||||
import { IConstraint } from '../../../interfaces/strategy';
|
import { IConstraint } from '../../../interfaces/strategy';
|
||||||
import FeatureStrategiesSeparator from '../../feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesSeparator/FeatureStrategiesSeparator';
|
import FeatureStrategiesSeparator from '../../feature/FeatureView/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesSeparator/FeatureStrategiesSeparator';
|
||||||
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
|
||||||
|
@ -20,7 +20,7 @@ const EnvironmentStrategyDialog = ({
|
|||||||
}: IEnvironmentStrategyDialogProps) => {
|
}: IEnvironmentStrategyDialogProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const strategiesLink = `/projects/${projectId}/features2/${featureId}/strategies?environment=${environmentName}&addStrategy=true`;
|
const strategiesLink = `/projects/${projectId}/features/${featureId}/strategies?environment=${environmentName}&addStrategy=true`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { weightTypes } from '../feature/variant/enums';
|
import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVariantsList/AddFeatureVariant/enums';
|
||||||
import differenceInDays from 'date-fns/differenceInDays';
|
import differenceInDays from 'date-fns/differenceInDays';
|
||||||
|
|
||||||
const dateTimeOptions = {
|
const dateTimeOptions = {
|
||||||
@ -50,7 +50,7 @@ export const trim = value => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function updateWeight(variants, totalWeight) {
|
export function updateWeight(variants, totalWeight) {
|
||||||
if (variants.length === 0){
|
if (variants.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const variantMetadata = variants.reduce(
|
const variantMetadata = variants.reduce(
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
.header {
|
|
||||||
padding: var(--card-header-padding);
|
|
||||||
margin-bottom: var(--card-margin-y);
|
|
||||||
word-break: break-all;
|
|
||||||
border-bottom: var(--default-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: var(--h1-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.formButtons {
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supporting {
|
|
||||||
font-size: var(--caption-size);
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: var(--card-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container section {
|
|
||||||
margin: 1rem 0
|
|
||||||
}
|
|
||||||
|
|
||||||
.h6 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alpha {
|
|
||||||
color: rgba(0,0,0,.54);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inset {
|
|
||||||
background-color: rgb(250, 250, 250);
|
|
||||||
padding: var(--card-padding);
|
|
||||||
max-width: 650px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valueField {
|
|
||||||
width: 130px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legalValueButton {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formContainer {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
max-width: 650px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formContainer > *, .inset > * {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
@ -27,7 +26,7 @@ import useUnleashContext from '../../../hooks/api/getters/useUnleashContext/useU
|
|||||||
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
|
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
|
|
||||||
const ContextList = ({ removeContextField }) => {
|
const ContextList = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
@ -164,10 +163,4 @@ const ContextList = ({ removeContextField }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ContextList.propTypes = {
|
|
||||||
contextFields: PropTypes.array.isRequired,
|
|
||||||
removeContextField: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContextList;
|
export default ContextList;
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ContextList from './ContextList';
|
|
||||||
import { fetchContext, removeContextField } from '../../../store/context/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const list = state.context.toJS();
|
|
||||||
|
|
||||||
return {
|
|
||||||
contextFields: list,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
removeContextField: contextField => {
|
|
||||||
removeContextField(contextField)(dispatch);
|
|
||||||
},
|
|
||||||
fetchContext: () => fetchContext()(dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextList);
|
|
||||||
|
|
||||||
export default ContextFieldListContainer;
|
|
@ -1,311 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button, Chip, TextField, Switch, Typography } from '@material-ui/core';
|
|
||||||
import styles from './Context.module.scss';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { FormButtons, styles as commonStyles } from '../common';
|
|
||||||
import { trim } from '../common/util';
|
|
||||||
import PageContent from '../common/PageContent/PageContent';
|
|
||||||
import ConditionallyRender from '../common/ConditionallyRender';
|
|
||||||
import { Alert } from '@material-ui/lab';
|
|
||||||
import { Add } from '@material-ui/icons';
|
|
||||||
|
|
||||||
const sortIgnoreCase = (a, b) => {
|
|
||||||
a = a.toLowerCase();
|
|
||||||
b = b.toLowerCase();
|
|
||||||
if (a === b) return 0;
|
|
||||||
if (a > b) return 1;
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddContextComponent extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
contextField: props.contextField,
|
|
||||||
errors: {},
|
|
||||||
currentLegalValue: '',
|
|
||||||
dirty: false,
|
|
||||||
focusedLegalValue: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeydown = e => {
|
|
||||||
if (e.key === 'Enter' && this.state.focusedLegalValue) {
|
|
||||||
this.addLegalValue(e);
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
this.onSubmit(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener('keydown', this.handleKeydown);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener('keydown', this.handleKeydown);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (state.contextField.initial && !props.contextField.initial) {
|
|
||||||
return { contextField: props.contextField };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue = (field, value) => {
|
|
||||||
const { contextField } = this.state;
|
|
||||||
contextField[field] = value;
|
|
||||||
this.setState({ contextField, dirty: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
validateContextName = async name => {
|
|
||||||
const { errors } = this.state;
|
|
||||||
const { validateName, editMode } = this.props;
|
|
||||||
|
|
||||||
if (editMode) return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await validateName(name);
|
|
||||||
errors.name = undefined;
|
|
||||||
} catch (err) {
|
|
||||||
errors.name = err.message;
|
|
||||||
}
|
|
||||||
this.setState({ errors });
|
|
||||||
if (errors.name) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancel = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.props.history.push('/context');
|
|
||||||
};
|
|
||||||
|
|
||||||
onSubmit = async evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
const { contextField } = this.state;
|
|
||||||
|
|
||||||
const valid = await this.validateContextName(contextField.name);
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
this.props
|
|
||||||
.submit(contextField)
|
|
||||||
.then(() => this.props.history.push('/context'))
|
|
||||||
.catch(e =>
|
|
||||||
this.setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
errors: { api: e.toString() },
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateCurrentLegalValue = evt => {
|
|
||||||
this.setState({ currentLegalValue: trim(evt.target.value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
addLegalValue = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
const { contextField, currentLegalValue, errors } = this.state;
|
|
||||||
|
|
||||||
if (!currentLegalValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contextField.legalValues.indexOf(currentLegalValue) !== -1) {
|
|
||||||
errors.currentLegalValue = 'Duplicate legal value';
|
|
||||||
this.setState({ errors });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const legalValues = contextField.legalValues.concat(
|
|
||||||
trim(currentLegalValue)
|
|
||||||
);
|
|
||||||
contextField.legalValues = legalValues.sort(sortIgnoreCase);
|
|
||||||
this.setState({
|
|
||||||
contextField,
|
|
||||||
currentLegalValue: '',
|
|
||||||
errors: {},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
removeLegalValue = index => {
|
|
||||||
const { contextField } = this.state;
|
|
||||||
const legalValues = contextField.legalValues.filter(
|
|
||||||
(_, i) => i !== index
|
|
||||||
);
|
|
||||||
contextField.legalValues = legalValues;
|
|
||||||
this.setState({ contextField });
|
|
||||||
};
|
|
||||||
|
|
||||||
renderLegalValue = (value, index) => (
|
|
||||||
<Chip
|
|
||||||
key={`${value}:${index}`}
|
|
||||||
className={styles.chip}
|
|
||||||
onDelete={() => this.removeLegalValue(index)}
|
|
||||||
label={value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { contextField, errors } = this.state;
|
|
||||||
const { editMode } = this.props;
|
|
||||||
const submitText = editMode ? 'Update' : 'Create';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent headerContent="Create context field">
|
|
||||||
<div className={styles.supporting}>
|
|
||||||
Context fields are a basic building block used in Unleash to
|
|
||||||
control roll-out. They can be used together with strategy
|
|
||||||
constraints as part of the activation strategy evaluation.
|
|
||||||
</div>
|
|
||||||
<form onSubmit={this.onSubmit}>
|
|
||||||
<section className={styles.formContainer}>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={errors.api}
|
|
||||||
show={
|
|
||||||
<Alert severity="error">
|
|
||||||
{this.state.errors.api}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className={commonStyles.fullwidth}
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
defaultValue={contextField.name}
|
|
||||||
error={errors.name}
|
|
||||||
helperText={errors.name}
|
|
||||||
disabled={editMode}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onBlur={v =>
|
|
||||||
this.validateContextName(v.target.value)
|
|
||||||
}
|
|
||||||
onChange={v =>
|
|
||||||
this.setValue('name', trim(v.target.value))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className={commonStyles.fullwidth}
|
|
||||||
rowsMax={1}
|
|
||||||
label="Description"
|
|
||||||
error={errors.description}
|
|
||||||
helperText={errors.description}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
defaultValue={contextField.description}
|
|
||||||
onChange={v =>
|
|
||||||
this.setValue('description', v.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</section>
|
|
||||||
<section className={styles.inset}>
|
|
||||||
<h6 className={styles.h6}>Legal values</h6>
|
|
||||||
<p className={styles.alpha}>
|
|
||||||
By defining the legal values the Unleash Admin UI
|
|
||||||
will validate the user input. A concrete example
|
|
||||||
would be that we know all values for our
|
|
||||||
“environment” (local, development, stage,
|
|
||||||
production).
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<TextField
|
|
||||||
label="Value"
|
|
||||||
name="value"
|
|
||||||
className={styles.valueField}
|
|
||||||
onFocus={() =>
|
|
||||||
this.setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
focusedLegalValue: true,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onBlur={() =>
|
|
||||||
this.setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
focusedLegalValue: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
value={this.state.currentLegalValue}
|
|
||||||
error={!!errors.currentLegalValue}
|
|
||||||
helperText={errors.currentLegalValue}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onChange={this.updateCurrentLegalValue}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className={styles.legalValueButton}
|
|
||||||
startIcon={<Add />}
|
|
||||||
onClick={this.addLegalValue}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{contextField.legalValues.map(
|
|
||||||
this.renderLegalValue
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<br />
|
|
||||||
<section>
|
|
||||||
<Typography variant="subtitle1">
|
|
||||||
Custom stickiness (beta)
|
|
||||||
</Typography>
|
|
||||||
<p
|
|
||||||
className={classnames(
|
|
||||||
styles.alpha,
|
|
||||||
styles.formContainer
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
By enabling stickiness on this context field you can
|
|
||||||
use it together with the flexible-rollout strategy.
|
|
||||||
This will guarantee a consistent behavior for
|
|
||||||
specific values of this context field. PS! Not all
|
|
||||||
client SDK's support this feature yet!{' '}
|
|
||||||
<a
|
|
||||||
href="https://docs.getunleash.io/advanced/stickiness"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<Switch
|
|
||||||
label="Allow stickiness"
|
|
||||||
checked={contextField.stickiness}
|
|
||||||
value={contextField.stickiness}
|
|
||||||
onChange={() =>
|
|
||||||
this.setValue(
|
|
||||||
'stickiness',
|
|
||||||
!contextField.stickiness
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<div className={styles.formButtons}>
|
|
||||||
<FormButtons
|
|
||||||
submitText={submitText}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddContextComponent.propTypes = {
|
|
||||||
contextField: PropTypes.object.isRequired,
|
|
||||||
validateName: PropTypes.func.isRequired,
|
|
||||||
submit: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
editMode: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddContextComponent;
|
|
@ -12,16 +12,16 @@ import {
|
|||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { FileCopy } from '@material-ui/icons';
|
import { FileCopy } from '@material-ui/icons';
|
||||||
|
|
||||||
import { styles as commonStyles } from '../../../common';
|
import { styles as commonStyles } from '../../common';
|
||||||
import styles from './CopyFeature.module.scss';
|
import styles from './CopyFeature.module.scss';
|
||||||
|
|
||||||
import { trim } from '../../../common/util';
|
import { trim } from '../../common/util';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
import { getTogglePath } from '../../../utils/route-path-helpers';
|
||||||
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
||||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const CopyFeature = props => {
|
const CopyFeature = props => {
|
||||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||||
@ -96,13 +96,7 @@ const CopyFeature = props => {
|
|||||||
<p className={styles.text}>
|
<p className={styles.text}>
|
||||||
You are about to create a new feature toggle by cloning the
|
You are about to create a new feature toggle by cloning the
|
||||||
configuration of feature toggle
|
configuration of feature toggle
|
||||||
<Link
|
<Link to={getTogglePath(projectId, copyToggleName)}>
|
||||||
to={getTogglePath(
|
|
||||||
projectId,
|
|
||||||
copyToggleName,
|
|
||||||
uiConfig.flags.E
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{copyToggleName}
|
{copyToggleName}
|
||||||
</Link>
|
</Link>
|
||||||
. You must give the new feature toggle a unique name before
|
. You must give the new feature toggle a unique name before
|
@ -1,8 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import CopyFeatureComponent from './CopyFeature';
|
import CopyFeatureComponent from './CopyFeature';
|
||||||
import {
|
import { validateName } from '../../../store/feature-toggle/actions';
|
||||||
validateName
|
|
||||||
} from '../../../../store/feature-toggle/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
history: props.history,
|
history: props.history,
|
@ -1,15 +1,15 @@
|
|||||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import FeatureForm from '../FeatureForm/FeatureForm';
|
import FeatureForm from '../FeatureForm/FeatureForm';
|
||||||
import useFeatureForm from '../hooks/useFeatureForm';
|
import useFeatureForm from '../hooks/useFeatureForm';
|
||||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useToast from '../../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
import { CF_CREATE_BTN_ID } from '../../../../testIds';
|
import { CF_CREATE_BTN_ID } from '../../../testIds';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import UIContext from '../../../../contexts/UIContext';
|
import UIContext from '../../../contexts/UIContext';
|
||||||
|
|
||||||
const CreateFeature = () => {
|
const CreateFeature = () => {
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
@ -46,7 +46,7 @@ const CreateFeature = () => {
|
|||||||
const payload = getTogglePayload();
|
const payload = getTogglePayload();
|
||||||
try {
|
try {
|
||||||
await createFeatureToggle(project, payload);
|
await createFeatureToggle(project, payload);
|
||||||
history.push(`/projects/${project}/features2/${name}`);
|
history.push(`/projects/${project}/features/${name}`);
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Toggle created successfully',
|
title: 'Toggle created successfully',
|
||||||
text: 'Now you can start using your toggle.',
|
text: 'Now you can start using your toggle.',
|
@ -1,15 +1,15 @@
|
|||||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import FeatureForm from '../FeatureForm/FeatureForm';
|
import FeatureForm from '../FeatureForm/FeatureForm';
|
||||||
import useFeatureForm from '../hooks/useFeatureForm';
|
import useFeatureForm from '../hooks/useFeatureForm';
|
||||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useToast from '../../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
||||||
import { IFeatureViewParams } from '../../../../interfaces/params';
|
import { IFeatureViewParams } from '../../../interfaces/params';
|
||||||
import * as jsonpatch from 'fast-json-patch';
|
import * as jsonpatch from 'fast-json-patch';
|
||||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const EditFeature = () => {
|
const EditFeature = () => {
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
@ -53,7 +53,7 @@ const EditFeature = () => {
|
|||||||
const patch = createPatch();
|
const patch = createPatch();
|
||||||
try {
|
try {
|
||||||
await patchFeatureToggle(project, featureId, patch);
|
await patchFeatureToggle(project, featureId, patch);
|
||||||
history.push(`/projects/${project}/features2/${name}`);
|
history.push(`/projects/${project}/features/${name}`);
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Toggle updated successfully',
|
title: 'Toggle updated successfully',
|
||||||
text: 'Now you can start using your toggle.',
|
text: 'Now you can start using your toggle.',
|
@ -1,17 +0,0 @@
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles(theme => ({
|
|
||||||
bodyContainer: {
|
|
||||||
borderRadius: '12.5px',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
padding: '2rem',
|
|
||||||
},
|
|
||||||
formContainer: {
|
|
||||||
marginBottom: '1.5rem',
|
|
||||||
maxWidth: '350px',
|
|
||||||
},
|
|
||||||
nameInput: {
|
|
||||||
marginRight: `1.5rem`,
|
|
||||||
minWidth: `250px`
|
|
||||||
}
|
|
||||||
}));
|
|
@ -1,176 +0,0 @@
|
|||||||
import { useEffect, useState, FormEvent } from 'react';
|
|
||||||
import { useHistory, useParams } from 'react-router';
|
|
||||||
import { useStyles } from './FeatureCreate.styles';
|
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
|
||||||
import PageContent from '../../common/PageContent';
|
|
||||||
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
|
||||||
import { CardActions } from '@material-ui/core';
|
|
||||||
import FeatureTypeSelect from '../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
|
||||||
import {
|
|
||||||
CF_CREATE_BTN_ID,
|
|
||||||
CF_DESC_ID,
|
|
||||||
CF_NAME_ID,
|
|
||||||
CF_TYPE_ID,
|
|
||||||
} from '../../../testIds';
|
|
||||||
import { getTogglePath } from '../../../utils/route-path-helpers';
|
|
||||||
import { IFeatureToggleDTO } from '../../../interfaces/featureToggle';
|
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
|
||||||
import { FormButtons } from '../../common';
|
|
||||||
import useQueryParams from '../../../hooks/useQueryParams';
|
|
||||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import Input from '../../common/Input/Input';
|
|
||||||
import ProjectSelect from '../project-select-container';
|
|
||||||
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { trim } from '../../common/util';
|
|
||||||
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
|
||||||
|
|
||||||
const FeatureCreate = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const commonStyles = useCommonStyles();
|
|
||||||
const { projectId } = useParams<IFeatureViewParams>();
|
|
||||||
const params = useQueryParams();
|
|
||||||
const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi();
|
|
||||||
const history = useHistory();
|
|
||||||
const [toggle, setToggle] = useState<IFeatureToggleDTO>({
|
|
||||||
name: params.get('name') || '',
|
|
||||||
description: '',
|
|
||||||
type: 'release',
|
|
||||||
stale: false,
|
|
||||||
variants: [],
|
|
||||||
project: projectId,
|
|
||||||
archived: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [nameError, setNameError] = useState('');
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const { permissions } = useUser();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.onbeforeunload = () =>
|
|
||||||
'Data will be lost if you leave the page, are you sure?';
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
//@ts-ignore
|
|
||||||
window.onbeforeunload = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onCancel = () => history.push(`/projects/${projectId}`);
|
|
||||||
|
|
||||||
const validateName = async (featureToggleName: string) => {
|
|
||||||
if (featureToggleName.length > 0) {
|
|
||||||
try {
|
|
||||||
await validateFeatureToggleName(featureToggleName);
|
|
||||||
} catch (err: any) {
|
|
||||||
setNameError(
|
|
||||||
err && err.message ? err.message : 'Could not check name'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (evt: FormEvent<HTMLFormElement>) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
await validateName(toggle.name);
|
|
||||||
|
|
||||||
if(!toggle.name) {
|
|
||||||
setNameError('Name is not allowed to be empty');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nameError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createFeatureToggle(toggle.project, toggle)
|
|
||||||
history.push(getTogglePath(toggle.project, toggle.name, uiConfig.flags.E));
|
|
||||||
// Trigger
|
|
||||||
} catch (err) {
|
|
||||||
if(err instanceof Error) {
|
|
||||||
if (err.toString().includes('not allowed to be empty')) {
|
|
||||||
setNameError('Name is not allowed to be empty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setValue = (field: string, value: string) => {
|
|
||||||
setToggle({ ...toggle, [field]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
headerContent="Create feature toggle"
|
|
||||||
bodyClass={styles.bodyContainer}
|
|
||||||
>
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
<input type="hidden" name="project" value={projectId} />
|
|
||||||
<div className={styles.formContainer}>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
label="Name"
|
|
||||||
placeholder="Unique-name"
|
|
||||||
className={styles.nameInput}
|
|
||||||
name="name"
|
|
||||||
inputProps={{
|
|
||||||
'data-test': CF_NAME_ID,
|
|
||||||
}}
|
|
||||||
value={toggle.name}
|
|
||||||
error={Boolean(nameError)}
|
|
||||||
helperText={nameError}
|
|
||||||
onBlur={v => validateName(v.target.value)}
|
|
||||||
onChange={v => {
|
|
||||||
setValue('name', trim(v.target.value));
|
|
||||||
setNameError('');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<section className={styles.formContainer}>
|
|
||||||
<FeatureTypeSelect
|
|
||||||
value={toggle.type}
|
|
||||||
onChange={v => setValue('type', v.target.value)}
|
|
||||||
label={'Toggle type'}
|
|
||||||
id="feature-type-select"
|
|
||||||
editable
|
|
||||||
inputProps={{
|
|
||||||
'data-test': CF_TYPE_ID,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section className={styles.formContainer}>
|
|
||||||
<ProjectSelect
|
|
||||||
value={toggle.project}
|
|
||||||
onChange={v => setValue('project', v.target.value)}
|
|
||||||
filter={projectFilterGenerator({ permissions }, CREATE_FEATURE)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section className={styles.formContainer}>
|
|
||||||
<Input
|
|
||||||
className={commonStyles.fullWidth}
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
label="Description"
|
|
||||||
placeholder="A short description of the feature toggle"
|
|
||||||
value={toggle.description}
|
|
||||||
inputProps={{
|
|
||||||
'data-test': CF_DESC_ID,
|
|
||||||
}}
|
|
||||||
onChange={v => setValue('description', v.target.value)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<CardActions>
|
|
||||||
<FormButtons
|
|
||||||
submitText={'Create'}
|
|
||||||
primaryButtonTestId={CF_CREATE_BTN_ID}
|
|
||||||
onCancel={onCancel}
|
|
||||||
/>
|
|
||||||
</CardActions>
|
|
||||||
</form>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeatureCreate;
|
|
@ -1,16 +1,16 @@
|
|||||||
import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
|
||||||
import Input from '../../../common/Input/Input';
|
|
||||||
import { Button, FormControl, Switch, Typography } from '@material-ui/core';
|
import { Button, FormControl, Switch, Typography } from '@material-ui/core';
|
||||||
import { useStyles } from './FeatureForm.styles';
|
import { useStyles } from './FeatureForm.styles';
|
||||||
import FeatureTypeSelect from '../../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
||||||
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from '../../../../testIds';
|
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from '../../../testIds';
|
||||||
import useFeatureTypes from '../../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
import useFeatureTypes from '../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
|
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
|
||||||
import useUser from '../../../../hooks/api/getters/useUser/useUser';
|
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||||
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
|
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
||||||
import FeatureProjectSelect from '../../FeatureView2/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
|
import FeatureProjectSelect from '../FeatureView/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { trim } from '../../../common/util';
|
import { trim } from '../../common/util';
|
||||||
|
import Input from '../../common/Input/Input';
|
||||||
|
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
interface IFeatureToggleForm {
|
interface IFeatureToggleForm {
|
||||||
type: string;
|
type: string;
|
@ -15,8 +15,8 @@ import { styles as commonStyles } from '../../../common';
|
|||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus';
|
import FeatureStatus from '../../FeatureView/FeatureStatus/FeatureStatus';
|
||||||
import FeatureType from '../../FeatureView2/FeatureType/FeatureType';
|
import FeatureType from '../../FeatureView/FeatureType/FeatureType';
|
||||||
import useProjects from '../../../../hooks/api/getters/useProjects/useProjects';
|
import useProjects from '../../../../hooks/api/getters/useProjects/useProjects';
|
||||||
import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton';
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ const FeatureToggleListItem = ({
|
|||||||
condition={!isArchive}
|
condition={!isArchive}
|
||||||
show={
|
show={
|
||||||
<Link
|
<Link
|
||||||
to={getTogglePath(feature.project, name, flags.E)}
|
to={getTogglePath(feature.project, name)}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
commonStyles.listLink,
|
commonStyles.listLink,
|
||||||
commonStyles.truncate
|
commonStyles.truncate
|
||||||
|
@ -64,7 +64,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="listLink truncate"
|
className="listLink truncate"
|
||||||
href="/projects/default/features/Another/strategies"
|
href="/projects/default/features/Another"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -201,7 +201,7 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="listLink truncate"
|
className="listLink truncate"
|
||||||
href="/projects/undefined/features/Another/strategies"
|
href="/projects/undefined/features/Another"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
@ -9,8 +9,8 @@ import useToast from '../../../../hooks/useToast';
|
|||||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
import { SyntheticEvent } from 'react-router/node_modules/@types/react';
|
import { SyntheticEvent } from 'react-router/node_modules/@types/react';
|
||||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus';
|
import FeatureStatus from '../../FeatureView/FeatureStatus/FeatureStatus';
|
||||||
import FeatureType from '../../FeatureView2/FeatureType/FeatureType';
|
import FeatureType from '../../FeatureView/FeatureType/FeatureType';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import CreatedAt from './CreatedAt';
|
import CreatedAt from './CreatedAt';
|
||||||
import useProject from '../../../../hooks/api/getters/useProject/useProject';
|
import useProject from '../../../../hooks/api/getters/useProject/useProject';
|
||||||
@ -57,7 +57,7 @@ const FeatureToggleListNewItem = ({
|
|||||||
|
|
||||||
const onClick = (e: SyntheticEvent) => {
|
const onClick = (e: SyntheticEvent) => {
|
||||||
if (!ref.current?.contains(e.target)) {
|
if (!ref.current?.contains(e.target)) {
|
||||||
history.push(getTogglePath(projectId, name, uiConfig.flags.E));
|
history.push(getTogglePath(projectId, name));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,16 +2,20 @@ import { useParams } from 'react-router';
|
|||||||
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
import { useStyles } from './FeatureLog.styles';
|
import { useStyles } from './FeatureLog.styles';
|
||||||
import { IFeatureViewParams } from '../../../../interfaces/params';
|
import { IFeatureViewParams } from '../../../../interfaces/params';
|
||||||
import HistoryComponent from '../../../history/FeatureEventHistory';
|
import { FeatureEventHistory } from '../../../history/FeatureEventHistory/FeatureEventHistory';
|
||||||
|
|
||||||
const FeatureLog = () => {
|
const FeatureLog = () => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const { feature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
if (!feature.name) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<HistoryComponent toggleName={feature.name} />
|
<FeatureEventHistory toggleName={feature.name} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -63,7 +63,7 @@ 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 strategiesLink = `/projects/${projectId}/features/${featureId}/strategies?environment=${featureEnvironment?.name}&addStrategy=true`;
|
||||||
|
|
||||||
const getStrategyIcons = () => {
|
const getStrategyIcons = () => {
|
||||||
const strategyObjects = featureEnvironment?.strategies.reduce(
|
const strategyObjects = featureEnvironment?.strategies.reduce(
|
||||||
@ -81,6 +81,8 @@ const FeatureOverviewEnvironment = ({
|
|||||||
{} as { [key: string]: IStrategyIconObject }
|
{} as { [key: string]: IStrategyIconObject }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!strategyObjects) return [];
|
||||||
|
|
||||||
return Object.keys(strategyObjects).map(strategyName => {
|
return Object.keys(strategyObjects).map(strategyName => {
|
||||||
return { ...strategyObjects[strategyName], name: strategyName };
|
return { ...strategyObjects[strategyName], name: strategyName };
|
||||||
});
|
});
|
@ -22,7 +22,7 @@ const FeatureOverviewEnvironmentBody = ({
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const strategiesLink = `/projects/${projectId}/features2/${featureId}/strategies?environment=${featureEnvironment?.name}&addStrategy=true`;
|
const strategiesLink = `/projects/${projectId}/features/${featureId}/strategies?environment=${featureEnvironment?.name}`;
|
||||||
|
|
||||||
if (!featureEnvironment) return null;
|
if (!featureEnvironment) return null;
|
||||||
|
|
@ -13,8 +13,10 @@ const FeatureOverviewEnvironmentMetrics = ({
|
|||||||
}: IFeatureOverviewEnvironmentMetrics) => {
|
}: IFeatureOverviewEnvironmentMetrics) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
|
if (!environmentMetric) return null;
|
||||||
|
|
||||||
const total = environmentMetric.yes + environmentMetric.no;
|
const total = environmentMetric.yes + environmentMetric.no;
|
||||||
const percentage = calculatePercentage(total, environmentMetric.yes);
|
const percentage = calculatePercentage(total, environmentMetric?.yes);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!environmentMetric ||
|
!environmentMetric ||
|
@ -40,7 +40,7 @@ const FeatureOverviewEnvironmentStrategy = ({
|
|||||||
environmentId={environmentName}
|
environmentId={environmentName}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/projects/${projectId}/features2/${featureId}/strategies?environment=${environmentName}`}
|
to={`/projects/${projectId}/features/${featureId}/strategies?environment=${environmentName}`}
|
||||||
>
|
>
|
||||||
<Settings />
|
<Settings />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
@ -49,7 +49,7 @@ const FeatureOverviewMetaData = () => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
permission={UPDATE_FEATURE}
|
permission={UPDATE_FEATURE}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/projects/${projectId}/features2/${featureId}/settings`}
|
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||||
>
|
>
|
||||||
<Edit className={styles.editIcon} />
|
<Edit className={styles.editIcon} />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
@ -64,7 +64,7 @@ const FeatureOverviewMetaData = () => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
permission={UPDATE_FEATURE}
|
permission={UPDATE_FEATURE}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/projects/${projectId}/features2/${featureId}/settings`}
|
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||||
>
|
>
|
||||||
<Edit className={styles.editIcon} />
|
<Edit className={styles.editIcon} />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
@ -59,7 +59,7 @@ const FeatureSettingsProject = () => {
|
|||||||
setDirty(false);
|
setDirty(false);
|
||||||
setShowConfirmDialog(false);
|
setShowConfirmDialog(false);
|
||||||
history.replace(
|
history.replace(
|
||||||
`/projects/${newProject}/features2/${featureId}/settings`
|
`/projects/${newProject}/features/${featureId}/settings`
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setToastApiError(e.message);
|
setToastApiError(e.message);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user