mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-24 20:06:55 +01:00
Feat/material UI (#250)
Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai> Co-authored-by: Christopher Kolstad <git@chriswk.no>
This commit is contained in:
parent
335a0a3cc3
commit
dbed06f300
@ -38,13 +38,8 @@
|
|||||||
"prepublish": "npm run build"
|
"prepublish": "npm run build"
|
||||||
},
|
},
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@material-ui/core": "^4.11.3",
|
|
||||||
"@material-ui/icons": "^4.11.2",
|
|
||||||
"@material-ui/lab": "4.0.0-alpha.57",
|
"@material-ui/lab": "4.0.0-alpha.57",
|
||||||
"classnames": "^2.2.6",
|
|
||||||
"date-fns": "^2.17.0",
|
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||||
@ -52,12 +47,17 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
"@babel/plugin-transform-runtime": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.5",
|
||||||
"@babel/preset-react": "^7.9.4",
|
"@babel/preset-react": "^7.9.4",
|
||||||
|
"@material-ui/core": "^4.11.3",
|
||||||
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@material-ui/styles": "^4.11.3",
|
||||||
"array-move": "^2.2.1",
|
"array-move": "^2.2.1",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^25.3.0",
|
"babel-jest": "^25.3.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"css-loader": "^2.1.1",
|
"css-loader": "^2.1.1",
|
||||||
|
"date-fns": "^2.17.0",
|
||||||
"debounce": "^1.2.0",
|
"debounce": "^1.2.0",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"enzyme": "^3.9.0",
|
"enzyme": "^3.9.0",
|
||||||
@ -72,7 +72,8 @@
|
|||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"lodash": "^4.17.20",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.flow": "^3.5.0",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.5.3",
|
||||||
@ -84,11 +85,8 @@
|
|||||||
"react-dnd": "^11.1.3",
|
"react-dnd": "^11.1.3",
|
||||||
"react-dnd-html5-backend": "^11.1.3",
|
"react-dnd-html5-backend": "^11.1.3",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-mdl": "^2.1.0",
|
|
||||||
"react-modal": "^3.1.13",
|
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-select": "^3.1.0",
|
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^16.14.0",
|
||||||
"react-timeago": "^4.4.0",
|
"react-timeago": "^4.4.0",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
@ -100,6 +98,7 @@
|
|||||||
"toolbox-loader": "0.0.3",
|
"toolbox-loader": "0.0.3",
|
||||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||||
"webpack": "^4.17.1",
|
"webpack": "^4.17.1",
|
||||||
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
"webpack-cli": "^3.1.0",
|
"webpack-cli": "^3.1.0",
|
||||||
"webpack-dev-server": "^3.11.2",
|
"webpack-dev-server": "^3.11.2",
|
||||||
"whatwg-fetch": "^3.4.1"
|
"whatwg-fetch": "^3.4.1"
|
||||||
@ -121,5 +120,7 @@
|
|||||||
"testPathIgnorePatterns": [
|
"testPathIgnorePatterns": [
|
||||||
"/src/store/addons/__tests__/data.js"
|
"/src/store/addons/__tests__/data.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
frontend/src/__mocks__/react-mdl.js
vendored
37
frontend/src/__mocks__/react-mdl.js
vendored
@ -1,37 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
Card: 'react-mdl-Card',
|
|
||||||
CardActions: 'react-mdl-CardActions',
|
|
||||||
CardTitle: 'react-mdl-CardTitle',
|
|
||||||
CardText: 'react-mdl-CardText',
|
|
||||||
CardMenu: 'react-mdl-CardMenu',
|
|
||||||
DataTable: 'react-mdl-DataTable',
|
|
||||||
Drawer: 'react-mdl-Drawer',
|
|
||||||
Cell: 'react-mdl-Cell',
|
|
||||||
Chip: 'react-mdl-Chip',
|
|
||||||
Grid: 'react-mdl-Grid',
|
|
||||||
Button: 'react-mdl-Button',
|
|
||||||
FABButton: 'react-mdl-FABButton',
|
|
||||||
Icon: 'react-mdl-Icon',
|
|
||||||
IconButton: 'react-mdl-IconButton',
|
|
||||||
List: 'react-mdl-List',
|
|
||||||
ListItem: 'react-mdl-ListItem',
|
|
||||||
ListItemContent: 'react-mdl-ListItemContent',
|
|
||||||
ListItemAction: 'react-mdl-ListItemAction',
|
|
||||||
Menu: 'react-mdl-Menu',
|
|
||||||
MenuItem: 'react-mdl-MenuItem',
|
|
||||||
Navigation: 'react-mdl-Navigation',
|
|
||||||
ProgressBar: 'react-mdl-ProgressBar',
|
|
||||||
Switch: 'react-mdl-Switch',
|
|
||||||
Tab: 'react-mdl-Tab',
|
|
||||||
Tabs: 'react-mdl-Tabs',
|
|
||||||
TableHeader: 'react-mdl-TableHeader',
|
|
||||||
Textfield: 'react-mdl-Textfield',
|
|
||||||
FooterDropDownSection: 'react-mdl-FooterDropDownSection',
|
|
||||||
FooterSection: 'react-mdl-FooterSection',
|
|
||||||
FooterLinkList: 'react-mdl-FooterLinkList',
|
|
||||||
Tooltip: 'react-mdl-Tooltip',
|
|
||||||
Dialog: 'react-mdl-Dialog',
|
|
||||||
DialogTitle: 'react-mdl-DialogTitle',
|
|
||||||
DialogContent: 'react-mdl-DialogContent',
|
|
||||||
DialogActions: 'react-mdl-DialogActions',
|
|
||||||
};
|
|
||||||
12
frontend/src/__mocks__/react-modal.js
vendored
12
frontend/src/__mocks__/react-modal.js
vendored
@ -1,12 +0,0 @@
|
|||||||
// __mocks__/react-modal.js
|
|
||||||
const Modal = require('react-modal');
|
|
||||||
|
|
||||||
const oldFn = Modal.setAppElement;
|
|
||||||
Modal.setAppElement = element => {
|
|
||||||
if (element === '#app') {
|
|
||||||
// otherwise it will throw aria warnings.
|
|
||||||
return oldFn(document.createElement('div'));
|
|
||||||
}
|
|
||||||
oldFn(element);
|
|
||||||
};
|
|
||||||
module.exports = Modal;
|
|
||||||
@ -1,17 +1,74 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
html { height: 100%; overflow:auto; }
|
.skeleton {
|
||||||
body { height: 100%; }
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton::before {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
content-visibility: hidden;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 5000;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0) 0,
|
||||||
|
rgba(255, 255, 255, 0.2) 100%,
|
||||||
|
rgba(255, 255, 255, 0.5) 100%,
|
||||||
|
rgba(255, 255, 255, 0)
|
||||||
|
);
|
||||||
|
animation: shimmer 3s infinite;
|
||||||
|
content: "";
|
||||||
|
z-index: 5001;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* FONT SIZE */
|
/* FONT SIZE */
|
||||||
--h1-size: 1.25rem;
|
--h1-size: 1.25rem;
|
||||||
--p-size: 1.1rem;
|
--p-size: 1rem;
|
||||||
|
--caption-size: 0.9rem;
|
||||||
|
|
||||||
/* PADDING */
|
/* PADDING */
|
||||||
--card-padding: 2rem;
|
--card-padding: 2rem;
|
||||||
--card-padding-x: 2rem;
|
--card-padding-x: 2rem;
|
||||||
--card-padding-y: 2rem;
|
--card-padding-y: 2rem;
|
||||||
|
--card-header-padding: 1rem 2rem;
|
||||||
|
--drawer-padding: 1rem 1.5rem;
|
||||||
|
--page-padding: 2rem 0;
|
||||||
|
--list-header-padding: 1rem;
|
||||||
|
|
||||||
/* MARGIN */
|
/* MARGIN */
|
||||||
--card-margin-y: 1rem;
|
--card-margin-y: 1rem;
|
||||||
@ -21,6 +78,25 @@ body { height: 100%; }
|
|||||||
--success: #3bd86e;
|
--success: #3bd86e;
|
||||||
--danger: #d95e5e;
|
--danger: #d95e5e;
|
||||||
--warning: #d67c3d;
|
--warning: #d67c3d;
|
||||||
|
--drawer-link-active: #000;
|
||||||
|
--drawer-link-active-bg: #f1f1f1;
|
||||||
|
--drawer-link-inactive: #424242;
|
||||||
|
--primary: #607d8b;
|
||||||
|
|
||||||
|
/* WIDTHS */
|
||||||
|
--drawer-width: 250px;
|
||||||
|
--dropdownMenuWidth: 200px;
|
||||||
|
|
||||||
|
/* BOX SHADOWS */
|
||||||
|
--chip-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||||
|
0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
/* BORDERS */
|
||||||
|
--default-border: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -29,3 +105,22 @@ h2 {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiCardHeader-title {
|
||||||
|
font-size: var(--p-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
:root {
|
||||||
|
--drawer-padding: 0.75rem 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
35
frontend/src/common.styles.js
Normal file
35
frontend/src/common.styles.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useCommonStyles = makeStyles(theme => ({
|
||||||
|
contentSpacingY: {
|
||||||
|
'& > *': {
|
||||||
|
margin: '0.6rem 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentSpacingX: {
|
||||||
|
'& > *': {
|
||||||
|
margin: '0 0.8rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
margin: '1rem 0',
|
||||||
|
backgroundColor: theme.palette.division.main,
|
||||||
|
height: '3px',
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
flexRow: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
flexColumn: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
flexWrap: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
textCenter: {
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Card } from 'react-mdl';
|
import { Paper } from '@material-ui/core';
|
||||||
import PropTypes from 'prop-types';
|
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/conditionally-render';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { isFeatureExpired } from './utils';
|
import { isFeatureExpired } from '../utils';
|
||||||
|
|
||||||
import styles from './reporting.module.scss';
|
import styles from './ReportCard.module.scss';
|
||||||
|
|
||||||
const ReportCard = ({ features }) => {
|
const ReportCard = ({ features }) => {
|
||||||
const getActiveToggles = () => {
|
const getActiveToggles = () => {
|
||||||
@ -76,7 +76,7 @@ const ReportCard = ({ features }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.card}>
|
<Paper className={styles.card}>
|
||||||
<div className={styles.reportCardContainer}>
|
<div className={styles.reportCardContainer}>
|
||||||
<div className={styles.reportCardListContainer}>
|
<div className={styles.reportCardListContainer}>
|
||||||
<h2 className={styles.header}>Toggle report</h2>
|
<h2 className={styles.header}>Toggle report</h2>
|
||||||
@ -113,7 +113,7 @@ const ReportCard = ({ features }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--card-padding);
|
||||||
|
margin: var(--card-margin-y) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: var(--h1-size);
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ReportCard from './report-card';
|
import ReportCard from './ReportCard';
|
||||||
import { filterByProject } from './utils';
|
import { filterByProject } from '../utils';
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const features = state.features.toJS();
|
const features = state.features.toJS();
|
||||||
@ -1,18 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import { Paper, MenuItem } from '@material-ui/core';
|
||||||
import { Card, Menu, MenuItem } from 'react-mdl';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import ReportToggleListItem from './report-toggle-list-item';
|
import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem';
|
||||||
import ReportToggleListHeader from './report-toggle-list-header';
|
import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader';
|
||||||
import ConditionallyRender from '../common/conditionally-render';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import DropdownMenu from '../../common/dropdown-menu';
|
||||||
|
|
||||||
import { getObjectProperties, getCheckedState, applyCheckedToFeatures } from './utils';
|
import { getObjectProperties, getCheckedState, applyCheckedToFeatures } from '../utils';
|
||||||
|
|
||||||
import useSort from './useSort';
|
import useSort from '../useSort';
|
||||||
|
|
||||||
import styles from './reporting.module.scss';
|
import styles from './ReportToggleList.module.scss';
|
||||||
import { DropdownButton } from '../common';
|
|
||||||
|
|
||||||
/* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */
|
/* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */
|
||||||
const BULK_ACTIONS_ON = false;
|
const BULK_ACTIONS_ON = false;
|
||||||
@ -47,26 +46,20 @@ const ReportToggleList = ({ features, selectedProject }) => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
const renderBulkActionsMenu = () => (
|
const renderBulkActionsMenu = () => (
|
||||||
<span>
|
<DropdownMenu
|
||||||
<DropdownButton
|
id="bulk-actions"
|
||||||
className={classnames('mdl-button', styles.bulkAction)}
|
|
||||||
id="bulk_actions"
|
|
||||||
label="Bulk actions"
|
label="Bulk actions"
|
||||||
/>
|
renderOptions={() => (
|
||||||
<Menu
|
<>
|
||||||
target="bulk_actions"
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
onClick={() => console.log("Hi")}
|
|
||||||
style={{ width: '168px' }}
|
|
||||||
>
|
|
||||||
<MenuItem>Mark toggles as stale</MenuItem>
|
<MenuItem>Mark toggles as stale</MenuItem>
|
||||||
<MenuItem>Delete toggles</MenuItem>
|
<MenuItem>Delete toggles</MenuItem>
|
||||||
</Menu>
|
</>
|
||||||
</span>
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.reportToggleList}>
|
<Paper className={styles.reportToggleList}>
|
||||||
<div className={styles.reportToggleListHeader}>
|
<div className={styles.reportToggleListHeader}>
|
||||||
<h3 className={styles.reportToggleListHeading}>Overview</h3>
|
<h3 className={styles.reportToggleListHeading}>Overview</h3>
|
||||||
<ConditionallyRender condition={BULK_ACTIONS_ON} show={renderBulkActionsMenu} />
|
<ConditionallyRender condition={BULK_ACTIONS_ON} show={renderBulkActionsMenu} />
|
||||||
@ -83,7 +76,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
|
|||||||
<tbody>{renderListRows()}</tbody>
|
<tbody>{renderListRows()}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
.reportToggleList {
|
||||||
|
width: 100%;
|
||||||
|
margin: var(--card-margin-y) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulkAction {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: var(--p-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportToggleListHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
padding: 1rem var(--card-padding-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportToggleListInnerContainer {
|
||||||
|
padding: var(--card-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportToggleListHeading {
|
||||||
|
font-size: var(--h1-size);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { filterByProject } from './utils';
|
import { filterByProject } from '../utils';
|
||||||
|
|
||||||
import ReportToggleList from './report-toggle-list';
|
import ReportToggleList from './ReportToggleList';
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const features = state.features.toJS();
|
const features = state.features.toJS();
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Checkbox } from 'react-mdl';
|
import { Checkbox } from '@material-ui/core';
|
||||||
import UnfoldMoreOutlinedIcon from '@material-ui/icons/UnfoldMoreOutlined';
|
import UnfoldMoreOutlinedIcon from '@material-ui/icons/UnfoldMoreOutlined';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import ConditionallyRender from '../common/conditionally-render';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { NAME, LAST_SEEN, CREATED, EXPIRED, STATUS, REPORT } from './constants';
|
import { NAME, LAST_SEEN, CREATED, EXPIRED, STATUS, REPORT } from '../../constants';
|
||||||
|
|
||||||
import styles from './reporting.module.scss';
|
import styles from '../ReportToggleList.module.scss';
|
||||||
|
|
||||||
const ReportToggleListHeader = ({ handleCheckAll, checkAll, setSortData, bulkActionsOn }) => {
|
const ReportToggleListHeader = ({ handleCheckAll, checkAll, setSortData, bulkActionsOn }) => {
|
||||||
const handleSort = type => {
|
const handleSort = type => {
|
||||||
@ -24,7 +24,12 @@ const ReportToggleListHeader = ({ handleCheckAll, checkAll, setSortData, bulkAct
|
|||||||
condition={bulkActionsOn}
|
condition={bulkActionsOn}
|
||||||
show={
|
show={
|
||||||
<th>
|
<th>
|
||||||
<Checkbox onChange={handleCheckAll} value={checkAll} checked={checkAll} />
|
<Checkbox
|
||||||
|
onChange={handleCheckAll}
|
||||||
|
value={checkAll}
|
||||||
|
checked={checkAll}
|
||||||
|
className={styles.checkbox}
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -3,15 +3,15 @@ import classnames from 'classnames';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { Checkbox } from 'react-mdl';
|
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/conditionally-render';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { pluralize, getDates, expired, toggleExpiryByTypeMap, getDiffInDays } from './utils';
|
import { pluralize, getDates, expired, toggleExpiryByTypeMap, getDiffInDays } from '../../utils';
|
||||||
import { KILLSWITCH, PERMISSION } from './constants';
|
import { KILLSWITCH, PERMISSION } from '../../constants';
|
||||||
|
|
||||||
import styles from './reporting.module.scss';
|
import styles from '../ReportToggleList.module.scss';
|
||||||
|
|
||||||
const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checked, bulkActionsOn, setFeatures }) => {
|
const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checked, bulkActionsOn, setFeatures }) => {
|
||||||
const nameMatches = feature => feature.name === name;
|
const nameMatches = feature => feature.name === name;
|
||||||
@ -107,7 +107,12 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke
|
|||||||
condition={bulkActionsOn}
|
condition={bulkActionsOn}
|
||||||
show={
|
show={
|
||||||
<td>
|
<td>
|
||||||
<Checkbox checked={checked} value={checked} onChange={handleChange} />
|
<Checkbox
|
||||||
|
checked={checked}
|
||||||
|
value={checked}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.checkbox}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -3,14 +3,15 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Select from '../common/select';
|
import Select from '../common/select';
|
||||||
import ReportCardContainer from './report-card-container';
|
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
||||||
import ReportToggleListContainer from './report-toggle-list-container';
|
import ReportToggleListContainer from './ReportToggleList/ReportToggleListContainer';
|
||||||
|
|
||||||
import ConditionallyRender from '../common/conditionally-render';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { formatProjectOptions } from './utils';
|
import { formatProjectOptions } from './utils';
|
||||||
|
import { REPORTING_SELECT_ID } from '../../testIds';
|
||||||
|
|
||||||
import styles from './reporting.module.scss';
|
import styles from './Reporting.module.scss';
|
||||||
|
|
||||||
const Reporting = ({ fetchFeatureToggles, projects }) => {
|
const Reporting = ({ fetchFeatureToggles, projects }) => {
|
||||||
const [projectOptions, setProjectOptions] = useState([{ key: 'default', label: 'Default' }]);
|
const [projectOptions, setProjectOptions] = useState([{ key: 'default', label: 'Default' }]);
|
||||||
@ -40,11 +41,13 @@ const Reporting = ({ fetchFeatureToggles, projects }) => {
|
|||||||
name="project"
|
name="project"
|
||||||
className={styles.select}
|
className={styles.select}
|
||||||
options={projectOptions}
|
options={projectOptions}
|
||||||
value={setSelectedProject.label}
|
value={selectedProject}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
inputProps={{ ['data-test']: REPORTING_SELECT_ID }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const multipleProjects = projects.length > 1;
|
const multipleProjects = projects.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -125,7 +125,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #f1f1f1;
|
border-bottom: var(--default-border);
|
||||||
padding: 1rem var(--card-padding-x);
|
padding: 1rem var(--card-padding-x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchFeatureToggles } from '../../store/feature-toggle/actions';
|
import { fetchFeatureToggles } from '../../store/feature-toggle/actions';
|
||||||
|
|
||||||
import Reporting from './reporting';
|
import Reporting from './Reporting.jsx';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
projects: state.projects.toJS(),
|
projects: state.projects.toJS(),
|
||||||
@ -5,7 +5,8 @@ import { HashRouter } from 'react-router-dom';
|
|||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
import { mount } from 'enzyme/build';
|
import { mount } from 'enzyme/build';
|
||||||
|
|
||||||
import Reporting from '../reporting';
|
import Reporting from '../Reporting';
|
||||||
|
import { REPORTING_SELECT_ID } from '../../../testIds';
|
||||||
|
|
||||||
import { testProjects, testFeatures } from '../testData';
|
import { testProjects, testFeatures } from '../testData';
|
||||||
|
|
||||||
@ -15,13 +16,6 @@ const mockStore = {
|
|||||||
};
|
};
|
||||||
const mockReducer = state => state;
|
const mockReducer = state => state;
|
||||||
|
|
||||||
jest.mock('react-mdl', () => ({
|
|
||||||
Checkbox: jest.fn().mockImplementation(({ children }) => children),
|
|
||||||
Card: jest.fn().mockImplementation(({ children }) => children),
|
|
||||||
Menu: jest.fn().mockImplementation(({ children }) => children),
|
|
||||||
MenuItem: jest.fn().mockImplementation(({ children }) => children),
|
|
||||||
}));
|
|
||||||
|
|
||||||
test('changing projects renders only toggles from that project', () => {
|
test('changing projects renders only toggles from that project', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
@ -31,9 +25,7 @@ test('changing projects renders only toggles from that project', () => {
|
|||||||
</HashRouter>
|
</HashRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
const select = wrapper.find('.mdl-textfield__input').first();
|
const select = wrapper.find(`input[data-test="${REPORTING_SELECT_ID}"][value="default"]`).first();
|
||||||
expect(select.contains(<option value="default">Default</option>)).toBe(true);
|
|
||||||
expect(select.contains(<option value="myProject">MyProject</option>)).toBe(true);
|
|
||||||
|
|
||||||
let list = wrapper.find('tr');
|
let list = wrapper.find('tr');
|
||||||
/* Length of projects belonging to project (3) + header row (1) */
|
/* Length of projects belonging to project (3) + header row (1) */
|
||||||
70
frontend/src/component/addons/AddonList/AddonList.jsx
Normal file
70
frontend/src/component/addons/AddonList/AddonList.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ConfiguredAddons from './ConfiguredAddons';
|
||||||
|
import AvailableAddons from './AvailableAddons';
|
||||||
|
import { Avatar, Icon } from '@material-ui/core';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
marginRight: '16px',
|
||||||
|
float: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = name => {
|
||||||
|
switch (name) {
|
||||||
|
case 'slack':
|
||||||
|
return <img style={style} src="public/slack.svg" />;
|
||||||
|
case 'jira-comment':
|
||||||
|
return <img style={style} src="public/jira.svg" />;
|
||||||
|
case 'webhook':
|
||||||
|
return <img style={style} src="public/webhooks.svg" />;
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Avatar>
|
||||||
|
<Icon>device_hub</Icon>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (addons.length === 0) {
|
||||||
|
fetchAddons();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={addons.length > 0}
|
||||||
|
show={
|
||||||
|
<ConfiguredAddons
|
||||||
|
addons={addons}
|
||||||
|
toggleAddon={toggleAddon}
|
||||||
|
hasPermission={hasPermission}
|
||||||
|
removeAddon={removeAddon}
|
||||||
|
getIcon={getIcon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<AvailableAddons providers={providers} hasPermission={hasPermission} history={history} getIcon={getIcon} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddonList.propTypes = {
|
||||||
|
addons: PropTypes.array.isRequired,
|
||||||
|
providers: PropTypes.array.isRequired,
|
||||||
|
fetchAddons: PropTypes.func.isRequired,
|
||||||
|
removeAddon: PropTypes.func.isRequired,
|
||||||
|
toggleAddon: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddonList;
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
|
import { Button, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||||
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { CREATE_ADDON } from '../../../../permissions';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const AvailableAddons = ({ providers, getIcon, hasPermission, history }) => {
|
||||||
|
const renderProvider = provider => (
|
||||||
|
<ListItem key={provider.name}>
|
||||||
|
<ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
|
||||||
|
<ListItemText primary={provider.displayName} secondary={provider.description} />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(CREATE_ADDON)}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
name="device_hub"
|
||||||
|
onClick={() => history.push(`/addons/create/${provider.name}`)}
|
||||||
|
title="Configure"
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<PageContent headerContent="Available addons">
|
||||||
|
<List>{providers.map(provider => renderProvider(provider))}</List>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AvailableAddons.propTypes = {
|
||||||
|
providers: PropTypes.array.isRequired,
|
||||||
|
getIcon: PropTypes.func.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvailableAddons;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import AvailableAddons from './AvailableAddons';
|
||||||
|
|
||||||
|
export default AvailableAddons;
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { DELETE_ADDON, UPDATE_ADDON } from '../../../../permissions';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleAddon }) => {
|
||||||
|
const onRemoveAddon = addon => () => removeAddon(addon);
|
||||||
|
const renderAddon = addon => (
|
||||||
|
<ListItem key={addon.id}>
|
||||||
|
<ListItemAvatar>{getIcon(addon.provider)}</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<span>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(UPDATE_ADDON)}
|
||||||
|
show={
|
||||||
|
<Link to={`/addons/edit/${addon.id}`}>
|
||||||
|
<strong>{addon.provider}</strong>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
elseShow={<strong>{addon.provider}</strong>}
|
||||||
|
/>
|
||||||
|
{addon.enabled ? null : <small> (Disabled)</small>}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
secondary={addon.description}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(UPDATE_ADDON)}
|
||||||
|
show={
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
title={addon.enabled ? 'Disable addon' : 'Enable addon'}
|
||||||
|
onClick={() => toggleAddon(addon)}
|
||||||
|
>
|
||||||
|
<Icon>{addon.enabled ? 'visibility' : 'visibility_off'}</Icon>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(DELETE_ADDON)}
|
||||||
|
show={
|
||||||
|
<IconButton size="small" title="Remove addon" onClick={onRemoveAddon(addon)}>
|
||||||
|
<Icon>delete</Icon>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<PageContent headerContent="Configured addons">
|
||||||
|
<List>{addons.map(addon => renderAddon(addon))}</List>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ConfiguredAddons.propTypes = {
|
||||||
|
addons: PropTypes.array.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
removeAddon: PropTypes.func.isRequired,
|
||||||
|
toggleAddon: PropTypes.func.isRequired,
|
||||||
|
getIcon: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfiguredAddons;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import ConfiguredAddons from './ConfiguredAddons';
|
||||||
|
|
||||||
|
export default ConfiguredAddons;
|
||||||
3
frontend/src/component/addons/AddonList/index.js
Normal file
3
frontend/src/component/addons/AddonList/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import AddonListComponent from './AddonList';
|
||||||
|
|
||||||
|
export default AddonListComponent;
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Textfield, Card, CardTitle, CardText, CardActions, Switch, Grid, Cell } from 'react-mdl';
|
import { TextField, FormControlLabel, Switch } from '@material-ui/core';
|
||||||
|
|
||||||
import { FormButtons, styles as commonStyles } from '../common';
|
import { FormButtons, styles as commonStyles } from '../common';
|
||||||
import { trim } from '../common/util';
|
import { trim } from '../common/util';
|
||||||
import AddonParameters from './form-addon-parameters';
|
import AddonParameters from './form-addon-parameters';
|
||||||
import AddonEvents from './form-addon-events';
|
import AddonEvents from './form-addon-events';
|
||||||
import { cloneDeep } from 'lodash';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
|
import styles from './form-addon-component.module.scss';
|
||||||
|
import PageContent from '../common/PageContent/PageContent';
|
||||||
|
|
||||||
const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }) => {
|
const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }) => {
|
||||||
const [config, setConfig] = useState(addon);
|
const [config, setConfig] = useState(addon);
|
||||||
@ -98,49 +101,46 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
|
|||||||
const { name, description, documentationUrl = 'https://unleash.github.io/docs/addons' } = provider ? provider : {};
|
const { name, description, documentationUrl = 'https://unleash.github.io/docs/addons' } = provider ? provider : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<PageContent headerContent={`Configure ${name} addon`}>
|
||||||
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
<section className={styles.formSection}>
|
||||||
Configure {name}
|
|
||||||
</CardTitle>
|
|
||||||
<CardText>
|
|
||||||
{description}
|
{description}
|
||||||
<a href={documentationUrl} target="_blank">
|
<a href={documentationUrl} target="_blank">
|
||||||
Read more
|
Read more
|
||||||
</a>
|
</a>
|
||||||
<p className={commonStyles.error}>{errors.general}</p>
|
<p className={commonStyles.error}>{errors.general}</p>
|
||||||
</CardText>
|
</section>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<section style={{ padding: '16px' }}>
|
<section className={styles.formSection}>
|
||||||
<Grid noSpacing>
|
<TextField
|
||||||
<Cell col={4}>
|
size="small"
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
label="Provider"
|
label="Provider"
|
||||||
name="provider"
|
name="provider"
|
||||||
value={config.provider}
|
value={config.provider}
|
||||||
disabled
|
disabled
|
||||||
|
variant="outlined"
|
||||||
|
className={styles.nameInput}
|
||||||
/>
|
/>
|
||||||
</Cell>
|
<FormControlLabel
|
||||||
<Cell col={4} style={{ paddingTop: '14px' }}>
|
control={<Switch checked={config.enabled} onChange={onEnabled} />}
|
||||||
<Switch checked={config.enabled} onChange={onEnabled}>
|
label={config.enabled ? 'Enabled' : 'Disabled'}
|
||||||
{config.enabled ? 'Enabled' : 'Disabled'}
|
/>
|
||||||
</Switch>
|
</section>
|
||||||
</Cell>
|
<section className={styles.formSection}>
|
||||||
</Grid>
|
<TextField
|
||||||
|
size="small"
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
style={{ width: '80%' }}
|
style={{ width: '80%' }}
|
||||||
rows={1}
|
rows={4}
|
||||||
|
multiline
|
||||||
label="Description"
|
label="Description"
|
||||||
name="description"
|
name="description"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
value={config.description}
|
value={config.description}
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
onChange={setFieldValue('description')}
|
onChange={setFieldValue('description')}
|
||||||
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section style={{ padding: '16px' }}>
|
<section className={styles.formSection}>
|
||||||
<AddonEvents
|
<AddonEvents
|
||||||
provider={provider}
|
provider={provider}
|
||||||
checkedEvents={config.events}
|
checkedEvents={config.events}
|
||||||
@ -148,7 +148,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
|
|||||||
error={errors.events}
|
error={errors.events}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section style={{ padding: '16px' }}>
|
<section className={styles.formSection}>
|
||||||
<AddonParameters
|
<AddonParameters
|
||||||
provider={provider}
|
provider={provider}
|
||||||
config={config}
|
config={config}
|
||||||
@ -157,11 +157,11 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
|
|||||||
setParameterValue={setParameterValue}
|
setParameterValue={setParameterValue}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<CardActions>
|
<section className={styles.formSection}>
|
||||||
<FormButtons submitText={submitText} onCancel={cancel} />
|
<FormButtons submitText={submitText} onCancel={cancel} />
|
||||||
</CardActions>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
.nameInput {
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
padding: 10px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: var(--h1-size);
|
||||||
|
padding: var(--card-header-padding);
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import FormComponent from './form-addon-component';
|
import FormComponent from './form-addon-component';
|
||||||
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
|
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
|
||||||
import { cloneDeep } from 'lodash';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
// Required for to fill the initial form.
|
// Required for to fill the initial form.
|
||||||
const DEFAULT_DATA = {
|
const DEFAULT_DATA = {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Checkbox, Grid, Cell } from 'react-mdl';
|
import { Grid, FormControlLabel, Checkbox } from '@material-ui/core';
|
||||||
|
|
||||||
import { styles as commonStyles } from '../common';
|
import { styles as commonStyles } from '../common';
|
||||||
|
|
||||||
@ -11,11 +11,14 @@ const AddonEvents = ({ provider, checkedEvents, setEventValue, error }) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<h4>Events</h4>
|
<h4>Events</h4>
|
||||||
<span className={commonStyles.error}>{error}</span>
|
<span className={commonStyles.error}>{error}</span>
|
||||||
<Grid className="demo-grid-ruler">
|
<Grid container spacing={0}>
|
||||||
{provider.events.map(e => (
|
{provider.events.map(e => (
|
||||||
<Cell col={4} key={e}>
|
<Grid item xs={4} key={e}>
|
||||||
<Checkbox label={e} ripple checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />
|
<FormControlLabel
|
||||||
</Cell>
|
control={<Checkbox checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />}
|
||||||
|
label={e}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Textfield } from 'react-mdl';
|
import { TextField } from '@material-ui/core';
|
||||||
|
|
||||||
const MASKED_VALUE = '*****';
|
const MASKED_VALUE = '*****';
|
||||||
|
|
||||||
@ -18,23 +18,27 @@ const AddonParameter = ({ definition, config, errors, setParameterValue }) => {
|
|||||||
const value = config.parameters[definition.name] || '';
|
const value = config.parameters[definition.name] || '';
|
||||||
const type = resolveType(definition, value);
|
const type = resolveType(definition, value);
|
||||||
const error = errors.parameters[definition.name];
|
const error = errors.parameters[definition.name];
|
||||||
const descStyle = { fontSize: '0.8em', color: 'gray', marginTop: error ? '2px' : '-15px' };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '80%', marginTop: '25px' }}>
|
<div style={{ width: '80%', marginTop: '25px' }}>
|
||||||
<Textfield
|
<TextField
|
||||||
floatingLabel
|
size="small"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
rows={definition.type === 'textfield' ? 9 : 0}
|
rows={definition.type === 'textfield' ? 9 : 0}
|
||||||
|
multiline={definition.type === 'textfield'}
|
||||||
type={type}
|
type={type}
|
||||||
label={definition.displayName}
|
label={definition.displayName}
|
||||||
name={definition.name}
|
name={definition.name}
|
||||||
placeholder={definition.placeholder || ''}
|
placeholder={definition.placeholder || ''}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
error={error}
|
error={error}
|
||||||
onChange={setParameterValue(definition.name)}
|
onChange={setParameterValue(definition.name)}
|
||||||
|
variant="outlined"
|
||||||
|
helperText={definition.description}
|
||||||
/>
|
/>
|
||||||
<div style={descStyle}>{definition.description}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -54,8 +58,8 @@ const AddonParameters = ({ provider, config, errors, setParameterValue, editMode
|
|||||||
<h4>Parameters</h4>
|
<h4>Parameters</h4>
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<p>
|
<p>
|
||||||
Sensitive parameters will be masked with value "<i>*****</i>". If you don't change the value they
|
Sensitive parameters will be masked with value "<i>*****</i>
|
||||||
will not be updated when saving.
|
". If you don't change the value they will not be updated when saving.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{provider.parameters.map(p => (
|
{provider.parameters.map(p => (
|
||||||
@ -76,7 +80,7 @@ AddonParameters.propTypes = {
|
|||||||
config: PropTypes.object.isRequired,
|
config: PropTypes.object.isRequired,
|
||||||
errors: PropTypes.object.isRequired,
|
errors: PropTypes.object.isRequired,
|
||||||
setParameterValue: PropTypes.func.isRequired,
|
setParameterValue: PropTypes.func.isRequired,
|
||||||
editMode: PropTypes.bool.optional,
|
editMode: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddonParameters;
|
export default AddonParameters;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import AddonsListComponent from './list-component.jsx';
|
import AddonsListComponent from './AddonList';
|
||||||
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
|
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { List, ListItem, ListItemAction, IconButton, Card, Button } from 'react-mdl';
|
|
||||||
import { HeaderTitle, styles as commonStyles } from '../common';
|
|
||||||
import { CREATE_ADDON, DELETE_ADDON, UPDATE_ADDON } from '../../permissions';
|
|
||||||
|
|
||||||
const style = { width: '40px', height: '40px', marginRight: '16px', float: 'left' };
|
|
||||||
|
|
||||||
const getIcon = name => {
|
|
||||||
switch (name) {
|
|
||||||
case 'slack':
|
|
||||||
return <img style={style} src="public/slack.svg" />;
|
|
||||||
case 'jira-comment':
|
|
||||||
return <img style={style} src="public/jira.svg" />;
|
|
||||||
case 'webhook':
|
|
||||||
return <img style={style} src="public/webhooks.svg" />;
|
|
||||||
default:
|
|
||||||
return <i className="material-icons mdl-list__item-avatar">device_hub</i>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddonListComponent = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => {
|
|
||||||
useEffect(() => {
|
|
||||||
if (addons.length === 0) {
|
|
||||||
fetchAddons();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onRemoveAddon = addon => () => removeAddon(addon);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{addons.length > 0 ? (
|
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
|
||||||
<HeaderTitle title="Configured addons" />
|
|
||||||
<List>
|
|
||||||
{addons.map(addon => (
|
|
||||||
<ListItem key={addon.id} threeLine>
|
|
||||||
<span className={['mdl-list__item-primary-content'].join(' ')}>
|
|
||||||
{getIcon(addon.provider)}
|
|
||||||
<span>
|
|
||||||
{hasPermission(UPDATE_ADDON) ? (
|
|
||||||
<Link to={`/addons/edit/${addon.id}`}>
|
|
||||||
<strong>{addon.provider}</strong>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<strong>{addon.provider}</strong>
|
|
||||||
)}
|
|
||||||
{addon.enabled ? null : <small> (Disabled)</small>}
|
|
||||||
</span>
|
|
||||||
<span className="mdl-list__item-text-body">{addon.description}</span>
|
|
||||||
</span>
|
|
||||||
<ListItemAction>
|
|
||||||
{hasPermission(UPDATE_ADDON) ? (
|
|
||||||
<IconButton
|
|
||||||
name={addon.enabled ? 'visibility' : 'visibility_off'}
|
|
||||||
title={addon.enabled ? 'Disable addon' : 'Enable addon'}
|
|
||||||
onClick={() => toggleAddon(addon)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{hasPermission(DELETE_ADDON) ? (
|
|
||||||
<IconButton name="delete" title="Remove addon" onClick={onRemoveAddon(addon)} />
|
|
||||||
) : null}
|
|
||||||
</ListItemAction>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
<br />
|
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
|
||||||
<HeaderTitle title="Available addons" />
|
|
||||||
<List>
|
|
||||||
{providers.map((provider, i) => (
|
|
||||||
<ListItem key={i} threeLine>
|
|
||||||
<span className={['mdl-list__item-primary-content'].join(' ')}>
|
|
||||||
{getIcon(provider.name)}
|
|
||||||
<span>
|
|
||||||
<strong>{provider.displayName}</strong>
|
|
||||||
</span>
|
|
||||||
<span className="mdl-list__item-text-body">{provider.description}</span>
|
|
||||||
</span>
|
|
||||||
<ListItemAction>
|
|
||||||
{hasPermission(CREATE_ADDON) ? (
|
|
||||||
<Button
|
|
||||||
raised
|
|
||||||
colored
|
|
||||||
name="device_hub"
|
|
||||||
onClick={() => history.push(`/addons/create/${provider.name}`)}
|
|
||||||
title="Configure"
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</ListItemAction>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
AddonListComponent.propTypes = {
|
|
||||||
addons: PropTypes.array.isRequired,
|
|
||||||
providers: PropTypes.array.isRequired,
|
|
||||||
fetchAddons: PropTypes.func.isRequired,
|
|
||||||
removeAddon: PropTypes.func.isRequired,
|
|
||||||
toggleAddon: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddonListComponent;
|
|
||||||
@ -1,68 +1,60 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`renders correctly with empty version 1`] = `
|
exports[`renders correctly with empty version 1`] = `
|
||||||
<react-mdl-FooterSection
|
<section
|
||||||
logo="Unleash "
|
title="API details"
|
||||||
type="bottom"
|
|
||||||
>
|
>
|
||||||
|
<h4>
|
||||||
|
Unleash
|
||||||
|
</h4>
|
||||||
<small>
|
<small>
|
||||||
(test)
|
\`($
|
||||||
|
test
|
||||||
|
)\`
|
||||||
</small>
|
</small>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<br />
|
||||||
|
|
||||||
</small>
|
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
We are the best!
|
We are the best!
|
||||||
</small>
|
</small>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
</section>
|
||||||
|
|
||||||
</small>
|
|
||||||
</react-mdl-FooterSection>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly with ui-config 1`] = `
|
exports[`renders correctly with ui-config 1`] = `
|
||||||
<react-mdl-FooterSection
|
<section
|
||||||
logo="Unleash 1.1.0"
|
title="API details"
|
||||||
type="bottom"
|
|
||||||
>
|
>
|
||||||
|
<h4>
|
||||||
|
Unleash 1.1.0
|
||||||
|
</h4>
|
||||||
<small>
|
<small>
|
||||||
(test)
|
\`($
|
||||||
|
test
|
||||||
|
)\`
|
||||||
</small>
|
</small>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<br />
|
||||||
|
|
||||||
</small>
|
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
We are the best!
|
We are the best!
|
||||||
</small>
|
</small>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
</section>
|
||||||
|
|
||||||
</small>
|
|
||||||
</react-mdl-FooterSection>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly without uiConfig 1`] = `
|
exports[`renders correctly without uiConfig 1`] = `
|
||||||
<react-mdl-FooterSection
|
<section
|
||||||
logo="Unleash 1.1.0"
|
title="API details"
|
||||||
type="bottom"
|
|
||||||
>
|
>
|
||||||
<small>
|
<h4>
|
||||||
|
Unleash 1.1.0
|
||||||
</small>
|
</h4>
|
||||||
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<small>
|
|
||||||
|
|
||||||
</small>
|
|
||||||
<br />
|
<br />
|
||||||
<small />
|
<small />
|
||||||
<br />
|
<br />
|
||||||
<small>
|
</section>
|
||||||
|
|
||||||
</small>
|
|
||||||
</react-mdl-FooterSection>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import React from 'react';
|
|||||||
import ShowApiDetailsComponent from '../show-api-details-component';
|
import ShowApiDetailsComponent from '../show-api-details-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
|
||||||
|
|
||||||
test('renders correctly with empty version', () => {
|
test('renders correctly with empty version', () => {
|
||||||
const uiConfig = {
|
const uiConfig = {
|
||||||
name: 'Unleash',
|
name: 'Unleash',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FooterSection } from 'react-mdl';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
class ShowApiDetailsComponent extends Component {
|
class ShowApiDetailsComponent extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -15,12 +15,12 @@ class ShowApiDetailsComponent extends Component {
|
|||||||
if (versionInfo) {
|
if (versionInfo) {
|
||||||
if (versionInfo.current.enterprise) {
|
if (versionInfo.current.enterprise) {
|
||||||
versionStr = `${name} ${versionInfo.current.enterprise}`;
|
versionStr = `${name} ${versionInfo.current.enterprise}`;
|
||||||
if (versionInfo.latest && !versionInfo.isLatest) {
|
if (Object.keys(versionInfo.latest).includes('enterprise') && !versionInfo.isLatest) {
|
||||||
updateNotification = `Upgrade available - Latest Enterprise release: ${versionInfo.latest.enterprise}`;
|
updateNotification = `Upgrade available - Latest Enterprise release: ${versionInfo.latest.enterprise}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
versionStr = `${name} ${versionInfo.current.oss}`;
|
versionStr = `${name} ${versionInfo.current.oss}`;
|
||||||
if (versionInfo.latest && !versionInfo.isLatest) {
|
if (Object.keys(versionInfo.latest).includes('oss') && !versionInfo.isLatest) {
|
||||||
updateNotification = `Upgrade available - Latest OSS release: ${versionInfo.latest.oss}`;
|
updateNotification = `Upgrade available - Latest OSS release: ${versionInfo.latest.oss}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,15 +29,17 @@ class ShowApiDetailsComponent extends Component {
|
|||||||
versionStr = `${name} ${version}`;
|
versionStr = `${name} ${version}`;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FooterSection type="bottom" logo={`${versionStr}`}>
|
<section title="API details">
|
||||||
<small>{environment ? `(${environment})` : ''}</small>
|
<h4>{`${versionStr}`}</h4>
|
||||||
|
<ConditionallyRender condition={environment} show={<small>`(${environment})`</small>} />
|
||||||
|
<br />
|
||||||
|
<ConditionallyRender condition={updateNotification} show={<small>{updateNotification}`</small>} />
|
||||||
<br />
|
<br />
|
||||||
<small>{updateNotification ? `${updateNotification}` : ''}</small>
|
|
||||||
<br />
|
<br />
|
||||||
<small>{slogan}</small>
|
<small>{slogan}</small>
|
||||||
<br />
|
<br />
|
||||||
<small>{instanceId ? `${instanceId}` : ''}</small>
|
<ConditionallyRender condition={instanceId} show={<small>{`${instanceId}`}</small>} />
|
||||||
</FooterSection>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import PropTypes from 'prop-types';
|
|||||||
import { Route, Redirect, Switch } from 'react-router-dom';
|
import { Route, Redirect, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import Features from '../page/features';
|
import Features from '../page/features';
|
||||||
import { routes } from './menu/routes';
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import AuthenticationContainer from './user/authentication-container';
|
import AuthenticationContainer from './user/authentication-container';
|
||||||
import MainLayout from './layout/main';
|
import MainLayout from './layout/main';
|
||||||
|
|
||||||
|
import { routes } from './menu/routes';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
class App extends PureComponent {
|
class App extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
location: PropTypes.object.isRequired,
|
location: PropTypes.object.isRequired,
|
||||||
|
|||||||
@ -5,149 +5,346 @@ exports[`renders correctly if no application 1`] = `
|
|||||||
<p>
|
<p>
|
||||||
Loading...
|
Loading...
|
||||||
</p>
|
</p>
|
||||||
<react-mdl-ProgressBar
|
<div
|
||||||
indeterminate={true}
|
className="MuiLinearProgress-root MuiLinearProgress-colorPrimary MuiLinearProgress-indeterminate"
|
||||||
|
role="progressbar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiLinearProgress-bar MuiLinearProgress-barColorPrimary MuiLinearProgress-bar1Indeterminate"
|
||||||
|
style={Object {}}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
className="MuiLinearProgress-bar MuiLinearProgress-bar2Indeterminate MuiLinearProgress-barColorPrimary"
|
||||||
|
style={Object {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly with permissions 1`] = `
|
exports[`renders correctly with permissions 1`] = `
|
||||||
<react-mdl-Card
|
<div
|
||||||
className="fullwidth"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
shadow={0}
|
|
||||||
>
|
>
|
||||||
<react-mdl-CardTitle
|
<div
|
||||||
|
className="makeStyles-headerContainer-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerTitleContainer-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingRight": "64px",
|
"alignItems": "center",
|
||||||
"paddingTop": "24px",
|
"display": "flex",
|
||||||
"wordBreak": "break-all",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<react-mdl-Icon
|
<div
|
||||||
name="apps"
|
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
|
||||||
/>
|
style={
|
||||||
|
Object {
|
||||||
|
"marginRight": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
apps
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
test-app
|
test-app
|
||||||
</react-mdl-CardTitle>
|
</span>
|
||||||
<react-mdl-CardText
|
</h2>
|
||||||
style={
|
</div>
|
||||||
Object {
|
<div
|
||||||
"paddingTop": "0",
|
className="makeStyles-headerActions-7"
|
||||||
}
|
>
|
||||||
}
|
<a
|
||||||
|
aria-disabled={false}
|
||||||
|
className="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiIconButton-root MuiTypography-colorPrimary"
|
||||||
|
href="http://example.org"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
link
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textSecondary"
|
||||||
|
disabled={false}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
title="Delete application"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-bodyContainer-2"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className="MuiTypography-root MuiTypography-body1"
|
||||||
>
|
>
|
||||||
<p>
|
|
||||||
app description
|
app description
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p
|
||||||
|
className="MuiTypography-root MuiTypography-body2"
|
||||||
|
>
|
||||||
Created:
|
Created:
|
||||||
<strong>
|
<strong>
|
||||||
Invalid Date
|
Invalid Date
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
</react-mdl-CardText>
|
</div>
|
||||||
<react-mdl-CardMenu>
|
|
||||||
<a
|
|
||||||
className="mdl-color-text--grey-600"
|
|
||||||
href="http://example.org"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<react-mdl-Icon
|
|
||||||
name="link"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</react-mdl-CardMenu>
|
|
||||||
<div>
|
<div>
|
||||||
<react-mdl-CardActions
|
<div
|
||||||
border={true}
|
className="MuiPaper-root makeStyles-tabNav-8 MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiTabs-root"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiTabs-scroller MuiTabs-fixed"
|
||||||
|
onScroll={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"alignItems": "center",
|
"marginBottom": null,
|
||||||
"display": "flex",
|
"overflow": "hidden",
|
||||||
"justifyContent": "space-between",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span />
|
<div
|
||||||
<react-mdl-Button
|
className="MuiTabs-flexContainer MuiTabs-centered"
|
||||||
accent={true}
|
onKeyDown={[Function]}
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="tabpanel-0"
|
||||||
|
aria-selected={true}
|
||||||
|
className="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary Mui-selected"
|
||||||
|
disabled={false}
|
||||||
|
id="tab-0"
|
||||||
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
title="Delete application"
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Delete
|
<span
|
||||||
</react-mdl-Button>
|
className="MuiTab-wrapper"
|
||||||
</react-mdl-CardActions>
|
|
||||||
<hr />
|
|
||||||
<react-mdl-Tabs
|
|
||||||
activeTab={0}
|
|
||||||
className="mdl-color--grey-100"
|
|
||||||
onChange={[Function]}
|
|
||||||
ripple={true}
|
|
||||||
tabBarProps={
|
|
||||||
Object {
|
|
||||||
"style": Object {
|
|
||||||
"width": "100%",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<react-mdl-Tab>
|
Application overview
|
||||||
Details
|
</span>
|
||||||
</react-mdl-Tab>
|
<span
|
||||||
<react-mdl-Tab>
|
className="PrivateTabIndicator-root-9 PrivateTabIndicator-colorPrimary-10 MuiTabs-indicator"
|
||||||
Edit
|
style={Object {}}
|
||||||
</react-mdl-Tab>
|
/>
|
||||||
</react-mdl-Tabs>
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="tabpanel-1"
|
||||||
|
aria-selected={false}
|
||||||
|
className="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary"
|
||||||
|
disabled={false}
|
||||||
|
id="tab-1"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={-1}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTab-wrapper"
|
||||||
|
>
|
||||||
|
Edit application
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<react-mdl-Grid
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-labelledby="wrapped-tab-0"
|
||||||
|
hidden={false}
|
||||||
|
id="wrapped-tabpanel-0"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiGrid-root MuiGrid-container"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"margin": 0,
|
"margin": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<react-mdl-Cell
|
<div
|
||||||
col={6}
|
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-md-6 MuiGrid-grid-xl-6"
|
||||||
hidePhone={true}
|
>
|
||||||
phone={12}
|
<h6
|
||||||
tablet={4}
|
className="MuiTypography-root MuiTypography-subtitle1"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "1rem 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h6>
|
|
||||||
Toggles
|
Toggles
|
||||||
</h6>
|
</h6>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<ul
|
||||||
<react-mdl-ListItem
|
className="MuiList-root MuiList-padding"
|
||||||
twoLine={true}
|
|
||||||
>
|
>
|
||||||
<react-mdl-ListItemContent
|
<li
|
||||||
icon={
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
<span>
|
disabled={false}
|
||||||
<react-mdl-Switch
|
>
|
||||||
checked={true}
|
<div
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled={true}
|
||||||
|
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-13 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-disabled-15 Mui-disabled Mui-disabled Mui-disabled"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="PrivateSwitchBase-input-16 MuiSwitch-input"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
type="checkbox"
|
||||||
|
value={true}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-thumb"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
}
|
</span>
|
||||||
subtitle="this is A toggle"
|
<span
|
||||||
|
className="MuiSwitch-track"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/features/view/ToggleA"
|
href="/features/strategies/ToggleA"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
ToggleA
|
ToggleA
|
||||||
</a>
|
</a>
|
||||||
</react-mdl-ListItemContent>
|
</span>
|
||||||
</react-mdl-ListItem>
|
<p
|
||||||
<react-mdl-ListItem
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
twoLine={true}
|
|
||||||
>
|
>
|
||||||
<react-mdl-ListItemContent
|
this is A toggle
|
||||||
icon="report"
|
</p>
|
||||||
subtitle="Missing, want to create?"
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
report
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/features/create?name=ToggleB"
|
href="/features/create?name=ToggleB"
|
||||||
@ -155,26 +352,52 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
>
|
>
|
||||||
ToggleB
|
ToggleB
|
||||||
</a>
|
</a>
|
||||||
</react-mdl-ListItemContent>
|
</span>
|
||||||
</react-mdl-ListItem>
|
<p
|
||||||
</react-mdl-List>
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
</react-mdl-Cell>
|
>
|
||||||
<react-mdl-Cell
|
Missing, want to create?
|
||||||
col={6}
|
</p>
|
||||||
phone={12}
|
</div>
|
||||||
tablet={4}
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-md-6 MuiGrid-grid-xl-6"
|
||||||
|
>
|
||||||
|
<h6
|
||||||
|
className="MuiTypography-root MuiTypography-subtitle1"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "1rem 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h6>
|
|
||||||
Implemented strategies
|
Implemented strategies
|
||||||
</h6>
|
</h6>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<ul
|
||||||
<react-mdl-ListItem
|
className="MuiList-root MuiList-padding"
|
||||||
twoLine={true}
|
|
||||||
>
|
>
|
||||||
<react-mdl-ListItemContent
|
<li
|
||||||
icon="extension"
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
subtitle="A description"
|
disabled={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
extension
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/strategies/view/StrategyA"
|
href="/strategies/view/StrategyA"
|
||||||
@ -182,14 +405,33 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
>
|
>
|
||||||
StrategyA
|
StrategyA
|
||||||
</a>
|
</a>
|
||||||
</react-mdl-ListItemContent>
|
</span>
|
||||||
</react-mdl-ListItem>
|
<p
|
||||||
<react-mdl-ListItem
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
twoLine={true}
|
|
||||||
>
|
>
|
||||||
<react-mdl-ListItemContent
|
A description
|
||||||
icon="report"
|
</p>
|
||||||
subtitle="Missing, want to create?"
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
report
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/strategies/create?name=StrategyB"
|
href="/strategies/create?name=StrategyB"
|
||||||
@ -197,26 +439,59 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
>
|
>
|
||||||
StrategyB
|
StrategyB
|
||||||
</a>
|
</a>
|
||||||
</react-mdl-ListItemContent>
|
</span>
|
||||||
</react-mdl-ListItem>
|
<p
|
||||||
</react-mdl-List>
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
</react-mdl-Cell>
|
>
|
||||||
<react-mdl-Cell
|
Missing, want to create?
|
||||||
col={12}
|
</p>
|
||||||
tablet={12}
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiGrid-root MuiGrid-item MuiGrid-grid-md-12 MuiGrid-grid-xl-12"
|
||||||
|
>
|
||||||
|
<h6
|
||||||
|
className="MuiTypography-root MuiTypography-subtitle1"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "1rem 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h6>
|
|
||||||
1
|
1
|
||||||
Instances registered
|
Instances registered
|
||||||
</h6>
|
</h6>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<ul
|
||||||
<react-mdl-ListItem
|
className="MuiList-root MuiList-padding"
|
||||||
twoLine={true}
|
>
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
timeline
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
instance-1 (4.0)
|
||||||
|
</span>
|
||||||
|
<p
|
||||||
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<react-mdl-ListItemContent
|
|
||||||
icon="timeline"
|
|
||||||
subtitle={
|
|
||||||
<span>
|
<span>
|
||||||
123.123.123.123
|
123.123.123.123
|
||||||
last seen at
|
last seen at
|
||||||
@ -224,190 +499,121 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
02/23/2017, 03:56:49 PM
|
02/23/2017, 03:56:49 PM
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
}
|
</p>
|
||||||
>
|
</div>
|
||||||
instance-1
|
</li>
|
||||||
|
</ul>
|
||||||
(4.0)
|
</div>
|
||||||
</react-mdl-ListItemContent>
|
</div>
|
||||||
</react-mdl-ListItem>
|
</div>
|
||||||
</react-mdl-List>
|
<div
|
||||||
</react-mdl-Cell>
|
aria-labelledby="wrapped-tab-1"
|
||||||
</react-mdl-Grid>
|
hidden={true}
|
||||||
</react-mdl-Card>
|
id="wrapped-tabpanel-1"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly without permission 1`] = `
|
exports[`renders correctly without permission 1`] = `
|
||||||
<react-mdl-Card
|
<div
|
||||||
className="fullwidth"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
shadow={0}
|
|
||||||
>
|
>
|
||||||
<react-mdl-CardTitle
|
<div
|
||||||
|
className="makeStyles-headerContainer-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerTitleContainer-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingRight": "64px",
|
"alignItems": "center",
|
||||||
"paddingTop": "24px",
|
"display": "flex",
|
||||||
"wordBreak": "break-all",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<react-mdl-Icon
|
<div
|
||||||
name="apps"
|
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
|
||||||
/>
|
style={
|
||||||
|
Object {
|
||||||
|
"marginRight": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
apps
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
test-app
|
test-app
|
||||||
</react-mdl-CardTitle>
|
</span>
|
||||||
<react-mdl-CardText
|
</h2>
|
||||||
style={
|
</div>
|
||||||
Object {
|
<div
|
||||||
"paddingTop": "0",
|
className="makeStyles-headerActions-7"
|
||||||
}
|
>
|
||||||
}
|
<a
|
||||||
|
aria-disabled={false}
|
||||||
|
className="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiIconButton-root MuiTypography-colorPrimary"
|
||||||
|
href="http://example.org"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
link
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-bodyContainer-2"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className="MuiTypography-root MuiTypography-body1"
|
||||||
>
|
>
|
||||||
<p>
|
|
||||||
app description
|
app description
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p
|
||||||
|
className="MuiTypography-root MuiTypography-body2"
|
||||||
|
>
|
||||||
Created:
|
Created:
|
||||||
<strong>
|
<strong>
|
||||||
Invalid Date
|
Invalid Date
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
</react-mdl-CardText>
|
</div>
|
||||||
<react-mdl-CardMenu>
|
</div>
|
||||||
<a
|
</div>
|
||||||
className="mdl-color-text--grey-600"
|
|
||||||
href="http://example.org"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<react-mdl-Icon
|
|
||||||
name="link"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</react-mdl-CardMenu>
|
|
||||||
|
|
||||||
<react-mdl-Grid
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<react-mdl-Cell
|
|
||||||
col={6}
|
|
||||||
hidePhone={true}
|
|
||||||
phone={12}
|
|
||||||
tablet={4}
|
|
||||||
>
|
|
||||||
<h6>
|
|
||||||
Toggles
|
|
||||||
</h6>
|
|
||||||
<hr />
|
|
||||||
<react-mdl-List>
|
|
||||||
<react-mdl-ListItem
|
|
||||||
twoLine={true}
|
|
||||||
>
|
|
||||||
<react-mdl-ListItemContent
|
|
||||||
icon={
|
|
||||||
<span>
|
|
||||||
<react-mdl-Switch
|
|
||||||
checked={true}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
subtitle="this is A toggle"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/features/view/ToggleA"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
ToggleA
|
|
||||||
</a>
|
|
||||||
</react-mdl-ListItemContent>
|
|
||||||
</react-mdl-ListItem>
|
|
||||||
<react-mdl-ListItem
|
|
||||||
twoLine={true}
|
|
||||||
>
|
|
||||||
<react-mdl-ListItemContent
|
|
||||||
icon="report"
|
|
||||||
subtitle="Missing"
|
|
||||||
>
|
|
||||||
ToggleB
|
|
||||||
</react-mdl-ListItemContent>
|
|
||||||
</react-mdl-ListItem>
|
|
||||||
</react-mdl-List>
|
|
||||||
</react-mdl-Cell>
|
|
||||||
<react-mdl-Cell
|
|
||||||
col={6}
|
|
||||||
phone={12}
|
|
||||||
tablet={4}
|
|
||||||
>
|
|
||||||
<h6>
|
|
||||||
Implemented strategies
|
|
||||||
</h6>
|
|
||||||
<hr />
|
|
||||||
<react-mdl-List>
|
|
||||||
<react-mdl-ListItem
|
|
||||||
twoLine={true}
|
|
||||||
>
|
|
||||||
<react-mdl-ListItemContent
|
|
||||||
icon="extension"
|
|
||||||
subtitle="A description"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/strategies/view/StrategyA"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
StrategyA
|
|
||||||
</a>
|
|
||||||
</react-mdl-ListItemContent>
|
|
||||||
</react-mdl-ListItem>
|
|
||||||
<react-mdl-ListItem
|
|
||||||
twoLine={true}
|
|
||||||
>
|
|
||||||
<react-mdl-ListItemContent
|
|
||||||
icon="report"
|
|
||||||
subtitle="Missing"
|
|
||||||
>
|
|
||||||
StrategyB
|
|
||||||
</react-mdl-ListItemContent>
|
|
||||||
</react-mdl-ListItem>
|
|
||||||
</react-mdl-List>
|
|
||||||
</react-mdl-Cell>
|
|
||||||
<react-mdl-Cell
|
|
||||||
col={12}
|
|
||||||
tablet={12}
|
|
||||||
>
|
|
||||||
<h6>
|
|
||||||
1
|
|
||||||
Instances registered
|
|
||||||
</h6>
|
|
||||||
<hr />
|
|
||||||
<react-mdl-List>
|
|
||||||
<react-mdl-ListItem
|
|
||||||
twoLine={true}
|
|
||||||
>
|
|
||||||
<react-mdl-ListItemContent
|
|
||||||
icon="timeline"
|
|
||||||
subtitle={
|
|
||||||
<span>
|
|
||||||
123.123.123.123
|
|
||||||
last seen at
|
|
||||||
<small>
|
|
||||||
02/23/2017, 03:56:49 PM
|
|
||||||
</small>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
instance-1
|
|
||||||
|
|
||||||
(4.0)
|
|
||||||
</react-mdl-ListItemContent>
|
|
||||||
</react-mdl-ListItem>
|
|
||||||
</react-mdl-List>
|
|
||||||
</react-mdl-Cell>
|
|
||||||
</react-mdl-Grid>
|
|
||||||
</react-mdl-Card>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
import ClientApplications from '../application-edit-component';
|
import ClientApplications from '../application-edit-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions';
|
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions';
|
||||||
|
import theme from '../../../themes/main-theme';
|
||||||
jest.mock('react-mdl');
|
|
||||||
|
|
||||||
test('renders correctly if no application', () => {
|
test('renders correctly if no application', () => {
|
||||||
const tree = renderer
|
const tree = renderer
|
||||||
@ -27,6 +27,7 @@ test('renders correctly without permission', () => {
|
|||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create(
|
.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
<ClientApplications
|
<ClientApplications
|
||||||
fetchApplication={() => Promise.resolve({})}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
storeApplicationMetaData={jest.fn()}
|
storeApplicationMetaData={jest.fn()}
|
||||||
@ -72,6 +73,7 @@ test('renders correctly without permission', () => {
|
|||||||
location={{ locale: 'en-GB' }}
|
location={{ locale: 'en-GB' }}
|
||||||
hasPermission={() => false}
|
hasPermission={() => false}
|
||||||
/>
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
)
|
)
|
||||||
.toJSON();
|
.toJSON();
|
||||||
@ -83,6 +85,7 @@ test('renders correctly with permissions', () => {
|
|||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create(
|
.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
<ClientApplications
|
<ClientApplications
|
||||||
fetchApplication={() => Promise.resolve({})}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
storeApplicationMetaData={jest.fn()}
|
storeApplicationMetaData={jest.fn()}
|
||||||
@ -130,6 +133,7 @@ test('renders correctly with permissions', () => {
|
|||||||
[CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1
|
[CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
)
|
)
|
||||||
.toJSON();
|
.toJSON();
|
||||||
|
|||||||
@ -2,12 +2,16 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Button, Card, CardActions, CardTitle, CardText, CardMenu, Icon, ProgressBar, Tabs, Tab } from 'react-mdl';
|
import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
|
||||||
import { IconLink, styles as commonStyles } from '../common';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
|
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
|
||||||
import { UPDATE_APPLICATION } from '../../permissions';
|
import { UPDATE_APPLICATION } from '../../permissions';
|
||||||
import ApplicationView from './application-view';
|
import ApplicationView from './application-view';
|
||||||
import ApplicationUpdate from './application-update';
|
import ApplicationUpdate from './application-update';
|
||||||
|
import TabNav from '../common/TabNav/TabNav';
|
||||||
|
import Dialogue from '../common/Dialogue';
|
||||||
|
import PageContent from '../common/PageContent';
|
||||||
|
import HeaderTitle from '../common/HeaderTitle';
|
||||||
|
|
||||||
class ClientApplications extends PureComponent {
|
class ClientApplications extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -23,7 +27,11 @@ class ClientApplications extends PureComponent {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super();
|
super();
|
||||||
this.state = { activeTab: 0, loading: !props.application };
|
this.state = {
|
||||||
|
activeTab: 0,
|
||||||
|
loading: !props.application,
|
||||||
|
prompt: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -34,9 +42,11 @@ class ClientApplications extends PureComponent {
|
|||||||
|
|
||||||
deleteApplication = async evt => {
|
deleteApplication = async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
// if (window.confirm('Are you sure you want to remove this application?')) {
|
||||||
const { deleteApplication, appName } = this.props;
|
const { deleteApplication, appName } = this.props;
|
||||||
await deleteApplication(appName);
|
await deleteApplication(appName);
|
||||||
this.props.history.push('/applications');
|
this.props.history.push('/applications');
|
||||||
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -44,7 +54,7 @@ class ClientApplications extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
<ProgressBar indeterminate />
|
<LinearProgress />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!this.props.application) {
|
} else if (!this.props.application) {
|
||||||
@ -53,8 +63,23 @@ class ClientApplications extends PureComponent {
|
|||||||
const { application, storeApplicationMetaData, hasPermission } = this.props;
|
const { application, storeApplicationMetaData, hasPermission } = this.props;
|
||||||
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
|
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
|
||||||
|
|
||||||
const content =
|
const toggleModal = () => {
|
||||||
this.state.activeTab === 0 ? (
|
this.setState(prev => ({ ...prev, prompt: !prev.prompt }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderModal = () => (
|
||||||
|
<Dialogue
|
||||||
|
open={this.state.prompt}
|
||||||
|
onClose={toggleModal}
|
||||||
|
onClick={this.deleteApplication}
|
||||||
|
title="Are you sure you want to delete this application?"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabData = [
|
||||||
|
{
|
||||||
|
label: 'Application overview',
|
||||||
|
component: (
|
||||||
<ApplicationView
|
<ApplicationView
|
||||||
strategies={strategies}
|
strategies={strategies}
|
||||||
instances={instances}
|
instances={instances}
|
||||||
@ -62,60 +87,74 @@ class ClientApplications extends PureComponent {
|
|||||||
hasPermission={hasPermission}
|
hasPermission={hasPermission}
|
||||||
formatFullDateTime={this.formatFullDateTime}
|
formatFullDateTime={this.formatFullDateTime}
|
||||||
/>
|
/>
|
||||||
) : (
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit application',
|
||||||
|
component: (
|
||||||
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
|
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
<PageContent
|
||||||
<CardTitle style={{ paddingTop: '24px', paddingRight: '64px', wordBreak: 'break-all' }}>
|
headerContent={
|
||||||
<Icon name={icon || 'apps'} />
|
<HeaderTitle
|
||||||
{appName}
|
title={
|
||||||
</CardTitle>
|
<span
|
||||||
<CardText style={{ paddingTop: '0' }}>
|
|
||||||
<p>{description || ''}</p>
|
|
||||||
<p>
|
|
||||||
Created: <strong>{this.formatDate(createdAt)}</strong>
|
|
||||||
</p>
|
|
||||||
</CardText>
|
|
||||||
{url && (
|
|
||||||
<CardMenu>
|
|
||||||
<IconLink url={url} icon="link" />
|
|
||||||
</CardMenu>
|
|
||||||
)}
|
|
||||||
{hasPermission(UPDATE_APPLICATION) ? (
|
|
||||||
<div>
|
|
||||||
<CardActions
|
|
||||||
border
|
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span />
|
<Avatar style={{ marginRight: '8px' }}>
|
||||||
<Button accent title="Delete application" onClick={this.deleteApplication}>
|
<Icon>{icon || 'apps'}</Icon>
|
||||||
|
</Avatar>
|
||||||
|
{appName}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={url}
|
||||||
|
show={
|
||||||
|
<IconButton component={Link} href={url}>
|
||||||
|
<Icon>link</Icon>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(UPDATE_APPLICATION)}
|
||||||
|
show={
|
||||||
|
<Button color="secondary" title="Delete application" onClick={toggleModal}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</CardActions>
|
}
|
||||||
<hr />
|
/>
|
||||||
<Tabs
|
</>
|
||||||
activeTab={this.state.activeTab}
|
}
|
||||||
onChange={tabId => this.setState({ activeTab: tabId })}
|
/>
|
||||||
ripple
|
}
|
||||||
tabBarProps={{ style: { width: '100%' } }}
|
|
||||||
className="mdl-color--grey-100"
|
|
||||||
>
|
>
|
||||||
<Tab>Details</Tab>
|
<div>
|
||||||
<Tab>Edit</Tab>
|
<Typography variant="body1">{description || ''}</Typography>
|
||||||
</Tabs>
|
<Typography variant="body2">
|
||||||
|
Created: <strong>{this.formatDate(createdAt)}</strong>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<ConditionallyRender
|
||||||
''
|
condition={hasPermission(UPDATE_APPLICATION)}
|
||||||
)}
|
show={
|
||||||
|
<div>
|
||||||
|
{renderModal()}
|
||||||
|
|
||||||
{content}
|
<TabNav tabData={tabData} />
|
||||||
</Card>
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,10 @@ const mapStateToProps = (state, props) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const Constainer = connect(mapStateToProps, {
|
const Container = connect(mapStateToProps, {
|
||||||
fetchApplication,
|
fetchApplication,
|
||||||
storeApplicationMetaData,
|
storeApplicationMetaData,
|
||||||
deleteApplication,
|
deleteApplication,
|
||||||
})(ApplicationEdit);
|
})(ApplicationEdit);
|
||||||
|
|
||||||
export default Constainer;
|
export default Container;
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ProgressBar, Card, CardText, Icon } from 'react-mdl';
|
import { Icon, CircularProgress } from '@material-ui/core';
|
||||||
import { AppsLinkList, styles as commonStyles } from '../common';
|
import { AppsLinkList, styles as commonStyles } from '../common';
|
||||||
import SearchField from '../common/search-field';
|
import SearchField from '../common/SearchField/SearchField';
|
||||||
|
import PageContent from '../common/PageContent/PageContent';
|
||||||
|
import HeaderTitle from '../common/HeaderTitle';
|
||||||
|
|
||||||
const Empty = () => (
|
const Empty = () => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<CardText style={{ textAlign: 'center' }}>
|
<section style={{ textAlign: 'center' }}>
|
||||||
<Icon name="warning" style={{ fontSize: '5em' }} /> <br />
|
<Icon>warning</Icon> <br />
|
||||||
<br />
|
<br />
|
||||||
Oh snap, it does not seem like you have connected any applications. To connect your application to Unleash
|
Oh snap, it does not seem like you have connected any applications. To connect your application to Unleash
|
||||||
you will require a Client SDK.
|
you will require a Client SDK.
|
||||||
@ -15,7 +17,7 @@ const Empty = () => (
|
|||||||
<br />
|
<br />
|
||||||
You can read more about how to use Unleash in your application in the{' '}
|
You can read more about how to use Unleash in your application in the{' '}
|
||||||
<a href="https://www.unleash-hosted.com/docs/use-feature-toggle">documentation.</a>
|
<a href="https://www.unleash-hosted.com/docs/use-feature-toggle">documentation.</a>
|
||||||
</CardText>
|
</section>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -35,20 +37,22 @@ class ClientStrategies extends Component {
|
|||||||
const { applications } = this.props;
|
const { applications } = this.props;
|
||||||
|
|
||||||
if (!applications) {
|
if (!applications) {
|
||||||
return <ProgressBar indeterminate />;
|
return <CircularProgress variant="indeterminate" />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<div className={commonStyles.toolbar}>
|
<div className={commonStyles.searchField}>
|
||||||
<SearchField
|
<SearchField
|
||||||
value={this.props.settings.filter}
|
value={this.props.settings.filter}
|
||||||
updateValue={this.props.updateSetting.bind(this, 'filter')}
|
updateValue={this.props.updateSetting.bind(this, 'filter')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
<PageContent headerContent={<HeaderTitle title="Applications" />}>
|
||||||
|
<div className={commonStyles.fullwidth}>
|
||||||
{applications.length > 0 ? <AppsLinkList apps={applications} /> : <Empty />}
|
{applications.length > 0 ? <AppsLinkList apps={applications} /> : <Empty />}
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,51 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, Cell } from 'react-mdl';
|
import { TextField, Grid } from '@material-ui/core';
|
||||||
import StatefulTextfield from './stateful-textfield';
|
import { useCommonStyles } from '../../common.styles';
|
||||||
import icons from './icon-names';
|
import icons from './icon-names';
|
||||||
import MySelect from '../common/select';
|
import MySelect from '../common/select';
|
||||||
|
|
||||||
function ApplicationUpdate({ application, storeApplicationMetaData }) {
|
function ApplicationUpdate({ application, storeApplicationMetaData }) {
|
||||||
const { appName, icon, url, description } = application;
|
const { appName, icon, url, description } = application;
|
||||||
|
const [localUrl, setLocalUrl] = useState(url);
|
||||||
|
const [localDescription, setLocalDescription] = useState(description);
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid>
|
<Grid container style={{ marginTop: '1rem' }}>
|
||||||
<Cell col={12}>
|
<Grid item sm={12} xs={12} className={commonStyles.contentSpacingY}>
|
||||||
|
<Grid item>
|
||||||
<MySelect
|
<MySelect
|
||||||
label="Icon"
|
label="Icon"
|
||||||
options={icons.map(v => ({ key: v, label: v }))}
|
options={icons.map(v => ({ key: v, label: v }))}
|
||||||
value={icon || 'apps'}
|
value={icon || 'apps'}
|
||||||
onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)}
|
onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)}
|
||||||
filled
|
|
||||||
/>
|
/>
|
||||||
<StatefulTextfield
|
</Grid>
|
||||||
value={url}
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
value={localUrl}
|
||||||
|
onChange={e => setLocalUrl(e.target.value)}
|
||||||
label="Application URL"
|
label="Application URL"
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
type="url"
|
type="url"
|
||||||
onBlur={e => storeApplicationMetaData(appName, 'url', e.target.value)}
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onBlur={() => storeApplicationMetaData(appName, 'url', localUrl)}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
<br />
|
<Grid item>
|
||||||
<StatefulTextfield
|
<TextField
|
||||||
value={description}
|
value={localDescription}
|
||||||
label="Description"
|
label="Description"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
rows={2}
|
rows={2}
|
||||||
onBlur={e => storeApplicationMetaData(appName, 'description', e.target.value)}
|
onChange={e => setLocalDescription(e.target.value)}
|
||||||
|
onBlur={() => storeApplicationMetaData(appName, 'description', localDescription)}
|
||||||
/>
|
/>
|
||||||
</Cell>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,99 +1,151 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, Cell, List, ListItem, ListItemContent, Switch } from 'react-mdl';
|
import { Grid, List, ListItem, ListItemText, ListItemAvatar, Switch, Icon, Typography } from '@material-ui/core';
|
||||||
import { shorten } from '../common';
|
import { shorten } from '../common';
|
||||||
import { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions';
|
import { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions';
|
||||||
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) {
|
function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) {
|
||||||
return (
|
const notFoundListItem = ({ createUrl, name, permission }) => (
|
||||||
<Grid style={{ margin: 0 }}>
|
<ConditionallyRender
|
||||||
<Cell col={6} tablet={4} phone={12} hidePhone>
|
key={`not_found_conditional_${name}`}
|
||||||
<h6> Toggles</h6>
|
condition={hasPermission(permission)}
|
||||||
<hr />
|
show={
|
||||||
<List>
|
<ListItem key={`not_found_${name}`}>
|
||||||
{seenToggles.map(({ name, description, enabled, notFound }, i) =>
|
<ListItemAvatar>
|
||||||
notFound ? (
|
<Icon>report</Icon>
|
||||||
<ListItem twoLine key={i}>
|
</ListItemAvatar>
|
||||||
{hasPermission(CREATE_FEATURE) ? (
|
<ListItemText
|
||||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
primary={<Link to={`${createUrl}?name=${name}`}>{name}</Link>}
|
||||||
<Link to={`/features/create?name=${name}`}>{name}</Link>
|
secondary={'Missing, want to create?'}
|
||||||
</ListItemContent>
|
/>
|
||||||
) : (
|
|
||||||
<ListItemContent icon={'report'} subtitle={'Missing'}>
|
|
||||||
{name}
|
|
||||||
</ListItemContent>
|
|
||||||
)}
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : (
|
|
||||||
<ListItem twoLine key={i}>
|
|
||||||
<ListItemContent
|
|
||||||
icon={
|
|
||||||
<span>
|
|
||||||
<Switch disabled checked={!!enabled} />
|
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
subtitle={shorten(description, 60)}
|
elseShow={
|
||||||
>
|
<ListItem key={`not_found_${name}`}>
|
||||||
<Link to={`/features/view/${name}`}>{shorten(name, 50)}</Link>
|
<ListItemAvatar>
|
||||||
</ListItemContent>
|
<Icon>report</Icon>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={name} secondary={`Could not find feature toggle with name ${name}`} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)
|
}
|
||||||
)}
|
/>
|
||||||
</List>
|
);
|
||||||
</Cell>
|
|
||||||
<Cell col={6} tablet={4} phone={12}>
|
// eslint-disable-next-line react/prop-types
|
||||||
<h6>Implemented strategies</h6>
|
const foundListItem = ({ viewUrl, name, showSwitch, enabled, description, i }) => (
|
||||||
|
<ListItem key={`found_${name}-${i}`}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<ConditionallyRender
|
||||||
|
key={`conditional_avatar_${name}`}
|
||||||
|
condition={showSwitch}
|
||||||
|
show={<Switch disabled value={!!enabled} />}
|
||||||
|
elseShow={<Icon>extension</Icon>}
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={<Link to={`${viewUrl}/${name}`}>{shorten(name, 50)}</Link>}
|
||||||
|
secondary={shorten(description, 60)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Grid container style={{ margin: 0 }}>
|
||||||
|
<Grid item xl={6} md={6} xs={12}>
|
||||||
|
<Typography variant="subtitle1" style={{ padding: '1rem 0' }}>
|
||||||
|
Toggles
|
||||||
|
</Typography>
|
||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
{strategies.map(({ name, description, notFound }, i) =>
|
{seenToggles.map(({ name, description, enabled, notFound }, i) => (
|
||||||
notFound ? (
|
<ConditionallyRender
|
||||||
<ListItem twoLine key={`${name}-${i}`}>
|
key={`toggle_conditional_${name}`}
|
||||||
{hasPermission(CREATE_STRATEGY) ? (
|
condition={notFound}
|
||||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
show={notFoundListItem({
|
||||||
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
|
createUrl: '/features/create',
|
||||||
</ListItemContent>
|
name,
|
||||||
) : (
|
permission: CREATE_FEATURE,
|
||||||
<ListItemContent icon={'report'} subtitle={'Missing'}>
|
i,
|
||||||
{name}
|
})}
|
||||||
</ListItemContent>
|
elseShow={foundListItem({
|
||||||
)}
|
viewUrl: '/features/strategies',
|
||||||
</ListItem>
|
name,
|
||||||
) : (
|
showSwitch: true,
|
||||||
<ListItem twoLine key={`${name}-${i}`}>
|
enabled,
|
||||||
<ListItemContent icon={'extension'} subtitle={shorten(description, 60)}>
|
description,
|
||||||
<Link to={`/strategies/view/${name}`}>{shorten(name, 50)}</Link>
|
i,
|
||||||
</ListItemContent>
|
})}
|
||||||
</ListItem>
|
/>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</List>
|
</List>
|
||||||
</Cell>
|
</Grid>
|
||||||
<Cell col={12} tablet={12}>
|
<Grid item xl={6} md={6} xs={12}>
|
||||||
<h6>{instances.length} Instances registered</h6>
|
<Typography variant="subtitle1" style={{ padding: '1rem 0' }}>
|
||||||
|
Implemented strategies
|
||||||
|
</Typography>
|
||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }, i) => (
|
{strategies.map(({ name, description, notFound }, i) => (
|
||||||
<ListItem key={i} twoLine>
|
<ConditionallyRender
|
||||||
<ListItemContent
|
key={`strategies_conditional_${name}`}
|
||||||
icon="timeline"
|
condition={notFound}
|
||||||
subtitle={
|
show={notFoundListItem({
|
||||||
|
createUrl: '/strategies/create',
|
||||||
|
name,
|
||||||
|
permission: CREATE_STRATEGY,
|
||||||
|
i,
|
||||||
|
})}
|
||||||
|
elseShow={foundListItem({
|
||||||
|
viewUrl: '/strategies/view',
|
||||||
|
name,
|
||||||
|
showSwitch: false,
|
||||||
|
enabled: undefined,
|
||||||
|
description,
|
||||||
|
i,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xl={12} md={12}>
|
||||||
|
<Typography variant="subtitle1" style={{ padding: '1rem 0' }}>
|
||||||
|
{instances.length} Instances registered
|
||||||
|
</Typography>
|
||||||
|
<hr />
|
||||||
|
<List>
|
||||||
|
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }) => (
|
||||||
|
<ListItem key={`${instanceId}`}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Icon>timeline</Icon>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<ConditionallyRender
|
||||||
|
key={`${instanceId}_conditional`}
|
||||||
|
condition={sdkVersion}
|
||||||
|
show={`${instanceId} (${sdkVersion})`}
|
||||||
|
elseShow={instanceId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
<span>
|
<span>
|
||||||
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small>
|
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
{instanceId} {sdkVersion ? `(${sdkVersion})` : ''}
|
|
||||||
</ListItemContent>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Cell>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationView.propTypes = {
|
ApplicationView.propTypes = {
|
||||||
|
createUrl: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
permission: PropTypes.string,
|
||||||
instances: PropTypes.array.isRequired,
|
instances: PropTypes.array.isRequired,
|
||||||
seenToggles: PropTypes.array.isRequired,
|
seenToggles: PropTypes.array.isRequired,
|
||||||
strategies: PropTypes.array.isRequired,
|
strategies: PropTypes.array.isRequired,
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Textfield } from 'react-mdl';
|
|
||||||
|
|
||||||
function StatefulTextfield({ value, label, placeholder, rows, onBlur }) {
|
|
||||||
const [localValue, setLocalValue] = useState(value);
|
|
||||||
|
|
||||||
const onChange = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLocalValue(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Textfield
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
label={label}
|
|
||||||
placeholder={placeholder}
|
|
||||||
floatingLabel
|
|
||||||
rows={rows}
|
|
||||||
value={localValue}
|
|
||||||
onChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StatefulTextfield.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
rows: PropTypes.number,
|
|
||||||
onBlur: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatefulTextfield;
|
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import FeatureListComponent from './../feature/list/list-component';
|
import FeatureListComponent from '../feature/FeatureToggleList/FeatureToggleList';
|
||||||
import { fetchArchive, revive } from './../../store/archive/actions';
|
import { fetchArchive, revive } from './../../store/archive/actions';
|
||||||
import { updateSettingForGroup } from './../../store/settings/actions';
|
import { updateSettingForGroup } from './../../store/settings/actions';
|
||||||
import { mapStateToPropsConfigurable } from '../feature/list/list-container';
|
import { mapStateToPropsConfigurable } from '../feature/FeatureToggleList';
|
||||||
|
|
||||||
const mapStateToProps = mapStateToPropsConfigurable(false);
|
const mapStateToProps = mapStateToPropsConfigurable(false);
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchArchive,
|
fetcher: () => fetchArchive(),
|
||||||
revive,
|
revive,
|
||||||
updateSetting: updateSettingForGroup('feature'),
|
updateSetting: updateSettingForGroup('feature'),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchArchive, revive } from './../../store/archive/actions';
|
import { fetchArchive, revive } from './../../store/archive/actions';
|
||||||
import ViewToggleComponent from './../feature/view/view-component';
|
import ViewToggleComponent from '../feature/FeatureView/FeatureView';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../permissions';
|
||||||
import { fetchTags } from '../../store/feature-tags/actions';
|
import { fetchTags } from '../../store/feature-tags/actions';
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import ConditionallyRender from './ConditionallyRender';
|
||||||
|
|
||||||
|
export default ConditionallyRender;
|
||||||
37
frontend/src/component/common/Dialogue/Dialogue.jsx
Normal file
37
frontend/src/component/common/Dialogue/Dialogue.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogTitle, DialogActions, DialogContent, Button } from '@material-ui/core';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const ConfirmDialogue = ({ children, open, onClick, onClose, title, primaryButtonText, secondaryButtonText }) => (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-labelledby={'simple-modal-title'}
|
||||||
|
aria-describedby={'simple-modal-description'}
|
||||||
|
>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<ConditionallyRender condition={children} show={<DialogContent>{children}</DialogContent>} />
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button color="primary" onClick={onClick} autoFocus>
|
||||||
|
{primaryButtonText || "Yes, I'm sure"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose}>{secondaryButtonText || 'No take me back.'} </Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
|
||||||
|
ConfirmDialogue.propTypes = {
|
||||||
|
primaryButtonText: PropTypes.string,
|
||||||
|
secondaryButtonText: PropTypes.string,
|
||||||
|
children: PropTypes.object,
|
||||||
|
open: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
ariaLabel: PropTypes.string,
|
||||||
|
ariaDescription: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmDialogue;
|
||||||
3
frontend/src/component/common/Dialogue/index.jsx
Normal file
3
frontend/src/component/common/Dialogue/index.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Dialogue from './Dialogue';
|
||||||
|
|
||||||
|
export default Dialogue;
|
||||||
36
frontend/src/component/common/HeaderTitle/HeaderTitle.jsx
Normal file
36
frontend/src/component/common/HeaderTitle/HeaderTitle.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Typography } from '@material-ui/core';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
const HeaderTitle = ({ title, actions, subtitle, variant, loading }) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const headerClasses = classnames({ skeleton: loading });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.headerTitleContainer}>
|
||||||
|
<div className={headerClasses}>
|
||||||
|
<Typography variant={variant || 'h2'} className={styles.headerTitle}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{subtitle && <small>{subtitle}</small>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConditionallyRender condition={actions} show={<div className={styles.headerActions}>{actions}</div>} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderTitle;
|
||||||
|
|
||||||
|
HeaderTitle.propTypes = {
|
||||||
|
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
|
||||||
|
subtitle: PropTypes.string,
|
||||||
|
variant: PropTypes.string,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
actions: PropTypes.element,
|
||||||
|
};
|
||||||
3
frontend/src/component/common/HeaderTitle/index.jsx
Normal file
3
frontend/src/component/common/HeaderTitle/index.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import HeaderTitle from './HeaderTitle';
|
||||||
|
|
||||||
|
export default HeaderTitle;
|
||||||
18
frontend/src/component/common/HeaderTitle/styles.js
Normal file
18
frontend/src/component/common/HeaderTitle/styles.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
headerTitleContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
51
frontend/src/component/common/PageContent/PageContent.jsx
Normal file
51
frontend/src/component/common/PageContent/PageContent.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import HeaderTitle from '../HeaderTitle';
|
||||||
|
import { Paper } from '@material-ui/core';
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
const PageContent = ({ children, headerContent, disablePadding, disableBorder, ...rest }) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const headerClasses = classnames(styles.headerContainer, {
|
||||||
|
[styles.paddingDisabled]: disablePadding,
|
||||||
|
[styles.borderDisabled]: disableBorder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyClasses = classnames(styles.bodyContainer, {
|
||||||
|
[styles.paddingDisabled]: disablePadding,
|
||||||
|
[styles.borderDisabled]: disableBorder,
|
||||||
|
});
|
||||||
|
|
||||||
|
let header = null;
|
||||||
|
if (headerContent) {
|
||||||
|
if (typeof headerContent === 'string') {
|
||||||
|
header = (
|
||||||
|
<div className={headerClasses}>
|
||||||
|
<HeaderTitle title={headerContent} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
header = <div className={headerClasses}>{headerContent}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const paperProps = disableBorder ? { elevation: 0 } : {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper {...rest} {...paperProps}>
|
||||||
|
{header}
|
||||||
|
<div className={bodyClasses}>{children}</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageContent;
|
||||||
|
|
||||||
|
PageContent.propTypes = {
|
||||||
|
headerContent: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
|
||||||
|
disablePadding: PropTypes.bool,
|
||||||
|
disableBorder: PropTypes.bool,
|
||||||
|
};
|
||||||
3
frontend/src/component/common/PageContent/index.jsx
Normal file
3
frontend/src/component/common/PageContent/index.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import PageContent from './PageContent';
|
||||||
|
|
||||||
|
export default PageContent;
|
||||||
23
frontend/src/component/common/PageContent/styles.js
Normal file
23
frontend/src/component/common/PageContent/styles.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
headerContainer: {
|
||||||
|
padding: theme.padding.pageContent.header,
|
||||||
|
borderBottom: theme.borders.default,
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
padding: '1.5rem 1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bodyContainer: {
|
||||||
|
padding: theme.padding.pageContent.body,
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
padding: '1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paddingDisabled: {
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
borderDisabled: {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MenuItem } from '@material-ui/core';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import DropdownMenu from '../dropdown-menu';
|
||||||
|
|
||||||
|
const ALL_PROJECTS = { id: '*', name: '> All projects' };
|
||||||
|
|
||||||
|
const ProjectSelect = ({ projects, currentProjectId, updateCurrentProject }) => {
|
||||||
|
const setProject = v => {
|
||||||
|
const id = typeof v === 'string' ? v.trim() : '';
|
||||||
|
updateCurrentProject(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projects || projects.length === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO fixme
|
||||||
|
let curentProject = projects.find(i => i.id === currentProjectId);
|
||||||
|
if (!curentProject) {
|
||||||
|
curentProject = ALL_PROJECTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeProject = e => {
|
||||||
|
const target = e.target.getAttribute('data-target');
|
||||||
|
setProject(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderProjectItem = (selectedId, item) => (
|
||||||
|
<MenuItem disabled={selectedId === item.id} data-target={item.id} key={item.id}>
|
||||||
|
{item.name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderProjectOptions = () => {
|
||||||
|
const start = [
|
||||||
|
<MenuItem disabled={curentProject === ALL_PROJECTS} data-target={ALL_PROJECTS.id}>
|
||||||
|
{ALL_PROJECTS.name}
|
||||||
|
</MenuItem>,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...start, ...projects.map(p => renderProjectItem(currentProjectId, p))];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DropdownMenu
|
||||||
|
id={'project'}
|
||||||
|
title="Select project"
|
||||||
|
label={`${curentProject.name}`}
|
||||||
|
callback={handleChangeProject}
|
||||||
|
renderOptions={renderProjectOptions}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectSelect.propTypes = {
|
||||||
|
projects: PropTypes.array.isRequired,
|
||||||
|
fetchProjects: PropTypes.func.isRequired,
|
||||||
|
currentProjectId: PropTypes.string.isRequired,
|
||||||
|
updateCurrentProject: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectSelect;
|
||||||
@ -1,13 +1,11 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Component from './project-component';
|
import ProjectSelect from './ProjectSelect';
|
||||||
import { fetchProjects } from './../../../store/project/actions';
|
import { fetchProjects } from '../../../store/project/actions';
|
||||||
import { P } from '../../common/flags';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
enabled: !!state.uiConfig.toJS().flags[P],
|
|
||||||
projects: state.projects.toJS(),
|
projects: state.projects.toJS(),
|
||||||
currentProjectId: ownProps.settings.currentProjectId || '*',
|
currentProjectId: ownProps.settings.currentProjectId || '*',
|
||||||
updateCurrentProject: id => ownProps.updateSetting('currentProjectId', id),
|
updateCurrentProject: id => ownProps.updateSetting('currentProjectId', id),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, { fetchProjects })(Component);
|
export default connect(mapStateToProps, { fetchProjects })(ProjectSelect);
|
||||||
59
frontend/src/component/common/SearchField/SearchField.jsx
Normal file
59
frontend/src/component/common/SearchField/SearchField.jsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { debounce } from 'debounce';
|
||||||
|
import { InputBase } from '@material-ui/core';
|
||||||
|
import SearchIcon from '@material-ui/icons/Search';
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
function SearchField({ value = '', updateValue, className }) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const [localValue, setLocalValue] = useState(value);
|
||||||
|
const debounceUpdateValue = debounce(updateValue, 500);
|
||||||
|
|
||||||
|
const handleCange = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const v = e.target.value || '';
|
||||||
|
setLocalValue(v);
|
||||||
|
debounceUpdateValue(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
updateValue(localValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateNow = () => {
|
||||||
|
updateValue(localValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={classnames(styles.search, className)}>
|
||||||
|
<SearchIcon className={styles.searchIcon} />
|
||||||
|
<InputBase
|
||||||
|
placeholder="Search…"
|
||||||
|
classes={{
|
||||||
|
root: styles.inputRoot,
|
||||||
|
input: styles.input,
|
||||||
|
}}
|
||||||
|
inputProps={{ 'aria-label': 'search' }}
|
||||||
|
value={localValue}
|
||||||
|
onChange={handleCange}
|
||||||
|
onBlur={updateNow}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchField.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
updateValue: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchField;
|
||||||
3
frontend/src/component/common/SearchField/index.jsx
Normal file
3
frontend/src/component/common/SearchField/index.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SearchField from './SearchField';
|
||||||
|
|
||||||
|
export default SearchField;
|
||||||
24
frontend/src/component/common/SearchField/styles.js
Normal file
24
frontend/src/component/common/SearchField/styles.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
search: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: theme.palette.searchField.main,
|
||||||
|
borderRadius: theme.borders.radius.main,
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
maxWidth: '450px',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
margin: '0 auto',
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchIcon: {
|
||||||
|
marginRight: '8px',
|
||||||
|
},
|
||||||
|
inputRoot: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
63
frontend/src/component/common/TabNav/TabNav.jsx
Normal file
63
frontend/src/component/common/TabNav/TabNav.jsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Tabs, Tab, Paper } from '@material-ui/core';
|
||||||
|
|
||||||
|
import TabPanel from './TabPanel';
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
const a11yProps = index => ({
|
||||||
|
id: `tab-${index}`,
|
||||||
|
'aria-controls': `tabpanel-${index}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const TabNav = ({ tabData, className, startingTab = 0 }) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const [activeTab, setActiveTab] = useState(startingTab);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const renderTabs = () =>
|
||||||
|
tabData.map((tab, index) => (
|
||||||
|
<Tab
|
||||||
|
key={`${tab.label}_${index}`}
|
||||||
|
label={tab.label}
|
||||||
|
{...a11yProps(index)}
|
||||||
|
onClick={() => history.push(tab.path)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const renderTabPanels = () =>
|
||||||
|
tabData.map((tab, index) => (
|
||||||
|
<TabPanel key={`tab_panel_${index}`} value={activeTab} index={index}>
|
||||||
|
{tab.component}
|
||||||
|
</TabPanel>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Paper className={styles.tabNav}>
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(_, tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
}}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
{renderTabs()}
|
||||||
|
</Tabs>
|
||||||
|
</Paper>
|
||||||
|
<div className={className}>{renderTabPanels()}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TabNav.propTypes = {
|
||||||
|
tabData: PropTypes.array.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
startingTab: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabNav;
|
||||||
22
frontend/src/component/common/TabNav/TabPanel/TabPanel.jsx
Normal file
22
frontend/src/component/common/TabNav/TabPanel/TabPanel.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const TabPanel = ({ children, value, index, ...other }) => (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`wrapped-tabpanel-${index}`}
|
||||||
|
aria-labelledby={`wrapped-tab-${index}`}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{value === index && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
TabPanel.propTypes = {
|
||||||
|
value: PropTypes.number,
|
||||||
|
index: PropTypes.number,
|
||||||
|
children: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabPanel;
|
||||||
3
frontend/src/component/common/TabNav/TabPanel/index.jsx
Normal file
3
frontend/src/component/common/TabNav/TabPanel/index.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import TabPanel from './TabPanel';
|
||||||
|
|
||||||
|
export default TabPanel;
|
||||||
3
frontend/src/component/common/TabNav/index.jsx
Normal file
3
frontend/src/component/common/TabNav/index.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import TabNav from './TabNav';
|
||||||
|
|
||||||
|
export default TabNav;
|
||||||
7
frontend/src/component/common/TabNav/styles.js
Normal file
7
frontend/src/component/common/TabNav/styles.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
tabNav: {
|
||||||
|
backgroundColor: theme.palette.tabs.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
/** Select **/
|
||||||
|
|
||||||
.truncate {
|
.truncate {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -42,21 +44,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 64px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 20px 16px 20px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleText {
|
.titleText {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 24px
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 20px 14px 20px 16px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,3 +103,29 @@
|
|||||||
.error {
|
.error {
|
||||||
color: #d50000;
|
color: #d50000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerTitle {
|
||||||
|
font-size: var(--h1-size);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 8px 16px 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentPadding {
|
||||||
|
padding: var(--card-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentSpacing > * {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchField {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|||||||
57
frontend/src/component/common/dropdown-menu.jsx
Normal file
57
frontend/src/component/common/dropdown-menu.jsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Menu } from '@material-ui/core';
|
||||||
|
import { DropdownButton } from '.';
|
||||||
|
|
||||||
|
import styles from './common.module.scss';
|
||||||
|
|
||||||
|
const DropdownMenu = ({ renderOptions, id, title, callback, icon = 'arrow_drop_down', label, startIcon, ...rest }) => {
|
||||||
|
const [anchor, setAnchor] = React.useState(null);
|
||||||
|
|
||||||
|
const handleOpen = e => setAnchor(e.currentTarget);
|
||||||
|
|
||||||
|
const handleClose = e => {
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnchor(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownButton
|
||||||
|
id={id}
|
||||||
|
label={label}
|
||||||
|
title={title}
|
||||||
|
startIcon={startIcon}
|
||||||
|
onClick={handleOpen}
|
||||||
|
aria-controls={id}
|
||||||
|
aria-haspopup="true"
|
||||||
|
icon={icon}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
id={id}
|
||||||
|
className={styles.dropdownMenu}
|
||||||
|
onClick={handleClose}
|
||||||
|
anchorEl={anchor}
|
||||||
|
open={Boolean(anchor)}
|
||||||
|
>
|
||||||
|
{renderOptions()}
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DropdownMenu.propTypes = {
|
||||||
|
renderOptions: PropTypes.func,
|
||||||
|
id: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
callback: PropTypes.func,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
startIcon: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenu;
|
||||||
@ -1,62 +1,65 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { List, ListItem, ListItemContent, Button, Icon, Switch, MenuItem } from 'react-mdl';
|
import {
|
||||||
|
List,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar,
|
||||||
|
Button,
|
||||||
|
Avatar,
|
||||||
|
Typography,
|
||||||
|
} from '@material-ui/core';
|
||||||
import styles from './common.module.scss';
|
import styles from './common.module.scss';
|
||||||
|
import ConditionallyRender from './ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
export { styles };
|
export { styles };
|
||||||
|
|
||||||
export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str);
|
export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str);
|
||||||
|
|
||||||
export const AppsLinkList = ({ apps }) => (
|
export const AppsLinkList = ({ apps }) => (
|
||||||
<List>
|
<List>
|
||||||
{apps.length > 0 &&
|
<ConditionallyRender
|
||||||
apps.map(({ appName, description, icon }) => (
|
condition={apps.length > 0}
|
||||||
<ListItem twoLine key={appName}>
|
show={apps.map(({ appName, description, icon }) => (
|
||||||
<span className="mdl-list__item-primary-content" style={{ minWidth: 0 }}>
|
<ListItem key={appName} className={styles.listItem}>
|
||||||
<Icon name={icon || 'apps'} className="mdl-list__item-avatar" />
|
<ListItemAvatar>
|
||||||
<Link to={`/applications/${appName}`} className={[styles.listLink, styles.truncate].join(' ')}>
|
<Avatar>
|
||||||
|
<ConditionallyRender
|
||||||
|
key={`avatar_conditional_${appName}`}
|
||||||
|
condition={icon}
|
||||||
|
show={<Icon>{icon}</Icon>}
|
||||||
|
elseShow={<Icon>apps</Icon>}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Link
|
||||||
|
to={`/applications/${appName}`}
|
||||||
|
className={[styles.listLink, styles.truncate].join(' ')}
|
||||||
|
>
|
||||||
{appName}
|
{appName}
|
||||||
<span className={['mdl-list__item-sub-title', styles.truncate].join(' ')}>
|
|
||||||
{description || 'No description'}
|
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
}
|
||||||
|
secondary={description || 'No description'}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
|
/>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
AppsLinkList.propTypes = {
|
AppsLinkList.propTypes = {
|
||||||
apps: PropTypes.array.isRequired,
|
apps: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HeaderTitle = ({ title, actions, subtitle }) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
borderBottom: '1px solid #f9f9f9',
|
|
||||||
marginBottom: '10px',
|
|
||||||
padding: '16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ flex: '2' }}>
|
|
||||||
<h6 style={{ margin: 0 }}>{title}</h6>
|
|
||||||
{subtitle && <small>{subtitle}</small>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{actions && <div style={{ flex: '1', textAlign: 'right' }}>{actions}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
HeaderTitle.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
subtitle: PropTypes.string,
|
|
||||||
actions: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DataTableHeader = ({ title, actions }) => (
|
export const DataTableHeader = ({ title, actions }) => (
|
||||||
<div className={styles.dataTableHeader}>
|
<div className={styles.dataTableHeader}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
<h2 className={styles.titleText}>{title}</h2>
|
<Typography variant="h2" className={styles.titleText}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
{actions && <div className={styles.actions}>{actions}</div>}
|
{actions && <div className={styles.actions}>{actions}</div>}
|
||||||
</div>
|
</div>
|
||||||
@ -66,11 +69,15 @@ DataTableHeader.propTypes = {
|
|||||||
actions: PropTypes.any,
|
actions: PropTypes.any,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormButtons = ({ submitText = 'Create', onCancel }) => (
|
export const FormButtons = ({ submitText = 'Create', onCancel, primaryButtonTestId }) => (
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" ripple raised primary icon="add">
|
<Button
|
||||||
<Icon name="add" />
|
data-test={primaryButtonTestId}
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Icon>add</Icon>}
|
||||||
|
>
|
||||||
{submitText}
|
{submitText}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@ -82,37 +89,7 @@ export const FormButtons = ({ submitText = 'Create', onCancel }) => (
|
|||||||
FormButtons.propTypes = {
|
FormButtons.propTypes = {
|
||||||
submitText: PropTypes.string,
|
submitText: PropTypes.string,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
};
|
primaryButtonTestId: PropTypes.string,
|
||||||
|
|
||||||
export const SwitchWithLabel = ({ onChange, checked, children, ...switchProps }) => (
|
|
||||||
<span className={styles.switchWithLabel}>
|
|
||||||
<span className={styles.label}>{children}</span>
|
|
||||||
<span className={styles.switch}>
|
|
||||||
<Switch checked={checked} onChange={onChange} {...switchProps} />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
SwitchWithLabel.propTypes = {
|
|
||||||
checked: PropTypes.bool,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TogglesLinkList = ({ toggles }) => (
|
|
||||||
<List style={{ textAlign: 'left' }} className={styles.truncate}>
|
|
||||||
{toggles.length > 0 &&
|
|
||||||
toggles.map(({ name, description = '-', icon = 'toggle' }) => (
|
|
||||||
<ListItem twoLine key={name}>
|
|
||||||
<ListItemContent avatar={icon} subtitle={description}>
|
|
||||||
<Link key={name} to={`/features/view/${name}`}>
|
|
||||||
{name}
|
|
||||||
</Link>
|
|
||||||
</ListItemContent>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
TogglesLinkList.propTypes = {
|
|
||||||
toggles: PropTypes.array,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getIcon(type) {
|
export function getIcon(type) {
|
||||||
@ -132,7 +109,7 @@ export function getIcon(type) {
|
|||||||
|
|
||||||
export const IconLink = ({ url, icon }) => (
|
export const IconLink = ({ url, icon }) => (
|
||||||
<a href={url} target="_blank" rel="noopener" className="mdl-color-text--grey-600">
|
<a href={url} target="_blank" rel="noopener" className="mdl-color-text--grey-600">
|
||||||
<Icon name={icon} />
|
<Icon>{icon}</Icon>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
IconLink.propTypes = {
|
IconLink.propTypes = {
|
||||||
@ -140,22 +117,41 @@ IconLink.propTypes = {
|
|||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownButton = ({ label, id, className = styles.dropdownButton, title, style }) => (
|
export const DropdownButton = ({
|
||||||
<Button id={id} className={className} title={title} style={style}>
|
label,
|
||||||
|
id,
|
||||||
|
className = styles.dropdownButton,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
startIcon,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}) => (
|
||||||
|
<Button
|
||||||
|
id={id}
|
||||||
|
className={className}
|
||||||
|
title={title}
|
||||||
|
style={style}
|
||||||
|
{...rest}
|
||||||
|
startIcon={startIcon}
|
||||||
|
endIcon={<Icon>{icon}</Icon>}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
DropdownButton.propTypes = {
|
DropdownButton.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
startIcon: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
||||||
<MenuItem disabled={disabled} style={{ display: 'flex', alignItems: 'center' }} {...menuItemProps}>
|
<MenuItem disabled={disabled} style={{ display: 'flex', alignItems: 'center' }} {...menuItemProps}>
|
||||||
<Icon name={icon} style={{ paddingRight: '16px' }} />
|
<Icon style={{ paddingRight: '16px' }}>{icon}</Icon>
|
||||||
{label}
|
{label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Textfield } from 'react-mdl';
|
import { TextField } from '@material-ui/core';
|
||||||
|
|
||||||
function InputListField({ label, values = [], error, name, updateValues, placeholder = '', onBlur = () => {} }) {
|
function InputListField({ label, values = [], error, name, updateValues, placeholder = '', onBlur = () => {} }) {
|
||||||
const handleChange = evt => {
|
const handleChange = evt => {
|
||||||
@ -21,10 +21,10 @@ function InputListField({ label, values = [], error, name, updateValues, placeho
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textfield
|
<TextField
|
||||||
name={name}
|
name={name}
|
||||||
floatingLabel
|
error={error !== undefined}
|
||||||
error={error}
|
helperText={error}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={values ? values.join(', ') : ''}
|
value={values ? values.join(', ') : ''}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@ -32,6 +32,8 @@ function InputListField({ label, values = [], error, name, updateValues, placeho
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
label={label}
|
label={label}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { debounce } from 'debounce';
|
|
||||||
import { FABButton, Icon, Textfield } from 'react-mdl';
|
|
||||||
|
|
||||||
function SearchField({ value = '', updateValue }) {
|
|
||||||
const [localValue, setLocalValue] = useState(value);
|
|
||||||
const debounceUpdateValue = debounce(updateValue, 500);
|
|
||||||
|
|
||||||
const handleCange = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const v = e.target.value || '';
|
|
||||||
setLocalValue(v);
|
|
||||||
debounceUpdateValue(v);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = e => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
updateValue(localValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateNow = () => {
|
|
||||||
updateValue(localValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
value={localValue}
|
|
||||||
onChange={handleCange}
|
|
||||||
onBlur={updateNow}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
label="Search"
|
|
||||||
style={{ width: '500px', maxWidth: '80%' }}
|
|
||||||
/>
|
|
||||||
<FABButton mini className={'mdl-cell--hide-phone'}>
|
|
||||||
<Icon name="search" />
|
|
||||||
</FABButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchField.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
updateValue: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchField;
|
|
||||||
@ -1,50 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import { Select, FormControl, MenuItem, InputLabel } from '@material-ui/core';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const Select = ({ name, value, label, options, style, onChange, disabled = false, filled, className }) => {
|
const SelectMenu = ({ name, value, label, options, onChange, id, disabled = false, className, ...rest }) => {
|
||||||
const wrapper = Object.assign({ width: 'auto' }, style);
|
const renderSelectItems = () =>
|
||||||
|
options.map(option => (
|
||||||
|
<MenuItem key={option.key} value={option.key} title={option.title}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<FormControl variant="outlined" size="small">
|
||||||
className={classnames(
|
<InputLabel htmlFor={id} id={id}>
|
||||||
'mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded',
|
{label}
|
||||||
className
|
</InputLabel>
|
||||||
)}
|
<Select
|
||||||
style={wrapper}
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
className="mdl-textfield__input"
|
|
||||||
name={name}
|
name={name}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
className={className}
|
||||||
|
label={label}
|
||||||
|
id={id}
|
||||||
|
size="small"
|
||||||
value={value}
|
value={value}
|
||||||
style={{
|
{...rest}
|
||||||
width: 'auto',
|
|
||||||
background: filled ? '#f5f5f5' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{options.map(o => (
|
{renderSelectItems()}
|
||||||
<option key={o.key} value={o.key} title={o.title}>
|
</Select>
|
||||||
{o.label}
|
</FormControl>
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<label className="mdl-textfield__label" htmlFor="textfield-contextName">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Select.propTypes = {
|
SelectMenu.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
id: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
filled: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Select;
|
export default SelectMenu;
|
||||||
|
|||||||
65
frontend/src/component/context/Context.module.scss
Normal file
65
frontend/src/component/context/Context.module.scss
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
.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: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueField {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalValueButton {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > *, .inset > * {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
94
frontend/src/component/context/ContextList/ContextList.jsx
Normal file
94
frontend/src/component/context/ContextList/ContextList.jsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../../permissions';
|
||||||
|
import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
import ConfirmDialogue from '../../common/Dialogue';
|
||||||
|
|
||||||
|
const ContextList = ({ removeContextField, hasPermission, history, contextFields }) => {
|
||||||
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
|
const [name, setName] = useState();
|
||||||
|
|
||||||
|
const styles = useStyles();
|
||||||
|
const contextList = () =>
|
||||||
|
contextFields.map(field => (
|
||||||
|
<ListItem key={field.name} classes={{ root: styles.listItem }}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Icon>album</Icon>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Link to={`/context/edit/${field.name}`}>
|
||||||
|
<strong>{field.name}</strong>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
secondary={field.description}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(DELETE_CONTEXT_FIELD)}
|
||||||
|
show={
|
||||||
|
<Tooltip title="Delete context field">
|
||||||
|
<IconButton
|
||||||
|
aria-label="delete"
|
||||||
|
onClick={() => {
|
||||||
|
setName(field.name);
|
||||||
|
setShowDelDialogue(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon>delete</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
));
|
||||||
|
const headerButton = () => (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(CREATE_CONTEXT_FIELD)}
|
||||||
|
show={
|
||||||
|
<Tooltip title="Add context type">
|
||||||
|
<IconButton onClick={() => history.push('/context/create')}>
|
||||||
|
<Icon>add</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<PageContent headerContent={<HeaderTitle actions={headerButton()} title={'Context fields'} />}>
|
||||||
|
<List>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={contextFields.length > 0}
|
||||||
|
show={contextList}
|
||||||
|
elseShow={<ListItem>No context fields defined</ListItem>}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
<ConfirmDialogue
|
||||||
|
open={showDelDialogue}
|
||||||
|
onClick={() => {
|
||||||
|
removeContextField({ name });
|
||||||
|
setName(undefined);
|
||||||
|
setShowDelDialogue(false);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setName(undefined);
|
||||||
|
setShowDelDialogue(false);
|
||||||
|
}}
|
||||||
|
title="Really delete context field"
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextList.propTypes = {
|
||||||
|
contextFields: PropTypes.array.isRequired,
|
||||||
|
removeContextField: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextList;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ContextFieldListComponent from './list-component.jsx';
|
import ContextList from './ContextList';
|
||||||
import { fetchContext, removeContextField } from './../../store/context/actions';
|
import { fetchContext, removeContextField } from '../../../store/context/actions';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const list = state.context.toJS();
|
const list = state.context.toJS();
|
||||||
@ -14,14 +14,11 @@ const mapStateToProps = state => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
removeContextField: contextField => {
|
removeContextField: contextField => {
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
if (window.confirm('Are you sure you want to remove this context field?')) {
|
|
||||||
removeContextField(contextField)(dispatch);
|
removeContextField(contextField)(dispatch);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
fetchContext: () => fetchContext()(dispatch),
|
fetchContext: () => fetchContext()(dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextFieldListComponent);
|
const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextList);
|
||||||
|
|
||||||
export default ContextFieldListContainer;
|
export default ContextFieldListContainer;
|
||||||
7
frontend/src/component/context/ContextList/styles.js
Normal file
7
frontend/src/component/context/ContextList/styles.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
listItem: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Chip, Textfield, Card, CardTitle, CardText, CardActions, Checkbox } from 'react-mdl';
|
import { Button, Chip, TextField, Switch, Icon, Typography } from '@material-ui/core';
|
||||||
|
import styles from './Context.module.scss';
|
||||||
|
import classnames from 'classnames';
|
||||||
import { FormButtons, styles as commonStyles } from '../common';
|
import { FormButtons, styles as commonStyles } from '../common';
|
||||||
import { trim } from '../common/util';
|
import { trim } from '../common/util';
|
||||||
|
import PageContent from '../common/PageContent/PageContent';
|
||||||
|
|
||||||
const sortIgnoreCase = (a, b) => {
|
const sortIgnoreCase = (a, b) => {
|
||||||
a = a.toLowerCase();
|
a = a.toLowerCase();
|
||||||
@ -99,12 +101,10 @@ class AddContextComponent extends Component {
|
|||||||
renderLegalValue = (value, index) => (
|
renderLegalValue = (value, index) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={`${value}:${index}`}
|
key={`${value}:${index}`}
|
||||||
className="mdl-color--blue-grey-100"
|
className={styles.chip}
|
||||||
style={{ marginRight: '4px' }}
|
onDelete={() => this.removeLegalValue(index)}
|
||||||
onClose={() => this.removeLegalValue(index)}
|
label={value}
|
||||||
>
|
/>
|
||||||
{value}
|
|
||||||
</Chip>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -113,65 +113,78 @@ class AddContextComponent extends Component {
|
|||||||
const submitText = editMode ? 'Update' : 'Create';
|
const submitText = editMode ? 'Update' : 'Create';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<PageContent headerContent="Create context field">
|
||||||
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
<div className={styles.supporting}>
|
||||||
Create context field
|
|
||||||
</CardTitle>
|
|
||||||
<CardText>
|
|
||||||
Context fields are a basic building block used in Unleash to control roll-out. They can be used
|
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.
|
together with strategy constraints as part of the activation strategy evaluation.
|
||||||
</CardText>
|
</div>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
<section style={{ padding: '16px' }}>
|
<section className={styles.formContainer}>
|
||||||
<Textfield
|
<TextField
|
||||||
floatingLabel
|
className={commonStyles.fullwidth}
|
||||||
label="Name"
|
label="Name"
|
||||||
name="name"
|
name="name"
|
||||||
value={contextField.name}
|
defaultValue={contextField.name}
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
|
helperText={errors.name}
|
||||||
disabled={editMode}
|
disabled={editMode}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
onBlur={v => this.validateContextName(v.target.value)}
|
onBlur={v => this.validateContextName(v.target.value)}
|
||||||
onChange={v => this.setValue('name', trim(v.target.value))}
|
onChange={v => this.setValue('name', trim(v.target.value))}
|
||||||
/>
|
/>
|
||||||
<Textfield
|
<TextField
|
||||||
floatingLabel
|
className={commonStyles.fullwidth}
|
||||||
style={{ width: '100%' }}
|
rowsMax={1}
|
||||||
rows={1}
|
|
||||||
label="Description"
|
label="Description"
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
value={contextField.description}
|
helperText={errors.description}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
defaultValue={contextField.description}
|
||||||
onChange={v => this.setValue('description', v.target.value)}
|
onChange={v => this.setValue('description', v.target.value)}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<section style={{ padding: '16px', background: '#fafafa' }}>
|
</section>
|
||||||
<h6 style={{ marginTop: '0' }}>Legal values</h6>
|
<section className={styles.inset}>
|
||||||
<p style={{ color: 'rgba(0,0,0,.54)' }}>
|
<h6 className={styles.h6}>Legal values</h6>
|
||||||
By defining the legal values the Unleash Admin UI will validate the user input. A
|
<p className={styles.alpha}>
|
||||||
concrete example would be that we know all values for our “environment” (local,
|
By defining the legal values the Unleash Admin UI will validate the user input. A concrete
|
||||||
development, stage, production).
|
example would be that we know all values for our “environment” (local, development, stage,
|
||||||
|
production).
|
||||||
</p>
|
</p>
|
||||||
<Textfield
|
<div>
|
||||||
floatingLabel
|
<TextField
|
||||||
label="Value"
|
label="Value"
|
||||||
name="value"
|
name="value"
|
||||||
style={{ width: '130px' }}
|
className={styles.valueField}
|
||||||
value={this.state.currentLegalValue}
|
value={this.state.currentLegalValue}
|
||||||
error={errors.currentLegalValue}
|
error={!!errors.currentLegalValue}
|
||||||
|
helperText={errors.currentLegalValue}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
onChange={this.updateCurrentLegalValue}
|
onChange={this.updateCurrentLegalValue}
|
||||||
/>
|
/>
|
||||||
<Button onClick={this.addLegalValue} colored accent raised>
|
<Button
|
||||||
|
className={styles.legalValueButton}
|
||||||
|
startIcon={<Icon>add</Icon>}
|
||||||
|
onClick={this.addLegalValue}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
||||||
</section>
|
</section>
|
||||||
<br />
|
<br />
|
||||||
<section style={{ padding: '16px' }}>
|
<section>
|
||||||
<h6 style={{ marginTop: '0' }}>Custom stickiness (beta)</h6>
|
<Typography variant="subtitle1">Custom stickiness (beta)</Typography>
|
||||||
<p style={{ color: 'rgba(0,0,0,.54)' }}>
|
<p className={classnames(styles.alpha, styles.formContainer)}>
|
||||||
By enabling stickiness on this context field you can use it together with the
|
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
|
flexible-rollout strategy. This will guarantee a consistent behavior for specific values of
|
||||||
of this context field. PS! Not all client SDK's support this feature yet!{' '}
|
this context field. PS! Not all client SDK's support this feature yet!{' '}
|
||||||
<a
|
<a
|
||||||
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
|
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -179,19 +192,17 @@ class AddContextComponent extends Component {
|
|||||||
Read more
|
Read more
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<Checkbox
|
<Switch
|
||||||
label="Allow stickiness"
|
label="Allow stickiness"
|
||||||
ripple
|
value={contextField.stickiness}
|
||||||
checked={contextField.stickiness}
|
|
||||||
onChange={() => this.setValue('stickiness', !contextField.stickiness)}
|
onChange={() => this.setValue('stickiness', !contextField.stickiness)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
<div className={styles.formButtons}>
|
||||||
<CardActions>
|
|
||||||
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||||
</CardActions>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { List, ListItem, ListItemAction, ListItemContent, IconButton, Card } from 'react-mdl';
|
|
||||||
import { HeaderTitle, styles as commonStyles } from '../common';
|
|
||||||
import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../permissions';
|
|
||||||
|
|
||||||
class ContextFieldListComponent extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
contextFields: PropTypes.array.isRequired,
|
|
||||||
fetchContext: PropTypes.func.isRequired,
|
|
||||||
removeContextField: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// this.props.fetchContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeContextField = (contextField, evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.props.removeContextField(contextField);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { contextFields, hasPermission } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
|
||||||
<HeaderTitle
|
|
||||||
title="Context Fields"
|
|
||||||
actions={
|
|
||||||
hasPermission(CREATE_CONTEXT_FIELD) ? (
|
|
||||||
<IconButton
|
|
||||||
raised
|
|
||||||
colored
|
|
||||||
accent
|
|
||||||
name="add"
|
|
||||||
onClick={() => this.props.history.push('/context/create')}
|
|
||||||
title="Add new context field"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<List>
|
|
||||||
{contextFields.length > 0 ? (
|
|
||||||
contextFields.map((field, i) => (
|
|
||||||
<ListItem key={i} twoLine>
|
|
||||||
<ListItemContent icon="album" subtitle={field.description}>
|
|
||||||
<Link to={`/context/edit/${field.name}`}>
|
|
||||||
<strong>{field.name}</strong>
|
|
||||||
</Link>
|
|
||||||
</ListItemContent>
|
|
||||||
<ListItemAction>
|
|
||||||
{hasPermission(DELETE_CONTEXT_FIELD) ? (
|
|
||||||
<IconButton
|
|
||||||
name="delete"
|
|
||||||
title="Remove contextField"
|
|
||||||
onClick={this.removeContextField.bind(this, field)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</ListItemAction>
|
|
||||||
</ListItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<ListItem>No context fields defined</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContextFieldListComponent;
|
|
||||||
@ -1,16 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Snackbar, Icon } from 'react-mdl';
|
import { Snackbar, Icon, IconButton } from '@material-ui/core';
|
||||||
|
|
||||||
const ErrorComponent = ({ errors, ...props }) => {
|
const ErrorComponent = ({ errors, ...props }) => {
|
||||||
const showError = errors.length > 0;
|
const showError = errors.length > 0;
|
||||||
const error = showError ? errors[0] : undefined;
|
const error = showError ? errors[0] : undefined;
|
||||||
const muteError = () => props.muteError(error);
|
const muteError = () => props.muteError(error);
|
||||||
return (
|
return (
|
||||||
<Snackbar action="Dismiss" active={showError} onActionClick={muteError} onTimeout={muteError} timeout={10000}>
|
<Snackbar
|
||||||
<Icon name="question_answer" /> {error}
|
action={
|
||||||
</Snackbar>
|
<React.Fragment>
|
||||||
|
<IconButton size="small" aria-label="close" color="inherit" onClick={muteError}>
|
||||||
|
<Icon>close</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
open={showError}
|
||||||
|
onClose={muteError}
|
||||||
|
autoHideDuration={10000}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<Icon>question_answer</Icon>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,172 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Button, List, Tooltip, IconButton, Icon } from '@material-ui/core';
|
||||||
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
|
|
||||||
|
import FeatureToggleListItem from './FeatureToggleListItem';
|
||||||
|
import SearchField from '../../common/SearchField/SearchField';
|
||||||
|
import FeatureToggleListActions from './FeatureToggleListActions';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
|
||||||
|
import loadingFeatures from './loadingFeatures';
|
||||||
|
|
||||||
|
import { CREATE_FEATURE } from '../../../permissions';
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
const FeatureToggleList = ({
|
||||||
|
fetcher,
|
||||||
|
features,
|
||||||
|
hasPermission,
|
||||||
|
settings,
|
||||||
|
revive,
|
||||||
|
updateSetting,
|
||||||
|
featureMetrics,
|
||||||
|
toggleFeature,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetcher();
|
||||||
|
}, [fetcher]);
|
||||||
|
|
||||||
|
const toggleMetrics = () => {
|
||||||
|
updateSetting('showLastHour', !settings.showLastHour);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSort = v => {
|
||||||
|
updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFeatures = () => {
|
||||||
|
features.forEach(e => {
|
||||||
|
e.reviveName = e.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return loadingFeatures.map(feature => (
|
||||||
|
<FeatureToggleListItem
|
||||||
|
key={feature.name}
|
||||||
|
settings={settings}
|
||||||
|
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||||
|
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||||
|
feature={feature}
|
||||||
|
toggleFeature={toggleFeature}
|
||||||
|
revive={revive}
|
||||||
|
hasPermission={hasPermission}
|
||||||
|
className={'skeleton'}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return features.map(feature => (
|
||||||
|
<FeatureToggleListItem
|
||||||
|
key={feature.name}
|
||||||
|
settings={settings}
|
||||||
|
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||||
|
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||||
|
feature={feature}
|
||||||
|
toggleFeature={toggleFeature}
|
||||||
|
revive={revive}
|
||||||
|
hasPermission={hasPermission}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.featureContainer}>
|
||||||
|
<div className={styles.searchBarContainer}>
|
||||||
|
<SearchField
|
||||||
|
value={settings.filter}
|
||||||
|
updateValue={updateSetting.bind(this, 'filter')}
|
||||||
|
className={classnames(styles.searchBar, {
|
||||||
|
skeleton: loading,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PageContent
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
loading={loading}
|
||||||
|
title="Feature toggles"
|
||||||
|
actions={
|
||||||
|
<div className={styles.actionsContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!smallScreen}
|
||||||
|
show={
|
||||||
|
<FeatureToggleListActions
|
||||||
|
settings={settings}
|
||||||
|
toggleMetrics={toggleMetrics}
|
||||||
|
setSort={setSort}
|
||||||
|
updateSetting={updateSetting}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(CREATE_FEATURE)}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={smallScreen}
|
||||||
|
show={
|
||||||
|
<Tooltip title="Create feature toggle">
|
||||||
|
<IconButton
|
||||||
|
component={Link}
|
||||||
|
to="/features/create"
|
||||||
|
data-test="add-feature-btn"
|
||||||
|
>
|
||||||
|
<Icon>add</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Button
|
||||||
|
to="/features/create"
|
||||||
|
data-test="add-feature-btn"
|
||||||
|
size="large"
|
||||||
|
color="secondary"
|
||||||
|
variant="contained"
|
||||||
|
component={Link}
|
||||||
|
className={classnames({
|
||||||
|
skeleton: loading,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Create feature toggle
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List>{renderFeatures()}</List>
|
||||||
|
</PageContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FeatureToggleList.propTypes = {
|
||||||
|
features: PropTypes.array.isRequired,
|
||||||
|
featureMetrics: PropTypes.object.isRequired,
|
||||||
|
fetcher: PropTypes.func,
|
||||||
|
revive: PropTypes.func,
|
||||||
|
updateSetting: PropTypes.func.isRequired,
|
||||||
|
toggleFeature: PropTypes.func,
|
||||||
|
settings: PropTypes.object,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureToggleList;
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { MenuItem } from '@material-ui/core';
|
||||||
|
import { MenuItemWithIcon } from '../../../common';
|
||||||
|
import DropdownMenu from '../../../common/dropdown-menu';
|
||||||
|
import ProjectSelect from '../../../common/ProjectSelect';
|
||||||
|
|
||||||
|
const sortingOptions = [
|
||||||
|
{ type: 'name', displayName: 'Name' },
|
||||||
|
{ type: 'type', displayName: 'Type' },
|
||||||
|
{ type: 'enabled', displayName: 'Enabled' },
|
||||||
|
{ type: 'stale', displayName: 'Stale' },
|
||||||
|
{ type: 'created', displayName: 'Created' },
|
||||||
|
{ type: 'Last seen', displayName: 'Last seen' },
|
||||||
|
{ type: 'strategies', displayName: 'Strategies' },
|
||||||
|
{ type: 'metrics', displayName: 'Metrics' },
|
||||||
|
];
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
const FeatureToggleListActions = ({ settings, setSort, toggleMetrics, updateSetting, loading }) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const handleSort = e => {
|
||||||
|
const target = e.target.getAttribute('data-target');
|
||||||
|
setSort(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled = type => settings.sort === type;
|
||||||
|
|
||||||
|
const renderSortingOptions = () =>
|
||||||
|
sortingOptions.map(option => (
|
||||||
|
<MenuItem key={option.type} disabled={isDisabled(option.type)} data-target={option.type}>
|
||||||
|
{option.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
const renderMetricsOptions = () => [
|
||||||
|
<MenuItemWithIcon
|
||||||
|
icon="hourglass_empty"
|
||||||
|
disabled={!settings.showLastHour}
|
||||||
|
data-target="minute"
|
||||||
|
label="Last minute"
|
||||||
|
key={1}
|
||||||
|
/>,
|
||||||
|
<MenuItemWithIcon
|
||||||
|
icon="hourglass_full"
|
||||||
|
disabled={settings.showLastHour}
|
||||||
|
data-target="hour"
|
||||||
|
label="Last hour"
|
||||||
|
key={2}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<DropdownMenu
|
||||||
|
id={'metric'}
|
||||||
|
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
||||||
|
title="Metric interval"
|
||||||
|
callback={toggleMetrics}
|
||||||
|
renderOptions={renderMetricsOptions}
|
||||||
|
className={classnames({ skeleton: loading })}
|
||||||
|
/>
|
||||||
|
<DropdownMenu
|
||||||
|
id={'sorting'}
|
||||||
|
label={`By ${settings.sort}`}
|
||||||
|
callback={handleSort}
|
||||||
|
renderOptions={renderSortingOptions}
|
||||||
|
title="Sort by"
|
||||||
|
className={classnames({ skeleton: loading })}
|
||||||
|
/>
|
||||||
|
<ProjectSelect settings={settings} updateSetting={updateSetting} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FeatureToggleListActions.propTypes = {
|
||||||
|
settings: PropTypes.object,
|
||||||
|
setSort: PropTypes.func,
|
||||||
|
toggleMetrics: PropTypes.func,
|
||||||
|
updateSetting: PropTypes.func,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureToggleListActions;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import FeatureToggleListActions from './FeatureToggleListActions';
|
||||||
|
|
||||||
|
export default FeatureToggleListActions;
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
actions: {
|
||||||
|
'& > *': {
|
||||||
|
margin: '0 0.25rem',
|
||||||
|
},
|
||||||
|
marginRight: '0.25rem',
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Switch, Icon, IconButton, ListItem } from '@material-ui/core';
|
||||||
|
import TimeAgo from 'react-timeago';
|
||||||
|
import Progress from '../../progress-component';
|
||||||
|
import Status from '../../status-component';
|
||||||
|
import FeatureToggleListItemChip from './FeatureToggleListItemChip';
|
||||||
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
import { UPDATE_FEATURE } from '../../../../permissions';
|
||||||
|
import { calc, styles as commonStyles } from '../../../common';
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
const FeatureToggleListItem = ({
|
||||||
|
feature,
|
||||||
|
toggleFeature,
|
||||||
|
settings,
|
||||||
|
metricsLastHour = { yes: 0, no: 0, isFallback: true },
|
||||||
|
metricsLastMinute = { yes: 0, no: 0, isFallback: true },
|
||||||
|
revive,
|
||||||
|
hasPermission,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const { name, description, enabled, type, stale, createdAt } = feature;
|
||||||
|
const { showLastHour = false } = settings;
|
||||||
|
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
||||||
|
const percent =
|
||||||
|
1 *
|
||||||
|
(showLastHour
|
||||||
|
? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0)
|
||||||
|
: calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0));
|
||||||
|
const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem {...rest} className={classnames(styles.listItem, rest.className)}>
|
||||||
|
<span className={styles.listItemMetric}>
|
||||||
|
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
|
||||||
|
</span>
|
||||||
|
<span className={styles.listItemToggle}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(UPDATE_FEATURE)}
|
||||||
|
show={
|
||||||
|
<Switch
|
||||||
|
disabled={toggleFeature === undefined}
|
||||||
|
title={`Toggle ${name}`}
|
||||||
|
key="left-actions"
|
||||||
|
onChange={() => toggleFeature(!enabled, name)}
|
||||||
|
checked={enabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={<Switch disabled title={`Toggle ${name}`} key="left-actions" checked={enabled} />}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className={classnames(styles.listItemLink)}>
|
||||||
|
<Link to={featureUrl} className={classnames(commonStyles.listLink, commonStyles.truncate)}>
|
||||||
|
<span className={commonStyles.toggleName}>{name} </span>
|
||||||
|
<small>
|
||||||
|
<TimeAgo date={createdAt} live={false} />
|
||||||
|
</small>
|
||||||
|
<div>
|
||||||
|
<span className={commonStyles.truncate}>{description}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span className={classnames(styles.listItemStrategies, commonStyles.hideLt920)}>
|
||||||
|
<Status stale={stale} showActive={false} />
|
||||||
|
<FeatureToggleListItemChip type={type} />
|
||||||
|
</span>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={revive && hasPermission(UPDATE_FEATURE)}
|
||||||
|
show={
|
||||||
|
<IconButton onClick={() => revive(feature.name)}>
|
||||||
|
<Icon>undo</Icon>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
elseShow={<span />}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FeatureToggleListItem.propTypes = {
|
||||||
|
feature: PropTypes.object,
|
||||||
|
toggleFeature: PropTypes.func,
|
||||||
|
settings: PropTypes.object,
|
||||||
|
metricsLastHour: PropTypes.object,
|
||||||
|
metricsLastMinute: PropTypes.object,
|
||||||
|
revive: PropTypes.func,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(FeatureToggleListItem);
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Chip } from '@material-ui/core';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
const FeatureToggleListItemChip = ({ type, types, onClick }) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const typeObject = types.find(o => o.id === type) || {
|
||||||
|
id: type,
|
||||||
|
name: type,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip className={styles.typeChip} title={typeObject.description} label={typeObject.name} onClick={onClick} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FeatureToggleListItemChip.propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
types: PropTypes.array,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(FeatureToggleListItemChip);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Component from './feature-type-component';
|
import Component from './FeatureToggleListItemChip';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
types: state.featureTypes.toJS(),
|
types: state.featureTypes.toJS(),
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
typeChip: {
|
||||||
|
margin: '0 8px',
|
||||||
|
boxShadow: theme.boxShadows.chip.main,
|
||||||
|
backgroundColor: theme.palette.chips.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import FeatureToggleListItem from './FeatureToggleListItem';
|
||||||
|
|
||||||
|
export default FeatureToggleListItem;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
listItem: {
|
||||||
|
padding: '0',
|
||||||
|
margin: '0.25rem 0',
|
||||||
|
},
|
||||||
|
listItemMetric: {
|
||||||
|
width: '40px',
|
||||||
|
marginRight: '1rem',
|
||||||
|
flexShrink: '0',
|
||||||
|
},
|
||||||
|
listItemSvg: {
|
||||||
|
fill: theme.palette.icons.lightGrey,
|
||||||
|
},
|
||||||
|
listItemLink: {
|
||||||
|
marginLeft: '10px',
|
||||||
|
minWidth: '0',
|
||||||
|
},
|
||||||
|
listItemStrategies: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -0,0 +1,193 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders correctly with one feature 1`] = `
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="makeStyles-listItemMetric-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z"
|
||||||
|
fill="#E0E0E0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled={false}
|
||||||
|
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-6 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={null}
|
||||||
|
title="Toggle Another"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
className="PrivateSwitchBase-input-9 MuiSwitch-input"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-thumb"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-track"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="makeStyles-listItemLink-4"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="listLink truncate"
|
||||||
|
href="/features/strategies/Another"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="toggleName"
|
||||||
|
>
|
||||||
|
Another
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
<time
|
||||||
|
dateTime="2018-02-04T20:27:52.127Z"
|
||||||
|
title="2018-02-04T20:27:52.127Z"
|
||||||
|
>
|
||||||
|
3 years ago
|
||||||
|
</time>
|
||||||
|
</small>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
another's description
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="makeStyles-listItemStrategies-5 hideLt920"
|
||||||
|
/>
|
||||||
|
<span />
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`renders correctly with one feature without permission 1`] = `
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="makeStyles-listItemMetric-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z"
|
||||||
|
fill="#E0E0E0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled={true}
|
||||||
|
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-6 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-disabled-8 Mui-disabled Mui-disabled Mui-disabled"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={-1}
|
||||||
|
title="Toggle Another"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
className="PrivateSwitchBase-input-9 MuiSwitch-input"
|
||||||
|
disabled={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-thumb"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiSwitch-track"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="makeStyles-listItemLink-4"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="listLink truncate"
|
||||||
|
href="/features/strategies/Another"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="toggleName"
|
||||||
|
>
|
||||||
|
Another
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
<time
|
||||||
|
dateTime="2018-02-04T20:27:52.127Z"
|
||||||
|
title="2018-02-04T20:27:52.127Z"
|
||||||
|
>
|
||||||
|
3 years ago
|
||||||
|
</time>
|
||||||
|
</small>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
another's description
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="makeStyles-listItemStrategies-5 hideLt920"
|
||||||
|
/>
|
||||||
|
<span />
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
@ -0,0 +1,381 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders correctly with one feature 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-searchBarContainer-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-search-4"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden={true}
|
||||||
|
className="MuiSvgIcon-root makeStyles-searchIcon-5"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
className="MuiInputBase-root makeStyles-inputRoot-6"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label="search"
|
||||||
|
className="MuiInputBase-input"
|
||||||
|
onAnimationStart={[Function]}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
placeholder="Search…"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerContainer-7"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerTitleContainer-11"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="MuiTypography-root makeStyles-headerTitle-12 MuiTypography-h2"
|
||||||
|
>
|
||||||
|
Feature toggles
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerActions-13"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-actionsContainer-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-actions-14"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="metric"
|
||||||
|
aria-haspopup="true"
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
disabled={false}
|
||||||
|
id="metric"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
title="Metric interval"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Last minute
|
||||||
|
<span
|
||||||
|
className="MuiButton-endIcon MuiButton-iconSizeMedium"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
arrow_drop_down
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="sorting"
|
||||||
|
aria-haspopup="true"
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
disabled={false}
|
||||||
|
id="sorting"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
title="Sort by"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
By name
|
||||||
|
<span
|
||||||
|
className="MuiButton-endIcon MuiButton-iconSizeMedium"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
arrow_drop_down
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
aria-disabled={false}
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary MuiButton-containedSizeLarge MuiButton-sizeLarge"
|
||||||
|
data-test="add-feature-btn"
|
||||||
|
href="/features/create"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Create feature toggle
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-bodyContainer-8"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
className="MuiList-root MuiList-padding"
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
|
feature={
|
||||||
|
Object {
|
||||||
|
"name": "Another",
|
||||||
|
"reviveName": "Another",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasPermission={[Function]}
|
||||||
|
settings={
|
||||||
|
Object {
|
||||||
|
"sort": "name",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleFeature={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`renders correctly with one feature without permissions 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-searchBarContainer-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-search-4"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden={true}
|
||||||
|
className="MuiSvgIcon-root makeStyles-searchIcon-5"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
className="MuiInputBase-root makeStyles-inputRoot-6"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label="search"
|
||||||
|
className="MuiInputBase-input"
|
||||||
|
onAnimationStart={[Function]}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
placeholder="Search…"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerContainer-7"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerTitleContainer-11"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="MuiTypography-root makeStyles-headerTitle-12 MuiTypography-h2"
|
||||||
|
>
|
||||||
|
Feature toggles
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerActions-13"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-actionsContainer-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-actions-14"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="metric"
|
||||||
|
aria-haspopup="true"
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
disabled={false}
|
||||||
|
id="metric"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
title="Metric interval"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Last minute
|
||||||
|
<span
|
||||||
|
className="MuiButton-endIcon MuiButton-iconSizeMedium"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
arrow_drop_down
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="sorting"
|
||||||
|
aria-haspopup="true"
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
disabled={false}
|
||||||
|
id="sorting"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
title="Sort by"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
By name
|
||||||
|
<span
|
||||||
|
className="MuiButton-endIcon MuiButton-iconSizeMedium"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
arrow_drop_down
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-bodyContainer-8"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
className="MuiList-root MuiList-padding"
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
|
feature={
|
||||||
|
Object {
|
||||||
|
"name": "Another",
|
||||||
|
"reviveName": "Another",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasPermission={[Function]}
|
||||||
|
settings={
|
||||||
|
Object {
|
||||||
|
"sort": "name",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleFeature={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
|
|
||||||
import Feature from '../list-item-component';
|
import FeatureToggleListItem from '../FeatureToggleListItem';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { UPDATE_FEATURE } from '../../../../permissions';
|
import { UPDATE_FEATURE } from '../../../../permissions';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
import theme from '../../../../themes/main-theme';
|
||||||
jest.mock('../feature-type-container');
|
|
||||||
|
jest.mock('../FeatureToggleListItem/FeatureToggleListItemChip');
|
||||||
|
|
||||||
test('renders correctly with one feature', () => {
|
test('renders correctly with one feature', () => {
|
||||||
const feature = {
|
const feature = {
|
||||||
@ -28,7 +30,8 @@ test('renders correctly with one feature', () => {
|
|||||||
const settings = { sort: 'name' };
|
const settings = { sort: 'name' };
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Feature
|
<ThemeProvider theme={theme}>
|
||||||
|
<FeatureToggleListItem
|
||||||
key={0}
|
key={0}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||||
@ -37,6 +40,7 @@ test('renders correctly with one feature', () => {
|
|||||||
toggleFeature={jest.fn()}
|
toggleFeature={jest.fn()}
|
||||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||||
/>
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -63,7 +67,8 @@ test('renders correctly with one feature without permission', () => {
|
|||||||
const settings = { sort: 'name' };
|
const settings = { sort: 'name' };
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Feature
|
<ThemeProvider theme={theme}>
|
||||||
|
<FeatureToggleListItem
|
||||||
key={0}
|
key={0}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||||
@ -72,6 +77,7 @@ test('renders correctly with one feature without permission', () => {
|
|||||||
toggleFeature={jest.fn()}
|
toggleFeature={jest.fn()}
|
||||||
hasPermission={() => false}
|
hasPermission={() => false}
|
||||||
/>
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
|
|
||||||
|
import FeatureToggleList from '../FeatureToggleList';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import { CREATE_FEATURE } from '../../../../permissions';
|
||||||
|
import theme from '../../../../themes/main-theme';
|
||||||
|
|
||||||
|
jest.mock('../FeatureToggleListItem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: 'ListItem',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../common/ProjectSelect');
|
||||||
|
|
||||||
|
test('renders correctly with one feature', () => {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
name: 'Another',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} };
|
||||||
|
const settings = { sort: 'name' };
|
||||||
|
const tree = renderer.create(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<FeatureToggleList
|
||||||
|
updateSetting={jest.fn()}
|
||||||
|
settings={settings}
|
||||||
|
history={{}}
|
||||||
|
featureMetrics={featureMetrics}
|
||||||
|
features={features}
|
||||||
|
toggleFeature={jest.fn()}
|
||||||
|
fetcher={jest.fn()}
|
||||||
|
hasPermission={permission => permission === CREATE_FEATURE}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders correctly with one feature without permissions', () => {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
name: 'Another',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} };
|
||||||
|
const settings = { sort: 'name' };
|
||||||
|
const tree = renderer.create(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<FeatureToggleList
|
||||||
|
updateSetting={jest.fn()}
|
||||||
|
settings={settings}
|
||||||
|
history={{}}
|
||||||
|
featureMetrics={featureMetrics}
|
||||||
|
features={features}
|
||||||
|
toggleFeature={jest.fn()}
|
||||||
|
fetcher={jest.fn()}
|
||||||
|
hasPermission={() => false}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggle/actions';
|
import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggle/actions';
|
||||||
import { updateSettingForGroup } from '../../../store/settings/actions';
|
import { updateSettingForGroup } from '../../../store/settings/actions';
|
||||||
|
import FeatureToggleList from './FeatureToggleList';
|
||||||
|
|
||||||
import FeatureListComponent from './list-component';
|
|
||||||
import { hasPermission } from '../../../permissions';
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
function checkConstraints(strategy, regex) {
|
function checkConstraints(strategy, regex) {
|
||||||
@ -99,15 +99,16 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
|||||||
featureMetrics,
|
featureMetrics,
|
||||||
settings,
|
settings,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
|
loading: state.apiCalls.fetchTogglesState.loading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapStateToProps = mapStateToPropsConfigurable(true);
|
const mapStateToProps = mapStateToPropsConfigurable(true);
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
fetchFeatureToggles,
|
fetcher: () => fetchFeatureToggles(),
|
||||||
updateSetting: updateSettingForGroup('feature'),
|
updateSetting: updateSettingForGroup('feature'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const FeatureListContainer = connect(mapStateToProps, mapDispatchToProps)(FeatureListComponent);
|
const FeatureToggleListContainer = connect(mapStateToProps, mapDispatchToProps)(FeatureToggleList);
|
||||||
|
|
||||||
export default FeatureListContainer;
|
export default FeatureToggleListContainer;
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
const loadingFeatures = [
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'one',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'two',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'three',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'four',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'five',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'six',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'seven',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'eight',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'nine',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-03-19T09:16:21.329Z',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
lastSeenAt: '2021-03-24T10:46:38.036Z',
|
||||||
|
name: 'ten',
|
||||||
|
project: 'default',
|
||||||
|
reviveName: 'cool-thing',
|
||||||
|
stale: true,
|
||||||
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
|
type: 'release',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default loadingFeatures;
|
||||||
14
frontend/src/component/feature/FeatureToggleList/styles.js
Normal file
14
frontend/src/component/feature/FeatureToggleList/styles.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
actionsContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
listParagraph: {
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
searchBarContainer: {
|
||||||
|
marginBottom: '2rem',
|
||||||
|
},
|
||||||
|
});
|
||||||
326
frontend/src/component/feature/FeatureView/FeatureView.jsx
Normal file
326
frontend/src/component/feature/FeatureView/FeatureView.jsx
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Paper, Typography, Button, Switch, LinearProgress } from '@material-ui/core';
|
||||||
|
|
||||||
|
import HistoryComponent from '../../history/history-list-toggle-container';
|
||||||
|
import MetricComponent from '../view/metric-container';
|
||||||
|
import UpdateStrategies from '../view/update-strategies-container';
|
||||||
|
import EditVariants from '../variant/update-variant-container';
|
||||||
|
import FeatureTypeSelect from '../feature-type-select-container';
|
||||||
|
import ProjectSelect from '../project-select-container';
|
||||||
|
import UpdateDescriptionComponent from '../view/update-description-component';
|
||||||
|
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions';
|
||||||
|
import StatusComponent from '../status-component';
|
||||||
|
import FeatureTagComponent from '../feature-tag-component';
|
||||||
|
import StatusUpdateComponent from '../view/status-update-component';
|
||||||
|
import AddTagDialog from '../add-tag-dialog-container';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import TabNav from '../../common/TabNav';
|
||||||
|
|
||||||
|
import { scrollToTop } from '../../common/util';
|
||||||
|
|
||||||
|
import styles from './FeatureView.module.scss';
|
||||||
|
import ConfirmDialogue from '../../common/Dialogue';
|
||||||
|
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
|
||||||
|
const FeatureView = ({
|
||||||
|
activeTab,
|
||||||
|
featureToggleName,
|
||||||
|
features,
|
||||||
|
toggleFeature,
|
||||||
|
setStale,
|
||||||
|
removeFeatureToggle,
|
||||||
|
revive,
|
||||||
|
fetchArchive,
|
||||||
|
fetchFeatureToggles,
|
||||||
|
editFeatureToggle,
|
||||||
|
featureToggle,
|
||||||
|
history,
|
||||||
|
hasPermission,
|
||||||
|
untagFeature,
|
||||||
|
featureTags,
|
||||||
|
fetchTags,
|
||||||
|
tagTypes,
|
||||||
|
}) => {
|
||||||
|
const isFeatureView = !!fetchFeatureToggles;
|
||||||
|
const [delDialog, setDelDialog] = useState(false);
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToTop();
|
||||||
|
fetchTags(featureToggleName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (features.length === 0) {
|
||||||
|
if (isFeatureView) {
|
||||||
|
fetchFeatureToggles();
|
||||||
|
} else {
|
||||||
|
fetchArchive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [features]);
|
||||||
|
|
||||||
|
const getTabComponent = key => {
|
||||||
|
switch (key) {
|
||||||
|
case 'activation':
|
||||||
|
if (isFeatureView && hasPermission(UPDATE_FEATURE)) {
|
||||||
|
return <UpdateStrategies featureToggle={featureToggle} features={features} history={history} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UpdateStrategies
|
||||||
|
featureToggle={featureToggle}
|
||||||
|
features={features}
|
||||||
|
history={history}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'metrics':
|
||||||
|
return <MetricComponent featureToggle={featureToggle} />;
|
||||||
|
case 'variants':
|
||||||
|
return (
|
||||||
|
<EditVariants
|
||||||
|
featureToggle={featureToggle}
|
||||||
|
features={features}
|
||||||
|
history={history}
|
||||||
|
hasPermission={hasPermission}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'log':
|
||||||
|
return <HistoryComponent toggleName={featureToggleName} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getTabData = () => [
|
||||||
|
{
|
||||||
|
label: 'Activation',
|
||||||
|
component: getTabComponent('activation'),
|
||||||
|
name: 'strategies',
|
||||||
|
path: `/features/strategies/${featureToggleName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Metrics',
|
||||||
|
component: getTabComponent('metrics'),
|
||||||
|
name: 'metrics',
|
||||||
|
path: `/features/metrics/${featureToggleName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Variants',
|
||||||
|
component: getTabComponent('variants'),
|
||||||
|
name: 'variants',
|
||||||
|
path: `/features/variants/${featureToggleName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Log',
|
||||||
|
component: getTabComponent('log'),
|
||||||
|
name: 'logs',
|
||||||
|
path: `/features/logs/${featureToggleName}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!featureToggle) {
|
||||||
|
if (features.length === 0) {
|
||||||
|
return <LinearProgress />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Could not find the toggle{' '}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(CREATE_FEATURE)}
|
||||||
|
show={
|
||||||
|
<Link
|
||||||
|
to={{
|
||||||
|
pathname: '/features/create',
|
||||||
|
query: { name: featureToggleName },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{featureToggleName}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
elseShow={<span>featureToggleName</span>}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeToggle = () => {
|
||||||
|
removeFeatureToggle(featureToggle.name);
|
||||||
|
history.push('/features');
|
||||||
|
};
|
||||||
|
const reviveToggle = () => {
|
||||||
|
revive(featureToggle.name);
|
||||||
|
history.push('/features');
|
||||||
|
};
|
||||||
|
const updateDescription = description => {
|
||||||
|
let feature = { ...featureToggle, description };
|
||||||
|
if (Array.isArray(feature.strategies)) {
|
||||||
|
feature.strategies.forEach(s => {
|
||||||
|
delete s.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editFeatureToggle(feature);
|
||||||
|
};
|
||||||
|
const updateType = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const type = evt.target.value;
|
||||||
|
let feature = { ...featureToggle, type };
|
||||||
|
if (Array.isArray(feature.strategies)) {
|
||||||
|
feature.strategies.forEach(s => {
|
||||||
|
delete s.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editFeatureToggle(feature);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProject = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const project = evt.target.value;
|
||||||
|
let feature = { ...featureToggle, project };
|
||||||
|
if (Array.isArray(feature.strategies)) {
|
||||||
|
feature.strategies.forEach(s => {
|
||||||
|
delete s.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editFeatureToggle(feature);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStale = stale => {
|
||||||
|
setStale(stale, featureToggleName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = getTabData();
|
||||||
|
|
||||||
|
const findActiveTab = activeTab => tabs.findIndex(tab => tab.name === activeTab);
|
||||||
|
return (
|
||||||
|
<Paper className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Typography variant="h1" className={styles.heading}>
|
||||||
|
{featureToggle.name}
|
||||||
|
</Typography>
|
||||||
|
<StatusComponent stale={featureToggle.stale} />
|
||||||
|
</div>
|
||||||
|
<div className={classnames(styles.featureInfoContainer, commonStyles.contentSpacingY)}>
|
||||||
|
<UpdateDescriptionComponent
|
||||||
|
isFeatureView={isFeatureView}
|
||||||
|
description={featureToggle.description}
|
||||||
|
update={updateDescription}
|
||||||
|
hasPermission={hasPermission}
|
||||||
|
/>
|
||||||
|
<div className={styles.selectContainer}>
|
||||||
|
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} label="Feature type" />
|
||||||
|
|
||||||
|
<ProjectSelect value={featureToggle.project} onChange={updateProject} label="Project" filled />
|
||||||
|
</div>
|
||||||
|
<FeatureTagComponent
|
||||||
|
featureToggleName={featureToggle.name}
|
||||||
|
tags={featureTags}
|
||||||
|
tagTypes={tagTypes}
|
||||||
|
untagFeature={untagFeature}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<span style={{ paddingRight: '24px' }}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasPermission(UPDATE_FEATURE)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
disabled={!isFeatureView}
|
||||||
|
checked={featureToggle.enabled}
|
||||||
|
onChange={() => toggleFeature(!featureToggle.enabled, featureToggle.name)}
|
||||||
|
/>
|
||||||
|
<span>{featureToggle.enabled ? 'Enabled' : 'Disabled'}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<Switch disabled checked={featureToggle.enabled} />
|
||||||
|
<span>{featureToggle.enabled ? 'Enabled' : 'Disabled'}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isFeatureView}
|
||||||
|
show={
|
||||||
|
<div>
|
||||||
|
<AddTagDialog featureToggleName={featureToggle.name} />
|
||||||
|
<StatusUpdateComponent stale={featureToggle.stale} updateStale={updateStale} />
|
||||||
|
<Button
|
||||||
|
title="Create new feature toggle by cloning configuration"
|
||||||
|
component={Link}
|
||||||
|
to={`/features/copy/${featureToggle.name}`}
|
||||||
|
>
|
||||||
|
Clone
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={!hasPermission(DELETE_FEATURE)}
|
||||||
|
onClick={() => {
|
||||||
|
setDelDialog(true);
|
||||||
|
}}
|
||||||
|
title="Archive feature toggle"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Archive
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Button
|
||||||
|
disabled={!hasPermission(UPDATE_FEATURE)}
|
||||||
|
onClick={reviveToggle}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Revive
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<TabNav tabData={tabs} className={styles.tabContentContainer} startingTab={findActiveTab(activeTab)} />
|
||||||
|
<ConfirmDialogue
|
||||||
|
open={delDialog}
|
||||||
|
title="Are you sure you want to archive this toggle"
|
||||||
|
onClick={() => {
|
||||||
|
setDelDialog(false);
|
||||||
|
removeToggle();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FeatureView.propTypes = {
|
||||||
|
activeTab: PropTypes.string.isRequired,
|
||||||
|
featureToggleName: PropTypes.string.isRequired,
|
||||||
|
features: PropTypes.array.isRequired,
|
||||||
|
toggleFeature: PropTypes.func,
|
||||||
|
setStale: PropTypes.func,
|
||||||
|
removeFeatureToggle: PropTypes.func,
|
||||||
|
revive: PropTypes.func,
|
||||||
|
fetchArchive: PropTypes.func,
|
||||||
|
fetchFeatureToggles: PropTypes.func,
|
||||||
|
fetchFeatureToggle: PropTypes.func,
|
||||||
|
editFeatureToggle: PropTypes.func,
|
||||||
|
featureToggle: PropTypes.object,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
fetchTags: PropTypes.func,
|
||||||
|
untagFeature: PropTypes.func,
|
||||||
|
featureTags: PropTypes.array,
|
||||||
|
tagTypes: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureView;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user