Merge branch 'master' into folder-to-redux

This commit is contained in:
Torkel Ödegaard 2018-09-11 15:07:03 +02:00
commit 89ea47e7fb
38 changed files with 934 additions and 880 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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={
<EmptyListCTA
model={
Object {
"width": "1%",
"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.",
}
}
/>
</tr>
</thead>
<tbody />
</table>
</div>
</div>
</div>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
public/app/types/teams.ts Normal file
View File

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

730
yarn.lock

File diff suppressed because it is too large Load Diff