Skip to content

Commit

Permalink
Merge branch 'empty-cta-apikeys' of https://github.com/mondras/grafana
Browse files Browse the repository at this point in the history
…into mondras-empty-cta-apikeys
  • Loading branch information
torkelo committed Oct 29, 2018
2 parents e47de56 + 35688b2 commit 1a65046
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 409 deletions.
11 changes: 8 additions & 3 deletions public/app/core/components/Animations/SlideDown.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import React from 'react';
import Transition from 'react-transition-group/Transition';

interface Style {
transition?: string;
overflow?: string;
}

const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value.
// If this is not enough, pass in <SlideDown maxHeight="....
const defaultDuration = 200;
const defaultStyle = {
export const defaultStyle: Style = {
transition: `max-height ${defaultDuration}ms ease-in-out`,
overflow: 'hidden',
};

export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
// There are 4 main states a Transition can be in:
// ENTERING, ENTERED, EXITING, EXITED
// https://reactcommunity.org/react-transition-group/
Expand All @@ -25,7 +30,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
{state => (
<div
style={{
...defaultStyle,
...style,
...transitionStyles[state],
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const model = {
buttonIcon: 'ga css class',
buttonLink: 'http://url/to/destination',
buttonTitle: 'Click me',
onClick: jest.fn(),
proTip: 'This is a tip',
proTipLink: 'http://url/to/tip/destination',
proTipLinkTitle: 'Learn more',
Expand Down
3 changes: 2 additions & 1 deletion public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class EmptyListCTA extends Component<Props, any> {
buttonIcon,
buttonLink,
buttonTitle,
onClick,
proTip,
proTipLink,
proTipLinkTitle,
Expand All @@ -19,7 +20,7 @@ class EmptyListCTA extends Component<Props, any> {
return (
<div className="empty-list-cta">
<div className="empty-list-cta__title">{title}</div>
<a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
<a onClick={onClick} href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
<i className={buttonIcon} />
{buttonTitle}
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`EmptyListCTA renders correctly 1`] = `
<a
className="empty-list-cta__button btn btn-xlarge btn-success"
href="http://url/to/destination"
onClick={[MockFunction]}
>
<i
className="ga css class"
Expand Down
16 changes: 11 additions & 5 deletions public/app/features/api-keys/ApiKeysPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React from 'react';
import { shallow } from 'enzyme';
import { Props, ApiKeysPage } from './ApiKeysPage';
import { NavModel, ApiKey } from 'app/types';
Expand All @@ -14,6 +14,7 @@ const setup = (propOverrides?: object) => {
deleteApiKey: jest.fn(),
setSearchQuery: jest.fn(),
addApiKey: jest.fn(),
apiKeysCount: 0,
};

Object.assign(props, propOverrides);
Expand All @@ -28,14 +29,19 @@ const setup = (propOverrides?: object) => {
};

describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
it('should render API keys table if there are any keys', () => {
const { wrapper } = setup({
apiKeys: getMultipleMockKeys(5),
apiKeysCount: 5,
});

expect(wrapper).toMatchSnapshot();
});

it('should render API keys table', () => {
it('should render CTA if there are no API keys', () => {
const { wrapper } = setup({
apiKeys: getMultipleMockKeys(5),
apiKeys: getMultipleMockKeys(0),
apiKeysCount: 0,
hasFetched: true,
});

Expand Down
232 changes: 134 additions & 98 deletions public/app/features/api-keys/ApiKeysPage.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,6 +24,7 @@ export interface Props {
deleteApiKey: typeof deleteApiKey;
setSearchQuery: typeof setSearchQuery;
addApiKey: typeof addApiKey;
apiKeysCount: number;
}

export interface State {
Expand Down Expand Up @@ -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>
);
}
Expand All @@ -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
};
}

Expand Down
Loading

0 comments on commit 1a65046

Please sign in to comment.