Skip to content

Commit 1a65046

Browse files
committed
Merge branch 'empty-cta-apikeys' of https://github.com/mondras/grafana into mondras-empty-cta-apikeys
2 parents e47de56 + 35688b2 commit 1a65046

File tree

8 files changed

+185
-409
lines changed

8 files changed

+185
-409
lines changed

public/app/core/components/Animations/SlideDown.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import React from 'react';
22
import Transition from 'react-transition-group/Transition';
33

4+
interface Style {
5+
transition?: string;
6+
overflow?: string;
7+
}
8+
49
const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value.
510
// If this is not enough, pass in <SlideDown maxHeight="....
611
const defaultDuration = 200;
7-
const defaultStyle = {
12+
export const defaultStyle: Style = {
813
transition: `max-height ${defaultDuration}ms ease-in-out`,
914
overflow: 'hidden',
1015
};
1116

12-
export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
17+
export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
1318
// There are 4 main states a Transition can be in:
1419
// ENTERING, ENTERED, EXITING, EXITED
1520
// https://reactcommunity.org/react-transition-group/
@@ -25,7 +30,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
2530
{state => (
2631
<div
2732
style={{
28-
...defaultStyle,
33+
...style,
2934
...transitionStyles[state],
3035
}}
3136
>

public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const model = {
77
buttonIcon: 'ga css class',
88
buttonLink: 'http://url/to/destination',
99
buttonTitle: 'Click me',
10+
onClick: jest.fn(),
1011
proTip: 'This is a tip',
1112
proTipLink: 'http://url/to/tip/destination',
1213
proTipLinkTitle: 'Learn more',

public/app/core/components/EmptyListCTA/EmptyListCTA.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class EmptyListCTA extends Component<Props, any> {
1111
buttonIcon,
1212
buttonLink,
1313
buttonTitle,
14+
onClick,
1415
proTip,
1516
proTipLink,
1617
proTipLinkTitle,
@@ -19,7 +20,7 @@ class EmptyListCTA extends Component<Props, any> {
1920
return (
2021
<div className="empty-list-cta">
2122
<div className="empty-list-cta__title">{title}</div>
22-
<a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
23+
<a onClick={onClick} href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
2324
<i className={buttonIcon} />
2425
{buttonTitle}
2526
</a>

public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ exports[`EmptyListCTA renders correctly 1`] = `
1212
<a
1313
className="empty-list-cta__button btn btn-xlarge btn-success"
1414
href="http://url/to/destination"
15+
onClick={[MockFunction]}
1516
>
1617
<i
1718
className="ga css class"

public/app/features/api-keys/ApiKeysPage.test.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React from 'react';
22
import { shallow } from 'enzyme';
33
import { Props, ApiKeysPage } from './ApiKeysPage';
44
import { NavModel, ApiKey } from 'app/types';
@@ -14,6 +14,7 @@ const setup = (propOverrides?: object) => {
1414
deleteApiKey: jest.fn(),
1515
setSearchQuery: jest.fn(),
1616
addApiKey: jest.fn(),
17+
apiKeysCount: 0,
1718
};
1819

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

3031
describe('Render', () => {
31-
it('should render component', () => {
32-
const { wrapper } = setup();
32+
it('should render API keys table if there are any keys', () => {
33+
const { wrapper } = setup({
34+
apiKeys: getMultipleMockKeys(5),
35+
apiKeysCount: 5,
36+
});
37+
3338
expect(wrapper).toMatchSnapshot();
3439
});
3540

36-
it('should render API keys table', () => {
41+
it('should render CTA if there are no API keys', () => {
3742
const { wrapper } = setup({
38-
apiKeys: getMultipleMockKeys(5),
43+
apiKeys: getMultipleMockKeys(0),
44+
apiKeysCount: 0,
3945
hasFetched: true,
4046
});
4147

public/app/features/api-keys/ApiKeysPage.tsx

Lines changed: 134 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import React, { PureComponent } from 'react';
1+
import React, { PureComponent } from 'react';
22
import ReactDOMServer from 'react-dom/server';
33
import { connect } from 'react-redux';
44
import { hot } from 'react-hot-loader';
55
import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types';
66
import { getNavModel } from 'app/core/selectors/navModel';
7-
import { getApiKeys } from './state/selectors';
7+
import { getApiKeys, getApiKeysCount } from './state/selectors';
88
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
99
import PageHeader from 'app/core/components/PageHeader/PageHeader';
10-
import SlideDown from 'app/core/components/Animations/SlideDown';
1110
import PageLoader from 'app/core/components/PageLoader/PageLoader';
11+
import SlideDown, { defaultStyle as slideDownDefaultStyle } from 'app/core/components/Animations/SlideDown';
1212
import ApiKeysAddedModal from './ApiKeysAddedModal';
1313
import config from 'app/core/config';
1414
import appEvents from 'app/core/app_events';
15+
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
16+
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
1517

1618
export interface Props {
1719
navModel: NavModel;
@@ -22,6 +24,7 @@ export interface Props {
2224
deleteApiKey: typeof deleteApiKey;
2325
setSearchQuery: typeof setSearchQuery;
2426
addApiKey: typeof addApiKey;
27+
apiKeysCount: number;
2528
}
2629

2730
export interface State {
@@ -101,115 +104,147 @@ export class ApiKeysPage extends PureComponent<Props, any> {
101104
});
102105
};
103106

104-
renderTable() {
105-
const { apiKeys } = this.props;
106-
107-
return [
108-
<h3 key="header" className="page-heading">
109-
Existing Keys
110-
</h3>,
111-
<table key="table" className="filter-table">
112-
<thead>
113-
<tr>
114-
<th>Name</th>
115-
<th>Role</th>
116-
<th style={{ width: '34px' }} />
117-
</tr>
118-
</thead>
119-
{apiKeys.length > 0 && (
120-
<tbody>
121-
{apiKeys.map(key => {
122-
return (
123-
<tr key={key.id}>
124-
<td>{key.name}</td>
125-
<td>{key.role}</td>
126-
<td>
127-
<a onClick={() => this.onDeleteApiKey(key)} className="btn btn-danger btn-mini">
128-
<i className="fa fa-remove" />
129-
</a>
130-
</td>
131-
</tr>
132-
);
133-
})}
134-
</tbody>
107+
renderEmptyList() {
108+
const { isAdding } = this.state;
109+
return (
110+
<div className="page-container page-body">
111+
{!isAdding && (
112+
<EmptyListCTA
113+
model={{
114+
title: "You haven't added any API Keys yet.",
115+
buttonIcon: 'fa fa-plus',
116+
buttonLink: '#',
117+
onClick: this.onToggleAdding,
118+
buttonTitle: ' New API Key',
119+
proTip: 'Remember you can provide view-only API access to other applications.',
120+
proTipLink: '',
121+
proTipLinkTitle: '',
122+
proTipTarget: '_blank',
123+
}}
124+
/>
135125
)}
136-
</table>,
137-
];
126+
{this.renderAddApiKeyForm()}
127+
</div>
128+
);
138129
}
139130

140-
render() {
131+
renderAddApiKeyForm() {
141132
const { newApiKey, isAdding } = this.state;
142-
const { hasFetched, navModel, searchQuery } = this.props;
133+
const slideDownStyle = isAdding ? slideDownDefaultStyle : { ...slideDownDefaultStyle, transition: 'unset' };
143134

144135
return (
145-
<div>
146-
<PageHeader model={navModel} />
147-
<div className="page-container page-body">
148-
<div className="page-action-bar">
149-
<div className="gf-form gf-form--grow">
150-
<label className="gf-form--has-input-icon gf-form--grow">
136+
<SlideDown in={isAdding} style={slideDownStyle}>
137+
<div className="cta-form">
138+
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
139+
<i className="fa fa-close" />
140+
</button>
141+
<h5>Add API Key</h5>
142+
<form className="gf-form-group" onSubmit={this.onAddApiKey}>
143+
<div className="gf-form-inline">
144+
<div className="gf-form max-width-21">
145+
<span className="gf-form-label">Key name</span>
151146
<input
152147
type="text"
153148
className="gf-form-input"
154-
placeholder="Search keys"
155-
value={searchQuery}
156-
onChange={this.onSearchQueryChange}
149+
value={newApiKey.name}
150+
placeholder="Name"
151+
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)}
157152
/>
158-
<i className="gf-form-input-icon fa fa-search" />
159-
</label>
153+
</div>
154+
<div className="gf-form">
155+
<span className="gf-form-label">Role</span>
156+
<span className="gf-form-select-wrapper">
157+
<select
158+
className="gf-form-input gf-size-auto"
159+
value={newApiKey.role}
160+
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Role)}
161+
>
162+
{Object.keys(OrgRole).map(role => {
163+
return (
164+
<option key={role} label={role} value={role}>
165+
{role}
166+
</option>
167+
);
168+
})}
169+
</select>
170+
</span>
171+
</div>
172+
<div className="gf-form">
173+
<button className="btn gf-form-btn btn-success">Add</button>
174+
</div>
160175
</div>
176+
</form>
177+
</div>
178+
</SlideDown>
179+
);
180+
}
161181

162-
<div className="page-action-bar__spacer" />
163-
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
164-
<i className="fa fa-plus" /> Add API Key
165-
</button>
182+
renderApiKeyList() {
183+
const { isAdding } = this.state;
184+
const { apiKeys, searchQuery } = this.props;
185+
186+
return (
187+
<div className="page-container page-body">
188+
<div className="page-action-bar">
189+
<div className="gf-form gf-form--grow">
190+
<label className="gf-form--has-input-icon gf-form--grow">
191+
<input
192+
type="text"
193+
className="gf-form-input"
194+
placeholder="Search keys"
195+
value={searchQuery}
196+
onChange={this.onSearchQueryChange}
197+
/>
198+
<i className="gf-form-input-icon fa fa-search" />
199+
</label>
166200
</div>
167201

168-
<SlideDown in={isAdding}>
169-
<div className="cta-form">
170-
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
171-
<i className="fa fa-close" />
172-
</button>
173-
<h5>Add API Key</h5>
174-
<form className="gf-form-group" onSubmit={this.onAddApiKey}>
175-
<div className="gf-form-inline">
176-
<div className="gf-form max-width-21">
177-
<span className="gf-form-label">Key name</span>
178-
<input
179-
type="text"
180-
className="gf-form-input"
181-
value={newApiKey.name}
182-
placeholder="Name"
183-
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)}
184-
/>
185-
</div>
186-
<div className="gf-form">
187-
<span className="gf-form-label">Role</span>
188-
<span className="gf-form-select-wrapper">
189-
<select
190-
className="gf-form-input gf-size-auto"
191-
value={newApiKey.role}
192-
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Role)}
193-
>
194-
{Object.keys(OrgRole).map(role => {
195-
return (
196-
<option key={role} label={role} value={role}>
197-
{role}
198-
</option>
199-
);
200-
})}
201-
</select>
202-
</span>
203-
</div>
204-
<div className="gf-form">
205-
<button className="btn gf-form-btn btn-success">Add</button>
206-
</div>
207-
</div>
208-
</form>
209-
</div>
210-
</SlideDown>
211-
{hasFetched ? this.renderTable() : <PageLoader pageName="Api keys" />}
202+
<div className="page-action-bar__spacer" />
203+
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
204+
<i className="fa fa-plus" /> Add API Key
205+
</button>
212206
</div>
207+
208+
{this.renderAddApiKeyForm()}
209+
210+
<h3 className="page-heading">Existing Keys</h3>
211+
<table className="filter-table">
212+
<thead>
213+
<tr>
214+
<th>Name</th>
215+
<th>Role</th>
216+
<th style={{ width: '34px' }} />
217+
</tr>
218+
</thead>
219+
{apiKeys.length > 0 ? (
220+
<tbody>
221+
{apiKeys.map(key => {
222+
return (
223+
<tr key={key.id}>
224+
<td>{key.name}</td>
225+
<td>{key.role}</td>
226+
<td>
227+
<DeleteButton onConfirmDelete={() => this.onDeleteApiKey(key)} />
228+
</td>
229+
</tr>
230+
);
231+
})}
232+
</tbody>
233+
) : null}
234+
</table>
235+
</div>
236+
);
237+
}
238+
239+
render() {
240+
const { hasFetched, navModel, apiKeysCount } = this.props;
241+
242+
return (
243+
<div>
244+
<PageHeader model={navModel} />
245+
{hasFetched ?
246+
(apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList())
247+
: <PageLoader pageName="Api keys" />}
213248
</div>
214249
);
215250
}
@@ -220,7 +255,8 @@ function mapStateToProps(state) {
220255
navModel: getNavModel(state.navIndex, 'apikeys'),
221256
apiKeys: getApiKeys(state.apiKeys),
222257
searchQuery: state.apiKeys.searchQuery,
223-
hasFetched: state.apiKeys.hasFetched,
258+
apiKeysCount: getApiKeysCount(state.apiKeys),
259+
hasFetched: state.apiKeys.hasFetched
224260
};
225261
}
226262

0 commit comments

Comments
 (0)