feat: handle API rate limit

This commit is contained in:
EnixCoda 2018-10-27 19:05:31 +08:00
parent 68b78216c9
commit 6916850d85
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
4 changed files with 97 additions and 40 deletions

View file

@ -89,7 +89,14 @@ export default class SettingsBar extends React.PureComponent {
onAccessTokenChange(accessToken)
this.setState({
accessToken: '',
accessTokenHint: <span><a href="#" onClick={() => window.location.reload()}>Reload</a> to activate!</span>,
accessTokenHint: (
<span>
<a href="#" onClick={() => window.location.reload()}>
Reload
</a>{' '}
to activate!
</span>
),
})
}
}
@ -126,38 +133,44 @@ export default class SettingsBar extends React.PureComponent {
const { setCompressSingleton } = this.props
setCompressSingleton(compress)
this.setState({
compressHint: <span>Saved, <a href="#" onClick={() => window.location.reload()}>reload</a> to apply.</span>,
compressHint: (
<span>
Saved,{' '}
<a href="#" onClick={() => window.location.reload()}>
reload
</a>{' '}
to apply.
</span>
),
})
}
render() {
const { accessTokenHint, toggleShowSideBarShortcut, compressSingletonFolder, shortcutHint, accessToken, compressHint } = this.state
const {
accessTokenHint,
toggleShowSideBarShortcut,
compressSingletonFolder,
shortcutHint,
accessToken,
compressHint,
} = this.state
const { toggleShowSettings, activated, accessToken: hasAccessToken } = this.props
return (
<div className={'gitako-settings-bar'}>
{activated && (
<React.Fragment>
<h3 className={'gitako-settings-bar-title'}>
Settings
</h3>
<h3 className={'gitako-settings-bar-title'}>Settings</h3>
<div className={'gitako-settings-bar-content'}>
<div className={'shadow-shelter'} />
<div className={'gitako-settings-bar-content-section access-token'}>
<h4>Access Token</h4>
<span>With access token provided, Gitako can access more repositories.</span>
<br />
<a href="https://github.com/blog/1509-personal-api-tokens" target="_blank">
Help: how to create access token?
<a
href="https://github.com/EnixCoda/Gitako/wiki/How-to-create-access-token-for-Gitako%3F"
target="_blank"
>
Why & how to create it?
</a>
<br />
<span>
Gitako stores the token in&nbsp;
<a href="https://developer.chrome.com/apps/storage" target="_blank">
chrome local storage
</a>
&nbsp;locally and safely.
</span>
<br />
<div className={'access-token-input-control'}>
<input
className={'access-token-input form-control'}
@ -199,10 +212,18 @@ export default class SettingsBar extends React.PureComponent {
<div className={'gitako-settings-bar-content-section singleton'}>
<h4>
Compress singleton folder&nbsp;
<a href={wikiLinks.compressSingletonFolder} target={'_blank'}>(?)</a>
<a href={wikiLinks.compressSingletonFolder} target={'_blank'}>
(?)
</a>
</h4>
<label htmlFor={'compress-singleton-folder'}>
<input id={'compress-singleton-folder'} name={'compress-singleton-folder'} type={'checkbox'} onChange={this.setCompressSingletonFolder} checked={compressSingletonFolder} />
<input
id={'compress-singleton-folder'}
name={'compress-singleton-folder'}
type={'checkbox'}
onChange={this.setCompressSingletonFolder}
checked={compressSingletonFolder}
/>
&nbsp; {compressSingletonFolder ? 'enabled' : 'disabled'}
</label>
{compressHint && <div className={'hint'}>{compressHint}</div>}
@ -220,7 +241,12 @@ export default class SettingsBar extends React.PureComponent {
</React.Fragment>
)}
<div className={'placeholder-row'}>
<a className={'version'} href={wikiLinks.changeLog} target={'_blank'} title={'Check out new features!'}>
<a
className={'version'}
href={wikiLinks.changeLog}
target={'_blank'}
title={'Check out new features!'}
>
v{version}
</a>
{activated ? (
@ -230,11 +256,7 @@ export default class SettingsBar extends React.PureComponent {
onClick={toggleShowSettings}
/>
) : (
<Icon
type={'gear'}
className={'show-settings-icon'}
onClick={toggleShowSettings}
/>
<Icon type={'gear'} className={'show-settings-icon'} onClick={toggleShowSettings} />
)}
</div>
</div>

View file

@ -79,23 +79,46 @@ export default class Gitako extends React.PureComponent {
<div className={'description'}>
<h5>Access Denied</h5>
<p>
Gitako needs access token with proper scopes (recommended: repo) to access this
repository. Please save it in the settings below.
Due to{' '}
<a target="_blank" href="https://developer.github.com/v3/#rate-limiting">
limitation of GitHub
</a>{' '}
or{' '}
<a target="_blank" href="https://developer.github.com/v3/#authentication">
auth needs
</a>
, Gitako needs access token to continue. Please follow the instructions in the settings
panel below.
</p>
</div>
)
}
renderContent() {
const { errorDueToAuth, metaData, treeData, showSettings, accessToken, compressSingletonFolder, toggleShowSettings } = this.props
const {
errorDueToAuth,
metaData,
treeData,
showSettings,
accessToken,
compressSingletonFolder,
toggleShowSettings,
} = this.props
return (
<div className={'gitako-side-bar-content'}>
{metaData && <MetaBar metaData={metaData} />}
{errorDueToAuth
? this.renderAccessDeniedError()
: metaData &&
<FileExplorer toggleShowSettings={toggleShowSettings} metaData={metaData} treeData={treeData} freeze={showSettings} accessToken={accessToken} compressSingletonFolder={compressSingletonFolder} />
}
: metaData && (
<FileExplorer
toggleShowSettings={toggleShowSettings}
metaData={metaData}
treeData={treeData}
freeze={showSettings}
accessToken={accessToken}
compressSingletonFolder={compressSingletonFolder}
/>
)}
</div>
)
}
@ -119,7 +142,11 @@ export default class Gitako extends React.PureComponent {
return (
<div className={'gitako-side-bar'}>
<Portal into={logoContainerElement}>
<ToggleShowButton error={error} shouldShow={shouldShow} toggleShowSideBar={toggleShowSideBar} />
<ToggleShowButton
error={error}
shouldShow={shouldShow}
toggleShowSideBar={toggleShowSideBar}
/>
</Portal>
<Resizable className={cx({ hidden: error || !shouldShow })} baseSize={baseSize}>
<div className={'gitako-side-bar-body'}>
@ -133,7 +160,7 @@ export default class Gitako extends React.PureComponent {
compressSingletonFolder={compressSingletonFolder}
toggleShowSideBarShortcut={toggleShowSideBarShortcut}
setCompressSingleton={setCompressSingleton}
/>
/>
</div>
</Resizable>
</div>

View file

@ -1,5 +1,5 @@
import DOMHelper, { REPO_TYPE_PRIVATE } from 'utils/DOMHelper'
import GitHubHelper, { NOT_FOUND, BAD_CREDENTIALS } from 'utils/GitHubHelper'
import GitHubHelper, { NOT_FOUND, BAD_CREDENTIALS, API_RATE_LIMIT } from 'utils/GitHubHelper'
import configHelper from 'utils/configHelper'
import URLHelper from 'utils/URLHelper'
import keyHelper from 'utils/keyHelper'
@ -53,9 +53,9 @@ const init = dispatch => async () => {
const handleError = dispatch => async (err) => {
// TODO: detect request time exceeds limit
if (err.message === NOT_FOUND || err.message === BAD_CREDENTIALS) {
if (err.message === NOT_FOUND || err.message === BAD_CREDENTIALS || err.message === API_RATE_LIMIT ) {
const repoPageType = await DOMHelper.getRepoPageType()
const errorDueToAuth = repoPageType === REPO_TYPE_PRIVATE || err.message === BAD_CREDENTIALS
const errorDueToAuth = repoPageType === REPO_TYPE_PRIVATE || err.message === BAD_CREDENTIALS || err.message === API_RATE_LIMIT
dispatch({ errorDueToAuth })
dispatch(setShowSettings, true)
dispatch(setShouldShow, errorDueToAuth)

View file

@ -1,6 +1,11 @@
import { raiseError } from 'analytics'
export const NOT_FOUND = 'Repo Not Found'
export const BAD_CREDENTIALS = 'Bad credentials'
export const API_RATE_LIMIT = `API rate limit`
function apiRateLimitExceeded(content) {
return content && content['documentation_url'] === 'https://developer.github.com/v3/#rate-limiting'
}
async function request(url, { accessToken } = {}) {
const headers = {}
@ -11,9 +16,12 @@ async function request(url, { accessToken } = {}) {
if (res.status === 200) return res.json()
// for private repo, GitHub api also responses with 404 when unauthorized
else if (res.status === 404) throw new Error(NOT_FOUND)
else if (!res.ok) raiseError(new Error(`Got ${res.statusText} when requesting ${url}`))
const content = await res.json()
throw new Error(content && content.message)
else {
const content = await res.json()
if (apiRateLimitExceeded(content)) throw new Error(API_RATE_LIMIT)
else if (!res.ok) raiseError(new Error(`Got ${res.statusText} when requesting ${url}`))
throw new Error(content && content.message)
}
}
async function getRepoMeta({ userName, repoName, accessToken }) {