Add some tooltips to OAuth form

This commit is contained in:
Gregory Schier 2017-07-21 13:11:01 -07:00
parent 4c2322f808
commit 483ca03668
3 changed files with 101 additions and 48 deletions

View File

@ -1,22 +1,46 @@
import React, {PropTypes, PureComponent} from 'react'; // @flow
import type {Request} from '../../../../models/request';
import type {OAuth2Token} from '../../../../models/o-auth-2-token';
import React from 'react';
import moment from 'moment'; import moment from 'moment';
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import OneLineEditor from '../../codemirror/one-line-editor'; import OneLineEditor from '../../codemirror/one-line-editor';
import * as misc from '../../../../common/misc'; import * as misc from '../../../../common/misc';
import {GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_PASSWORD, GRANT_TYPE_IMPLICIT} from '../../../../network/o-auth-2/constants'; import {GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_IMPLICIT, GRANT_TYPE_PASSWORD} from '../../../../network/o-auth-2/constants';
import authorizationUrls from '../../../../datasets/authorization-urls'; import authorizationUrls from '../../../../datasets/authorization-urls';
import accessTokenUrls from '../../../../datasets/access-token-urls'; import accessTokenUrls from '../../../../datasets/access-token-urls';
import getAccessToken from '../../../../network/o-auth-2/get-token'; import getAccessToken from '../../../../network/o-auth-2/get-token';
import * as models from '../../../../models'; import * as models from '../../../../models';
import Link from '../../base/link'; import Link from '../../base/link';
import {trackEvent} from '../../../../analytics/index'; import {trackEvent} from '../../../../analytics/index';
import HelpTooltip from '../../help-tooltip';
const getAuthorizationUrls = () => authorizationUrls; const getAuthorizationUrls = () => authorizationUrls;
const getAccessTokenUrls = () => accessTokenUrls; const getAccessTokenUrls = () => accessTokenUrls;
@autobind @autobind
class OAuth2 extends PureComponent { class OAuth2 extends React.PureComponent {
constructor (props) { props: {
handleRender: Function,
handleGetRenderContext: Function,
handleUpdateSettingsShowPasswords: Function,
onChange: Function,
request: Request,
showPasswords: boolean,
// Optional
oAuth2Token: OAuth2Token | null
};
state: {
error: string,
loading: boolean
};
_handleChangeProperty: Function;
constructor (props: any) {
super(props); super(props);
this.state = { this.state = {
@ -27,36 +51,36 @@ class OAuth2 extends PureComponent {
this._handleChangeProperty = misc.debounce(this._handleChangeProperty, 500); this._handleChangeProperty = misc.debounce(this._handleChangeProperty, 500);
} }
async _handleUpdateAccessToken (e) { async _handleUpdateAccessToken (e: Event & {target: HTMLButtonElement}): Promise<void> {
const {oAuth2Token} = this.props; const {oAuth2Token} = this.props;
const accessToken = e.target.value; const accessToken = e.target.value;
if (oAuth2Token) { if (oAuth2Token) {
await models.oAuth2Token.update(this.props.oAuth2Token, {accessToken}); await models.oAuth2Token.update(oAuth2Token, {accessToken});
} else { } else {
await models.oAuth2Token.create({accessToken, parentId: this.props.request._id}); await models.oAuth2Token.create({accessToken, parentId: this.props.request._id});
} }
} }
async _handleUpdateRefreshToken (e) { async _handleUpdateRefreshToken (e: Event & {target: HTMLButtonElement}): Promise<void> {
const {oAuth2Token} = this.props; const {oAuth2Token} = this.props;
const refreshToken = e.target.value; const refreshToken = e.target.value;
if (oAuth2Token) { if (oAuth2Token) {
await models.oAuth2Token.update(this.props.oAuth2Token, {refreshToken}); await models.oAuth2Token.update(oAuth2Token, {refreshToken});
} else { } else {
await models.oAuth2Token.create({refreshToken, parentId: this.props.request._id}); await models.oAuth2Token.create({refreshToken, parentId: this.props.request._id});
} }
} }
async _handleClearTokens () { async _handleClearTokens (): Promise<void> {
const oAuth2Token = await models.oAuth2Token.getByParentId(this.props.request._id); const oAuth2Token = await models.oAuth2Token.getByParentId(this.props.request._id);
if (oAuth2Token) { if (oAuth2Token) {
await models.oAuth2Token.remove(oAuth2Token); await models.oAuth2Token.remove(oAuth2Token);
} }
} }
async _handleRefreshToken () { async _handleRefreshToken (): Promise<void> {
// First, clear the state and the current tokens // First, clear the state and the current tokens
this.setState({error: '', loading: true}); this.setState({error: '', loading: true});
@ -64,73 +88,82 @@ class OAuth2 extends PureComponent {
try { try {
const authentication = await this.props.handleRender(request.authentication); const authentication = await this.props.handleRender(request.authentication);
const oAuth2Token = await getAccessToken(request._id, authentication, true); await getAccessToken(request._id, authentication, true);
this.setState({token: oAuth2Token, loading: false}); this.setState({loading: false});
} catch (err) { } catch (err) {
await this._handleClearTokens(); // Clear existing tokens if there's an error await this._handleClearTokens(); // Clear existing tokens if there's an error
this.setState({error: err.message, loading: false}); this.setState({error: err.message, loading: false});
} }
} }
_handleChangeProperty (property, value) { _handleChangeProperty (property: string, value: string | boolean): void {
const {request} = this.props; const {request} = this.props;
const authentication = Object.assign({}, request.authentication, {[property]: value}); const authentication = Object.assign({}, request.authentication, {[property]: value});
this.props.onChange(authentication); this.props.onChange(authentication);
} }
_handleChangeClientId (value) { _handleChangeClientId (value: string): void {
this._handleChangeProperty('clientId', value); this._handleChangeProperty('clientId', value);
} }
_handleChangeCredentialsInBody (e) { _handleChangeCredentialsInBody (e: Event & {target: HTMLButtonElement}): void {
this._handleChangeProperty('credentialsInBody', e.target.value === 'true'); this._handleChangeProperty('credentialsInBody', e.target.value === 'true');
} }
_handleChangeClientSecret (value) { _handleChangeClientSecret (value: string): void {
this._handleChangeProperty('clientSecret', value); this._handleChangeProperty('clientSecret', value);
} }
_handleChangeAuthorizationUrl (value) { _handleChangeAuthorizationUrl (value: string): void {
this._handleChangeProperty('authorizationUrl', value); this._handleChangeProperty('authorizationUrl', value);
} }
_handleChangeAccessTokenUrl (value) { _handleChangeAccessTokenUrl (value: string): void {
this._handleChangeProperty('accessTokenUrl', value); this._handleChangeProperty('accessTokenUrl', value);
} }
_handleChangeRedirectUrl (value) { _handleChangeRedirectUrl (value: string): void {
this._handleChangeProperty('redirectUrl', value); this._handleChangeProperty('redirectUrl', value);
} }
_handleChangeScope (value) { _handleChangeScope (value: string): void {
this._handleChangeProperty('scope', value); this._handleChangeProperty('scope', value);
} }
_handleChangeState (value) { _handleChangeState (value: string): void {
this._handleChangeProperty('state', value); this._handleChangeProperty('state', value);
} }
_handleChangeUsername (value) { _handleChangeUsername (value: string): void {
this._handleChangeProperty('username', value); this._handleChangeProperty('username', value);
} }
_handleChangePassword (value) { _handleChangePassword (value: string): void {
this._handleChangeProperty('password', value); this._handleChangeProperty('password', value);
} }
_handleChangeGrantType (e) { _handleChangeGrantType (e: Event & {target: HTMLButtonElement}): void {
trackEvent('OAuth 2', 'Change Grant Type', e.target.value); trackEvent('OAuth 2', 'Change Grant Type', e.target.value);
this._handleChangeProperty('grantType', e.target.value); this._handleChangeProperty('grantType', e.target.value);
} }
renderInputRow (label, property, onChange, handleAutocomplete = null) { renderInputRow (
label: string,
property: string,
onChange: Function,
help: string | null = null,
handleAutocomplete: Function | null = null
): React.Element<*> {
const {handleRender, handleGetRenderContext, request} = this.props; const {handleRender, handleGetRenderContext, request} = this.props;
const id = label.replace(/ /g, '-'); const id = label.replace(/ /g, '-');
const type = !this.props.showPasswords && property === 'password' ? 'password' : 'text'; const type = !this.props.showPasswords && property === 'password' ? 'password' : 'text';
return ( return (
<tr key={id}> <tr key={id}>
<td className="pad-right no-wrap valign-middle"> <td className="pad-right no-wrap valign-middle">
<label htmlFor={id} className="label--small no-pad">{label}</label> <label htmlFor={id} className="label--small no-pad">
{label}
{help && <HelpTooltip>{help}</HelpTooltip>}
</label>
</td> </td>
<td className="wide"> <td className="wide">
<div className="form-control form-control--underlined no-margin"> <div className="form-control form-control--underlined no-margin">
@ -149,7 +182,13 @@ class OAuth2 extends PureComponent {
); );
} }
renderSelectRow (label, property, options, onChange) { renderSelectRow (
label: string,
property: string,
options: Array<{name: string, value: string}>,
onChange: Function,
help: string | null = null
): React.Element<*> {
const {request} = this.props; const {request} = this.props;
const id = label.replace(/ /g, '-'); const id = label.replace(/ /g, '-');
const value = request.authentication.hasOwnProperty(property) const value = request.authentication.hasOwnProperty(property)
@ -159,7 +198,10 @@ class OAuth2 extends PureComponent {
return ( return (
<tr key={id}> <tr key={id}>
<td className="pad-right no-wrap valign-middle"> <td className="pad-right no-wrap valign-middle">
<label htmlFor={id} className="label--small no-pad">{label}</label> <label htmlFor={id} className="label--small no-pad">
{label}
{help && <HelpTooltip>{help}</HelpTooltip>}
</label>
</td> </td>
<td className="wide"> <td className="wide">
<div className="form-control form-control--outlined no-margin"> <div className="form-control form-control--outlined no-margin">
@ -174,8 +216,8 @@ class OAuth2 extends PureComponent {
); );
} }
renderGrantTypeFields (grantType) { renderGrantTypeFields (grantType: string): Array<React.Element<*>> {
let fields = null; let fields = [];
const clientId = this.renderInputRow( const clientId = this.renderInputRow(
'Client ID', 'Client ID',
@ -193,6 +235,7 @@ class OAuth2 extends PureComponent {
'Authorization URL', 'Authorization URL',
'authorizationUrl', 'authorizationUrl',
this._handleChangeAuthorizationUrl, this._handleChangeAuthorizationUrl,
null,
getAuthorizationUrls getAuthorizationUrls
); );
@ -200,13 +243,15 @@ class OAuth2 extends PureComponent {
'Access Token URL', 'Access Token URL',
'accessTokenUrl', 'accessTokenUrl',
this._handleChangeAccessTokenUrl, this._handleChangeAccessTokenUrl,
null,
getAccessTokenUrls getAccessTokenUrls
); );
const redirectUri = this.renderInputRow( const redirectUri = this.renderInputRow(
'Redirect URL', 'Redirect URL',
'redirectUrl', 'redirectUrl',
this._handleChangeRedirectUrl this._handleChangeRedirectUrl,
'Insomnia will intercept this no matter what, so it can be whatever you want/need'
); );
const state = this.renderInputRow( const state = this.renderInputRow(
@ -240,7 +285,8 @@ class OAuth2 extends PureComponent {
{name: 'As Basic Auth Header (default)', value: 'false'}, {name: 'As Basic Auth Header (default)', value: 'false'},
{name: 'In Request Body', value: 'true'} {name: 'In Request Body', value: 'true'}
], ],
this._handleChangeCredentialsInBody this._handleChangeCredentialsInBody,
'Whether or not to send credentials as Basic Auth, or as plain text in the request body',
); );
if (grantType === GRANT_TYPE_AUTHORIZATION_CODE) { if (grantType === GRANT_TYPE_AUTHORIZATION_CODE) {
@ -285,7 +331,7 @@ class OAuth2 extends PureComponent {
return fields; return fields;
} }
renderExpireAt (token) { renderExpireAt (token: OAuth2Token | null): React.Element<*> | string | null {
if (!token) { if (!token) {
return null; return null;
} }
@ -387,16 +433,4 @@ class OAuth2 extends PureComponent {
} }
} }
OAuth2.propTypes = {
handleRender: PropTypes.func.isRequired,
handleGetRenderContext: PropTypes.func.isRequired,
handleUpdateSettingsShowPasswords: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
showPasswords: PropTypes.bool.isRequired,
// Optional
oAuth2Token: PropTypes.object
};
export default OAuth2; export default OAuth2;

View File

@ -63,21 +63,22 @@ class Tooltip extends React.PureComponent {
const tooltipRect = tooltip.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect();
const bubbleRect = bubble.getBoundingClientRect(); const bubbleRect = bubble.getBoundingClientRect();
const margin = 3;
switch (this.props.position) { switch (this.props.position) {
case 'right': case 'right':
bubble.style.top = `${tooltipRect.top - (bubbleRect.height / 2) + (tooltipRect.height / 2)}px`; bubble.style.top = `${tooltipRect.top - (bubbleRect.height / 2) + (tooltipRect.height / 2)}px`;
bubble.style.left = `${tooltipRect.left + tooltipRect.width}px`; bubble.style.left = `${tooltipRect.left + tooltipRect.width + margin}px`;
break; break;
case 'bottom': case 'bottom':
bubble.style.top = `${tooltipRect.top + tooltipRect.height}px`; bubble.style.top = `${tooltipRect.top + tooltipRect.height + margin}px`;
bubble.style.left = `${tooltipRect.left - (bubbleRect.width / 2) + (tooltipRect.width / 2)}px`; bubble.style.left = `${tooltipRect.left - (bubbleRect.width / 2) + (tooltipRect.width / 2)}px`;
break; break;
case 'top': case 'top':
default: default:
bubble.style.top = `${tooltipRect.top - bubbleRect.height}px`; bubble.style.top = `${tooltipRect.top - bubbleRect.height - margin}px`;
bubble.style.left = `${tooltipRect.left - (bubbleRect.width / 2) + (tooltipRect.width / 2)}px`; bubble.style.left = `${tooltipRect.left - (bubbleRect.width / 2) + (tooltipRect.width / 2)}px`;
break; break;
} }
@ -89,6 +90,17 @@ class Tooltip extends React.PureComponent {
_handleMouseLeave (): void { _handleMouseLeave (): void {
clearTimeout(this._showTimeout); clearTimeout(this._showTimeout);
this.setState({visible: false}); this.setState({visible: false});
const bubble = ReactDOM.findDOMNode(this._bubble);
if (!bubble || !(bubble instanceof HTMLDivElement)) {
return;
}
// Reset positioning stuff
bubble.style.left = '';
bubble.style.top = '';
bubble.style.bottom = '';
bubble.style.right = '';
} }
_getContainer (): HTMLElement { _getContainer (): HTMLElement {

7
flow-typed/moment.js vendored Normal file
View File

@ -0,0 +1,7 @@
declare type moment = {
fromNow: () => string;
};
declare module 'moment' {
declare module.exports: (date: any) => moment
}