mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Convert remaining profile bits to React (#24310)
* reactify user sessions * all reactified * more cleanup * comment edit * Profile: Fix casing * Profile: Add Page wrapper * Profile: New form styles for UserProfileEditForm * Profile: Use new form styles for SharedPreferences * Profile: Use radioButtonGroup for SharedPreferences * Grafana UI: Add FieldSet * Grafana UI: Add story * Grafana UI: Add docs * Grafana UI: Export FieldSet * Profile: USe FieldSet * Profile: Sort sessions Co-authored-by: Clarity-89 <homes89@ukr.net>
This commit is contained in:
parent
8474794aaa
commit
5293c9dd84
@ -20,7 +20,6 @@ import {
|
||||
} from '@grafana/ui';
|
||||
const { SecretFormField } = LegacyForms;
|
||||
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
||||
import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper';
|
||||
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
|
||||
import { HelpModal } from './components/help/HelpModal';
|
||||
import { Footer } from './components/Footer/Footer';
|
||||
@ -164,8 +163,6 @@ export function registerAngularDirectives() {
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
|
||||
react2AngularDirective('reactProfileWrapper', ReactProfileWrapper, []);
|
||||
|
||||
react2AngularDirective('lokiAnnotationsQueryEditor', LokiAnnotationsQueryEditor, [
|
||||
'expr',
|
||||
'onChange',
|
||||
|
@ -1,12 +1,23 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css } from 'emotion';
|
||||
|
||||
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
|
||||
const { Select } = LegacyForms;
|
||||
import {
|
||||
Select,
|
||||
Field,
|
||||
Form,
|
||||
Tooltip,
|
||||
Icon,
|
||||
stylesFactory,
|
||||
Label,
|
||||
Button,
|
||||
RadioButtonGroup,
|
||||
FieldSet,
|
||||
} from '@grafana/ui';
|
||||
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
export interface Props {
|
||||
resourceUri: string;
|
||||
@ -19,7 +30,7 @@ export interface State {
|
||||
dashboards: DashboardSearchHit[];
|
||||
}
|
||||
|
||||
const themes = [
|
||||
const themes: SelectableValue[] = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
@ -86,9 +97,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitForm = async (event: React.SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
onSubmitForm = async () => {
|
||||
const { homeDashboardId, theme, timezone } = this.state;
|
||||
|
||||
await backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
|
||||
@ -99,11 +108,8 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
onThemeChanged = (theme: SelectableValue<string>) => {
|
||||
if (!theme || typeof theme.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
this.setState({ theme: theme.value });
|
||||
onThemeChanged = (value: string) => {
|
||||
this.setState({ theme: value });
|
||||
};
|
||||
|
||||
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
|
||||
@ -126,27 +132,31 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { theme, timezone, homeDashboardId, dashboards } = this.state;
|
||||
const styles = getStyles();
|
||||
|
||||
return (
|
||||
<form className="section gf-form-group" onSubmit={this.onSubmitForm}>
|
||||
<h3 className="page-heading">Preferences</h3>
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label width-11">UI Theme</span>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
value={themes.find(item => item.value === theme)}
|
||||
<Form onSubmit={this.onSubmitForm}>
|
||||
{() => {
|
||||
return (
|
||||
<FieldSet label="Preferences">
|
||||
<Field label="UI Theme">
|
||||
<RadioButtonGroup
|
||||
options={themes}
|
||||
value={themes.find(item => item.value === theme)?.value}
|
||||
onChange={this.onThemeChanged}
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
width={11}
|
||||
tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={
|
||||
<Label>
|
||||
<span className={styles.labelText}>Home Dashboard</span>
|
||||
<Tooltip content="Not finding dashboard you want? Star it first, then it should appear in this select box.">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
}
|
||||
>
|
||||
Home Dashboard
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
|
||||
getOptionValue={i => i.id}
|
||||
@ -154,27 +164,34 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
onChange={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
|
||||
options={dashboards}
|
||||
placeholder="Choose default dashboard"
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form" aria-label={selectors.components.TimeZonePicker.container}>
|
||||
<label className="gf-form-label width-11">Timezone</label>
|
||||
</Field>
|
||||
|
||||
<Field label="Timezone" aria-label={selectors.components.TimeZonePicker.container}>
|
||||
<Select
|
||||
isSearchable={true}
|
||||
value={timeZones.find(item => item.value === timezone)}
|
||||
onChange={this.onTimeZoneChanged}
|
||||
options={timeZones}
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<div className="gf-form-button-row">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
<Button variant="primary">Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FieldSet>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SharedPreferences;
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
labelText: css`
|
||||
margin-right: 6px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { User, Team } from 'app/types';
|
||||
import { User, Team, UserOrg, UserSession } from 'app/types';
|
||||
import { config } from 'app/core/config';
|
||||
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||
|
||||
export interface UserAPI {
|
||||
changePassword: (changePassword: ChangePasswordFields) => void;
|
||||
@ -9,14 +10,17 @@ export interface UserAPI {
|
||||
loadUser: () => void;
|
||||
loadTeams: () => void;
|
||||
loadOrgs: () => void;
|
||||
loadSessions: () => void;
|
||||
setUserOrg: (org: UserOrg) => void;
|
||||
revokeUserSession: (tokenId: number) => void;
|
||||
}
|
||||
|
||||
interface LoadingStates {
|
||||
export interface LoadingStates {
|
||||
changePassword: boolean;
|
||||
loadUser: boolean;
|
||||
loadTeams: boolean;
|
||||
loadOrgs: boolean;
|
||||
loadSessions: boolean;
|
||||
updateUserProfile: boolean;
|
||||
updateUserOrg: boolean;
|
||||
}
|
||||
@ -33,21 +37,23 @@ export interface ProfileUpdateFields {
|
||||
login: string;
|
||||
}
|
||||
|
||||
export interface UserOrg {
|
||||
orgId: number;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
userId?: number; // passed, will load user on mount
|
||||
children: (api: UserAPI, states: LoadingStates, teams: Team[], orgs: UserOrg[], user?: User) => JSX.Element;
|
||||
children: (
|
||||
api: UserAPI,
|
||||
states: LoadingStates,
|
||||
teams: Team[],
|
||||
orgs: UserOrg[],
|
||||
sessions: UserSession[],
|
||||
user?: User
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
user?: User;
|
||||
teams: Team[];
|
||||
orgs: UserOrg[];
|
||||
sessions: UserSession[];
|
||||
loadingStates: LoadingStates;
|
||||
}
|
||||
|
||||
@ -55,11 +61,13 @@ export class UserProvider extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
teams: [] as Team[],
|
||||
orgs: [] as UserOrg[],
|
||||
sessions: [] as UserSession[],
|
||||
loadingStates: {
|
||||
changePassword: false,
|
||||
loadUser: true,
|
||||
loadTeams: false,
|
||||
loadOrgs: false,
|
||||
loadSessions: false,
|
||||
updateUserProfile: false,
|
||||
updateUserOrg: false,
|
||||
},
|
||||
@ -101,6 +109,50 @@ export class UserProvider extends PureComponent<Props, State> {
|
||||
this.setState({ orgs, loadingStates: { ...this.state.loadingStates, loadOrgs: false } });
|
||||
};
|
||||
|
||||
loadSessions = async () => {
|
||||
this.setState({
|
||||
loadingStates: { ...this.state.loadingStates, loadSessions: true },
|
||||
});
|
||||
|
||||
await getBackendSrv()
|
||||
.get('/api/user/auth-tokens')
|
||||
.then((sessions: UserSession[]) => {
|
||||
sessions = sessions
|
||||
// Show active sessions first
|
||||
.sort((a, b) => Number(b.isActive) - Number(a.isActive))
|
||||
.map((session: UserSession) => {
|
||||
return {
|
||||
id: session.id,
|
||||
isActive: session.isActive,
|
||||
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||
clientIp: session.clientIp,
|
||||
browser: session.browser,
|
||||
browserVersion: session.browserVersion,
|
||||
os: session.os,
|
||||
osVersion: session.osVersion,
|
||||
device: session.device,
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ sessions, loadingStates: { ...this.state.loadingStates, loadSessions: false } });
|
||||
});
|
||||
};
|
||||
|
||||
revokeUserSession = async (tokenId: number) => {
|
||||
await getBackendSrv()
|
||||
.post('/api/user/revoke-auth-token', {
|
||||
authTokenId: tokenId,
|
||||
})
|
||||
.then(() => {
|
||||
const sessions = this.state.sessions.filter((session: UserSession) => {
|
||||
return session.id !== tokenId;
|
||||
});
|
||||
|
||||
this.setState({ sessions });
|
||||
});
|
||||
};
|
||||
|
||||
setUserOrg = async (org: UserOrg) => {
|
||||
this.setState({
|
||||
loadingStates: { ...this.state.loadingStates, updateUserOrg: true },
|
||||
@ -119,9 +171,7 @@ export class UserProvider extends PureComponent<Props, State> {
|
||||
this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: true } });
|
||||
await getBackendSrv()
|
||||
.put('/api/user', payload)
|
||||
.then(() => {
|
||||
this.loadUser();
|
||||
})
|
||||
.then(this.loadUser)
|
||||
.catch(e => console.log(e))
|
||||
.finally(() => {
|
||||
this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: false } });
|
||||
@ -130,18 +180,20 @@ export class UserProvider extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { loadingStates, teams, orgs, user } = this.state;
|
||||
const { loadingStates, teams, orgs, sessions, user } = this.state;
|
||||
|
||||
const api = {
|
||||
const api: UserAPI = {
|
||||
changePassword: this.changePassword,
|
||||
loadUser: this.loadUser,
|
||||
loadTeams: this.loadTeams,
|
||||
loadOrgs: this.loadOrgs,
|
||||
loadSessions: this.loadSessions,
|
||||
revokeUserSession: this.revokeUserSession,
|
||||
updateUserProfile: this.updateUserProfile,
|
||||
setUserOrg: this.setUserOrg,
|
||||
};
|
||||
|
||||
return <>{children(api, loadingStates, teams, orgs, user)}</>;
|
||||
return <>{children(api, loadingStates, teams, orgs, sessions, user)}</>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { UserProvider } from 'app/core/utils/UserProvider';
|
||||
import { UserProfileEditForm } from './UserProfileEditForm';
|
||||
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
import { UserTeams } from './UserTeams';
|
||||
import { UserOrganizations } from './UserOrganizations';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
|
||||
export const ReactProfileWrapper = () => (
|
||||
<UserProvider userId={config.bootData.user.id}>
|
||||
{(api, states, teams, orgs, user) => {
|
||||
return (
|
||||
<>
|
||||
{states.loadUser ? (
|
||||
<LoadingPlaceholder text="Loading user profile..." />
|
||||
) : (
|
||||
<UserProfileEditForm
|
||||
updateProfile={api.updateUserProfile}
|
||||
isSavingUser={states.updateUserProfile}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
<SharedPreferences resourceUri="user" />
|
||||
<UserTeams isLoading={states.loadTeams} loadTeams={api.loadTeams} teams={teams} />
|
||||
{!states.loadUser && (
|
||||
<UserOrganizations
|
||||
isLoading={states.loadOrgs}
|
||||
setUserOrg={api.setUserOrg}
|
||||
loadOrgs={api.loadOrgs}
|
||||
orgs={orgs}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UserProvider>
|
||||
);
|
||||
|
||||
export default ReactProfileWrapper;
|
@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { User } from 'app/types';
|
||||
import { UserOrg } from 'app/core/utils/UserProvider';
|
||||
import { User, UserOrg } from 'app/types';
|
||||
import { LoadingPlaceholder, Button } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
|
69
public/app/features/profile/UserProfileEdit.tsx
Normal file
69
public/app/features/profile/UserProfileEdit.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { FC } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { UserProvider, UserAPI, LoadingStates } from 'app/core/utils/UserProvider';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { User, Team, UserOrg, UserSession, StoreState } from 'app/types';
|
||||
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { UserTeams } from './UserTeams';
|
||||
import { UserSessions } from './UserSessions';
|
||||
import { UserOrganizations } from './UserOrganizations';
|
||||
import { UserProfileEditForm } from './UserProfileEditForm';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
}
|
||||
|
||||
export const UserProfileEdit: FC<Props> = ({ navModel }) => (
|
||||
<Page navModel={navModel}>
|
||||
<UserProvider userId={config.bootData.user.id}>
|
||||
{(api: UserAPI, states: LoadingStates, teams: Team[], orgs: UserOrg[], sessions: UserSession[], user: User) => {
|
||||
return (
|
||||
<Page.Contents>
|
||||
{states.loadUser ? (
|
||||
<LoadingPlaceholder text="Loading user profile..." />
|
||||
) : (
|
||||
<UserProfileEditForm
|
||||
updateProfile={api.updateUserProfile}
|
||||
isSavingUser={states.updateUserProfile}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
<SharedPreferences resourceUri="user" />
|
||||
<UserTeams isLoading={states.loadTeams} loadTeams={api.loadTeams} teams={teams} />
|
||||
{!states.loadUser && (
|
||||
<>
|
||||
<UserOrganizations
|
||||
isLoading={states.loadOrgs}
|
||||
setUserOrg={api.setUserOrg}
|
||||
loadOrgs={api.loadOrgs}
|
||||
orgs={orgs}
|
||||
user={user}
|
||||
/>
|
||||
<UserSessions
|
||||
isLoading={states.loadSessions}
|
||||
loadSessions={api.loadSessions}
|
||||
revokeUserSession={api.revokeUserSession}
|
||||
sessions={sessions}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Page.Contents>
|
||||
);
|
||||
}}
|
||||
</UserProvider>
|
||||
</Page>
|
||||
);
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'profile-settings'),
|
||||
};
|
||||
}
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, null)(UserProfileEdit));
|
@ -1,6 +1,5 @@
|
||||
import React, { PureComponent, ChangeEvent, MouseEvent } from 'react';
|
||||
import { Button, InlineFormLabel, LegacyForms, Tooltip, Icon } from '@grafana/ui';
|
||||
const { Input } = LegacyForms;
|
||||
import React, { FC } from 'react';
|
||||
import { Button, Tooltip, Icon, Form, Input, Field, FieldSet } from '@grafana/ui';
|
||||
import { User } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
import { ProfileUpdateFields } from 'app/core/utils/UserProvider';
|
||||
@ -11,96 +10,57 @@ export interface Props {
|
||||
updateProfile: (payload: ProfileUpdateFields) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
name: string;
|
||||
email: string;
|
||||
login: string;
|
||||
}
|
||||
|
||||
export class UserProfileEditForm extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
user: { name, email, login },
|
||||
} = this.props;
|
||||
|
||||
this.state = {
|
||||
name,
|
||||
email,
|
||||
login,
|
||||
};
|
||||
}
|
||||
|
||||
onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: event.target.value });
|
||||
};
|
||||
|
||||
onEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ email: event.target.value });
|
||||
};
|
||||
|
||||
onLoginChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ login: event.target.value });
|
||||
};
|
||||
|
||||
onSubmitProfileUpdate = (event: MouseEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
this.props.updateProfile({ ...this.state });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, email, login } = this.state;
|
||||
const { isSavingUser } = this.props;
|
||||
const { disableLoginForm } = config;
|
||||
|
||||
export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfile }) => {
|
||||
const onSubmitProfileUpdate = (data: ProfileUpdateFields) => {
|
||||
updateProfile(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-sub-heading">Edit Profile</h3>
|
||||
<form name="userForm" className="gf-form-group">
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel className="width-8">Name</InlineFormLabel>
|
||||
<Input className="gf-form-input max-width-22" type="text" onChange={this.onNameChange} value={name} />
|
||||
</div>
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel className="width-8">Email</InlineFormLabel>
|
||||
<Form onSubmit={onSubmitProfileUpdate} validateOn="onBlur">
|
||||
{({ register, errors }) => {
|
||||
return (
|
||||
<FieldSet label="Edit Profile">
|
||||
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
||||
<Input name="name" ref={register({ required: true })} placeholder="Name" defaultValue={user.name} />
|
||||
</Field>
|
||||
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
|
||||
<Input
|
||||
className="gf-form-input max-width-22"
|
||||
type="text"
|
||||
onChange={this.onEmailChange}
|
||||
value={email}
|
||||
disabled={disableLoginForm}
|
||||
name="email"
|
||||
ref={register({ required: true })}
|
||||
placeholder="Email"
|
||||
defaultValue={user.email}
|
||||
suffix={<InputSuffix />}
|
||||
/>
|
||||
{disableLoginForm && (
|
||||
<Tooltip content="Login Details Locked - managed in another system.">
|
||||
<Icon name="lock" className="gf-form-icon--right-absolute" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel className="width-8">Username</InlineFormLabel>
|
||||
</Field>
|
||||
<Field label="Username" disabled={disableLoginForm}>
|
||||
<Input
|
||||
className="gf-form-input max-width-22"
|
||||
type="text"
|
||||
onChange={this.onLoginChange}
|
||||
value={login}
|
||||
disabled={disableLoginForm}
|
||||
name="login"
|
||||
ref={register}
|
||||
defaultValue={user.login}
|
||||
placeholder="Username"
|
||||
suffix={<InputSuffix />}
|
||||
/>
|
||||
{disableLoginForm && (
|
||||
<Tooltip content="Login Details Locked - managed in another system.">
|
||||
<Icon name="lock" className="gf-form-icon--right-absolute" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
<div className="gf-form-button-row">
|
||||
<Button variant="primary" onClick={this.onSubmitProfileUpdate} disabled={isSavingUser}>
|
||||
<Button variant="primary" disabled={isSavingUser}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileEditForm;
|
||||
|
||||
const InputSuffix: FC = () => {
|
||||
return disableLoginForm ? (
|
||||
<Tooltip content="Login Details Locked - managed in another system.">
|
||||
<Icon name="lock" />
|
||||
</Tooltip>
|
||||
) : null;
|
||||
};
|
||||
|
67
public/app/features/profile/UserSessions.tsx
Normal file
67
public/app/features/profile/UserSessions.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { User, UserSession } from 'app/types';
|
||||
import { LoadingPlaceholder, Button, Icon } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
user: User;
|
||||
sessions: UserSession[];
|
||||
isLoading: boolean;
|
||||
loadSessions: () => void;
|
||||
revokeUserSession: (tokenId: number) => void;
|
||||
}
|
||||
|
||||
export class UserSessions extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.loadSessions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, sessions, revokeUserSession } = this.props;
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder text="Loading sessions..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<h3 className="page-sub-heading">Sessions</h3>
|
||||
<div className="gf-form-group">
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Last seen</th>
|
||||
<th>Logged on</th>
|
||||
<th>IP address</th>
|
||||
<th>Browser & OS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions.map((session: UserSession, index) => (
|
||||
<tr key={index}>
|
||||
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
|
||||
<td>{session.createdAt}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>
|
||||
{session.browser} on {session.os} {session.osVersion}
|
||||
</td>
|
||||
<td>
|
||||
<Button size="sm" variant="destructive" onClick={() => revokeUserSession(session.id)}>
|
||||
<Icon name="power" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserSessions;
|
@ -1 +0,0 @@
|
||||
import './ProfileCtrl';
|
@ -1,36 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<react-profile-wrapper></react-profile-wrapper>
|
||||
|
||||
<h3 class="page-heading">Sessions</h3>
|
||||
<div class="gf-form-group">
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Last seen</th>
|
||||
<th>Logged on</th>
|
||||
<th>IP address</th>
|
||||
<th>Browser & OS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="session in ctrl.sessions">
|
||||
<td ng-if="session.isActive">Now</td>
|
||||
<td ng-if="!session.isActive">{{ session.seenAt }}</td>
|
||||
<td>{{ session.createdAt }}</td>
|
||||
<td>{{ session.clientIp }}</td>
|
||||
<td>{{ session.browser }} on {{ session.os }} {{ session.osVersion }}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-small" ng-click="ctrl.revokeUserSession(session.id)">
|
||||
<icon name="'power'"></icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer />
|
@ -302,10 +302,12 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
},
|
||||
})
|
||||
.when('/profile', {
|
||||
templateUrl: 'public/app/features/profile/partials/profile.html',
|
||||
controller: 'ProfileCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
template: '<react-container />',
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(import(/* webpackChunkName: "UserProfileEdit" */ 'app/features/profile/UserProfileEdit')),
|
||||
},
|
||||
})
|
||||
.when('/profile/password', {
|
||||
template: '<react-container />',
|
||||
@ -313,7 +315,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(/* webPackChunkName: "ChangePasswordPage" */ 'app/features/profile/ChangePasswordPage')
|
||||
import(/* webpackChunkName: "ChangePasswordPage" */ 'app/features/profile/ChangePasswordPage')
|
||||
),
|
||||
},
|
||||
})
|
||||
@ -322,7 +324,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(import(/* webPackChunkName: "SelectOrgPage" */ 'app/features/org/SelectOrgPage')),
|
||||
SafeDynamicImport(import(/* webpackChunkName: "SelectOrgPage" */ 'app/features/org/SelectOrgPage')),
|
||||
},
|
||||
})
|
||||
// ADMIN
|
||||
|
Loading…
Reference in New Issue
Block a user