Add "search box" and a "add new" box to the new API Keys page #13411

This commit is contained in:
Johannes Schill
2018-09-26 13:45:04 +02:00
parent cc0802cc39
commit e3d579e410
6 changed files with 177 additions and 18 deletions

View File

@@ -1,12 +1,13 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { NavModel, ApiKey } from '../../types';
import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
// import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import { loadApiKeys, deleteApiKey } from './state/actions';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import SlideDown from 'app/core/components/Animations/SlideDown';
export interface Props {
navModel: NavModel;
@@ -14,12 +15,31 @@ export interface Props {
searchQuery: string;
loadApiKeys: typeof loadApiKeys;
deleteApiKey: typeof deleteApiKey;
// loadTeams: typeof loadTeams;
// deleteTeam: typeof deleteTeam;
// setSearchQuery: typeof setSearchQuery;
setSearchQuery: typeof setSearchQuery;
addApiKey: typeof addApiKey;
}
export interface State {
isAdding: boolean;
newApiKey: NewApiKey;
}
enum ApiKeyStateProps {
Name = 'name',
Role = 'role',
}
const initialApiKeyState = {
name: '',
role: OrgRole.Viewer,
};
export class ApiKeysPage extends PureComponent<Props, any> {
constructor(props) {
super(props);
this.state = { isAdding: false, newApiKey: initialApiKeyState };
}
componentDidMount() {
this.fetchApiKeys();
}
@@ -28,19 +48,120 @@ export class ApiKeysPage extends PureComponent<Props, any> {
await this.props.loadApiKeys();
}
deleteApiKey(id: number) {
onDeleteApiKey(id: number) {
return () => {
this.props.deleteApiKey(id);
};
}
onSearchQueryChange = evt => {
this.props.setSearchQuery(evt.target.value);
};
onToggleAdding = () => {
this.setState({ isAdding: !this.state.isAdding });
};
onAddApiKey = async evt => {
evt.preventDefault();
this.props.addApiKey(this.state.newApiKey);
this.setState((prevState: State) => {
return {
...prevState,
newApiKey: initialApiKeyState,
};
});
};
onApiKeyStateUpdate = (evt, prop: string) => {
const value = evt.currentTarget.value;
this.setState((prevState: State) => {
const newApiKey = {
...prevState.newApiKey,
};
newApiKey[prop] = value;
return {
...prevState,
newApiKey: newApiKey,
};
});
};
render() {
const { navModel, apiKeys } = this.props;
const { newApiKey, isAdding } = this.state;
const { navModel, apiKeys, searchQuery } = this.props;
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">
<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>
<div className="page-action-bar__spacer" />
{/* <button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}> */}
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
<i className="fa fa-plus" /> Add API Key
</button>
</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>
<h3 className="page-heading">Existing Keys</h3>
<table className="filter-table">
<thead>
@@ -59,7 +180,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
<td>{key.name}</td>
<td>{key.role}</td>
<td>
<a onClick={this.deleteApiKey(key.id)} className="btn btn-danger btn-mini">
<a onClick={this.onDeleteApiKey(key.id)} className="btn btn-danger btn-mini">
<i className="fa fa-remove" />
</a>
</td>
@@ -78,7 +199,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
function mapStateToProps(state) {
return {
navModel: getNavModel(state.navIndex, 'apikeys'),
apiKeys: state.apiKeys.keys,
apiKeys: getApiKeys(state.apiKeys),
searchQuery: state.apiKeys.searchQuery,
// searchQuery: getSearchQuery(state.teams),
};
}
@@ -86,9 +208,8 @@ function mapStateToProps(state) {
const mapDispatchToProps = {
loadApiKeys,
deleteApiKey,
// loadTeams,
// deleteTeam,
// setSearchQuery,
setSearchQuery,
addApiKey,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage));

View File

@@ -1,10 +1,10 @@
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { StoreState, ApiKey } from 'app/types';
import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
export enum ActionTypes {
LoadApiKeys = 'LOAD_API_KEYS',
SetApiKeysSearchQuery = 'SET_API_KEYS_SEARCH_QUERY',
}
export interface LoadApiKeysAction {
@@ -12,15 +12,27 @@ export interface LoadApiKeysAction {
payload: ApiKey[];
}
export type Action = LoadApiKeysAction;
export interface SetSearchQueryAction {
type: ActionTypes.SetApiKeysSearchQuery;
payload: string;
}
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
export type Action = LoadApiKeysAction | SetSearchQueryAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({
type: ActionTypes.LoadApiKeys,
payload: apiKeys,
});
export function addApiKey(apiKey: ApiKey): ThunkResult<void> {
return async dispatch => {
await getBackendSrv().post('/api/auth/keys', apiKey);
dispatch(loadApiKeys());
};
}
export function loadApiKeys(): ThunkResult<void> {
return async dispatch => {
const response = await getBackendSrv().get('/api/auth/keys');
@@ -35,3 +47,8 @@ export function deleteApiKey(id: number): ThunkResult<void> {
.then(dispatch(loadApiKeys()));
};
}
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
type: ActionTypes.SetApiKeysSearchQuery,
payload: searchQuery,
});

View File

@@ -1,12 +1,17 @@
import { ApiKeysState } from 'app/types';
import { Action, ActionTypes } from './actions';
export const initialApiKeysState: ApiKeysState = { keys: [] };
export const initialApiKeysState: ApiKeysState = {
keys: [],
searchQuery: '',
};
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
switch (action.type) {
case ActionTypes.LoadApiKeys:
return { ...state, keys: action.payload };
case ActionTypes.SetApiKeysSearchQuery:
return { ...state, searchQuery: action.payload };
}
return state;
};

View File

@@ -0,0 +1,9 @@
import { ApiKeysState } from 'app/types';
export const getApiKeys = (state: ApiKeysState) => {
const regex = RegExp(state.searchQuery, 'i');
return state.keys.filter(key => {
return regex.test(key.name) || regex.test(key.role);
});
};

View File

@@ -6,6 +6,12 @@ export interface ApiKey {
role: OrgRole;
}
export interface NewApiKey {
name: string;
role: OrgRole;
}
export interface ApiKeysState {
keys: ApiKey[];
searchQuery: string;
}

View File

@@ -7,7 +7,7 @@ import { DashboardState } from './dashboard';
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
import { DataSource } from './datasources';
import { PluginMeta } from './plugins';
import { ApiKey, ApiKeysState } from './apiKeys';
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
import { User } from './user';
export {
@@ -37,6 +37,7 @@ export {
PluginMeta,
ApiKey,
ApiKeysState,
NewApiKey,
User,
};