mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
feat: add support for projects
This commit is contained in:
parent
8d1ffba52c
commit
264e9c56ae
@ -140,8 +140,8 @@ IconLink.propTypes = {
|
|||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownButton = ({ label, id, className }) => (
|
export const DropdownButton = ({ label, id, className, title }) => (
|
||||||
<Button id={id} className={className || styles.dropdownButton}>
|
<Button id={id} className={[className, styles.dropdownButton].join(' ')} title={title}>
|
||||||
{label}
|
{label}
|
||||||
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -149,6 +149,7 @@ export const DropdownButton = ({ label, id, className }) => (
|
|||||||
DropdownButton.propTypes = {
|
DropdownButton.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
||||||
|
@ -51,11 +51,15 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
3 years ago
|
3 years ago
|
||||||
</time>
|
</time>
|
||||||
</small>
|
</small>
|
||||||
|
<div
|
||||||
|
className="mdl-list__item-sub-title"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className="mdl-list__item-sub-title truncate"
|
className="truncate"
|
||||||
>
|
>
|
||||||
another's description
|
another's description
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@ -115,11 +119,15 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
3 years ago
|
3 years ago
|
||||||
</time>
|
</time>
|
||||||
</small>
|
</small>
|
||||||
|
<div
|
||||||
|
className="mdl-list__item-sub-title"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className="mdl-list__item-sub-title truncate"
|
className="truncate"
|
||||||
>
|
>
|
||||||
another's description
|
another's description
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -57,6 +57,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
className=" dropdownButton"
|
className=" dropdownButton"
|
||||||
id="metric"
|
id="metric"
|
||||||
|
title="Metric interval"
|
||||||
>
|
>
|
||||||
Last minute
|
Last minute
|
||||||
<react-mdl-Icon
|
<react-mdl-Icon
|
||||||
@ -116,6 +117,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
className=" dropdownButton"
|
className=" dropdownButton"
|
||||||
id="sorting"
|
id="sorting"
|
||||||
|
title="Sort by"
|
||||||
>
|
>
|
||||||
By name
|
By name
|
||||||
<react-mdl-Icon
|
<react-mdl-Icon
|
||||||
@ -175,6 +177,14 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
Metrics
|
Metrics
|
||||||
</react-mdl-MenuItem>
|
</react-mdl-MenuItem>
|
||||||
</react-mdl-Menu>
|
</react-mdl-Menu>
|
||||||
|
<Project
|
||||||
|
settings={
|
||||||
|
Object {
|
||||||
|
"sort": "name",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSetting={[MockFunction]}
|
||||||
|
/>
|
||||||
</react-mdl-CardActions>
|
</react-mdl-CardActions>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<react-mdl-List>
|
||||||
@ -243,6 +253,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
className=" dropdownButton"
|
className=" dropdownButton"
|
||||||
id="metric"
|
id="metric"
|
||||||
|
title="Metric interval"
|
||||||
>
|
>
|
||||||
Last minute
|
Last minute
|
||||||
<react-mdl-Icon
|
<react-mdl-Icon
|
||||||
@ -302,6 +313,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
className=" dropdownButton"
|
className=" dropdownButton"
|
||||||
id="sorting"
|
id="sorting"
|
||||||
|
title="Sort by"
|
||||||
>
|
>
|
||||||
By name
|
By name
|
||||||
<react-mdl-Icon
|
<react-mdl-Icon
|
||||||
@ -361,6 +373,14 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
Metrics
|
Metrics
|
||||||
</react-mdl-MenuItem>
|
</react-mdl-MenuItem>
|
||||||
</react-mdl-Menu>
|
</react-mdl-Menu>
|
||||||
|
<Project
|
||||||
|
settings={
|
||||||
|
Object {
|
||||||
|
"sort": "name",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSetting={[MockFunction]}
|
||||||
|
/>
|
||||||
</react-mdl-CardActions>
|
</react-mdl-CardActions>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<react-mdl-List>
|
||||||
|
@ -58,6 +58,11 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="release"
|
value="release"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ProjectSelect
|
||||||
|
filled={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
</react-mdl-CardText>
|
</react-mdl-CardText>
|
||||||
<react-mdl-CardActions
|
<react-mdl-CardActions
|
||||||
border={true}
|
border={true}
|
||||||
@ -92,7 +97,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
className="mdl-button"
|
className="mdl-button dropdownButton"
|
||||||
id="update_status"
|
id="update_status"
|
||||||
>
|
>
|
||||||
Status
|
Status
|
||||||
|
@ -11,6 +11,8 @@ jest.mock('../feature-list-item-component', () => ({
|
|||||||
default: 'Feature',
|
default: 'Feature',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../project-container', () => 'Project');
|
||||||
|
|
||||||
test('renders correctly with one feature', () => {
|
test('renders correctly with one feature', () => {
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,7 @@ jest.mock('../form/form-update-feature-container', () => ({
|
|||||||
default: 'UpdateFeatureToggleComponent',
|
default: 'UpdateFeatureToggleComponent',
|
||||||
}));
|
}));
|
||||||
jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect');
|
jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect');
|
||||||
|
jest.mock('../form/project-select-container', () => 'ProjectSelect');
|
||||||
|
|
||||||
test('renders correctly with one feature', () => {
|
test('renders correctly with one feature', () => {
|
||||||
const feature = {
|
const feature = {
|
||||||
|
@ -53,7 +53,9 @@ const Feature = ({
|
|||||||
<small className="mdl-color-text--blue-grey-300">
|
<small className="mdl-color-text--blue-grey-300">
|
||||||
<TimeAgo date={createdAt} live={false} />
|
<TimeAgo date={createdAt} live={false} />
|
||||||
</small>
|
</small>
|
||||||
<span className={['mdl-list__item-sub-title', commonStyles.truncate].join(' ')}>{description}</span>
|
<div className="mdl-list__item-sub-title">
|
||||||
|
<span className={commonStyles.truncate}>{description}</span>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>
|
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>
|
||||||
|
@ -33,6 +33,7 @@ exports[`render the create feature page 1`] = `
|
|||||||
name="name"
|
name="name"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
placeholder="Unique-name"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
@ -64,6 +65,17 @@ exports[`render the create feature page 1`] = `
|
|||||||
</react-mdl-Switch>
|
</react-mdl-Switch>
|
||||||
</react-mdl-Cell>
|
</react-mdl-Cell>
|
||||||
</react-mdl-Grid>
|
</react-mdl-Grid>
|
||||||
|
<section
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0 16px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Connect(ProjectSelectComponent)
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
<section
|
<section
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
@ -75,6 +87,7 @@ exports[`render the create feature page 1`] = `
|
|||||||
floatingLabel={true}
|
floatingLabel={true}
|
||||||
label="Description"
|
label="Description"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
placeholder="A short description of the feature toggle"
|
||||||
rows={1}
|
rows={1}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl';
|
import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl';
|
||||||
import StrategiesSection from './strategies-section-container';
|
import StrategiesSection from './strategies-section-container';
|
||||||
import FeatureTypeSelect from './feature-type-select-container';
|
import FeatureTypeSelect from './feature-type-select-container';
|
||||||
|
import ProjectSelect from './project-select-container';
|
||||||
|
|
||||||
import { FormButtons } from './../../common';
|
import { FormButtons } from './../../common';
|
||||||
import { styles as commonStyles } from '../../common';
|
import { styles as commonStyles } from '../../common';
|
||||||
@ -44,6 +45,7 @@ class AddFeatureComponent extends Component {
|
|||||||
floatingLabel
|
floatingLabel
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
label="Name"
|
label="Name"
|
||||||
|
placeholder="Unique-name"
|
||||||
name="name"
|
name="name"
|
||||||
value={input.name}
|
value={input.name}
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
@ -65,12 +67,16 @@ class AddFeatureComponent extends Component {
|
|||||||
</Switch>
|
</Switch>
|
||||||
</Cell>
|
</Cell>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<section style={{ padding: '0 16px' }}>
|
||||||
|
<ProjectSelect value={input.project} onChange={v => setValue('project', v.target.value)} />
|
||||||
|
</section>
|
||||||
<section style={{ padding: '0 16px' }}>
|
<section style={{ padding: '0 16px' }}>
|
||||||
<Textfield
|
<Textfield
|
||||||
floatingLabel
|
floatingLabel
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
rows={1}
|
rows={1}
|
||||||
label="Description"
|
label="Description"
|
||||||
|
placeholder="A short description of the feature toggle"
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
value={input.description}
|
value={input.description}
|
||||||
onChange={v => setValue('description', v.target.value)}
|
onChange={v => setValue('description', v.target.value)}
|
||||||
|
@ -13,7 +13,14 @@ class WrapperComponent extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
const name = loadNameFromHash();
|
const name = loadNameFromHash();
|
||||||
this.state = {
|
this.state = {
|
||||||
featureToggle: { name, description: '', type: 'release', strategies: [], enabled: true },
|
featureToggle: {
|
||||||
|
name,
|
||||||
|
description: '',
|
||||||
|
type: 'release',
|
||||||
|
strategies: [],
|
||||||
|
enabled: true,
|
||||||
|
project: props.currentProjectId,
|
||||||
|
},
|
||||||
errors: {},
|
errors: {},
|
||||||
dirty: false,
|
dirty: false,
|
||||||
};
|
};
|
||||||
@ -109,6 +116,7 @@ class WrapperComponent extends Component {
|
|||||||
WrapperComponent.propTypes = {
|
WrapperComponent.propTypes = {
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
createFeatureToggles: PropTypes.func.isRequired,
|
createFeatureToggles: PropTypes.func.isRequired,
|
||||||
|
currentProjectId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
@ -116,6 +124,13 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
|
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
const FormAddContainer = connect(() => ({}), mapDispatchToProps)(WrapperComponent);
|
const mapStateToProps = state => {
|
||||||
|
const settings = state.settings.toJS().feature || {};
|
||||||
|
const currentProjectId = settings.currentProjectId || 'default';
|
||||||
|
|
||||||
|
return { currentProjectId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(WrapperComponent);
|
||||||
|
|
||||||
export default FormAddContainer;
|
export default FormAddContainer;
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import MySelect from '../../common/select';
|
||||||
|
|
||||||
|
class ProjectSelectComponent extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { fetchProjects, projects } = this.props;
|
||||||
|
if (projects[0].inital && fetchProjects) {
|
||||||
|
this.props.fetchProjects();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { value, projects, onChange, filled } = this.props;
|
||||||
|
|
||||||
|
if (!projects || projects.length === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = projects.map(t => ({ key: t.id, label: t.name, title: t.description }));
|
||||||
|
|
||||||
|
if (!options.find(o => o.key === value)) {
|
||||||
|
options.push({ key: value, label: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MySelect label="Project" options={options} value={value} onChange={onChange} filled={filled} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectSelectComponent.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
filled: PropTypes.bool,
|
||||||
|
projects: PropTypes.array.isRequired,
|
||||||
|
fetchProjects: PropTypes.func,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectSelectComponent;
|
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ProjectSelectComponent from './project-select-component';
|
||||||
|
import { fetchProjects } from './../../../store/project/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const projects = state.projects.toJS();
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectContainer = connect(mapStateToProps, { fetchProjects })(ProjectSelectComponent);
|
||||||
|
|
||||||
|
export default ProjectContainer;
|
@ -7,6 +7,7 @@ import Feature from './feature-list-item-component';
|
|||||||
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
|
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
|
||||||
import SearchField from '../common/search-field';
|
import SearchField from '../common/search-field';
|
||||||
import { CREATE_FEATURE } from '../../permissions';
|
import { CREATE_FEATURE } from '../../permissions';
|
||||||
|
import ProjectMenu from './project-container';
|
||||||
|
|
||||||
export default class FeatureListComponent extends React.Component {
|
export default class FeatureListComponent extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -76,7 +77,11 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<DropdownButton id="metric" label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`} />
|
<DropdownButton
|
||||||
|
id="metric"
|
||||||
|
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
||||||
|
title="Metric interval"
|
||||||
|
/>
|
||||||
<Menu target="metric" onClick={() => this.toggleMetrics()} style={{ width: '168px' }}>
|
<Menu target="metric" onClick={() => this.toggleMetrics()} style={{ width: '168px' }}>
|
||||||
<MenuItemWithIcon
|
<MenuItemWithIcon
|
||||||
icon="hourglass_empty"
|
icon="hourglass_empty"
|
||||||
@ -91,7 +96,7 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
label="Last hour"
|
label="Last hour"
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
<DropdownButton id="sorting" label={`By ${settings.sort}`} />
|
<DropdownButton id="sorting" label={`By ${settings.sort}`} title="Sort by" />
|
||||||
<Menu
|
<Menu
|
||||||
target="sorting"
|
target="sorting"
|
||||||
onClick={e => this.setSort(e.target.getAttribute('data-target'))}
|
onClick={e => this.setSort(e.target.getAttribute('data-target'))}
|
||||||
@ -119,6 +124,7 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
Metrics
|
Metrics
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
<ProjectMenu settings={this.props.settings} updateSetting={this.props.updateSetting} />
|
||||||
</CardActions>
|
</CardActions>
|
||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
|
@ -9,6 +9,11 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
|||||||
const featureMetrics = state.featureMetrics.toJS();
|
const featureMetrics = state.featureMetrics.toJS();
|
||||||
const settings = state.settings.toJS().feature || {};
|
const settings = state.settings.toJS().feature || {};
|
||||||
let features = isFeature ? state.features.toJS() : state.archive.get('list').toArray();
|
let features = isFeature ? state.features.toJS() : state.archive.get('list').toArray();
|
||||||
|
|
||||||
|
if (settings.currentProjectId) {
|
||||||
|
features = features.filter(f => f.project === settings.currentProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.filter) {
|
if (settings.filter) {
|
||||||
try {
|
try {
|
||||||
const regex = new RegExp(settings.filter, 'i');
|
const regex = new RegExp(settings.filter, 'i');
|
||||||
|
66
frontend/src/component/feature/project-component.jsx
Normal file
66
frontend/src/component/feature/project-component.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Menu, MenuItem } from 'react-mdl';
|
||||||
|
import { DropdownButton } from '../common';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const ALL_PROJECTS = { id: undefined, name: '> All projects' };
|
||||||
|
|
||||||
|
function projectItem(selectedId, item) {
|
||||||
|
return (
|
||||||
|
<MenuItem disabled={selectedId === item.id} data-target={item.id} key={item.id}>
|
||||||
|
{item.name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectComponent({ fetchProjects, projects, currentProjectId, updateCurrentProject }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (projects[0].inital) {
|
||||||
|
fetchProjects();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function 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;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DropdownButton
|
||||||
|
className="mdl-color--amber-50"
|
||||||
|
id="project"
|
||||||
|
label={`${curentProject.name}`}
|
||||||
|
title="Select project"
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
target="project"
|
||||||
|
onClick={e => setProject(e.target.getAttribute('data-target'))}
|
||||||
|
style={{ width: '168px' }}
|
||||||
|
>
|
||||||
|
<MenuItem disabled={curentProject === ALL_PROJECTS} data-target={ALL_PROJECTS.id}>
|
||||||
|
{ALL_PROJECTS.name}
|
||||||
|
</MenuItem>
|
||||||
|
{projects.map(p => projectItem(currentProjectId, p))}
|
||||||
|
</Menu>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectComponent.propTypes = {
|
||||||
|
projects: PropTypes.array.isRequired,
|
||||||
|
fetchProjects: PropTypes.func.isRequired,
|
||||||
|
currentProjectId: PropTypes.string.isRequired,
|
||||||
|
updateCurrentProject: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectComponent;
|
11
frontend/src/component/feature/project-container.jsx
Normal file
11
frontend/src/component/feature/project-container.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Component from './project-component';
|
||||||
|
import { fetchProjects } from './../../store/project/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
|
projects: state.projects.toJS(),
|
||||||
|
currentProjectId: ownProps.settings.currentProjectId || '*',
|
||||||
|
updateCurrentProject: id => ownProps.updateSetting('currentProjectId', id),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, { fetchProjects })(Component);
|
@ -9,6 +9,7 @@ import EditFeatureToggle from './form/form-update-feature-container';
|
|||||||
import EditVariants from './variant/update-variant-container';
|
import EditVariants from './variant/update-variant-container';
|
||||||
import ViewFeatureToggle from './form/form-view-feature-container';
|
import ViewFeatureToggle from './form/form-view-feature-container';
|
||||||
import FeatureTypeSelect from './form/feature-type-select-container';
|
import FeatureTypeSelect from './form/feature-type-select-container';
|
||||||
|
import ProjectSelect from './form/project-select-container';
|
||||||
import UpdateDescriptionComponent from './form/update-description-component';
|
import UpdateDescriptionComponent from './form/update-description-component';
|
||||||
import { styles as commonStyles } from '../common';
|
import { styles as commonStyles } from '../common';
|
||||||
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
|
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
|
||||||
@ -162,6 +163,19 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
this.props.editFeatureToggle(feature);
|
this.props.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.editFeatureToggle(feature);
|
||||||
|
};
|
||||||
|
|
||||||
const updateStale = stale => {
|
const updateStale = stale => {
|
||||||
this.props.setStale(stale, featureToggleName);
|
this.props.setStale(stale, featureToggleName);
|
||||||
};
|
};
|
||||||
@ -187,6 +201,8 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
</CardText>
|
</CardText>
|
||||||
<CardText style={{ paddingTop: 0 }}>
|
<CardText style={{ paddingTop: 0 }}>
|
||||||
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled />
|
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled />
|
||||||
|
|
||||||
|
<ProjectSelect value={featureToggle.project} onChange={updateProject} filled />
|
||||||
</CardText>
|
</CardText>
|
||||||
|
|
||||||
<CardActions
|
<CardActions
|
||||||
|
@ -140,6 +140,25 @@ Array [
|
|||||||
"path": "/context",
|
"path": "/context",
|
||||||
"title": "Context Fields",
|
"title": "Context Fields",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/projects",
|
||||||
|
"path": "/projects/create",
|
||||||
|
"title": "Create",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/projects",
|
||||||
|
"path": "/projects/edit/:id",
|
||||||
|
"title": ":id",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"hidden": true,
|
||||||
|
"icon": "folder_open",
|
||||||
|
"path": "/projects",
|
||||||
|
"title": "Projects",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": "exit_to_app",
|
"icon": "exit_to_app",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { routes, baseRoutes, getRoute } from '../routes';
|
import { routes, baseRoutes, getRoute } from '../routes';
|
||||||
|
|
||||||
test('returns all defined routes', () => {
|
test('returns all defined routes', () => {
|
||||||
expect(routes.length).toEqual(17);
|
expect(routes.length).toEqual(20);
|
||||||
expect(routes).toMatchSnapshot();
|
expect(routes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,6 +15,9 @@ import ContextFields from '../../page/context';
|
|||||||
import CreateContextField from '../../page/context/create';
|
import CreateContextField from '../../page/context/create';
|
||||||
import EditContextField from '../../page/context/edit';
|
import EditContextField from '../../page/context/edit';
|
||||||
import LogoutFeatures from '../../page/user/logout';
|
import LogoutFeatures from '../../page/user/logout';
|
||||||
|
import ListProjects from '../../page/project';
|
||||||
|
import CreateProject from '../../page/project/create';
|
||||||
|
import EditProject from '../../page/project/edit';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Features
|
// Features
|
||||||
@ -50,6 +53,11 @@ export const routes = [
|
|||||||
{ path: '/context/edit/:name', parent: '/context', title: ':name', component: EditContextField },
|
{ path: '/context/edit/:name', parent: '/context', title: ':name', component: EditContextField },
|
||||||
{ path: '/context', title: 'Context Fields', icon: 'apps', component: ContextFields, hidden: true },
|
{ path: '/context', title: 'Context Fields', icon: 'apps', component: ContextFields, hidden: true },
|
||||||
|
|
||||||
|
// Project
|
||||||
|
{ path: '/projects/create', parent: '/projects', title: 'Create', component: CreateProject },
|
||||||
|
{ path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject },
|
||||||
|
{ path: '/projects', title: 'Projects', icon: 'folder_open', component: ListProjects, hidden: true },
|
||||||
|
|
||||||
{ path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures },
|
{ path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
17
frontend/src/component/project/create-project-container.js
Normal file
17
frontend/src/component/project/create-project-container.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ProjectComponent from './form-project-component';
|
||||||
|
import { createProject, validateId } from './../../store/project/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({
|
||||||
|
project: { id: '', name: '', description: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
validateId,
|
||||||
|
submit: project => createProject(project)(dispatch),
|
||||||
|
editMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(ProjectComponent);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
23
frontend/src/component/project/edit-project-container.js
Normal file
23
frontend/src/component/project/edit-project-container.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Component from './form-project-component';
|
||||||
|
import { updateProject, validateId } from './../../store/project/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => {
|
||||||
|
const projectBase = { id: '', name: '', description: '' };
|
||||||
|
const realProject = state.projects.toJS().find(n => n.id === props.projectId);
|
||||||
|
const project = Object.assign(projectBase, realProject);
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
validateId,
|
||||||
|
submit: project => updateProject(project)(dispatch),
|
||||||
|
editMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
120
frontend/src/component/project/form-project-component.jsx
Normal file
120
frontend/src/component/project/form-project-component.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Textfield, Card, CardTitle, CardText, CardActions } from 'react-mdl';
|
||||||
|
|
||||||
|
import { FormButtons, styles as commonStyles } from '../common';
|
||||||
|
import { trim } from '../common/util';
|
||||||
|
|
||||||
|
class AddContextComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
project: props.project,
|
||||||
|
errors: {},
|
||||||
|
currentLegalValue: '',
|
||||||
|
dirty: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
if (!state.project.id && props.project.id) {
|
||||||
|
return { project: props.project };
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue = (field, value) => {
|
||||||
|
const { project } = this.state;
|
||||||
|
project[field] = value;
|
||||||
|
this.setState({ project, dirty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
validateId = async id => {
|
||||||
|
const { errors } = this.state;
|
||||||
|
const { validateId } = this.props;
|
||||||
|
try {
|
||||||
|
await validateId(id);
|
||||||
|
errors.id = undefined;
|
||||||
|
} catch (err) {
|
||||||
|
errors.id = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ errors });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.history.push('/projects');
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { project } = this.state;
|
||||||
|
await this.props.submit(project);
|
||||||
|
this.props.history.push('/projects');
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { project, errors } = this.state;
|
||||||
|
const { editMode } = this.props;
|
||||||
|
const submitText = editMode ? 'Update' : 'Create';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
|
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
||||||
|
{submitText} Project
|
||||||
|
</CardTitle>
|
||||||
|
<CardText>Projects allows you to group feature toggles together in the managemnt UI.</CardText>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<section style={{ padding: '16px' }}>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Project Id"
|
||||||
|
name="id"
|
||||||
|
placeholder="A-unique-key"
|
||||||
|
value={project.id}
|
||||||
|
error={errors.id}
|
||||||
|
disabled={editMode}
|
||||||
|
onBlur={v => this.validateId(v.target.value)}
|
||||||
|
onChange={v => this.setValue('id', trim(v.target.value))}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Project name"
|
||||||
|
value={project.name}
|
||||||
|
error={errors.name}
|
||||||
|
onChange={v => this.setValue('name', v.target.value)}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="A short description"
|
||||||
|
rows={1}
|
||||||
|
label="Description"
|
||||||
|
error={errors.description}
|
||||||
|
value={project.description}
|
||||||
|
onChange={v => this.setValue('description', v.target.value)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<CardActions>
|
||||||
|
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||||
|
</CardActions>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddContextComponent.propTypes = {
|
||||||
|
project: PropTypes.object.isRequired,
|
||||||
|
validateId: PropTypes.func.isRequired,
|
||||||
|
submit: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
editMode: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddContextComponent;
|
80
frontend/src/component/project/list-component.jsx
Normal file
80
frontend/src/component/project/list-component.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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_PROJECT, DELETE_PROJECT } from '../../permissions';
|
||||||
|
|
||||||
|
class ProjectListComponent extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
projects: PropTypes.array.isRequired,
|
||||||
|
fetchProjects: PropTypes.func.isRequired,
|
||||||
|
removeProject: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeProject = (project, evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.removeProject(project);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { projects, hasPermission } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
|
<HeaderTitle
|
||||||
|
title="Projects (beta)"
|
||||||
|
actions={
|
||||||
|
hasPermission(CREATE_PROJECT) ? (
|
||||||
|
<IconButton
|
||||||
|
raised
|
||||||
|
colored
|
||||||
|
accent
|
||||||
|
name="add"
|
||||||
|
onClick={() => this.props.history.push('/projects/create')}
|
||||||
|
title="Add new project field"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<List>
|
||||||
|
{projects.length > 0 ? (
|
||||||
|
projects.map((project, i) => (
|
||||||
|
<ListItem key={i} twoLine>
|
||||||
|
<ListItemContent icon="folder_open" subtitle={project.description}>
|
||||||
|
<Link to={`/projects/edit/${project.id}`}>
|
||||||
|
<strong>{project.name}</strong>
|
||||||
|
</Link>
|
||||||
|
</ListItemContent>
|
||||||
|
<ListItemAction>
|
||||||
|
{hasPermission(DELETE_PROJECT) ? (
|
||||||
|
<IconButton
|
||||||
|
name="delete"
|
||||||
|
title="Remove project"
|
||||||
|
onClick={this.removeProject.bind(this, project)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</ListItemAction>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<ListItem>No projects defined</ListItem>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectListComponent;
|
27
frontend/src/component/project/list-container.jsx
Normal file
27
frontend/src/component/project/list-container.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Component from './list-component.jsx';
|
||||||
|
import { fetchProjects, removeProject } from './../../store/project/actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const projects = state.projects.toJS();
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
removeProject: project => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm('Are you sure you want to remove this project?')) {
|
||||||
|
removeProject(project)(dispatch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchProjects: () => fetchProjects()(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProjectListContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||||
|
|
||||||
|
export default ProjectListContainer;
|
@ -33,6 +33,14 @@ export class ForbiddenError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
constructor(statusCode) {
|
||||||
|
super('The requested resource could not be found but may be available in the future');
|
||||||
|
this.name = 'NotFoundError';
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function throwIfNotSuccess(response) {
|
export function throwIfNotSuccess(response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
@ -43,6 +51,10 @@ export function throwIfNotSuccess(response) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
response.json().then(body => reject(new ForbiddenError(response.status, body)));
|
response.json().then(body => reject(new ForbiddenError(response.status, body)));
|
||||||
});
|
});
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reject(new NotFoundError(response.status));
|
||||||
|
});
|
||||||
} else if (response.status > 399 && response.status < 499) {
|
} else if (response.status > 399 && response.status < 499) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
response.json().then(body => {
|
response.json().then(body => {
|
||||||
|
52
frontend/src/data/project-api.js
Normal file
52
frontend/src/data/project-api.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from './helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/projects';
|
||||||
|
|
||||||
|
function fetchAll() {
|
||||||
|
return fetch(URI, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(project) {
|
||||||
|
return fetch(URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(project),
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(project) {
|
||||||
|
return fetch(`${URI}/${project.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(project),
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(project) {
|
||||||
|
return fetch(`${URI}/${project.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(id) {
|
||||||
|
return fetch(`${URI}/validate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(id),
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchAll,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
validate,
|
||||||
|
};
|
11
frontend/src/page/project/create.js
Normal file
11
frontend/src/page/project/create.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CreateProject from '../../component/project/create-project-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ history }) => <CreateProject title="Create Project" history={history} />;
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
14
frontend/src/page/project/edit.js
Normal file
14
frontend/src/page/project/edit.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import EditProject from '../../component/project/edit-project-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ match: { params }, history }) => (
|
||||||
|
<EditProject projectId={params.id} title="Edit project" history={history} />
|
||||||
|
);
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
11
frontend/src/page/project/index.js
Normal file
11
frontend/src/page/project/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ProjectList from '../../component/project/list-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ history }) => <ProjectList history={history} />;
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
@ -9,6 +9,9 @@ export const UPDATE_APPLICATION = 'UPDATE_APPLICATION';
|
|||||||
export const CREATE_CONTEXT_FIELD = 'CREATE_CONTEXT_FIELD';
|
export const CREATE_CONTEXT_FIELD = 'CREATE_CONTEXT_FIELD';
|
||||||
export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
|
export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
|
||||||
export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
|
export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
|
||||||
|
export const CREATE_PROJECT = 'CREATE_PROJECT';
|
||||||
|
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
||||||
|
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
||||||
|
|
||||||
export function hasPermission(user, permission) {
|
export function hasPermission(user, permission) {
|
||||||
return (
|
return (
|
||||||
|
@ -13,6 +13,8 @@ import { ERROR_UPDATING_STRATEGY, ERROR_CREATING_STRATEGY, ERROR_RECEIVE_STRATEG
|
|||||||
|
|
||||||
import { ERROR_ADD_CONTEXT_FIELD, ERROR_UPDATE_CONTEXT_FIELD } from './context/actions';
|
import { ERROR_ADD_CONTEXT_FIELD, ERROR_UPDATE_CONTEXT_FIELD } from './context/actions';
|
||||||
|
|
||||||
|
import { ERROR_REMOVING_PROJECT, ERROR_ADD_PROJECT, ERROR_UPDATE_PROJECT } from './project/actions';
|
||||||
|
|
||||||
import { UPDATE_APPLICATION_FIELD } from './application/actions';
|
import { UPDATE_APPLICATION_FIELD } from './application/actions';
|
||||||
|
|
||||||
import { FORBIDDEN } from './util';
|
import { FORBIDDEN } from './util';
|
||||||
@ -44,6 +46,9 @@ const strategies = (state = getInitState(), action) => {
|
|||||||
case ERROR_RECEIVE_STRATEGIES:
|
case ERROR_RECEIVE_STRATEGIES:
|
||||||
case ERROR_ADD_CONTEXT_FIELD:
|
case ERROR_ADD_CONTEXT_FIELD:
|
||||||
case ERROR_UPDATE_CONTEXT_FIELD:
|
case ERROR_UPDATE_CONTEXT_FIELD:
|
||||||
|
case ERROR_REMOVING_PROJECT:
|
||||||
|
case ERROR_UPDATE_PROJECT:
|
||||||
|
case ERROR_ADD_PROJECT:
|
||||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||||
case FORBIDDEN:
|
case FORBIDDEN:
|
||||||
return addErrorIfNotAlreadyInList(state, action.error.message || '403 Forbidden');
|
return addErrorIfNotAlreadyInList(state, action.error.message || '403 Forbidden');
|
||||||
|
@ -13,6 +13,7 @@ import user from './user';
|
|||||||
import applications from './application';
|
import applications from './application';
|
||||||
import uiConfig from './ui-config';
|
import uiConfig from './ui-config';
|
||||||
import context from './context';
|
import context from './context';
|
||||||
|
import projects from './project';
|
||||||
|
|
||||||
const unleashStore = combineReducers({
|
const unleashStore = combineReducers({
|
||||||
features,
|
features,
|
||||||
@ -29,6 +30,7 @@ const unleashStore = combineReducers({
|
|||||||
applications,
|
applications,
|
||||||
uiConfig,
|
uiConfig,
|
||||||
context,
|
context,
|
||||||
|
projects,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default unleashStore;
|
export default unleashStore;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { fetchUIConfig } from './ui-config/actions';
|
import { fetchUIConfig } from './ui-config/actions';
|
||||||
import { fetchContext } from './context/actions';
|
import { fetchContext } from './context/actions';
|
||||||
import { fetchFeatureTypes } from './feature-type/actions';
|
import { fetchFeatureTypes } from './feature-type/actions';
|
||||||
|
import { fetchProjects } from './project/actions';
|
||||||
|
|
||||||
export function loadInitalData() {
|
export function loadInitalData() {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
fetchUIConfig()(dispatch);
|
fetchUIConfig()(dispatch);
|
||||||
fetchContext()(dispatch);
|
fetchContext()(dispatch);
|
||||||
fetchFeatureTypes()(dispatch);
|
fetchFeatureTypes()(dispatch);
|
||||||
|
fetchProjects()(dispatch);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
57
frontend/src/store/project/actions.js
Normal file
57
frontend/src/store/project/actions.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import api from '../../data/project-api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
|
export const RECEIVE_PROJECT = 'RECEIVE_PROJECT';
|
||||||
|
export const ERROR_RECEIVE_PROJECT = 'ERROR_RECEIVE_PROJECT';
|
||||||
|
export const REMOVE_PROJECT = 'REMOVE_PROJECT';
|
||||||
|
export const ERROR_REMOVING_PROJECT = 'ERROR_REMOVING_PROJECT';
|
||||||
|
export const ADD_PROJECT = 'ADD_PROJECT';
|
||||||
|
export const ERROR_ADD_PROJECT = 'ERROR_ADD_PROJECT';
|
||||||
|
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
||||||
|
export const ERROR_UPDATE_PROJECT = 'ERROR_UPDATE_PROJECT';
|
||||||
|
|
||||||
|
const addProject = project => ({ type: ADD_PROJECT, project });
|
||||||
|
const upProject = project => ({ type: UPDATE_PROJECT, project });
|
||||||
|
const delProject = project => ({ type: REMOVE_PROJECT, project });
|
||||||
|
|
||||||
|
export function fetchProjects() {
|
||||||
|
return () => {};
|
||||||
|
/*
|
||||||
|
const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.fetchAll()
|
||||||
|
.then(json => {
|
||||||
|
dispatch(receiveProjects(json.projects));
|
||||||
|
})
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_PROJECT));
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeProject(project) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.remove(project)
|
||||||
|
.then(() => dispatch(delProject(project)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_PROJECT));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProject(project) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.create(project)
|
||||||
|
.then(() => dispatch(addProject(project)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_ADD_PROJECT));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProject(project) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.update(project)
|
||||||
|
.then(() => dispatch(upProject(project)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_PROJECT));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateId(id) {
|
||||||
|
return api.validate({ id });
|
||||||
|
}
|
33
frontend/src/store/project/index.js
Normal file
33
frontend/src/store/project/index.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { List } from 'immutable';
|
||||||
|
import { RECEIVE_PROJECT, REMOVE_PROJECT, ADD_PROJECT, UPDATE_PROJECT } from './actions';
|
||||||
|
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||||
|
|
||||||
|
const DEFAULT_PROJECTS = [{ id: 'default', name: 'Default', inital: true }];
|
||||||
|
|
||||||
|
function getInitState() {
|
||||||
|
return new List(DEFAULT_PROJECTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategies = (state = getInitState(), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case RECEIVE_PROJECT:
|
||||||
|
return new List(action.value);
|
||||||
|
case REMOVE_PROJECT: {
|
||||||
|
const index = state.findIndex(item => item.id === action.project.id);
|
||||||
|
return state.remove(index);
|
||||||
|
}
|
||||||
|
case ADD_PROJECT:
|
||||||
|
return state.push(action.project);
|
||||||
|
case UPDATE_PROJECT: {
|
||||||
|
const index = state.findIndex(item => item.id === action.project.id);
|
||||||
|
return state.set(index, action.project);
|
||||||
|
}
|
||||||
|
case USER_LOGOUT:
|
||||||
|
case USER_LOGIN:
|
||||||
|
return getInitState();
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default strategies;
|
@ -1,4 +1,4 @@
|
|||||||
import { fromJS, Map as $Map } from 'immutable';
|
import { fromJS } from 'immutable';
|
||||||
import { UPDATE_SETTING } from './actions';
|
import { UPDATE_SETTING } from './actions';
|
||||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||||
|
|
||||||
@ -6,12 +6,16 @@ import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
|||||||
const localStorage = window.localStorage || {};
|
const localStorage = window.localStorage || {};
|
||||||
const SETTINGS = 'settings';
|
const SETTINGS = 'settings';
|
||||||
|
|
||||||
|
const DEFAULT = fromJS({
|
||||||
|
feature: { currentProjectId: 'default' },
|
||||||
|
});
|
||||||
|
|
||||||
function getInitState() {
|
function getInitState() {
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(localStorage.getItem(SETTINGS));
|
const state = JSON.parse(localStorage.getItem(SETTINGS));
|
||||||
return state ? fromJS(state) : new $Map();
|
return state ? DEFAULT.merge(state) : DEFAULT;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return new $Map();
|
return DEFAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user