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';
|
} from '@grafana/ui';
|
||||||
const { SecretFormField } = LegacyForms;
|
const { SecretFormField } = LegacyForms;
|
||||||
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
||||||
import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper';
|
|
||||||
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
|
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
|
||||||
import { HelpModal } from './components/help/HelpModal';
|
import { HelpModal } from './components/help/HelpModal';
|
||||||
import { Footer } from './components/Footer/Footer';
|
import { Footer } from './components/Footer/Footer';
|
||||||
@ -164,8 +163,6 @@ export function registerAngularDirectives() {
|
|||||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
react2AngularDirective('reactProfileWrapper', ReactProfileWrapper, []);
|
|
||||||
|
|
||||||
react2AngularDirective('lokiAnnotationsQueryEditor', LokiAnnotationsQueryEditor, [
|
react2AngularDirective('lokiAnnotationsQueryEditor', LokiAnnotationsQueryEditor, [
|
||||||
'expr',
|
'expr',
|
||||||
'onChange',
|
'onChange',
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
|
import {
|
||||||
const { Select } = LegacyForms;
|
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 { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
resourceUri: string;
|
resourceUri: string;
|
||||||
@ -19,7 +30,7 @@ export interface State {
|
|||||||
dashboards: DashboardSearchHit[];
|
dashboards: DashboardSearchHit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const themes = [
|
const themes: SelectableValue[] = [
|
||||||
{ value: '', label: 'Default' },
|
{ value: '', label: 'Default' },
|
||||||
{ value: 'dark', label: 'Dark' },
|
{ value: 'dark', label: 'Dark' },
|
||||||
{ value: 'light', label: 'Light' },
|
{ value: 'light', label: 'Light' },
|
||||||
@ -86,9 +97,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmitForm = async (event: React.SyntheticEvent) => {
|
onSubmitForm = async () => {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const { homeDashboardId, theme, timezone } = this.state;
|
const { homeDashboardId, theme, timezone } = this.state;
|
||||||
|
|
||||||
await backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
|
await backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
|
||||||
@ -99,11 +108,8 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
onThemeChanged = (theme: SelectableValue<string>) => {
|
onThemeChanged = (value: string) => {
|
||||||
if (!theme || typeof theme.value !== 'string') {
|
this.setState({ theme: value });
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({ theme: theme.value });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
|
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
|
||||||
@ -126,55 +132,66 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { theme, timezone, homeDashboardId, dashboards } = this.state;
|
const { theme, timezone, homeDashboardId, dashboards } = this.state;
|
||||||
|
const styles = getStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="section gf-form-group" onSubmit={this.onSubmitForm}>
|
<Form onSubmit={this.onSubmitForm}>
|
||||||
<h3 className="page-heading">Preferences</h3>
|
{() => {
|
||||||
<div className="gf-form">
|
return (
|
||||||
<span className="gf-form-label width-11">UI Theme</span>
|
<FieldSet label="Preferences">
|
||||||
<Select
|
<Field label="UI Theme">
|
||||||
isSearchable={false}
|
<RadioButtonGroup
|
||||||
value={themes.find(item => item.value === theme)}
|
options={themes}
|
||||||
options={themes}
|
value={themes.find(item => item.value === theme)?.value}
|
||||||
onChange={this.onThemeChanged}
|
onChange={this.onThemeChanged}
|
||||||
width={20}
|
/>
|
||||||
/>
|
</Field>
|
||||||
</div>
|
|
||||||
<div className="gf-form">
|
<Field
|
||||||
<InlineFormLabel
|
label={
|
||||||
width={11}
|
<Label>
|
||||||
tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
|
<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.">
|
||||||
Home Dashboard
|
<Icon name="info-circle" />
|
||||||
</InlineFormLabel>
|
</Tooltip>
|
||||||
<Select
|
</Label>
|
||||||
value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
|
}
|
||||||
getOptionValue={i => i.id}
|
>
|
||||||
getOptionLabel={this.getFullDashName}
|
<Select
|
||||||
onChange={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
|
value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
|
||||||
options={dashboards}
|
getOptionValue={i => i.id}
|
||||||
placeholder="Choose default dashboard"
|
getOptionLabel={this.getFullDashName}
|
||||||
width={20}
|
onChange={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
|
||||||
/>
|
options={dashboards}
|
||||||
</div>
|
placeholder="Choose default dashboard"
|
||||||
<div className="gf-form" aria-label={selectors.components.TimeZonePicker.container}>
|
/>
|
||||||
<label className="gf-form-label width-11">Timezone</label>
|
</Field>
|
||||||
<Select
|
|
||||||
isSearchable={true}
|
<Field label="Timezone" aria-label={selectors.components.TimeZonePicker.container}>
|
||||||
value={timeZones.find(item => item.value === timezone)}
|
<Select
|
||||||
onChange={this.onTimeZoneChanged}
|
isSearchable={true}
|
||||||
options={timeZones}
|
value={timeZones.find(item => item.value === timezone)}
|
||||||
width={20}
|
onChange={this.onTimeZoneChanged}
|
||||||
/>
|
options={timeZones}
|
||||||
</div>
|
/>
|
||||||
<div className="gf-form-button-row">
|
</Field>
|
||||||
<button type="submit" className="btn btn-primary">
|
<div className="gf-form-button-row">
|
||||||
Save
|
<Button variant="primary">Save</Button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</FieldSet>
|
||||||
</form>
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SharedPreferences;
|
export default SharedPreferences;
|
||||||
|
|
||||||
|
const getStyles = stylesFactory(() => {
|
||||||
|
return {
|
||||||
|
labelText: css`
|
||||||
|
margin-right: 6px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
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 { config } from 'app/core/config';
|
||||||
|
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||||
|
|
||||||
export interface UserAPI {
|
export interface UserAPI {
|
||||||
changePassword: (changePassword: ChangePasswordFields) => void;
|
changePassword: (changePassword: ChangePasswordFields) => void;
|
||||||
@ -9,14 +10,17 @@ export interface UserAPI {
|
|||||||
loadUser: () => void;
|
loadUser: () => void;
|
||||||
loadTeams: () => void;
|
loadTeams: () => void;
|
||||||
loadOrgs: () => void;
|
loadOrgs: () => void;
|
||||||
|
loadSessions: () => void;
|
||||||
setUserOrg: (org: UserOrg) => void;
|
setUserOrg: (org: UserOrg) => void;
|
||||||
|
revokeUserSession: (tokenId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingStates {
|
export interface LoadingStates {
|
||||||
changePassword: boolean;
|
changePassword: boolean;
|
||||||
loadUser: boolean;
|
loadUser: boolean;
|
||||||
loadTeams: boolean;
|
loadTeams: boolean;
|
||||||
loadOrgs: boolean;
|
loadOrgs: boolean;
|
||||||
|
loadSessions: boolean;
|
||||||
updateUserProfile: boolean;
|
updateUserProfile: boolean;
|
||||||
updateUserOrg: boolean;
|
updateUserOrg: boolean;
|
||||||
}
|
}
|
||||||
@ -33,21 +37,23 @@ export interface ProfileUpdateFields {
|
|||||||
login: string;
|
login: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserOrg {
|
|
||||||
orgId: number;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
userId?: number; // passed, will load user on mount
|
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 {
|
export interface State {
|
||||||
user?: User;
|
user?: User;
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
orgs: UserOrg[];
|
orgs: UserOrg[];
|
||||||
|
sessions: UserSession[];
|
||||||
loadingStates: LoadingStates;
|
loadingStates: LoadingStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,11 +61,13 @@ export class UserProvider extends PureComponent<Props, State> {
|
|||||||
state: State = {
|
state: State = {
|
||||||
teams: [] as Team[],
|
teams: [] as Team[],
|
||||||
orgs: [] as UserOrg[],
|
orgs: [] as UserOrg[],
|
||||||
|
sessions: [] as UserSession[],
|
||||||
loadingStates: {
|
loadingStates: {
|
||||||
changePassword: false,
|
changePassword: false,
|
||||||
loadUser: true,
|
loadUser: true,
|
||||||
loadTeams: false,
|
loadTeams: false,
|
||||||
loadOrgs: false,
|
loadOrgs: false,
|
||||||
|
loadSessions: false,
|
||||||
updateUserProfile: false,
|
updateUserProfile: false,
|
||||||
updateUserOrg: false,
|
updateUserOrg: false,
|
||||||
},
|
},
|
||||||
@ -101,6 +109,50 @@ export class UserProvider extends PureComponent<Props, State> {
|
|||||||
this.setState({ orgs, loadingStates: { ...this.state.loadingStates, loadOrgs: false } });
|
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) => {
|
setUserOrg = async (org: UserOrg) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loadingStates: { ...this.state.loadingStates, updateUserOrg: true },
|
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 } });
|
this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: true } });
|
||||||
await getBackendSrv()
|
await getBackendSrv()
|
||||||
.put('/api/user', payload)
|
.put('/api/user', payload)
|
||||||
.then(() => {
|
.then(this.loadUser)
|
||||||
this.loadUser();
|
|
||||||
})
|
|
||||||
.catch(e => console.log(e))
|
.catch(e => console.log(e))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: false } });
|
this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: false } });
|
||||||
@ -130,18 +180,20 @@ export class UserProvider extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children } = this.props;
|
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,
|
changePassword: this.changePassword,
|
||||||
loadUser: this.loadUser,
|
loadUser: this.loadUser,
|
||||||
loadTeams: this.loadTeams,
|
loadTeams: this.loadTeams,
|
||||||
loadOrgs: this.loadOrgs,
|
loadOrgs: this.loadOrgs,
|
||||||
|
loadSessions: this.loadSessions,
|
||||||
|
revokeUserSession: this.revokeUserSession,
|
||||||
updateUserProfile: this.updateUserProfile,
|
updateUserProfile: this.updateUserProfile,
|
||||||
setUserOrg: this.setUserOrg,
|
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 React, { PureComponent } from 'react';
|
||||||
import { User } from 'app/types';
|
import { User, UserOrg } from 'app/types';
|
||||||
import { UserOrg } from 'app/core/utils/UserProvider';
|
|
||||||
import { LoadingPlaceholder, Button } from '@grafana/ui';
|
import { LoadingPlaceholder, Button } from '@grafana/ui';
|
||||||
|
|
||||||
export interface Props {
|
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 React, { FC } from 'react';
|
||||||
import { Button, InlineFormLabel, LegacyForms, Tooltip, Icon } from '@grafana/ui';
|
import { Button, Tooltip, Icon, Form, Input, Field, FieldSet } from '@grafana/ui';
|
||||||
const { Input } = LegacyForms;
|
|
||||||
import { User } from 'app/types';
|
import { User } from 'app/types';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { ProfileUpdateFields } from 'app/core/utils/UserProvider';
|
import { ProfileUpdateFields } from 'app/core/utils/UserProvider';
|
||||||
@ -11,96 +10,57 @@ export interface Props {
|
|||||||
updateProfile: (payload: ProfileUpdateFields) => void;
|
updateProfile: (payload: ProfileUpdateFields) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
const { disableLoginForm } = config;
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
login: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserProfileEditForm extends PureComponent<Props, State> {
|
export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfile }) => {
|
||||||
constructor(props: Props) {
|
const onSubmitProfileUpdate = (data: ProfileUpdateFields) => {
|
||||||
super(props);
|
updateProfile(data);
|
||||||
|
|
||||||
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>) => {
|
return (
|
||||||
this.setState({ email: event.target.value });
|
<Form onSubmit={onSubmitProfileUpdate} validateOn="onBlur">
|
||||||
};
|
{({ register, errors }) => {
|
||||||
|
return (
|
||||||
onLoginChange = (event: ChangeEvent<HTMLInputElement>) => {
|
<FieldSet label="Edit Profile">
|
||||||
this.setState({ login: event.target.value });
|
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
||||||
};
|
<Input name="name" ref={register({ required: true })} placeholder="Name" defaultValue={user.name} />
|
||||||
|
</Field>
|
||||||
onSubmitProfileUpdate = (event: MouseEvent<HTMLInputElement>) => {
|
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
|
||||||
event.preventDefault();
|
<Input
|
||||||
this.props.updateProfile({ ...this.state });
|
name="email"
|
||||||
};
|
ref={register({ required: true })}
|
||||||
|
placeholder="Email"
|
||||||
render() {
|
defaultValue={user.email}
|
||||||
const { name, email, login } = this.state;
|
suffix={<InputSuffix />}
|
||||||
const { isSavingUser } = this.props;
|
/>
|
||||||
const { disableLoginForm } = config;
|
</Field>
|
||||||
|
<Field label="Username" disabled={disableLoginForm}>
|
||||||
return (
|
<Input
|
||||||
<>
|
name="login"
|
||||||
<h3 className="page-sub-heading">Edit Profile</h3>
|
ref={register}
|
||||||
<form name="userForm" className="gf-form-group">
|
defaultValue={user.login}
|
||||||
<div className="gf-form max-width-30">
|
placeholder="Username"
|
||||||
<InlineFormLabel className="width-8">Name</InlineFormLabel>
|
suffix={<InputSuffix />}
|
||||||
<Input className="gf-form-input max-width-22" type="text" onChange={this.onNameChange} value={name} />
|
/>
|
||||||
</div>
|
</Field>
|
||||||
<div className="gf-form max-width-30">
|
<div className="gf-form-button-row">
|
||||||
<InlineFormLabel className="width-8">Email</InlineFormLabel>
|
<Button variant="primary" disabled={isSavingUser}>
|
||||||
<Input
|
Save
|
||||||
className="gf-form-input max-width-22"
|
</Button>
|
||||||
type="text"
|
</div>
|
||||||
onChange={this.onEmailChange}
|
</FieldSet>
|
||||||
value={email}
|
);
|
||||||
disabled={disableLoginForm}
|
}}
|
||||||
/>
|
</Form>
|
||||||
{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>
|
|
||||||
<Input
|
|
||||||
className="gf-form-input max-width-22"
|
|
||||||
type="text"
|
|
||||||
onChange={this.onLoginChange}
|
|
||||||
value={login}
|
|
||||||
disabled={disableLoginForm}
|
|
||||||
/>
|
|
||||||
{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-button-row">
|
|
||||||
<Button variant="primary" onClick={this.onSubmitProfileUpdate} disabled={isSavingUser}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserProfileEditForm;
|
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', {
|
.when('/profile', {
|
||||||
templateUrl: 'public/app/features/profile/partials/profile.html',
|
template: '<react-container />',
|
||||||
controller: 'ProfileCtrl',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
reloadOnSearch: false,
|
reloadOnSearch: false,
|
||||||
|
resolve: {
|
||||||
|
component: () =>
|
||||||
|
SafeDynamicImport(import(/* webpackChunkName: "UserProfileEdit" */ 'app/features/profile/UserProfileEdit')),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/profile/password', {
|
.when('/profile/password', {
|
||||||
template: '<react-container />',
|
template: '<react-container />',
|
||||||
@ -313,7 +315,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
|||||||
resolve: {
|
resolve: {
|
||||||
component: () =>
|
component: () =>
|
||||||
SafeDynamicImport(
|
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,
|
reloadOnSearch: false,
|
||||||
resolve: {
|
resolve: {
|
||||||
component: () =>
|
component: () =>
|
||||||
SafeDynamicImport(import(/* webPackChunkName: "SelectOrgPage" */ 'app/features/org/SelectOrgPage')),
|
SafeDynamicImport(import(/* webpackChunkName: "SelectOrgPage" */ 'app/features/org/SelectOrgPage')),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// ADMIN
|
// ADMIN
|
||||||
|
Loading…
Reference in New Issue
Block a user