mirror of https://github.com/grafana/grafana
parent
38637f056f
commit
fe4f2f71c3
@ -1,23 +0,0 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import OrgActionBar, { Props } from './OrgActionBar'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
searchQuery: '', |
||||
setSearchQuery: jest.fn(), |
||||
linkButton: { href: 'some/url', title: 'test' }, |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
return shallow(<OrgActionBar {...props} />); |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -1,38 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; |
||||
|
||||
export interface Props { |
||||
searchQuery: string; |
||||
layoutMode?: LayoutMode; |
||||
setLayoutMode?: (mode: LayoutMode) => {}; |
||||
setSearchQuery: (value: string) => {}; |
||||
linkButton: { href: string; title: string }; |
||||
} |
||||
|
||||
export default class OrgActionBar extends PureComponent<Props> { |
||||
render() { |
||||
const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; |
||||
|
||||
return ( |
||||
<div className="page-action-bar"> |
||||
<div className="gf-form gf-form--grow"> |
||||
<label className="gf-form--has-input-icon"> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input width-20" |
||||
value={searchQuery} |
||||
onChange={event => setSearchQuery(event.target.value)} |
||||
placeholder="Filter by name or type" |
||||
/> |
||||
<i className="gf-form-input-icon fa fa-search" /> |
||||
</label> |
||||
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => setLayoutMode(mode)} /> |
||||
</div> |
||||
<div className="page-action-bar__spacer" /> |
||||
<a className="btn btn-success" href={linkButton.href} target="_blank"> |
||||
{linkButton.title} |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import { DataSourcesActionBar, Props } from './DataSourcesActionBar'; |
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
layoutMode: LayoutModes.Grid, |
||||
searchQuery: '', |
||||
setDataSourcesLayoutMode: jest.fn(), |
||||
setDataSourcesSearchQuery: jest.fn(), |
||||
}; |
||||
|
||||
return shallow(<DataSourcesActionBar {...props} />); |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -0,0 +1,62 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; |
||||
import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; |
||||
import { getDataSourcesLayoutMode, getDataSourcesSearchQuery } from './state/selectors'; |
||||
|
||||
export interface Props { |
||||
searchQuery: string; |
||||
layoutMode: LayoutMode; |
||||
setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; |
||||
setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; |
||||
} |
||||
|
||||
export class DataSourcesActionBar extends PureComponent<Props> { |
||||
onSearchQueryChange = event => { |
||||
this.props.setDataSourcesSearchQuery(event.target.value); |
||||
}; |
||||
|
||||
render() { |
||||
const { searchQuery, layoutMode, setDataSourcesLayoutMode } = this.props; |
||||
|
||||
return ( |
||||
<div className="page-action-bar"> |
||||
<div className="gf-form gf-form--grow"> |
||||
<label className="gf-form--has-input-icon"> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input width-20" |
||||
value={searchQuery} |
||||
onChange={this.onSearchQueryChange} |
||||
placeholder="Filter by name or type" |
||||
/> |
||||
<i className="gf-form-input-icon fa fa-search" /> |
||||
</label> |
||||
<LayoutSelector |
||||
mode={layoutMode} |
||||
onLayoutModeChanged={(mode: LayoutMode) => setDataSourcesLayoutMode(mode)} |
||||
/> |
||||
</div> |
||||
<div className="page-action-bar__spacer" /> |
||||
<a className="page-header__cta btn btn-success" href="datasources/new"> |
||||
<i className="fa fa-plus" /> |
||||
Add data source |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
searchQuery: getDataSourcesSearchQuery(state.dataSources), |
||||
layoutMode: getDataSourcesLayoutMode(state.dataSources), |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
setDataSourcesLayoutMode, |
||||
setDataSourcesSearchQuery, |
||||
}; |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DataSourcesActionBar); |
@ -0,0 +1,42 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`Render should render component 1`] = ` |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon" |
||||
> |
||||
<input |
||||
className="gf-form-input width-20" |
||||
onChange={[Function]} |
||||
placeholder="Filter by name or type" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
<LayoutSelector |
||||
mode="grid" |
||||
onLayoutModeChanged={[Function]} |
||||
/> |
||||
</div> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
<a |
||||
className="page-header__cta btn btn-success" |
||||
href="datasources/new" |
||||
> |
||||
<i |
||||
className="fa fa-plus" |
||||
/> |
||||
Add data source |
||||
</a> |
||||
</div> |
||||
`; |
@ -0,0 +1,87 @@ |
||||
import config from 'app/core/config'; |
||||
import coreModule from 'app/core/core_module'; |
||||
import Remarkable from 'remarkable'; |
||||
import _ from 'lodash'; |
||||
|
||||
export class OrgUsersCtrl { |
||||
unfiltered: any; |
||||
users: any; |
||||
pendingInvites: any; |
||||
editor: any; |
||||
navModel: any; |
||||
externalUserMngLinkUrl: string; |
||||
externalUserMngLinkName: string; |
||||
externalUserMngInfo: string; |
||||
canInvite: boolean; |
||||
searchQuery: string; |
||||
showInvites: boolean; |
||||
|
||||
/** @ngInject */ |
||||
constructor(private $scope, private backendSrv, navModelSrv, $sce) { |
||||
this.navModel = navModelSrv.getNav('cfg', 'users', 0); |
||||
|
||||
this.get(); |
||||
this.externalUserMngLinkUrl = config.externalUserMngLinkUrl; |
||||
this.externalUserMngLinkName = config.externalUserMngLinkName; |
||||
this.canInvite = !config.disableLoginForm && !config.externalUserMngLinkName; |
||||
|
||||
// render external user management info markdown
|
||||
if (config.externalUserMngInfo) { |
||||
this.externalUserMngInfo = new Remarkable({ |
||||
linkTarget: '__blank', |
||||
}).render(config.externalUserMngInfo); |
||||
} |
||||
} |
||||
|
||||
get() { |
||||
this.backendSrv.get('/api/org/users').then(users => { |
||||
this.users = users; |
||||
this.unfiltered = users; |
||||
}); |
||||
this.backendSrv.get('/api/org/invites').then(pendingInvites => { |
||||
this.pendingInvites = pendingInvites; |
||||
}); |
||||
} |
||||
|
||||
onQueryUpdated() { |
||||
const regex = new RegExp(this.searchQuery, 'ig'); |
||||
this.users = _.filter(this.unfiltered, item => { |
||||
return regex.test(item.email) || regex.test(item.login); |
||||
}); |
||||
} |
||||
|
||||
updateOrgUser(user) { |
||||
this.backendSrv.patch('/api/org/users/' + user.userId, user); |
||||
} |
||||
|
||||
removeUser(user) { |
||||
this.$scope.appEvent('confirm-modal', { |
||||
title: 'Delete', |
||||
text: 'Are you sure you want to delete user ' + user.login + '?', |
||||
yesText: 'Delete', |
||||
icon: 'fa-warning', |
||||
onConfirm: () => { |
||||
this.removeUserConfirmed(user); |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
removeUserConfirmed(user) { |
||||
this.backendSrv.delete('/api/org/users/' + user.userId).then(this.get.bind(this)); |
||||
} |
||||
|
||||
revokeInvite(invite, evt) { |
||||
evt.stopPropagation(); |
||||
this.backendSrv.patch('/api/org/invites/' + invite.code + '/revoke').then(this.get.bind(this)); |
||||
} |
||||
|
||||
copyInviteToClipboard(evt) { |
||||
evt.stopPropagation(); |
||||
} |
||||
|
||||
getInviteUrl(invite) { |
||||
return invite.url; |
||||
} |
||||
} |
||||
|
||||
coreModule.controller('OrgUsersCtrl', OrgUsersCtrl); |
@ -0,0 +1,105 @@ |
||||
<page-header model="ctrl.navModel"></page-header> |
||||
|
||||
<div class="page-container page-body"> |
||||
<div class="page-action-bar"> |
||||
<label class="gf-form gf-form--has-input-icon"> |
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by username or email" /> |
||||
<i class="gf-form-input-icon fa fa-search"></i> |
||||
</label> |
||||
|
||||
<div ng-if="ctrl.pendingInvites.length" style="margin-left: 1rem"> |
||||
<button class="btn toggle-btn active" ng-if="!ctrl.showInvites"> |
||||
Users |
||||
</button><button class="btn toggle-btn" ng-if="!ctrl.showInvites" ng-click="ctrl.showInvites = true"> |
||||
Pending Invites ({{ctrl.pendingInvites.length}}) |
||||
</button> |
||||
<button class="btn toggle-btn" ng-if="ctrl.showInvites" ng-click="ctrl.showInvites = false"> |
||||
Users |
||||
</button><button class="btn toggle-btn active" ng-if="ctrl.showInvites"> |
||||
Pending Invites ({{ctrl.pendingInvites.length}}) |
||||
</button> |
||||
</div> |
||||
|
||||
<div class="page-action-bar__spacer"></div> |
||||
|
||||
<a class="btn btn-success" href="org/users/invite" ng-show="ctrl.canInvite"> |
||||
<i class="fa fa-plus"></i> |
||||
<span>Invite</span> |
||||
</a> |
||||
|
||||
<a class="btn btn-success" ng-href="{{ctrl.externalUserMngLinkUrl}}" target="_blank" ng-if="ctrl.externalUserMngLinkUrl"> |
||||
<i class="fa fa-external-link-square"></i> |
||||
{{ctrl.externalUserMngLinkName}} |
||||
</a> |
||||
</div> |
||||
|
||||
<div class="grafana-info-box" ng-if="ctrl.externalUserMngInfo"> |
||||
<span ng-bind-html="ctrl.externalUserMngInfo"></span> |
||||
</div> |
||||
|
||||
<div ng-hide="ctrl.showInvites"> |
||||
<table class="filter-table form-inline"> |
||||
<thead> |
||||
<tr> |
||||
<th></th> |
||||
<th>Login</th> |
||||
<th>Email</th> |
||||
<th> |
||||
Seen |
||||
<tip>Time since user was seen using Grafana</tip> |
||||
</th> |
||||
<th>Role</th> |
||||
<th style="width: 34px;"></th> |
||||
</tr> |
||||
</thead> |
||||
<tr ng-repeat="user in ctrl.users"> |
||||
<td class="width-4 text-center"> |
||||
<img class="filter-table__avatar" ng-src="{{user.avatarUrl}}"></img> |
||||
</td> |
||||
<td>{{user.login}}</td> |
||||
<td><span class="ellipsis">{{user.email}}</span></td> |
||||
<td>{{user.lastSeenAtAge}}</td> |
||||
<td> |
||||
<div class="gf-form-select-wrapper width-12"> |
||||
<select type="text" ng-model="user.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)"> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<a ng-click="ctrl.removeUser(user)" class="btn btn-danger btn-mini"> |
||||
<i class="fa fa-remove"></i> |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</div> |
||||
|
||||
<div ng-if="ctrl.showInvites"> |
||||
<table class="filter-table form-inline"> |
||||
<thead> |
||||
<tr> |
||||
<th>Email</th> |
||||
<th>Name</th> |
||||
<th></th> |
||||
<th style="width: 34px;"></th> |
||||
</tr> |
||||
</thead> |
||||
<tr ng-repeat="invite in ctrl.pendingInvites"> |
||||
<td>{{invite.email}}</td> |
||||
<td>{{invite.name}}</td> |
||||
<td class="text-right"> |
||||
<button class="btn btn-inverse btn-mini" clipboard-button="ctrl.getInviteUrl(invite)" ng-click="ctrl.copyInviteToClipboard($event)"> |
||||
<i class="fa fa-clipboard"></i> Copy Invite |
||||
</button> |
||||
|
||||
</td> |
||||
<td> |
||||
<button class="btn btn-danger btn-mini" ng-click="ctrl.revokeInvite(invite, $event)"> |
||||
<i class="fa fa-remove"></i> |
||||
</button> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
|
@ -0,0 +1,31 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import { PluginActionBar, Props } from './PluginActionBar'; |
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
searchQuery: '', |
||||
layoutMode: LayoutModes.Grid, |
||||
setLayoutMode: jest.fn(), |
||||
setPluginsSearchQuery: jest.fn(), |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
const wrapper = shallow(<PluginActionBar {...props} />); |
||||
const instance = wrapper.instance() as PluginActionBar; |
||||
|
||||
return { |
||||
wrapper, |
||||
instance, |
||||
}; |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const { wrapper } = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -0,0 +1,62 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; |
||||
import { setLayoutMode, setPluginsSearchQuery } from './state/actions'; |
||||
import { getPluginsSearchQuery, getLayoutMode } from './state/selectors'; |
||||
|
||||
export interface Props { |
||||
searchQuery: string; |
||||
layoutMode: LayoutMode; |
||||
setLayoutMode: typeof setLayoutMode; |
||||
setPluginsSearchQuery: typeof setPluginsSearchQuery; |
||||
} |
||||
|
||||
export class PluginActionBar extends PureComponent<Props> { |
||||
onSearchQueryChange = event => { |
||||
this.props.setPluginsSearchQuery(event.target.value); |
||||
}; |
||||
|
||||
render() { |
||||
const { searchQuery, layoutMode, setLayoutMode } = this.props; |
||||
|
||||
return ( |
||||
<div className="page-action-bar"> |
||||
<div className="gf-form gf-form--grow"> |
||||
<label className="gf-form--has-input-icon"> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input width-20" |
||||
value={searchQuery} |
||||
onChange={this.onSearchQueryChange} |
||||
placeholder="Filter by name or type" |
||||
/> |
||||
<i className="gf-form-input-icon fa fa-search" /> |
||||
</label> |
||||
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => setLayoutMode(mode)} /> |
||||
</div> |
||||
<div className="page-action-bar__spacer" /> |
||||
<a |
||||
className="btn btn-success" |
||||
href="https://grafana.com/plugins?utm_source=grafana_plugin_list" |
||||
target="_blank" |
||||
> |
||||
Find more plugins on Grafana.com |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
searchQuery: getPluginsSearchQuery(state.plugins), |
||||
layoutMode: getLayoutMode(state.plugins), |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
setPluginsSearchQuery, |
||||
setLayoutMode, |
||||
}; |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar); |
@ -1,32 +0,0 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import InviteesTable, { Props } from './InviteesTable'; |
||||
import { Invitee } from 'app/types'; |
||||
import { getMockInvitees } from './__mocks__/userMocks'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
invitees: [] as Invitee[], |
||||
revokeInvite: jest.fn(), |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
return shallow(<InviteesTable {...props} />); |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('should render invitees', () => { |
||||
const wrapper = setup({ |
||||
invitees: getMockInvitees(5), |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -1,64 +0,0 @@ |
||||
import React, { createRef, PureComponent } from 'react'; |
||||
import { Invitee } from 'app/types'; |
||||
|
||||
export interface Props { |
||||
invitees: Invitee[]; |
||||
revokeInvite: (code: string) => void; |
||||
} |
||||
|
||||
export default class InviteesTable extends PureComponent<Props> { |
||||
private copyUrlRef = createRef<HTMLTextAreaElement>(); |
||||
|
||||
copyToClipboard = () => { |
||||
const node = this.copyUrlRef.current; |
||||
|
||||
if (node) { |
||||
node.select(); |
||||
document.execCommand('copy'); |
||||
} |
||||
}; |
||||
|
||||
render() { |
||||
const { invitees, revokeInvite } = this.props; |
||||
|
||||
return ( |
||||
<table className="filter-table form-inline"> |
||||
<thead> |
||||
<tr> |
||||
<th>Email</th> |
||||
<th>Name</th> |
||||
<th /> |
||||
<th style={{ width: '34px' }} /> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{invitees.map((invitee, index) => { |
||||
return ( |
||||
<tr key={`${invitee.id}-${index}`}> |
||||
<td>{invitee.email}</td> |
||||
<td>{invitee.name}</td> |
||||
<td className="text-right"> |
||||
<button className="btn btn-inverse btn-mini" onClick={this.copyToClipboard}> |
||||
<textarea |
||||
readOnly={true} |
||||
value={invitee.url} |
||||
style={{ position: 'absolute', right: -1000 }} |
||||
ref={this.copyUrlRef} |
||||
/> |
||||
<i className="fa fa-clipboard" /> Copy Invite |
||||
</button> |
||||
|
||||
</td> |
||||
<td> |
||||
<button className="btn btn-danger btn-mini" onClick={() => revokeInvite(invitee.code)}> |
||||
<i className="fa fa-remove" /> |
||||
</button> |
||||
</td> |
||||
</tr> |
||||
); |
||||
})} |
||||
</tbody> |
||||
</table> |
||||
); |
||||
} |
||||
} |
@ -1,51 +0,0 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import { UsersActionBar, Props } from './UsersActionBar'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
searchQuery: '', |
||||
setUsersSearchQuery: jest.fn(), |
||||
showInvites: jest.fn(), |
||||
pendingInvitesCount: 0, |
||||
canInvite: false, |
||||
externalUserMngLinkUrl: '', |
||||
externalUserMngLinkName: '', |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
return shallow(<UsersActionBar {...props} />); |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('should render pending invites button', () => { |
||||
const wrapper = setup({ |
||||
pendingInvitesCount: 5, |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('should show invite button', () => { |
||||
const wrapper = setup({ |
||||
canInvite: true, |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('should show external user management button', () => { |
||||
const wrapper = setup({ |
||||
externalUserMngLinkUrl: 'some/url', |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -1,80 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
import { setUsersSearchQuery } from './state/actions'; |
||||
import { getInviteesCount, getUsersSearchQuery } from './state/selectors'; |
||||
|
||||
export interface Props { |
||||
searchQuery: string; |
||||
setUsersSearchQuery: typeof setUsersSearchQuery; |
||||
showInvites: () => void; |
||||
pendingInvitesCount: number; |
||||
canInvite: boolean; |
||||
externalUserMngLinkUrl: string; |
||||
externalUserMngLinkName: string; |
||||
} |
||||
|
||||
export class UsersActionBar extends PureComponent<Props> { |
||||
render() { |
||||
const { |
||||
canInvite, |
||||
externalUserMngLinkName, |
||||
externalUserMngLinkUrl, |
||||
searchQuery, |
||||
pendingInvitesCount, |
||||
setUsersSearchQuery, |
||||
showInvites, |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div className="page-action-bar"> |
||||
<div className="gf-form gf-form--grow"> |
||||
<label className="gf-form--has-input-icon"> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input width-20" |
||||
value={searchQuery} |
||||
onChange={event => setUsersSearchQuery(event.target.value)} |
||||
placeholder="Filter by name or type" |
||||
/> |
||||
<i className="gf-form-input-icon fa fa-search" /> |
||||
</label> |
||||
|
||||
<div className="page-action-bar__spacer" /> |
||||
{pendingInvitesCount > 0 && ( |
||||
<button className="btn btn-inverse" onClick={showInvites}> |
||||
Pending Invites ({pendingInvitesCount}) |
||||
</button> |
||||
)} |
||||
{canInvite && ( |
||||
<a className="btn btn-success" href="org/users/invite"> |
||||
<i className="fa fa-plus" /> |
||||
<span>Invite</span> |
||||
</a> |
||||
)} |
||||
{externalUserMngLinkUrl && ( |
||||
<a className="btn btn-success" href={externalUserMngLinkUrl} target="_blank"> |
||||
<i className="fa fa-external-link-square" /> |
||||
{externalUserMngLinkName} |
||||
</a> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
searchQuery: getUsersSearchQuery(state.users), |
||||
pendingInvitesCount: getInviteesCount(state.users), |
||||
externalUserMngLinkName: state.users.externalUserMngLinkName, |
||||
externalUserMngLinkUrl: state.users.externalUserMngLinkUrl, |
||||
canInvite: state.users.canInvite, |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
setUsersSearchQuery, |
||||
}; |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UsersActionBar); |
@ -1,55 +0,0 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import { UsersListPage, Props } from './UsersListPage'; |
||||
import { Invitee, NavModel, OrgUser } from 'app/types'; |
||||
import { getMockUser } from './__mocks__/userMocks'; |
||||
import appEvents from '../../core/app_events'; |
||||
|
||||
jest.mock('../../core/app_events', () => ({ |
||||
emit: jest.fn(), |
||||
})); |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
navModel: {} as NavModel, |
||||
users: [] as OrgUser[], |
||||
invitees: [] as Invitee[], |
||||
searchQuery: '', |
||||
externalUserMngInfo: '', |
||||
revokeInvite: jest.fn(), |
||||
loadInvitees: jest.fn(), |
||||
loadUsers: jest.fn(), |
||||
updateUser: jest.fn(), |
||||
removeUser: jest.fn(), |
||||
setUsersSearchQuery: jest.fn(), |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
const wrapper = shallow(<UsersListPage {...props} />); |
||||
const instance = wrapper.instance() as UsersListPage; |
||||
|
||||
return { |
||||
wrapper, |
||||
instance, |
||||
}; |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const { wrapper } = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
|
||||
describe('Functions', () => { |
||||
it('should emit show remove user modal', () => { |
||||
const { instance } = setup(); |
||||
const mockUser = getMockUser(); |
||||
|
||||
instance.onRemoveUser(mockUser); |
||||
|
||||
expect(appEvents.emit).toHaveBeenCalled(); |
||||
}); |
||||
}); |
@ -1,125 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { connect } from 'react-redux'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
import UsersActionBar from './UsersActionBar'; |
||||
import UsersTable from 'app/features/users/UsersTable'; |
||||
import InviteesTable from './InviteesTable'; |
||||
import { Invitee, NavModel, OrgUser } from 'app/types'; |
||||
import appEvents from 'app/core/app_events'; |
||||
import { loadUsers, loadInvitees, revokeInvite, setUsersSearchQuery, updateUser, removeUser } from './state/actions'; |
||||
import { getNavModel } from '../../core/selectors/navModel'; |
||||
import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors'; |
||||
|
||||
export interface Props { |
||||
navModel: NavModel; |
||||
invitees: Invitee[]; |
||||
users: OrgUser[]; |
||||
searchQuery: string; |
||||
externalUserMngInfo: string; |
||||
loadUsers: typeof loadUsers; |
||||
loadInvitees: typeof loadInvitees; |
||||
setUsersSearchQuery: typeof setUsersSearchQuery; |
||||
updateUser: typeof updateUser; |
||||
removeUser: typeof removeUser; |
||||
revokeInvite: typeof revokeInvite; |
||||
} |
||||
|
||||
export interface State { |
||||
showInvites: boolean; |
||||
} |
||||
|
||||
export class UsersListPage extends PureComponent<Props, State> { |
||||
state = { |
||||
showInvites: false, |
||||
}; |
||||
|
||||
componentDidMount() { |
||||
this.fetchUsers(); |
||||
this.fetchInvitees(); |
||||
} |
||||
|
||||
async fetchUsers() { |
||||
return await this.props.loadUsers(); |
||||
} |
||||
|
||||
async fetchInvitees() { |
||||
return await this.props.loadInvitees(); |
||||
} |
||||
|
||||
onRoleChange = (role, user) => { |
||||
const updatedUser = { ...user, role: role }; |
||||
|
||||
this.props.updateUser(updatedUser); |
||||
}; |
||||
|
||||
onRemoveUser = user => { |
||||
appEvents.emit('confirm-modal', { |
||||
title: 'Delete', |
||||
text: 'Are you sure you want to delete user ' + user.login + '?', |
||||
yesText: 'Delete', |
||||
icon: 'fa-warning', |
||||
onConfirm: () => { |
||||
this.props.removeUser(user.userId); |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
onRevokeInvite = code => { |
||||
this.props.revokeInvite(code); |
||||
}; |
||||
|
||||
showInvites = () => { |
||||
this.setState(prevState => ({ |
||||
showInvites: !prevState.showInvites, |
||||
})); |
||||
}; |
||||
|
||||
render() { |
||||
const { externalUserMngInfo, invitees, navModel, users } = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<PageHeader model={navModel} /> |
||||
<div className="page-container page-body"> |
||||
<UsersActionBar showInvites={this.showInvites} /> |
||||
{externalUserMngInfo && ( |
||||
<div className="grafana-info-box"> |
||||
<span>{externalUserMngInfo}</span> |
||||
</div> |
||||
)} |
||||
{this.state.showInvites ? ( |
||||
<InviteesTable invitees={invitees} revokeInvite={code => this.onRevokeInvite(code)} /> |
||||
) : ( |
||||
<UsersTable |
||||
users={users} |
||||
onRoleChange={(role, user) => this.onRoleChange(role, user)} |
||||
onRemoveUser={user => this.onRemoveUser(user)} |
||||
/> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
navModel: getNavModel(state.navIndex, 'users'), |
||||
users: getUsers(state.users), |
||||
searchQuery: getUsersSearchQuery(state.users), |
||||
invitees: getInvitees(state.users), |
||||
externalUserMngInfo: state.users.externalUserMngInfo, |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
loadUsers, |
||||
loadInvitees, |
||||
setUsersSearchQuery, |
||||
updateUser, |
||||
removeUser, |
||||
revokeInvite, |
||||
}; |
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage)); |
@ -1,33 +0,0 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import UsersTable, { Props } from './UsersTable'; |
||||
import { OrgUser } from 'app/types'; |
||||
import { getMockUsers } from './__mocks__/userMocks'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
users: [] as OrgUser[], |
||||
onRoleChange: jest.fn(), |
||||
onRemoveUser: jest.fn(), |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
return shallow(<UsersTable {...props} />); |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('should render users table', () => { |
||||
const wrapper = setup({ |
||||
users: getMockUsers(5), |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -1,67 +0,0 @@ |
||||
import React, { SFC } from 'react'; |
||||
import { OrgUser } from 'app/types'; |
||||
|
||||
export interface Props { |
||||
users: OrgUser[]; |
||||
onRoleChange: (role: string, user: OrgUser) => void; |
||||
onRemoveUser: (user: OrgUser) => void; |
||||
} |
||||
|
||||
const UsersTable: SFC<Props> = props => { |
||||
const { users, onRoleChange, onRemoveUser } = props; |
||||
|
||||
return ( |
||||
<table className="filter-table form-inline"> |
||||
<thead> |
||||
<tr> |
||||
<th /> |
||||
<th>Login</th> |
||||
<th>Email</th> |
||||
<th>Seen</th> |
||||
<th>Role</th> |
||||
<th style={{ width: '34px' }} /> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{users.map((user, index) => { |
||||
return ( |
||||
<tr key={`${user.userId}-${index}`}> |
||||
<td className="width-4 text-center"> |
||||
<img className="filter-table__avatar" src={user.avatarUrl} /> |
||||
</td> |
||||
<td>{user.login}</td> |
||||
<td> |
||||
<span className="ellipsis">{user.email}</span> |
||||
</td> |
||||
<td>{user.lastSeenAtAge}</td> |
||||
<td> |
||||
<div className="gf-form-select-wrapper width-12"> |
||||
<select |
||||
value={user.role} |
||||
className="gf-form-input" |
||||
onChange={event => onRoleChange(event.target.value, user)} |
||||
> |
||||
{['Viewer', 'Editor', 'Admin'].map((option, index) => { |
||||
return ( |
||||
<option value={option} key={`${option}-${index}`}> |
||||
{option} |
||||
</option> |
||||
); |
||||
})} |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div onClick={() => onRemoveUser(user)} className="btn btn-danger btn-mini"> |
||||
<i className="fa fa-remove" /> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
); |
||||
})} |
||||
</tbody> |
||||
</table> |
||||
); |
||||
}; |
||||
|
||||
export default UsersTable; |
@ -1,56 +0,0 @@ |
||||
export const getMockUsers = (amount: number) => { |
||||
const users = []; |
||||
|
||||
for (let i = 0; i <= amount; i++) { |
||||
users.push({ |
||||
avatarUrl: 'url/to/avatar', |
||||
email: `user-${i}@test.com`, |
||||
lastSeenAt: '2018-10-01', |
||||
lastSeenAtAge: '', |
||||
login: `user-${i}`, |
||||
orgId: 1, |
||||
role: 'Admin', |
||||
userId: i, |
||||
}); |
||||
} |
||||
|
||||
return users; |
||||
}; |
||||
|
||||
export const getMockUser = () => { |
||||
return { |
||||
avatarUrl: 'url/to/avatar', |
||||
email: `user@test.com`, |
||||
lastSeenAt: '2018-10-01', |
||||
lastSeenAtAge: '', |
||||
login: `user`, |
||||
orgId: 1, |
||||
role: 'Admin', |
||||
userId: 2, |
||||
}; |
||||
}; |
||||
|
||||
export const getMockInvitees = (amount: number) => { |
||||
const invitees = []; |
||||
|
||||
for (let i = 0; i <= amount; i++) { |
||||
invitees.push({ |
||||
code: `asdfasdfsadf-${i}`, |
||||
createdOn: '2018-10-02', |
||||
email: `invitee-${i}@test.com`, |
||||
emailSent: true, |
||||
emailSentOn: '2018-10-02', |
||||
id: i, |
||||
invitedByEmail: 'admin@grafana.com', |
||||
invitedByLogin: 'admin', |
||||
invitedByName: 'admin', |
||||
name: `invitee-${i}`, |
||||
orgId: 1, |
||||
role: 'viewer', |
||||
status: 'not accepted', |
||||
url: `localhost/invite/$${i}`, |
||||
}); |
||||
} |
||||
|
||||
return invitees; |
||||
}; |
@ -1,141 +0,0 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`Render should render component 1`] = ` |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon" |
||||
> |
||||
<input |
||||
className="gf-form-input width-20" |
||||
onChange={[Function]} |
||||
placeholder="Filter by name or type" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
</div> |
||||
</div> |
||||
`; |
||||
|
||||
exports[`Render should render pending invites button 1`] = ` |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon" |
||||
> |
||||
<input |
||||
className="gf-form-input width-20" |
||||
onChange={[Function]} |
||||
placeholder="Filter by name or type" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
<button |
||||
className="btn btn-inverse" |
||||
onClick={[MockFunction]} |
||||
> |
||||
Pending Invites ( |
||||
5 |
||||
) |
||||
</button> |
||||
</div> |
||||
</div> |
||||
`; |
||||
|
||||
exports[`Render should show external user management button 1`] = ` |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon" |
||||
> |
||||
<input |
||||
className="gf-form-input width-20" |
||||
onChange={[Function]} |
||||
placeholder="Filter by name or type" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
<a |
||||
className="btn btn-success" |
||||
href="some/url" |
||||
target="_blank" |
||||
> |
||||
<i |
||||
className="fa fa-external-link-square" |
||||
/> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
`; |
||||
|
||||
exports[`Render should show invite button 1`] = ` |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon" |
||||
> |
||||
<input |
||||
className="gf-form-input width-20" |
||||
onChange={[Function]} |
||||
placeholder="Filter by name or type" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
<a |
||||
className="btn btn-success" |
||||
href="org/users/invite" |
||||
> |
||||
<i |
||||
className="fa fa-plus" |
||||
/> |
||||
<span> |
||||
Invite |
||||
</span> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
`; |
@ -1,21 +0,0 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`Render should render component 1`] = ` |
||||
<div> |
||||
<PageHeader |
||||
model={Object {}} |
||||
/> |
||||
<div |
||||
className="page-container page-body" |
||||
> |
||||
<Connect(UsersActionBar) |
||||
showInvites={[Function]} |
||||
/> |
||||
<UsersTable |
||||
onRemoveUser={[Function]} |
||||
onRoleChange={[Function]} |
||||
users={Array []} |
||||
/> |
||||
</div> |
||||
</div> |
||||
`; |
@ -1,444 +0,0 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`Render should render component 1`] = ` |
||||
<table |
||||
className="filter-table form-inline" |
||||
> |
||||
<thead> |
||||
<tr> |
||||
<th /> |
||||
<th> |
||||
Login |
||||
</th> |
||||
<th> |
||||
Email |
||||
</th> |
||||
<th> |
||||
Seen |
||||
</th> |
||||
<th> |
||||
Role |
||||
</th> |
||||
<th |
||||
style={ |
||||
Object { |
||||
"width": "34px", |
||||
} |
||||
} |
||||
/> |
||||
</tr> |
||||
</thead> |
||||
<tbody /> |
||||
</table> |
||||
`; |
||||
|
||||
exports[`Render should render users table 1`] = ` |
||||
<table |
||||
className="filter-table form-inline" |
||||
> |
||||
<thead> |
||||
<tr> |
||||
<th /> |
||||
<th> |
||||
Login |
||||
</th> |
||||
<th> |
||||
Email |
||||
</th> |
||||
<th> |
||||
Seen |
||||
</th> |
||||
<th> |
||||
Role |
||||
</th> |
||||
<th |
||||
style={ |
||||
Object { |
||||
"width": "34px", |
||||
} |
||||
} |
||||
/> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr |
||||
key="0-0" |
||||
> |
||||
<td |
||||
className="width-4 text-center" |
||||
> |
||||
<img |
||||
className="filter-table__avatar" |
||||
src="url/to/avatar" |
||||
/> |
||||
</td> |
||||
<td> |
||||
user-0 |
||||
</td> |
||||
<td> |
||||
<span |
||||
className="ellipsis" |
||||
> |
||||
user-0@test.com |
||||
</span> |
||||
</td> |
||||
<td /> |
||||
<td> |
||||
<div |
||||
className="gf-form-select-wrapper width-12" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
key="Viewer-0" |
||||
value="Viewer" |
||||
> |
||||
Viewer |
||||
</option> |
||||
<option |
||||
key="Editor-1" |
||||
value="Editor" |
||||
> |
||||
Editor |
||||
</option> |
||||
<option |
||||
key="Admin-2" |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div |
||||
className="btn btn-danger btn-mini" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-remove" |
||||
/> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key="1-1" |
||||
> |
||||
<td |
||||
className="width-4 text-center" |
||||
> |
||||
<img |
||||
className="filter-table__avatar" |
||||
src="url/to/avatar" |
||||
/> |
||||
</td> |
||||
<td> |
||||
user-1 |
||||
</td> |
||||
<td> |
||||
<span |
||||
className="ellipsis" |
||||
> |
||||
user-1@test.com |
||||
</span> |
||||
</td> |
||||
<td /> |
||||
<td> |
||||
<div |
||||
className="gf-form-select-wrapper width-12" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
key="Viewer-0" |
||||
value="Viewer" |
||||
> |
||||
Viewer |
||||
</option> |
||||
<option |
||||
key="Editor-1" |
||||
value="Editor" |
||||
> |
||||
Editor |
||||
</option> |
||||
<option |
||||
key="Admin-2" |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div |
||||
className="btn btn-danger btn-mini" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-remove" |
||||
/> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key="2-2" |
||||
> |
||||
<td |
||||
className="width-4 text-center" |
||||
> |
||||
<img |
||||
className="filter-table__avatar" |
||||
src="url/to/avatar" |
||||
/> |
||||
</td> |
||||
<td> |
||||
user-2 |
||||
</td> |
||||
<td> |
||||
<span |
||||
className="ellipsis" |
||||
> |
||||
user-2@test.com |
||||
</span> |
||||
</td> |
||||
<td /> |
||||
<td> |
||||
<div |
||||
className="gf-form-select-wrapper width-12" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
key="Viewer-0" |
||||
value="Viewer" |
||||
> |
||||
Viewer |
||||
</option> |
||||
<option |
||||
key="Editor-1" |
||||
value="Editor" |
||||
> |
||||
Editor |
||||
</option> |
||||
<option |
||||
key="Admin-2" |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div |
||||
className="btn btn-danger btn-mini" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-remove" |
||||
/> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key="3-3" |
||||
> |
||||
<td |
||||
className="width-4 text-center" |
||||
> |
||||
<img |
||||
className="filter-table__avatar" |
||||
src="url/to/avatar" |
||||
/> |
||||
</td> |
||||
<td> |
||||
user-3 |
||||
</td> |
||||
<td> |
||||
<span |
||||
className="ellipsis" |
||||
> |
||||
user-3@test.com |
||||
</span> |
||||
</td> |
||||
<td /> |
||||
<td> |
||||
<div |
||||
className="gf-form-select-wrapper width-12" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
key="Viewer-0" |
||||
value="Viewer" |
||||
> |
||||
Viewer |
||||
</option> |
||||
<option |
||||
key="Editor-1" |
||||
value="Editor" |
||||
> |
||||
Editor |
||||
</option> |
||||
<option |
||||
key="Admin-2" |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div |
||||
className="btn btn-danger btn-mini" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-remove" |
||||
/> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key="4-4" |
||||
> |
||||
<td |
||||
className="width-4 text-center" |
||||
> |
||||
<img |
||||
className="filter-table__avatar" |
||||
src="url/to/avatar" |
||||
/> |
||||
</td> |
||||
<td> |
||||
user-4 |
||||
</td> |
||||
<td> |
||||
<span |
||||
className="ellipsis" |
||||
> |
||||
user-4@test.com |
||||
</span> |
||||
</td> |
||||
<td /> |
||||
<td> |
||||
<div |
||||
className="gf-form-select-wrapper width-12" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
key="Viewer-0" |
||||
value="Viewer" |
||||
> |
||||
Viewer |
||||
</option> |
||||
<option |
||||
key="Editor-1" |
||||
value="Editor" |
||||
> |
||||
Editor |
||||
</option> |
||||
<option |
||||
key="Admin-2" |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div |
||||
className="btn btn-danger btn-mini" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-remove" |
||||
/> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key="5-5" |
||||
> |
||||
<td |
||||
className="width-4 text-center" |
||||
> |
||||
<img |
||||
className="filter-table__avatar" |
||||
src="url/to/avatar" |
||||
/> |
||||
</td> |
||||
<td> |
||||
user-5 |
||||
</td> |
||||
<td> |
||||
<span |
||||
className="ellipsis" |
||||
> |
||||
user-5@test.com |
||||
</span> |
||||
</td> |
||||
<td /> |
||||
<td> |
||||
<div |
||||
className="gf-form-select-wrapper width-12" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
key="Viewer-0" |
||||
value="Viewer" |
||||
> |
||||
Viewer |
||||
</option> |
||||
<option |
||||
key="Editor-1" |
||||
value="Editor" |
||||
> |
||||
Editor |
||||
</option> |
||||
<option |
||||
key="Admin-2" |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div |
||||
className="btn btn-danger btn-mini" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-remove" |
||||
/> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
`; |
@ -1,79 +0,0 @@ |
||||
import { ThunkAction } from 'redux-thunk'; |
||||
import { StoreState } from '../../../types'; |
||||
import { getBackendSrv } from '../../../core/services/backend_srv'; |
||||
import { Invitee, OrgUser } from 'app/types'; |
||||
|
||||
export enum ActionTypes { |
||||
LoadUsers = 'LOAD_USERS', |
||||
LoadInvitees = 'LOAD_INVITEES', |
||||
SetUsersSearchQuery = 'SET_USERS_SEARCH_QUERY', |
||||
} |
||||
|
||||
export interface LoadUsersAction { |
||||
type: ActionTypes.LoadUsers; |
||||
payload: OrgUser[]; |
||||
} |
||||
|
||||
export interface LoadInviteesAction { |
||||
type: ActionTypes.LoadInvitees; |
||||
payload: Invitee[]; |
||||
} |
||||
|
||||
export interface SetUsersSearchQueryAction { |
||||
type: ActionTypes.SetUsersSearchQuery; |
||||
payload: string; |
||||
} |
||||
|
||||
const usersLoaded = (users: OrgUser[]): LoadUsersAction => ({ |
||||
type: ActionTypes.LoadUsers, |
||||
payload: users, |
||||
}); |
||||
|
||||
const inviteesLoaded = (invitees: Invitee[]): LoadInviteesAction => ({ |
||||
type: ActionTypes.LoadInvitees, |
||||
payload: invitees, |
||||
}); |
||||
|
||||
export const setUsersSearchQuery = (query: string): SetUsersSearchQueryAction => ({ |
||||
type: ActionTypes.SetUsersSearchQuery, |
||||
payload: query, |
||||
}); |
||||
|
||||
export type Action = LoadUsersAction | SetUsersSearchQueryAction | LoadInviteesAction; |
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>; |
||||
|
||||
export function loadUsers(): ThunkResult<void> { |
||||
return async dispatch => { |
||||
const users = await getBackendSrv().get('/api/org/users'); |
||||
dispatch(usersLoaded(users)); |
||||
}; |
||||
} |
||||
|
||||
export function loadInvitees(): ThunkResult<void> { |
||||
return async dispatch => { |
||||
const invitees = await getBackendSrv().get('/api/org/invites'); |
||||
dispatch(inviteesLoaded(invitees)); |
||||
}; |
||||
} |
||||
|
||||
export function updateUser(user: OrgUser): ThunkResult<void> { |
||||
return async dispatch => { |
||||
await getBackendSrv().patch(`/api/org/users/${user.userId}`, user); |
||||
dispatch(loadUsers()); |
||||
}; |
||||
} |
||||
|
||||
export function removeUser(userId: number): ThunkResult<void> { |
||||
return async dispatch => { |
||||
await getBackendSrv().delete(`/api/org/users/${userId}`); |
||||
dispatch(loadUsers()); |
||||
}; |
||||
} |
||||
|
||||
export function revokeInvite(code: string): ThunkResult<void> { |
||||
return async dispatch => { |
||||
await getBackendSrv().patch(`/api/org/invites/${code}/revoke`, {}); |
||||
dispatch(loadInvitees()); |
||||
}; |
||||
} |
@ -1,32 +0,0 @@ |
||||
import { Invitee, OrgUser, UsersState } from 'app/types'; |
||||
import { Action, ActionTypes } from './actions'; |
||||
import config from '../../../core/config'; |
||||
|
||||
export const initialState: UsersState = { |
||||
invitees: [] as Invitee[], |
||||
users: [] as OrgUser[], |
||||
searchQuery: '', |
||||
canInvite: !config.disableLoginForm && !config.externalUserMngLinkName, |
||||
externalUserMngInfo: config.externalUserMngInfo, |
||||
externalUserMngLinkName: config.externalUserMngLinkName, |
||||
externalUserMngLinkUrl: config.externalUserMngLinkUrl, |
||||
}; |
||||
|
||||
export const usersReducer = (state = initialState, action: Action): UsersState => { |
||||
switch (action.type) { |
||||
case ActionTypes.LoadUsers: |
||||
return { ...state, users: action.payload }; |
||||
|
||||
case ActionTypes.LoadInvitees: |
||||
return { ...state, invitees: action.payload }; |
||||
|
||||
case ActionTypes.SetUsersSearchQuery: |
||||
return { ...state, searchQuery: action.payload }; |
||||
} |
||||
|
||||
return state; |
||||
}; |
||||
|
||||
export default { |
||||
users: usersReducer, |
||||
}; |
@ -1,18 +0,0 @@ |
||||
export const getUsers = state => { |
||||
const regex = new RegExp(state.searchQuery, 'i'); |
||||
|
||||
return state.users.filter(user => { |
||||
return regex.test(user.login) || regex.test(user.email); |
||||
}); |
||||
}; |
||||
|
||||
export const getInvitees = state => { |
||||
const regex = new RegExp(state.searchQuery, 'i'); |
||||
|
||||
return state.invitees.filter(invitee => { |
||||
return regex.test(invitee.name) || regex.test(invitee.email); |
||||
}); |
||||
}; |
||||
|
||||
export const getInviteesCount = state => state.invitees.length; |
||||
export const getUsersSearchQuery = state => state.searchQuery; |
@ -1,37 +0,0 @@ |
||||
export interface Invitee { |
||||
code: string; |
||||
createdOn: string; |
||||
email: string; |
||||
emailSent: boolean; |
||||
emailSentOn: string; |
||||
id: number; |
||||
invitedByEmail: string; |
||||
invitedByLogin: string; |
||||
invitedByName: string; |
||||
name: string; |
||||
orgId: number; |
||||
role: string; |
||||
status: string; |
||||
url: string; |
||||
} |
||||
|
||||
export interface User { |
||||
avatarUrl: string; |
||||
email: string; |
||||
lastSeenAt: string; |
||||
lastSeenAtAge: string; |
||||
login: string; |
||||
orgId: number; |
||||
role: string; |
||||
userId: number; |
||||
} |
||||
|
||||
export interface UsersState { |
||||
users: User[]; |
||||
invitees: Invitee[]; |
||||
searchQuery: string; |
||||
canInvite: boolean; |
||||
externalUserMngLinkUrl: string; |
||||
externalUserMngLinkName: string; |
||||
externalUserMngInfo: string; |
||||
} |
Loading…
Reference in new issue