mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Admin: New Admin User page (#20498)
* admin: user page to react WIP * admin user page: basic view * admin user page: refactor, extract orgs and permissions components * admin user: change sessions actions styles * admin user: add disable button * user admin: add change grafana admin action * user admin: able to change org role and remove org * user admin: confirm force logout * user admin: change org button style * user admin: add confirm modals for critical actions * user admin: lock down ldap user info * user admin: align with latest design changes * user admin: add LDAP sync * admin user: confirm button * user admin: add to org modal * user admin: fix ConfirmButton story * admin user: handle grafana admin change * ConfirmButton: make styled component * ConfirmButton: completely styled component * User Admin: permissions section refactor * admin user: refactor (orgs and sessions) * ConfirmButton: able to set confirm variant * admin user: inline org removal * admin user: show ldap sync info only for ldap users * admin user: edit profile * ConfirmButton: some fixes after review * Chore: fix storybook build * admin user: rename handlers * admin user: remove LdapUserPage import from routes * Chore: fix ConfirmButton tests * Chore: fix user api endpoint tests * Chore: update failed test snapshots * admin user: redux actions WIP * admin user: use new ConfirmModal component for user profile * admin user: use new ConfirmModal component for sessions * admin user: use lockMessage * ConfirmButton: use primary button as default * admin user: fix ActionButton color * UI: use Icon component for Modal * UI: refactor ConfirmModal after Modal changes * UI: add link button variant * UI: able to use custom ConfirmButton * Chore: fix type errors after ConfirmButton refactor * Chore: revert Graph component changes (works with TS 3.7) * Chore: use Forms.Button instead of ActionButton * admin user: align items * admin user: align add to org modal * UI: organization picker component * admin user: use org picker for AddToOrgModal * admin user: org actions * admin user: connect sessions actions * admin user: updateUserPermissions action * admin user: enable delete user action * admin user: sync ldap user * Chore: refactor, remove unused code * Chore: refactor, move api calls to actions * admin user: set user password action * Chore: refactor, remove unused components * admin user: set input focus on edit * admin user: pass user into debug LDAP mapping * UserAdminPage: Ux changes * UserAdminPage: align buttons to the left * UserAdminPage: align delete user button * UserAdminPage: swap add to org modal buttons * UserAdminPage: set password field to empty when editing * UserAdminPage: fix tests * Updated button border * Chore: fix ConfirmButton after changes introduced in #21092 Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
108039af33
commit
8505d90768
@ -16,7 +16,7 @@ const defaultProps = {
|
|||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
size: ['xs', 'sm', 'md', 'lg'],
|
size: ['xs', 'sm', 'md', 'lg'],
|
||||||
variant: ['primary', 'secondary', 'danger', 'inverse', 'transparent'],
|
variant: ['primary', 'secondary', 'danger', 'inverse', 'transparent', 'link'],
|
||||||
};
|
};
|
||||||
const combinationOptions = {
|
const combinationOptions = {
|
||||||
CombinationRenderer: ThemeableCombinationsRowRenderer,
|
CombinationRenderer: ThemeableCombinationsRowRenderer,
|
||||||
|
@ -67,6 +67,13 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant, textAndIco
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'link':
|
||||||
|
background = css`
|
||||||
|
${buttonVariantStyles('', '', theme.colors.linkExternal, 'rgba(0, 0, 0, 0.1)', true)};
|
||||||
|
background: transparent;
|
||||||
|
`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -133,9 +133,11 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<span className={styles.buttonContainer}>
|
<span className={styles.buttonContainer}>
|
||||||
{typeof children === 'string' ? (
|
{typeof children === 'string' ? (
|
||||||
<Forms.Button className={buttonClass} size={size} variant="link" onClick={onClick}>
|
<span className={buttonClass}>
|
||||||
{children}
|
<Forms.Button size={size} variant="link" onClick={onClick}>
|
||||||
</Forms.Button>
|
{children}
|
||||||
|
</Forms.Button>
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className={buttonClass} onClick={onClick}>
|
<span className={buttonClass} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -38,7 +38,7 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) =>
|
|||||||
) as string;
|
) as string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
borderColor: selectThemeVariant({ light: theme.colors.gray70, dark: theme.colors.gray33 }, theme.type),
|
borderColor: selectThemeVariant({ light: theme.colors.gray85, dark: theme.colors.gray25 }, theme.type),
|
||||||
background: buttonVariantStyles(
|
background: buttonVariantStyles(
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@ -57,7 +57,6 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) =>
|
|||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
background: buttonVariantStyles('transparent', 'transparent', theme.colors.linkExternal),
|
background: buttonVariantStyles('transparent', 'transparent', theme.colors.linkExternal),
|
||||||
variantStyles: css`
|
variantStyles: css`
|
||||||
text-decoration: underline;
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -11,6 +11,7 @@ export enum InputStatus {
|
|||||||
interface Props extends React.HTMLProps<HTMLInputElement> {
|
interface Props extends React.HTMLProps<HTMLInputElement> {
|
||||||
validationEvents?: ValidationEvents;
|
validationEvents?: ValidationEvents;
|
||||||
hideErrorMessage?: boolean;
|
hideErrorMessage?: boolean;
|
||||||
|
inputRef?: React.LegacyRef<HTMLInputElement>;
|
||||||
|
|
||||||
// Override event props and append status as argument
|
// Override event props and append status as argument
|
||||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||||
@ -70,14 +71,14 @@ export class Input extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { validationEvents, className, hideErrorMessage, ...restProps } = this.props;
|
const { validationEvents, className, hideErrorMessage, inputRef, ...restProps } = this.props;
|
||||||
const { error } = this.state;
|
const { error } = this.state;
|
||||||
const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className);
|
const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className);
|
||||||
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
|
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flexGrow: 1 }}>
|
<div style={{ flexGrow: 1 }}>
|
||||||
<input {...inputElementProps} className={inputClassName} />
|
<input {...inputElementProps} ref={inputRef} className={inputClassName} />
|
||||||
{error && !hideErrorMessage && <span>{error}</span>}
|
{error && !hideErrorMessage && <span>{error}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -36,6 +36,8 @@ func getUserUserProfile(userID int64) Response {
|
|||||||
query.Result.IsExternal = true
|
query.Result.IsExternal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email)
|
||||||
|
|
||||||
return JSON(200, query.Result)
|
return JSON(200, query.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@ -48,9 +50,10 @@ func TestUserApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sc.handlerFunc = GetUserByID
|
sc.handlerFunc = GetUserByID
|
||||||
|
avatarUrl := dtos.GetGravatarUrl("daniel@grafana.com")
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
expected := `
|
expected := fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"email": "daniel@grafana.com",
|
"email": "daniel@grafana.com",
|
||||||
@ -64,10 +67,11 @@ func TestUserApiEndpoint(t *testing.T) {
|
|||||||
"authLabels": [
|
"authLabels": [
|
||||||
"LDAP"
|
"LDAP"
|
||||||
],
|
],
|
||||||
|
"avatarUrl": "%s",
|
||||||
"updatedAt": "2019-02-11T17:30:40Z",
|
"updatedAt": "2019-02-11T17:30:40Z",
|
||||||
"createdAt": "2019-02-11T17:30:40Z"
|
"createdAt": "2019-02-11T17:30:40Z"
|
||||||
}
|
}
|
||||||
`
|
`, avatarUrl)
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, sc.resp.Code)
|
require.Equal(t, http.StatusOK, sc.resp.Code)
|
||||||
require.JSONEq(t, expected, sc.resp.Body.String())
|
require.JSONEq(t, expected, sc.resp.Body.String())
|
||||||
@ -109,6 +113,7 @@ func TestUserApiEndpoint(t *testing.T) {
|
|||||||
"isDisabled": false,
|
"isDisabled": false,
|
||||||
"authLabels": null,
|
"authLabels": null,
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
|
"avatarUrl": "",
|
||||||
"updatedAt": "2019-02-11T17:30:40Z",
|
"updatedAt": "2019-02-11T17:30:40Z",
|
||||||
"createdAt": "2019-02-11T17:30:40Z"
|
"createdAt": "2019-02-11T17:30:40Z"
|
||||||
}
|
}
|
||||||
|
@ -227,6 +227,7 @@ type UserProfileDTO struct {
|
|||||||
AuthLabels []string `json:"authLabels"`
|
AuthLabels []string `json:"authLabels"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSearchHitDTO struct {
|
type UserSearchHitDTO struct {
|
||||||
|
71
public/app/core/components/Select/OrgPicker.tsx
Normal file
71
public/app/core/components/Select/OrgPicker.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { AsyncSelect } from '@grafana/ui';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import { Organization } from 'app/types';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface OrgSelectItem {
|
||||||
|
id: number;
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onSelected: (org: OrgSelectItem) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OrgPicker extends PureComponent<Props, State> {
|
||||||
|
orgs: Organization[];
|
||||||
|
|
||||||
|
state: State = {
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
async loadOrgs() {
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
const orgs = await getBackendSrv().get('/api/orgs');
|
||||||
|
this.orgs = orgs;
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
return orgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrgOptions = async (query: string): Promise<Array<SelectableValue<number>>> => {
|
||||||
|
if (!this.orgs) {
|
||||||
|
await this.loadOrgs();
|
||||||
|
}
|
||||||
|
return this.orgs.map(
|
||||||
|
(org: Organization): SelectableValue<number> => ({
|
||||||
|
id: org.id,
|
||||||
|
value: org.id,
|
||||||
|
label: org.name,
|
||||||
|
name: org.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, onSelected } = this.props;
|
||||||
|
const { isLoading } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="org-picker">
|
||||||
|
<AsyncSelect
|
||||||
|
className={className}
|
||||||
|
isLoading={isLoading}
|
||||||
|
defaultOptions={true}
|
||||||
|
isSearchable={false}
|
||||||
|
loadOptions={this.getOrgOptions}
|
||||||
|
onChange={onSelected}
|
||||||
|
placeholder="Select organization"
|
||||||
|
noOptionsMessage={() => 'No organizations found'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
import React, { FC } from 'react';
|
|
||||||
import { UserInfo } from './UserInfo';
|
|
||||||
import { LdapUserPermissions } from './ldap/LdapUserPermissions';
|
|
||||||
import { User } from 'app/types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DisabledUserInfo: FC<Props> = ({ user }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LdapUserPermissions
|
|
||||||
permissions={{
|
|
||||||
isGrafanaAdmin: (user as any).isGrafanaAdmin,
|
|
||||||
isDisabled: (user as any).isDisabled,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<UserInfo user={user} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
200
public/app/features/admin/UserAdminPage.tsx
Normal file
200
public/app/features/admin/UserAdminPage.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { NavModel } from '@grafana/data';
|
||||||
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
|
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import Page from 'app/core/components/Page/Page';
|
||||||
|
import { UserProfile } from './UserProfile';
|
||||||
|
import { UserPermissions } from './UserPermissions';
|
||||||
|
import { UserSessions } from './UserSessions';
|
||||||
|
import { UserLdapSyncInfo } from './UserLdapSyncInfo';
|
||||||
|
import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError } from 'app/types';
|
||||||
|
import {
|
||||||
|
loadAdminUserPage,
|
||||||
|
revokeSession,
|
||||||
|
revokeAllSessions,
|
||||||
|
updateUser,
|
||||||
|
setUserPassword,
|
||||||
|
disableUser,
|
||||||
|
enableUser,
|
||||||
|
deleteUser,
|
||||||
|
updateUserPermissions,
|
||||||
|
addOrgUser,
|
||||||
|
updateOrgUserRole,
|
||||||
|
deleteOrgUser,
|
||||||
|
syncLdapUser,
|
||||||
|
} from './state/actions';
|
||||||
|
import { UserOrgs } from './UserOrgs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navModel: NavModel;
|
||||||
|
userId: number;
|
||||||
|
user: UserDTO;
|
||||||
|
orgs: UserOrg[];
|
||||||
|
sessions: UserSession[];
|
||||||
|
ldapSyncInfo: SyncInfo;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: UserAdminError;
|
||||||
|
|
||||||
|
loadAdminUserPage: typeof loadAdminUserPage;
|
||||||
|
revokeSession: typeof revokeSession;
|
||||||
|
revokeAllSessions: typeof revokeAllSessions;
|
||||||
|
updateUser: typeof updateUser;
|
||||||
|
setUserPassword: typeof setUserPassword;
|
||||||
|
disableUser: typeof disableUser;
|
||||||
|
enableUser: typeof enableUser;
|
||||||
|
deleteUser: typeof deleteUser;
|
||||||
|
updateUserPermissions: typeof updateUserPermissions;
|
||||||
|
addOrgUser: typeof addOrgUser;
|
||||||
|
updateOrgUserRole: typeof updateOrgUserRole;
|
||||||
|
deleteOrgUser: typeof deleteOrgUser;
|
||||||
|
syncLdapUser: typeof syncLdapUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
// isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserAdminPage extends PureComponent<Props, State> {
|
||||||
|
state = {
|
||||||
|
// isLoading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const { userId, loadAdminUserPage } = this.props;
|
||||||
|
loadAdminUserPage(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserUpdate = (user: UserDTO) => {
|
||||||
|
this.props.updateUser(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
onPasswordChange = (password: string) => {
|
||||||
|
const { userId, setUserPassword } = this.props;
|
||||||
|
setUserPassword(userId, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserDelete = (userId: number) => {
|
||||||
|
this.props.deleteUser(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserDisable = (userId: number) => {
|
||||||
|
this.props.disableUser(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserEnable = (userId: number) => {
|
||||||
|
this.props.enableUser(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onGrafanaAdminChange = (isGrafanaAdmin: boolean) => {
|
||||||
|
const { userId, updateUserPermissions } = this.props;
|
||||||
|
updateUserPermissions(userId, isGrafanaAdmin);
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRemove = (orgId: number) => {
|
||||||
|
const { userId, deleteOrgUser } = this.props;
|
||||||
|
deleteOrgUser(userId, orgId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRoleChange = (orgId: number, newRole: string) => {
|
||||||
|
const { userId, updateOrgUserRole } = this.props;
|
||||||
|
updateOrgUserRole(userId, orgId, newRole);
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgAdd = (orgId: number, role: string) => {
|
||||||
|
const { user, addOrgUser } = this.props;
|
||||||
|
addOrgUser(user, orgId, role);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSessionRevoke = (tokenId: number) => {
|
||||||
|
const { userId, revokeSession } = this.props;
|
||||||
|
revokeSession(tokenId, userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onAllSessionsRevoke = () => {
|
||||||
|
const { userId, revokeAllSessions } = this.props;
|
||||||
|
revokeAllSessions(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserSync = () => {
|
||||||
|
const { userId, syncLdapUser } = this.props;
|
||||||
|
syncLdapUser(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { navModel, user, orgs, sessions, ldapSyncInfo, isLoading } = this.props;
|
||||||
|
// const { isLoading } = this.state;
|
||||||
|
const isLDAPUser = user && user.isExternal && user.authLabels && user.authLabels.includes('LDAP');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navModel={navModel}>
|
||||||
|
<Page.Contents isLoading={isLoading}>
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<UserProfile
|
||||||
|
user={user}
|
||||||
|
onUserUpdate={this.onUserUpdate}
|
||||||
|
onUserDelete={this.onUserDelete}
|
||||||
|
onUserDisable={this.onUserDisable}
|
||||||
|
onUserEnable={this.onUserEnable}
|
||||||
|
onPasswordChange={this.onPasswordChange}
|
||||||
|
/>
|
||||||
|
{isLDAPUser && config.buildInfo.isEnterprise && ldapSyncInfo && (
|
||||||
|
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} />
|
||||||
|
)}
|
||||||
|
<UserPermissions isGrafanaAdmin={user.isGrafanaAdmin} onGrafanaAdminChange={this.onGrafanaAdminChange} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{orgs && (
|
||||||
|
<UserOrgs
|
||||||
|
orgs={orgs}
|
||||||
|
onOrgRemove={this.onOrgRemove}
|
||||||
|
onOrgRoleChange={this.onOrgRoleChange}
|
||||||
|
onOrgAdd={this.onOrgAdd}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessions && (
|
||||||
|
<UserSessions
|
||||||
|
sessions={sessions}
|
||||||
|
onSessionRevoke={this.onSessionRevoke}
|
||||||
|
onAllSessionsRevoke={this.onAllSessionsRevoke}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Page.Contents>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
|
userId: getRouteParamsId(state.location),
|
||||||
|
navModel: getNavModel(state.navIndex, 'global-users'),
|
||||||
|
user: state.userAdmin.user,
|
||||||
|
sessions: state.userAdmin.sessions,
|
||||||
|
orgs: state.userAdmin.orgs,
|
||||||
|
ldapSyncInfo: state.ldap.syncInfo,
|
||||||
|
isLoading: state.userAdmin.isLoading,
|
||||||
|
error: state.userAdmin.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadAdminUserPage,
|
||||||
|
updateUser,
|
||||||
|
setUserPassword,
|
||||||
|
disableUser,
|
||||||
|
enableUser,
|
||||||
|
deleteUser,
|
||||||
|
updateUserPermissions,
|
||||||
|
addOrgUser,
|
||||||
|
updateOrgUserRole,
|
||||||
|
deleteOrgUser,
|
||||||
|
revokeSession,
|
||||||
|
revokeAllSessions,
|
||||||
|
syncLdapUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UserAdminPage));
|
@ -1,36 +0,0 @@
|
|||||||
import React, { FC } from 'react';
|
|
||||||
import { User } from 'app/types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserInfo: FC<Props> = ({ user }) => {
|
|
||||||
return (
|
|
||||||
<div className="gf-form-group">
|
|
||||||
<div className="gf-form">
|
|
||||||
<table className="filter-table form-inline">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colSpan={2}>User information</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td className="width-16">Name</td>
|
|
||||||
<td>{user.name}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="width-16">Username</td>
|
|
||||||
<td>{user.login}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="width-16">Email</td>
|
|
||||||
<td>{user.email}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
82
public/app/features/admin/UserLdapSyncInfo.tsx
Normal file
82
public/app/features/admin/UserLdapSyncInfo.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { dateTime } from '@grafana/data';
|
||||||
|
import { SyncInfo, UserDTO } from 'app/types';
|
||||||
|
import { Button, LinkButton } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ldapSyncInfo: SyncInfo;
|
||||||
|
user: UserDTO;
|
||||||
|
onUserSync: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
||||||
|
const debugLDAPMappingBaseURL = '/admin/ldap';
|
||||||
|
|
||||||
|
export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
||||||
|
onUserSync = () => {
|
||||||
|
this.props.onUserSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { ldapSyncInfo, user } = this.props;
|
||||||
|
const nextSyncTime = dateTime(ldapSyncInfo.nextSync).format(syncTimeFormat);
|
||||||
|
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
||||||
|
const prevSyncTime = prevSyncSuccessful ? dateTime(ldapSyncInfo.prevSync.started).format(syncTimeFormat) : '';
|
||||||
|
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?user=${user && user.login}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="page-heading">LDAP Synchronisation</h3>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>External sync</td>
|
||||||
|
<td>User synced via LDAP – some changes must be done in LDAP or mappings.</td>
|
||||||
|
<td>
|
||||||
|
<span className="label label-tag">LDAP</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{ldapSyncInfo.enabled ? (
|
||||||
|
<>
|
||||||
|
<td>Next scheduled synchronisation</td>
|
||||||
|
<td colSpan={2}>{nextSyncTime}</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td>Next scheduled synchronisation</td>
|
||||||
|
<td colSpan={2}>Not enabled</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{prevSyncSuccessful ? (
|
||||||
|
<>
|
||||||
|
<td>Last synchronisation</td>
|
||||||
|
<td>{prevSyncTime}</td>
|
||||||
|
<td>Successful</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<td colSpan={3}>Last synchronisation</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-button-row">
|
||||||
|
<Button variant="secondary" onClick={this.onUserSync}>
|
||||||
|
Sync user
|
||||||
|
</Button>
|
||||||
|
<LinkButton variant="inverse" href={debugLDAPMappingURL}>
|
||||||
|
Debug LDAP Mapping
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
271
public/app/features/admin/UserOrgs.tsx
Normal file
271
public/app/features/admin/UserOrgs.tsx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { Modal, Themeable, stylesFactory, withTheme, ConfirmButton, Forms } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { UserOrg, Organization } from 'app/types';
|
||||||
|
import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orgs: UserOrg[];
|
||||||
|
|
||||||
|
onOrgRemove: (orgId: number) => void;
|
||||||
|
onOrgRoleChange: (orgId: number, newRole: string) => void;
|
||||||
|
onOrgAdd: (orgId: number, role: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showAddOrgModal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserOrgs extends PureComponent<Props, State> {
|
||||||
|
state = {
|
||||||
|
showAddOrgModal: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
showOrgAddModal = (show: boolean) => () => {
|
||||||
|
this.setState({ showAddOrgModal: show });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { orgs, onOrgRoleChange, onOrgRemove, onOrgAdd } = this.props;
|
||||||
|
const { showAddOrgModal } = this.state;
|
||||||
|
const addToOrgContainerClass = css`
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="page-heading">Organisations</h3>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
|
<tbody>
|
||||||
|
{orgs.map((org, index) => (
|
||||||
|
<OrgRow
|
||||||
|
key={`${org.orgId}-${index}`}
|
||||||
|
org={org}
|
||||||
|
onOrgRoleChange={onOrgRoleChange}
|
||||||
|
onOrgRemove={onOrgRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className={addToOrgContainerClass}>
|
||||||
|
<Forms.Button variant="secondary" onClick={this.showOrgAddModal(true)}>
|
||||||
|
Add user to organization
|
||||||
|
</Forms.Button>
|
||||||
|
</div>
|
||||||
|
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.showOrgAddModal(false)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ORG_ROLES = ['Viewer', 'Editor', 'Admin'];
|
||||||
|
|
||||||
|
const getOrgRowStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
removeButton: css`
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: ${theme.colors.blue95};
|
||||||
|
`,
|
||||||
|
label: css`
|
||||||
|
font-weight: 500;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
interface OrgRowProps extends Themeable {
|
||||||
|
org: UserOrg;
|
||||||
|
onOrgRemove: (orgId: number) => void;
|
||||||
|
onOrgRoleChange: (orgId: number, newRole: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrgRowState {
|
||||||
|
currentRole: string;
|
||||||
|
isChangingRole: boolean;
|
||||||
|
isRemovingFromOrg: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
||||||
|
state = {
|
||||||
|
currentRole: this.props.org.role,
|
||||||
|
isChangingRole: false,
|
||||||
|
isRemovingFromOrg: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRemove = () => {
|
||||||
|
const { org } = this.props;
|
||||||
|
this.props.onOrgRemove(org.orgId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeRoleClick = () => {
|
||||||
|
const { org } = this.props;
|
||||||
|
this.setState({ isChangingRole: true, currentRole: org.role });
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRemoveClick = () => {
|
||||||
|
this.setState({ isRemovingFromOrg: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRoleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const newRole = event.target.value;
|
||||||
|
this.setState({ currentRole: newRole });
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRoleSave = () => {
|
||||||
|
this.props.onOrgRoleChange(this.props.org.orgId, this.state.currentRole);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelClick = () => {
|
||||||
|
this.setState({ isChangingRole: false, isRemovingFromOrg: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { org, theme } = this.props;
|
||||||
|
const { currentRole, isChangingRole, isRemovingFromOrg } = this.state;
|
||||||
|
const styles = getOrgRowStyles(theme);
|
||||||
|
const labelClass = cx('width-16', styles.label);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={labelClass}>{org.name}</td>
|
||||||
|
{isChangingRole ? (
|
||||||
|
<td>
|
||||||
|
<div className="gf-form-select-wrapper width-8">
|
||||||
|
<select value={currentRole} className="gf-form-input" onChange={this.onOrgRoleChange}>
|
||||||
|
{ORG_ROLES.map((option, index) => {
|
||||||
|
return (
|
||||||
|
<option value={option} key={`${option}-${index}`}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td className="width-25">{org.role}</td>
|
||||||
|
)}
|
||||||
|
{!isRemovingFromOrg && (
|
||||||
|
<td colSpan={isChangingRole ? 2 : 1}>
|
||||||
|
<div className="pull-right">
|
||||||
|
<ConfirmButton
|
||||||
|
confirmText="Save"
|
||||||
|
onClick={this.onChangeRoleClick}
|
||||||
|
onCancel={this.onCancelClick}
|
||||||
|
onConfirm={this.onOrgRoleSave}
|
||||||
|
>
|
||||||
|
Change role
|
||||||
|
</ConfirmButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{!isChangingRole && (
|
||||||
|
<td colSpan={isRemovingFromOrg ? 2 : 1}>
|
||||||
|
<div className="pull-right">
|
||||||
|
<ConfirmButton
|
||||||
|
confirmText="Confirm removal"
|
||||||
|
confirmVariant="danger"
|
||||||
|
onClick={this.onOrgRemoveClick}
|
||||||
|
onCancel={this.onCancelClick}
|
||||||
|
onConfirm={this.onOrgRemove}
|
||||||
|
>
|
||||||
|
Remove from organisation
|
||||||
|
</ConfirmButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrgRow = withTheme(UnThemedOrgRow);
|
||||||
|
|
||||||
|
const getAddToOrgModalStyles = stylesFactory(() => ({
|
||||||
|
modal: css`
|
||||||
|
width: 500px;
|
||||||
|
`,
|
||||||
|
buttonRow: css`
|
||||||
|
text-align: center;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface AddToOrgModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOrgAdd(orgId: number, role: string): void;
|
||||||
|
onDismiss?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddToOrgModalState {
|
||||||
|
selectedOrg: Organization;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgModalState> {
|
||||||
|
state: AddToOrgModalState = {
|
||||||
|
selectedOrg: null,
|
||||||
|
role: 'Admin',
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgSelect = (org: OrgSelectItem) => {
|
||||||
|
this.setState({ selectedOrg: { ...org } });
|
||||||
|
};
|
||||||
|
|
||||||
|
onOrgRoleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
this.setState({
|
||||||
|
role: event.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddUserToOrg = () => {
|
||||||
|
const { selectedOrg, role } = this.state;
|
||||||
|
this.props.onOrgAdd(selectedOrg.id, role);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel = () => {
|
||||||
|
this.props.onDismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isOpen } = this.props;
|
||||||
|
const { role } = this.state;
|
||||||
|
const styles = getAddToOrgModalStyles();
|
||||||
|
const buttonRowClass = cx('gf-form-button-row', styles.buttonRow);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className={styles.modal} title="Add to an organization" isOpen={isOpen} onDismiss={this.onCancel}>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<h6 className="">Organisation</h6>
|
||||||
|
<OrgPicker className="width-25" onSelected={this.onOrgSelect} />
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<h6 className="">Role</h6>
|
||||||
|
<div className="gf-form-select-wrapper width-16">
|
||||||
|
<select value={role} className="gf-form-input" onChange={this.onOrgRoleChange}>
|
||||||
|
{ORG_ROLES.map((option, index) => {
|
||||||
|
return (
|
||||||
|
<option value={option} key={`${option}-${index}`}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={buttonRowClass}>
|
||||||
|
<Forms.Button variant="primary" onClick={this.onAddUserToOrg}>
|
||||||
|
Add to organization
|
||||||
|
</Forms.Button>
|
||||||
|
<Forms.Button variant="secondary" onClick={this.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Forms.Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
107
public/app/features/admin/UserPermissions.tsx
Normal file
107
public/app/features/admin/UserPermissions.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { ConfirmButton } from '@grafana/ui';
|
||||||
|
import { cx } from 'emotion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isGrafanaAdmin: boolean;
|
||||||
|
|
||||||
|
onGrafanaAdminChange: (isGrafanaAdmin: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isEditing: boolean;
|
||||||
|
currentAdminOption: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserPermissions extends PureComponent<Props, State> {
|
||||||
|
state = {
|
||||||
|
isEditing: false,
|
||||||
|
currentAdminOption: this.props.isGrafanaAdmin ? 'YES' : 'NO',
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeClick = () => {
|
||||||
|
this.setState({ isEditing: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelClick = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditing: false,
|
||||||
|
currentAdminOption: this.props.isGrafanaAdmin ? 'YES' : 'NO',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onGrafanaAdminChange = () => {
|
||||||
|
const { currentAdminOption } = this.state;
|
||||||
|
const newIsGrafanaAdmin = currentAdminOption === 'YES' ? true : false;
|
||||||
|
this.props.onGrafanaAdminChange(newIsGrafanaAdmin);
|
||||||
|
};
|
||||||
|
|
||||||
|
onAdminOptionSelect = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
this.setState({ currentAdminOption: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isGrafanaAdmin } = this.props;
|
||||||
|
const { isEditing, currentAdminOption } = this.state;
|
||||||
|
const changeButtonContainerClass = cx('pull-right');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="page-heading">Permissions</h3>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="width-16">Grafana Admin</td>
|
||||||
|
{isEditing ? (
|
||||||
|
<td colSpan={2}>
|
||||||
|
<div className="gf-form-select-wrapper width-8">
|
||||||
|
<select
|
||||||
|
value={currentAdminOption}
|
||||||
|
className="gf-form-input"
|
||||||
|
onChange={this.onAdminOptionSelect}
|
||||||
|
>
|
||||||
|
{['YES', 'NO'].map((option, index) => {
|
||||||
|
return (
|
||||||
|
<option value={option} key={`${option}-${index}`}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td colSpan={2}>
|
||||||
|
{isGrafanaAdmin ? (
|
||||||
|
<>
|
||||||
|
<i className="gicon gicon-shield" /> Yes
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>No</>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<div className={changeButtonContainerClass}>
|
||||||
|
<ConfirmButton
|
||||||
|
className="pull-right"
|
||||||
|
onClick={this.onChangeClick}
|
||||||
|
onConfirm={this.onGrafanaAdminChange}
|
||||||
|
onCancel={this.onCancelClick}
|
||||||
|
confirmText="Change"
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</ConfirmButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
329
public/app/features/admin/UserProfile.tsx
Normal file
329
public/app/features/admin/UserProfile.tsx
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
import React, { PureComponent, FC } from 'react';
|
||||||
|
import { UserDTO } from 'app/types';
|
||||||
|
import { cx, css } from 'emotion';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { ConfirmButton, Input, ConfirmModal, InputStatus, Forms, stylesFactory } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: UserDTO;
|
||||||
|
|
||||||
|
onUserUpdate: (user: UserDTO) => void;
|
||||||
|
onUserDelete: (userId: number) => void;
|
||||||
|
onUserDisable: (userId: number) => void;
|
||||||
|
onUserEnable: (userId: number) => void;
|
||||||
|
onPasswordChange(password: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isLoading: boolean;
|
||||||
|
showDeleteModal: boolean;
|
||||||
|
showDisableModal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserProfile extends PureComponent<Props, State> {
|
||||||
|
state = {
|
||||||
|
isLoading: false,
|
||||||
|
showDeleteModal: false,
|
||||||
|
showDisableModal: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
showDeleteUserModal = (show: boolean) => () => {
|
||||||
|
this.setState({ showDeleteModal: show });
|
||||||
|
};
|
||||||
|
|
||||||
|
showDisableUserModal = (show: boolean) => () => {
|
||||||
|
this.setState({ showDisableModal: show });
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserDelete = () => {
|
||||||
|
const { user, onUserDelete } = this.props;
|
||||||
|
onUserDelete(user.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserDisable = () => {
|
||||||
|
const { user, onUserDisable } = this.props;
|
||||||
|
onUserDisable(user.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserEnable = () => {
|
||||||
|
const { user, onUserEnable } = this.props;
|
||||||
|
onUserEnable(user.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserNameChange = (newValue: string) => {
|
||||||
|
const { user, onUserUpdate } = this.props;
|
||||||
|
onUserUpdate({
|
||||||
|
...user,
|
||||||
|
name: newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserEmailChange = (newValue: string) => {
|
||||||
|
const { user, onUserUpdate } = this.props;
|
||||||
|
onUserUpdate({
|
||||||
|
...user,
|
||||||
|
email: newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserLoginChange = (newValue: string) => {
|
||||||
|
const { user, onUserUpdate } = this.props;
|
||||||
|
onUserUpdate({
|
||||||
|
...user,
|
||||||
|
login: newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onPasswordChange = (newValue: string) => {
|
||||||
|
this.props.onPasswordChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user } = this.props;
|
||||||
|
const { showDeleteModal, showDisableModal } = this.state;
|
||||||
|
const lockMessage = 'Synced via LDAP';
|
||||||
|
const styles = getStyles(config.theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="page-heading">User information</h3>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
|
<tbody>
|
||||||
|
<UserProfileRow
|
||||||
|
label="Name"
|
||||||
|
value={user.name}
|
||||||
|
locked={user.isExternal}
|
||||||
|
lockMessage={lockMessage}
|
||||||
|
onChange={this.onUserNameChange}
|
||||||
|
/>
|
||||||
|
<UserProfileRow
|
||||||
|
label="Email"
|
||||||
|
value={user.email}
|
||||||
|
locked={user.isExternal}
|
||||||
|
lockMessage={lockMessage}
|
||||||
|
onChange={this.onUserEmailChange}
|
||||||
|
/>
|
||||||
|
<UserProfileRow
|
||||||
|
label="Username"
|
||||||
|
value={user.login}
|
||||||
|
locked={user.isExternal}
|
||||||
|
lockMessage={lockMessage}
|
||||||
|
onChange={this.onUserLoginChange}
|
||||||
|
/>
|
||||||
|
<UserProfileRow
|
||||||
|
label="Password"
|
||||||
|
value="********"
|
||||||
|
inputType="password"
|
||||||
|
locked={user.isExternal}
|
||||||
|
lockMessage={lockMessage}
|
||||||
|
onChange={this.onPasswordChange}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonRow}>
|
||||||
|
<Forms.Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
|
||||||
|
Delete User
|
||||||
|
</Forms.Button>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={showDeleteModal}
|
||||||
|
title="Delete user"
|
||||||
|
body="Are you sure you want to delete this user?"
|
||||||
|
confirmText="Delete user"
|
||||||
|
onConfirm={this.onUserDelete}
|
||||||
|
onDismiss={this.showDeleteUserModal(false)}
|
||||||
|
/>
|
||||||
|
{user.isDisabled ? (
|
||||||
|
<Forms.Button variant="secondary" onClick={this.onUserEnable}>
|
||||||
|
Enable User
|
||||||
|
</Forms.Button>
|
||||||
|
) : (
|
||||||
|
<Forms.Button variant="secondary" onClick={this.showDisableUserModal(true)}>
|
||||||
|
Disable User
|
||||||
|
</Forms.Button>
|
||||||
|
)}
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={showDisableModal}
|
||||||
|
title="Disable user"
|
||||||
|
body="Are you sure you want to disable this user?"
|
||||||
|
confirmText="Disable user"
|
||||||
|
onConfirm={this.onUserDisable}
|
||||||
|
onDismiss={this.showDisableUserModal(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
buttonRow: css`
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
> * {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UserProfileRowProps {
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
locked?: boolean;
|
||||||
|
lockMessage?: string;
|
||||||
|
inputType?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserProfileRowState {
|
||||||
|
value: string;
|
||||||
|
editing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfileRowState> {
|
||||||
|
inputElem: HTMLInputElement;
|
||||||
|
|
||||||
|
static defaultProps: Partial<UserProfileRowProps> = {
|
||||||
|
value: '',
|
||||||
|
locked: false,
|
||||||
|
lockMessage: '',
|
||||||
|
inputType: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
editing: false,
|
||||||
|
value: this.props.value || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
setInputElem = (elem: any) => {
|
||||||
|
this.inputElem = elem;
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditClick = () => {
|
||||||
|
if (this.props.inputType === 'password') {
|
||||||
|
// Reset value for password field
|
||||||
|
this.setState({ editing: true, value: '' }, this.focusInput);
|
||||||
|
} else {
|
||||||
|
this.setState({ editing: true }, this.focusInput);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelClick = () => {
|
||||||
|
this.setState({ editing: false, value: this.props.value || '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInputChange = (event: React.ChangeEvent<HTMLInputElement>, status?: InputStatus) => {
|
||||||
|
if (status === InputStatus.Invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ value: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInputBlur = (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => {
|
||||||
|
if (status === InputStatus.Invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ value: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
focusInput = () => {
|
||||||
|
if (this.inputElem && this.inputElem.focus) {
|
||||||
|
this.inputElem.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSave = () => {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(this.state.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, locked, lockMessage, inputType } = this.props;
|
||||||
|
const { value } = this.state;
|
||||||
|
const labelClass = cx(
|
||||||
|
'width-16',
|
||||||
|
css`
|
||||||
|
font-weight: 500;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const editButtonContainerClass = cx('pull-right');
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={labelClass}>{label}</td>
|
||||||
|
<td className="width-25" colSpan={2}>
|
||||||
|
{this.state.editing ? (
|
||||||
|
<Input
|
||||||
|
className="width-20"
|
||||||
|
type={inputType}
|
||||||
|
defaultValue={value}
|
||||||
|
onBlur={this.onInputBlur}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
inputRef={this.setInputElem}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{this.props.value}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={editButtonContainerClass}>
|
||||||
|
<ConfirmButton
|
||||||
|
confirmText="Save"
|
||||||
|
onClick={this.onEditClick}
|
||||||
|
onConfirm={this.onSave}
|
||||||
|
onCancel={this.onCancelClick}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</ConfirmButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LockedRowProps {
|
||||||
|
label: string;
|
||||||
|
value?: any;
|
||||||
|
lockMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LockedRow: FC<LockedRowProps> = ({ label, value, lockMessage }) => {
|
||||||
|
const lockMessageClass = cx(
|
||||||
|
'pull-right',
|
||||||
|
css`
|
||||||
|
font-style: italic;
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const labelClass = cx(
|
||||||
|
'width-16',
|
||||||
|
css`
|
||||||
|
font-weight: 500;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={labelClass}>{label}</td>
|
||||||
|
<td className="width-25" colSpan={2}>
|
||||||
|
{value}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={lockMessageClass}>{lockMessage}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { ConfirmButton, ConfirmModal, Forms } from '@grafana/ui';
|
||||||
import { UserSession } from 'app/types';
|
import { UserSession } from 'app/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -8,19 +10,37 @@ interface Props {
|
|||||||
onAllSessionsRevoke: () => void;
|
onAllSessionsRevoke: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserSessions extends PureComponent<Props> {
|
interface State {
|
||||||
handleSessionRevoke = (id: number) => {
|
showLogoutModal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserSessions extends PureComponent<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
showLogoutModal: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
showLogoutConfirmationModal = (show: boolean) => () => {
|
||||||
|
this.setState({ showLogoutModal: show });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSessionRevoke = (id: number) => {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.onSessionRevoke(id);
|
this.props.onSessionRevoke(id);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
handleAllSessionsRevoke = () => {
|
onAllSessionsRevoke = () => {
|
||||||
|
this.setState({ showLogoutModal: false });
|
||||||
this.props.onAllSessionsRevoke();
|
this.props.onAllSessionsRevoke();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { sessions } = this.props;
|
const { sessions } = this.props;
|
||||||
|
const { showLogoutModal } = this.state;
|
||||||
|
|
||||||
|
const logoutFromAllDevicesClass = css`
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -45,21 +65,35 @@ export class UserSessions extends PureComponent<Props> {
|
|||||||
<td>{session.clientIp}</td>
|
<td>{session.clientIp}</td>
|
||||||
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="btn btn-danger btn-small" onClick={this.handleSessionRevoke(session.id)}>
|
<div className="pull-right">
|
||||||
<i className="fa fa-power-off" />
|
<ConfirmButton
|
||||||
</button>
|
confirmText="Confirm logout"
|
||||||
|
confirmVariant="danger"
|
||||||
|
onConfirm={this.onSessionRevoke(session.id)}
|
||||||
|
>
|
||||||
|
Force logout
|
||||||
|
</ConfirmButton>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form-button-row">
|
<div className={logoutFromAllDevicesClass}>
|
||||||
{sessions.length > 0 && (
|
{sessions.length > 0 && (
|
||||||
<button className="btn btn-danger" onClick={this.handleAllSessionsRevoke}>
|
<Forms.Button variant="secondary" onClick={this.showLogoutConfirmationModal(true)}>
|
||||||
Logout user from all devices
|
Force logout from all devices
|
||||||
</button>
|
</Forms.Button>
|
||||||
)}
|
)}
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={showLogoutModal}
|
||||||
|
title="Force logout from all devices"
|
||||||
|
body="Are you sure you want to force logout from all devices?"
|
||||||
|
confirmText="Force logout"
|
||||||
|
onConfirm={this.onAllSessionsRevoke}
|
||||||
|
onDismiss={this.showLogoutConfirmationModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -19,7 +19,7 @@ export class UserSyncInfo extends PureComponent<Props, State> {
|
|||||||
isSyncing: false,
|
isSyncing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSyncClick = async () => {
|
onSyncClick = async () => {
|
||||||
const { onSync } = this.props;
|
const { onSync } = this.props;
|
||||||
this.setState({ isSyncing: true });
|
this.setState({ isSyncing: true });
|
||||||
try {
|
try {
|
||||||
@ -41,7 +41,7 @@ export class UserSyncInfo extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button className={`btn btn-secondary pull-right`} onClick={this.handleSyncClick} disabled={isDisabled}>
|
<button className={`btn btn-secondary pull-right`} onClick={this.onSyncClick} disabled={isDisabled}>
|
||||||
<span className="btn-title">Sync user</span>
|
<span className="btn-title">Sync user</span>
|
||||||
{isSyncing && <i className="fa fa-spinner fa-fw fa-spin run-icon" />}
|
{isSyncing && <i className="fa fa-spinner fa-fw fa-spin run-icon" />}
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,6 +25,7 @@ interface Props {
|
|||||||
ldapSyncInfo: SyncInfo;
|
ldapSyncInfo: SyncInfo;
|
||||||
ldapError: LdapError;
|
ldapError: LdapError;
|
||||||
userError?: LdapError;
|
userError?: LdapError;
|
||||||
|
username?: string;
|
||||||
|
|
||||||
loadLdapState: typeof loadLdapState;
|
loadLdapState: typeof loadLdapState;
|
||||||
loadLdapSyncStatus: typeof loadLdapSyncStatus;
|
loadLdapSyncStatus: typeof loadLdapSyncStatus;
|
||||||
@ -43,8 +44,12 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
await this.props.clearUserMappingInfo();
|
const { username, clearUserMappingInfo, loadUserMapping } = this.props;
|
||||||
|
await clearUserMappingInfo();
|
||||||
await this.fetchLDAPStatus();
|
await this.fetchLDAPStatus();
|
||||||
|
if (username) {
|
||||||
|
await loadUserMapping(username);
|
||||||
|
}
|
||||||
this.setState({ isLoading: false });
|
this.setState({ isLoading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +76,7 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ldapUser, userError, ldapError, ldapSyncInfo, ldapConnectionInfo, navModel } = this.props;
|
const { ldapUser, userError, ldapError, ldapSyncInfo, ldapConnectionInfo, navModel, username } = this.props;
|
||||||
const { isLoading } = this.state;
|
const { isLoading } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -91,7 +96,15 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
<h3 className="page-heading">Test user mapping</h3>
|
<h3 className="page-heading">Test user mapping</h3>
|
||||||
<div className="gf-form-group">
|
<div className="gf-form-group">
|
||||||
<form onSubmit={this.search} className="gf-form-inline">
|
<form onSubmit={this.search} className="gf-form-inline">
|
||||||
<FormField label="Username" labelWidth={8} inputWidth={30} type="text" id="username" name="username" />
|
<FormField
|
||||||
|
label="Username"
|
||||||
|
labelWidth={8}
|
||||||
|
inputWidth={30}
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
defaultValue={username}
|
||||||
|
/>
|
||||||
<button type="submit" className="btn btn-primary">
|
<button type="submit" className="btn btn-primary">
|
||||||
Run
|
Run
|
||||||
</button>
|
</button>
|
||||||
@ -117,6 +130,7 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
navModel: getNavModel(state.navIndex, 'ldap'),
|
navModel: getNavModel(state.navIndex, 'ldap'),
|
||||||
|
username: state.location.routeParams.user,
|
||||||
ldapConnectionInfo: state.ldap.connectionInfo,
|
ldapConnectionInfo: state.ldap.connectionInfo,
|
||||||
ldapUser: state.ldap.user,
|
ldapUser: state.ldap.user,
|
||||||
ldapSyncInfo: state.ldap.syncInfo,
|
ldapSyncInfo: state.ldap.syncInfo,
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { hot } from 'react-hot-loader';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { NavModel } from '@grafana/data';
|
|
||||||
import { Alert } from '@grafana/ui';
|
|
||||||
import Page from 'app/core/components/Page/Page';
|
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
|
||||||
import {
|
|
||||||
AppNotificationSeverity,
|
|
||||||
LdapError,
|
|
||||||
LdapUser,
|
|
||||||
StoreState,
|
|
||||||
User,
|
|
||||||
UserSession,
|
|
||||||
SyncInfo,
|
|
||||||
LdapUserSyncInfo,
|
|
||||||
} from 'app/types';
|
|
||||||
import {
|
|
||||||
clearUserError,
|
|
||||||
loadLdapUserInfo,
|
|
||||||
revokeSession,
|
|
||||||
revokeAllSessions,
|
|
||||||
loadLdapSyncStatus,
|
|
||||||
syncUser,
|
|
||||||
} from '../state/actions';
|
|
||||||
import { LdapUserInfo } from './LdapUserInfo';
|
|
||||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
|
||||||
import { UserSessions } from '../UserSessions';
|
|
||||||
import { UserInfo } from '../UserInfo';
|
|
||||||
import { UserSyncInfo } from '../UserSyncInfo';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
navModel: NavModel;
|
|
||||||
userId: number;
|
|
||||||
user: User;
|
|
||||||
sessions: UserSession[];
|
|
||||||
ldapUser: LdapUser;
|
|
||||||
userError?: LdapError;
|
|
||||||
ldapSyncInfo?: SyncInfo;
|
|
||||||
|
|
||||||
loadLdapUserInfo: typeof loadLdapUserInfo;
|
|
||||||
clearUserError: typeof clearUserError;
|
|
||||||
loadLdapSyncStatus: typeof loadLdapSyncStatus;
|
|
||||||
syncUser: typeof syncUser;
|
|
||||||
revokeSession: typeof revokeSession;
|
|
||||||
revokeAllSessions: typeof revokeAllSessions;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LdapUserPage extends PureComponent<Props, State> {
|
|
||||||
state = {
|
|
||||||
isLoading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const { userId, loadLdapUserInfo, loadLdapSyncStatus } = this.props;
|
|
||||||
try {
|
|
||||||
await loadLdapUserInfo(userId);
|
|
||||||
await loadLdapSyncStatus();
|
|
||||||
} finally {
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClearUserError = () => {
|
|
||||||
this.props.clearUserError();
|
|
||||||
};
|
|
||||||
|
|
||||||
onSyncUser = () => {
|
|
||||||
const { syncUser, user } = this.props;
|
|
||||||
if (syncUser && user) {
|
|
||||||
syncUser(user.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onSessionRevoke = (tokenId: number) => {
|
|
||||||
const { userId, revokeSession } = this.props;
|
|
||||||
revokeSession(tokenId, userId);
|
|
||||||
};
|
|
||||||
|
|
||||||
onAllSessionsRevoke = () => {
|
|
||||||
const { userId, revokeAllSessions } = this.props;
|
|
||||||
revokeAllSessions(userId);
|
|
||||||
};
|
|
||||||
|
|
||||||
isUserError = (): boolean => {
|
|
||||||
return !!(this.props.userError && this.props.userError.title);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { user, ldapUser, userError, navModel, sessions, ldapSyncInfo } = this.props;
|
|
||||||
const { isLoading } = this.state;
|
|
||||||
|
|
||||||
const userSyncInfo: LdapUserSyncInfo = {};
|
|
||||||
if (ldapSyncInfo) {
|
|
||||||
userSyncInfo.nextSync = ldapSyncInfo.nextSync;
|
|
||||||
}
|
|
||||||
if (user) {
|
|
||||||
userSyncInfo.prevSync = (user as any).updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page navModel={navModel}>
|
|
||||||
<Page.Contents isLoading={isLoading}>
|
|
||||||
<div className="grafana-info-box">
|
|
||||||
This user is synced via LDAP – All changes must be done in LDAP or mappings.
|
|
||||||
</div>
|
|
||||||
{userError && userError.title && (
|
|
||||||
<div className="gf-form-group">
|
|
||||||
<Alert
|
|
||||||
title={userError.title}
|
|
||||||
severity={AppNotificationSeverity.Error}
|
|
||||||
children={userError.body}
|
|
||||||
onRemove={this.onClearUserError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{userSyncInfo && (
|
|
||||||
<UserSyncInfo syncInfo={userSyncInfo} onSync={this.onSyncUser} disableSync={this.isUserError()} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ldapUser && <LdapUserInfo ldapUser={ldapUser} />}
|
|
||||||
{!ldapUser && user && <UserInfo user={user} />}
|
|
||||||
|
|
||||||
{sessions && (
|
|
||||||
<UserSessions
|
|
||||||
sessions={sessions}
|
|
||||||
onSessionRevoke={this.onSessionRevoke}
|
|
||||||
onAllSessionsRevoke={this.onAllSessionsRevoke}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Page.Contents>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
|
||||||
userId: getRouteParamsId(state.location),
|
|
||||||
navModel: getNavModel(state.navIndex, 'global-users'),
|
|
||||||
user: state.ldapUser.user,
|
|
||||||
ldapUser: state.ldapUser.ldapUser,
|
|
||||||
userError: state.ldapUser.userError,
|
|
||||||
ldapSyncInfo: state.ldapUser.ldapSyncInfo,
|
|
||||||
sessions: state.ldapUser.sessions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
loadLdapUserInfo,
|
|
||||||
loadLdapSyncStatus,
|
|
||||||
syncUser,
|
|
||||||
revokeSession,
|
|
||||||
revokeAllSessions,
|
|
||||||
clearUserError,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LdapUserPage));
|
|
@ -1,34 +1,196 @@
|
|||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { ThunkResult } from 'app/types';
|
import { dateTime } from '@grafana/data';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getLdapState,
|
userAdminPageLoadedAction,
|
||||||
getLdapSyncStatus,
|
userProfileLoadedAction,
|
||||||
getUser,
|
userOrgsLoadedAction,
|
||||||
getUserInfo,
|
|
||||||
getUserSessions,
|
|
||||||
revokeAllUserSessions,
|
|
||||||
revokeUserSession,
|
|
||||||
syncLdapUser,
|
|
||||||
} from './apis';
|
|
||||||
import {
|
|
||||||
clearUserErrorAction,
|
|
||||||
clearUserMappingInfoAction,
|
|
||||||
ldapConnectionInfoLoadedAction,
|
|
||||||
ldapFailedAction,
|
|
||||||
ldapSyncStatusLoadedAction,
|
|
||||||
userLoadedAction,
|
|
||||||
userMappingInfoFailedAction,
|
|
||||||
userMappingInfoLoadedAction,
|
|
||||||
userSessionsLoadedAction,
|
userSessionsLoadedAction,
|
||||||
userSyncFailedAction,
|
userAdminPageFailedAction,
|
||||||
|
ldapConnectionInfoLoadedAction,
|
||||||
|
ldapSyncStatusLoadedAction,
|
||||||
|
userMappingInfoLoadedAction,
|
||||||
|
userMappingInfoFailedAction,
|
||||||
|
clearUserMappingInfoAction,
|
||||||
|
clearUserErrorAction,
|
||||||
|
ldapFailedAction,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
|
|
||||||
// Actions
|
// UserAdminPage
|
||||||
|
|
||||||
|
export function loadAdminUserPage(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
dispatch(userAdminPageLoadedAction(false));
|
||||||
|
await dispatch(loadUserProfile(userId));
|
||||||
|
await dispatch(loadUserOrgs(userId));
|
||||||
|
await dispatch(loadUserSessions(userId));
|
||||||
|
if (config.ldapEnabled && config.buildInfo.isEnterprise) {
|
||||||
|
await dispatch(loadLdapSyncStatus());
|
||||||
|
}
|
||||||
|
dispatch(userAdminPageLoadedAction(true));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
error.isHandled = true;
|
||||||
|
const userError = {
|
||||||
|
title: error.data.message,
|
||||||
|
body: error.data.error,
|
||||||
|
};
|
||||||
|
dispatch(userAdminPageFailedAction(userError));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadUserProfile(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const user = await getBackendSrv().get(`/api/users/${userId}`);
|
||||||
|
dispatch(userProfileLoadedAction(user));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUser(user: UserDTO): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().put(`/api/users/${user.id}`, user);
|
||||||
|
dispatch(loadAdminUserPage(user.id));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUserPassword(userId: number, password: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const payload = { password };
|
||||||
|
await getBackendSrv().put(`/api/admin/users/${userId}/password`, payload);
|
||||||
|
dispatch(loadAdminUserPage(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableUser(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().post(`/api/admin/users/${userId}/disable`);
|
||||||
|
// dispatch(loadAdminUserPage(userId));
|
||||||
|
dispatch(updateLocation({ path: '/admin/users' }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableUser(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().post(`/api/admin/users/${userId}/enable`);
|
||||||
|
dispatch(loadAdminUserPage(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteUser(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().delete(`/api/admin/users/${userId}`);
|
||||||
|
dispatch(updateLocation({ path: '/admin/users' }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserPermissions(userId: number, isGrafanaAdmin: boolean): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const payload = { isGrafanaAdmin };
|
||||||
|
await getBackendSrv().put(`/api/admin/users/${userId}/permissions`, payload);
|
||||||
|
dispatch(loadAdminUserPage(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadUserOrgs(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const orgs = await getBackendSrv().get(`/api/users/${userId}/orgs`);
|
||||||
|
dispatch(userOrgsLoadedAction(orgs));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addOrgUser(user: UserDTO, orgId: number, role: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const payload = {
|
||||||
|
loginOrEmail: user.login,
|
||||||
|
role: role,
|
||||||
|
};
|
||||||
|
await getBackendSrv().post(`/api/orgs/${orgId}/users/`, payload);
|
||||||
|
dispatch(loadAdminUserPage(user.id));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateOrgUserRole(userId: number, orgId: number, role: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const payload = { role };
|
||||||
|
await getBackendSrv().patch(`/api/orgs/${orgId}/users/${userId}`, payload);
|
||||||
|
dispatch(loadAdminUserPage(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteOrgUser(userId: number, orgId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().delete(`/api/orgs/${orgId}/users/${userId}`);
|
||||||
|
dispatch(loadAdminUserPage(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadUserSessions(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const tokens = await getBackendSrv().get(`/api/admin/users/${userId}/auth-tokens`);
|
||||||
|
tokens.reverse();
|
||||||
|
const sessions = tokens.map((session: UserSession) => {
|
||||||
|
return {
|
||||||
|
id: session.id,
|
||||||
|
isActive: session.isActive,
|
||||||
|
seenAt: dateTime(session.seenAt).fromNow(),
|
||||||
|
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
||||||
|
clientIp: session.clientIp,
|
||||||
|
browser: session.browser,
|
||||||
|
browserVersion: session.browserVersion,
|
||||||
|
os: session.os,
|
||||||
|
osVersion: session.osVersion,
|
||||||
|
device: session.device,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dispatch(userSessionsLoadedAction(sessions));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revokeSession(tokenId: number, userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const payload = { authTokenId: tokenId };
|
||||||
|
await getBackendSrv().post(`/api/admin/users/${userId}/revoke-auth-token`, payload);
|
||||||
|
dispatch(loadUserSessions(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revokeAllSessions(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().post(`/api/admin/users/${userId}/logout`);
|
||||||
|
dispatch(loadUserSessions(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP user actions
|
||||||
|
|
||||||
|
export function loadLdapSyncStatus(): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
// Available only in enterprise
|
||||||
|
if (config.buildInfo.isEnterprise) {
|
||||||
|
const syncStatus = await getBackendSrv().get(`/api/admin/ldap-sync-status`);
|
||||||
|
dispatch(ldapSyncStatusLoadedAction(syncStatus));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncLdapUser(userId: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().post(`/api/admin/ldap/sync/${userId}`);
|
||||||
|
dispatch(loadAdminUserPage(userId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP debug page
|
||||||
|
|
||||||
export function loadLdapState(): ThunkResult<void> {
|
export function loadLdapState(): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
try {
|
try {
|
||||||
const connectionInfo = await getLdapState();
|
const connectionInfo = await getBackendSrv().get(`/api/admin/ldap/status`);
|
||||||
dispatch(ldapConnectionInfoLoadedAction(connectionInfo));
|
dispatch(ldapConnectionInfoLoadedAction(connectionInfo));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.isHandled = true;
|
error.isHandled = true;
|
||||||
@ -41,20 +203,17 @@ export function loadLdapState(): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadLdapSyncStatus(): ThunkResult<void> {
|
|
||||||
return async dispatch => {
|
|
||||||
if (config.buildInfo.isEnterprise) {
|
|
||||||
// Available only in enterprise
|
|
||||||
const syncStatus = await getLdapSyncStatus();
|
|
||||||
dispatch(ldapSyncStatusLoadedAction(syncStatus));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadUserMapping(username: string): ThunkResult<void> {
|
export function loadUserMapping(username: string): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
try {
|
try {
|
||||||
const userInfo = await getUserInfo(username);
|
const response = await getBackendSrv().get(`/api/admin/ldap/${username}`);
|
||||||
|
const { name, surname, email, login, isGrafanaAdmin, isDisabled, roles, teams } = response;
|
||||||
|
const userInfo: LdapUser = {
|
||||||
|
info: { name, surname, email, login },
|
||||||
|
permissions: { isGrafanaAdmin, isDisabled },
|
||||||
|
roles,
|
||||||
|
teams,
|
||||||
|
};
|
||||||
dispatch(userMappingInfoLoadedAction(userInfo));
|
dispatch(userMappingInfoLoadedAction(userInfo));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.isHandled = true;
|
error.isHandled = true;
|
||||||
@ -80,54 +239,3 @@ export function clearUserMappingInfo(): ThunkResult<void> {
|
|||||||
dispatch(clearUserMappingInfoAction());
|
dispatch(clearUserMappingInfoAction());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncUser(userId: number): ThunkResult<void> {
|
|
||||||
return async dispatch => {
|
|
||||||
try {
|
|
||||||
await syncLdapUser(userId);
|
|
||||||
dispatch(loadLdapUserInfo(userId));
|
|
||||||
dispatch(loadLdapSyncStatus());
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(userSyncFailedAction());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadLdapUserInfo(userId: number): ThunkResult<void> {
|
|
||||||
return async dispatch => {
|
|
||||||
try {
|
|
||||||
const user = await getUser(userId);
|
|
||||||
dispatch(userLoadedAction(user));
|
|
||||||
dispatch(loadUserSessions(userId));
|
|
||||||
dispatch(loadUserMapping(user.login));
|
|
||||||
} catch (error) {
|
|
||||||
error.isHandled = true;
|
|
||||||
const userError = {
|
|
||||||
title: error.data.message,
|
|
||||||
body: error.data.error,
|
|
||||||
};
|
|
||||||
dispatch(userMappingInfoFailedAction(userError));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadUserSessions(userId: number): ThunkResult<void> {
|
|
||||||
return async dispatch => {
|
|
||||||
const sessions = await getUserSessions(userId);
|
|
||||||
dispatch(userSessionsLoadedAction(sessions));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function revokeSession(tokenId: number, userId: number): ThunkResult<void> {
|
|
||||||
return async dispatch => {
|
|
||||||
await revokeUserSession(tokenId, userId);
|
|
||||||
dispatch(loadUserSessions(userId));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function revokeAllSessions(userId: number): ThunkResult<void> {
|
|
||||||
return async dispatch => {
|
|
||||||
await revokeAllUserSessions(userId);
|
|
||||||
dispatch(loadUserSessions(userId));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { dateTime } from '@grafana/data';
|
|
||||||
import { LdapUser, LdapConnectionInfo, UserSession, SyncInfo, User } from 'app/types';
|
|
||||||
|
|
||||||
export interface ServerStat {
|
export interface ServerStat {
|
||||||
name: string;
|
name: string;
|
||||||
@ -33,60 +31,3 @@ export const getServerStats = async (): Promise<ServerStat[]> => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLdapState = async (): Promise<LdapConnectionInfo> => {
|
|
||||||
return await getBackendSrv().get(`/api/admin/ldap/status`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLdapSyncStatus = async (): Promise<SyncInfo> => {
|
|
||||||
return await getBackendSrv().get(`/api/admin/ldap-sync-status`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const syncLdapUser = async (userId: number) => {
|
|
||||||
return await getBackendSrv().post(`/api/admin/ldap/sync/${userId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserInfo = async (username: string): Promise<LdapUser> => {
|
|
||||||
const response = await getBackendSrv().get(`/api/admin/ldap/${username}`);
|
|
||||||
const { name, surname, email, login, isGrafanaAdmin, isDisabled, roles, teams } = response;
|
|
||||||
return {
|
|
||||||
info: { name, surname, email, login },
|
|
||||||
permissions: { isGrafanaAdmin, isDisabled },
|
|
||||||
roles,
|
|
||||||
teams,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUser = async (id: number): Promise<User> => {
|
|
||||||
return await getBackendSrv().get('/api/users/' + id);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserSessions = async (id: number) => {
|
|
||||||
const sessions = await getBackendSrv().get('/api/admin/users/' + id + '/auth-tokens');
|
|
||||||
sessions.reverse();
|
|
||||||
|
|
||||||
return sessions.map((session: UserSession) => {
|
|
||||||
return {
|
|
||||||
id: session.id,
|
|
||||||
isActive: session.isActive,
|
|
||||||
seenAt: dateTime(session.seenAt).fromNow(),
|
|
||||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
|
||||||
clientIp: session.clientIp,
|
|
||||||
browser: session.browser,
|
|
||||||
browserVersion: session.browserVersion,
|
|
||||||
os: session.os,
|
|
||||||
osVersion: session.osVersion,
|
|
||||||
device: session.device,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const revokeUserSession = async (tokenId: number, userId: number) => {
|
|
||||||
return await getBackendSrv().post(`/api/admin/users/${userId}/revoke-auth-token`, {
|
|
||||||
authTokenId: tokenId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const revokeAllUserSessions = async (userId: number) => {
|
|
||||||
return await getBackendSrv().post(`/api/admin/users/${userId}/logout`);
|
|
||||||
};
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
import {
|
import {
|
||||||
clearUserErrorAction,
|
|
||||||
clearUserMappingInfoAction,
|
clearUserMappingInfoAction,
|
||||||
ldapConnectionInfoLoadedAction,
|
ldapConnectionInfoLoadedAction,
|
||||||
ldapFailedAction,
|
ldapFailedAction,
|
||||||
ldapReducer,
|
ldapReducer,
|
||||||
ldapSyncStatusLoadedAction,
|
ldapSyncStatusLoadedAction,
|
||||||
ldapUserReducer,
|
userAdminReducer,
|
||||||
userLoadedAction,
|
userProfileLoadedAction,
|
||||||
userMappingInfoFailedAction,
|
userMappingInfoFailedAction,
|
||||||
userMappingInfoLoadedAction,
|
userMappingInfoLoadedAction,
|
||||||
userSessionsLoadedAction,
|
userSessionsLoadedAction,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { LdapState, LdapUser, LdapUserState, User } from 'app/types';
|
import { LdapState, LdapUser, UserAdminState, UserDTO } from 'app/types';
|
||||||
|
|
||||||
const makeInitialLdapState = (): LdapState => ({
|
const makeInitialLdapState = (): LdapState => ({
|
||||||
connectionInfo: [],
|
connectionInfo: [],
|
||||||
@ -23,11 +22,11 @@ const makeInitialLdapState = (): LdapState => ({
|
|||||||
userError: null,
|
userError: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeInitialLdapUserState = (): LdapUserState => ({
|
const makeInitialUserAdminState = (): UserAdminState => ({
|
||||||
user: null,
|
user: null,
|
||||||
ldapUser: null,
|
|
||||||
ldapSyncInfo: null,
|
|
||||||
sessions: [],
|
sessions: [],
|
||||||
|
orgs: [],
|
||||||
|
isLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTestUserMapping = (): LdapUser => ({
|
const getTestUserMapping = (): LdapUser => ({
|
||||||
@ -45,13 +44,14 @@ const getTestUserMapping = (): LdapUser => ({
|
|||||||
teams: [],
|
teams: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTestUser = (): User => ({
|
const getTestUser = (): UserDTO => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'user@localhost',
|
email: 'user@localhost',
|
||||||
login: 'user',
|
login: 'user',
|
||||||
name: 'User',
|
name: 'User',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
label: '',
|
isGrafanaAdmin: false,
|
||||||
|
isDisabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LDAP page reducer', () => {
|
describe('LDAP page reducer', () => {
|
||||||
@ -203,32 +203,28 @@ describe('LDAP page reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Edit LDAP user page reducer', () => {
|
describe('Edit Admin user page reducer', () => {
|
||||||
describe('When user loaded', () => {
|
describe('When user loaded', () => {
|
||||||
it('should set user and clear user error', () => {
|
it('should set user and clear user error', () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
...makeInitialLdapUserState(),
|
...makeInitialUserAdminState(),
|
||||||
userError: {
|
|
||||||
title: 'User not found',
|
|
||||||
body: 'Cannot find user',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reducerTester<LdapUserState>()
|
reducerTester<UserAdminState>()
|
||||||
.givenReducer(ldapUserReducer, initialState)
|
.givenReducer(userAdminReducer, initialState)
|
||||||
.whenActionIsDispatched(userLoadedAction(getTestUser()))
|
.whenActionIsDispatched(userProfileLoadedAction(getTestUser()))
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...makeInitialLdapUserState(),
|
...makeInitialUserAdminState(),
|
||||||
|
|
||||||
user: getTestUser(),
|
user: getTestUser(),
|
||||||
userError: null,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when userSessionsLoadedAction is dispatched', () => {
|
describe('when userSessionsLoadedAction is dispatched', () => {
|
||||||
it('then state should be correct', () => {
|
it('then state should be correct', () => {
|
||||||
reducerTester<LdapUserState>()
|
reducerTester<UserAdminState>()
|
||||||
.givenReducer(ldapUserReducer, { ...makeInitialLdapUserState() })
|
.givenReducer(userAdminReducer, { ...makeInitialUserAdminState() })
|
||||||
.whenActionIsDispatched(
|
.whenActionIsDispatched(
|
||||||
userSessionsLoadedAction([
|
userSessionsLoadedAction([
|
||||||
{
|
{
|
||||||
@ -246,7 +242,7 @@ describe('Edit LDAP user page reducer', () => {
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...makeInitialLdapUserState(),
|
...makeInitialUserAdminState(),
|
||||||
sessions: [
|
sessions: [
|
||||||
{
|
{
|
||||||
browser: 'Chrome',
|
browser: 'Chrome',
|
||||||
@ -264,80 +260,4 @@ describe('Edit LDAP user page reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when userMappingInfoLoadedAction is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LdapUserState>()
|
|
||||||
.givenReducer(ldapUserReducer, {
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(userMappingInfoLoadedAction(getTestUserMapping()))
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
ldapUser: getTestUserMapping(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when userMappingInfoFailedAction is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LdapUserState>()
|
|
||||||
.givenReducer(ldapUserReducer, { ...makeInitialLdapUserState() })
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
userMappingInfoFailedAction({
|
|
||||||
title: 'User not found',
|
|
||||||
body: 'Cannot find user',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
userError: {
|
|
||||||
title: 'User not found',
|
|
||||||
body: 'Cannot find user',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when clearUserErrorAction is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LdapUserState>()
|
|
||||||
.givenReducer(ldapUserReducer, {
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
userError: {
|
|
||||||
title: 'User not found',
|
|
||||||
body: 'Cannot find user',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(clearUserErrorAction())
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
userError: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when ldapSyncStatusLoadedAction is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LdapUserState>()
|
|
||||||
.givenReducer(ldapUserReducer, {
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
ldapSyncStatusLoadedAction({
|
|
||||||
enabled: true,
|
|
||||||
schedule: '0 0 * * * *',
|
|
||||||
nextSync: '2019-01-01T12:00:00Z',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...makeInitialLdapUserState(),
|
|
||||||
ldapSyncInfo: {
|
|
||||||
enabled: true,
|
|
||||||
schedule: '0 0 * * * *',
|
|
||||||
nextSync: '2019-01-01T12:00:00Z',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -4,10 +4,12 @@ import {
|
|||||||
LdapError,
|
LdapError,
|
||||||
LdapState,
|
LdapState,
|
||||||
LdapUser,
|
LdapUser,
|
||||||
LdapUserState,
|
|
||||||
SyncInfo,
|
SyncInfo,
|
||||||
User,
|
UserAdminState,
|
||||||
|
UserDTO,
|
||||||
|
UserOrg,
|
||||||
UserSession,
|
UserSession,
|
||||||
|
UserAdminError,
|
||||||
} from 'app/types';
|
} from 'app/types';
|
||||||
|
|
||||||
const initialLdapState: LdapState = {
|
const initialLdapState: LdapState = {
|
||||||
@ -18,13 +20,6 @@ const initialLdapState: LdapState = {
|
|||||||
userError: null,
|
userError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialLdapUserState: LdapUserState = {
|
|
||||||
user: null,
|
|
||||||
ldapUser: null,
|
|
||||||
ldapSyncInfo: null,
|
|
||||||
sessions: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const ldapSlice = createSlice({
|
const ldapSlice = createSlice({
|
||||||
name: 'ldap',
|
name: 'ldap',
|
||||||
initialState: initialLdapState,
|
initialState: initialLdapState,
|
||||||
@ -75,59 +70,54 @@ export const {
|
|||||||
|
|
||||||
export const ldapReducer = ldapSlice.reducer;
|
export const ldapReducer = ldapSlice.reducer;
|
||||||
|
|
||||||
const ldapUserSlice = createSlice({
|
// UserAdminPage
|
||||||
name: 'ldapUser',
|
|
||||||
initialState: initialLdapUserState,
|
const initialUserAdminState: UserAdminState = {
|
||||||
|
user: null,
|
||||||
|
sessions: [],
|
||||||
|
orgs: [],
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userAdminSlice = createSlice({
|
||||||
|
name: 'userAdmin',
|
||||||
|
initialState: initialUserAdminState,
|
||||||
reducers: {
|
reducers: {
|
||||||
userLoadedAction: (state, action: PayloadAction<User>): LdapUserState => ({
|
userProfileLoadedAction: (state, action: PayloadAction<UserDTO>): UserAdminState => ({
|
||||||
...state,
|
...state,
|
||||||
user: action.payload,
|
user: action.payload,
|
||||||
userError: null,
|
|
||||||
}),
|
}),
|
||||||
userSessionsLoadedAction: (state, action: PayloadAction<UserSession[]>): LdapUserState => ({
|
userOrgsLoadedAction: (state, action: PayloadAction<UserOrg[]>): UserAdminState => ({
|
||||||
|
...state,
|
||||||
|
orgs: action.payload,
|
||||||
|
}),
|
||||||
|
userSessionsLoadedAction: (state, action: PayloadAction<UserSession[]>): UserAdminState => ({
|
||||||
...state,
|
...state,
|
||||||
sessions: action.payload,
|
sessions: action.payload,
|
||||||
}),
|
}),
|
||||||
userSyncFailedAction: (state, action: PayloadAction<undefined>): LdapUserState => state,
|
userAdminPageLoadedAction: (state, action: PayloadAction<boolean>): UserAdminState => ({
|
||||||
|
...state,
|
||||||
|
isLoading: !action.payload,
|
||||||
|
}),
|
||||||
|
userAdminPageFailedAction: (state, action: PayloadAction<UserAdminError>): UserAdminState => ({
|
||||||
|
...state,
|
||||||
|
error: action.payload,
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
extraReducers: builder =>
|
|
||||||
builder
|
|
||||||
.addCase(
|
|
||||||
userMappingInfoLoadedAction,
|
|
||||||
(state, action): LdapUserState => ({
|
|
||||||
...state,
|
|
||||||
ldapUser: action.payload,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addCase(
|
|
||||||
userMappingInfoFailedAction,
|
|
||||||
(state, action): LdapUserState => ({
|
|
||||||
...state,
|
|
||||||
ldapUser: null,
|
|
||||||
userError: action.payload,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addCase(
|
|
||||||
clearUserErrorAction,
|
|
||||||
(state, action): LdapUserState => ({
|
|
||||||
...state,
|
|
||||||
userError: null,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addCase(
|
|
||||||
ldapSyncStatusLoadedAction,
|
|
||||||
(state, action): LdapUserState => ({
|
|
||||||
...state,
|
|
||||||
ldapSyncInfo: action.payload,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { userLoadedAction, userSessionsLoadedAction, userSyncFailedAction } = ldapUserSlice.actions;
|
export const {
|
||||||
|
userProfileLoadedAction,
|
||||||
|
userOrgsLoadedAction,
|
||||||
|
userSessionsLoadedAction,
|
||||||
|
userAdminPageLoadedAction,
|
||||||
|
userAdminPageFailedAction,
|
||||||
|
} = userAdminSlice.actions;
|
||||||
|
|
||||||
export const ldapUserReducer = ldapUserSlice.reducer;
|
export const userAdminReducer = userAdminSlice.reducer;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ldap: ldapReducer,
|
ldap: ldapReducer,
|
||||||
ldapUser: ldapUserReducer,
|
userAdmin: userAdminReducer,
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import CreateFolderCtrl from 'app/features/folders/CreateFolderCtrl';
|
|||||||
import FolderDashboardsCtrl from 'app/features/folders/FolderDashboardsCtrl';
|
import FolderDashboardsCtrl from 'app/features/folders/FolderDashboardsCtrl';
|
||||||
import DashboardImportCtrl from 'app/features/manage-dashboards/DashboardImportCtrl';
|
import DashboardImportCtrl from 'app/features/manage-dashboards/DashboardImportCtrl';
|
||||||
import LdapPage from 'app/features/admin/ldap/LdapPage';
|
import LdapPage from 'app/features/admin/ldap/LdapPage';
|
||||||
|
import UserAdminPage from 'app/features/admin/UserAdminPage';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { ILocationProvider, route } from 'angular';
|
import { ILocationProvider, route } from 'angular';
|
||||||
// Types
|
// Types
|
||||||
@ -298,9 +299,15 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
|||||||
templateUrl: 'public/app/features/admin/partials/new_user.html',
|
templateUrl: 'public/app/features/admin/partials/new_user.html',
|
||||||
controller: 'AdminEditUserCtrl',
|
controller: 'AdminEditUserCtrl',
|
||||||
})
|
})
|
||||||
|
// .when('/admin/users/edit/:id', {
|
||||||
|
// templateUrl: 'public/app/features/admin/partials/edit_user.html',
|
||||||
|
// controller: 'AdminEditUserCtrl',
|
||||||
|
// })
|
||||||
.when('/admin/users/edit/:id', {
|
.when('/admin/users/edit/:id', {
|
||||||
templateUrl: 'public/app/features/admin/partials/edit_user.html',
|
template: '<react-container />',
|
||||||
controller: 'AdminEditUserCtrl',
|
resolve: {
|
||||||
|
component: () => UserAdminPage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/admin/orgs', {
|
.when('/admin/orgs', {
|
||||||
templateUrl: 'public/app/features/admin/partials/orgs.html',
|
templateUrl: 'public/app/features/admin/partials/orgs.html',
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { User, UserSession } from 'app/types';
|
|
||||||
|
|
||||||
interface LdapMapping {
|
interface LdapMapping {
|
||||||
cfgAttrValue: string;
|
cfgAttrValue: string;
|
||||||
ldapValue: string;
|
ldapValue: string;
|
||||||
@ -85,11 +83,3 @@ export interface LdapState {
|
|||||||
userError?: LdapError;
|
userError?: LdapError;
|
||||||
ldapError?: LdapError;
|
ldapError?: LdapError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LdapUserState {
|
|
||||||
user?: User;
|
|
||||||
ldapUser?: LdapUser;
|
|
||||||
ldapSyncInfo?: SyncInfo;
|
|
||||||
sessions?: UserSession[];
|
|
||||||
userError?: LdapError;
|
|
||||||
}
|
|
||||||
|
@ -9,12 +9,12 @@ import { FolderState } from './folders';
|
|||||||
import { DashboardState } from './dashboard';
|
import { DashboardState } from './dashboard';
|
||||||
import { DataSourcesState } from './datasources';
|
import { DataSourcesState } from './datasources';
|
||||||
import { ExploreState } from './explore';
|
import { ExploreState } from './explore';
|
||||||
import { UsersState, UserState } from './user';
|
import { UsersState, UserState, UserAdminState } from './user';
|
||||||
import { OrganizationState } from './organization';
|
import { OrganizationState } from './organization';
|
||||||
import { AppNotificationsState } from './appNotifications';
|
import { AppNotificationsState } from './appNotifications';
|
||||||
import { PluginsState } from './plugins';
|
import { PluginsState } from './plugins';
|
||||||
import { ApplicationState } from './application';
|
import { ApplicationState } from './application';
|
||||||
import { LdapState, LdapUserState } from './ldap';
|
import { LdapState } from './ldap';
|
||||||
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
|
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
|
||||||
import { ApiKeysState } from './apiKeys';
|
import { ApiKeysState } from './apiKeys';
|
||||||
|
|
||||||
@ -36,8 +36,8 @@ export interface StoreState {
|
|||||||
plugins: PluginsState;
|
plugins: PluginsState;
|
||||||
application: ApplicationState;
|
application: ApplicationState;
|
||||||
ldap: LdapState;
|
ldap: LdapState;
|
||||||
ldapUser: LdapUserState;
|
|
||||||
apiKeys: ApiKeysState;
|
apiKeys: ApiKeysState;
|
||||||
|
userAdmin: UserAdminState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -22,6 +22,21 @@ export interface User {
|
|||||||
orgId?: number;
|
orgId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserDTO {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
isGrafanaAdmin: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
isExternal?: boolean;
|
||||||
|
updatedAt?: string;
|
||||||
|
authLabels?: string[];
|
||||||
|
theme?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
orgId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Invitee {
|
export interface Invitee {
|
||||||
code: string;
|
code: string;
|
||||||
createdOn: string;
|
createdOn: string;
|
||||||
@ -67,3 +82,22 @@ export interface UserSession {
|
|||||||
osVersion: string;
|
osVersion: string;
|
||||||
device: string;
|
device: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserOrg {
|
||||||
|
name: string;
|
||||||
|
orgId: number;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAdminState {
|
||||||
|
user: UserDTO;
|
||||||
|
sessions: UserSession[];
|
||||||
|
orgs: UserOrg[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error?: UserAdminError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAdminError {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
|
|
||||||
.page-body {
|
.page-body {
|
||||||
padding-top: $spacer * 2;
|
padding-top: $spacer * 2;
|
||||||
|
padding-bottom: $spacer * 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-heading {
|
.page-heading {
|
||||||
|
Loading…
Reference in New Issue
Block a user