1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Merge pull request #225 from Unleash/feat/remove_apps

This commit is contained in:
Ivar Conradi Østhus 2020-09-25 21:13:27 +02:00 committed by GitHub
commit c2e16e7f73
19 changed files with 461 additions and 270 deletions

View File

@ -30,7 +30,7 @@
"build:ico": "cp public/*.ico dist/.",
"build:img": "cp public/*.png dist/public/.",
"start": "NODE_ENV=development webpack-dev-server --progress --colors",
"start:heroku": "UNLEASH_API=http://unleash.herokuapp.com npm run start",
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com npm run start",
"lint": "eslint . --ext js,jsx",
"lint:fix": "eslint . --ext js,jsx --fix",
"test": "jest",

View File

@ -23,7 +23,7 @@ exports[`renders correctly with permissions 1`] = `
<react-mdl-Icon
name="apps"
/>
 
test-app
</react-mdl-CardTitle>
<react-mdl-CardText>
@ -41,27 +41,48 @@ exports[`renders correctly with permissions 1`] = `
/>
</a>
</react-mdl-CardMenu>
<hr />
<react-mdl-Tabs
activeTab={0}
className="mdl-color--grey-100"
onChange={[Function]}
ripple={true}
tabBarProps={
Object {
"style": Object {
"width": "100%",
},
<div>
<react-mdl-CardActions
border={true}
style={
Object {
"alignItems": "center",
"display": "flex",
"justifyContent": "space-between",
}
}
}
>
<react-mdl-Tab>
Details
</react-mdl-Tab>
<react-mdl-Tab>
Edit
</react-mdl-Tab>
</react-mdl-Tabs>
>
<span />
<react-mdl-Button
accent={true}
onClick={[Function]}
title="Delete application"
>
Delete
</react-mdl-Button>
</react-mdl-CardActions>
<hr />
<react-mdl-Tabs
activeTab={0}
className="mdl-color--grey-100"
onChange={[Function]}
ripple={true}
tabBarProps={
Object {
"style": Object {
"width": "100%",
},
}
}
>
<react-mdl-Tab>
Details
</react-mdl-Tab>
<react-mdl-Tab>
Edit
</react-mdl-Tab>
</react-mdl-Tabs>
</div>
<react-mdl-Grid
style={
Object {
@ -71,6 +92,7 @@ exports[`renders correctly with permissions 1`] = `
>
<react-mdl-Cell
col={6}
hidePhone={true}
phone={12}
tablet={4}
>
@ -178,8 +200,7 @@ exports[`renders correctly with permissions 1`] = `
subtitle={
<span>
123.123.123.123
last seen at
last seen at
<small>
02/23/2017, 03:56:49 PM
</small>
@ -214,7 +235,7 @@ exports[`renders correctly without permission 1`] = `
<react-mdl-Icon
name="apps"
/>
 
test-app
</react-mdl-CardTitle>
<react-mdl-CardText>
@ -232,7 +253,6 @@ exports[`renders correctly without permission 1`] = `
/>
</a>
</react-mdl-CardMenu>
<hr />
<react-mdl-Grid
style={
@ -243,6 +263,7 @@ exports[`renders correctly without permission 1`] = `
>
<react-mdl-Cell
col={6}
hidePhone={true}
phone={12}
tablet={4}
>
@ -340,8 +361,7 @@ exports[`renders correctly without permission 1`] = `
subtitle={
<span>
123.123.123.123
last seen at
last seen at
<small>
02/23/2017, 03:56:49 PM
</small>

View File

@ -1,61 +1,13 @@
/* eslint react/no-multi-comp:off */
import React, { Component, PureComponent } from 'react';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import {
Grid,
Cell,
Card,
CardTitle,
CardText,
CardMenu,
List,
ListItem,
ListItemContent,
Textfield,
Icon,
ProgressBar,
Tabs,
Tab,
Switch,
} from 'react-mdl';
import { IconLink, shorten, styles as commonStyles } from '../common';
import { Button, Card, CardActions, CardTitle, CardText, CardMenu, Icon, ProgressBar, Tabs, Tab } from 'react-mdl';
import { IconLink, styles as commonStyles } from '../common';
import { formatFullDateTimeWithLocale } from '../common/util';
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../permissions';
import icons from './icon-names';
import MySelect from '../common/select';
class StatefulTextfield extends Component {
static propTypes = {
value: PropTypes.string,
label: PropTypes.string,
rows: PropTypes.number,
onBlur: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = { value: props.value };
this.setValue = function setValue(e) {
this.setState({ value: e.target.value });
}.bind(this);
}
render() {
return (
<Textfield
style={{ width: '100%' }}
label={this.props.label}
floatingLabel
rows={this.props.rows}
value={this.state.value}
onChange={this.setValue}
onBlur={this.props.onBlur}
/>
);
}
}
import { UPDATE_APPLICATION } from '../../permissions';
import ApplicationView from './application-view';
import ApplicationUpdate from './application-update';
class ClientApplications extends PureComponent {
static propTypes = {
@ -64,154 +16,52 @@ class ClientApplications extends PureComponent {
application: PropTypes.object,
location: PropTypes.object,
storeApplicationMetaData: PropTypes.func.isRequired,
deleteApplication: PropTypes.func.isRequired,
hasPermission: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
constructor() {
super();
this.state = { activeTab: 0 };
}
componentDidMount() {
this.props.fetchApplication(this.props.appName);
}
formatFullDateTime(v) {
return formatFullDateTimeWithLocale(v, this.props.location.locale);
}
formatFullDateTime = v => formatFullDateTimeWithLocale(v, this.props.location.locale);
deleteApplication = async evt => {
evt.preventDefault();
const { deleteApplication, appName } = this.props;
await deleteApplication(appName);
this.props.history.push('/applications');
};
render() {
if (!this.props.application) {
return <ProgressBar indeterminate />;
}
const { application, storeApplicationMetaData, hasPermission } = this.props;
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', color } = application;
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps' } = application;
const content =
this.state.activeTab === 0 ? (
<Grid style={{ margin: 0 }}>
<Cell col={6} tablet={4} phone={12}>
<h6> Toggles</h6>
<hr />
<List>
{seenToggles.map(({ name, description, enabled, notFound }, i) =>
notFound ? (
<ListItem twoLine key={i}>
{hasPermission(CREATE_FEATURE) ? (
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
) : (
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
)}
</ListItem>
) : (
<ListItem twoLine key={i}>
<ListItemContent
icon={
<span>
<Switch disabled checked={!!enabled} />
</span>
}
subtitle={shorten(description, 60)}
>
<Link to={`/features/view/${name}`}>{shorten(name, 50)}</Link>
</ListItemContent>
</ListItem>
)
)}
</List>
</Cell>
<Cell col={6} tablet={4} phone={12}>
<h6>Implemented strategies</h6>
<hr />
<List>
{strategies.map(({ name, description, notFound }, i) =>
notFound ? (
<ListItem twoLine key={`${name}-${i}`}>
{hasPermission(CREATE_STRATEGY) ? (
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
) : (
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
)}
</ListItem>
) : (
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'extension'} subtitle={shorten(description, 60)}>
<Link to={`/strategies/view/${name}`}>{shorten(name, 50)}</Link>
</ListItemContent>
</ListItem>
)
)}
</List>
</Cell>
<Cell col={12} tablet={12}>
<h6>{instances.length} Instances registered</h6>
<hr />
<List>
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }, i) => (
<ListItem key={i} twoLine>
<ListItemContent
icon="timeline"
subtitle={
<span>
{clientIp} last seen at{' '}
<small>{this.formatFullDateTime(lastSeen)}</small>
</span>
}
>
{instanceId} {sdkVersion ? `(${sdkVersion})` : ''}
</ListItemContent>
</ListItem>
))}
</List>
</Cell>
</Grid>
<ApplicationView
strategies={strategies}
instances={instances}
seenToggles={seenToggles}
hasPermission={hasPermission}
formatFullDateTime={this.formatFullDateTime}
/>
) : (
<Grid>
<Cell col={12}>
<h5>Edit app meta data</h5>
</Cell>
<Cell col={6} tablet={12}>
<StatefulTextfield
value={url}
label="URL"
type="url"
onBlur={e => storeApplicationMetaData(appName, 'url', e.target.value)}
/>
<br />
<StatefulTextfield
value={description}
label="Description"
rows={5}
onBlur={e => storeApplicationMetaData(appName, 'description', e.target.value)}
/>
</Cell>
<Cell col={6} tablet={12}>
<MySelect
label="Icon"
options={icons.map(v => ({ name: v, label: v }))}
value={icon}
onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)}
filled
/>
<StatefulTextfield
value={color}
label="Color"
onBlur={e => storeApplicationMetaData(appName, 'color', e.target.value)}
/>
</Cell>
</Grid>
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
);
return (
<Card shadow={0} className={commonStyles.fullwidth}>
<CardTitle style={{ paddingTop: '24px', paddingRight: '64px', wordBreak: 'break-all' }}>
<Icon name={icon} />
<Icon name={icon || 'apps'} />
&nbsp;{appName}
</CardTitle>
{description && <CardText>{description}</CardText>}
@ -220,18 +70,33 @@ class ClientApplications extends PureComponent {
<IconLink url={url} icon="link" />
</CardMenu>
)}
<hr />
{hasPermission(UPDATE_APPLICATION) ? (
<Tabs
activeTab={this.state.activeTab}
onChange={tabId => this.setState({ activeTab: tabId })}
ripple
tabBarProps={{ style: { width: '100%' } }}
className="mdl-color--grey-100"
>
<Tab>Details</Tab>
<Tab>Edit</Tab>
</Tabs>
<div>
<CardActions
border
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<span />
<Button accent title="Delete application" onClick={this.deleteApplication}>
Delete
</Button>
</CardActions>
<hr />
<Tabs
activeTab={this.state.activeTab}
onChange={tabId => this.setState({ activeTab: tabId })}
ripple
tabBarProps={{ style: { width: '100%' } }}
className="mdl-color--grey-100"
>
<Tab>Details</Tab>
<Tab>Edit</Tab>
</Tabs>
</div>
) : (
''
)}

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import ApplicationEdit from './application-edit-component';
import { fetchApplication, storeApplicationMetaData } from './../../store/application/actions';
import { fetchApplication, storeApplicationMetaData, deleteApplication } from './../../store/application/actions';
import { hasPermission } from '../../permissions';
const mapStateToProps = (state, props) => {
@ -19,6 +19,7 @@ const mapStateToProps = (state, props) => {
const Constainer = connect(mapStateToProps, {
fetchApplication,
storeApplicationMetaData,
deleteApplication,
})(ApplicationEdit);
export default Constainer;

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ProgressBar, Card, CardText, Icon } from 'react-mdl';
import { AppsLinkList, styles as commonStyles } from '../common';
import SearchField from '../common/search-field';
const Empty = () => (
<React.Fragment>
@ -22,6 +23,8 @@ class ClientStrategies extends Component {
static propTypes = {
applications: PropTypes.array,
fetchAll: PropTypes.func.isRequired,
settings: PropTypes.object.isRequired,
updateSetting: PropTypes.func.isRequired,
};
componentDidMount() {
@ -35,9 +38,17 @@ class ClientStrategies extends Component {
return <ProgressBar indeterminate />;
}
return (
<Card shadow={0} className={commonStyles.fullwidth}>
{applications.length > 0 ? <AppsLinkList apps={applications} /> : <Empty />}
</Card>
<div>
<div className={commonStyles.toolbar}>
<SearchField
value={this.props.settings.filter}
updateValue={this.props.updateSetting.bind(this, 'filter')}
/>
</div>
<Card shadow={0} className={commonStyles.fullwidth}>
{applications.length > 0 ? <AppsLinkList apps={applications} /> : <Empty />}
</Card>
</div>
);
}
}

View File

@ -1,9 +1,21 @@
import { connect } from 'react-redux';
import ApplicationList from './application-list-component';
import { fetchAll } from './../../store/application/actions';
import { updateSettingForGroup } from '../../store/settings/actions';
const mapStateToProps = state => ({ applications: state.applications.get('list').toJS() });
const mapStateToProps = state => {
const applications = state.applications.get('list').toJS();
const settings = state.settings.toJS().application || {};
const Container = connect(mapStateToProps, { fetchAll })(ApplicationList);
const regex = new RegExp(settings.filter, 'i');
return {
applications: settings.filter ? applications.filter(a => regex.test(a.appName)) : applications,
settings,
};
};
const mapDispatchToProps = { fetchAll, updateSetting: updateSettingForGroup('application') };
const Container = connect(mapStateToProps, mapDispatchToProps)(ApplicationList);
export default Container;

View File

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Grid, Cell } from 'react-mdl';
import StatefulTextfield from './stateful-textfield';
import icons from './icon-names';
import MySelect from '../common/select';
function ApplicationUpdate({ application, storeApplicationMetaData }) {
const { appName, icon, url, description } = application;
return (
<Grid>
<Cell col={12}>
<MySelect
label="Icon"
options={icons.map(v => ({ key: v, label: v }))}
value={icon || 'apps'}
onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)}
filled
/>
<StatefulTextfield
value={url}
label="Application URL"
placeholder="https://example.com"
type="url"
onBlur={e => storeApplicationMetaData(appName, 'url', e.target.value)}
/>
<br />
<StatefulTextfield
value={description}
label="Description"
rows={2}
onBlur={e => storeApplicationMetaData(appName, 'description', e.target.value)}
/>
</Cell>
</Grid>
);
}
ApplicationUpdate.propTypes = {
application: PropTypes.object.isRequired,
storeApplicationMetaData: PropTypes.func.isRequired,
};
export default ApplicationUpdate;

View File

@ -0,0 +1,104 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Grid, Cell, List, ListItem, ListItemContent, Switch } from 'react-mdl';
import { shorten } from '../common';
import { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions';
function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) {
return (
<Grid style={{ margin: 0 }}>
<Cell col={6} tablet={4} phone={12} hidePhone>
<h6> Toggles</h6>
<hr />
<List>
{seenToggles.map(({ name, description, enabled, notFound }, i) =>
notFound ? (
<ListItem twoLine key={i}>
{hasPermission(CREATE_FEATURE) ? (
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
) : (
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
)}
</ListItem>
) : (
<ListItem twoLine key={i}>
<ListItemContent
icon={
<span>
<Switch disabled checked={!!enabled} />
</span>
}
subtitle={shorten(description, 60)}
>
<Link to={`/features/view/${name}`}>{shorten(name, 50)}</Link>
</ListItemContent>
</ListItem>
)
)}
</List>
</Cell>
<Cell col={6} tablet={4} phone={12}>
<h6>Implemented strategies</h6>
<hr />
<List>
{strategies.map(({ name, description, notFound }, i) =>
notFound ? (
<ListItem twoLine key={`${name}-${i}`}>
{hasPermission(CREATE_STRATEGY) ? (
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
) : (
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
)}
</ListItem>
) : (
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'extension'} subtitle={shorten(description, 60)}>
<Link to={`/strategies/view/${name}`}>{shorten(name, 50)}</Link>
</ListItemContent>
</ListItem>
)
)}
</List>
</Cell>
<Cell col={12} tablet={12}>
<h6>{instances.length} Instances registered</h6>
<hr />
<List>
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }, i) => (
<ListItem key={i} twoLine>
<ListItemContent
icon="timeline"
subtitle={
<span>
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small>
</span>
}
>
{instanceId} {sdkVersion ? `(${sdkVersion})` : ''}
</ListItemContent>
</ListItem>
))}
</List>
</Cell>
</Grid>
);
}
ApplicationView.propTypes = {
instances: PropTypes.array.isRequired,
seenToggles: PropTypes.array.isRequired,
strategies: PropTypes.array.isRequired,
hasPermission: PropTypes.func.isRequired,
formatFullDateTime: PropTypes.func.isRequired,
};
export default ApplicationView;

View File

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Textfield } from 'react-mdl';
function StatefulTextfield({ value, label, placeholder, rows, onBlur }) {
const [localValue, setLocalValue] = useState(value);
const onChange = e => {
e.preventDefault();
setLocalValue(e.target.value);
};
return (
<Textfield
style={{ width: '100%' }}
label={label}
placeholder={placeholder}
floatingLabel
rows={rows}
value={localValue}
onChange={onChange}
onBlur={onBlur}
/>
);
}
StatefulTextfield.propTypes = {
value: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
rows: PropTypes.number,
onBlur: PropTypes.func.isRequired,
};
export default StatefulTextfield;

View File

@ -85,4 +85,17 @@
.toggleName {
color: #37474f !important;
font-weight: 500;
}
.toolbar {
position: relative;
padding: 0 24px 16px 24px;
text-align: center;
}
.toolbarButton {
position: absolute;
top: 56px;
right: 24px;
z-index: 2;
}

View File

@ -11,14 +11,14 @@ export const shorten = (str, len = 50) => (str && str.length > len ? `${str.subs
export const AppsLinkList = ({ apps }) => (
<List>
{apps.length > 0 &&
apps.map(({ appName, description = '-', icon }) => (
apps.map(({ appName, description, icon }) => (
<ListItem twoLine key={appName}>
<span className="mdl-list__item-primary-content" style={{ minWidth: 0 }}>
<Icon name={icon || 'apps'} className="mdl-list__item-avatar" />
<Link to={`/applications/${appName}`} className={[styles.listLink, styles.truncate].join(' ')}>
{appName}
<span className={['mdl-list__item-sub-title', styles.truncate].join(' ')}>
{description}
{description || 'No descriptionn'}
</span>
</Link>
</span>

View File

@ -0,0 +1,50 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'debounce';
import { FABButton, Icon, Textfield } from 'react-mdl';
function SearchField({ value, updateValue }) {
const [localValue, setLocalValue] = useState(value);
const debounceUpdateValue = debounce(updateValue, 500);
const handleCange = e => {
e.preventDefault();
const v = e.target.value || '';
setLocalValue(v);
debounceUpdateValue(v);
};
const handleKeyPress = e => {
if (e.key === 'Enter') {
updateValue(localValue);
}
};
const updateNow = () => {
updateValue(localValue);
};
return (
<div>
<Textfield
floatingLabel
value={localValue}
onChange={handleCange}
onBlur={updateNow}
onKeyPress={handleKeyPress}
label="Search"
style={{ width: '500px', maxWidth: '80%' }}
/>
<FABButton mini className={'mdl-cell--hide-phone'}>
<Icon name="search" />
</FABButton>
</div>
);
}
SearchField.propTypes = {
value: PropTypes.string.isRequired,
updateValue: PropTypes.func.isRequired,
};
export default SearchField;

View File

@ -5,16 +5,29 @@ exports[`renders correctly with one feature 1`] = `
<div
className="toolbar"
>
<react-mdl-Textfield
floatingLabel={true}
label="Search"
onChange={[Function]}
style={
Object {
"width": "100%",
<div>
<react-mdl-Textfield
floatingLabel={true}
label="Search"
onBlur={[Function]}
onChange={[Function]}
onKeyPress={[Function]}
style={
Object {
"maxWidth": "80%",
"width": "500px",
}
}
}
/>
/>
<react-mdl-FABButton
className="mdl-cell--hide-phone"
mini={true}
>
<react-mdl-Icon
name="search"
/>
</react-mdl-FABButton>
</div>
<a
className="toolbarButton"
href="/features/create"
@ -190,16 +203,29 @@ exports[`renders correctly with one feature without permissions 1`] = `
<div
className="toolbar"
>
<react-mdl-Textfield
floatingLabel={true}
label="Search"
onChange={[Function]}
style={
Object {
"width": "100%",
<div>
<react-mdl-Textfield
floatingLabel={true}
label="Search"
onBlur={[Function]}
onChange={[Function]}
onKeyPress={[Function]}
style={
Object {
"maxWidth": "80%",
"width": "500px",
}
}
}
/>
/>
<react-mdl-FABButton
className="mdl-cell--hide-phone"
mini={true}
>
<react-mdl-Icon
name="search"
/>
</react-mdl-FABButton>
</div>
</div>
<react-mdl-Card

View File

@ -1,15 +1,3 @@
.toolbar {
position: relative;
padding: 0 104px 16px 24px;
}
.toolbarButton {
position: absolute;
top: 56px;
right: 24px;
z-index: 2;
}
.listItemMetric {
width: 40px;
flex-shrink: 0;

View File

@ -2,10 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'debounce';
import { Link } from 'react-router-dom';
import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
import { Icon, FABButton, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
import Feature from './feature-list-item-component';
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
import styles from './feature.scss';
import SearchField from '../common/search-field';
import { CREATE_FEATURE } from '../../permissions';
export default class FeatureListComponent extends React.Component {
@ -59,18 +59,13 @@ export default class FeatureListComponent extends React.Component {
});
return (
<div>
<div className={styles.toolbar}>
<Textfield
floatingLabel
value={this.state.filter}
onChange={e => {
this.setFilter(e.target.value);
}}
label="Search"
style={{ width: '100%' }}
<div className={commonStyles.toolbar}>
<SearchField
value={this.props.settings.filter}
updateValue={this.props.updateSetting.bind(this, 'filter')}
/>
{hasPermission(CREATE_FEATURE) ? (
<Link to="/features/create" className={styles.toolbarButton}>
<Link to="/features/create" className={commonStyles.toolbarButton}>
<FABButton accent title="Create feature toggle">
<Icon name="add" />
</FABButton>

View File

@ -34,9 +34,18 @@ function storeApplicationMetaData(appName, key, value) {
}).then(throwIfNotSuccess);
}
function deleteApplication(appName) {
return fetch(`${URI}/${appName}`, {
method: 'DELETE',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
export default {
fetchApplication,
fetchAll,
fetchApplicationsWithStrategyName,
storeApplicationMetaData,
deleteApplication,
};

View File

@ -2,10 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import ApplicationEditComponent from '../../component/application/application-edit-container';
const render = ({ match: { params } }) => <ApplicationEditComponent appName={params.name} />;
const render = ({ match: { params }, history }) => <ApplicationEditComponent appName={params.name} history={history} />;
render.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -7,6 +7,8 @@ export const ERROR_UPDATING_APPLICATION_DATA = 'ERROR_UPDATING_APPLICATION_DATA'
export const RECEIVE_APPLICATION = 'RECEIVE_APPLICATION';
export const UPDATE_APPLICATION_FIELD = 'UPDATE_APPLICATION_FIELD';
export const DELETE_APPLICATION = 'DELETE_APPLICATION';
export const ERROR_DELETE_APPLICATION = 'ERROR_DELETE_APPLICATION';
const recieveAllApplications = json => ({
type: RECEIVE_ALL_APPLICATIONS,
@ -41,3 +43,11 @@ export function fetchApplication(appName) {
.then(json => dispatch(recieveApplication(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
}
export function deleteApplication(appName) {
return dispatch =>
api
.deleteApplication(appName)
.then(() => dispatch({ type: DELETE_APPLICATION, appName }))
.catch(dispatchAndThrow(dispatch, ERROR_DELETE_APPLICATION));
}

View File

@ -1,5 +1,5 @@
import { fromJS, List, Map } from 'immutable';
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION, UPDATE_APPLICATION_FIELD } from './actions';
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION, UPDATE_APPLICATION_FIELD, DELETE_APPLICATION } from './actions';
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
function getInitState() {
@ -14,6 +14,11 @@ const store = (state = getInitState(), action) => {
return state.set('list', new List(action.value.applications));
case UPDATE_APPLICATION_FIELD:
return state.setIn(['apps', action.appName, action.key], action.value);
case DELETE_APPLICATION: {
const index = state.get('list').findIndex(item => item.appName === action.appName);
const result = state.removeIn(['list', index]);
return result.removeIn(['apps', action.appName]);
}
case USER_LOGOUT:
case USER_LOGIN:
return getInitState();