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

Merge branch 'master' into folder-to-redux

Torkel Ödegaard 7 лет назад
Родитель
Сommit
89ea47e7fb
38 измененных файлов с 896 добавлено и 802 удалено
  1. 1 0
      CHANGELOG.md
  2. 0 3
      Makefile
  3. 30 0
      docs/sources/auth/generic-oauth.md
  4. 1 6
      jest.config.js
  5. 6 5
      package.json
  6. 2 1
      pkg/services/sqlstore/dashboard.go
  7. 3 0
      pkg/tsdb/cloudwatch/metric_find_query.go
  8. 0 4
      public/app/core/actions/navModel.ts
  9. 0 8
      public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap
  10. 0 8
      public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap
  11. 2 2
      public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap
  12. 0 1
      public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
  13. 4 4
      public/app/features/alerting/state/actions.ts
  14. 2 2
      public/app/features/alerting/state/reducers.test.ts
  15. 2 2
      public/app/features/alerting/state/reducers.ts
  16. 13 1
      public/app/features/manage-dashboards/state/actions.ts
  17. 2 0
      public/app/features/manage-dashboards/state/reducers.ts
  18. 63 0
      public/app/features/teams/TeamGroupSync.test.tsx
  19. 7 11
      public/app/features/teams/TeamGroupSync.tsx
  20. 2 0
      public/app/features/teams/TeamList.test.tsx
  21. 6 5
      public/app/features/teams/TeamList.tsx
  22. 14 1
      public/app/features/teams/__mocks__/teamMocks.ts
  23. 281 0
      public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap
  24. 14 64
      public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
  25. 1 11
      public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
  26. 5 6
      public/app/features/teams/state/actions.ts
  27. 1 1
      public/app/features/teams/state/reducers.ts
  28. 11 6
      public/app/features/teams/state/selectors.ts
  29. 1 1
      public/app/routes/routes.ts
  30. 0 40
      public/app/stores/NavStore/NavStore.ts
  31. 0 4
      public/app/stores/RootStore/RootStore.ts
  32. 0 156
      public/app/stores/TeamsStore/TeamsStore.ts
  33. 35 0
      public/app/types/alerting.ts
  34. 23 130
      public/app/types/index.ts
  35. 15 0
      public/app/types/location.ts
  36. 22 0
      public/app/types/navModel.ts
  37. 32 0
      public/app/types/teams.ts
  38. 295 319
      yarn.lock

+ 1 - 0
CHANGELOG.md

@@ -3,6 +3,7 @@
 ### Minor
 
 * **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon)
+* **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc)
 
 # 5.3.0 (unreleased)
 

+ 0 - 3
Makefile

@@ -43,6 +43,3 @@ test: test-go test-js
 
 run:
 	./bin/grafana-server
-
-protoc:
-	protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/.

+ 30 - 0
docs/sources/auth/generic-oauth.md

@@ -174,6 +174,36 @@ allowed_organizations =
     allowed_organizations =
     ```
 
+## Set up OAuth2 with Centrify
+
+1.  Create a new Custom OpenID Connect application configuration in the Centrify dashboard.
+
+2.  Create a memorable unique Application ID, e.g. "grafana", "grafana_aws", etc.
+
+3.  Put in other basic configuration (name, description, logo, category)
+
+4.  On the Trust tab, generate a long password and put it into the OpenID Connect Client Secret field.
+
+5.  Put the URL to the front page of your Grafana instance into the "Resource Application URL" field.
+
+6.  Add an authorized Redirect URI like https://your-grafana-server/login/generic_oauth
+
+7.  Set up permissions, policies, etc. just like any other Centrify app
+
+8.  Configure Grafana as follows:
+
+    ```bash
+    [auth.generic_oauth]
+    name = Centrify
+    enabled = true
+    allow_sign_up = true
+    client_id = <OpenID Connect Client ID from Centrify>
+    client_secret = <your generated OpenID Connect Client Sercret"
+    scopes = openid email name
+    auth_url = https://<your domain>.my.centrify.com/OAuth2/Authorize/<Application ID>
+    token_url = https://<your domain>.my.centrify.com/OAuth2/Token/<Application ID>
+    ```
+
 <hr>
 
 

+ 1 - 6
jest.config.js

@@ -1,13 +1,8 @@
 
 module.exports = {
   verbose: false,
-  "globals": {
-    "ts-jest": {
-      "tsConfigFile": "tsconfig.json"
-    }
-  },
   "transform": {
-    "^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
+    "^.+\\.(ts|tsx)$": "ts-jest"
   },
   "moduleDirectories": ["node_modules", "public"],
   "roots": [

+ 6 - 5
package.json

@@ -34,7 +34,7 @@
     "expect.js": "~0.2.0",
     "expose-loader": "^0.7.3",
     "file-loader": "^1.1.11",
-    "fork-ts-checker-webpack-plugin": "^0.4.2",
+    "fork-ts-checker-webpack-plugin": "^0.4.9",
     "gaze": "^1.1.2",
     "glob": "~7.0.0",
     "grunt": "1.0.1",
@@ -56,7 +56,7 @@
     "html-webpack-harddisk-plugin": "^0.2.0",
     "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
-    "jest": "^22.0.4",
+    "jest": "^23.6.0",
     "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
     "mini-css-extract-plugin": "^0.4.0",
@@ -80,12 +80,12 @@
     "style-loader": "^0.21.0",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
-    "ts-jest": "^22.4.6",
-    "ts-loader": "^4.3.0",
+    "ts-jest": "^23.1.4",
+    "ts-loader": "^5.1.0",
     "tslib": "^1.9.3",
     "tslint": "^5.8.0",
     "tslint-loader": "^3.5.3",
-    "typescript": "^2.6.2",
+    "typescript": "^3.0.3",
     "uglifyjs-webpack-plugin": "^1.2.7",
     "webpack": "^4.8.0",
     "webpack-bundle-analyzer": "^2.9.0",
@@ -133,6 +133,7 @@
     "angular-native-dragdrop": "1.2.2",
     "angular-route": "1.6.6",
     "angular-sanitize": "1.6.6",
+    "babel-jest": "^23.6.0",
     "babel-polyfill": "^6.26.0",
     "baron": "^3.0.3",
     "brace": "^0.10.0",

+ 2 - 1
pkg/services/sqlstore/dashboard.go

@@ -295,7 +295,8 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
 					FROM dashboard
 					INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id
 					WHERE dashboard.org_id=?
-					GROUP BY term`
+					GROUP BY term
+					ORDER BY term`
 
 	query.Result = make([]*m.DashboardTagCloudItem, 0)
 	sess := x.Sql(sql, query.OrgId)

+ 3 - 0
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -466,6 +466,9 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
 						return nil, errors.New("invalid attribute path")
 					}
 					v = v.FieldByName(key)
+					if !v.IsValid() {
+						return nil, errors.New("invalid attribute path")
+					}
 				}
 				if attr, ok := v.Interface().(*string); ok {
 					data = *attr

+ 0 - 4
public/app/core/actions/navModel.ts

@@ -6,10 +6,6 @@ export enum ActionTypes {
 
 export type Action = UpdateNavIndexAction;
 
-// this action is not used yet
-// kind of just a placeholder, will be need for dynamic pages
-// like datasource edit, teams edit page
-
 export interface UpdateNavIndexAction {
   type: ActionTypes.UpdateNavIndex;
   payload: NavModelItem;

+ 0 - 8
public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap

@@ -6,7 +6,6 @@ exports[`TeamPicker renders correctly 1`] = `
 >
   <div
     className="Select gf-form-input gf-form-input--form-dropdown  is-clearable is-loading is-searchable Select--single"
-    style={undefined}
   >
     <div
       className="Select-control"
@@ -15,7 +14,6 @@ exports[`TeamPicker renders correctly 1`] = `
       onTouchEnd={[Function]}
       onTouchMove={[Function]}
       onTouchStart={[Function]}
-      style={undefined}
     >
       <span
         className="Select-multi-value-wrapper"
@@ -36,14 +34,9 @@ exports[`TeamPicker renders correctly 1`] = `
         >
           <input
             aria-activedescendant="react-select-2--value"
-            aria-describedby={undefined}
             aria-expanded="false"
             aria-haspopup="false"
-            aria-label={undefined}
-            aria-labelledby={undefined}
             aria-owns=""
-            className={undefined}
-            id={undefined}
             onBlur={[Function]}
             onChange={[Function]}
             onFocus={[Function]}
@@ -55,7 +48,6 @@ exports[`TeamPicker renders correctly 1`] = `
                 "width": "5px",
               }
             }
-            tabIndex={undefined}
             value=""
           />
           <div

+ 0 - 8
public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap

@@ -6,7 +6,6 @@ exports[`UserPicker renders correctly 1`] = `
 >
   <div
     className="Select gf-form-input gf-form-input--form-dropdown  is-clearable is-loading is-searchable Select--single"
-    style={undefined}
   >
     <div
       className="Select-control"
@@ -15,7 +14,6 @@ exports[`UserPicker renders correctly 1`] = `
       onTouchEnd={[Function]}
       onTouchMove={[Function]}
       onTouchStart={[Function]}
-      style={undefined}
     >
       <span
         className="Select-multi-value-wrapper"
@@ -36,14 +34,9 @@ exports[`UserPicker renders correctly 1`] = `
         >
           <input
             aria-activedescendant="react-select-2--value"
-            aria-describedby={undefined}
             aria-expanded="false"
             aria-haspopup="false"
-            aria-label={undefined}
-            aria-labelledby={undefined}
             aria-owns=""
-            className={undefined}
-            id={undefined}
             onBlur={[Function]}
             onChange={[Function]}
             onFocus={[Function]}
@@ -55,7 +48,6 @@ exports[`UserPicker renders correctly 1`] = `
                 "width": "5px",
               }
             }
-            tabIndex={undefined}
             value=""
           />
           <div

+ 2 - 2
public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap

@@ -6,7 +6,7 @@ exports[`Render should render component 1`] = `
 >
   <a
     className="sidemenu-link"
-    href="login?redirect=blank"
+    href="login?redirect=%2F"
     target="_self"
   >
     <span
@@ -18,7 +18,7 @@ exports[`Render should render component 1`] = `
     </span>
   </a>
   <a
-    href="login?redirect=blank"
+    href="login?redirect=%2F"
     target="_self"
   >
     <ul

+ 0 - 1
public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap

@@ -66,7 +66,6 @@ exports[`ServerStats Should render table with stats 1`] = `
               <a
                 className="gf-tabs-link active"
                 href="Admin"
-                target={undefined}
               >
                 <i
                   className="icon"

+ 4 - 4
public/app/features/alerting/state/actions.ts

@@ -1,15 +1,15 @@
 import { getBackendSrv } from 'app/core/services/backend_srv';
-import { AlertRuleApi, StoreState } from 'app/types';
+import { AlertRuleDTO, StoreState } from 'app/types';
 import { ThunkAction } from 'redux-thunk';
 
 export enum ActionTypes {
   LoadAlertRules = 'LOAD_ALERT_RULES',
-  SetSearchQuery = 'SET_SEARCH_QUERY',
+  SetSearchQuery = 'SET_ALERT_SEARCH_QUERY',
 }
 
 export interface LoadAlertRulesAction {
   type: ActionTypes.LoadAlertRules;
-  payload: AlertRuleApi[];
+  payload: AlertRuleDTO[];
 }
 
 export interface SetSearchQueryAction {
@@ -17,7 +17,7 @@ export interface SetSearchQueryAction {
   payload: string;
 }
 
-export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({
+export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({
   type: ActionTypes.LoadAlertRules,
   payload: rules,
 });

+ 2 - 2
public/app/features/alerting/state/reducers.test.ts

@@ -1,9 +1,9 @@
 import { ActionTypes, Action } from './actions';
 import { alertRulesReducer, initialState } from './reducers';
-import { AlertRuleApi } from '../../../types';
+import { AlertRuleDTO } from 'app/types';
 
 describe('Alert rules', () => {
-  const payload: AlertRuleApi[] = [
+  const payload: AlertRuleDTO[] = [
     {
       id: 2,
       dashboardId: 7,

+ 2 - 2
public/app/features/alerting/state/reducers.ts

@@ -1,5 +1,5 @@
 import moment from 'moment';
-import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types';
+import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types';
 import { Action, ActionTypes } from './actions';
 import alertDef from './alertDef';
 
@@ -29,7 +29,7 @@ function convertToAlertRule(rule, state): AlertRule {
 export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
   switch (action.type) {
     case ActionTypes.LoadAlertRules: {
-      const alertRules: AlertRuleApi[] = action.payload;
+      const alertRules: AlertRuleDTO[] = action.payload;
 
       const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
         return convertToAlertRule(rule, rule.state);

+ 13 - 1
public/app/features/manage-dashboards/state/actions.ts

@@ -6,6 +6,8 @@ import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
 
 export enum ActionTypes {
   LoadFolder = 'LOAD_FOLDER',
+  SetFolderTitle = 'SET_FOLDER_TITLE',
+  SaveFolder = 'SAVE_FOLDER',
 }
 
 export interface LoadFolderAction {
@@ -18,7 +20,17 @@ export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({
   payload: folder,
 });
 
-export type Action = LoadFolderAction;
+export interface SetFolderTitleAction {
+  type: ActionTypes.SetFolderTitle;
+  payload: string;
+}
+
+export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({
+  type: ActionTypes.SetFolderTitle,
+  payload: newTitle,
+});
+
+export type Action = LoadFolderAction | SetFolderTitleAction;
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
 

+ 2 - 0
public/app/features/manage-dashboards/state/reducers.ts

@@ -5,8 +5,10 @@ export const inititalState: FolderState = {
   uid: 'loading',
   id: -1,
   title: 'loading',
+  url: '',
   canSave: false,
   hasChanged: false,
+  version: 0,
 };
 
 export const folderReducer = (state = inititalState, action: Action): FolderState => {

+ 63 - 0
public/app/features/teams/TeamGroupSync.test.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { Props, TeamGroupSync } from './TeamGroupSync';
+import { TeamGroup } from '../../types';
+import { getMockTeamGroups } from './__mocks__/teamMocks';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    groups: [] as TeamGroup[],
+    loadTeamGroups: jest.fn(),
+    addTeamGroup: jest.fn(),
+    removeTeamGroup: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<TeamGroupSync {...props} />);
+  const instance = wrapper.instance() as TeamGroupSync;
+
+  return {
+    wrapper,
+    instance,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render groups table', () => {
+    const { wrapper } = setup({
+      groups: getMockTeamGroups(3),
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('Functions', () => {
+  it('should call add group', () => {
+    const { instance } = setup();
+
+    instance.setState({ newGroupId: 'some/group' });
+    const mockEvent = { preventDefault: jest.fn() };
+
+    instance.onAddGroup(mockEvent);
+
+    expect(instance.props.addTeamGroup).toHaveBeenCalledWith('some/group');
+  });
+
+  it('should call remove group', () => {
+    const { instance } = setup();
+
+    const mockGroup: TeamGroup = { teamId: 1, groupId: 'some/group' };
+
+    instance.onRemoveGroup(mockGroup);
+
+    expect(instance.props.removeTeamGroup).toHaveBeenCalledWith('some/group');
+  });
+});

+ 7 - 11
public/app/features/teams/TeamGroupSync.tsx

@@ -38,11 +38,12 @@ export class TeamGroupSync extends PureComponent<Props, State> {
     this.setState({ isAdding: !this.state.isAdding });
   };
 
-  onNewGroupIdChanged = evt => {
-    this.setState({ newGroupId: evt.target.value });
+  onNewGroupIdChanged = event => {
+    this.setState({ newGroupId: event.target.value });
   };
 
-  onAddGroup = () => {
+  onAddGroup = event => {
+    event.preventDefault();
     this.props.addTeamGroup(this.state.newGroupId);
     this.setState({ isAdding: false, newGroupId: '' });
   };
@@ -93,7 +94,7 @@ export class TeamGroupSync extends PureComponent<Props, State> {
               <i className="fa fa-close" />
             </button>
             <h5>Add External Group</h5>
-            <div className="gf-form-inline">
+            <form className="gf-form-inline" onSubmit={this.onAddGroup}>
               <div className="gf-form">
                 <input
                   type="text"
@@ -105,16 +106,11 @@ export class TeamGroupSync extends PureComponent<Props, State> {
               </div>
 
               <div className="gf-form">
-                <button
-                  className="btn btn-success gf-form-btn"
-                  onClick={this.onAddGroup}
-                  type="submit"
-                  disabled={!this.isNewGroupValid()}
-                >
+                <button className="btn btn-success gf-form-btn" type="submit" disabled={!this.isNewGroupValid()}>
                   Add group
                 </button>
               </div>
-            </div>
+            </form>
           </div>
         </SlideDown>
 

+ 2 - 0
public/app/features/teams/TeamList.test.tsx

@@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => {
     deleteTeam: jest.fn(),
     setSearchQuery: jest.fn(),
     searchQuery: '',
+    teamsCount: 0,
   };
 
   Object.assign(props, propOverrides);
@@ -34,6 +35,7 @@ describe('Render', () => {
   it('should render teams table', () => {
     const { wrapper } = setup({
       teams: getMultipleMockTeams(5),
+      teamsCount: 5,
     });
 
     expect(wrapper).toMatchSnapshot();

+ 6 - 5
public/app/features/teams/TeamList.tsx

@@ -6,16 +6,17 @@ import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import { NavModel, Team } from '../../types';
 import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
-import { getSearchQuery, getTeams } from './state/selectors';
+import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
 import { getNavModel } from 'app/core/selectors/navModel';
 
 export interface Props {
   navModel: NavModel;
   teams: Team[];
+  searchQuery: string;
+  teamsCount: number;
   loadTeams: typeof loadTeams;
   deleteTeam: typeof deleteTeam;
   setSearchQuery: typeof setSearchQuery;
-  searchQuery: string;
 }
 
 export class TeamList extends PureComponent<Props, any> {
@@ -125,13 +126,12 @@ export class TeamList extends PureComponent<Props, any> {
   }
 
   render() {
-    const { navModel, teams } = this.props;
+    const { navModel, teamsCount } = this.props;
 
     return (
       <div>
         <PageHeader model={navModel} />
-        {teams.length > 0 && this.renderTeamList()}
-        {teams.length === 0 && this.renderEmptyList()}
+        {teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
       </div>
     );
   }
@@ -142,6 +142,7 @@ function mapStateToProps(state) {
     navModel: getNavModel(state.navIndex, 'teams'),
     teams: getTeams(state.teams),
     searchQuery: getSearchQuery(state.teams),
+    teamsCount: getTeamsCount(state.teams),
   };
 }
 

+ 14 - 1
public/app/features/teams/__mocks__/teamMocks.ts

@@ -1,4 +1,4 @@
-import { Team, TeamMember } from '../../../types';
+import { Team, TeamGroup, TeamMember } from '../../../types';
 
 export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
   const teams: Team[] = [];
@@ -50,3 +50,16 @@ export const getMockTeamMember = (): TeamMember => {
     login: 'testUser',
   };
 };
+
+export const getMockTeamGroups = (amount: number): TeamGroup[] => {
+  const groups: TeamGroup[] = [];
+
+  for (let i = 1; i <= amount; i++) {
+    groups.push({
+      groupId: `group-${i}`,
+      teamId: 1,
+    });
+  }
+
+  return groups;
+};

+ 281 - 0
public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap

@@ -0,0 +1,281 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <div
+    className="page-action-bar"
+  >
+    <h3
+      className="page-sub-heading"
+    >
+      External group sync
+    </h3>
+    <class_1
+      className="page-sub-heading-icon"
+      content="Sync LDAP or OAuth groups with your Grafana teams."
+      placement="auto"
+    >
+      <i
+        className="gicon gicon-question gicon--has-hover"
+      />
+    </class_1>
+    <div
+      className="page-action-bar__spacer"
+    />
+  </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 External Group
+      </h5>
+      <form
+        className="gf-form-inline"
+        onSubmit={[Function]}
+      >
+        <div
+          className="gf-form"
+        >
+          <input
+            className="gf-form-input width-30"
+            onChange={[Function]}
+            placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
+            type="text"
+            value=""
+          />
+        </div>
+        <div
+          className="gf-form"
+        >
+          <button
+            className="btn btn-success gf-form-btn"
+            disabled={true}
+            type="submit"
+          >
+            Add group
+          </button>
+        </div>
+      </form>
+    </div>
+  </Component>
+  <div
+    className="empty-list-cta"
+  >
+    <div
+      className="empty-list-cta__title"
+    >
+      There are no external groups to sync with
+    </div>
+    <button
+      className="empty-list-cta__button btn btn-xlarge btn-success"
+      onClick={[Function]}
+    >
+      <i
+        className="gicon gicon-add-team"
+      />
+      Add Group
+    </button>
+    <div
+      className="empty-list-cta__pro-tip"
+    >
+      <i
+        className="fa fa-rocket"
+      />
+       
+      Sync LDAP or OAuth groups with your Grafana teams.
+      <a
+        className="text-link empty-list-cta__pro-tip-link"
+        href="asd"
+        target="_blank"
+      >
+        Learn more
+      </a>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render groups table 1`] = `
+<div>
+  <div
+    className="page-action-bar"
+  >
+    <h3
+      className="page-sub-heading"
+    >
+      External group sync
+    </h3>
+    <class_1
+      className="page-sub-heading-icon"
+      content="Sync LDAP or OAuth groups with your Grafana teams."
+      placement="auto"
+    >
+      <i
+        className="gicon gicon-question gicon--has-hover"
+      />
+    </class_1>
+    <div
+      className="page-action-bar__spacer"
+    />
+    <button
+      className="btn btn-success pull-right"
+      onClick={[Function]}
+    >
+      <i
+        className="fa fa-plus"
+      />
+       Add group
+    </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 External Group
+      </h5>
+      <form
+        className="gf-form-inline"
+        onSubmit={[Function]}
+      >
+        <div
+          className="gf-form"
+        >
+          <input
+            className="gf-form-input width-30"
+            onChange={[Function]}
+            placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
+            type="text"
+            value=""
+          />
+        </div>
+        <div
+          className="gf-form"
+        >
+          <button
+            className="btn btn-success gf-form-btn"
+            disabled={true}
+            type="submit"
+          >
+            Add group
+          </button>
+        </div>
+      </form>
+    </div>
+  </Component>
+  <div
+    className="admin-list-table"
+  >
+    <table
+      className="filter-table filter-table--hover form-inline"
+    >
+      <thead>
+        <tr>
+          <th>
+            External Group ID
+          </th>
+          <th
+            style={
+              Object {
+                "width": "1%",
+              }
+            }
+          />
+        </tr>
+      </thead>
+      <tbody>
+        <tr
+          key="group-1"
+        >
+          <td>
+            group-1
+          </td>
+          <td
+            style={
+              Object {
+                "width": "1%",
+              }
+            }
+          >
+            <a
+              className="btn btn-danger btn-mini"
+              onClick={[Function]}
+            >
+              <i
+                className="fa fa-remove"
+              />
+            </a>
+          </td>
+        </tr>
+        <tr
+          key="group-2"
+        >
+          <td>
+            group-2
+          </td>
+          <td
+            style={
+              Object {
+                "width": "1%",
+              }
+            }
+          >
+            <a
+              className="btn btn-danger btn-mini"
+              onClick={[Function]}
+            >
+              <i
+                className="fa fa-remove"
+              />
+            </a>
+          </td>
+        </tr>
+        <tr
+          key="group-3"
+        >
+          <td>
+            group-3
+          </td>
+          <td
+            style={
+              Object {
+                "width": "1%",
+              }
+            }
+          >
+            <a
+              className="btn btn-danger btn-mini"
+              onClick={[Function]}
+            >
+              <i
+                className="fa fa-remove"
+              />
+            </a>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+`;

+ 14 - 64
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap

@@ -8,70 +8,20 @@ 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 teams"
-            type="text"
-            value=""
-          />
-          <i
-            className="gf-form-input-icon fa fa-search"
-          />
-        </label>
-      </div>
-      <div
-        className="page-action-bar__spacer"
-      />
-      <a
-        className="btn btn-success"
-        href="org/teams/new"
-      >
-        <i
-          className="fa fa-plus"
-        />
-         New team
-      </a>
-    </div>
-    <div
-      className="admin-list-table"
-    >
-      <table
-        className="filter-table filter-table--hover form-inline"
-      >
-        <thead>
-          <tr>
-            <th />
-            <th>
-              Name
-            </th>
-            <th>
-              Email
-            </th>
-            <th>
-              Members
-            </th>
-            <th
-              style={
-                Object {
-                  "width": "1%",
-                }
-              }
-            />
-          </tr>
-        </thead>
-        <tbody />
-      </table>
-    </div>
+    <EmptyListCTA
+      model={
+        Object {
+          "buttonIcon": "fa fa-plus",
+          "buttonLink": "org/teams/new",
+          "buttonTitle": " New team",
+          "proTip": "Assign folder and dashboard permissions to teams instead of users to ease administration.",
+          "proTipLink": "",
+          "proTipLinkTitle": "",
+          "proTipTarget": "_blank",
+          "title": "You haven't created any teams yet.",
+        }
+      }
+    />
   </div>
 </div>
 `;

+ 1 - 11
public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap

@@ -16,17 +16,7 @@ exports[`Render should render group sync page 1`] = `
   <div
     className="page-container page-body"
   >
-    <TeamGroupSync
-      team={
-        Object {
-          "avatarUrl": "some/url/",
-          "email": "test@test.com",
-          "id": 1,
-          "memberCount": 1,
-          "name": "test",
-        }
-      }
-    />
+    <Connect(TeamGroupSync) />
   </div>
 </div>
 `;

+ 5 - 6
public/app/features/teams/state/actions.ts

@@ -1,15 +1,14 @@
 import { ThunkAction } from 'redux-thunk';
 import { getBackendSrv } from 'app/core/services/backend_srv';
-import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from '../../../types';
-import { updateNavIndex } from '../../../core/actions';
-import { UpdateNavIndexAction } from '../../../core/actions/navModel';
+import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types';
+import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
 import config from 'app/core/config';
 
 export enum ActionTypes {
   LoadTeams = 'LOAD_TEAMS',
   LoadTeam = 'LOAD_TEAM',
-  SetSearchQuery = 'SET_SEARCH_QUERY',
-  SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY',
+  SetSearchQuery = 'SET_TEAM_SEARCH_QUERY',
+  SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY',
   LoadTeamMembers = 'TEAM_MEMBERS_LOADED',
   LoadTeamGroups = 'TEAM_GROUPS_LOADED',
 }
@@ -121,7 +120,7 @@ function buildNavModel(team: Team): NavModelItem {
     navModel.children.push({
       active: false,
       icon: 'fa fa-fw fa-refresh',
-      id: 'team-settings',
+      id: `team-groupsync-${team.id}`,
       text: 'External group sync',
       url: `org/teams/edit/${team.id}/groupsync`,
     });

+ 1 - 1
public/app/features/teams/state/reducers.ts

@@ -1,4 +1,4 @@
-import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types';
+import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
 import { Action, ActionTypes } from './actions';
 
 export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };

+ 11 - 6
public/app/features/teams/state/selectors.ts

@@ -1,14 +1,19 @@
-export const getSearchQuery = state => state.searchQuery;
-export const getSearchMemberQuery = state => state.searchMemberQuery;
-export const getTeamGroups = state => state.groups;
+import { Team, TeamsState, TeamState } from 'app/types';
 
-export const getTeam = (state, currentTeamId) => {
+export const getSearchQuery = (state: TeamsState) => state.searchQuery;
+export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
+export const getTeamGroups = (state: TeamState) => state.groups;
+export const getTeamsCount = (state: TeamsState) => state.teams.length;
+
+export const getTeam = (state: TeamState, currentTeamId): Team | null => {
   if (state.team.id === parseInt(currentTeamId, 10)) {
     return state.team;
   }
+
+  return null;
 };
 
-export const getTeams = state => {
+export const getTeams = (state: TeamsState) => {
   const regex = RegExp(state.searchQuery, 'i');
 
   return state.teams.filter(team => {
@@ -16,7 +21,7 @@ export const getTeams = state => {
   });
 };
 
-export const getTeamMembers = state => {
+export const getTeamMembers = (state: TeamState) => {
   const regex = RegExp(state.searchMemberQuery, 'i');
 
   return state.members.filter(member => {

+ 1 - 1
public/app/routes/routes.ts

@@ -4,9 +4,9 @@ import './ReactContainer';
 import ServerStats from 'app/features/admin/ServerStats';
 import AlertRuleList from 'app/features/alerting/AlertRuleList';
 import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
-import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage';
 import TeamPages from 'app/features/teams/TeamPages';
 import TeamList from 'app/features/teams/TeamList';
+import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage';
 
 /** @ngInject */
 export function setupAngularRoutes($routeProvider, $locationProvider) {

+ 0 - 40
public/app/stores/NavStore/NavStore.ts

@@ -1,7 +1,6 @@
 import _ from 'lodash';
 import { types, getEnv } from 'mobx-state-tree';
 import { NavItem } from './NavItem';
-import { Team } from '../TeamsStore/TeamsStore';
 
 export const NavStore = types
   .model('NavStore', {
@@ -116,43 +115,4 @@ export const NavStore = types
 
       self.main = NavItem.create(main);
     },
-
-    initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) {
-      const main = {
-        img: team.avatarUrl,
-        id: 'team-' + team.id,
-        subTitle: 'Manage members & settings',
-        url: '',
-        text: team.name,
-        breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
-        children: [
-          {
-            active: tab === 'members',
-            icon: 'gicon gicon-team',
-            id: 'team-members',
-            text: 'Members',
-            url: `org/teams/edit/${team.id}/members`,
-          },
-          {
-            active: tab === 'settings',
-            icon: 'fa fa-fw fa-sliders',
-            id: 'team-settings',
-            text: 'Settings',
-            url: `org/teams/edit/${team.id}/settings`,
-          },
-        ],
-      };
-
-      if (isSyncEnabled) {
-        main.children.splice(1, 0, {
-          active: tab === 'groupsync',
-          icon: 'fa fa-fw fa-refresh',
-          id: 'team-settings',
-          text: 'External group sync',
-          url: `org/teams/edit/${team.id}/groupsync`,
-        });
-      }
-
-      self.main = NavItem.create(main);
-    },
   }));

+ 0 - 4
public/app/stores/RootStore/RootStore.ts

@@ -3,7 +3,6 @@ import { NavStore } from './../NavStore/NavStore';
 import { ViewStore } from './../ViewStore/ViewStore';
 import { FolderStore } from './../FolderStore/FolderStore';
 import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
-import { TeamsStore } from './../TeamsStore/TeamsStore';
 
 export const RootStore = types.model({
   nav: types.optional(NavStore, {}),
@@ -17,9 +16,6 @@ export const RootStore = types.model({
     routeParams: {},
   }),
   folder: types.optional(FolderStore, {}),
-  teams: types.optional(TeamsStore, {
-    map: {},
-  }),
 });
 
 type RootStoreType = typeof RootStore.Type;

+ 0 - 156
public/app/stores/TeamsStore/TeamsStore.ts

@@ -1,156 +0,0 @@
-import { types, getEnv, flow } from 'mobx-state-tree';
-
-export const TeamMemberModel = types.model('TeamMember', {
-  userId: types.identifier(types.number),
-  teamId: types.number,
-  avatarUrl: types.string,
-  email: types.string,
-  login: types.string,
-});
-
-type TeamMemberType = typeof TeamMemberModel.Type;
-export interface TeamMember extends TeamMemberType {}
-
-export const TeamGroupModel = types.model('TeamGroup', {
-  groupId: types.identifier(types.string),
-  teamId: types.number,
-});
-
-type TeamGroupType = typeof TeamGroupModel.Type;
-export interface TeamGroup extends TeamGroupType {}
-
-export const TeamModel = types
-  .model('Team', {
-    id: types.identifier(types.number),
-    name: types.string,
-    avatarUrl: types.string,
-    email: types.string,
-    memberCount: types.number,
-    search: types.optional(types.string, ''),
-    members: types.optional(types.map(TeamMemberModel), {}),
-    groups: types.optional(types.map(TeamGroupModel), {}),
-  })
-  .views(self => ({
-    get filteredMembers(this: Team) {
-      const members = this.members.values();
-      const regex = new RegExp(self.search, 'i');
-      return members.filter(member => {
-        return regex.test(member.login) || regex.test(member.email);
-      });
-    },
-  }))
-  .actions(self => ({
-    setName(name: string) {
-      self.name = name;
-    },
-
-    setEmail(email: string) {
-      self.email = email;
-    },
-
-    setSearchQuery(query: string) {
-      self.search = query;
-    },
-
-    update: flow(function* load() {
-      const backendSrv = getEnv(self).backendSrv;
-
-      yield backendSrv.put(`/api/teams/${self.id}`, {
-        name: self.name,
-        email: self.email,
-      });
-    }),
-
-    loadMembers: flow(function* load() {
-      const backendSrv = getEnv(self).backendSrv;
-      const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`);
-      self.members.clear();
-
-      for (const member of rsp) {
-        self.members.set(member.userId.toString(), TeamMemberModel.create(member));
-      }
-    }),
-
-    removeMember: flow(function* load(member: TeamMember) {
-      const backendSrv = getEnv(self).backendSrv;
-      yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`);
-      // remove from store map
-      self.members.delete(member.userId.toString());
-    }),
-
-    addMember: flow(function* load(userId: number) {
-      const backendSrv = getEnv(self).backendSrv;
-      yield backendSrv.post(`/api/teams/${self.id}/members`, { userId: userId });
-    }),
-
-    loadGroups: flow(function* load() {
-      const backendSrv = getEnv(self).backendSrv;
-      const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`);
-      self.groups.clear();
-
-      for (const group of rsp) {
-        self.groups.set(group.groupId, TeamGroupModel.create(group));
-      }
-    }),
-
-    addGroup: flow(function* load(groupId: string) {
-      const backendSrv = getEnv(self).backendSrv;
-      yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId });
-      self.groups.set(
-        groupId,
-        TeamGroupModel.create({
-          teamId: self.id,
-          groupId: groupId,
-        })
-      );
-    }),
-
-    removeGroup: flow(function* load(groupId: string) {
-      const backendSrv = getEnv(self).backendSrv;
-      yield backendSrv.delete(`/api/teams/${self.id}/groups/${groupId}`);
-      self.groups.delete(groupId);
-    }),
-  }));
-
-type TeamType = typeof TeamModel.Type;
-export interface Team extends TeamType {}
-
-export const TeamsStore = types
-  .model('TeamsStore', {
-    map: types.map(TeamModel),
-    search: types.optional(types.string, ''),
-  })
-  .views(self => ({
-    get filteredTeams(this: any) {
-      const teams = this.map.values();
-      const regex = new RegExp(self.search, 'i');
-      return teams.filter(team => {
-        return regex.test(team.name);
-      });
-    },
-  }))
-  .actions(self => ({
-    loadTeams: flow(function* load() {
-      const backendSrv = getEnv(self).backendSrv;
-      const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 });
-      self.map.clear();
-
-      for (const team of rsp.teams) {
-        self.map.set(team.id.toString(), TeamModel.create(team));
-      }
-    }),
-
-    setSearchQuery(query: string) {
-      self.search = query;
-    },
-
-    loadById: flow(function* load(id: string) {
-      if (self.map.has(id)) {
-        return;
-      }
-
-      const backendSrv = getEnv(self).backendSrv;
-      const team = yield backendSrv.get(`/api/teams/${id}`);
-      self.map.set(id, TeamModel.create(team));
-    }),
-  }));

+ 35 - 0
public/app/types/alerting.ts

@@ -0,0 +1,35 @@
+export interface AlertRuleDTO {
+  id: number;
+  dashboardId: number;
+  dashboardUid: string;
+  dashboardSlug: string;
+  panelId: number;
+  name: string;
+  state: string;
+  newStateDate: string;
+  evalDate: string;
+  evalData?: object;
+  executionError: string;
+  url: string;
+}
+
+export interface AlertRule {
+  id: number;
+  dashboardId: number;
+  panelId: number;
+  name: string;
+  state: string;
+  stateText: string;
+  stateIcon: string;
+  stateClass: string;
+  stateAge: string;
+  url: string;
+  info?: string;
+  executionError?: string;
+  evalData?: { noData: boolean };
+}
+
+export interface AlertRulesState {
+  items: AlertRule[];
+  searchQuery: string;
+}

+ 23 - 130
public/app/types/index.ts

@@ -1,134 +1,28 @@
+import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams';
+import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
+import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
+import { NavModel, NavModelItem, NavIndex } from './navModel';
 import { FolderDTO, FolderState } from './dashboard';
 
-export { FolderDTO, FolderState };
-
-//
-// Location
-//
-
-export interface LocationUpdate {
-  path?: string;
-  query?: UrlQueryMap;
-  routeParams?: UrlQueryMap;
-}
-
-export interface LocationState {
-  url: string;
-  path: string;
-  query: UrlQueryMap;
-  routeParams: UrlQueryMap;
-}
-
-export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
-export type UrlQueryMap = { [s: string]: UrlQueryValue };
-
-//
-// Alerting
-//
-
-export interface AlertRuleApi {
-  id: number;
-  dashboardId: number;
-  dashboardUid: string;
-  dashboardSlug: string;
-  panelId: number;
-  name: string;
-  state: string;
-  newStateDate: string;
-  evalDate: string;
-  evalData?: object;
-  executionError: string;
-  url: string;
-}
-
-export interface AlertRule {
-  id: number;
-  dashboardId: number;
-  panelId: number;
-  name: string;
-  state: string;
-  stateText: string;
-  stateIcon: string;
-  stateClass: string;
-  stateAge: string;
-  url: string;
-  info?: string;
-  executionError?: string;
-  evalData?: { noData: boolean };
-}
-
-//
-// Teams
-//
-
-export interface Team {
-  id: number;
-  name: string;
-  avatarUrl: string;
-  email: string;
-  memberCount: number;
-}
-
-export interface TeamMember {
-  userId: number;
-  teamId: number;
-  avatarUrl: string;
-  email: string;
-  login: string;
-}
-
-export interface TeamGroup {
-  groupId: string;
-  teamId: number;
-}
-
-//
-// NavModel
-//
-
-export interface NavModelItem {
-  text: string;
-  url: string;
-  subTitle?: string;
-  icon?: string;
-  img?: string;
-  id: string;
-  active?: boolean;
-  hideFromTabs?: boolean;
-  divider?: boolean;
-  children?: NavModelItem[];
-  breadcrumbs?: Array<{ title: string; url: string }>;
-  target?: string;
-  parentItem?: NavModelItem;
-}
-
-export interface NavModel {
-  main: NavModelItem;
-  node: NavModelItem;
-}
-
-export type NavIndex = { [s: string]: NavModelItem };
-
-//
-// Store
-//
-
-export interface AlertRulesState {
-  items: AlertRule[];
-  searchQuery: string;
-}
-
-export interface TeamsState {
-  teams: Team[];
-  searchQuery: string;
-}
-
-export interface TeamState {
-  team: Team;
-  members: TeamMember[];
-  groups: TeamGroup[];
-  searchMemberQuery: string;
-}
+export {
+  Team,
+  TeamsState,
+  TeamState,
+  TeamGroup,
+  TeamMember,
+  AlertRuleDTO,
+  AlertRule,
+  AlertRulesState,
+  LocationState,
+  LocationUpdate,
+  NavModel,
+  NavModelItem,
+  NavIndex,
+  UrlQueryMap,
+  UrlQueryValue,
+  FolderDTO,
+  FolderState,
+};
 
 export interface StoreState {
   navIndex: NavIndex;
@@ -136,5 +30,4 @@ export interface StoreState {
   alertRules: AlertRulesState;
   teams: TeamsState;
   team: TeamState;
-  folder: FolderState;
 }

+ 15 - 0
public/app/types/location.ts

@@ -0,0 +1,15 @@
+export interface LocationUpdate {
+  path?: string;
+  query?: UrlQueryMap;
+  routeParams?: UrlQueryMap;
+}
+
+export interface LocationState {
+  url: string;
+  path: string;
+  query: UrlQueryMap;
+  routeParams: UrlQueryMap;
+}
+
+export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
+export type UrlQueryMap = { [s: string]: UrlQueryValue };

+ 22 - 0
public/app/types/navModel.ts

@@ -0,0 +1,22 @@
+export interface NavModelItem {
+  text: string;
+  url: string;
+  subTitle?: string;
+  icon?: string;
+  img?: string;
+  id: string;
+  active?: boolean;
+  hideFromTabs?: boolean;
+  divider?: boolean;
+  children?: NavModelItem[];
+  breadcrumbs?: Array<{ title: string; url: string }>;
+  target?: string;
+  parentItem?: NavModelItem;
+}
+
+export interface NavModel {
+  main: NavModelItem;
+  node: NavModelItem;
+}
+
+export type NavIndex = { [s: string]: NavModelItem };

+ 32 - 0
public/app/types/teams.ts

@@ -0,0 +1,32 @@
+export interface Team {
+  id: number;
+  name: string;
+  avatarUrl: string;
+  email: string;
+  memberCount: number;
+}
+
+export interface TeamMember {
+  userId: number;
+  teamId: number;
+  avatarUrl: string;
+  email: string;
+  login: string;
+}
+
+export interface TeamGroup {
+  groupId: string;
+  teamId: number;
+}
+
+export interface TeamsState {
+  teams: Team[];
+  searchQuery: string;
+}
+
+export interface TeamState {
+  team: Team;
+  members: TeamMember[];
+  groups: TeamGroup[];
+  searchMemberQuery: string;
+}

Разница между файлами не показана из-за своего большого размера
+ 295 - 319
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов