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"
|
||||
},
|
||||
"main": "./index.js",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.17.0",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
@ -52,12 +47,17 @@
|
||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@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",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.3.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"date-fns": "^2.17.0",
|
||||
"debounce": "^1.2.0",
|
||||
"debug": "^4.1.1",
|
||||
"enzyme": "^3.9.0",
|
||||
@ -72,7 +72,8 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"immutable": "^3.8.1",
|
||||
"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",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-sass": "^4.5.3",
|
||||
@ -84,11 +85,8 @@
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-mdl": "^2.1.0",
|
||||
"react-modal": "^3.1.13",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-select": "^3.1.0",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"react-timeago": "^4.4.0",
|
||||
"redux": "^4.0.5",
|
||||
@ -100,6 +98,7 @@
|
||||
"toolbox-loader": "0.0.3",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"whatwg-fetch": "^3.4.1"
|
||||
@ -121,5 +120,7 @@
|
||||
"testPathIgnorePatterns": [
|
||||
"/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; }
|
||||
body { height: 100%; }
|
||||
.skeleton {
|
||||
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 {
|
||||
/* FONT SIZE */
|
||||
--h1-size: 1.25rem;
|
||||
--p-size: 1.1rem;
|
||||
--p-size: 1rem;
|
||||
--caption-size: 0.9rem;
|
||||
|
||||
/* PADDING */
|
||||
--card-padding: 2rem;
|
||||
--card-padding-x: 2rem;
|
||||
--card-padding-y: 2rem;
|
||||
--card-header-padding: 1rem 2rem;
|
||||
--drawer-padding: 1rem 1.5rem;
|
||||
--page-padding: 2rem 0;
|
||||
--list-header-padding: 1rem;
|
||||
|
||||
/* MARGIN */
|
||||
--card-margin-y: 1rem;
|
||||
@ -21,6 +78,25 @@ body { height: 100%; }
|
||||
--success: #3bd86e;
|
||||
--danger: #d95e5e;
|
||||
--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,
|
||||
@ -29,3 +105,22 @@ h2 {
|
||||
margin: 0;
|
||||
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 classnames from 'classnames';
|
||||
import { Card } from 'react-mdl';
|
||||
import { Paper } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
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 getActiveToggles = () => {
|
||||
@ -76,7 +76,7 @@ const ReportCard = ({ features }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Paper className={styles.card}>
|
||||
<div className={styles.reportCardContainer}>
|
||||
<div className={styles.reportCardListContainer}>
|
||||
<h2 className={styles.header}>Toggle report</h2>
|
||||
@ -113,7 +113,7 @@ const ReportCard = ({ features }) => {
|
||||
</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 ReportCard from './report-card';
|
||||
import { filterByProject } from './utils';
|
||||
import ReportCard from './ReportCard';
|
||||
import { filterByProject } from '../utils';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const features = state.features.toJS();
|
||||
@ -1,18 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Card, Menu, MenuItem } from 'react-mdl';
|
||||
import { Paper, MenuItem } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ReportToggleListItem from './report-toggle-list-item';
|
||||
import ReportToggleListHeader from './report-toggle-list-header';
|
||||
import ConditionallyRender from '../common/conditionally-render';
|
||||
import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem';
|
||||
import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader';
|
||||
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 { DropdownButton } from '../common';
|
||||
import styles from './ReportToggleList.module.scss';
|
||||
|
||||
/* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */
|
||||
const BULK_ACTIONS_ON = false;
|
||||
@ -47,26 +46,20 @@ const ReportToggleList = ({ features, selectedProject }) => {
|
||||
));
|
||||
|
||||
const renderBulkActionsMenu = () => (
|
||||
<span>
|
||||
<DropdownButton
|
||||
className={classnames('mdl-button', styles.bulkAction)}
|
||||
id="bulk_actions"
|
||||
label="Bulk actions"
|
||||
/>
|
||||
<Menu
|
||||
target="bulk_actions"
|
||||
/* eslint-disable-next-line */
|
||||
onClick={() => console.log("Hi")}
|
||||
style={{ width: '168px' }}
|
||||
>
|
||||
<MenuItem>Mark toggles as stale</MenuItem>
|
||||
<MenuItem>Delete toggles</MenuItem>
|
||||
</Menu>
|
||||
</span>
|
||||
<DropdownMenu
|
||||
id="bulk-actions"
|
||||
label="Bulk actions"
|
||||
renderOptions={() => (
|
||||
<>
|
||||
<MenuItem>Mark toggles as stale</MenuItem>
|
||||
<MenuItem>Delete toggles</MenuItem>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={styles.reportToggleList}>
|
||||
<Paper className={styles.reportToggleList}>
|
||||
<div className={styles.reportToggleListHeader}>
|
||||
<h3 className={styles.reportToggleListHeading}>Overview</h3>
|
||||
<ConditionallyRender condition={BULK_ACTIONS_ON} show={renderBulkActionsMenu} />
|
||||
@ -83,7 +76,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
|
||||
<tbody>{renderListRows()}</tbody>
|
||||
</table>
|
||||
</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 { filterByProject } from './utils';
|
||||
import { filterByProject } from '../utils';
|
||||
|
||||
import ReportToggleList from './report-toggle-list';
|
||||
import ReportToggleList from './ReportToggleList';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const features = state.features.toJS();
|
||||
@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from 'react-mdl';
|
||||
import { Checkbox } from '@material-ui/core';
|
||||
import UnfoldMoreOutlinedIcon from '@material-ui/icons/UnfoldMoreOutlined';
|
||||
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 handleSort = type => {
|
||||
@ -24,7 +24,12 @@ const ReportToggleListHeader = ({ handleCheckAll, checkAll, setSortData, bulkAct
|
||||
condition={bulkActionsOn}
|
||||
show={
|
||||
<th>
|
||||
<Checkbox onChange={handleCheckAll} value={checkAll} checked={checkAll} />
|
||||
<Checkbox
|
||||
onChange={handleCheckAll}
|
||||
value={checkAll}
|
||||
checked={checkAll}
|
||||
className={styles.checkbox}
|
||||
/>
|
||||
</th>
|
||||
}
|
||||
/>
|
||||
@ -3,15 +3,15 @@ import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
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 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 { KILLSWITCH, PERMISSION } from './constants';
|
||||
import { pluralize, getDates, expired, toggleExpiryByTypeMap, getDiffInDays } from '../../utils';
|
||||
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 nameMatches = feature => feature.name === name;
|
||||
@ -107,7 +107,12 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke
|
||||
condition={bulkActionsOn}
|
||||
show={
|
||||
<td>
|
||||
<Checkbox checked={checked} value={checked} onChange={handleChange} />
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
value={checked}
|
||||
onChange={handleChange}
|
||||
className={styles.checkbox}
|
||||
/>
|
||||
</td>
|
||||
}
|
||||
/>
|
||||
@ -3,14 +3,15 @@ import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Select from '../common/select';
|
||||
import ReportCardContainer from './report-card-container';
|
||||
import ReportToggleListContainer from './report-toggle-list-container';
|
||||
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
||||
import ReportToggleListContainer from './ReportToggleList/ReportToggleListContainer';
|
||||
|
||||
import ConditionallyRender from '../common/conditionally-render';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
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 [projectOptions, setProjectOptions] = useState([{ key: 'default', label: 'Default' }]);
|
||||
@ -40,11 +41,13 @@ const Reporting = ({ fetchFeatureToggles, projects }) => {
|
||||
name="project"
|
||||
className={styles.select}
|
||||
options={projectOptions}
|
||||
value={setSelectedProject.label}
|
||||
value={selectedProject}
|
||||
onChange={onChange}
|
||||
inputProps={{ ['data-test']: REPORTING_SELECT_ID }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const multipleProjects = projects.length > 1;
|
||||
|
||||
return (
|
||||
@ -125,7 +125,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f1f1f1;
|
||||
border-bottom: var(--default-border);
|
||||
padding: 1rem var(--card-padding-x);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchFeatureToggles } from '../../store/feature-toggle/actions';
|
||||
|
||||
import Reporting from './reporting';
|
||||
import Reporting from './Reporting.jsx';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
projects: state.projects.toJS(),
|
||||
@ -5,7 +5,8 @@ import { HashRouter } from 'react-router-dom';
|
||||
import { createStore } from 'redux';
|
||||
import { mount } from 'enzyme/build';
|
||||
|
||||
import Reporting from '../reporting';
|
||||
import Reporting from '../Reporting';
|
||||
import { REPORTING_SELECT_ID } from '../../../testIds';
|
||||
|
||||
import { testProjects, testFeatures } from '../testData';
|
||||
|
||||
@ -15,13 +16,6 @@ const mockStore = {
|
||||
};
|
||||
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', () => {
|
||||
const wrapper = mount(
|
||||
<HashRouter>
|
||||
@ -31,9 +25,7 @@ test('changing projects renders only toggles from that project', () => {
|
||||
</HashRouter>
|
||||
);
|
||||
|
||||
const select = wrapper.find('.mdl-textfield__input').first();
|
||||
expect(select.contains(<option value="default">Default</option>)).toBe(true);
|
||||
expect(select.contains(<option value="myProject">MyProject</option>)).toBe(true);
|
||||
const select = wrapper.find(`input[data-test="${REPORTING_SELECT_ID}"][value="default"]`).first();
|
||||
|
||||
let list = wrapper.find('tr');
|
||||
/* 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 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 { trim } from '../common/util';
|
||||
import AddonParameters from './form-addon-parameters';
|
||||
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 [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 : {};
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
||||
Configure {name}
|
||||
</CardTitle>
|
||||
<CardText>
|
||||
<PageContent headerContent={`Configure ${name} addon`}>
|
||||
<section className={styles.formSection}>
|
||||
{description}
|
||||
<a href={documentationUrl} target="_blank">
|
||||
Read more
|
||||
</a>
|
||||
<p className={commonStyles.error}>{errors.general}</p>
|
||||
</CardText>
|
||||
</section>
|
||||
<form onSubmit={onSubmit}>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<Grid noSpacing>
|
||||
<Cell col={4}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
label="Provider"
|
||||
name="provider"
|
||||
value={config.provider}
|
||||
disabled
|
||||
/>
|
||||
</Cell>
|
||||
<Cell col={4} style={{ paddingTop: '14px' }}>
|
||||
<Switch checked={config.enabled} onChange={onEnabled}>
|
||||
{config.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Switch>
|
||||
</Cell>
|
||||
</Grid>
|
||||
|
||||
<Textfield
|
||||
floatingLabel
|
||||
<section className={styles.formSection}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Provider"
|
||||
name="provider"
|
||||
value={config.provider}
|
||||
disabled
|
||||
variant="outlined"
|
||||
className={styles.nameInput}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={config.enabled} onChange={onEnabled} />}
|
||||
label={config.enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.formSection}>
|
||||
<TextField
|
||||
size="small"
|
||||
style={{ width: '80%' }}
|
||||
rows={1}
|
||||
rows={4}
|
||||
multiline
|
||||
label="Description"
|
||||
name="description"
|
||||
placeholder=""
|
||||
value={config.description}
|
||||
error={errors.description}
|
||||
onChange={setFieldValue('description')}
|
||||
variant="outlined"
|
||||
/>
|
||||
</section>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<section className={styles.formSection}>
|
||||
<AddonEvents
|
||||
provider={provider}
|
||||
checkedEvents={config.events}
|
||||
@ -148,7 +148,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
|
||||
error={errors.events}
|
||||
/>
|
||||
</section>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<section className={styles.formSection}>
|
||||
<AddonParameters
|
||||
provider={provider}
|
||||
config={config}
|
||||
@ -157,11 +157,11 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
|
||||
setParameterValue={setParameterValue}
|
||||
/>
|
||||
</section>
|
||||
<CardActions>
|
||||
<section className={styles.formSection}>
|
||||
<FormButtons submitText={submitText} onCancel={cancel} />
|
||||
</CardActions>
|
||||
</section>
|
||||
</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 FormComponent from './form-addon-component';
|
||||
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
// Required for to fill the initial form.
|
||||
const DEFAULT_DATA = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
@ -11,11 +11,14 @@ const AddonEvents = ({ provider, checkedEvents, setEventValue, error }) => {
|
||||
<React.Fragment>
|
||||
<h4>Events</h4>
|
||||
<span className={commonStyles.error}>{error}</span>
|
||||
<Grid className="demo-grid-ruler">
|
||||
<Grid container spacing={0}>
|
||||
{provider.events.map(e => (
|
||||
<Cell col={4} key={e}>
|
||||
<Checkbox label={e} ripple checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />
|
||||
</Cell>
|
||||
<Grid item xs={4} key={e}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />}
|
||||
label={e}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Textfield } from 'react-mdl';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
const MASKED_VALUE = '*****';
|
||||
|
||||
@ -18,23 +18,27 @@ const AddonParameter = ({ definition, config, errors, setParameterValue }) => {
|
||||
const value = config.parameters[definition.name] || '';
|
||||
const type = resolveType(definition, value);
|
||||
const error = errors.parameters[definition.name];
|
||||
const descStyle = { fontSize: '0.8em', color: 'gray', marginTop: error ? '2px' : '-15px' };
|
||||
|
||||
return (
|
||||
<div style={{ width: '80%', marginTop: '25px' }}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
<TextField
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
rows={definition.type === 'textfield' ? 9 : 0}
|
||||
multiline={definition.type === 'textfield'}
|
||||
type={type}
|
||||
label={definition.displayName}
|
||||
name={definition.name}
|
||||
placeholder={definition.placeholder || ''}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
value={value}
|
||||
error={error}
|
||||
onChange={setParameterValue(definition.name)}
|
||||
variant="outlined"
|
||||
helperText={definition.description}
|
||||
/>
|
||||
<div style={descStyle}>{definition.description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -54,8 +58,8 @@ const AddonParameters = ({ provider, config, errors, setParameterValue, editMode
|
||||
<h4>Parameters</h4>
|
||||
{editMode ? (
|
||||
<p>
|
||||
Sensitive parameters will be masked with value "<i>*****</i>". If you don't change the value they
|
||||
will not be updated when saving.
|
||||
Sensitive parameters will be masked with value "<i>*****</i>
|
||||
". If you don't change the value they will not be updated when saving.
|
||||
</p>
|
||||
) : null}
|
||||
{provider.parameters.map(p => (
|
||||
@ -76,7 +80,7 @@ AddonParameters.propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
setParameterValue: PropTypes.func.isRequired,
|
||||
editMode: PropTypes.bool.optional,
|
||||
editMode: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AddonParameters;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import AddonsListComponent from './list-component.jsx';
|
||||
import AddonsListComponent from './AddonList';
|
||||
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
|
||||
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
|
||||
|
||||
exports[`renders correctly with empty version 1`] = `
|
||||
<react-mdl-FooterSection
|
||||
logo="Unleash "
|
||||
type="bottom"
|
||||
<section
|
||||
title="API details"
|
||||
>
|
||||
<h4>
|
||||
Unleash
|
||||
</h4>
|
||||
<small>
|
||||
(test)
|
||||
\`($
|
||||
test
|
||||
)\`
|
||||
</small>
|
||||
<br />
|
||||
<small>
|
||||
|
||||
</small>
|
||||
<br />
|
||||
<br />
|
||||
<small>
|
||||
We are the best!
|
||||
</small>
|
||||
<br />
|
||||
<small>
|
||||
|
||||
</small>
|
||||
</react-mdl-FooterSection>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`renders correctly with ui-config 1`] = `
|
||||
<react-mdl-FooterSection
|
||||
logo="Unleash 1.1.0"
|
||||
type="bottom"
|
||||
<section
|
||||
title="API details"
|
||||
>
|
||||
<h4>
|
||||
Unleash 1.1.0
|
||||
</h4>
|
||||
<small>
|
||||
(test)
|
||||
\`($
|
||||
test
|
||||
)\`
|
||||
</small>
|
||||
<br />
|
||||
<small>
|
||||
|
||||
</small>
|
||||
<br />
|
||||
<br />
|
||||
<small>
|
||||
We are the best!
|
||||
</small>
|
||||
<br />
|
||||
<small>
|
||||
|
||||
</small>
|
||||
</react-mdl-FooterSection>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`renders correctly without uiConfig 1`] = `
|
||||
<react-mdl-FooterSection
|
||||
logo="Unleash 1.1.0"
|
||||
type="bottom"
|
||||
<section
|
||||
title="API details"
|
||||
>
|
||||
<small>
|
||||
|
||||
</small>
|
||||
<h4>
|
||||
Unleash 1.1.0
|
||||
</h4>
|
||||
<br />
|
||||
<br />
|
||||
<small>
|
||||
|
||||
</small>
|
||||
<br />
|
||||
<small />
|
||||
<br />
|
||||
<small>
|
||||
|
||||
</small>
|
||||
</react-mdl-FooterSection>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@ -3,8 +3,6 @@ import React from 'react';
|
||||
import ShowApiDetailsComponent from '../show-api-details-component';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
jest.mock('react-mdl');
|
||||
|
||||
test('renders correctly with empty version', () => {
|
||||
const uiConfig = {
|
||||
name: 'Unleash',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FooterSection } from 'react-mdl';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
class ShowApiDetailsComponent extends Component {
|
||||
static propTypes = {
|
||||
@ -15,12 +15,12 @@ class ShowApiDetailsComponent extends Component {
|
||||
if (versionInfo) {
|
||||
if (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}`;
|
||||
}
|
||||
} else {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@ -29,15 +29,17 @@ class ShowApiDetailsComponent extends Component {
|
||||
versionStr = `${name} ${version}`;
|
||||
}
|
||||
return (
|
||||
<FooterSection type="bottom" logo={`${versionStr}`}>
|
||||
<small>{environment ? `(${environment})` : ''}</small>
|
||||
<section title="API details">
|
||||
<h4>{`${versionStr}`}</h4>
|
||||
<ConditionallyRender condition={environment} show={<small>`(${environment})`</small>} />
|
||||
<br />
|
||||
<ConditionallyRender condition={updateNotification} show={<small>{updateNotification}`</small>} />
|
||||
<br />
|
||||
<small>{updateNotification ? `${updateNotification}` : ''}</small>
|
||||
<br />
|
||||
<small>{slogan}</small>
|
||||
<br />
|
||||
<small>{instanceId ? `${instanceId}` : ''}</small>
|
||||
</FooterSection>
|
||||
<ConditionallyRender condition={instanceId} show={<small>{`${instanceId}`}</small>} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,12 @@ import PropTypes from 'prop-types';
|
||||
import { Route, Redirect, Switch } from 'react-router-dom';
|
||||
|
||||
import Features from '../page/features';
|
||||
import { routes } from './menu/routes';
|
||||
import styles from './styles.module.scss';
|
||||
import AuthenticationContainer from './user/authentication-container';
|
||||
import MainLayout from './layout/main';
|
||||
|
||||
import { routes } from './menu/routes';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
class App extends PureComponent {
|
||||
static propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
|
||||
@ -5,409 +5,615 @@ exports[`renders correctly if no application 1`] = `
|
||||
<p>
|
||||
Loading...
|
||||
</p>
|
||||
<react-mdl-ProgressBar
|
||||
indeterminate={true}
|
||||
/>
|
||||
<div
|
||||
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>
|
||||
`;
|
||||
|
||||
exports[`renders correctly with permissions 1`] = `
|
||||
<react-mdl-Card
|
||||
className="fullwidth"
|
||||
shadow={0}
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<react-mdl-CardTitle
|
||||
style={
|
||||
Object {
|
||||
"paddingRight": "64px",
|
||||
"paddingTop": "24px",
|
||||
"wordBreak": "break-all",
|
||||
}
|
||||
}
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
>
|
||||
<react-mdl-Icon
|
||||
name="apps"
|
||||
/>
|
||||
|
||||
test-app
|
||||
</react-mdl-CardTitle>
|
||||
<react-mdl-CardText
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": "0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>
|
||||
app description
|
||||
</p>
|
||||
<p>
|
||||
Created:
|
||||
<strong>
|
||||
Invalid Date
|
||||
</strong>
|
||||
</p>
|
||||
</react-mdl-CardText>
|
||||
<react-mdl-CardMenu>
|
||||
<a
|
||||
className="mdl-color-text--grey-600"
|
||||
href="http://example.org"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
<div
|
||||
className="makeStyles-headerTitleContainer-5"
|
||||
>
|
||||
<react-mdl-Icon
|
||||
name="link"
|
||||
/>
|
||||
</a>
|
||||
</react-mdl-CardMenu>
|
||||
<div>
|
||||
<react-mdl-CardActions
|
||||
border={true}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span />
|
||||
<react-mdl-Button
|
||||
accent={true}
|
||||
onClick={[Function]}
|
||||
title="Delete application"
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
Delete
|
||||
</react-mdl-Button>
|
||||
</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>
|
||||
Details
|
||||
</react-mdl-Tab>
|
||||
<react-mdl-Tab>
|
||||
Edit
|
||||
</react-mdl-Tab>
|
||||
</react-mdl-Tabs>
|
||||
<h2
|
||||
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
|
||||
>
|
||||
<span
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
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
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
<react-mdl-Grid
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
<div
|
||||
className="makeStyles-bodyContainer-2"
|
||||
>
|
||||
<react-mdl-Cell
|
||||
col={6}
|
||||
hidePhone={true}
|
||||
phone={12}
|
||||
tablet={4}
|
||||
>
|
||||
<h6>
|
||||
Toggles
|
||||
</h6>
|
||||
<hr />
|
||||
<react-mdl-List>
|
||||
<react-mdl-ListItem
|
||||
twoLine={true}
|
||||
<div>
|
||||
<p
|
||||
className="MuiTypography-root MuiTypography-body1"
|
||||
>
|
||||
app description
|
||||
</p>
|
||||
<p
|
||||
className="MuiTypography-root MuiTypography-body2"
|
||||
>
|
||||
Created:
|
||||
<strong>
|
||||
Invalid Date
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-8 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
>
|
||||
<react-mdl-ListItemContent
|
||||
icon={
|
||||
<span>
|
||||
<react-mdl-Switch
|
||||
checked={true}
|
||||
disabled={true}
|
||||
<div
|
||||
className="MuiTabs-scroller MuiTabs-fixed"
|
||||
onScroll={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": null,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer MuiTabs-centered"
|
||||
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]}
|
||||
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"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper"
|
||||
>
|
||||
Application overview
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-9 PrivateTabIndicator-colorPrimary-10 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</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}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-labelledby="wrapped-tab-0"
|
||||
hidden={false}
|
||||
id="wrapped-tabpanel-0"
|
||||
role="tabpanel"
|
||||
>
|
||||
<react-mdl-ListItemContent
|
||||
icon="report"
|
||||
subtitle="Missing, want to create?"
|
||||
>
|
||||
<a
|
||||
href="/features/create?name=ToggleB"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleB
|
||||
</a>
|
||||
</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, want to create?"
|
||||
>
|
||||
<a
|
||||
href="/strategies/create?name=StrategyB"
|
||||
onClick={[Function]}
|
||||
>
|
||||
StrategyB
|
||||
</a>
|
||||
</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>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-container"
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
instance-1
|
||||
|
||||
(4.0)
|
||||
</react-mdl-ListItemContent>
|
||||
</react-mdl-ListItem>
|
||||
</react-mdl-List>
|
||||
</react-mdl-Cell>
|
||||
</react-mdl-Grid>
|
||||
</react-mdl-Card>
|
||||
<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",
|
||||
}
|
||||
}
|
||||
>
|
||||
Toggles
|
||||
</h6>
|
||||
<hr />
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<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}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
value={true}
|
||||
/>
|
||||
<span
|
||||
className="MuiSwitch-thumb"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiSwitch-track"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="MuiListItemText-root MuiListItemText-multiline"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/features/strategies/ToggleA"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleA
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
this is A toggle
|
||||
</p>
|
||||
</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
|
||||
href="/features/create?name=ToggleB"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleB
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
Missing, want to create?
|
||||
</p>
|
||||
</div>
|
||||
</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",
|
||||
}
|
||||
}
|
||||
>
|
||||
Implemented strategies
|
||||
</h6>
|
||||
<hr />
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
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
|
||||
href="/strategies/view/StrategyA"
|
||||
onClick={[Function]}
|
||||
>
|
||||
StrategyA
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
A description
|
||||
</p>
|
||||
</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
|
||||
href="/strategies/create?name=StrategyB"
|
||||
onClick={[Function]}
|
||||
>
|
||||
StrategyB
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
Missing, want to create?
|
||||
</p>
|
||||
</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",
|
||||
}
|
||||
}
|
||||
>
|
||||
1
|
||||
Instances registered
|
||||
</h6>
|
||||
<hr />
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<span>
|
||||
123.123.123.123
|
||||
last seen at
|
||||
<small>
|
||||
02/23/2017, 03:56:49 PM
|
||||
</small>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="wrapped-tab-1"
|
||||
hidden={true}
|
||||
id="wrapped-tabpanel-1"
|
||||
role="tabpanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders correctly without permission 1`] = `
|
||||
<react-mdl-Card
|
||||
className="fullwidth"
|
||||
shadow={0}
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<react-mdl-CardTitle
|
||||
style={
|
||||
Object {
|
||||
"paddingRight": "64px",
|
||||
"paddingTop": "24px",
|
||||
"wordBreak": "break-all",
|
||||
}
|
||||
}
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
>
|
||||
<react-mdl-Icon
|
||||
name="apps"
|
||||
/>
|
||||
|
||||
test-app
|
||||
</react-mdl-CardTitle>
|
||||
<react-mdl-CardText
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": "0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>
|
||||
app description
|
||||
</p>
|
||||
<p>
|
||||
Created:
|
||||
<strong>
|
||||
Invalid Date
|
||||
</strong>
|
||||
</p>
|
||||
</react-mdl-CardText>
|
||||
<react-mdl-CardMenu>
|
||||
<a
|
||||
className="mdl-color-text--grey-600"
|
||||
href="http://example.org"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
<div
|
||||
className="makeStyles-headerTitleContainer-5"
|
||||
>
|
||||
<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}
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h2
|
||||
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
}
|
||||
}
|
||||
>
|
||||
instance-1
|
||||
|
||||
(4.0)
|
||||
</react-mdl-ListItemContent>
|
||||
</react-mdl-ListItem>
|
||||
</react-mdl-List>
|
||||
</react-mdl-Cell>
|
||||
</react-mdl-Grid>
|
||||
</react-mdl-Card>
|
||||
<div
|
||||
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
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
app description
|
||||
</p>
|
||||
<p
|
||||
className="MuiTypography-root MuiTypography-body2"
|
||||
>
|
||||
Created:
|
||||
<strong>
|
||||
Invalid Date
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import ClientApplications from '../application-edit-component';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions';
|
||||
|
||||
jest.mock('react-mdl');
|
||||
import theme from '../../../themes/main-theme';
|
||||
|
||||
test('renders correctly if no application', () => {
|
||||
const tree = renderer
|
||||
@ -27,51 +27,53 @@ test('renders correctly without permission', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<MemoryRouter>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
)
|
||||
.toJSON();
|
||||
@ -83,53 +85,55 @@ test('renders correctly with permissions', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<MemoryRouter>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
history={{}}
|
||||
deleteApplication={jest.fn()}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
hasPermission={permission =>
|
||||
[CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1
|
||||
}
|
||||
/>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
history={{}}
|
||||
deleteApplication={jest.fn()}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
hasPermission={permission =>
|
||||
[CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
)
|
||||
.toJSON();
|
||||
|
||||
@ -2,12 +2,16 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Card, CardActions, CardTitle, CardText, CardMenu, Icon, ProgressBar, Tabs, Tab } from 'react-mdl';
|
||||
import { IconLink, styles as commonStyles } from '../common';
|
||||
import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
|
||||
import { UPDATE_APPLICATION } from '../../permissions';
|
||||
import ApplicationView from './application-view';
|
||||
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 {
|
||||
static propTypes = {
|
||||
@ -23,7 +27,11 @@ class ClientApplications extends PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.state = { activeTab: 0, loading: !props.application };
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
loading: !props.application,
|
||||
prompt: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -34,9 +42,11 @@ class ClientApplications extends PureComponent {
|
||||
|
||||
deleteApplication = async evt => {
|
||||
evt.preventDefault();
|
||||
// if (window.confirm('Are you sure you want to remove this application?')) {
|
||||
const { deleteApplication, appName } = this.props;
|
||||
await deleteApplication(appName);
|
||||
this.props.history.push('/applications');
|
||||
// }
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -44,7 +54,7 @@ class ClientApplications extends PureComponent {
|
||||
return (
|
||||
<div>
|
||||
<p>Loading...</p>
|
||||
<ProgressBar indeterminate />
|
||||
<LinearProgress />
|
||||
</div>
|
||||
);
|
||||
} else if (!this.props.application) {
|
||||
@ -53,69 +63,98 @@ class ClientApplications extends PureComponent {
|
||||
const { application, storeApplicationMetaData, hasPermission } = this.props;
|
||||
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
|
||||
|
||||
const content =
|
||||
this.state.activeTab === 0 ? (
|
||||
<ApplicationView
|
||||
strategies={strategies}
|
||||
instances={instances}
|
||||
seenToggles={seenToggles}
|
||||
hasPermission={hasPermission}
|
||||
formatFullDateTime={this.formatFullDateTime}
|
||||
/>
|
||||
) : (
|
||||
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
|
||||
);
|
||||
const toggleModal = () => {
|
||||
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
|
||||
strategies={strategies}
|
||||
instances={instances}
|
||||
seenToggles={seenToggles}
|
||||
hasPermission={hasPermission}
|
||||
formatFullDateTime={this.formatFullDateTime}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Edit application',
|
||||
component: (
|
||||
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
||||
<CardTitle style={{ paddingTop: '24px', paddingRight: '64px', wordBreak: 'break-all' }}>
|
||||
<Icon name={icon || 'apps'} />
|
||||
{appName}
|
||||
</CardTitle>
|
||||
<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={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span />
|
||||
<Button accent title="Delete application" onClick={this.deleteApplication}>
|
||||
Delete
|
||||
</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>
|
||||
<Tab>Edit</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar style={{ marginRight: '8px' }}>
|
||||
<Icon>{icon || 'apps'}</Icon>
|
||||
</Avatar>
|
||||
{appName}
|
||||
</span>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={url}
|
||||
show={
|
||||
<IconButton component={Link} href={url}>
|
||||
<Icon>link</Icon>
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
|
||||
{content}
|
||||
</Card>
|
||||
<ConditionallyRender
|
||||
condition={hasPermission(UPDATE_APPLICATION)}
|
||||
show={
|
||||
<Button color="secondary" title="Delete application" onClick={toggleModal}>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="body1">{description || ''}</Typography>
|
||||
<Typography variant="body2">
|
||||
Created: <strong>{this.formatDate(createdAt)}</strong>
|
||||
</Typography>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={hasPermission(UPDATE_APPLICATION)}
|
||||
show={
|
||||
<div>
|
||||
{renderModal()}
|
||||
|
||||
<TabNav tabData={tabData} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,10 +16,10 @@ const mapStateToProps = (state, props) => {
|
||||
};
|
||||
};
|
||||
|
||||
const Constainer = connect(mapStateToProps, {
|
||||
const Container = connect(mapStateToProps, {
|
||||
fetchApplication,
|
||||
storeApplicationMetaData,
|
||||
deleteApplication,
|
||||
})(ApplicationEdit);
|
||||
|
||||
export default Constainer;
|
||||
export default Container;
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import React, { Component } from 'react';
|
||||
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 SearchField from '../common/search-field';
|
||||
import SearchField from '../common/SearchField/SearchField';
|
||||
import PageContent from '../common/PageContent/PageContent';
|
||||
import HeaderTitle from '../common/HeaderTitle';
|
||||
|
||||
const Empty = () => (
|
||||
<React.Fragment>
|
||||
<CardText style={{ textAlign: 'center' }}>
|
||||
<Icon name="warning" style={{ fontSize: '5em' }} /> <br />
|
||||
<section style={{ textAlign: 'center' }}>
|
||||
<Icon>warning</Icon> <br />
|
||||
<br />
|
||||
Oh snap, it does not seem like you have connected any applications. To connect your application to Unleash
|
||||
you will require a Client SDK.
|
||||
@ -15,7 +17,7 @@ const Empty = () => (
|
||||
<br />
|
||||
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>
|
||||
</CardText>
|
||||
</section>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@ -35,20 +37,22 @@ class ClientStrategies extends Component {
|
||||
const { applications } = this.props;
|
||||
|
||||
if (!applications) {
|
||||
return <ProgressBar indeterminate />;
|
||||
return <CircularProgress variant="indeterminate" />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={commonStyles.toolbar}>
|
||||
<>
|
||||
<div className={commonStyles.searchField}>
|
||||
<SearchField
|
||||
value={this.props.settings.filter}
|
||||
updateValue={this.props.updateSetting.bind(this, 'filter')}
|
||||
/>
|
||||
</div>
|
||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
||||
{applications.length > 0 ? <AppsLinkList apps={applications} /> : <Empty />}
|
||||
</Card>
|
||||
</div>
|
||||
<PageContent headerContent={<HeaderTitle title="Applications" />}>
|
||||
<div className={commonStyles.fullwidth}>
|
||||
{applications.length > 0 ? <AppsLinkList apps={applications} /> : <Empty />}
|
||||
</div>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,51 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Grid, Cell } from 'react-mdl';
|
||||
import StatefulTextfield from './stateful-textfield';
|
||||
import { TextField, Grid } from '@material-ui/core';
|
||||
import { useCommonStyles } from '../../common.styles';
|
||||
import icons from './icon-names';
|
||||
import MySelect from '../common/select';
|
||||
|
||||
function ApplicationUpdate({ application, storeApplicationMetaData }) {
|
||||
const { appName, icon, url, description } = application;
|
||||
const [localUrl, setLocalUrl] = useState(url);
|
||||
const [localDescription, setLocalDescription] = useState(description);
|
||||
const commonStyles = useCommonStyles();
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Cell col={12}>
|
||||
<MySelect
|
||||
label="Icon"
|
||||
options={icons.map(v => ({ key: v, label: v }))}
|
||||
value={icon || 'apps'}
|
||||
onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)}
|
||||
filled
|
||||
/>
|
||||
<StatefulTextfield
|
||||
value={url}
|
||||
label="Application URL"
|
||||
placeholder="https://example.com"
|
||||
type="url"
|
||||
onBlur={e => storeApplicationMetaData(appName, 'url', e.target.value)}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<StatefulTextfield
|
||||
value={description}
|
||||
label="Description"
|
||||
rows={2}
|
||||
onBlur={e => storeApplicationMetaData(appName, 'description', e.target.value)}
|
||||
/>
|
||||
</Cell>
|
||||
<Grid container style={{ marginTop: '1rem' }}>
|
||||
<Grid item sm={12} xs={12} className={commonStyles.contentSpacingY}>
|
||||
<Grid item>
|
||||
<MySelect
|
||||
label="Icon"
|
||||
options={icons.map(v => ({ key: v, label: v }))}
|
||||
value={icon || 'apps'}
|
||||
onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
value={localUrl}
|
||||
onChange={e => setLocalUrl(e.target.value)}
|
||||
label="Application URL"
|
||||
placeholder="https://example.com"
|
||||
type="url"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onBlur={() => storeApplicationMetaData(appName, 'url', localUrl)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
value={localDescription}
|
||||
label="Description"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
rows={2}
|
||||
onChange={e => setLocalDescription(e.target.value)}
|
||||
onBlur={() => storeApplicationMetaData(appName, 'description', localDescription)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,99 +1,151 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
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 { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) {
|
||||
const notFoundListItem = ({ createUrl, name, permission }) => (
|
||||
<ConditionallyRender
|
||||
key={`not_found_conditional_${name}`}
|
||||
condition={hasPermission(permission)}
|
||||
show={
|
||||
<ListItem key={`not_found_${name}`}>
|
||||
<ListItemAvatar>
|
||||
<Icon>report</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<Link to={`${createUrl}?name=${name}`}>{name}</Link>}
|
||||
secondary={'Missing, want to create?'}
|
||||
/>
|
||||
</ListItem>
|
||||
}
|
||||
elseShow={
|
||||
<ListItem key={`not_found_${name}`}>
|
||||
<ListItemAvatar>
|
||||
<Icon>report</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={name} secondary={`Could not find feature toggle with name ${name}`} />
|
||||
</ListItem>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
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 style={{ margin: 0 }}>
|
||||
<Cell col={6} tablet={4} phone={12} hidePhone>
|
||||
<h6> Toggles</h6>
|
||||
<Grid container style={{ margin: 0 }}>
|
||||
<Grid item xl={6} md={6} xs={12}>
|
||||
<Typography variant="subtitle1" style={{ padding: '1rem 0' }}>
|
||||
Toggles
|
||||
</Typography>
|
||||
<hr />
|
||||
<List>
|
||||
{seenToggles.map(({ name, description, enabled, notFound }, i) =>
|
||||
notFound ? (
|
||||
<ListItem twoLine key={i}>
|
||||
{hasPermission(CREATE_FEATURE) ? (
|
||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
||||
<Link to={`/features/create?name=${name}`}>{name}</Link>
|
||||
</ListItemContent>
|
||||
) : (
|
||||
<ListItemContent icon={'report'} subtitle={'Missing'}>
|
||||
{name}
|
||||
</ListItemContent>
|
||||
)}
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem twoLine key={i}>
|
||||
<ListItemContent
|
||||
icon={
|
||||
<span>
|
||||
<Switch disabled checked={!!enabled} />
|
||||
</span>
|
||||
}
|
||||
subtitle={shorten(description, 60)}
|
||||
>
|
||||
<Link to={`/features/view/${name}`}>{shorten(name, 50)}</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
{seenToggles.map(({ name, description, enabled, notFound }, i) => (
|
||||
<ConditionallyRender
|
||||
key={`toggle_conditional_${name}`}
|
||||
condition={notFound}
|
||||
show={notFoundListItem({
|
||||
createUrl: '/features/create',
|
||||
name,
|
||||
permission: CREATE_FEATURE,
|
||||
i,
|
||||
})}
|
||||
elseShow={foundListItem({
|
||||
viewUrl: '/features/strategies',
|
||||
name,
|
||||
showSwitch: true,
|
||||
enabled,
|
||||
description,
|
||||
i,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={6} tablet={4} phone={12}>
|
||||
<h6>Implemented strategies</h6>
|
||||
</Grid>
|
||||
<Grid item xl={6} md={6} xs={12}>
|
||||
<Typography variant="subtitle1" style={{ padding: '1rem 0' }}>
|
||||
Implemented strategies
|
||||
</Typography>
|
||||
<hr />
|
||||
<List>
|
||||
{strategies.map(({ name, description, notFound }, i) =>
|
||||
notFound ? (
|
||||
<ListItem twoLine key={`${name}-${i}`}>
|
||||
{hasPermission(CREATE_STRATEGY) ? (
|
||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
||||
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
|
||||
</ListItemContent>
|
||||
) : (
|
||||
<ListItemContent icon={'report'} subtitle={'Missing'}>
|
||||
{name}
|
||||
</ListItemContent>
|
||||
)}
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem twoLine key={`${name}-${i}`}>
|
||||
<ListItemContent icon={'extension'} subtitle={shorten(description, 60)}>
|
||||
<Link to={`/strategies/view/${name}`}>{shorten(name, 50)}</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
{strategies.map(({ name, description, notFound }, i) => (
|
||||
<ConditionallyRender
|
||||
key={`strategies_conditional_${name}`}
|
||||
condition={notFound}
|
||||
show={notFoundListItem({
|
||||
createUrl: '/strategies/create',
|
||||
name,
|
||||
permission: CREATE_STRATEGY,
|
||||
i,
|
||||
})}
|
||||
elseShow={foundListItem({
|
||||
viewUrl: '/strategies/view',
|
||||
name,
|
||||
showSwitch: false,
|
||||
enabled: undefined,
|
||||
description,
|
||||
i,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={12} tablet={12}>
|
||||
<h6>{instances.length} Instances registered</h6>
|
||||
</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 }, i) => (
|
||||
<ListItem key={i} twoLine>
|
||||
<ListItemContent
|
||||
icon="timeline"
|
||||
subtitle={
|
||||
{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>
|
||||
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{instanceId} {sdkVersion ? `(${sdkVersion})` : ''}
|
||||
</ListItemContent>
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Cell>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ApplicationView.propTypes = {
|
||||
createUrl: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
permission: PropTypes.string,
|
||||
instances: PropTypes.array.isRequired,
|
||||
seenToggles: 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 FeatureListComponent from './../feature/list/list-component';
|
||||
import FeatureListComponent from '../feature/FeatureToggleList/FeatureToggleList';
|
||||
import { fetchArchive, revive } from './../../store/archive/actions';
|
||||
import { updateSettingForGroup } from './../../store/settings/actions';
|
||||
import { mapStateToPropsConfigurable } from '../feature/list/list-container';
|
||||
import { mapStateToPropsConfigurable } from '../feature/FeatureToggleList';
|
||||
|
||||
const mapStateToProps = mapStateToPropsConfigurable(false);
|
||||
const mapDispatchToProps = {
|
||||
fetchArchive,
|
||||
fetcher: () => fetchArchive(),
|
||||
revive,
|
||||
updateSetting: updateSettingForGroup('feature'),
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
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 { 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 Component from './project-component';
|
||||
import { fetchProjects } from './../../../store/project/actions';
|
||||
import { P } from '../../common/flags';
|
||||
import ProjectSelect from './ProjectSelect';
|
||||
import { fetchProjects } from '../../../store/project/actions';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
enabled: !!state.uiConfig.toJS().flags[P],
|
||||
projects: state.projects.toJS(),
|
||||
currentProjectId: ownProps.settings.currentProjectId || '*',
|
||||
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 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -42,27 +44,21 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
|
||||
.title {
|
||||
padding: 20px 16px 20px 24px;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 24px
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 14px 20px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.switchWithLabel {
|
||||
display: flex;
|
||||
|
||||
|
||||
.label {
|
||||
padding-right: 16px;
|
||||
line-height: 24px;
|
||||
@ -106,4 +102,30 @@
|
||||
|
||||
.error {
|
||||
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 PropTypes from 'prop-types';
|
||||
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 ConditionallyRender from './ConditionallyRender/ConditionallyRender';
|
||||
|
||||
export { styles };
|
||||
|
||||
export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str);
|
||||
|
||||
export const AppsLinkList = ({ apps }) => (
|
||||
<List>
|
||||
{apps.length > 0 &&
|
||||
apps.map(({ appName, description, icon }) => (
|
||||
<ListItem twoLine key={appName}>
|
||||
<span className="mdl-list__item-primary-content" style={{ minWidth: 0 }}>
|
||||
<Icon name={icon || 'apps'} className="mdl-list__item-avatar" />
|
||||
<Link to={`/applications/${appName}`} className={[styles.listLink, styles.truncate].join(' ')}>
|
||||
{appName}
|
||||
<span className={['mdl-list__item-sub-title', styles.truncate].join(' ')}>
|
||||
{description || 'No description'}
|
||||
</span>
|
||||
</Link>
|
||||
</span>
|
||||
<ConditionallyRender
|
||||
condition={apps.length > 0}
|
||||
show={apps.map(({ appName, description, icon }) => (
|
||||
<ListItem key={appName} className={styles.listItem}>
|
||||
<ListItemAvatar>
|
||||
<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}
|
||||
</Link>
|
||||
}
|
||||
secondary={description || 'No description'}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
/>
|
||||
</List>
|
||||
);
|
||||
AppsLinkList.propTypes = {
|
||||
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 }) => (
|
||||
<div className={styles.dataTableHeader}>
|
||||
<div className={styles.title}>
|
||||
<h2 className={styles.titleText}>{title}</h2>
|
||||
<Typography variant="h2" className={styles.titleText}>
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
{actions && <div className={styles.actions}>{actions}</div>}
|
||||
</div>
|
||||
@ -66,11 +69,15 @@ DataTableHeader.propTypes = {
|
||||
actions: PropTypes.any,
|
||||
};
|
||||
|
||||
export const FormButtons = ({ submitText = 'Create', onCancel }) => (
|
||||
export const FormButtons = ({ submitText = 'Create', onCancel, primaryButtonTestId }) => (
|
||||
<div>
|
||||
<Button type="submit" ripple raised primary icon="add">
|
||||
<Icon name="add" />
|
||||
|
||||
<Button
|
||||
data-test={primaryButtonTestId}
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
startIcon={<Icon>add</Icon>}
|
||||
>
|
||||
{submitText}
|
||||
</Button>
|
||||
|
||||
@ -82,37 +89,7 @@ export const FormButtons = ({ submitText = 'Create', onCancel }) => (
|
||||
FormButtons.propTypes = {
|
||||
submitText: PropTypes.string,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
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,
|
||||
primaryButtonTestId: PropTypes.string,
|
||||
};
|
||||
|
||||
export function getIcon(type) {
|
||||
@ -132,7 +109,7 @@ export function getIcon(type) {
|
||||
|
||||
export const IconLink = ({ url, icon }) => (
|
||||
<a href={url} target="_blank" rel="noopener" className="mdl-color-text--grey-600">
|
||||
<Icon name={icon} />
|
||||
<Icon>{icon}</Icon>
|
||||
</a>
|
||||
);
|
||||
IconLink.propTypes = {
|
||||
@ -140,22 +117,41 @@ IconLink.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
export const DropdownButton = ({ label, id, className = styles.dropdownButton, title, style }) => (
|
||||
<Button id={id} className={className} title={title} style={style}>
|
||||
export const DropdownButton = ({
|
||||
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}
|
||||
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
DropdownButton.propTypes = {
|
||||
label: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
startIcon: PropTypes.string,
|
||||
};
|
||||
|
||||
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
||||
<MenuItem disabled={disabled} style={{ display: 'flex', alignItems: 'center' }} {...menuItemProps}>
|
||||
<Icon name={icon} style={{ paddingRight: '16px' }} />
|
||||
<Icon style={{ paddingRight: '16px' }}>{icon}</Icon>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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 = () => {} }) {
|
||||
const handleChange = evt => {
|
||||
@ -21,10 +21,10 @@ function InputListField({ label, values = [], error, name, updateValues, placeho
|
||||
};
|
||||
|
||||
return (
|
||||
<Textfield
|
||||
<TextField
|
||||
name={name}
|
||||
floatingLabel
|
||||
error={error}
|
||||
error={error !== undefined}
|
||||
helperText={error}
|
||||
placeholder={placeholder}
|
||||
value={values ? values.join(', ') : ''}
|
||||
onKeyDown={handleKeyDown}
|
||||
@ -32,6 +32,8 @@ function InputListField({ label, values = [], error, name, updateValues, placeho
|
||||
onBlur={onBlur}
|
||||
label={label}
|
||||
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 classnames from 'classnames';
|
||||
import { Select, FormControl, MenuItem, InputLabel } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Select = ({ name, value, label, options, style, onChange, disabled = false, filled, className }) => {
|
||||
const wrapper = Object.assign({ width: 'auto' }, style);
|
||||
const SelectMenu = ({ name, value, label, options, onChange, id, disabled = false, className, ...rest }) => {
|
||||
const renderSelectItems = () =>
|
||||
options.map(option => (
|
||||
<MenuItem key={option.key} value={option.key} title={option.title}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded',
|
||||
className
|
||||
)}
|
||||
style={wrapper}
|
||||
>
|
||||
<select
|
||||
className="mdl-textfield__input"
|
||||
<FormControl variant="outlined" size="small">
|
||||
<InputLabel htmlFor={id} id={id}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
label={label}
|
||||
id={id}
|
||||
size="small"
|
||||
value={value}
|
||||
style={{
|
||||
width: 'auto',
|
||||
background: filled ? '#f5f5f5' : 'none',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{options.map(o => (
|
||||
<option key={o.key} value={o.key} title={o.title}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="mdl-textfield__label" htmlFor="textfield-contextName">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
{renderSelectItems()}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
Select.propTypes = {
|
||||
SelectMenu.propTypes = {
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
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 ContextFieldListComponent from './list-component.jsx';
|
||||
import { fetchContext, removeContextField } from './../../store/context/actions';
|
||||
import { hasPermission } from '../../permissions';
|
||||
import ContextList from './ContextList';
|
||||
import { fetchContext, removeContextField } from '../../../store/context/actions';
|
||||
import { hasPermission } from '../../../permissions';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const list = state.context.toJS();
|
||||
@ -14,14 +14,11 @@ const mapStateToProps = state => {
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
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),
|
||||
});
|
||||
|
||||
const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextFieldListComponent);
|
||||
const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextList);
|
||||
|
||||
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 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 { trim } from '../common/util';
|
||||
import PageContent from '../common/PageContent/PageContent';
|
||||
|
||||
const sortIgnoreCase = (a, b) => {
|
||||
a = a.toLowerCase();
|
||||
@ -99,12 +101,10 @@ class AddContextComponent extends Component {
|
||||
renderLegalValue = (value, index) => (
|
||||
<Chip
|
||||
key={`${value}:${index}`}
|
||||
className="mdl-color--blue-grey-100"
|
||||
style={{ marginRight: '4px' }}
|
||||
onClose={() => this.removeLegalValue(index)}
|
||||
>
|
||||
{value}
|
||||
</Chip>
|
||||
className={styles.chip}
|
||||
onDelete={() => this.removeLegalValue(index)}
|
||||
label={value}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
@ -113,85 +113,96 @@ class AddContextComponent extends Component {
|
||||
const submitText = editMode ? 'Update' : 'Create';
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
||||
Create context field
|
||||
</CardTitle>
|
||||
<CardText>
|
||||
<PageContent headerContent="Create context field">
|
||||
<div className={styles.supporting}>
|
||||
Context fields are a basic building block used in Unleash to control roll-out. They can be used
|
||||
together with strategy constraints as part of the activation strategy evaluation.
|
||||
</CardText>
|
||||
</div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
<section className={styles.formContainer}>
|
||||
<TextField
|
||||
className={commonStyles.fullwidth}
|
||||
label="Name"
|
||||
name="name"
|
||||
value={contextField.name}
|
||||
defaultValue={contextField.name}
|
||||
error={errors.name}
|
||||
helperText={errors.name}
|
||||
disabled={editMode}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onBlur={v => this.validateContextName(v.target.value)}
|
||||
onChange={v => this.setValue('name', trim(v.target.value))}
|
||||
/>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
style={{ width: '100%' }}
|
||||
rows={1}
|
||||
<TextField
|
||||
className={commonStyles.fullwidth}
|
||||
rowsMax={1}
|
||||
label="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)}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<section style={{ padding: '16px', background: '#fafafa' }}>
|
||||
<h6 style={{ marginTop: '0' }}>Legal values</h6>
|
||||
<p style={{ color: 'rgba(0,0,0,.54)' }}>
|
||||
By defining the legal values the Unleash Admin UI will validate the user input. A
|
||||
concrete example would be that we know all values for our “environment” (local,
|
||||
development, stage, production).
|
||||
</p>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
</section>
|
||||
<section className={styles.inset}>
|
||||
<h6 className={styles.h6}>Legal values</h6>
|
||||
<p className={styles.alpha}>
|
||||
By defining the legal values the Unleash Admin UI will validate the user input. A concrete
|
||||
example would be that we know all values for our “environment” (local, development, stage,
|
||||
production).
|
||||
</p>
|
||||
<div>
|
||||
<TextField
|
||||
label="Value"
|
||||
name="value"
|
||||
style={{ width: '130px' }}
|
||||
className={styles.valueField}
|
||||
value={this.state.currentLegalValue}
|
||||
error={errors.currentLegalValue}
|
||||
error={!!errors.currentLegalValue}
|
||||
helperText={errors.currentLegalValue}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
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
|
||||
</Button>
|
||||
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
||||
</section>
|
||||
<br />
|
||||
<section style={{ padding: '16px' }}>
|
||||
<h6 style={{ marginTop: '0' }}>Custom stickiness (beta)</h6>
|
||||
<p style={{ color: 'rgba(0,0,0,.54)' }}>
|
||||
By enabling stickiness on this context field you can use it together with the
|
||||
flexible-rollout strategy. This will guarantee a consistent behavior for specific values
|
||||
of this context field. PS! Not all client SDK's support this feature yet!{' '}
|
||||
<a
|
||||
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
|
||||
target="_blank"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</p>
|
||||
<Checkbox
|
||||
label="Allow stickiness"
|
||||
ripple
|
||||
checked={contextField.stickiness}
|
||||
onChange={() => this.setValue('stickiness', !contextField.stickiness)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
||||
</section>
|
||||
<CardActions>
|
||||
<br />
|
||||
<section>
|
||||
<Typography variant="subtitle1">Custom stickiness (beta)</Typography>
|
||||
<p className={classnames(styles.alpha, styles.formContainer)}>
|
||||
By enabling stickiness on this context field you can use it together with the
|
||||
flexible-rollout strategy. This will guarantee a consistent behavior for specific values of
|
||||
this context field. PS! Not all client SDK's support this feature yet!{' '}
|
||||
<a
|
||||
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
|
||||
target="_blank"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</p>
|
||||
<Switch
|
||||
label="Allow stickiness"
|
||||
value={contextField.stickiness}
|
||||
onChange={() => this.setValue('stickiness', !contextField.stickiness)}
|
||||
/>
|
||||
</section>
|
||||
<div className={styles.formButtons}>
|
||||
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||
</CardActions>
|
||||
</div>
|
||||
</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 PropTypes from 'prop-types';
|
||||
|
||||
import { Snackbar, Icon } from 'react-mdl';
|
||||
import { Snackbar, Icon, IconButton } from '@material-ui/core';
|
||||
|
||||
const ErrorComponent = ({ errors, ...props }) => {
|
||||
const showError = errors.length > 0;
|
||||
const error = showError ? errors[0] : undefined;
|
||||
const muteError = () => props.muteError(error);
|
||||
return (
|
||||
<Snackbar action="Dismiss" active={showError} onActionClick={muteError} onTimeout={muteError} timeout={10000}>
|
||||
<Icon name="question_answer" /> {error}
|
||||
</Snackbar>
|
||||
<Snackbar
|
||||
action={
|
||||
<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 Component from './feature-type-component';
|
||||
import Component from './FeatureToggleListItemChip';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
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 { 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 { UPDATE_FEATURE } from '../../../../permissions';
|
||||
|
||||
jest.mock('react-mdl');
|
||||
jest.mock('../feature-type-container');
|
||||
import theme from '../../../../themes/main-theme';
|
||||
|
||||
jest.mock('../FeatureToggleListItem/FeatureToggleListItemChip');
|
||||
|
||||
test('renders correctly with one feature', () => {
|
||||
const feature = {
|
||||
@ -28,15 +30,17 @@ test('renders correctly with one feature', () => {
|
||||
const settings = { sort: 'name' };
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<Feature
|
||||
key={0}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={jest.fn()}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
<ThemeProvider theme={theme}>
|
||||
<FeatureToggleListItem
|
||||
key={0}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={jest.fn()}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@ -63,15 +67,17 @@ test('renders correctly with one feature without permission', () => {
|
||||
const settings = { sort: 'name' };
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<Feature
|
||||
key={0}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={jest.fn()}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
<ThemeProvider theme={theme}>
|
||||
<FeatureToggleListItem
|
||||
key={0}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={jest.fn()}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</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 { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggle/actions';
|
||||
import { updateSettingForGroup } from '../../../store/settings/actions';
|
||||
import FeatureToggleList from './FeatureToggleList';
|
||||
|
||||
import FeatureListComponent from './list-component';
|
||||
import { hasPermission } from '../../../permissions';
|
||||
|
||||
function checkConstraints(strategy, regex) {
|
||||
@ -99,15 +99,16 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
||||
featureMetrics,
|
||||
settings,
|
||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||
loading: state.apiCalls.fetchTogglesState.loading,
|
||||
};
|
||||
};
|
||||
const mapStateToProps = mapStateToPropsConfigurable(true);
|
||||
const mapDispatchToProps = {
|
||||
toggleFeature,
|
||||
fetchFeatureToggles,
|
||||
fetcher: () => fetchFeatureToggles(),
|
||||
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