Seems that hierarchical error handling works..

TreeTable component seems to work too.
Edit is half-way through. Create / delete are TBD.
This commit is contained in:
Tomas Bures 2017-06-05 23:59:08 +02:00
parent 79ea9e1897
commit 5e4c86f626
24 changed files with 9967 additions and 261 deletions

18
client/src/lib/axios.js Normal file
View file

@ -0,0 +1,18 @@
'use strict';
import csfrToken from 'csfrToken';
import axios from 'axios';
import interoperableErrors from '../../../shared/interoperable-errors';
const axiosInst = axios.create({
headers: {'X-CSRF-TOKEN': csfrToken}
});
const axiosWrapper = {
get: (...args) => axiosInst.get(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
put: (...args) => axiosInst.put(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
post: (...args) => axiosInst.post(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
delete: (...args) => axiosInst.delete(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error })
};
export default axiosWrapper;

View file

@ -0,0 +1,67 @@
'use strict';
import PropTypes from 'prop-types';
function withErrorHandling(target) {
const inst = target.prototype;
const contextTypes = target.contextTypes || {};
contextTypes.parentErrorHandler = PropTypes.object;
target.contextTypes = contextTypes;
const childContextTypes = target.childContextTypes || {};
childContextTypes.parentErrorHandler = PropTypes.object;
target.childContextTypes = childContextTypes;
const existingGetChildContext = inst.getChildContext;
if (existingGetChildContext) {
inst.getChildContext = function() {
const childContext = (this::existingGetChildContext)();
childContext.parentErrorHandler = this;
return childContext;
}
} else {
inst.getChildContext = function() {
return {
parentErrorHandler: this
};
}
}
return target;
}
function handleError(that, error) {
let errorHandled;
if (that.errorHandler) {
errorHandled = that.errorHandler(error);
}
if (!errorHandled && that.context.parentErrorHandler) {
errorHandled = handleError(that.context.parentErrorHandler, error);
}
if (!errorHandled) {
throw error;
}
}
function withAsyncErrorHandler(target, name, descriptor) {
let fn = descriptor.value;
descriptor.value = async function () {
try {
await fn.apply(this, arguments)
} catch (error) {
handleError(this, error);
}
};
return descriptor;
}
export {
withErrorHandling,
withAsyncErrorHandler
}

View file

@ -1,10 +1,14 @@
'use strict';
import React, { Component } from 'react';
import axios from 'axios';
import axios from './axios';
import Immutable from 'immutable';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import interoperableErrors from '../../../shared/interoperable-errors';
import { withSectionHelpers } from './page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import { TreeTable } from './tree';
const FormState = {
Loading: 0,
@ -12,7 +16,10 @@ const FormState = {
Ready: 2
};
@translate()
@withSectionHelpers
@withErrorHandling
class Form extends Component {
static propTypes = {
stateOwner: PropTypes.object.isRequired,
@ -20,28 +27,44 @@ class Form extends Component {
}
static childContextTypes = {
stateOwner: PropTypes.object
formStateOwner: PropTypes.object
}
getChildContext() {
return {
stateOwner: this.props.stateOwner
formStateOwner: this.props.stateOwner
};
}
@withAsyncErrorHandler
async onSubmit(evt) {
evt.preventDefault();
const t = this.props.t;
if (this.props.onSubmitAsync) {
this.props.stateOwner.disableForm();
this.props.stateOwner.setFormStatusMessage(t('Submitting...'));
try {
evt.preventDefault();
await this.props.onSubmitAsync(evt);
if (this.props.onSubmitAsync) {
this.props.stateOwner.disableForm();
this.props.stateOwner.setFormStatusMessage('info', t('Submitting...'));
this.props.stateOwner.setFormStatusMessage();
this.props.stateOwner.enableForm();
await this.props.onSubmitAsync(evt);
this.props.stateOwner.setFormStatusMessage();
this.props.stateOwner.enableForm();
}
} catch (error) {
if (error instanceof interoperableErrors.ChangedError) {
this.props.stateOwner.disableForm();
this.props.stateOwner.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
</span>
);
return;
}
throw error;
}
}
@ -49,11 +72,12 @@ class Form extends Component {
const t = this.props.t;
const owner = this.props.stateOwner;
const props = this.props;
const statusMessage = owner.getFormStatusMessage();
const statusMessageText = owner.getFormStatusMessageText();
const statusMessageSeverity = owner.getFormStatusMessageSeverity();
if (!owner.isFormReady()) {
if (owner.isFormWithLoadingNotice()) {
return <div>{t('Loading ...')}</div>
return <p className={`alert alert-info mt-form-status`} role="alert">{t('Loading ...')}</p>
} else {
return <div></div>;
}
@ -63,7 +87,7 @@ class Form extends Component {
<fieldset disabled={owner.isFormDisabled()}>
{props.children}
</fieldset>
{statusMessage && <p className="col-sm-10 col-sm-offset-2 alert alert-info mt-form-status" role="alert">{owner.getFormStatusMessage()}</p>}
{statusMessageText && <p className={`col-sm-10 col-sm-offset-2 alert alert-${statusMessageSeverity} mt-form-status`} role="alert">{statusMessageText}</p>}
</form>
);
}
@ -92,17 +116,17 @@ class InputField extends Component {
}
static contextTypes = {
stateOwner: PropTypes.object.isRequired
formStateOwner: PropTypes.object.isRequired
}
render() {
const props = this.props;
const owner = this.context.stateOwner;
const owner = this.context.formStateOwner;
const id = this.props.id;
const htmlId = 'form_' + id;
return wrapInput(id, htmlId, owner, props.label,
<input type="text" value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindToFormValue(id)}/>
<input type="text" value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindChangeEventToFormValue(id)}/>
);
}
}
@ -115,17 +139,17 @@ class TextArea extends Component {
}
static contextTypes = {
stateOwner: PropTypes.object.isRequired
formStateOwner: PropTypes.object.isRequired
}
render() {
const props = this.props;
const owner = this.context.stateOwner;
const owner = this.context.formStateOwner;
const id = this.props.id;
const htmlId = 'form_' + id;
return wrapInput(id, htmlId, owner, props.label,
<textarea id={htmlId} value={owner.getFormValue(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindToFormValue(id)}></textarea>
<textarea id={htmlId} value={owner.getFormValue(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindChangeEventToFormValue(id)}></textarea>
);
}
}
@ -142,6 +166,7 @@ class ButtonRow extends Component {
}
}
@withErrorHandling
class Button extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
@ -153,9 +178,10 @@ class Button extends Component {
}
static contextTypes = {
stateOwner: PropTypes.object.isRequired
formStateOwner: PropTypes.object.isRequired
}
@withAsyncErrorHandler
async onClick(evt) {
if (this.props.onClick) {
evt.preventDefault();
@ -165,9 +191,9 @@ class Button extends Component {
} else if (this.props.onClickAsync) {
evt.preventDefault();
this.context.stateOwner.disableForm();
this.context.formStateOwner.disableForm();
await this.props.onClickAsync(evt);
this.context.stateOwner.enableForm();
this.context.formStateOwner.enableForm();
}
}
@ -197,23 +223,65 @@ class Button extends Component {
}
}
class TreeTableSelect extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
dataUrl: PropTypes.string.isRequired
}
static contextTypes = {
formStateOwner: PropTypes.object.isRequired
}
async onSelectionChangedAsync(sel) {
const owner = this.context.formStateOwner;
owner.updateFormValue(this.props.id, sel);
}
render() {
const props = this.props;
const owner = this.context.formStateOwner;
const id = this.props.id;
const htmlId = 'form_' + id;
return (
<div className={owner.addFormValidationClass('form-group', id)} >
<div className="col-sm-2">
<label htmlFor={htmlId} className="control-label">{props.label}</label>
</div>
<div className="col-sm-10">
<TreeTable dataUrl={this.props.dataUrl} selectMode={TreeTable.SelectMode.SINGLE} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
</div>
<div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help'}>{owner.getFormValidationMessage(id)}</div>
</div>
);
}
}
function withForm(target) {
const inst = target.prototype;
const cleanFormState = Immutable.Map({
state: FormState.Loading,
isValidationShown: false,
isDisabled: false,
statusMessageText: '',
data: Immutable.Map()
});
inst.initFormState = function() {
const state = this.state || {};
state.formState = Immutable.Map({
state: FormState.Loading,
isValidationShown: false,
isDisabled: false,
statusMessage: '',
data: Immutable.Map()
});
state.formState = cleanFormState;
this.state = state;
};
inst.resetFormState = function() {
this.setState({
formState: cleanFormState
});
};
inst.populateFormValuesFromURL = function(url) {
setTimeout(() => {
this.setState(previousState => {
@ -226,7 +294,12 @@ function withForm(target) {
}, 500);
axios.get(url).then(response => {
this.populateFormValues(response.data);
const data = response.data;
data.originalHash = data.hash;
delete data.hash;
this.populateFormValues(data);
});
};
@ -257,7 +330,7 @@ function withForm(target) {
}));
};
inst.bindToFormValue = function(name) {
inst.bindChangeEventToFormValue = function(name) {
return evt => this.updateFormValue(name, evt.target.value);
};
@ -322,12 +395,21 @@ function withForm(target) {
return !this.state.formState.get('data').find(attr => attr.get('error'));
};
inst.getFormStatusMessage = function() {
return this.state.formState.get('statusMessage');
inst.getFormStatusMessageText = function() {
return this.state.formState.get('statusMessageText');
};
inst.setFormStatusMessage = function(message) {
this.setState(previousState => ({formState: previousState.formState.set('statusMessage', message)}));
inst.getFormStatusMessageSeverity = function() {
return this.state.formState.get('statusMessageSeverity');
};
inst.setFormStatusMessage = function(severity, text) {
this.setState(previousState => ({
formState: previousState.formState.withMutations(map => {
map.set('statusMessageText', text);
map.set('statusMessageSeverity', severity);
})
}));
};
inst.enableForm = function() {
@ -342,6 +424,7 @@ function withForm(target) {
return this.state.formState.get('isDisabled');
};
return target;
}
@ -351,5 +434,6 @@ export {
InputField,
TextArea,
ButtonRow,
Button
Button,
TreeTableSelect
}

View file

@ -14,7 +14,7 @@ i18n
ns: ['common'],
defaultNS: 'common',
debug: true,
debug: false,
// cache: {
// enabled: true

View file

@ -1,4 +1,4 @@
.mt-button-row > button{
.mt-button-row > button {
margin-right: 15px;
}
@ -9,4 +9,12 @@
.mt-form-status {
padding-top: 5px;
padding-bottom: 5px;
}
}
.mt-action-links > a {
margin-right: 8px;
}
.mt-action-links > a:last-child {
margin-right: 0px;
}

View file

@ -6,9 +6,14 @@ import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'
import './page.css';
import { withErrorHandling } from './error-handling';
class PageContent extends Component {
static propTypes = {
structure: PropTypes.object.isRequired
}
getRoutes(urlPrefix, children) {
let routes = [];
for (let routeKey in children) {
@ -49,6 +54,10 @@ class PageContent extends Component {
@withRouter
class Breadcrumb extends Component {
static propTypes = {
structure: PropTypes.object.isRequired
}
renderElement(breadcrumbElem) {
if (breadcrumbElem.isActive) {
return <li key={breadcrumbElem.idx} className="active">{breadcrumbElem.title}</li>;
@ -91,6 +100,113 @@ class Breadcrumb extends Component {
}
}
@translate()
class DismissibleAlert extends Component {
static propTypes = {
severity: PropTypes.string.isRequired,
onClose: PropTypes.func
}
close() {
if (this.props.onClose) {
this.props.onClose();
}
}
render() {
const t = this.props.t;
return (
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
<button type="button" className="close" aria-label={t('Close')} onClick={::this.close}><span aria-hidden="true">&times;</span></button>
{this.props.children}
</div>
)
}
}
@withRouter
@withErrorHandling
class SectionContent extends Component {
constructor(props) {
super(props);
this.state = {
flashMessageText: ''
}
this.historyUnlisten = props.history.listen((location, action) => {
this.closeFlashMessage();
})
}
static propTypes = {
structure: PropTypes.object.isRequired,
root: PropTypes.string.isRequired
}
static childContextTypes = {
sectionContent: PropTypes.object
}
getChildContext() {
return {
sectionContent: this
};
}
getFlashMessageText() {
return this.state.flashMessageText;
}
getFlashMessageSeverity() {
return this.state.flashMessageSeverity;
}
setFlashMessage(severity, text) {
this.setState({
flashMessageText: text,
flashMessageSeverity: severity
});
}
navigateTo(path) {
this.props.history.push(path);
}
navigateToWithFlashMessage(path, severity, text) {
this.props.history.push(path);
this.setFlashMessage(severity, text);
}
errorHandler(error) {
if (error.response && error.response.data && error.response.data.message) {
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
} else {
this.navigateToWithFlashMessage(this.props.root, 'danger', error.message);
}
return true;
}
closeFlashMessage() {
this.setState({
flashMessageText: ''
})
}
render() {
return (
<div>
<Breadcrumb structure={this.props.structure} />
{(this.state.flashMessageText && <DismissibleAlert severity={this.state.flashMessageSeverity} onClose={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>)}
<PageContent structure={this.props.structure}/>
</div>
);
}
}
@translate()
class Section extends Component {
constructor(props) {
@ -104,18 +220,21 @@ class Section extends Component {
this.structure = structure;
}
static propTypes = {
structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
root: PropTypes.string.isRequired
}
render() {
return (
<Router>
<div>
<Breadcrumb structure={this.structure} />
<PageContent structure={this.structure}/>
</div>
<SectionContent root={this.props.root} structure={this.structure} />
</Router>
);
}
}
class Title extends Component {
render() {
return (
@ -194,11 +313,43 @@ class Button extends Component {
}
}
function withSectionHelpers(target) {
const inst = target.prototype;
const contextTypes = target.contextTypes || {};
contextTypes.sectionContent = PropTypes.object.isRequired;
target.contextTypes = contextTypes;
inst.getFlashMessageText = function() {
return this.context.sectionContent.getFlashMessageText();
};
inst.getFlashMessageSeverity = function() {
return this.context.sectionContent.getFlashMessageSeverity();
};
inst.setFlashMessage = function(severity, text) {
return this.context.sectionContent.setFlashMessage(severity, text);
};
inst.navigateTo = function(path) {
return this.context.sectionContent.navigateTo(path);
}
inst.navigateToWithFlashMessage = function(path, severity, text) {
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
}
return target;
}
export {
Section,
Title,
Toolbar,
Button,
NavButton
NavButton,
withSectionHelpers
};

16
client/src/lib/tree.css Normal file
View file

@ -0,0 +1,16 @@
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td {
background-color: transparent;
}
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title {
color: #333333;
}
.mt-treetable-container {
padding-top: 9px;
}
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
border-top: 0px none;
}

228
client/src/lib/tree.js Normal file
View file

@ -0,0 +1,228 @@
'use strict';
import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import jQuery from 'jquery';
import '../../public/jquery/jquery-ui-1.12.1.min.js';
import '../../public/fancytree/jquery.fancytree-all.js';
import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
import './tree.css';
import axios from 'axios';
import { withSectionHelpers } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
@translate()
@withSectionHelpers
@withErrorHandling
class TreeTable extends Component {
constructor(props) {
super(props);
this.selectMode = this.props.selectMode || TreeTable.SelectMode.NONE;
let selection = props.selection;
if (this.selectMode == TreeTable.SelectMode.MULTI) {
selection = selection.slice().sort();
}
this.state = {
treeData: [],
selection: selection
};
this.loadData();
}
@withAsyncErrorHandler
async loadData() {
axios.get(this.props.dataUrl)
.then(response => {
this.setState({
treeData: [ response.data ]
});
});
}
static propTypes = {
dataUrl: PropTypes.string.isRequired,
selectMode: PropTypes.number,
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
onSelectionChangedAsync: PropTypes.func,
actionLinks: PropTypes.array,
withHeader: PropTypes.bool
}
static SelectMode = {
NONE: 0,
SINGLE: 1,
MULTI: 2
}
componentDidMount() {
const glyphOpts = {
map: {
expanderClosed: 'glyphicon glyphicon-menu-right',
expanderLazy: 'glyphicon glyphicon-menu-right', // glyphicon-plus-sign
expanderOpen: 'glyphicon glyphicon-menu-down', // glyphicon-collapse-down
checkbox: 'glyphicon glyphicon-unchecked',
checkboxSelected: 'glyphicon glyphicon-check',
checkboxUnknown: 'glyphicon glyphicon-share',
}
};
let createNodeFn;
if (this.props.actionLinks) {
const actionLinks = this.props.actionLinks;
createNodeFn = (event, data) => {
const node = data.node;
const tdList = jQuery(node.tr).find(">td");
const linksContainer = jQuery('<span class="mt-action-links"/>');
const links = actionLinks.map(({label, link}) => {
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href="#">{label}</a>);
const lnk = jQuery(lnkHtml);
lnk.click(() => this.navigateTo(link(node.key)));
linksContainer.append(lnk);
});
tdList.eq(1).html(linksContainer);
};
} else {
createNodeFn = (event, data) => {};
}
this.tree = jQuery(this.domTable).fancytree({
extensions: ['glyph', 'table'],
glyph: glyphOpts,
selectMode: (this.selectMode == TreeTable.SelectMode.MULTI ? 2 : 1),
icon: false,
autoScroll: true,
scrollParent: jQuery(this.domTableContainer),
source: this.state.treeData,
table: {
nodeColumnIdx: 0
},
createNode: createNodeFn,
checkbox: this.selectMode == TreeTable.SelectMode.MULTI,
activate: (this.selectMode == TreeTable.SelectMode.SINGLE ? ::this.onActivate : null),
select: (this.selectMode == TreeTable.SelectMode.MULTI ? ::this.onSelect : null)
}).fancytree("getTree");
this.updateSelection();
}
componentDidUpdate() {
this.tree.reload(this.state.treeData);
this.updateSelection();
}
updateSelection() {
const tree = this.tree;
if (this.selectMode == TreeTable.SelectMode.MULTI) {
const selectSet = new Set(this.state.selection);
tree.enableUpdate(false);
tree.visit(node => node.setSelected(selectSet.has(node.key)));
tree.enableUpdate(true);
} else if (this.selectMode == TreeTable.SelectMode.SINGLE) {
this.tree.activateKey(this.state.selection);
}
}
@withAsyncErrorHandler
async onSelectionChanged(sel) {
if (this.props.onSelectionChangedAsync) {
await this.props.onSelectionChangedAsync(sel);
}
}
// Single-select
onActivate(event, data) {
const selection = this.tree.getActiveNode().key;
if (selection !== this.state.selection) {
this.setState({
selection
});
this.onSelectionChanged(selection);
}
}
// Multi-select
onSelect(event, data) {
const newSel = this.tree.getSelectedNodes().map(node => node.key).sort();
const oldSel = this.state.selection;
let updated = false;
const length = oldSel.length
if (length === newSel.length) {
for (let i = 0; i < length; i++) {
if (oldSel[i] !== newSel[i]) {
updated = true;
break;
}
}
} else {
updated = true;
}
if (updated) {
this.setState({
selection: newSel
});
this.onSelectionChanged(selection);
}
}
render() {
const t = this.props.t;
const props = this.props;
const actionLinks = props.actionLinks;
const withHeader = props.withHeader;
let containerClass = 'mt-treetable-container';
if (this.selectMode == TreeTable.SelectMode.NONE) {
containerClass += ' mt-treetable-inactivable';
}
if (!this.withHeader) {
containerClass += ' mt-treetable-noheader';
}
const container =
<div className={containerClass} ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
{props.withHeader &&
<thead>
<tr>
<th>{t('Name')}</th>
{actionLinks && <th></th>}
</tr>
</thead>
}
<tbody>
<tr>
<td></td>
{actionLinks && <td></td>}
</tr>
</tbody>
</table>
</div>;
return (
container
);
}
}
export {
TreeTable
}

View file

@ -2,24 +2,25 @@
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import csfrToken from 'csfrToken';
import { withForm, Form, InputField, TextArea, ButtonRow, Button} from '../lib/form';
import { withSectionHelpers } from '../lib/page'
import { withForm, Form, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
import { Title } from "../lib/page";
import axios from 'axios';
import axios from '../lib/axios';
@translate()
@withForm
@withSectionHelpers
export default class Edit extends Component {
constructor(props) {
super(props);
this.nsId = parseInt(this.props.match.params.nsId);
console.log('Constructing Edit');
this.initFormState();
this.populateFormValuesFromURL(`/namespaces/rest/namespaces/${this.nsId}`);
}
validateFormValues(state) {
const t = this.props.t;
@ -35,17 +36,15 @@ export default class Edit extends Component {
const data = this.getFormValues();
console.log(data);
const response = await axios.put(`/namespaces/rest/namespaces/${this.nsId}`);
console.log(response);
await axios.put(`/namespaces/rest/namespaces/${this.nsId}`, data);
} else {
this.showFormValidation();
}
}
async deleteHandler() {
this.setFormStatusMessage('Deleting namespace')
this.setFormStatusMessage()
this.setFormStatusMessage('Deleting namespace');
this.setFormStatusMessage();
}
render() {
@ -56,13 +55,15 @@ export default class Edit extends Component {
<Title>{t('Edit Namespace')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')} description={t('Namespace Name')}/>
<TextArea id="description" label={t('Description')} description={t('Description')}/>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Update')}/>
<Button className="btn-danger" icon="remove" label={t('Delete Namespace')} onClickAsync={::this.deleteHandler}/>
</ButtonRow>
{this.nsId !== 1 && <TreeTableSelect id="parent" label={t('Parent Namespace')} dataUrl="/namespaces/rest/namespacesTree"/>}
</Form>
</div>
);

View file

@ -2,14 +2,21 @@
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import NamespacesTreeTable from './NamespacesTreeTable';
import { Title, Toolbar, NavButton } from "../lib/page";
import { Title, Toolbar, NavButton } from '../lib/page';
import { TreeTable } from '../lib/tree';
@translate()
export default class List extends Component {
render() {
const t = this.props.t;
const actionLinks = [
{
label: 'Edit',
link: key => '/namespaces/edit/' + key
}
];
return (
<div>
<Toolbar>
@ -18,7 +25,7 @@ export default class List extends Component {
<Title>{t('Namespaces')}</Title>
<NamespacesTreeTable />
<TreeTable withHeader dataUrl="/namespaces/rest/namespacesTree" actionLinks={actionLinks} />
</div>
);
}

View file

@ -1,92 +0,0 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import jQuery from 'jquery';
import '../../public/jquery/jquery-ui-1.12.1.min.js';
import '../../public/fancytree/jquery.fancytree-all.min.js';
import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
import axios from 'axios';
@translate()
export default class NamespacesTreeTable extends Component {
constructor(props) {
super(props);
this.state = {
treeData: []
};
axios.get('/namespaces/rest/namespacesTree')
.then(response => {
this.setState({
treeData: response.data
});
});
}
componentDidMount() {
const history = this.props.history;
const glyphOpts = {
map: {
expanderClosed: 'glyphicon glyphicon-menu-right',
expanderLazy: 'glyphicon glyphicon-menu-right', // glyphicon-plus-sign
expanderOpen: 'glyphicon glyphicon-menu-down', // glyphicon-collapse-down
}
};
this.tree = jQuery(this.domTable).fancytree({
extensions: ['glyph', 'table'],
glyph: glyphOpts,
selectMode: 1,
icon: false,
autoScroll: true,
scrollParent: jQuery(this.domTableContainer),
source: this.state.treeData,
table: {
nodeColumnIdx: 0
},
createNode: (event, data) => {
const node = data.node;
const tdList = jQuery(node.tr).find(">td");
tdList.eq(1).html('<a href="#">Edit</a>').click(() => {
history.push('/namespaces/edit/' + node.key);
});
}
}).fancytree("getTree");
}
componentDidUpdate() {
this.tree.reload(this.state.treeData);
}
render() {
const t = this.props.t;
const container =
<div ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>{t('Name')}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>;
return (
container
);
}
}

View file

@ -38,7 +38,7 @@ const getStructure = t => ({
export default function() {
ReactDOM.render(
<I18nextProvider i18n={ i18n }><Section structure={getStructure}/></I18nextProvider>,
<I18nextProvider i18n={ i18n }><Section root='/namespaces' structure={getStructure}/></I18nextProvider>,
document.getElementById('root')
);
};