@@ -1,30 +1,29 @@ | |||
import { getJSON } from '../helpers/request.js'; | |||
import $ from 'jquery'; | |||
import { getJSON, postJSON, post } from '../helpers/request.js'; | |||
export function getComponents (data) { | |||
let url = baseUrl + '/api/components/search'; | |||
return $.get(url, data); | |||
return getJSON(url, data); | |||
} | |||
export function getProvisioned (data) { | |||
let url = baseUrl + '/api/projects/provisioned'; | |||
return $.get(url, data); | |||
return getJSON(url, data); | |||
} | |||
export function getGhosts (data) { | |||
let url = baseUrl + '/api/projects/ghosts'; | |||
return $.get(url, data); | |||
return getJSON(url, data); | |||
} | |||
export function deleteComponents (data) { | |||
let url = baseUrl + '/api/projects/bulk_delete'; | |||
return $.post(url, data); | |||
return post(url, data); | |||
} | |||
export function createProject (options) { | |||
options.url = baseUrl + '/api/projects/create'; | |||
options.type = 'POST'; | |||
return $.ajax(options); | |||
export function createProject (data) { | |||
let url = baseUrl + '/api/projects/create'; | |||
return postJSON(url, data); | |||
} | |||
export function getChildren (componentKey, metrics = []) { |
@@ -4,7 +4,7 @@ import PermissionsList from './permissions-list'; | |||
export default React.createClass({ | |||
getInitialState() { | |||
return { permissions: [] }; | |||
return { ready: false, permissions: [] }; | |||
}, | |||
componentDidMount() { | |||
@@ -14,18 +14,26 @@ export default React.createClass({ | |||
requestPermissions() { | |||
const url = `${window.baseUrl}/api/permissions/search_global_permissions`; | |||
$.get(url).done(r => { | |||
this.setState({ permissions: r.permissions }); | |||
this.setState({ ready: true, permissions: r.permissions }); | |||
}); | |||
}, | |||
renderSpinner () { | |||
if (this.state.ready) { | |||
return null; | |||
} | |||
return <i className="spinner"/>; | |||
}, | |||
render() { | |||
return ( | |||
<div className="page"> | |||
<header id="global-permissions-header" className="page-header"> | |||
<h1 className="page-title">{window.t('global_permissions.page')}</h1> | |||
{this.renderSpinner()} | |||
<p className="page-description">{window.t('global_permissions.page.description')}</p> | |||
</header> | |||
<PermissionsList permissions={this.state.permissions}/> | |||
<PermissionsList ready={this.state.ready} permissions={this.state.permissions}/> | |||
</div> | |||
); | |||
} |
@@ -1,6 +1,9 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import Permission from './permission'; | |||
export default React.createClass({ | |||
propTypes:{ | |||
permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired | |||
@@ -13,6 +16,7 @@ export default React.createClass({ | |||
}, | |||
render() { | |||
return <ul id="global-permissions-list">{this.renderPermissions()}</ul>; | |||
let className = classNames({ 'new-loading': !this.props.ready }); | |||
return <ul id="global-permissions-list" className={className}>{this.renderPermissions()}</ul>; | |||
} | |||
}); |
@@ -5,10 +5,23 @@ import Template from './templates/groups-header.hbs'; | |||
export default Marionette.ItemView.extend({ | |||
template: Template, | |||
collectionEvents: { | |||
'request': 'showSpinner', | |||
'sync': 'hideSpinner' | |||
}, | |||
events: { | |||
'click #groups-create': 'onCreateClick' | |||
}, | |||
showSpinner: function () { | |||
this.$('.spinner').removeClass('hidden'); | |||
}, | |||
hideSpinner: function () { | |||
this.$('.spinner').addClass('hidden'); | |||
}, | |||
onCreateClick: function (e) { | |||
e.preventDefault(); | |||
this.createGroup(); |
@@ -3,7 +3,20 @@ import ListItemView from './list-item-view'; | |||
export default Marionette.CollectionView.extend({ | |||
tagName: 'ul', | |||
childView: ListItemView | |||
childView: ListItemView, | |||
collectionEvents: { | |||
'request': 'showLoading', | |||
'sync': 'hideLoading' | |||
}, | |||
showLoading: function () { | |||
this.$el.addClass('new-loading'); | |||
}, | |||
hideLoading: function () { | |||
this.$el.removeClass('new-loading'); | |||
} | |||
}); | |||
@@ -1,5 +1,6 @@ | |||
<header class="page-header"> | |||
<h1 class="page-title">{{t 'user_groups.page'}}</h1> | |||
<i class="spinner hidden"></i> | |||
<div class="page-actions"> | |||
<div class="button-group"> | |||
<button id="groups-create">Create Group</button> |
@@ -9,10 +9,18 @@ export default React.createClass({ | |||
}).render(); | |||
}, | |||
renderSpinner () { | |||
if (this.props.ready) { | |||
return null; | |||
} | |||
return <i className="spinner"/>; | |||
}, | |||
render() { | |||
return ( | |||
<header id="project-permissions-header" className="page-header"> | |||
<h1 className="page-title">{window.t('permission_templates.page')}</h1> | |||
{this.renderSpinner()} | |||
<div className="page-actions"> | |||
<button onClick={this.onCreate}>Create</button> | |||
</div> |
@@ -12,7 +12,7 @@ export default React.createClass({ | |||
}, | |||
getInitialState() { | |||
return { permissions: [], permissionTemplates: [] }; | |||
return { ready: false, permissions: [], permissionTemplates: [] }; | |||
}, | |||
componentDidMount() { | |||
@@ -53,6 +53,7 @@ export default React.createClass({ | |||
let permissionTemplates = this.mergePermissionsToTemplates(r.permissionTemplates, permissions); | |||
let permissionTemplatesWithDefaults = this.mergeDefaultsToTemplates(permissionTemplates, r.defaultTemplates); | |||
this.setState({ | |||
ready: true, | |||
permissionTemplates: permissionTemplatesWithDefaults, | |||
permissions: permissions | |||
}); | |||
@@ -62,10 +63,10 @@ export default React.createClass({ | |||
render() { | |||
return ( | |||
<div className="page"> | |||
<Header | |||
refresh={this.requestPermissions}/> | |||
<Header ready={this.state.ready} refresh={this.requestPermissions}/> | |||
<PermissionTemplates | |||
ready={this.state.ready} | |||
permissionTemplates={this.state.permissionTemplates} | |||
permissions={this.state.permissions} | |||
topQualifiers={this.props.topQualifiers} |
@@ -1,7 +1,10 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import PermissionsHeader from './permissions-header'; | |||
import PermissionTemplate from './permission-template'; | |||
export default React.createClass({ | |||
propTypes:{ | |||
permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, | |||
@@ -18,8 +21,9 @@ export default React.createClass({ | |||
topQualifiers={this.props.topQualifiers} | |||
refresh={this.props.refresh}/>; | |||
}); | |||
let className = classNames('data zebra', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<table id="permission-templates" className="data zebra"> | |||
<table id="permission-templates" className={className}> | |||
<PermissionsHeader permissions={this.props.permissions}/> | |||
<tbody>{permissionTemplates}</tbody> | |||
</table> |
@@ -14,7 +14,7 @@ export default React.createClass({ | |||
}, | |||
getInitialState() { | |||
return { permissions: [], projects: [], total: 0 }; | |||
return { ready: false, permissions: [], projects: [], total: 0 }; | |||
}, | |||
componentDidMount() { | |||
@@ -42,18 +42,21 @@ export default React.createClass({ | |||
if (this.props.componentId) { | |||
data = { projectId: this.props.componentId }; | |||
} | |||
$.get(url, data).done(r => { | |||
let permissions = this.sortPermissions(r.permissions); | |||
let projects = this.mergePermissionsToProjects(r.projects, permissions); | |||
if (page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
} | |||
this.setState({ | |||
projects: projects, | |||
permissions: permissions, | |||
total: r.paging.total, | |||
page: r.paging.pageIndex, | |||
query: query | |||
this.setState({ ready: false }, () => { | |||
$.get(url, data).done(r => { | |||
let permissions = this.sortPermissions(r.permissions); | |||
let projects = this.mergePermissionsToProjects(r.projects, permissions); | |||
if (page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
} | |||
this.setState({ | |||
ready: true, | |||
projects: projects, | |||
permissions: permissions, | |||
total: r.paging.total, | |||
page: r.paging.pageIndex, | |||
query: query | |||
}); | |||
}); | |||
}); | |||
}, | |||
@@ -88,11 +91,19 @@ export default React.createClass({ | |||
); | |||
}, | |||
renderSpinner () { | |||
if (this.state.ready) { | |||
return null; | |||
} | |||
return <i className="spinner"/>; | |||
}, | |||
render() { | |||
return ( | |||
<div className="page"> | |||
<header id="project-permissions-header" className="page-header"> | |||
<h1 className="page-title">{window.t('roles.page')}</h1> | |||
{this.renderSpinner()} | |||
<div className="page-actions"> | |||
{this.renderBulkApplyButton()} | |||
</div> | |||
@@ -103,12 +114,14 @@ export default React.createClass({ | |||
search={this.search}/> | |||
<Permissions | |||
ready={this.state.ready} | |||
projects={this.state.projects} | |||
permissions={this.state.permissions} | |||
permissionTemplates={this.props.permissionTemplates} | |||
refresh={this.refresh}/> | |||
<PermissionsFooter {...this.props} | |||
ready={this.state.ready} | |||
count={this.state.projects.length} | |||
total={this.state.total} | |||
loadMore={this.loadMore}/> |
@@ -1,5 +1,7 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
export default React.createClass({ | |||
propTypes:{ | |||
count: React.PropTypes.number.isRequired, | |||
@@ -13,8 +15,9 @@ export default React.createClass({ | |||
} | |||
let hasMore = this.props.total > this.props.count; | |||
let loadMoreLink = <a onClick={this.props.loadMore} className="spacer-left" href="#">show more</a>; | |||
let className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<footer className="spacer-top note text-center"> | |||
<footer className={className}> | |||
{this.props.count}/{this.props.total} shown | |||
{hasMore ? loadMoreLink : null} | |||
</footer> |
@@ -1,7 +1,10 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import PermissionsHeader from './permissions-header'; | |||
import Project from './project'; | |||
export default React.createClass({ | |||
propTypes:{ | |||
projects: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, | |||
@@ -18,8 +21,9 @@ export default React.createClass({ | |||
permissionTemplates={this.props.permissionTemplates} | |||
refresh={this.props.refresh}/>; | |||
}); | |||
let className = classNames('data zebra', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<table id="projects" className="data zebra"> | |||
<table id="projects" className={className}> | |||
<PermissionsHeader permissions={this.props.permissions}/> | |||
<tbody>{projects}</tbody> | |||
</table> |
@@ -1,7 +1,8 @@ | |||
import ModalForm from '../../components/common/modal-form'; | |||
import {createProject} from '../../api/components'; | |||
import { createProject } from '../../api/components'; | |||
import Template from './templates/projects-create-form.hbs'; | |||
export default ModalForm.extend({ | |||
template: Template, | |||
@@ -27,20 +28,18 @@ export default ModalForm.extend({ | |||
key: this.$('#create-project-key').val() | |||
}; | |||
this.disableForm(); | |||
return createProject({ | |||
data, | |||
statusCode: { | |||
// do not show global error | |||
400: null | |||
} | |||
}).done(() => { | |||
if (this.options.refresh) { | |||
this.options.refresh(); | |||
} | |||
this.destroy(); | |||
}).fail((jqXHR) => { | |||
this.enableForm(); | |||
this.showErrors([{ msg: jqXHR.responseJSON.err_msg }]); | |||
}); | |||
return createProject(data) | |||
.then(() => { | |||
if (this.options.refresh) { | |||
this.options.refresh(); | |||
} | |||
this.destroy(); | |||
}) | |||
.catch(error => { | |||
this.enableForm(); | |||
if (error.response.status === 400) { | |||
error.response.json().then(obj => this.showErrors([{ msg: obj.err_msg }])); | |||
} | |||
}); | |||
} | |||
}); |
@@ -15,6 +15,7 @@ export default React.createClass({ | |||
getInitialState() { | |||
return { | |||
ready: false, | |||
projects: [], | |||
total: 0, | |||
page: 1, | |||
@@ -62,48 +63,49 @@ export default React.createClass({ | |||
requestGhosts() { | |||
let data = this.getFilters(); | |||
getGhosts(data).done(r => { | |||
getGhosts(data).then(r => { | |||
let projects = r.projects.map(project => { | |||
return _.extend(project, { id: project.uuid, qualifier: 'TRK' }); | |||
}); | |||
if (this.state.page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
} | |||
this.setState({ projects: projects, total: r.total }); | |||
this.setState({ ready: true, projects: projects, total: r.total }); | |||
}); | |||
}, | |||
requestProvisioned() { | |||
let data = this.getFilters(); | |||
getProvisioned(data).done(r => { | |||
getProvisioned(data).then(r => { | |||
let projects = r.projects.map(project => { | |||
return _.extend(project, { id: project.uuid, qualifier: 'TRK' }); | |||
}); | |||
if (this.state.page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
} | |||
this.setState({ projects: projects, total: r.total }); | |||
this.setState({ ready: true, projects: projects, total: r.total }); | |||
}); | |||
}, | |||
requestAllProjects() { | |||
let data = this.getFilters(); | |||
data.qualifiers = this.state.qualifiers; | |||
getComponents(data).done(r => { | |||
getComponents(data).then(r => { | |||
let projects = r.components; | |||
if (this.state.page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
} | |||
this.setState({ projects: projects, total: r.paging.total }); | |||
this.setState({ ready: true, projects: projects, total: r.paging.total }); | |||
}); | |||
}, | |||
loadMore() { | |||
this.setState({ page: this.state.page + 1 }, this.requestProjects); | |||
this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects); | |||
}, | |||
onSearch(query) { | |||
this.setState({ | |||
ready: false, | |||
page: 1, | |||
query, | |||
selection: [] | |||
@@ -112,6 +114,7 @@ export default React.createClass({ | |||
onTypeChanged(newType) { | |||
this.setState({ | |||
ready: false, | |||
page: 1, | |||
query: '', | |||
type: newType, | |||
@@ -122,6 +125,7 @@ export default React.createClass({ | |||
onQualifierChanged(newQualifier) { | |||
this.setState({ | |||
ready: false, | |||
page: 1, | |||
query: '', | |||
type: TYPE.ALL, | |||
@@ -153,7 +157,7 @@ export default React.createClass({ | |||
deleteProjects() { | |||
let ids = this.state.selection.join(','); | |||
deleteComponents({ ids }).done(() => { | |||
deleteComponents({ ids }).then(() => { | |||
this.setState({ page: 1, selection: [] }, this.requestProjects); | |||
}); | |||
}, | |||
@@ -174,6 +178,7 @@ export default React.createClass({ | |||
deleteProjects={this.deleteProjects}/> | |||
<Projects | |||
ready={this.state.ready} | |||
projects={this.state.projects} | |||
refresh={this.requestProjects} | |||
selection={this.state.selection} | |||
@@ -181,6 +186,7 @@ export default React.createClass({ | |||
onProjectDeselected={this.onProjectDeselected}/> | |||
<ListFooter | |||
ready={this.state.ready} | |||
count={this.state.projects.length} | |||
total={this.state.total} | |||
loadMore={this.loadMore}/> |
@@ -1,3 +1,4 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import { getComponentUrl } from '../../helpers/urls'; | |||
import Checkbox from '../../components/shared/checkbox'; | |||
@@ -43,8 +44,9 @@ export default React.createClass({ | |||
}, | |||
render() { | |||
let className = classNames('data', 'zebra', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<table className="data zebra"> | |||
<table className={className}> | |||
<tbody>{this.props.projects.map(this.renderProject)}</tbody> | |||
</table> | |||
); |
@@ -47,6 +47,10 @@ export default React.createClass({ | |||
return <Checkbox onCheck={this.onCheck} initiallyChecked={isChecked} thirdState={thirdState}/>; | |||
}, | |||
renderSpinner() { | |||
return <i className="spinner"/>; | |||
}, | |||
onCheck(checked) { | |||
if (checked) { | |||
this.props.onAllSelected(); | |||
@@ -56,7 +60,7 @@ export default React.createClass({ | |||
}, | |||
renderGhostsDescription () { | |||
if (this.props.type !== TYPE.GHOSTS) { | |||
if (this.props.type !== TYPE.GHOSTS || !this.props.ready) { | |||
return null; | |||
} | |||
return <div className="spacer-top alert alert-info">{window.t('bulk_deletion.ghosts.description')}</div>; | |||
@@ -89,7 +93,7 @@ export default React.createClass({ | |||
<tbody> | |||
<tr> | |||
<td className="thin text-middle"> | |||
{this.renderCheckbox()} | |||
{this.props.ready ? this.renderCheckbox() : this.renderSpinner()} | |||
</td> | |||
{this.renderQualifierFilter()} | |||
<td className="thin nowrap text-middle"> |
@@ -5,10 +5,23 @@ import Template from './templates/users-header.hbs'; | |||
export default Marionette.ItemView.extend({ | |||
template: Template, | |||
collectionEvents: { | |||
'request': 'showSpinner', | |||
'sync': 'hideSpinner' | |||
}, | |||
events: { | |||
'click #users-create': 'onCreateClick' | |||
}, | |||
showSpinner: function () { | |||
this.$('.spinner').removeClass('hidden'); | |||
}, | |||
hideSpinner: function () { | |||
this.$('.spinner').addClass('hidden'); | |||
}, | |||
onCreateClick: function (e) { | |||
e.preventDefault(); | |||
this.createUser(); |
@@ -3,7 +3,20 @@ import ListItemView from './list-item-view'; | |||
export default Marionette.CollectionView.extend({ | |||
tagName: 'ul', | |||
childView: ListItemView | |||
childView: ListItemView, | |||
collectionEvents: { | |||
'request': 'showLoading', | |||
'sync': 'hideLoading' | |||
}, | |||
showLoading: function () { | |||
this.$el.addClass('new-loading'); | |||
}, | |||
hideLoading: function () { | |||
this.$el.removeClass('new-loading'); | |||
} | |||
}); | |||
@@ -1,5 +1,6 @@ | |||
<header class="page-header"> | |||
<h1 class="page-title">{{t 'users.page'}}</h1> | |||
<i class="spinner hidden"></i> | |||
<div class="page-actions"> | |||
<div class="button-group"> | |||
<button id="users-create">Create User</button> |
@@ -1,5 +1,7 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
export default React.createClass({ | |||
propTypes: { | |||
count: React.PropTypes.number.isRequired, | |||
@@ -11,18 +13,25 @@ export default React.createClass({ | |||
return typeof this.props.loadMore === 'function'; | |||
}, | |||
loadMoreProxy(e) { | |||
handleLoadMore(e) { | |||
e.preventDefault(); | |||
if (this.canLoadMore()) { | |||
this.props.loadMore(); | |||
} | |||
}, | |||
renderLoading() { | |||
return <footer className="spacer-top note text-center"> | |||
{window.t('loading')} | |||
</footer>; | |||
}, | |||
render() { | |||
let hasMore = this.props.total > this.props.count, | |||
loadMoreLink = <a onClick={this.loadMoreProxy} className="spacer-left" href="#">show more</a>; | |||
loadMoreLink = <a onClick={this.handleLoadMore} className="spacer-left" href="#">show more</a>; | |||
let className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<footer className="spacer-top note text-center"> | |||
<footer className={className}> | |||
{this.props.count}/{this.props.total} shown | |||
{this.canLoadMore() && hasMore ? loadMoreLink : null} | |||
</footer> |
@@ -147,3 +147,13 @@ export function post (url, data) { | |||
.submit() | |||
.then(checkStatus); | |||
} | |||
/** | |||
* Delay promise for testing purposes | |||
* @param response | |||
* @returns {Promise} | |||
*/ | |||
export function delay (response) { | |||
return new Promise(resolve => setTimeout(() => resolve(response), 3000)); | |||
} |
@@ -49,6 +49,12 @@ body { | |||
.page-header { | |||
.clearfix; | |||
margin-bottom: 10px; | |||
.spinner { | |||
position: relative; | |||
top: 3px; | |||
margin-left: 8px; | |||
} | |||
} | |||
.page-title { |
@@ -104,6 +104,11 @@ td.big-spacer-top { padding-top: 16px; } | |||
justify-content: space-between !important; | |||
} | |||
.new-loading { | |||
opacity: 0.5; | |||
transition: opacity 0.5s ease; | |||
} | |||
// Background Color | |||