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:
commit
c2e16e7f73
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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'} />
|
||||
{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>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
46
frontend/src/component/application/application-update.jsx
Normal file
46
frontend/src/component/application/application-update.jsx
Normal 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;
|
104
frontend/src/component/application/application-view.jsx
Normal file
104
frontend/src/component/application/application-view.jsx
Normal 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;
|
35
frontend/src/component/application/stateful-textfield.js
Normal file
35
frontend/src/component/application/stateful-textfield.js
Normal 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;
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
50
frontend/src/component/common/search-field.jsx
Normal file
50
frontend/src/component/common/search-field.jsx
Normal 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;
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user