|
|
|
|
@ -1,17 +1,19 @@ |
|
|
|
|
import React, { PureComponent } from 'react'; |
|
|
|
|
import React, { PureComponent } from 'react'; |
|
|
|
|
import ReactDOMServer from 'react-dom/server'; |
|
|
|
|
import { connect } from 'react-redux'; |
|
|
|
|
import { hot } from 'react-hot-loader'; |
|
|
|
|
import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; |
|
|
|
|
import { getNavModel } from 'app/core/selectors/navModel'; |
|
|
|
|
import { getApiKeys } from './state/selectors'; |
|
|
|
|
import { getApiKeys, getApiKeysCount } from './state/selectors'; |
|
|
|
|
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; |
|
|
|
|
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
|
|
|
|
import SlideDown from 'app/core/components/Animations/SlideDown'; |
|
|
|
|
import PageLoader from 'app/core/components/PageLoader/PageLoader'; |
|
|
|
|
import SlideDown, { defaultStyle as slideDownDefaultStyle } from 'app/core/components/Animations/SlideDown'; |
|
|
|
|
import ApiKeysAddedModal from './ApiKeysAddedModal'; |
|
|
|
|
import config from 'app/core/config'; |
|
|
|
|
import appEvents from 'app/core/app_events'; |
|
|
|
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; |
|
|
|
|
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; |
|
|
|
|
|
|
|
|
|
export interface Props { |
|
|
|
|
navModel: NavModel; |
|
|
|
|
@ -22,6 +24,7 @@ export interface Props { |
|
|
|
|
deleteApiKey: typeof deleteApiKey; |
|
|
|
|
setSearchQuery: typeof setSearchQuery; |
|
|
|
|
addApiKey: typeof addApiKey; |
|
|
|
|
apiKeysCount: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export interface State { |
|
|
|
|
@ -101,115 +104,147 @@ export class ApiKeysPage extends PureComponent<Props, any> { |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
renderTable() { |
|
|
|
|
const { apiKeys } = this.props; |
|
|
|
|
|
|
|
|
|
return [ |
|
|
|
|
<h3 key="header" className="page-heading"> |
|
|
|
|
Existing Keys |
|
|
|
|
</h3>, |
|
|
|
|
<table key="table" className="filter-table"> |
|
|
|
|
<thead> |
|
|
|
|
<tr> |
|
|
|
|
<th>Name</th> |
|
|
|
|
<th>Role</th> |
|
|
|
|
<th style={{ width: '34px' }} /> |
|
|
|
|
</tr> |
|
|
|
|
</thead> |
|
|
|
|
{apiKeys.length > 0 && ( |
|
|
|
|
<tbody> |
|
|
|
|
{apiKeys.map(key => { |
|
|
|
|
return ( |
|
|
|
|
<tr key={key.id}> |
|
|
|
|
<td>{key.name}</td> |
|
|
|
|
<td>{key.role}</td> |
|
|
|
|
<td> |
|
|
|
|
<a onClick={() => this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> |
|
|
|
|
<i className="fa fa-remove" /> |
|
|
|
|
</a> |
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
); |
|
|
|
|
})} |
|
|
|
|
</tbody> |
|
|
|
|
renderEmptyList() { |
|
|
|
|
const { isAdding } = this.state; |
|
|
|
|
return ( |
|
|
|
|
<div className="page-container page-body"> |
|
|
|
|
{!isAdding && ( |
|
|
|
|
<EmptyListCTA |
|
|
|
|
model={{ |
|
|
|
|
title: "You haven't added any API Keys yet.", |
|
|
|
|
buttonIcon: 'fa fa-plus', |
|
|
|
|
buttonLink: '#', |
|
|
|
|
onClick: this.onToggleAdding, |
|
|
|
|
buttonTitle: ' New API Key', |
|
|
|
|
proTip: 'Remember you can provide view-only API access to other applications.', |
|
|
|
|
proTipLink: '', |
|
|
|
|
proTipLinkTitle: '', |
|
|
|
|
proTipTarget: '_blank', |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
)} |
|
|
|
|
</table>, |
|
|
|
|
]; |
|
|
|
|
{this.renderAddApiKeyForm()} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
render() { |
|
|
|
|
renderAddApiKeyForm() { |
|
|
|
|
const { newApiKey, isAdding } = this.state; |
|
|
|
|
const { hasFetched, navModel, searchQuery } = this.props; |
|
|
|
|
const slideDownStyle = isAdding ? slideDownDefaultStyle : { ...slideDownDefaultStyle, transition: 'unset' }; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div> |
|
|
|
|
<PageHeader model={navModel} /> |
|
|
|
|
<div className="page-container page-body"> |
|
|
|
|
<div className="page-action-bar"> |
|
|
|
|
<div className="gf-form gf-form--grow"> |
|
|
|
|
<label className="gf-form--has-input-icon gf-form--grow"> |
|
|
|
|
<SlideDown in={isAdding} style={slideDownStyle}> |
|
|
|
|
<div className="cta-form"> |
|
|
|
|
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}> |
|
|
|
|
<i className="fa fa-close" /> |
|
|
|
|
</button> |
|
|
|
|
<h5>Add API Key</h5> |
|
|
|
|
<form className="gf-form-group" onSubmit={this.onAddApiKey}> |
|
|
|
|
<div className="gf-form-inline"> |
|
|
|
|
<div className="gf-form max-width-21"> |
|
|
|
|
<span className="gf-form-label">Key name</span> |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="gf-form-input" |
|
|
|
|
placeholder="Search keys" |
|
|
|
|
value={searchQuery} |
|
|
|
|
onChange={this.onSearchQueryChange} |
|
|
|
|
value={newApiKey.name} |
|
|
|
|
placeholder="Name" |
|
|
|
|
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} |
|
|
|
|
/> |
|
|
|
|
<i className="gf-form-input-icon fa fa-search" /> |
|
|
|
|
</label> |
|
|
|
|
</div> |
|
|
|
|
<div className="gf-form"> |
|
|
|
|
<span className="gf-form-label">Role</span> |
|
|
|
|
<span className="gf-form-select-wrapper"> |
|
|
|
|
<select |
|
|
|
|
className="gf-form-input gf-size-auto" |
|
|
|
|
value={newApiKey.role} |
|
|
|
|
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Role)} |
|
|
|
|
> |
|
|
|
|
{Object.keys(OrgRole).map(role => { |
|
|
|
|
return ( |
|
|
|
|
<option key={role} label={role} value={role}> |
|
|
|
|
{role} |
|
|
|
|
</option> |
|
|
|
|
); |
|
|
|
|
})} |
|
|
|
|
</select> |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="gf-form"> |
|
|
|
|
<button className="btn gf-form-btn btn-success">Add</button> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</form> |
|
|
|
|
</div> |
|
|
|
|
</SlideDown> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
<div className="page-action-bar__spacer" /> |
|
|
|
|
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}> |
|
|
|
|
<i className="fa fa-plus" /> Add API Key |
|
|
|
|
</button> |
|
|
|
|
renderApiKeyList() { |
|
|
|
|
const { isAdding } = this.state; |
|
|
|
|
const { apiKeys, searchQuery } = this.props; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div className="page-container page-body"> |
|
|
|
|
<div className="page-action-bar"> |
|
|
|
|
<div className="gf-form gf-form--grow"> |
|
|
|
|
<label className="gf-form--has-input-icon gf-form--grow"> |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="gf-form-input" |
|
|
|
|
placeholder="Search keys" |
|
|
|
|
value={searchQuery} |
|
|
|
|
onChange={this.onSearchQueryChange} |
|
|
|
|
/> |
|
|
|
|
<i className="gf-form-input-icon fa fa-search" /> |
|
|
|
|
</label> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<SlideDown in={isAdding}> |
|
|
|
|
<div className="cta-form"> |
|
|
|
|
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}> |
|
|
|
|
<i className="fa fa-close" /> |
|
|
|
|
</button> |
|
|
|
|
<h5>Add API Key</h5> |
|
|
|
|
<form className="gf-form-group" onSubmit={this.onAddApiKey}> |
|
|
|
|
<div className="gf-form-inline"> |
|
|
|
|
<div className="gf-form max-width-21"> |
|
|
|
|
<span className="gf-form-label">Key name</span> |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="gf-form-input" |
|
|
|
|
value={newApiKey.name} |
|
|
|
|
placeholder="Name" |
|
|
|
|
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
<div className="gf-form"> |
|
|
|
|
<span className="gf-form-label">Role</span> |
|
|
|
|
<span className="gf-form-select-wrapper"> |
|
|
|
|
<select |
|
|
|
|
className="gf-form-input gf-size-auto" |
|
|
|
|
value={newApiKey.role} |
|
|
|
|
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Role)} |
|
|
|
|
> |
|
|
|
|
{Object.keys(OrgRole).map(role => { |
|
|
|
|
return ( |
|
|
|
|
<option key={role} label={role} value={role}> |
|
|
|
|
{role} |
|
|
|
|
</option> |
|
|
|
|
); |
|
|
|
|
})} |
|
|
|
|
</select> |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="gf-form"> |
|
|
|
|
<button className="btn gf-form-btn btn-success">Add</button> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</form> |
|
|
|
|
</div> |
|
|
|
|
</SlideDown> |
|
|
|
|
{hasFetched ? this.renderTable() : <PageLoader pageName="Api keys" />} |
|
|
|
|
<div className="page-action-bar__spacer" /> |
|
|
|
|
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}> |
|
|
|
|
<i className="fa fa-plus" /> Add API Key |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{this.renderAddApiKeyForm()} |
|
|
|
|
|
|
|
|
|
<h3 className="page-heading">Existing Keys</h3> |
|
|
|
|
<table className="filter-table"> |
|
|
|
|
<thead> |
|
|
|
|
<tr> |
|
|
|
|
<th>Name</th> |
|
|
|
|
<th>Role</th> |
|
|
|
|
<th style={{ width: '34px' }} /> |
|
|
|
|
</tr> |
|
|
|
|
</thead> |
|
|
|
|
{apiKeys.length > 0 ? ( |
|
|
|
|
<tbody> |
|
|
|
|
{apiKeys.map(key => { |
|
|
|
|
return ( |
|
|
|
|
<tr key={key.id}> |
|
|
|
|
<td>{key.name}</td> |
|
|
|
|
<td>{key.role}</td> |
|
|
|
|
<td> |
|
|
|
|
<DeleteButton onConfirmDelete={() => this.onDeleteApiKey(key)} /> |
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
); |
|
|
|
|
})} |
|
|
|
|
</tbody> |
|
|
|
|
) : null} |
|
|
|
|
</table> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
render() { |
|
|
|
|
const { hasFetched, navModel, apiKeysCount } = this.props; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div> |
|
|
|
|
<PageHeader model={navModel} /> |
|
|
|
|
{hasFetched ? |
|
|
|
|
(apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList()) |
|
|
|
|
: <PageLoader pageName="Api keys" />} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
@ -220,7 +255,8 @@ function mapStateToProps(state) { |
|
|
|
|
navModel: getNavModel(state.navIndex, 'apikeys'), |
|
|
|
|
apiKeys: getApiKeys(state.apiKeys), |
|
|
|
|
searchQuery: state.apiKeys.searchQuery, |
|
|
|
|
hasFetched: state.apiKeys.hasFetched, |
|
|
|
|
apiKeysCount: getApiKeysCount(state.apiKeys), |
|
|
|
|
hasFetched: state.apiKeys.hasFetched |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|