mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 9587_annotation_tags_wih_temp_var
* master: fix theme parameter not working problem while prefer theme set to light (#13232) fix: added type export to fix failing test fix: fixed typescript test error another circleci fix Another circleci fix changed gometalinter to use github master commented out metalinter as gopkg is having issues Fix prometheus label filtering for comparison queries (#13213) Upgrade react and enzyme (#13224) Teams page replace mobx (#13219) upgrade of typescript and tslint and jest (#13223) fix nil pointer dereference (#13221)
This commit is contained in:
@@ -81,7 +81,7 @@ jobs:
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
- run: 'go get -u gopkg.in/alecthomas/gometalinter.v2'
|
||||
- run: 'go get -u github.com/alecthomas/gometalinter'
|
||||
- run: 'go get -u github.com/tsenart/deadcode'
|
||||
- run: 'go get -u github.com/gordonklaus/ineffassign'
|
||||
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
|
||||
- run:
|
||||
name: run linters
|
||||
command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
||||
command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
||||
- run:
|
||||
name: run go vet
|
||||
command: 'go vet ./pkg/...'
|
||||
|
@@ -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": [
|
||||
|
33
package.json
33
package.json
@@ -11,12 +11,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^4.10.1",
|
||||
"@types/enzyme": "^2.8.9",
|
||||
"@types/enzyme": "^3.1.13",
|
||||
"@types/jest": "^21.1.4",
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.0.25",
|
||||
"@types/react": "^16.4.14",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-dom": "^16.0.3",
|
||||
"@types/react-dom": "^16.0.7",
|
||||
"angular-mocks": "1.6.6",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"axios": "^0.17.1",
|
||||
@@ -26,15 +26,15 @@
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"css-loader": "^0.28.7",
|
||||
"enzyme": "^3.1.0",
|
||||
"enzyme-adapter-react-16": "^1.0.1",
|
||||
"enzyme-to-json": "^3.3.0",
|
||||
"enzyme": "^3.6.0",
|
||||
"enzyme-adapter-react-16": "^1.5.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"es6-promise": "^3.0.2",
|
||||
"es6-shim": "^0.35.3",
|
||||
"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",
|
||||
@@ -72,20 +72,20 @@
|
||||
"postcss-loader": "^2.0.6",
|
||||
"postcss-reporter": "^5.0.0",
|
||||
"prettier": "1.9.2",
|
||||
"react-hot-loader": "^4.2.0",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"react-hot-loader": "^4.3.6",
|
||||
"react-test-renderer": "^16.5.0",
|
||||
"sass-lint": "^1.10.2",
|
||||
"sass-loader": "^7.0.1",
|
||||
"sinon": "1.17.6",
|
||||
"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",
|
||||
@@ -152,11 +153,11 @@
|
||||
"mousetrap": "^1.6.0",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
"prismjs": "^1.6.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"rc-cascader": "^0.14.0",
|
||||
"react": "^16.2.0",
|
||||
"react": "^16.5.0",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-dom": "^16.5.0",
|
||||
"react-grid-layout": "0.16.6",
|
||||
"react-highlight-words": "^0.10.0",
|
||||
"react-popper": "^0.7.5",
|
||||
|
@@ -91,6 +91,9 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
if themeURLParam == "light" {
|
||||
data.User.LightTheme = true
|
||||
data.Theme = "light"
|
||||
} else if themeURLParam == "dark" {
|
||||
data.User.LightTheme = false
|
||||
data.Theme = "dark"
|
||||
}
|
||||
|
||||
if hasEditPermissionInFoldersQuery.Result {
|
||||
|
@@ -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
|
||||
|
@@ -1,77 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import config from 'app/core/config';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import { NavStore } from 'app/stores/NavStore/NavStore';
|
||||
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import { ViewStore } from 'app/stores/ViewStore/ViewStore';
|
||||
import TeamMembers from './TeamMembers';
|
||||
import TeamSettings from './TeamSettings';
|
||||
import TeamGroupSync from './TeamGroupSync';
|
||||
|
||||
interface Props {
|
||||
nav: typeof NavStore.Type;
|
||||
teams: typeof TeamsStore.Type;
|
||||
view: typeof ViewStore.Type;
|
||||
}
|
||||
|
||||
@inject('nav', 'teams', 'view')
|
||||
@observer
|
||||
export class TeamPages extends React.Component<Props, any> {
|
||||
isSyncEnabled: boolean;
|
||||
currentPage: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.isSyncEnabled = config.buildInfo.isEnterprise;
|
||||
this.currentPage = this.getCurrentPage();
|
||||
|
||||
this.loadTeam();
|
||||
}
|
||||
|
||||
async loadTeam() {
|
||||
const { teams, nav, view } = this.props;
|
||||
|
||||
await teams.loadById(view.routeParams.get('id'));
|
||||
|
||||
nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
|
||||
}
|
||||
|
||||
getCurrentTeam(): Team {
|
||||
const { teams, view } = this.props;
|
||||
return teams.map.get(view.routeParams.get('id'));
|
||||
}
|
||||
|
||||
getCurrentPage() {
|
||||
const pages = ['members', 'settings', 'groupsync'];
|
||||
const currentPage = this.props.view.routeParams.get('page');
|
||||
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nav } = this.props;
|
||||
const currentTeam = this.getCurrentTeam();
|
||||
|
||||
if (!nav.main) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={nav as any} />
|
||||
{currentTeam && (
|
||||
<div className="page-container page-body">
|
||||
{this.currentPage === 'members' && <TeamMembers team={currentTeam} />}
|
||||
{this.currentPage === 'settings' && <TeamSettings team={currentTeam} />}
|
||||
{this.currentPage === 'groupsync' && this.isSyncEnabled && <TeamGroupSync team={currentTeam} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamPages);
|
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Team } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import { Label } from 'app/core/components/Forms/Forms';
|
||||
|
||||
interface Props {
|
||||
team: Team;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TeamSettings extends React.Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onChangeName = evt => {
|
||||
this.props.team.setName(evt.target.value);
|
||||
};
|
||||
|
||||
onChangeEmail = evt => {
|
||||
this.props.team.setEmail(evt.target.value);
|
||||
};
|
||||
|
||||
onUpdate = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.team.update();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-sub-heading">Team Settings</h3>
|
||||
<form name="teamDetailsForm" className="gf-form-group">
|
||||
<div className="gf-form max-width-30">
|
||||
<Label>Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={this.props.team.name}
|
||||
className="gf-form-input max-width-22"
|
||||
onChange={this.onChangeName}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form max-width-30">
|
||||
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
|
||||
Email
|
||||
</Label>
|
||||
<input
|
||||
type="email"
|
||||
className="gf-form-input max-width-22"
|
||||
value={this.props.team.email}
|
||||
placeholder="team@email.com"
|
||||
onChange={this.onChangeEmail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-button-row">
|
||||
<button type="submit" className="btn btn-success" onClick={this.onUpdate}>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamSettings);
|
@@ -1,3 +1,4 @@
|
||||
import { updateLocation } from './location';
|
||||
import { updateNavIndex, UpdateNavIndexAction } from './navModel';
|
||||
|
||||
export { updateLocation };
|
||||
export { updateLocation, updateNavIndex, UpdateNavIndexAction };
|
||||
|
@@ -1,13 +1,17 @@
|
||||
export type Action = UpdateNavIndexAction;
|
||||
import { NavModelItem } from '../../types';
|
||||
|
||||
// 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: 'UPDATE_NAV_INDEX';
|
||||
export enum ActionTypes {
|
||||
UpdateNavIndex = 'UPDATE_NAV_INDEX',
|
||||
}
|
||||
|
||||
export const updateNavIndex = (): UpdateNavIndexAction => ({
|
||||
type: 'UPDATE_NAV_INDEX',
|
||||
export type Action = UpdateNavIndexAction;
|
||||
|
||||
export interface UpdateNavIndexAction {
|
||||
type: ActionTypes.UpdateNavIndex;
|
||||
payload: NavModelItem;
|
||||
}
|
||||
|
||||
export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({
|
||||
type: ActionTypes.UpdateNavIndex,
|
||||
payload: item,
|
||||
});
|
||||
|
@@ -13,7 +13,6 @@ interface Props {
|
||||
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
||||
*/
|
||||
class CustomScrollbar extends PureComponent<Props> {
|
||||
|
||||
static defaultProps: Partial<Props> = {
|
||||
customClassName: 'custom-scrollbars',
|
||||
autoHide: true,
|
||||
|
@@ -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,9 +14,8 @@ exports[`TeamPicker renders correctly 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={undefined}
|
||||
>
|
||||
<span
|
||||
<div
|
||||
className="Select-multi-value-wrapper"
|
||||
id="react-select-2--value"
|
||||
>
|
||||
@@ -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
|
||||
@@ -74,7 +66,7 @@ exports[`TeamPicker renders correctly 1`] = `
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Select-loading-zone"
|
||||
|
@@ -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,9 +14,8 @@ exports[`UserPicker renders correctly 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={undefined}
|
||||
>
|
||||
<span
|
||||
<div
|
||||
className="Select-multi-value-wrapper"
|
||||
id="react-select-2--value"
|
||||
>
|
||||
@@ -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
|
||||
@@ -74,7 +66,7 @@ exports[`UserPicker renders correctly 1`] = `
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Select-loading-zone"
|
||||
|
@@ -6,12 +6,29 @@ Array [
|
||||
className="sidemenu__logo"
|
||||
key="logo"
|
||||
onClick={[Function]}
|
||||
/>,
|
||||
>
|
||||
<img
|
||||
alt="graphana_logo"
|
||||
src="public/img/grafana_icon.svg"
|
||||
/>
|
||||
</div>,
|
||||
<div
|
||||
className="sidemenu__logo_small_breakpoint"
|
||||
key="hamburger"
|
||||
onClick={[Function]}
|
||||
/>,
|
||||
>
|
||||
<i
|
||||
className="fa fa-bars"
|
||||
/>
|
||||
<span
|
||||
className="sidemenu__close"
|
||||
>
|
||||
<i
|
||||
className="fa fa-times"
|
||||
/>
|
||||
Close
|
||||
</span>
|
||||
</div>,
|
||||
<TopSection
|
||||
key="topsection"
|
||||
/>,
|
||||
|
@@ -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
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Action } from 'app/core/actions/navModel';
|
||||
import { NavModelItem, NavIndex } from 'app/types';
|
||||
import { Action, ActionTypes } from 'app/core/actions/navModel';
|
||||
import { NavIndex, NavModelItem } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export function buildInitialState(): NavIndex {
|
||||
@@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?
|
||||
export const initialState: NavIndex = buildInitialState();
|
||||
|
||||
export const navIndexReducer = (state = initialState, action: Action): NavIndex => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UpdateNavIndex:
|
||||
const newPages = {};
|
||||
const payload = action.payload;
|
||||
|
||||
for (const node of payload.children) {
|
||||
newPages[node.id] = {
|
||||
...node,
|
||||
parentItem: payload,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...state, ...newPages };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
3
public/app/core/selectors/location.ts
Normal file
3
public/app/core/selectors/location.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const getRouteParamsId = state => state.routeParams.id;
|
||||
|
||||
export const getRouteParamsPage = state => state.routeParams.page;
|
@@ -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);
|
||||
|
63
public/app/features/teams/TeamGroupSync.test.tsx
Normal file
63
public/app/features/teams/TeamGroupSync.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
@@ -1,12 +1,16 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||
import { TeamGroup } from '../../types';
|
||||
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
|
||||
import { getTeamGroups } from './state/selectors';
|
||||
|
||||
interface Props {
|
||||
team: Team;
|
||||
export interface Props {
|
||||
groups: TeamGroup[];
|
||||
loadTeamGroups: typeof loadTeamGroups;
|
||||
addTeamGroup: typeof addTeamGroup;
|
||||
removeTeamGroup: typeof removeTeamGroup;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -16,15 +20,40 @@ interface State {
|
||||
|
||||
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
||||
|
||||
@observer
|
||||
export class TeamGroupSync extends React.Component<Props, State> {
|
||||
export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newGroupId: '' };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.team.loadGroups();
|
||||
this.fetchTeamGroups();
|
||||
}
|
||||
|
||||
async fetchTeamGroups() {
|
||||
await this.props.loadTeamGroups();
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
this.setState({ isAdding: !this.state.isAdding });
|
||||
};
|
||||
|
||||
onNewGroupIdChanged = event => {
|
||||
this.setState({ newGroupId: event.target.value });
|
||||
};
|
||||
|
||||
onAddGroup = event => {
|
||||
event.preventDefault();
|
||||
this.props.addTeamGroup(this.state.newGroupId);
|
||||
this.setState({ isAdding: false, newGroupId: '' });
|
||||
};
|
||||
|
||||
onRemoveGroup = (group: TeamGroup) => {
|
||||
this.props.removeTeamGroup(group.groupId);
|
||||
};
|
||||
|
||||
isNewGroupValid() {
|
||||
return this.state.newGroupId.length > 1;
|
||||
}
|
||||
|
||||
renderGroup(group: TeamGroup) {
|
||||
@@ -40,30 +69,9 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
this.setState({ isAdding: !this.state.isAdding });
|
||||
};
|
||||
|
||||
onNewGroupIdChanged = evt => {
|
||||
this.setState({ newGroupId: evt.target.value });
|
||||
};
|
||||
|
||||
onAddGroup = () => {
|
||||
this.props.team.addGroup(this.state.newGroupId);
|
||||
this.setState({ isAdding: false, newGroupId: '' });
|
||||
};
|
||||
|
||||
onRemoveGroup = (group: TeamGroup) => {
|
||||
this.props.team.removeGroup(group.groupId);
|
||||
};
|
||||
|
||||
isNewGroupValid() {
|
||||
return this.state.newGroupId.length > 1;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isAdding, newGroupId } = this.state;
|
||||
const groups = this.props.team.groups.values();
|
||||
const groups = this.props.groups;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -86,7 +94,7 @@ export class TeamGroupSync extends React.Component<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"
|
||||
@@ -98,16 +106,11 @@ export class TeamGroupSync extends React.Component<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>
|
||||
|
||||
@@ -146,4 +149,16 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamGroupSync);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
groups: getTeamGroups(state.team),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTeamGroups,
|
||||
addTeamGroup,
|
||||
removeTeamGroup,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);
|
75
public/app/features/teams/TeamList.test.tsx
Normal file
75
public/app/features/teams/TeamList.test.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Props, TeamList } from './TeamList';
|
||||
import { NavModel, Team } from '../../types';
|
||||
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {} as NavModel,
|
||||
teams: [] as Team[],
|
||||
loadTeams: jest.fn(),
|
||||
deleteTeam: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
searchQuery: '',
|
||||
teamsCount: 0,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<TeamList {...props} />);
|
||||
const instance = wrapper.instance() as TeamList;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render teams table', () => {
|
||||
const { wrapper } = setup({
|
||||
teams: getMultipleMockTeams(5),
|
||||
teamsCount: 5,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Life cycle', () => {
|
||||
it('should call loadTeams', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.componentDidMount();
|
||||
|
||||
expect(instance.props.loadTeams).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
describe('Delete team', () => {
|
||||
it('should call delete team', () => {
|
||||
const { instance } = setup();
|
||||
instance.deleteTeam(getMockTeam());
|
||||
|
||||
expect(instance.props.deleteTeam).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on search query change', () => {
|
||||
it('should call setSearchQuery', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'test' } };
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,42 +1,42 @@
|
||||
import React from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import { NavStore } from 'app/stores/NavStore/NavStore';
|
||||
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
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, getTeamsCount } from './state/selectors';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
|
||||
interface Props {
|
||||
nav: typeof NavStore.Type;
|
||||
teams: typeof TeamsStore.Type;
|
||||
backendSrv: BackendSrv;
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
teams: Team[];
|
||||
searchQuery: string;
|
||||
teamsCount: number;
|
||||
loadTeams: typeof loadTeams;
|
||||
deleteTeam: typeof deleteTeam;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
}
|
||||
|
||||
@inject('nav', 'teams')
|
||||
@observer
|
||||
export class TeamList extends React.Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.props.nav.load('cfg', 'teams');
|
||||
export class TeamList extends PureComponent<Props, any> {
|
||||
componentDidMount() {
|
||||
this.fetchTeams();
|
||||
}
|
||||
|
||||
fetchTeams() {
|
||||
this.props.teams.loadTeams();
|
||||
async fetchTeams() {
|
||||
await this.props.loadTeams();
|
||||
}
|
||||
|
||||
deleteTeam(team: Team) {
|
||||
this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.teams.setSearchQuery(evt.target.value);
|
||||
deleteTeam = (team: Team) => {
|
||||
this.props.deleteTeam(team.id);
|
||||
};
|
||||
|
||||
renderTeamMember(team: Team): JSX.Element {
|
||||
onSearchQueryChange = event => {
|
||||
this.props.setSearchQuery(event.target.value);
|
||||
};
|
||||
|
||||
renderTeam(team: Team) {
|
||||
const teamUrl = `org/teams/edit/${team.id}`;
|
||||
|
||||
return (
|
||||
@@ -62,7 +62,28 @@ export class TeamList extends React.Component<Props, any> {
|
||||
);
|
||||
}
|
||||
|
||||
renderTeamList(teams) {
|
||||
renderEmptyList() {
|
||||
return (
|
||||
<div className="page-container page-body">
|
||||
<EmptyListCTA
|
||||
model={{
|
||||
title: "You haven't created any teams yet.",
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTeamList() {
|
||||
const { teams, searchQuery } = this.props;
|
||||
|
||||
return (
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
@@ -72,7 +93,7 @@ export class TeamList extends React.Component<Props, any> {
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search teams"
|
||||
value={teams.search}
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
@@ -97,49 +118,38 @@ export class TeamList extends React.Component<Props, any> {
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
||||
<tbody>{teams.map(team => this.renderTeam(team))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmptyList() {
|
||||
return (
|
||||
<div className="page-container page-body">
|
||||
<EmptyListCTA
|
||||
model={{
|
||||
title: "You haven't created any teams yet.",
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nav, teams } = this.props;
|
||||
let view;
|
||||
|
||||
if (teams.filteredTeams.length > 0) {
|
||||
view = this.renderTeamList(teams);
|
||||
} else {
|
||||
view = this.renderEmptyList();
|
||||
}
|
||||
const { navModel, teamsCount } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={nav as any} />
|
||||
{view}
|
||||
<PageHeader model={navModel} />
|
||||
{teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamList);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'teams'),
|
||||
teams: getTeams(state.teams),
|
||||
searchQuery: getSearchQuery(state.teams),
|
||||
teamsCount: getTeamsCount(state.teams),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTeams,
|
||||
deleteTeam,
|
||||
setSearchQuery,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList));
|
80
public/app/features/teams/TeamMembers.test.tsx
Normal file
80
public/app/features/teams/TeamMembers.test.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TeamMembers, Props, State } from './TeamMembers';
|
||||
import { TeamMember } from '../../types';
|
||||
import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
members: [] as TeamMember[],
|
||||
searchMemberQuery: '',
|
||||
setSearchMemberQuery: jest.fn(),
|
||||
loadTeamMembers: jest.fn(),
|
||||
addTeamMember: jest.fn(),
|
||||
removeTeamMember: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<TeamMembers {...props} />);
|
||||
const instance = wrapper.instance() as TeamMembers;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render team members', () => {
|
||||
const { wrapper } = setup({
|
||||
members: getMockTeamMembers(5),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
describe('on search member query change', () => {
|
||||
it('it should call setSearchMemberQuery', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'member' } };
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
|
||||
expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on remove member', () => {
|
||||
const { instance } = setup();
|
||||
const mockTeamMember = getMockTeamMember();
|
||||
|
||||
instance.onRemoveMember(mockTeamMember);
|
||||
|
||||
expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
describe('on add user to team', () => {
|
||||
const { wrapper, instance } = setup();
|
||||
const state = wrapper.state() as State;
|
||||
|
||||
state.newTeamMember = {
|
||||
id: 1,
|
||||
label: '',
|
||||
avatarUrl: '',
|
||||
login: '',
|
||||
};
|
||||
|
||||
instance.onAddUserToTeam();
|
||||
|
||||
expect(instance.props.addTeamMember).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
@@ -1,56 +1,42 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||
import { TeamMember } from '../../types';
|
||||
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
|
||||
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
|
||||
|
||||
interface Props {
|
||||
team: Team;
|
||||
export interface Props {
|
||||
members: TeamMember[];
|
||||
searchMemberQuery: string;
|
||||
loadTeamMembers: typeof loadTeamMembers;
|
||||
addTeamMember: typeof addTeamMember;
|
||||
removeTeamMember: typeof removeTeamMember;
|
||||
setSearchMemberQuery: typeof setSearchMemberQuery;
|
||||
}
|
||||
|
||||
interface State {
|
||||
export interface State {
|
||||
isAdding: boolean;
|
||||
newTeamMember?: User;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TeamMembers extends React.Component<Props, State> {
|
||||
export class TeamMembers extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newTeamMember: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.team.loadMembers();
|
||||
this.props.loadTeamMembers();
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.team.setSearchQuery(evt.target.value);
|
||||
onSearchQueryChange = event => {
|
||||
this.props.setSearchMemberQuery(event.target.value);
|
||||
};
|
||||
|
||||
removeMember(member: TeamMember) {
|
||||
this.props.team.removeMember(member);
|
||||
}
|
||||
|
||||
removeMemberConfirmed(member: TeamMember) {
|
||||
this.props.team.removeMember(member);
|
||||
}
|
||||
|
||||
renderMember(member: TeamMember) {
|
||||
return (
|
||||
<tr key={member.userId}>
|
||||
<td className="width-4 text-center">
|
||||
<img className="filter-table__avatar" src={member.avatarUrl} />
|
||||
</td>
|
||||
<td>{member.login}</td>
|
||||
<td>{member.email}</td>
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirmDelete={() => this.removeMember(member)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
onRemoveMember(member: TeamMember) {
|
||||
this.props.removeTeamMember(member.userId);
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
@@ -62,16 +48,29 @@ export class TeamMembers extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
onAddUserToTeam = async () => {
|
||||
await this.props.team.addMember(this.state.newTeamMember.id);
|
||||
await this.props.team.loadMembers();
|
||||
this.props.addTeamMember(this.state.newTeamMember.id);
|
||||
this.setState({ newTeamMember: null });
|
||||
};
|
||||
|
||||
renderMember(member: TeamMember) {
|
||||
return (
|
||||
<tr key={member.userId}>
|
||||
<td className="width-4 text-center">
|
||||
<img className="filter-table__avatar" src={member.avatarUrl} />
|
||||
</td>
|
||||
<td>{member.login}</td>
|
||||
<td>{member.email}</td>
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { newTeamMember, isAdding } = this.state;
|
||||
const members = this.props.team.filteredMembers;
|
||||
const { searchMemberQuery, members } = this.props;
|
||||
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
|
||||
const { team } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -82,7 +81,7 @@ export class TeamMembers extends React.Component<Props, State> {
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search members"
|
||||
value={team.search}
|
||||
value={searchMemberQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
@@ -124,7 +123,7 @@ export class TeamMembers extends React.Component<Props, State> {
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{members.map(member => this.renderMember(member))}</tbody>
|
||||
<tbody>{members && members.map(member => this.renderMember(member))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,4 +131,18 @@ export class TeamMembers extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamMembers);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
members: getTeamMembers(state.team),
|
||||
searchMemberQuery: getSearchMemberQuery(state.team),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTeamMembers,
|
||||
addTeamMember,
|
||||
removeTeamMember,
|
||||
setSearchMemberQuery,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers);
|
63
public/app/features/teams/TeamPages.test.tsx
Normal file
63
public/app/features/teams/TeamPages.test.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TeamPages, Props } from './TeamPages';
|
||||
import { NavModel, Team } from '../../types';
|
||||
import { getMockTeam } from './__mocks__/teamMocks';
|
||||
|
||||
jest.mock('app/core/config', () => ({
|
||||
buildInfo: { isEnterprise: true },
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {} as NavModel,
|
||||
teamId: 1,
|
||||
loadTeam: jest.fn(),
|
||||
pageName: 'members',
|
||||
team: {} as Team,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<TeamPages {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render member page if team not empty', () => {
|
||||
const { wrapper } = setup({
|
||||
team: getMockTeam(),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render settings page', () => {
|
||||
const { wrapper } = setup({
|
||||
team: getMockTeam(),
|
||||
pageName: 'settings',
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render group sync page', () => {
|
||||
const { wrapper } = setup({
|
||||
team: getMockTeam(),
|
||||
pageName: 'groupsync',
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
105
public/app/features/teams/TeamPages.tsx
Normal file
105
public/app/features/teams/TeamPages.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import config from 'app/core/config';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import TeamMembers from './TeamMembers';
|
||||
import TeamSettings from './TeamSettings';
|
||||
import TeamGroupSync from './TeamGroupSync';
|
||||
import { NavModel, Team } from '../../types';
|
||||
import { loadTeam } from './state/actions';
|
||||
import { getTeam } from './state/selectors';
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
|
||||
|
||||
export interface Props {
|
||||
team: Team;
|
||||
loadTeam: typeof loadTeam;
|
||||
teamId: number;
|
||||
pageName: string;
|
||||
navModel: NavModel;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isSyncEnabled: boolean;
|
||||
}
|
||||
|
||||
enum PageTypes {
|
||||
Members = 'members',
|
||||
Settings = 'settings',
|
||||
GroupSync = 'groupsync',
|
||||
}
|
||||
|
||||
export class TeamPages extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isSyncEnabled: config.buildInfo.isEnterprise,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchTeam();
|
||||
}
|
||||
|
||||
async fetchTeam() {
|
||||
const { loadTeam, teamId } = this.props;
|
||||
|
||||
await loadTeam(teamId);
|
||||
}
|
||||
|
||||
getCurrentPage() {
|
||||
const pages = ['members', 'settings', 'groupsync'];
|
||||
const currentPage = this.props.pageName;
|
||||
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
const { isSyncEnabled } = this.state;
|
||||
const currentPage = this.getCurrentPage();
|
||||
|
||||
switch (currentPage) {
|
||||
case PageTypes.Members:
|
||||
return <TeamMembers />;
|
||||
|
||||
case PageTypes.Settings:
|
||||
return <TeamSettings />;
|
||||
|
||||
case PageTypes.GroupSync:
|
||||
return isSyncEnabled && <TeamGroupSync />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { team, navModel } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
{team && Object.keys(team).length !== 0 && <div className="page-container page-body">{this.renderPage()}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const teamId = getRouteParamsId(state.location);
|
||||
const pageName = getRouteParamsPage(state.location) || 'members';
|
||||
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`),
|
||||
teamId: teamId,
|
||||
pageName: pageName,
|
||||
team: getTeam(state.team, teamId),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTeam,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));
|
44
public/app/features/teams/TeamSettings.test.tsx
Normal file
44
public/app/features/teams/TeamSettings.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Props, TeamSettings } from './TeamSettings';
|
||||
import { getMockTeam } from './__mocks__/teamMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
team: getMockTeam(),
|
||||
updateTeam: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<TeamSettings {...props} />);
|
||||
const instance = wrapper.instance() as TeamSettings;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
it('should update team', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { preventDefault: jest.fn() };
|
||||
|
||||
instance.setState({
|
||||
name: 'test11',
|
||||
});
|
||||
|
||||
instance.onUpdate(mockEvent);
|
||||
|
||||
expect(instance.props.updateTeam).toHaveBeenCalledWith('test11', 'test@test.com');
|
||||
});
|
||||
});
|
96
public/app/features/teams/TeamSettings.tsx
Normal file
96
public/app/features/teams/TeamSettings.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Label } from 'app/core/components/Forms/Forms';
|
||||
import { Team } from '../../types';
|
||||
import { updateTeam } from './state/actions';
|
||||
import { getRouteParamsId } from '../../core/selectors/location';
|
||||
import { getTeam } from './state/selectors';
|
||||
|
||||
export interface Props {
|
||||
team: Team;
|
||||
updateTeam: typeof updateTeam;
|
||||
}
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export class TeamSettings extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
name: props.team.name,
|
||||
email: props.team.email,
|
||||
};
|
||||
}
|
||||
|
||||
onChangeName = event => {
|
||||
this.setState({ name: event.target.value });
|
||||
};
|
||||
|
||||
onChangeEmail = event => {
|
||||
this.setState({ email: event.target.value });
|
||||
};
|
||||
|
||||
onUpdate = event => {
|
||||
const { name, email } = this.state;
|
||||
event.preventDefault();
|
||||
this.props.updateTeam(name, email);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, email } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-sub-heading">Team Settings</h3>
|
||||
<form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}>
|
||||
<div className="gf-form max-width-30">
|
||||
<Label>Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
className="gf-form-input max-width-22"
|
||||
onChange={this.onChangeName}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form max-width-30">
|
||||
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
|
||||
Email
|
||||
</Label>
|
||||
<input
|
||||
type="email"
|
||||
className="gf-form-input max-width-22"
|
||||
value={email}
|
||||
placeholder="team@email.com"
|
||||
onChange={this.onChangeEmail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-button-row">
|
||||
<button type="submit" className="btn btn-success">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const teamId = getRouteParamsId(state.location);
|
||||
|
||||
return {
|
||||
team: getTeam(state.team, teamId),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateTeam,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings);
|
59
public/app/features/teams/__mocks__/navModelMock.ts
Normal file
59
public/app/features/teams/__mocks__/navModelMock.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export const getMockNavModel = (pageName: string) => {
|
||||
return {
|
||||
node: {
|
||||
active: false,
|
||||
icon: 'gicon gicon-team',
|
||||
id: `team-${pageName}-2`,
|
||||
text: `${pageName}`,
|
||||
url: 'org/teams/edit/2/members',
|
||||
parentItem: {
|
||||
img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b',
|
||||
id: 'team-2',
|
||||
subTitle: 'Manage members & settings',
|
||||
url: '',
|
||||
text: 'test1',
|
||||
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||
children: [
|
||||
{
|
||||
active: false,
|
||||
icon: 'gicon gicon-team',
|
||||
id: 'team-members-2',
|
||||
text: 'Members',
|
||||
url: 'org/teams/edit/2/members',
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
icon: 'fa fa-fw fa-sliders',
|
||||
id: 'team-settings-2',
|
||||
text: 'Settings',
|
||||
url: 'org/teams/edit/2/settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
main: {
|
||||
img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b',
|
||||
id: 'team-2',
|
||||
subTitle: 'Manage members & settings',
|
||||
url: '',
|
||||
text: 'test1',
|
||||
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||
children: [
|
||||
{
|
||||
active: true,
|
||||
icon: 'gicon gicon-team',
|
||||
id: 'team-members-2',
|
||||
text: 'Members',
|
||||
url: 'org/teams/edit/2/members',
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
icon: 'fa fa-fw fa-sliders',
|
||||
id: 'team-settings-2',
|
||||
text: 'Settings',
|
||||
url: 'org/teams/edit/2/settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
65
public/app/features/teams/__mocks__/teamMocks.ts
Normal file
65
public/app/features/teams/__mocks__/teamMocks.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Team, TeamGroup, TeamMember } from '../../../types';
|
||||
|
||||
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
||||
const teams: Team[] = [];
|
||||
for (let i = 1; i <= numberOfTeams; i++) {
|
||||
teams.push({
|
||||
id: i,
|
||||
name: `test-${i}`,
|
||||
avatarUrl: 'some/url/',
|
||||
email: `test-${i}@test.com`,
|
||||
memberCount: i,
|
||||
});
|
||||
}
|
||||
|
||||
return teams;
|
||||
};
|
||||
|
||||
export const getMockTeam = (): Team => {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
avatarUrl: 'some/url/',
|
||||
email: 'test@test.com',
|
||||
memberCount: 1,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockTeamMembers = (amount: number): TeamMember[] => {
|
||||
const teamMembers: TeamMember[] = [];
|
||||
|
||||
for (let i = 1; i <= amount; i++) {
|
||||
teamMembers.push({
|
||||
userId: i,
|
||||
teamId: 1,
|
||||
avatarUrl: 'some/url/',
|
||||
email: 'test@test.com',
|
||||
login: `testUser-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
return teamMembers;
|
||||
};
|
||||
|
||||
export const getMockTeamMember = (): TeamMember => {
|
||||
return {
|
||||
userId: 1,
|
||||
teamId: 1,
|
||||
avatarUrl: 'some/url/',
|
||||
email: 'test@test.com',
|
||||
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>
|
||||
`;
|
354
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
Normal file
354
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
Normal file
@@ -0,0 +1,354 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`Render should render teams table 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search 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>
|
||||
<tr
|
||||
key="1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
test-1
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
test-1@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
1
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="2"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/2"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/2"
|
||||
>
|
||||
test-2
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/2"
|
||||
>
|
||||
test-2@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/2"
|
||||
>
|
||||
2
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="3"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/3"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/3"
|
||||
>
|
||||
test-3
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/3"
|
||||
>
|
||||
test-3@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/3"
|
||||
>
|
||||
3
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="4"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/4"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/4"
|
||||
>
|
||||
test-4
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/4"
|
||||
>
|
||||
test-4@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/4"
|
||||
>
|
||||
4
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="5"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/5"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/5"
|
||||
>
|
||||
test-5
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/5"
|
||||
>
|
||||
test-5@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/5"
|
||||
>
|
||||
5
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@@ -0,0 +1,317 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<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 members"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-success pull-right"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
Add a member
|
||||
</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 Team Member
|
||||
</h5>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
>
|
||||
<UserPicker
|
||||
className="width-30"
|
||||
onSelected={[Function]}
|
||||
value={null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
<div
|
||||
className="admin-list-table"
|
||||
>
|
||||
<table
|
||||
className="filter-table filter-table--hover form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "1%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render team members 1`] = `
|
||||
<div>
|
||||
<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 members"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-success pull-right"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
Add a member
|
||||
</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 Team Member
|
||||
</h5>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
>
|
||||
<UserPicker
|
||||
className="width-30"
|
||||
onSelected={[Function]}
|
||||
value={null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
<div
|
||||
className="admin-list-table"
|
||||
>
|
||||
<table
|
||||
className="filter-table filter-table--hover form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "1%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-1
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="2"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-2
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="3"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-3
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="4"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-4
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="5"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-5
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@@ -0,0 +1,48 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render group sync page 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<Connect(TeamGroupSync) />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render member page if team not empty 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<Connect(TeamMembers) />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render settings page 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<Connect(TeamSettings) />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@@ -0,0 +1,57 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<h3
|
||||
className="page-sub-heading"
|
||||
>
|
||||
Team Settings
|
||||
</h3>
|
||||
<form
|
||||
className="gf-form-group"
|
||||
name="teamDetailsForm"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="gf-form max-width-30"
|
||||
>
|
||||
<Component>
|
||||
Name
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input max-width-22"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
type="text"
|
||||
value="test"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form max-width-30"
|
||||
>
|
||||
<Component
|
||||
tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)"
|
||||
>
|
||||
Email
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input max-width-22"
|
||||
onChange={[Function]}
|
||||
placeholder="team@email.com"
|
||||
type="email"
|
||||
value="test@test.com"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form-button-row"
|
||||
>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
237
public/app/features/teams/state/actions.ts
Normal file
237
public/app/features/teams/state/actions.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
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_TEAM_SEARCH_QUERY',
|
||||
SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY',
|
||||
LoadTeamMembers = 'TEAM_MEMBERS_LOADED',
|
||||
LoadTeamGroups = 'TEAM_GROUPS_LOADED',
|
||||
}
|
||||
|
||||
export interface LoadTeamsAction {
|
||||
type: ActionTypes.LoadTeams;
|
||||
payload: Team[];
|
||||
}
|
||||
|
||||
export interface LoadTeamAction {
|
||||
type: ActionTypes.LoadTeam;
|
||||
payload: Team;
|
||||
}
|
||||
|
||||
export interface LoadTeamMembersAction {
|
||||
type: ActionTypes.LoadTeamMembers;
|
||||
payload: TeamMember[];
|
||||
}
|
||||
|
||||
export interface LoadTeamGroupsAction {
|
||||
type: ActionTypes.LoadTeamGroups;
|
||||
payload: TeamGroup[];
|
||||
}
|
||||
|
||||
export interface SetSearchQueryAction {
|
||||
type: ActionTypes.SetSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetSearchMemberQueryAction {
|
||||
type: ActionTypes.SetSearchMemberQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| LoadTeamsAction
|
||||
| SetSearchQueryAction
|
||||
| LoadTeamAction
|
||||
| LoadTeamMembersAction
|
||||
| SetSearchMemberQueryAction
|
||||
| LoadTeamGroupsAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
|
||||
|
||||
const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({
|
||||
type: ActionTypes.LoadTeams,
|
||||
payload: teams,
|
||||
});
|
||||
|
||||
const teamLoaded = (team: Team): LoadTeamAction => ({
|
||||
type: ActionTypes.LoadTeam,
|
||||
payload: team,
|
||||
});
|
||||
|
||||
const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({
|
||||
type: ActionTypes.LoadTeamMembers,
|
||||
payload: teamMembers,
|
||||
});
|
||||
|
||||
const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({
|
||||
type: ActionTypes.LoadTeamGroups,
|
||||
payload: teamGroups,
|
||||
});
|
||||
|
||||
export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({
|
||||
type: ActionTypes.SetSearchMemberQuery,
|
||||
payload: searchQuery,
|
||||
});
|
||||
|
||||
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
|
||||
type: ActionTypes.SetSearchQuery,
|
||||
payload: searchQuery,
|
||||
});
|
||||
|
||||
export function loadTeams(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 });
|
||||
dispatch(teamsLoaded(response.teams));
|
||||
};
|
||||
}
|
||||
|
||||
function buildNavModel(team: Team): NavModelItem {
|
||||
const navModel = {
|
||||
img: team.avatarUrl,
|
||||
id: 'team-' + team.id,
|
||||
subTitle: 'Manage members & settings',
|
||||
url: '',
|
||||
text: team.name,
|
||||
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||
children: [
|
||||
{
|
||||
active: false,
|
||||
icon: 'gicon gicon-team',
|
||||
id: `team-members-${team.id}`,
|
||||
text: 'Members',
|
||||
url: `org/teams/edit/${team.id}/members`,
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
icon: 'fa fa-fw fa-sliders',
|
||||
id: `team-settings-${team.id}`,
|
||||
text: 'Settings',
|
||||
url: `org/teams/edit/${team.id}/settings`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (config.buildInfo.isEnterprise) {
|
||||
navModel.children.push({
|
||||
active: false,
|
||||
icon: 'fa fa-fw fa-refresh',
|
||||
id: `team-groupsync-${team.id}`,
|
||||
text: 'External group sync',
|
||||
url: `org/teams/edit/${team.id}/groupsync`,
|
||||
});
|
||||
}
|
||||
|
||||
return navModel;
|
||||
}
|
||||
|
||||
export function loadTeam(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv()
|
||||
.get(`/api/teams/${id}`)
|
||||
.then(response => {
|
||||
dispatch(teamLoaded(response));
|
||||
dispatch(updateNavIndex(buildNavModel(response)));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTeamMembers(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.get(`/api/teams/${team.id}/members`)
|
||||
.then(response => {
|
||||
dispatch(teamMembersLoaded(response));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addTeamMember(id: number): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.post(`/api/teams/${team.id}/members`, { userId: id })
|
||||
.then(() => {
|
||||
dispatch(loadTeamMembers());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTeamMember(id: number): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.delete(`/api/teams/${team.id}/members/${id}`)
|
||||
.then(() => {
|
||||
dispatch(loadTeamMembers());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTeam(name: string, email: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
await getBackendSrv()
|
||||
.put(`/api/teams/${team.id}`, {
|
||||
name,
|
||||
email,
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(loadTeam(team.id));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTeamGroups(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.get(`/api/teams/${team.id}/groups`)
|
||||
.then(response => {
|
||||
dispatch(teamGroupsLoaded(response));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addTeamGroup(groupId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.post(`/api/teams/${team.id}/groups`, { groupId: groupId })
|
||||
.then(() => {
|
||||
dispatch(loadTeamGroups());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTeamGroup(groupId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.delete(`/api/teams/${team.id}/groups/${groupId}`)
|
||||
.then(() => {
|
||||
dispatch(loadTeamGroups());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteTeam(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv()
|
||||
.delete(`/api/teams/${id}`)
|
||||
.then(() => {
|
||||
dispatch(loadTeams());
|
||||
});
|
||||
};
|
||||
}
|
72
public/app/features/teams/state/reducers.test.ts
Normal file
72
public/app/features/teams/state/reducers.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers';
|
||||
import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks';
|
||||
|
||||
describe('teams reducer', () => {
|
||||
it('should set teams', () => {
|
||||
const payload = [getMockTeam()];
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadTeams,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamsReducer(initialTeamsState, action);
|
||||
|
||||
expect(result.teams).toEqual(payload);
|
||||
});
|
||||
|
||||
it('should set search query', () => {
|
||||
const payload = 'test';
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.SetSearchQuery,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamsReducer(initialTeamsState, action);
|
||||
|
||||
expect(result.searchQuery).toEqual('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('team reducer', () => {
|
||||
it('should set team', () => {
|
||||
const payload = getMockTeam();
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadTeam,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamReducer(initialTeamState, action);
|
||||
|
||||
expect(result.team).toEqual(payload);
|
||||
});
|
||||
|
||||
it('should set team members', () => {
|
||||
const mockTeamMember = getMockTeamMember();
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadTeamMembers,
|
||||
payload: [mockTeamMember],
|
||||
};
|
||||
|
||||
const result = teamReducer(initialTeamState, action);
|
||||
|
||||
expect(result.members).toEqual([mockTeamMember]);
|
||||
});
|
||||
|
||||
it('should set member search query', () => {
|
||||
const payload = 'member';
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.SetSearchMemberQuery,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamReducer(initialTeamState, action);
|
||||
|
||||
expect(result.searchMemberQuery).toEqual('member');
|
||||
});
|
||||
});
|
44
public/app/features/teams/state/reducers.ts
Normal file
44
public/app/features/teams/state/reducers.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
|
||||
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };
|
||||
export const initialTeamState: TeamState = {
|
||||
team: {} as Team,
|
||||
members: [] as TeamMember[],
|
||||
groups: [] as TeamGroup[],
|
||||
searchMemberQuery: '',
|
||||
};
|
||||
|
||||
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadTeams:
|
||||
return { ...state, teams: action.payload };
|
||||
|
||||
case ActionTypes.SetSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const teamReducer = (state = initialTeamState, action: Action): TeamState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadTeam:
|
||||
return { ...state, team: action.payload };
|
||||
|
||||
case ActionTypes.LoadTeamMembers:
|
||||
return { ...state, members: action.payload };
|
||||
|
||||
case ActionTypes.SetSearchMemberQuery:
|
||||
return { ...state, searchMemberQuery: action.payload };
|
||||
|
||||
case ActionTypes.LoadTeamGroups:
|
||||
return { ...state, groups: action.payload };
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
teams: teamsReducer,
|
||||
team: teamReducer,
|
||||
};
|
56
public/app/features/teams/state/selectors.test.ts
Normal file
56
public/app/features/teams/state/selectors.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { getTeam, getTeamMembers, getTeams } from './selectors';
|
||||
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
|
||||
import { Team, TeamGroup, TeamsState, TeamState } from '../../../types';
|
||||
|
||||
describe('Teams selectors', () => {
|
||||
describe('Get teams', () => {
|
||||
const mockTeams = getMultipleMockTeams(5);
|
||||
|
||||
it('should return teams if no search query', () => {
|
||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '' };
|
||||
|
||||
const teams = getTeams(mockState);
|
||||
|
||||
expect(teams).toEqual(mockTeams);
|
||||
});
|
||||
|
||||
it('Should filter teams if search query', () => {
|
||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' };
|
||||
|
||||
const teams = getTeams(mockState);
|
||||
|
||||
expect(teams.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Team selectors', () => {
|
||||
describe('Get team', () => {
|
||||
const mockTeam = getMockTeam();
|
||||
|
||||
it('should return team if matching with location team', () => {
|
||||
const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] };
|
||||
|
||||
const team = getTeam(mockState, '1');
|
||||
|
||||
expect(team).toEqual(mockTeam);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get members', () => {
|
||||
const mockTeamMembers = getMockTeamMembers(5);
|
||||
|
||||
it('should return team members', () => {
|
||||
const mockState: TeamState = {
|
||||
team: {} as Team,
|
||||
searchMemberQuery: '',
|
||||
members: mockTeamMembers,
|
||||
groups: [] as TeamGroup[],
|
||||
};
|
||||
|
||||
const members = getTeamMembers(mockState);
|
||||
|
||||
expect(members).toEqual(mockTeamMembers);
|
||||
});
|
||||
});
|
||||
});
|
30
public/app/features/teams/state/selectors.ts
Normal file
30
public/app/features/teams/state/selectors.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Team, TeamsState, TeamState } from 'app/types';
|
||||
|
||||
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: TeamsState) => {
|
||||
const regex = RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.teams.filter(team => {
|
||||
return regex.test(team.name);
|
||||
});
|
||||
};
|
||||
|
||||
export const getTeamMembers = (state: TeamState) => {
|
||||
const regex = RegExp(state.searchMemberQuery, 'i');
|
||||
|
||||
return state.members.filter(member => {
|
||||
return regex.test(member.login) || regex.test(member.email);
|
||||
});
|
||||
};
|
@@ -39,7 +39,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
|
||||
|
||||
// Add empty selector to bare metric name
|
||||
let previousWord;
|
||||
query = query.replace(/(\w+)\b(?![\(\]{=",])/g, (match, word, offset) => {
|
||||
query = query.replace(/([A-Za-z]\w*)\b(?![\(\]{=",])/g, (match, word, offset) => {
|
||||
// Check if inside a selector
|
||||
const nextSelectorStart = query.slice(offset).indexOf('{');
|
||||
const nextSelectorEnd = query.slice(offset).indexOf('}');
|
||||
|
@@ -376,6 +376,7 @@ describe('PrometheusDatasource', () => {
|
||||
'foo{bar="baz",instance="my-host.com:9100"}'
|
||||
);
|
||||
expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])');
|
||||
expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001');
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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 TeamPages from 'app/features/teams/TeamPages';
|
||||
import TeamList from 'app/features/teams/TeamList';
|
||||
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
||||
import TeamPages from 'app/containers/Teams/TeamPages';
|
||||
import TeamList from 'app/containers/Teams/TeamList';
|
||||
|
||||
/** @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));
|
||||
}),
|
||||
}));
|
@@ -3,10 +3,12 @@ import thunk from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import sharedReducers from 'app/core/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
import teamsReducers from 'app/features/teams/state/reducers';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
...alertingReducers,
|
||||
...teamsReducers,
|
||||
});
|
||||
|
||||
export let store;
|
||||
|
35
public/app/types/alerting.ts
Normal file
35
public/app/types/alerting.ts
Normal 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;
|
||||
}
|
@@ -1,96 +1,30 @@
|
||||
//
|
||||
// Location
|
||||
//
|
||||
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';
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
//
|
||||
// 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?: NavModelItem[];
|
||||
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 {
|
||||
Team,
|
||||
TeamsState,
|
||||
TeamState,
|
||||
TeamGroup,
|
||||
TeamMember,
|
||||
AlertRuleDTO,
|
||||
AlertRule,
|
||||
AlertRulesState,
|
||||
LocationState,
|
||||
LocationUpdate,
|
||||
NavModel,
|
||||
NavModelItem,
|
||||
NavIndex,
|
||||
UrlQueryMap,
|
||||
UrlQueryValue,
|
||||
};
|
||||
|
||||
export interface StoreState {
|
||||
navIndex: NavIndex;
|
||||
location: LocationState;
|
||||
alertRules: AlertRulesState;
|
||||
teams: TeamsState;
|
||||
team: TeamState;
|
||||
}
|
||||
|
15
public/app/types/location.ts
Normal file
15
public/app/types/location.ts
Normal 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 };
|
22
public/app/types/navModel.ts
Normal file
22
public/app/types/navModel.ts
Normal 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
32
public/app/types/teams.ts
Normal 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;
|
||||
}
|
@@ -7,7 +7,7 @@ module.exports = function(config, grunt) {
|
||||
}
|
||||
|
||||
return {
|
||||
tslint: 'node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json',
|
||||
tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json',
|
||||
jest: 'node ./node_modules/jest-cli/bin/jest.js ' + coverage,
|
||||
webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js',
|
||||
};
|
||||
|
Reference in New Issue
Block a user