Просмотр исходного кода

Merge branch 'mondras-empty-cta-apikeys'

Torkel Ödegaard 7 лет назад
Родитель
Сommit
05dee9e340

+ 12 - 5
public/app/core/components/Animations/SlideDown.tsx

@@ -1,15 +1,22 @@
-import React from 'react';
+import React from 'react';
 import Transition from 'react-transition-group/Transition';
 
-const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value.
+interface Style {
+  transition?: string;
+  overflow?: string;
+}
+
+// When animating using max-height we need to use a static value.
 // If this is not enough, pass in <SlideDown maxHeight="....
+const defaultMaxHeight = '200px';
 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/
@@ -25,7 +32,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
       {state => (
         <div
           style={{
-            ...defaultStyle,
+            ...style,
             ...transitionStyles[state],
           }}
         >

+ 1 - 0
public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx

@@ -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',

+ 2 - 1
public/app/core/components/EmptyListCTA/EmptyListCTA.tsx

@@ -11,6 +11,7 @@ class EmptyListCTA extends Component<Props, any> {
       buttonIcon,
       buttonLink,
       buttonTitle,
+      onClick,
       proTip,
       proTipLink,
       proTipLinkTitle,
@@ -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>

+ 1 - 0
public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap

@@ -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"

+ 11 - 5
public/app/features/api-keys/ApiKeysPage.test.tsx

@@ -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';
@@ -14,6 +14,7 @@ const setup = (propOverrides?: object) => {
     deleteApiKey: jest.fn(),
     setSearchQuery: jest.fn(),
     addApiKey: jest.fn(),
+    apiKeysCount: 0,
   };
 
   Object.assign(props, propOverrides);
@@ -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,
     });
 

+ 139 - 97
public/app/features/api-keys/ApiKeysPage.tsx

@@ -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 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 {
@@ -82,6 +85,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
       return {
         ...prevState,
         newApiKey: initialApiKeyState,
+        isAdding: false,
       };
     });
   };
@@ -101,115 +105,152 @@ 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;
 
     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}>
+        <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>
+    );
+  }
+
+  renderApiKeyList() {
+    const { isAdding } = this.state;
+    const { apiKeys, searchQuery } = this.props;
 
-            <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>
+    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,6 +261,7 @@ function mapStateToProps(state) {
     navModel: getNavModel(state.navIndex, 'apikeys'),
     apiKeys: getApiKeys(state.apiKeys),
     searchQuery: state.apiKeys.searchQuery,
+    apiKeysCount: getApiKeysCount(state.apiKeys),
     hasFetched: state.apiKeys.hasFetched,
   };
 }

+ 26 - 302
public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap

@@ -1,276 +1,17 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Render should render API keys table 1`] = `
+exports[`Render should render API keys table if there are any keys 1`] = `
 <div>
   <PageHeader
     model={Object {}}
   />
-  <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
-            className="gf-form-input"
-            onChange={[Function]}
-            placeholder="Search keys"
-            type="text"
-            value=""
-          />
-          <i
-            className="gf-form-input-icon fa fa-search"
-          />
-        </label>
-      </div>
-      <div
-        className="page-action-bar__spacer"
-      />
-      <button
-        className="btn btn-success pull-right"
-        disabled={false}
-        onClick={[Function]}
-      >
-        <i
-          className="fa fa-plus"
-        />
-         Add API Key
-      </button>
-    </div>
-    <Component
-      in={false}
-    >
-      <div
-        className="cta-form"
-      >
-        <button
-          className="cta-form__close btn btn-transparent"
-          onClick={[Function]}
-        >
-          <i
-            className="fa fa-close"
-          />
-        </button>
-        <h5>
-          Add API Key
-        </h5>
-        <form
-          className="gf-form-group"
-          onSubmit={[Function]}
-        >
-          <div
-            className="gf-form-inline"
-          >
-            <div
-              className="gf-form max-width-21"
-            >
-              <span
-                className="gf-form-label"
-              >
-                Key name
-              </span>
-              <input
-                className="gf-form-input"
-                onChange={[Function]}
-                placeholder="Name"
-                type="text"
-                value=""
-              />
-            </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"
-                  onChange={[Function]}
-                  value="Viewer"
-                >
-                  <option
-                    key="Viewer"
-                    label="Viewer"
-                    value="Viewer"
-                  >
-                    Viewer
-                  </option>
-                  <option
-                    key="Editor"
-                    label="Editor"
-                    value="Editor"
-                  >
-                    Editor
-                  </option>
-                  <option
-                    key="Admin"
-                    label="Admin"
-                    value="Admin"
-                  >
-                    Admin
-                  </option>
-                </select>
-              </span>
-            </div>
-            <div
-              className="gf-form"
-            >
-              <button
-                className="btn gf-form-btn btn-success"
-              >
-                Add
-              </button>
-            </div>
-          </div>
-        </form>
-      </div>
-    </Component>
-    <h3
-      className="page-heading"
-      key="header"
-    >
-      Existing Keys
-    </h3>
-    <table
-      className="filter-table"
-      key="table"
-    >
-      <thead>
-        <tr>
-          <th>
-            Name
-          </th>
-          <th>
-            Role
-          </th>
-          <th
-            style={
-              Object {
-                "width": "34px",
-              }
-            }
-          />
-        </tr>
-      </thead>
-      <tbody>
-        <tr
-          key="1"
-        >
-          <td>
-            test-1
-          </td>
-          <td>
-            Viewer
-          </td>
-          <td>
-            <a
-              className="btn btn-danger btn-mini"
-              onClick={[Function]}
-            >
-              <i
-                className="fa fa-remove"
-              />
-            </a>
-          </td>
-        </tr>
-        <tr
-          key="2"
-        >
-          <td>
-            test-2
-          </td>
-          <td>
-            Viewer
-          </td>
-          <td>
-            <a
-              className="btn btn-danger btn-mini"
-              onClick={[Function]}
-            >
-              <i
-                className="fa fa-remove"
-              />
-            </a>
-          </td>
-        </tr>
-        <tr
-          key="3"
-        >
-          <td>
-            test-3
-          </td>
-          <td>
-            Viewer
-          </td>
-          <td>
-            <a
-              className="btn btn-danger btn-mini"
-              onClick={[Function]}
-            >
-              <i
-                className="fa fa-remove"
-              />
-            </a>
-          </td>
-        </tr>
-        <tr
-          key="4"
-        >
-          <td>
-            test-4
-          </td>
-          <td>
-            Viewer
-          </td>
-          <td>
-            <a
-              className="btn btn-danger btn-mini"
-              onClick={[Function]}
-            >
-              <i
-                className="fa fa-remove"
-              />
-            </a>
-          </td>
-        </tr>
-        <tr
-          key="5"
-        >
-          <td>
-            test-5
-          </td>
-          <td>
-            Viewer
-          </td>
-          <td>
-            <a
-              className="btn btn-danger btn-mini"
-              onClick={[Function]}
-            >
-              <i
-                className="fa fa-remove"
-              />
-            </a>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
+  <PageLoader
+    pageName="Api keys"
+  />
 </div>
 `;
 
-exports[`Render should render component 1`] = `
+exports[`Render should render CTA if there are no API keys 1`] = `
 <div>
   <PageHeader
     model={Object {}}
@@ -278,43 +19,29 @@ exports[`Render should render component 1`] = `
   <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
-            className="gf-form-input"
-            onChange={[Function]}
-            placeholder="Search keys"
-            type="text"
-            value=""
-          />
-          <i
-            className="gf-form-input-icon fa fa-search"
-          />
-        </label>
-      </div>
-      <div
-        className="page-action-bar__spacer"
-      />
-      <button
-        className="btn btn-success pull-right"
-        disabled={false}
-        onClick={[Function]}
-      >
-        <i
-          className="fa fa-plus"
-        />
-         Add API Key
-      </button>
-    </div>
+    <EmptyListCTA
+      model={
+        Object {
+          "buttonIcon": "fa fa-plus",
+          "buttonLink": "#",
+          "buttonTitle": " New API Key",
+          "onClick": [Function],
+          "proTip": "Remember you can provide view-only API access to other applications.",
+          "proTipLink": "",
+          "proTipLinkTitle": "",
+          "proTipTarget": "_blank",
+          "title": "You haven't added any API Keys yet.",
+        }
+      }
+    />
     <Component
       in={false}
+      style={
+        Object {
+          "overflow": "hidden",
+          "transition": "unset",
+        }
+      }
     >
       <div
         className="cta-form"
@@ -406,9 +133,6 @@ exports[`Render should render component 1`] = `
         </form>
       </div>
     </Component>
-    <PageLoader
-      pageName="Api keys"
-    />
   </div>
 </div>
 `;

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

@@ -1,5 +1,7 @@
 import { ApiKeysState } from 'app/types';
 
+export const getApiKeysCount = (state: ApiKeysState) => state.keys.length;
+
 export const getApiKeys = (state: ApiKeysState) => {
   const regex = RegExp(state.searchQuery, 'i');