Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
3c8820ab
Commit
3c8820ab
authored
Oct 01, 2018
by
Peter Holmberg
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
invites table
parent
13666c84
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
318 additions
and
88 deletions
+318
-88
public/app/core/components/OrgActionBar/OrgActionBar.tsx
+2
-9
public/app/features/users/InviteesTable.tsx
+59
-0
public/app/features/users/UsersActionBar.tsx
+80
-0
public/app/features/users/UsersListPage.tsx
+53
-22
public/app/features/users/UsersTable.tsx
+50
-52
public/app/features/users/state/actions.ts
+27
-2
public/app/features/users/state/reducers.ts
+14
-2
public/app/features/users/state/selectors.ts
+9
-0
public/app/types/index.ts
+2
-1
public/app/types/users.ts
+22
-0
No files found.
public/app/core/components/OrgActionBar/OrgActionBar.tsx
View file @
3c8820ab
...
...
@@ -4,19 +4,14 @@ import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
export
interface
Props
{
searchQuery
:
string
;
layoutMode
?:
LayoutMode
;
showLayoutMode
:
boolean
;
setLayoutMode
?:
(
mode
:
LayoutMode
)
=>
{};
setSearchQuery
:
(
value
:
string
)
=>
{};
linkButton
:
{
href
:
string
;
title
:
string
};
}
export
default
class
OrgActionBar
extends
PureComponent
<
Props
>
{
static
defaultProps
=
{
showLayoutMode
:
true
,
};
render
()
{
const
{
searchQuery
,
layoutMode
,
setLayoutMode
,
linkButton
,
setSearchQuery
,
showLayoutMode
}
=
this
.
props
;
const
{
searchQuery
,
layoutMode
,
setLayoutMode
,
linkButton
,
setSearchQuery
}
=
this
.
props
;
return
(
<
div
className=
"page-action-bar"
>
...
...
@@ -31,9 +26,7 @@ export default class OrgActionBar extends PureComponent<Props> {
/>
<
i
className=
"gf-form-input-icon fa fa-search"
/>
</
label
>
{
showLayoutMode
&&
(
<
LayoutSelector
mode=
{
layoutMode
}
onLayoutModeChanged=
{
(
mode
:
LayoutMode
)
=>
setLayoutMode
(
mode
)
}
/>
)
}
<
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"
>
...
...
public/app/features/users/InviteesTable.tsx
0 → 100644
View file @
3c8820ab
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
copyRef
=
createRef
<
HTMLTextAreaElement
>
();
copyToClipboard
=
()
=>
{
const
node
=
this
.
copyRef
.
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=
{
{
display
:
'none'
}
}
ref=
{
this
.
copyRef
}
/>
<
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
>
);
}
}
public/app/features/users/UsersActionBar.tsx
0 → 100644
View file @
3c8820ab
import
React
,
{
PureComponent
}
from
'react'
;
import
{
connect
}
from
'react-redux'
;
import
{
setUsersSearchQuery
}
from
'./state/actions'
;
import
{
getInviteesCount
,
getUsersSearchQuery
}
from
'./state/selectors'
;
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
);
public/app/features/users/UsersListPage.tsx
View file @
3c8820ab
import
React
,
{
PureComponent
}
from
'react'
;
import
{
hot
}
from
'react-hot-loader'
;
import
{
connect
}
from
'react-redux'
;
import
OrgActionBar
from
'app/core/components/OrgActionBar/OrgActionBar'
;
import
PageHeader
from
'app/core/components/PageHeader/PageHeader'
;
import
UsersActionBar
from
'./UsersActionBar'
;
import
UsersTable
from
'app/features/users/UsersTable'
;
import
{
NavModel
,
User
}
from
'app/types'
;
import
InviteesTable
from
'./InviteesTable'
;
import
{
Invitee
,
NavModel
,
User
}
from
'app/types'
;
import
appEvents
from
'app/core/app_events'
;
import
{
loadUsers
,
setUsersSearchQuery
,
updateUser
,
removeUser
}
from
'./state/actions'
;
import
{
loadUsers
,
loadInvitees
,
revokeInvite
,
setUsersSearchQuery
,
updateUser
,
removeUser
}
from
'./state/actions'
;
import
{
getNavModel
}
from
'../../core/selectors/navModel'
;
import
{
getUsers
,
getUsersSearchQuery
}
from
'./state/selectors'
;
import
{
get
Invitees
,
get
Users
,
getUsersSearchQuery
}
from
'./state/selectors'
;
export
interface
Props
{
navModel
:
NavModel
;
invitees
:
Invitee
[];
users
:
User
[];
searchQuery
:
string
;
externalUserMngInfo
:
string
;
loadUsers
:
typeof
loadUsers
;
loadInvitees
:
typeof
loadInvitees
;
setUsersSearchQuery
:
typeof
setUsersSearchQuery
;
updateUser
:
typeof
updateUser
;
removeUser
:
typeof
removeUser
;
revokeInvite
:
typeof
revokeInvite
;
}
export
class
UsersListPage
extends
PureComponent
<
Props
>
{
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
};
...
...
@@ -47,29 +65,38 @@ export class UsersListPage extends PureComponent<Props> {
});
};
render
()
{
const
{
navModel
,
searchQuery
,
setUsersSearchQuery
,
users
}
=
this
.
props
;
onRevokeInvite
=
code
=>
{
this
.
props
.
revokeInvite
(
code
);
};
const
linkButton
=
{
href
:
'/org/users/add'
,
title
:
'Add user'
,
};
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"
>
<
OrgActionBar
searchQuery=
{
searchQuery
}
showLayoutMode=
{
false
}
setSearchQuery=
{
setUsersSearchQuery
}
linkButton=
{
linkButton
}
/>
<
UsersTable
users=
{
users
}
onRoleChange=
{
(
role
,
user
)
=>
this
.
onRoleChange
(
role
,
user
)
}
onRemoveUser=
{
user
=>
this
.
onRemoveUser
(
user
)
}
/>
<
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
>
);
...
...
@@ -81,14 +108,18 @@ function mapStateToProps(state) {
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
));
public/app/features/users/UsersTable.tsx
View file @
3c8820ab
...
...
@@ -11,58 +11,56 @@ const UsersTable: SFC<Props> = props => {
const
{
users
,
onRoleChange
,
onRemoveUser
}
=
props
;
return
(
<
div
>
<
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
>
</
div
>
<
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
>
);
};
...
...
public/app/features/users/state/actions.ts
View file @
3c8820ab
import
{
ThunkAction
}
from
'redux-thunk'
;
import
{
StoreState
}
from
'../../../types'
;
import
{
getBackendSrv
}
from
'../../../core/services/backend_srv'
;
import
{
User
}
from
'app/types'
;
import
{
Invitee
,
User
}
from
'app/types'
;
export
enum
ActionTypes
{
LoadUsers
=
'LOAD_USERS'
,
LoadInvitees
=
'LOAD_INVITEES'
,
SetUsersSearchQuery
=
'SET_USERS_SEARCH_QUERY'
,
}
...
...
@@ -13,6 +14,11 @@ export interface LoadUsersAction {
payload
:
User
[];
}
export
interface
LoadInviteesAction
{
type
:
ActionTypes
.
LoadInvitees
;
payload
:
Invitee
[];
}
export
interface
SetUsersSearchQueryAction
{
type
:
ActionTypes
.
SetUsersSearchQuery
;
payload
:
string
;
...
...
@@ -23,12 +29,17 @@ const usersLoaded = (users: User[]): LoadUsersAction => ({
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
;
export
type
Action
=
LoadUsersAction
|
SetUsersSearchQueryAction
|
LoadInviteesAction
;
type
ThunkResult
<
R
>
=
ThunkAction
<
R
,
StoreState
,
undefined
,
Action
>
;
...
...
@@ -39,6 +50,13 @@ export function loadUsers(): ThunkResult<void> {
};
}
export
function
loadInvitees
():
ThunkResult
<
void
>
{
return
async
dispatch
=>
{
const
invitees
=
await
getBackendSrv
().
get
(
'/api/org/invites'
);
dispatch
(
inviteesLoaded
(
invitees
));
};
}
export
function
updateUser
(
user
:
User
):
ThunkResult
<
void
>
{
return
async
dispatch
=>
{
await
getBackendSrv
().
patch
(
`/api/org/users/
${
user
.
userId
}
`
,
user
);
...
...
@@ -52,3 +70,10 @@ export function removeUser(userId: number): ThunkResult<void> {
dispatch
(
loadUsers
());
};
}
export
function
revokeInvite
(
code
:
string
):
ThunkResult
<
void
>
{
return
async
dispatch
=>
{
await
getBackendSrv
().
patch
(
`/api/org/invites/
${
code
}
/revoke`
,
{});
dispatch
(
loadInvitees
());
};
}
public/app/features/users/state/reducers.ts
View file @
3c8820ab
import
{
User
,
UsersState
}
from
'app/types'
;
import
{
Invitee
,
User
,
UsersState
}
from
'app/types'
;
import
{
Action
,
ActionTypes
}
from
'./actions'
;
import
config
from
'../../../core/config'
;
export
const
initialState
:
UsersState
=
{
users
:
[]
as
User
[],
searchQuery
:
''
};
export
const
initialState
:
UsersState
=
{
invitees
:
[]
as
Invitee
[],
users
:
[]
as
User
[],
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
};
}
...
...
public/app/features/users/state/selectors.ts
View file @
3c8820ab
...
...
@@ -6,4 +6,13 @@ export const getUsers = state => {
});
};
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
;
public/app/types/index.ts
View file @
3c8820ab
...
...
@@ -7,7 +7,7 @@ import { DashboardState } from './dashboard';
import
{
DashboardAcl
,
OrgRole
,
PermissionLevel
}
from
'./acl'
;
import
{
DataSource
,
DataSourcesState
}
from
'./datasources'
;
import
{
PluginMeta
,
Plugin
,
PluginsState
}
from
'./plugins'
;
import
{
User
,
UsersState
}
from
'./users'
;
import
{
Invitee
,
User
,
UsersState
}
from
'./users'
;
export
{
Team
,
...
...
@@ -37,6 +37,7 @@ export {
Plugin
,
PluginsState
,
DataSourcesState
,
Invitee
,
User
,
UsersState
,
};
...
...
public/app/types/users.ts
View file @
3c8820ab
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
;
...
...
@@ -11,5 +28,10 @@ export interface User {
export
interface
UsersState
{
users
:
User
[];
invitees
:
Invitee
[];
searchQuery
:
string
;
canInvite
:
boolean
;
externalUserMngLinkUrl
:
string
;
externalUserMngLinkName
:
string
;
externalUserMngInfo
:
string
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment