Browse Source

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

Johannes Schill 7 years ago
parent
commit
e3d579e410

+ 134 - 13
public/app/features/api-keys/ApiKeysPage.tsx

@@ -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));

+ 20 - 3
public/app/features/api-keys/state/actions.ts

@@ -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;
+}
+
+export type Action = LoadApiKeysAction | SetSearchQueryAction;
 
-type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
+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,
+});

+ 6 - 1
public/app/features/api-keys/state/reducers.ts

@@ -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;
 };

+ 9 - 0
public/app/features/api-keys/state/selectors.ts

@@ -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);
+  });
+};

+ 6 - 0
public/app/types/apiKeys.ts

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

+ 2 - 1
public/app/types/index.ts

@@ -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,
 };