Merge branch 'master' into folder-to-redux

pull/13235/head
Torkel Ödegaard 7 years ago
commit 89ea47e7fb
  1. 1
      CHANGELOG.md
  2. 3
      Makefile
  3. 30
      docs/sources/auth/generic-oauth.md
  4. 7
      jest.config.js
  5. 11
      package.json
  6. 3
      pkg/services/sqlstore/dashboard.go
  7. 3
      pkg/tsdb/cloudwatch/metric_find_query.go
  8. 4
      public/app/core/actions/navModel.ts
  9. 8
      public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap
  10. 8
      public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap
  11. 4
      public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap
  12. 1
      public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
  13. 8
      public/app/features/alerting/state/actions.ts
  14. 4
      public/app/features/alerting/state/reducers.test.ts
  15. 4
      public/app/features/alerting/state/reducers.ts
  16. 14
      public/app/features/manage-dashboards/state/actions.ts
  17. 2
      public/app/features/manage-dashboards/state/reducers.ts
  18. 63
      public/app/features/teams/TeamGroupSync.test.tsx
  19. 18
      public/app/features/teams/TeamGroupSync.tsx
  20. 2
      public/app/features/teams/TeamList.test.tsx
  21. 11
      public/app/features/teams/TeamList.tsx
  22. 15
      public/app/features/teams/__mocks__/teamMocks.ts
  23. 281
      public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap
  24. 78
      public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
  25. 12
      public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
  26. 11
      public/app/features/teams/state/actions.ts
  27. 2
      public/app/features/teams/state/reducers.ts
  28. 17
      public/app/features/teams/state/selectors.ts
  29. 2
      public/app/routes/routes.ts
  30. 40
      public/app/stores/NavStore/NavStore.ts
  31. 4
      public/app/stores/RootStore/RootStore.ts
  32. 156
      public/app/stores/TeamsStore/TeamsStore.ts
  33. 35
      public/app/types/alerting.ts
  34. 153
      public/app/types/index.ts
  35. 15
      public/app/types/location.ts
  36. 22
      public/app/types/navModel.ts
  37. 32
      public/app/types/teams.ts
  38. 722
      yarn.lock

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

@ -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/.

@ -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,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": [

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

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

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

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

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

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

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

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

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

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

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

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

@ -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 => {

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

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

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

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

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

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

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

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

@ -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 => {

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save