mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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>
 | 
				
			||||||
      <span
 | 
					      <div
 | 
				
			||||||
        className="mdl-list__item-sub-title truncate"
 | 
					        className="mdl-list__item-sub-title"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        another's description
 | 
					        <span
 | 
				
			||||||
      </span>
 | 
					          className="truncate"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          another's description
 | 
				
			||||||
 | 
					        </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>
 | 
				
			||||||
      <span
 | 
					      <div
 | 
				
			||||||
        className="mdl-list__item-sub-title truncate"
 | 
					        className="mdl-list__item-sub-title"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        another's description
 | 
					        <span
 | 
				
			||||||
      </span>
 | 
					          className="truncate"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          another's description
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </span>
 | 
					  </span>
 | 
				
			||||||
  <span
 | 
					  <span
 | 
				
			||||||
 | 
				
			|||||||
@ -55,8 +55,9 @@ exports[`renders correctly with one feature 1`] = `
 | 
				
			|||||||
  >
 | 
					  >
 | 
				
			||||||
    <react-mdl-CardActions>
 | 
					    <react-mdl-CardActions>
 | 
				
			||||||
      <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
 | 
				
			||||||
@ -114,8 +115,9 @@ exports[`renders correctly with one feature 1`] = `
 | 
				
			|||||||
        </react-mdl-MenuItem>
 | 
					        </react-mdl-MenuItem>
 | 
				
			||||||
      </react-mdl-Menu>
 | 
					      </react-mdl-Menu>
 | 
				
			||||||
      <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>
 | 
				
			||||||
@ -241,8 +251,9 @@ exports[`renders correctly with one feature without permissions 1`] = `
 | 
				
			|||||||
  >
 | 
					  >
 | 
				
			||||||
    <react-mdl-CardActions>
 | 
					    <react-mdl-CardActions>
 | 
				
			||||||
      <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
 | 
				
			||||||
@ -300,8 +311,9 @@ exports[`renders correctly with one feature without permissions 1`] = `
 | 
				
			|||||||
        </react-mdl-MenuItem>
 | 
					        </react-mdl-MenuItem>
 | 
				
			||||||
      </react-mdl-Menu>
 | 
					      </react-mdl-Menu>
 | 
				
			||||||
      <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