mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 07:35:45 -06:00
Merge branch 'master' into react-panels
This commit is contained in:
commit
902eba90d0
@ -88,6 +88,9 @@ jobs:
|
||||
- run:
|
||||
name: run linters
|
||||
command: 'gometalinter.v2 --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/...'
|
||||
|
||||
test-frontend:
|
||||
docker:
|
||||
@ -243,7 +246,7 @@ workflows:
|
||||
test-and-build:
|
||||
jobs:
|
||||
- build-all:
|
||||
filters: *filter-not-release
|
||||
filters: *filter-only-master
|
||||
- build-enterprise:
|
||||
filters: *filter-only-master
|
||||
- codespell:
|
||||
@ -267,9 +270,7 @@ workflows:
|
||||
- gometalinter
|
||||
- mysql-integration-test
|
||||
- postgres-integration-test
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
filters: *filter-only-master
|
||||
- deploy-enterprise-master:
|
||||
requires:
|
||||
- build-all
|
||||
|
@ -149,7 +149,7 @@
|
||||
"classnames": "^2.2.5",
|
||||
"clipboard": "^1.7.1",
|
||||
"d3": "^4.11.0",
|
||||
"d3-scale-chromatic": "^1.1.1",
|
||||
"d3-scale-chromatic": "^1.3.0",
|
||||
"eventemitter3": "^2.0.3",
|
||||
"file-saver": "^1.3.3",
|
||||
"immutable": "^3.8.2",
|
||||
|
@ -31,7 +31,7 @@ func TestAlertingApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
query.Result = []*m.TeamDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -119,7 +119,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
query.Result = []*m.TeamDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -39,7 +39,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
teamResp := []*m.Team{}
|
||||
teamResp := []*m.TeamDTO{}
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = teamResp
|
||||
return nil
|
||||
|
@ -61,7 +61,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
query.Result = []*m.TeamDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -230,7 +230,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
query.Result = []*m.TeamDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -93,5 +93,6 @@ func GetTeamByID(c *m.ReqContext) Response {
|
||||
return Error(500, "Failed to get Team", err)
|
||||
}
|
||||
|
||||
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
|
||||
return JSON(200, &query.Result)
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
func TestTeamApiEndpoint(t *testing.T) {
|
||||
Convey("Given two teams", t, func() {
|
||||
mockResult := models.SearchTeamQueryResult{
|
||||
Teams: []*models.SearchTeamDto{
|
||||
Teams: []*models.TeamDTO{
|
||||
{Name: "team1"},
|
||||
{Name: "team2"},
|
||||
},
|
||||
|
@ -49,13 +49,13 @@ type DeleteTeamCommand struct {
|
||||
type GetTeamByIdQuery struct {
|
||||
OrgId int64
|
||||
Id int64
|
||||
Result *Team
|
||||
Result *TeamDTO
|
||||
}
|
||||
|
||||
type GetTeamsByUserQuery struct {
|
||||
OrgId int64
|
||||
UserId int64 `json:"userId"`
|
||||
Result []*Team `json:"teams"`
|
||||
UserId int64 `json:"userId"`
|
||||
Result []*TeamDTO `json:"teams"`
|
||||
}
|
||||
|
||||
type SearchTeamsQuery struct {
|
||||
@ -68,7 +68,7 @@ type SearchTeamsQuery struct {
|
||||
Result SearchTeamQueryResult
|
||||
}
|
||||
|
||||
type SearchTeamDto struct {
|
||||
type TeamDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
@ -78,8 +78,8 @@ type SearchTeamDto struct {
|
||||
}
|
||||
|
||||
type SearchTeamQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Teams []*SearchTeamDto `json:"teams"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Teams []*TeamDTO `json:"teams"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ type dashboardGuardianImpl struct {
|
||||
dashId int64
|
||||
orgId int64
|
||||
acl []*m.DashboardAclInfoDTO
|
||||
groups []*m.Team
|
||||
teams []*m.TeamDTO
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
@ -186,15 +186,15 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
|
||||
return g.acl, nil
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
|
||||
if g.groups != nil {
|
||||
return g.groups, nil
|
||||
func (g *dashboardGuardianImpl) getTeams() ([]*m.TeamDTO, error) {
|
||||
if g.teams != nil {
|
||||
return g.teams, nil
|
||||
}
|
||||
|
||||
query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
|
||||
err := bus.Dispatch(&query)
|
||||
|
||||
g.groups = query.Result
|
||||
g.teams = query.Result
|
||||
return query.Result, err
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ type scenarioContext struct {
|
||||
givenUser *m.SignedInUser
|
||||
givenDashboardID int64
|
||||
givenPermissions []*m.DashboardAclInfoDTO
|
||||
givenTeams []*m.Team
|
||||
givenTeams []*m.TeamDTO
|
||||
updatePermissions []*m.DashboardAcl
|
||||
expectedFlags permissionFlags
|
||||
callerFile string
|
||||
@ -84,11 +84,11 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, per
|
||||
return nil
|
||||
})
|
||||
|
||||
teams := []*m.Team{}
|
||||
teams := []*m.TeamDTO{}
|
||||
|
||||
for _, p := range permissions {
|
||||
if p.TeamId > 0 {
|
||||
teams = append(teams, &m.Team{Id: p.TeamId})
|
||||
teams = append(teams, &m.TeamDTO{Id: p.TeamId})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,8 +98,6 @@ func (ns *NotificationService) Run(ctx context.Context) error {
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
|
||||
|
@ -58,7 +58,9 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
||||
cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", opts.Encoding)}, cmdArgs...)
|
||||
}
|
||||
|
||||
commandCtx, _ := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
|
||||
commandCtx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
|
@ -22,6 +22,16 @@ func init() {
|
||||
bus.AddHandler("sql", GetTeamMembers)
|
||||
}
|
||||
|
||||
func getTeamSelectSqlBase() string {
|
||||
return `SELECT
|
||||
team.id as id,
|
||||
team.org_id,
|
||||
team.name as name,
|
||||
team.email as email,
|
||||
(SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count
|
||||
FROM team as team `
|
||||
}
|
||||
|
||||
func CreateTeam(cmd *m.CreateTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
@ -130,21 +140,15 @@ func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession
|
||||
|
||||
func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||
query.Result = m.SearchTeamQueryResult{
|
||||
Teams: make([]*m.SearchTeamDto, 0),
|
||||
Teams: make([]*m.TeamDTO, 0),
|
||||
}
|
||||
queryWithWildcards := "%" + query.Query + "%"
|
||||
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql.WriteString(`select
|
||||
team.id as id,
|
||||
team.org_id,
|
||||
team.name as name,
|
||||
team.email as email,
|
||||
(select count(*) from team_member where team_member.team_id = team.id) as member_count
|
||||
from team as team
|
||||
where team.org_id = ?`)
|
||||
sql.WriteString(getTeamSelectSqlBase())
|
||||
sql.WriteString(` WHERE team.org_id = ?`)
|
||||
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
@ -186,8 +190,14 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||
}
|
||||
|
||||
func GetTeamById(query *m.GetTeamByIdQuery) error {
|
||||
var team m.Team
|
||||
exists, err := x.Where("org_id=? and id=?", query.OrgId, query.Id).Get(&team)
|
||||
var sql bytes.Buffer
|
||||
|
||||
sql.WriteString(getTeamSelectSqlBase())
|
||||
sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
|
||||
|
||||
var team m.TeamDTO
|
||||
exists, err := x.Sql(sql.String(), query.OrgId, query.Id).Get(&team)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -202,13 +212,15 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
|
||||
|
||||
// GetTeamsByUser is used by the Guardian when checking a users' permissions
|
||||
func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = make([]*m.Team, 0)
|
||||
query.Result = make([]*m.TeamDTO, 0)
|
||||
|
||||
sess := x.Table("team")
|
||||
sess.Join("INNER", "team_member", "team.id=team_member.team_id")
|
||||
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
|
||||
var sql bytes.Buffer
|
||||
|
||||
err := sess.Find(&query.Result)
|
||||
sql.WriteString(getTeamSelectSqlBase())
|
||||
sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`)
|
||||
sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`)
|
||||
|
||||
err := x.Sql(sql.String(), query.OrgId, query.UserId).Find(&query.Result)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -218,7 +218,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
elapsed := time.Now().Sub(start)
|
||||
clientLog.Debug("Decoded multisearch json response", "took", elapsed)
|
||||
|
||||
msr.status = res.StatusCode
|
||||
msr.Status = res.StatusCode
|
||||
|
||||
return &msr, nil
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ type MultiSearchRequest struct {
|
||||
|
||||
// MultiSearchResponse represents a multi search response
|
||||
type MultiSearchResponse struct {
|
||||
status int `json:"status,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Responses []*SearchResponse `json:"responses"`
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
<PageHeader model={nav as any} />
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
<h2 className="d-inline-block">Folder Permissions</h2>
|
||||
<h3 className="page-sub-heading">Folder Permissions</h3>
|
||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</Tooltip>
|
||||
@ -68,7 +68,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
</button>
|
||||
</div>
|
||||
<SlideDown in={permissions.isAddPermissionsVisible}>
|
||||
<AddPermissions permissions={permissions} backendSrv={backendSrv} />
|
||||
<AddPermissions permissions={permissions} />
|
||||
</SlideDown>
|
||||
<Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
|
||||
</div>
|
||||
|
149
public/app/containers/Teams/TeamGroupSync.tsx
Normal file
149
public/app/containers/Teams/TeamGroupSync.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { ITeam, ITeamGroup } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||
|
||||
interface Props {
|
||||
team: ITeam;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isAdding: boolean;
|
||||
newGroupId?: string;
|
||||
}
|
||||
|
||||
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
||||
|
||||
@observer
|
||||
export class TeamGroupSync extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newGroupId: '' };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.team.loadGroups();
|
||||
}
|
||||
|
||||
renderGroup(group: ITeamGroup) {
|
||||
return (
|
||||
<tr key={group.groupId}>
|
||||
<td>{group.groupId}</td>
|
||||
<td style={{ width: '1%' }}>
|
||||
<a className="btn btn-danger btn-mini" onClick={() => this.onRemoveGroup(group)}>
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
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: ITeamGroup) => {
|
||||
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();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
<h3 className="page-sub-heading">External group sync</h3>
|
||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={headerTooltip}>
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
{groups.length > 0 && (
|
||||
<button className="btn btn-success pull-right" onClick={this.onToggleAdding}>
|
||||
<i className="fa fa-plus" /> Add group
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SlideDown in={isAdding}>
|
||||
<div className="cta-form">
|
||||
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
<h5>Add External Group</h5>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-30"
|
||||
value={newGroupId}
|
||||
onChange={this.onNewGroupIdChanged}
|
||||
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<button
|
||||
className="btn btn-success gf-form-btn"
|
||||
onClick={this.onAddGroup}
|
||||
type="submit"
|
||||
disabled={!this.isNewGroupValid()}
|
||||
>
|
||||
Add group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideDown>
|
||||
|
||||
{groups.length === 0 &&
|
||||
!isAdding && (
|
||||
<div className="empty-list-cta">
|
||||
<div className="empty-list-cta__title">There are no external groups to sync with</div>
|
||||
<button onClick={this.onToggleAdding} className="empty-list-cta__button btn btn-xlarge btn-success">
|
||||
<i className="gicon gicon-add-team" />
|
||||
Add Group
|
||||
</button>
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
<i className="fa fa-rocket" /> {headerTooltip}
|
||||
<a className="text-link empty-list-cta__pro-tip-link" href="asd" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groups.length > 0 && (
|
||||
<div className="admin-list-table">
|
||||
<table className="filter-table filter-table--hover form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>External Group ID</th>
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{groups.map(group => this.renderGroup(group))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamGroupSync);
|
125
public/app/containers/Teams/TeamList.tsx
Normal file
125
public/app/containers/Teams/TeamList.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
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, ITeam } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
interface Props {
|
||||
nav: typeof NavStore.Type;
|
||||
teams: typeof TeamsStore.Type;
|
||||
backendSrv: BackendSrv;
|
||||
}
|
||||
|
||||
@inject('nav', 'teams')
|
||||
@observer
|
||||
export class TeamList extends React.Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.props.nav.load('cfg', 'teams');
|
||||
this.fetchTeams();
|
||||
}
|
||||
|
||||
fetchTeams() {
|
||||
this.props.teams.loadTeams();
|
||||
}
|
||||
|
||||
deleteTeam(team: ITeam) {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete Team ' + team.name + '?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.deleteTeamConfirmed(team);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteTeamConfirmed(team) {
|
||||
this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.teams.setSearchQuery(evt.target.value);
|
||||
};
|
||||
|
||||
renderTeamMember(team: ITeam): JSX.Element {
|
||||
let teamUrl = `org/teams/edit/${team.id}`;
|
||||
|
||||
return (
|
||||
<tr key={team.id}>
|
||||
<td className="width-4 text-center link-td">
|
||||
<a href={teamUrl}>
|
||||
<img className="filter-table__avatar" src={team.avatarUrl} />
|
||||
</a>
|
||||
</td>
|
||||
<td className="link-td">
|
||||
<a href={teamUrl}>{team.name}</a>
|
||||
</td>
|
||||
<td className="link-td">
|
||||
<a href={teamUrl}>{team.email}</a>
|
||||
</td>
|
||||
<td className="link-td">
|
||||
<a href={teamUrl}>{team.memberCount}</a>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<a onClick={() => this.deleteTeam(team)} className="btn btn-danger btn-small">
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nav, teams } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={nav as any} />
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search teams"
|
||||
value={teams.search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<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={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamList);
|
144
public/app/containers/Teams/TeamMembers.tsx
Normal file
144
public/app/containers/Teams/TeamMembers.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||
|
||||
interface Props {
|
||||
team: ITeam;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isAdding: boolean;
|
||||
newTeamMember?: User;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TeamMembers extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newTeamMember: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.team.loadMembers();
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.team.setSearchQuery(evt.target.value);
|
||||
};
|
||||
|
||||
removeMember(member: ITeamMember) {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Remove Member',
|
||||
text: 'Are you sure you want to remove ' + member.login + ' from this group?',
|
||||
yesText: 'Remove',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.removeMemberConfirmed(member);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
removeMemberConfirmed(member: ITeamMember) {
|
||||
this.props.team.removeMember(member);
|
||||
}
|
||||
|
||||
renderMember(member: ITeamMember) {
|
||||
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 style={{ width: '1%' }}>
|
||||
<a onClick={() => this.removeMember(member)} className="btn btn-danger btn-mini">
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
this.setState({ isAdding: !this.state.isAdding });
|
||||
};
|
||||
|
||||
onUserSelected = (user: User) => {
|
||||
this.setState({ newTeamMember: user });
|
||||
};
|
||||
|
||||
onAddUserToTeam = async () => {
|
||||
await this.props.team.addMember(this.state.newTeamMember.id);
|
||||
await this.props.team.loadMembers();
|
||||
this.setState({ newTeamMember: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { newTeamMember, isAdding } = this.state;
|
||||
const members = this.props.team.members.values();
|
||||
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search members"
|
||||
value={''}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
|
||||
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
||||
<i className="fa fa-plus" /> Add a member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SlideDown in={isAdding}>
|
||||
<div className="cta-form">
|
||||
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
<h5>Add Team Member</h5>
|
||||
<div className="gf-form-inline">
|
||||
<UserPicker onSelected={this.onUserSelected} className="width-30" value={newTeamMemberValue} />
|
||||
|
||||
{this.state.newTeamMember && (
|
||||
<button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
|
||||
Add to team
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SlideDown>
|
||||
|
||||
<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={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{members.map(member => this.renderMember(member))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(TeamMembers);
|
77
public/app/containers/Teams/TeamPages.tsx
Normal file
77
public/app/containers/Teams/TeamPages.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
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, ITeam } 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(): ITeam {
|
||||
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);
|
69
public/app/containers/Teams/TeamSettings.tsx
Normal file
69
public/app/containers/Teams/TeamSettings.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { ITeam } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import { Label } from 'app/core/components/Forms/Forms';
|
||||
|
||||
interface Props {
|
||||
team: ITeam;
|
||||
}
|
||||
|
||||
@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);
|
@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||
import LoginBackground from './components/Login/LoginBackground';
|
||||
import { SearchResult } from './components/search/SearchResult';
|
||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import UserPicker from './components/Picker/UserPicker';
|
||||
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
@ -19,6 +18,5 @@ export function registerAngularDirectives() {
|
||||
['onSelect', { watchDepth: 'reference' }],
|
||||
['tagOptions', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
|
||||
}
|
||||
|
21
public/app/core/components/Forms/Forms.tsx
Normal file
21
public/app/core/components/Forms/Forms.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, { SFC, ReactNode } from 'react';
|
||||
import Tooltip from '../Tooltip/Tooltip';
|
||||
|
||||
interface Props {
|
||||
tooltip?: string;
|
||||
for?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Label: SFC<Props> = props => {
|
||||
return (
|
||||
<span className="gf-form-label width-10">
|
||||
<span>{props.children}</span>
|
||||
{props.tooltip && (
|
||||
<Tooltip className="gf-form-help-icon--right-normal" placement="auto" content="hello">
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
@ -1,32 +1,32 @@
|
||||
import React from 'react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import AddPermissions from './AddPermissions';
|
||||
import { RootStore } from 'app/stores/RootStore/RootStore';
|
||||
import { backendSrv } from 'test/mocks/common';
|
||||
import { shallow } from 'enzyme';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
jest.mock('app/core/services/backend_srv', () => ({
|
||||
getBackendSrv: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return Promise.resolve([
|
||||
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
|
||||
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
||||
]);
|
||||
},
|
||||
post: jest.fn(() => Promise.resolve({})),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AddPermissions', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let instance;
|
||||
let backendSrv: any = getBackendSrv();
|
||||
|
||||
beforeAll(() => {
|
||||
backendSrv.get.mockReturnValue(
|
||||
Promise.resolve([
|
||||
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
|
||||
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
||||
])
|
||||
);
|
||||
|
||||
backendSrv.post = jest.fn(() => Promise.resolve({}));
|
||||
|
||||
store = RootStore.create(
|
||||
{},
|
||||
{
|
||||
backendSrv: backendSrv,
|
||||
}
|
||||
);
|
||||
|
||||
wrapper = shallow(<AddPermissions permissions={store.permissions} backendSrv={backendSrv} />);
|
||||
store = RootStore.create({}, { backendSrv: backendSrv });
|
||||
wrapper = shallow(<AddPermissions permissions={store.permissions} />);
|
||||
instance = wrapper.instance();
|
||||
return store.permissions.load(1, true, false);
|
||||
});
|
||||
@ -43,8 +43,8 @@ describe('AddPermissions', () => {
|
||||
login: 'user2',
|
||||
};
|
||||
|
||||
instance.typeChanged(evt);
|
||||
instance.userPicked(userItem);
|
||||
instance.onTypeChanged(evt);
|
||||
instance.onUserSelected(userItem);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
@ -70,8 +70,8 @@ describe('AddPermissions', () => {
|
||||
name: 'ug1',
|
||||
};
|
||||
|
||||
instance.typeChanged(evt);
|
||||
instance.teamPicked(teamItem);
|
||||
instance.onTypeChanged(evt);
|
||||
instance.onTeamSelected(teamItem);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
|
@ -1,24 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore';
|
||||
import UserPicker, { User } from 'app/core/components/Picker/UserPicker';
|
||||
import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker';
|
||||
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||
import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
|
||||
import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
|
||||
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
|
||||
|
||||
export interface IProps {
|
||||
export interface Props {
|
||||
permissions: any;
|
||||
backendSrv: any;
|
||||
}
|
||||
|
||||
@observer
|
||||
class AddPermissions extends Component<IProps, any> {
|
||||
class AddPermissions extends Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.userPicked = this.userPicked.bind(this);
|
||||
this.teamPicked = this.teamPicked.bind(this);
|
||||
this.permissionPicked = this.permissionPicked.bind(this);
|
||||
this.typeChanged = this.typeChanged.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@ -26,49 +21,49 @@ class AddPermissions extends Component<IProps, any> {
|
||||
permissions.resetNewType();
|
||||
}
|
||||
|
||||
typeChanged(evt) {
|
||||
onTypeChanged = evt => {
|
||||
const { value } = evt.target;
|
||||
const { permissions } = this.props;
|
||||
|
||||
permissions.setNewType(value);
|
||||
}
|
||||
};
|
||||
|
||||
userPicked(user: User) {
|
||||
onUserSelected = (user: User) => {
|
||||
const { permissions } = this.props;
|
||||
if (!user) {
|
||||
permissions.newItem.setUser(null, null);
|
||||
return;
|
||||
}
|
||||
return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
|
||||
}
|
||||
};
|
||||
|
||||
teamPicked(team: Team) {
|
||||
onTeamSelected = (team: Team) => {
|
||||
const { permissions } = this.props;
|
||||
if (!team) {
|
||||
permissions.newItem.setTeam(null, null);
|
||||
return;
|
||||
}
|
||||
return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
|
||||
}
|
||||
};
|
||||
|
||||
permissionPicked(permission: OptionWithDescription) {
|
||||
onPermissionChanged = (permission: OptionWithDescription) => {
|
||||
const { permissions } = this.props;
|
||||
return permissions.newItem.setPermission(permission.value);
|
||||
}
|
||||
};
|
||||
|
||||
resetNewType() {
|
||||
const { permissions } = this.props;
|
||||
return permissions.resetNewType();
|
||||
}
|
||||
|
||||
handleSubmit(evt) {
|
||||
onSubmit = evt => {
|
||||
evt.preventDefault();
|
||||
const { permissions } = this.props;
|
||||
permissions.addStoreItem();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { permissions, backendSrv } = this.props;
|
||||
const { permissions } = this.props;
|
||||
const newItem = permissions.newItem;
|
||||
const pickerClassName = 'width-20';
|
||||
|
||||
@ -79,12 +74,12 @@ class AddPermissions extends Component<IProps, any> {
|
||||
<button className="cta-form__close btn btn-transparent" onClick={permissions.hideAddPermissions}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
<form name="addPermission" onSubmit={this.handleSubmit}>
|
||||
<h6>Add Permission For</h6>
|
||||
<form name="addPermission" onSubmit={this.onSubmit}>
|
||||
<h5>Add Permission For</h5>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-select-wrapper">
|
||||
<select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.typeChanged}>
|
||||
<select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.onTypeChanged}>
|
||||
{aclTypes.map((option, idx) => {
|
||||
return (
|
||||
<option key={idx} value={option.value}>
|
||||
@ -98,30 +93,20 @@ class AddPermissions extends Component<IProps, any> {
|
||||
|
||||
{newItem.type === 'User' ? (
|
||||
<div className="gf-form">
|
||||
<UserPicker
|
||||
backendSrv={backendSrv}
|
||||
handlePicked={this.userPicked}
|
||||
value={newItem.userId}
|
||||
className={pickerClassName}
|
||||
/>
|
||||
<UserPicker onSelected={this.onUserSelected} value={newItem.userId} className={pickerClassName} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{newItem.type === 'Group' ? (
|
||||
<div className="gf-form">
|
||||
<TeamPicker
|
||||
backendSrv={backendSrv}
|
||||
handlePicked={this.teamPicked}
|
||||
value={newItem.teamId}
|
||||
className={pickerClassName}
|
||||
/>
|
||||
<TeamPicker onSelected={this.onTeamSelected} value={newItem.teamId} className={pickerClassName} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="gf-form">
|
||||
<DescriptionPicker
|
||||
optionsWithDesc={permissionOptions}
|
||||
handlePicked={this.permissionPicked}
|
||||
onSelected={this.onPermissionChanged}
|
||||
value={newItem.permission}
|
||||
disabled={false}
|
||||
className={'gf-form-input--form-dropdown-right'}
|
||||
|
@ -8,13 +8,14 @@ import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { FolderInfo } from './FolderInfo';
|
||||
|
||||
export interface IProps {
|
||||
export interface Props {
|
||||
dashboardId: number;
|
||||
folder?: FolderInfo;
|
||||
backendSrv: any;
|
||||
}
|
||||
|
||||
@observer
|
||||
class DashboardPermissions extends Component<IProps, any> {
|
||||
class DashboardPermissions extends Component<Props, any> {
|
||||
permissions: any;
|
||||
|
||||
constructor(props) {
|
||||
@ -53,7 +54,7 @@ class DashboardPermissions extends Component<IProps, any> {
|
||||
</div>
|
||||
</div>
|
||||
<SlideDown in={this.permissions.isAddPermissionsVisible}>
|
||||
<AddPermissions permissions={this.permissions} backendSrv={backendSrv} />
|
||||
<AddPermissions permissions={this.permissions} />
|
||||
</SlideDown>
|
||||
<Permissions
|
||||
permissions={this.permissions}
|
||||
|
@ -25,7 +25,7 @@ export default class DisabledPermissionListItem extends Component<IProps, any> {
|
||||
<div className="gf-form">
|
||||
<DescriptionPicker
|
||||
optionsWithDesc={permissionOptions}
|
||||
handlePicked={() => {}}
|
||||
onSelected={() => {}}
|
||||
value={item.permission}
|
||||
disabled={true}
|
||||
className={'gf-form-input--form-dropdown-right'}
|
||||
|
@ -68,7 +68,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
|
||||
<div className="gf-form">
|
||||
<DescriptionPicker
|
||||
optionsWithDesc={permissionOptions}
|
||||
handlePicked={handleChangePermission}
|
||||
onSelected={handleChangePermission}
|
||||
value={item.permission}
|
||||
disabled={item.inherited}
|
||||
className={'gf-form-input--form-dropdown-right'}
|
||||
|
@ -2,9 +2,9 @@ import React, { Component } from 'react';
|
||||
import Select from 'react-select';
|
||||
import DescriptionOption from './DescriptionOption';
|
||||
|
||||
export interface IProps {
|
||||
export interface Props {
|
||||
optionsWithDesc: OptionWithDescription[];
|
||||
handlePicked: (permission) => void;
|
||||
onSelected: (permission) => void;
|
||||
value: number;
|
||||
disabled: boolean;
|
||||
className?: string;
|
||||
@ -16,14 +16,14 @@ export interface OptionWithDescription {
|
||||
description: string;
|
||||
}
|
||||
|
||||
class DescriptionPicker extends Component<IProps, any> {
|
||||
class DescriptionPicker extends Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { optionsWithDesc, handlePicked, value, disabled, className } = this.props;
|
||||
const { optionsWithDesc, onSelected, value, disabled, className } = this.props;
|
||||
|
||||
return (
|
||||
<div className="permissions-picker">
|
||||
@ -34,7 +34,7 @@ class DescriptionPicker extends Component<IProps, any> {
|
||||
clearable={false}
|
||||
labelKey="label"
|
||||
options={optionsWithDesc}
|
||||
onChange={handlePicked}
|
||||
onChange={onSelected}
|
||||
className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||
optionComponent={DescriptionOption}
|
||||
placeholder="Choose"
|
||||
|
@ -1,19 +1,23 @@
|
||||
import React from 'react';
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import TeamPicker from './TeamPicker';
|
||||
import { TeamPicker } from './TeamPicker';
|
||||
|
||||
const model = {
|
||||
backendSrv: {
|
||||
get: () => {
|
||||
return new Promise((resolve, reject) => {});
|
||||
},
|
||||
jest.mock('app/core/services/backend_srv', () => ({
|
||||
getBackendSrv: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
};
|
||||
},
|
||||
handlePicked: () => {},
|
||||
};
|
||||
}));
|
||||
|
||||
describe('TeamPicker', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<TeamPicker {...model} />).toJSON();
|
||||
const props = {
|
||||
onSelected: () => {},
|
||||
};
|
||||
const tree = renderer.create(<TeamPicker {...props} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -1,18 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import Select from 'react-select';
|
||||
import PickerOption from './PickerOption';
|
||||
import withPicker from './withPicker';
|
||||
import { debounce } from 'lodash';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
export interface IProps {
|
||||
backendSrv: any;
|
||||
isLoading: boolean;
|
||||
toggleLoading: any;
|
||||
handlePicked: (user) => void;
|
||||
export interface Props {
|
||||
onSelected: (team: Team) => void;
|
||||
value?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isLoading;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: number;
|
||||
label: string;
|
||||
@ -20,13 +21,12 @@ export interface Team {
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
class TeamPicker extends Component<IProps, any> {
|
||||
export class TeamPicker extends Component<Props, State> {
|
||||
debouncedSearch: any;
|
||||
backendSrv: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = { isLoading: false };
|
||||
this.search = this.search.bind(this);
|
||||
|
||||
this.debouncedSearch = debounce(this.search, 300, {
|
||||
@ -36,9 +36,9 @@ class TeamPicker extends Component<IProps, any> {
|
||||
}
|
||||
|
||||
search(query?: string) {
|
||||
const { toggleLoading, backendSrv } = this.props;
|
||||
const backendSrv = getBackendSrv();
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
toggleLoading(true);
|
||||
return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
|
||||
const teams = result.teams.map(team => {
|
||||
return {
|
||||
@ -49,18 +49,18 @@ class TeamPicker extends Component<IProps, any> {
|
||||
};
|
||||
});
|
||||
|
||||
toggleLoading(false);
|
||||
this.setState({ isLoading: false });
|
||||
return { options: teams };
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async;
|
||||
const { isLoading, handlePicked, value, className } = this.props;
|
||||
const { onSelected, value, className } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="user-picker">
|
||||
<AsyncComponent
|
||||
<Select.Async
|
||||
valueKey="id"
|
||||
multi={false}
|
||||
labelKey="label"
|
||||
@ -69,10 +69,10 @@ class TeamPicker extends Component<IProps, any> {
|
||||
loadOptions={this.debouncedSearch}
|
||||
loadingPlaceholder="Loading..."
|
||||
noResultsText="No teams found"
|
||||
onChange={handlePicked}
|
||||
onChange={onSelected}
|
||||
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||
optionComponent={PickerOption}
|
||||
placeholder="Choose"
|
||||
placeholder="Select a team"
|
||||
value={value}
|
||||
autosize={true}
|
||||
/>
|
||||
@ -80,5 +80,3 @@ class TeamPicker extends Component<IProps, any> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withPicker(TeamPicker);
|
||||
|
@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import UserPicker from './UserPicker';
|
||||
import { UserPicker } from './UserPicker';
|
||||
|
||||
const model = {
|
||||
backendSrv: {
|
||||
get: () => {
|
||||
return new Promise((resolve, reject) => {});
|
||||
},
|
||||
jest.mock('app/core/services/backend_srv', () => ({
|
||||
getBackendSrv: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
};
|
||||
},
|
||||
handlePicked: () => {},
|
||||
};
|
||||
}));
|
||||
|
||||
describe('UserPicker', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<UserPicker {...model} />).toJSON();
|
||||
const tree = renderer.create(<UserPicker onSelected={() => {}} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -1,18 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
import Select from 'react-select';
|
||||
import PickerOption from './PickerOption';
|
||||
import withPicker from './withPicker';
|
||||
import { debounce } from 'lodash';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
export interface IProps {
|
||||
backendSrv: any;
|
||||
isLoading: boolean;
|
||||
toggleLoading: any;
|
||||
handlePicked: (user) => void;
|
||||
export interface Props {
|
||||
onSelected: (user: User) => void;
|
||||
value?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
label: string;
|
||||
@ -20,13 +21,12 @@ export interface User {
|
||||
login: string;
|
||||
}
|
||||
|
||||
class UserPicker extends Component<IProps, any> {
|
||||
export class UserPicker extends Component<Props, State> {
|
||||
debouncedSearch: any;
|
||||
backendSrv: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = { isLoading: false };
|
||||
this.search = this.search.bind(this);
|
||||
|
||||
this.debouncedSearch = debounce(this.search, 300, {
|
||||
@ -36,29 +36,34 @@ class UserPicker extends Component<IProps, any> {
|
||||
}
|
||||
|
||||
search(query?: string) {
|
||||
const { toggleLoading, backendSrv } = this.props;
|
||||
const backendSrv = getBackendSrv();
|
||||
|
||||
toggleLoading(true);
|
||||
return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => {
|
||||
const users = result.map(user => {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
return backendSrv
|
||||
.get(`/api/org/users?query=${query}&limit=10`)
|
||||
.then(result => {
|
||||
return {
|
||||
id: user.userId,
|
||||
label: `${user.login} - ${user.email}`,
|
||||
avatarUrl: user.avatarUrl,
|
||||
login: user.login,
|
||||
options: result.map(user => ({
|
||||
id: user.userId,
|
||||
label: `${user.login} - ${user.email}`,
|
||||
avatarUrl: user.avatarUrl,
|
||||
login: user.login,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
toggleLoading(false);
|
||||
return { options: users };
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async;
|
||||
const { isLoading, handlePicked, value, className } = this.props;
|
||||
const { value, className } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="user-picker">
|
||||
<AsyncComponent
|
||||
<Select.Async
|
||||
valueKey="id"
|
||||
multi={false}
|
||||
labelKey="label"
|
||||
@ -67,10 +72,10 @@ class UserPicker extends Component<IProps, any> {
|
||||
loadOptions={this.debouncedSearch}
|
||||
loadingPlaceholder="Loading..."
|
||||
noResultsText="No users found"
|
||||
onChange={handlePicked}
|
||||
onChange={this.props.onSelected}
|
||||
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||
optionComponent={PickerOption}
|
||||
placeholder="Choose"
|
||||
placeholder="Select user"
|
||||
value={value}
|
||||
autosize={true}
|
||||
/>
|
||||
@ -78,5 +83,3 @@ class UserPicker extends Component<IProps, any> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withPicker(UserPicker);
|
||||
|
@ -1,34 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export interface IProps {
|
||||
backendSrv: any;
|
||||
handlePicked: (data) => void;
|
||||
value?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function withPicker(WrappedComponent) {
|
||||
return class WithPicker extends Component<IProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.toggleLoading = this.toggleLoading.bind(this);
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleLoading(isLoading) {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
isLoading: isLoading,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WrappedComponent toggleLoading={this.toggleLoading} isLoading={this.state.isLoading} {...this.props} />;
|
||||
}
|
||||
};
|
||||
}
|
@ -8,7 +8,7 @@ import appEvents from 'app/core/app_events';
|
||||
import Drop from 'tether-drop';
|
||||
import { createStore } from 'app/stores/store';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { AngularLoader, setAngularLoader } from 'app/core/services/angular_loader';
|
||||
|
||||
@ -28,6 +28,7 @@ export class GrafanaCtrl {
|
||||
) {
|
||||
// make angular loader service available to react components
|
||||
setAngularLoader(angularLoader);
|
||||
setBackendSrv(backendSrv);
|
||||
// create store with env services
|
||||
createStore({ backendSrv, datasourceSrv });
|
||||
|
||||
|
@ -1,64 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
|
||||
const template = `
|
||||
<div class="dropdown">
|
||||
<gf-form-dropdown model="ctrl.group"
|
||||
get-options="ctrl.debouncedSearchGroups($query)"
|
||||
css-class="gf-size-auto"
|
||||
on-change="ctrl.onChange($option)"
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
`;
|
||||
export class TeamPickerCtrl {
|
||||
group: any;
|
||||
teamPicked: any;
|
||||
debouncedSearchGroups: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv) {
|
||||
this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
});
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.group = { text: 'Choose', value: null };
|
||||
}
|
||||
|
||||
searchGroups(query: string) {
|
||||
return Promise.resolve(
|
||||
this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => {
|
||||
return _.map(result.teams, ug => {
|
||||
return { text: ug.name, value: ug };
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onChange(option) {
|
||||
this.teamPicked({ $group: option.value });
|
||||
}
|
||||
}
|
||||
|
||||
export function teamPicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller: TeamPickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
teamPicked: '&',
|
||||
},
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
scope.$on('team-picker-reset', () => {
|
||||
ctrl.reset();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('teamPicker', teamPicker);
|
@ -1,71 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
|
||||
const template = `
|
||||
<div class="dropdown">
|
||||
<gf-form-dropdown model="ctrl.user"
|
||||
get-options="ctrl.debouncedSearchUsers($query)"
|
||||
css-class="gf-size-auto"
|
||||
on-change="ctrl.onChange($option)"
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
`;
|
||||
export class UserPickerCtrl {
|
||||
user: any;
|
||||
debouncedSearchUsers: any;
|
||||
userPicked: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv) {
|
||||
this.reset();
|
||||
this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
});
|
||||
}
|
||||
|
||||
searchUsers(query: string) {
|
||||
return Promise.resolve(
|
||||
this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => {
|
||||
return _.map(result.users, user => {
|
||||
return { text: user.login + ' - ' + user.email, value: user };
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onChange(option) {
|
||||
this.userPicked({ $user: option.value });
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.user = { text: 'Choose', value: null };
|
||||
}
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
login: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function userPicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller: UserPickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
userPicked: '&',
|
||||
},
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
scope.$on('user-picker-reset', () => {
|
||||
ctrl.reset();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('userPicker', userPicker);
|
@ -44,8 +44,6 @@ import { KeybindingSrv } from './services/keybindingSrv';
|
||||
import { helpModal } from './components/help/help';
|
||||
import { JsonExplorer } from './components/json_explorer/json_explorer';
|
||||
import { NavModelSrv, NavModel } from './nav_model_srv';
|
||||
import { userPicker } from './components/user_picker';
|
||||
import { teamPicker } from './components/team_picker';
|
||||
import { geminiScrollbar } from './components/scroll/scroll';
|
||||
import { pageScrollbar } from './components/scroll/page_scroll';
|
||||
import { gfPageDirective } from './components/gf_page';
|
||||
@ -83,8 +81,6 @@ export {
|
||||
JsonExplorer,
|
||||
NavModelSrv,
|
||||
NavModel,
|
||||
userPicker,
|
||||
teamPicker,
|
||||
geminiScrollbar,
|
||||
pageScrollbar,
|
||||
gfPageDirective,
|
||||
|
@ -368,3 +368,17 @@ export class BackendSrv {
|
||||
}
|
||||
|
||||
coreModule.service('backendSrv', BackendSrv);
|
||||
|
||||
//
|
||||
// Code below is to expore the service to react components
|
||||
//
|
||||
|
||||
let singletonInstance: BackendSrv;
|
||||
|
||||
export function setBackendSrv(instance: BackendSrv) {
|
||||
singletonInstance = instance;
|
||||
}
|
||||
|
||||
export function getBackendSrv(): BackendSrv {
|
||||
return singletonInstance;
|
||||
}
|
||||
|
@ -957,7 +957,7 @@ kbn.getUnitFormats = function() {
|
||||
text: 'throughput',
|
||||
submenu: [
|
||||
{ text: 'ops/sec (ops)', value: 'ops' },
|
||||
{ text: 'requets/sec (rps)', value: 'reqps' },
|
||||
{ text: 'requests/sec (rps)', value: 'reqps' },
|
||||
{ text: 'reads/sec (rps)', value: 'rps' },
|
||||
{ text: 'writes/sec (wps)', value: 'wps' },
|
||||
{ text: 'I/O ops/sec (iops)', value: 'iops' },
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<a class="navbar-page-btn" ng-click="ctrl.showSearch()">
|
||||
<i class="gicon gicon-dashboard"></i>
|
||||
{{ctrl.dashboard.title}}
|
||||
<span ng-if="ctrl.dashboard.meta.folderId > 0" class="navbar-page-btn--folder">{{ctrl.dashboard.meta.folderTitle}} / </span>{{ctrl.dashboard.title}}
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -104,10 +104,7 @@ export class FolderPickerCtrl {
|
||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||
|
||||
this.closeCreateFolder();
|
||||
this.folder = {
|
||||
text: result.title,
|
||||
value: result.id,
|
||||
};
|
||||
this.folder = { text: result.title, value: result.id };
|
||||
this.onFolderChange(this.folder);
|
||||
});
|
||||
}
|
||||
@ -149,17 +146,14 @@ export class FolderPickerCtrl {
|
||||
folder = result.length > 0 ? result[0] : resetFolder;
|
||||
}
|
||||
}
|
||||
this.folder = folder;
|
||||
this.onFolderLoad();
|
||||
});
|
||||
}
|
||||
|
||||
private onFolderLoad() {
|
||||
if (this.onLoad) {
|
||||
this.onLoad({
|
||||
$folder: { id: this.folder.value, title: this.folder.text },
|
||||
});
|
||||
}
|
||||
this.folder = folder;
|
||||
|
||||
// if this is not the same as our initial value notify parent
|
||||
if (this.folder.id !== this.initialFolderId) {
|
||||
this.onChange({ $folder: { id: this.folder.value, title: this.folder.text } });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +170,6 @@ export function folderPicker() {
|
||||
labelClass: '@',
|
||||
rootName: '@',
|
||||
onChange: '&',
|
||||
onLoad: '&',
|
||||
onCreateFolder: '&',
|
||||
enterFolderCreation: '&',
|
||||
exitFolderCreation: '&',
|
||||
|
@ -5,8 +5,6 @@ import './select_org_ctrl';
|
||||
import './change_password_ctrl';
|
||||
import './new_org_ctrl';
|
||||
import './user_invite_ctrl';
|
||||
import './teams_ctrl';
|
||||
import './team_details_ctrl';
|
||||
import './create_team_ctrl';
|
||||
import './org_api_keys_ctrl';
|
||||
import './org_details_ctrl';
|
||||
|
@ -1,105 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<h3 class="page-sub-heading">Team Details</h3>
|
||||
|
||||
<form name="teamDetailsForm" class="gf-form-group">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Name</span>
|
||||
<input type="text" required ng-model="ctrl.team.name" class="gf-form-input max-width-22">
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">
|
||||
Email
|
||||
<info-popover mode="right-normal">
|
||||
This is optional and is primarily used for allowing custom team avatars.
|
||||
</info-popover>
|
||||
</span>
|
||||
<input class="gf-form-input max-width-22" type="email" ng-model="ctrl.team.email" placeholder="email@test.com">
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="gf-form-group">
|
||||
|
||||
<h3 class="page-heading">Team Members</h3>
|
||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Add member</span>
|
||||
<!--
|
||||
Old picker
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
-->
|
||||
<select-user-picker class="width-7" handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="filter-table" ng-show="ctrl.teamMembers.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr ng-repeat="member in ctrl.teamMembers">
|
||||
<td class="width-4 text-center link-td">
|
||||
<img class="filter-table__avatar" ng-src="{{member.avatarUrl}}"></img>
|
||||
</td>
|
||||
<td>{{member.login}}</td>
|
||||
<td>{{member.email}}</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.removeTeamMember(member)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div>
|
||||
<em class="muted" ng-hide="ctrl.teamMembers.length > 0">
|
||||
This team has no members yet.
|
||||
</em>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.isMappingsEnabled">
|
||||
|
||||
<h3 class="page-heading">Mappings to external groups</h3>
|
||||
<form name="ctrl.addGroupForm" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Add group</span>
|
||||
<input class="gf-form-input max-width-22" type="text" ng-model="ctrl.newGroupId">
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.addGroup()">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="filter-table" ng-show="ctrl.teamGroups.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr ng-repeat="group in ctrl.teamGroups">
|
||||
<td>{{group.groupId}}</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.removeGroup(group)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div>
|
||||
<em class="muted" ng-hide="ctrl.teamGroups.length > 0">
|
||||
This team has no associated groups yet.
|
||||
</em>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -1,68 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<div class="page-action-bar">
|
||||
<label class="gf-form gf-form--grow gf-form--has-input-icon">
|
||||
<input type="text" class="gf-form-input max-width-20" placeholder="Find Team by name" tabindex="1" ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.get()" />
|
||||
<i class="gf-form-input-icon fa fa-search"></i>
|
||||
</label>
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
|
||||
<a class="btn btn-success" href="org/teams/new">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add Team
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-list-table">
|
||||
<table class="filter-table filter-table--hover form-inline" ng-show="ctrl.teams.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Members</th>
|
||||
<th style="width: 1%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="team in ctrl.teams">
|
||||
<td class="width-4 text-center link-td">
|
||||
<a href="org/teams/edit/{{team.id}}">
|
||||
<img class="filter-table__avatar" ng-src="{{team.avatarUrl}}"></img>
|
||||
</a>
|
||||
</td>
|
||||
<td class="link-td">
|
||||
<a href="org/teams/edit/{{team.id}}">{{team.name}}</a>
|
||||
</td>
|
||||
<td class="link-td">
|
||||
<a href="org/teams/edit/{{team.id}}">{{team.email}}</a>
|
||||
</td>
|
||||
<td class="link-td">
|
||||
<a href="org/teams/edit/{{team.id}}">{{team.memberCount}}</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a ng-click="ctrl.deleteTeam(team)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-list-paging" ng-if="ctrl.showPaging">
|
||||
<ol>
|
||||
<li ng-repeat="page in ctrl.pages">
|
||||
<button
|
||||
class="btn btn-small"
|
||||
ng-class="{'btn-secondary': page.current, 'btn-inverse': !page.current}"
|
||||
ng-click="ctrl.navigateToPage(page)">{{page.page}}</button>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<em class="muted" ng-hide="ctrl.teams.length > 0">
|
||||
No Teams found.
|
||||
</em>
|
||||
</div>
|
@ -1,42 +0,0 @@
|
||||
import '../team_details_ctrl';
|
||||
import TeamDetailsCtrl from '../team_details_ctrl';
|
||||
|
||||
describe('TeamDetailsCtrl', () => {
|
||||
var backendSrv = {
|
||||
searchUsers: jest.fn(() => Promise.resolve([])),
|
||||
get: jest.fn(() => Promise.resolve([])),
|
||||
post: jest.fn(() => Promise.resolve([])),
|
||||
};
|
||||
|
||||
//Team id
|
||||
var routeParams = {
|
||||
id: 1,
|
||||
};
|
||||
|
||||
var navModelSrv = {
|
||||
getNav: jest.fn(),
|
||||
};
|
||||
|
||||
var teamDetailsCtrl = new TeamDetailsCtrl({ $broadcast: jest.fn() }, backendSrv, routeParams, navModelSrv);
|
||||
|
||||
describe('when user is chosen to be added to team', () => {
|
||||
beforeEach(() => {
|
||||
teamDetailsCtrl = new TeamDetailsCtrl({ $broadcast: jest.fn() }, backendSrv, routeParams, navModelSrv);
|
||||
const userItem = {
|
||||
id: 2,
|
||||
login: 'user2',
|
||||
};
|
||||
teamDetailsCtrl.userPicked(userItem);
|
||||
});
|
||||
|
||||
it('should parse the result and save to db', () => {
|
||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/teams/1/members');
|
||||
expect(backendSrv.post.mock.calls[0][1].userId).toBe(2);
|
||||
});
|
||||
|
||||
it('should refresh the list after saving.', () => {
|
||||
expect(backendSrv.get.mock.calls[0][0]).toBe('/api/teams/1');
|
||||
expect(backendSrv.get.mock.calls[1][0]).toBe('/api/teams/1/members');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,108 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export default class TeamDetailsCtrl {
|
||||
team: Team;
|
||||
teamMembers: User[] = [];
|
||||
navModel: any;
|
||||
teamGroups: TeamGroup[] = [];
|
||||
newGroupId: string;
|
||||
isMappingsEnabled: boolean;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
|
||||
this.userPicked = this.userPicked.bind(this);
|
||||
this.get = this.get.bind(this);
|
||||
this.newGroupId = '';
|
||||
this.isMappingsEnabled = config.buildInfo.isEnterprise;
|
||||
this.get();
|
||||
}
|
||||
|
||||
get() {
|
||||
if (this.$routeParams && this.$routeParams.id) {
|
||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => {
|
||||
this.team = result;
|
||||
});
|
||||
|
||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`).then(result => {
|
||||
this.teamMembers = result;
|
||||
});
|
||||
|
||||
if (this.isMappingsEnabled) {
|
||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}/groups`).then(result => {
|
||||
this.teamGroups = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeTeamMember(teamMember: TeamMember) {
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Remove Member',
|
||||
text: 'Are you sure you want to remove ' + teamMember.login + ' from this group?',
|
||||
yesText: 'Remove',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.removeMemberConfirmed(teamMember);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
removeMemberConfirmed(teamMember: TeamMember) {
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.$scope.teamDetailsForm.$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.backendSrv.put('/api/teams/' + this.team.id, {
|
||||
name: this.team.name,
|
||||
email: this.team.email,
|
||||
});
|
||||
}
|
||||
|
||||
userPicked(user) {
|
||||
this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }).then(() => {
|
||||
this.$scope.$broadcast('user-picker-reset');
|
||||
this.get();
|
||||
});
|
||||
}
|
||||
|
||||
addGroup() {
|
||||
this.backendSrv.post(`/api/teams/${this.$routeParams.id}/groups`, { groupId: this.newGroupId }).then(() => {
|
||||
this.get();
|
||||
});
|
||||
}
|
||||
|
||||
removeGroup(group: TeamGroup) {
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/groups/${group.groupId}`).then(this.get);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TeamGroup {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
login: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
userId: number;
|
||||
name: string;
|
||||
login: string;
|
||||
}
|
||||
|
||||
coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl);
|
@ -1,66 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class TeamsCtrl {
|
||||
teams: any;
|
||||
pages = [];
|
||||
perPage = 50;
|
||||
page = 1;
|
||||
totalPages: number;
|
||||
showPaging = false;
|
||||
query: any = '';
|
||||
navModel: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
|
||||
this.get();
|
||||
}
|
||||
|
||||
get() {
|
||||
this.backendSrv
|
||||
.get(`/api/teams/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
|
||||
.then(result => {
|
||||
this.teams = result.teams;
|
||||
this.page = result.page;
|
||||
this.perPage = result.perPage;
|
||||
this.totalPages = Math.ceil(result.totalCount / result.perPage);
|
||||
this.showPaging = this.totalPages > 1;
|
||||
this.pages = [];
|
||||
|
||||
for (var i = 1; i < this.totalPages + 1; i++) {
|
||||
this.pages.push({ page: i, current: i === this.page });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigateToPage(page) {
|
||||
this.page = page.page;
|
||||
this.get();
|
||||
}
|
||||
|
||||
deleteTeam(team) {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete Team ' + team.name + '?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.deleteTeamConfirmed(team);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteTeamConfirmed(team) {
|
||||
this.backendSrv.delete('/api/teams/' + team.id).then(this.get.bind(this));
|
||||
}
|
||||
|
||||
openTeamModal() {
|
||||
appEvents.emit('show-modal', {
|
||||
templateHtml: '<create-team-modal></create-team-modal>',
|
||||
modalClass: 'modal--narrow',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('TeamsCtrl', TeamsCtrl);
|
@ -218,7 +218,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
// and add built in variables interval and interval_ms
|
||||
var scopedVars = Object.assign({}, this.panel.scopedVars, {
|
||||
__interval: { text: this.interval, value: this.interval },
|
||||
__interval_ms: { text: this.intervalMs, value: this.intervalMs },
|
||||
__interval_ms: { text: String(this.intervalMs), value: String(this.intervalMs) },
|
||||
});
|
||||
|
||||
var metricsQuery = {
|
||||
|
@ -57,7 +57,7 @@ System.config({
|
||||
css: 'vendor/plugin-css/css.js',
|
||||
},
|
||||
meta: {
|
||||
'plugin*': {
|
||||
'*': {
|
||||
esModule: true,
|
||||
authorization: true,
|
||||
loader: 'plugin-loader',
|
||||
|
@ -3,6 +3,7 @@ import { Variable, assignModelProperties, variableTypes } from './variable';
|
||||
|
||||
export class AdhocVariable implements Variable {
|
||||
filters: any[];
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults = {
|
||||
type: 'adhoc',
|
||||
@ -11,6 +12,7 @@ export class AdhocVariable implements Variable {
|
||||
hide: 0,
|
||||
datasource: null,
|
||||
filters: [],
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -4,6 +4,7 @@ export class ConstantVariable implements Variable {
|
||||
query: string;
|
||||
options: any[];
|
||||
current: any;
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults = {
|
||||
type: 'constant',
|
||||
@ -13,6 +14,7 @@ export class ConstantVariable implements Variable {
|
||||
query: '',
|
||||
current: {},
|
||||
options: [],
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -7,6 +7,7 @@ export class CustomVariable implements Variable {
|
||||
includeAll: boolean;
|
||||
multi: boolean;
|
||||
current: any;
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults = {
|
||||
type: 'custom',
|
||||
@ -19,6 +20,7 @@ export class CustomVariable implements Variable {
|
||||
includeAll: false,
|
||||
multi: false,
|
||||
allValue: null,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -7,6 +7,7 @@ export class DatasourceVariable implements Variable {
|
||||
options: any;
|
||||
current: any;
|
||||
refresh: any;
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults = {
|
||||
type: 'datasource',
|
||||
@ -18,6 +19,7 @@ export class DatasourceVariable implements Variable {
|
||||
options: [],
|
||||
query: '',
|
||||
refresh: 1,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -11,6 +11,7 @@ export class IntervalVariable implements Variable {
|
||||
query: string;
|
||||
refresh: number;
|
||||
current: any;
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults = {
|
||||
type: 'interval',
|
||||
@ -24,6 +25,7 @@ export class IntervalVariable implements Variable {
|
||||
auto: false,
|
||||
auto_min: '10s',
|
||||
auto_count: 30,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -22,6 +22,7 @@ export class QueryVariable implements Variable {
|
||||
tagsQuery: string;
|
||||
tagValuesQuery: string;
|
||||
tags: any[];
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults = {
|
||||
type: 'query',
|
||||
@ -42,6 +43,7 @@ export class QueryVariable implements Variable {
|
||||
useTags: false,
|
||||
tagsQuery: '',
|
||||
tagValuesQuery: '',
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -345,6 +345,49 @@ describe('templateSrv', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl skip url sync', function() {
|
||||
beforeEach(function() {
|
||||
initTemplateSrv([
|
||||
{
|
||||
name: 'test',
|
||||
skipUrlSync: true,
|
||||
current: { value: 'value' },
|
||||
getValueForUrl: function() {
|
||||
return this.current.value;
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include template variable value in url', function() {
|
||||
var params = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params);
|
||||
expect(params['var-test']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value with skip url sync', function() {
|
||||
beforeEach(function() {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
skipUrlSync: true,
|
||||
current: { value: ['val1', 'val2'] },
|
||||
getValueForUrl: function() {
|
||||
return this.current.value;
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include template variable value in url', function() {
|
||||
var params = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params);
|
||||
expect(params['var-test']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
|
||||
beforeEach(function() {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
|
||||
@ -359,6 +402,20 @@ describe('templateSrv', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', function() {
|
||||
beforeEach(function() {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
|
||||
});
|
||||
|
||||
it('should not set scoped value as url params', function() {
|
||||
var params = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params, {
|
||||
test: { name: 'test', value: 'val1', skipUrlSync: true },
|
||||
});
|
||||
expect(params['var-test']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceWithText', function() {
|
||||
beforeEach(function() {
|
||||
initTemplateSrv([
|
||||
|
@ -250,8 +250,14 @@ export class TemplateSrv {
|
||||
fillVariableValuesForUrl(params, scopedVars) {
|
||||
_.each(this.variables, function(variable) {
|
||||
if (scopedVars && scopedVars[variable.name] !== void 0) {
|
||||
if (scopedVars[variable.name].skipUrlSync) {
|
||||
return;
|
||||
}
|
||||
params['var-' + variable.name] = scopedVars[variable.name].value;
|
||||
} else {
|
||||
if (variable.skipUrlSync) {
|
||||
return;
|
||||
}
|
||||
params['var-' + variable.name] = variable.getValueForUrl();
|
||||
}
|
||||
});
|
||||
|
@ -39,6 +39,14 @@ export default class CloudWatchDatasource {
|
||||
item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
|
||||
item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
|
||||
|
||||
// valid ExtendedStatistics is like p90.00, check the pattern
|
||||
let hasInvalidStatistics = item.statistics.some(s => {
|
||||
return s.indexOf('p') === 0 && !/p\d{2}\.\d{2}/.test(s);
|
||||
});
|
||||
if (hasInvalidStatistics) {
|
||||
throw { message: 'Invalid extended statistics' };
|
||||
}
|
||||
|
||||
return _.extend(
|
||||
{
|
||||
refId: item.refId,
|
||||
@ -404,6 +412,7 @@ export default class CloudWatchDatasource {
|
||||
}
|
||||
|
||||
expandTemplateVariable(targets, scopedVars, templateSrv) {
|
||||
// Datasource and template srv logic uber-complected. This should be cleaned up.
|
||||
return _.chain(targets)
|
||||
.map(target => {
|
||||
var dimensionKey = _.findKey(target.dimensions, v => {
|
||||
|
@ -1,32 +1,38 @@
|
||||
import '../datasource';
|
||||
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import CloudWatchDatasource from '../datasource';
|
||||
import 'app/features/dashboard/time_srv';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('CloudWatchDatasource', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var instanceSettings = {
|
||||
let instanceSettings = {
|
||||
jsonData: { defaultRegion: 'us-east-1', access: 'proxy' },
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
|
||||
beforeEach(ctx.createService('timeSrv'));
|
||||
let templateSrv = {
|
||||
data: {},
|
||||
templateSettings: { interpolate: /\[\[([\s\S]+?)\]\]/g },
|
||||
replace: text => _.template(text, templateSrv.templateSettings)(templateSrv.data),
|
||||
variableExists: () => false,
|
||||
};
|
||||
|
||||
beforeEach(
|
||||
angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
|
||||
ctx.$q = $q;
|
||||
ctx.$httpBackend = $httpBackend;
|
||||
ctx.$rootScope = $rootScope;
|
||||
ctx.ds = $injector.instantiate(CloudWatchDatasource, {
|
||||
instanceSettings: instanceSettings,
|
||||
});
|
||||
$httpBackend.when('GET', /\.html$/).respond('');
|
||||
})
|
||||
);
|
||||
let timeSrv = {
|
||||
time: { from: 'now-1h', to: 'now' },
|
||||
timeRange: () => {
|
||||
return {
|
||||
from: dateMath.parse(timeSrv.time.from, false),
|
||||
to: dateMath.parse(timeSrv.time.to, true),
|
||||
};
|
||||
},
|
||||
};
|
||||
let backendSrv = {};
|
||||
let ctx = <any>{
|
||||
backendSrv,
|
||||
templateSrv,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.ds = new CloudWatchDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv);
|
||||
});
|
||||
|
||||
describe('When performing CloudWatch query', function() {
|
||||
var requestParams;
|
||||
@ -67,24 +73,23 @@ describe('CloudWatchDatasource', function() {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(params) {
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(params => {
|
||||
requestParams = params.data;
|
||||
return ctx.$q.when({ data: response });
|
||||
};
|
||||
return Promise.resolve({ data: response });
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate the correct query', function(done) {
|
||||
ctx.ds.query(query).then(function() {
|
||||
var params = requestParams.queries[0];
|
||||
expect(params.namespace).to.be(query.targets[0].namespace);
|
||||
expect(params.metricName).to.be(query.targets[0].metricName);
|
||||
expect(params.dimensions['InstanceId']).to.be('i-12345678');
|
||||
expect(params.statistics).to.eql(query.targets[0].statistics);
|
||||
expect(params.period).to.be(query.targets[0].period);
|
||||
expect(params.namespace).toBe(query.targets[0].namespace);
|
||||
expect(params.metricName).toBe(query.targets[0].metricName);
|
||||
expect(params.dimensions['InstanceId']).toBe('i-12345678');
|
||||
expect(params.statistics).toEqual(query.targets[0].statistics);
|
||||
expect(params.period).toBe(query.targets[0].period);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should generate the correct query with interval variable', function(done) {
|
||||
@ -111,116 +116,37 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
ctx.ds.query(query).then(function() {
|
||||
var params = requestParams.queries[0];
|
||||
expect(params.period).to.be('600');
|
||||
expect(params.period).toBe('600');
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should cancel query for invalid extended statistics', function () {
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: ['pNN.NN'],
|
||||
period: '60s',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(ctx.ds.query.bind(ctx.ds, query)).toThrow(/Invalid extended statistics/);
|
||||
});
|
||||
|
||||
it('should return series list', function(done) {
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(result.data[0].target).to.be(response.results.A.series[0].name);
|
||||
expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
|
||||
expect(result.data[0].target).toBe(response.results.A.series[0].name);
|
||||
expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should generate the correct targets by expanding template variables', function() {
|
||||
var templateSrv = {
|
||||
variables: [
|
||||
{
|
||||
name: 'instance_id',
|
||||
options: [
|
||||
{ text: 'i-23456789', value: 'i-23456789', selected: false },
|
||||
{ text: 'i-34567890', value: 'i-34567890', selected: true },
|
||||
],
|
||||
current: {
|
||||
text: 'i-34567890',
|
||||
value: 'i-34567890',
|
||||
},
|
||||
},
|
||||
],
|
||||
replace: function(target, scopedVars) {
|
||||
if (target === '$instance_id' && scopedVars['instance_id']['text'] === 'i-34567890') {
|
||||
return 'i-34567890';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
getVariableName: function(e) {
|
||||
return 'instance_id';
|
||||
},
|
||||
variableExists: function(e) {
|
||||
return true;
|
||||
},
|
||||
containsVariable: function(str, variableName) {
|
||||
return str.indexOf('$' + variableName) !== -1;
|
||||
},
|
||||
};
|
||||
|
||||
var targets = [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: '$instance_id',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
period: 300,
|
||||
},
|
||||
];
|
||||
|
||||
var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv);
|
||||
expect(result[0].dimensions.InstanceId).to.be('i-34567890');
|
||||
});
|
||||
|
||||
it('should generate the correct targets by expanding template variables from url', function() {
|
||||
var templateSrv = {
|
||||
variables: [
|
||||
{
|
||||
name: 'instance_id',
|
||||
options: [
|
||||
{ text: 'i-23456789', value: 'i-23456789', selected: false },
|
||||
{ text: 'i-34567890', value: 'i-34567890', selected: false },
|
||||
],
|
||||
current: 'i-45678901',
|
||||
},
|
||||
],
|
||||
replace: function(target, scopedVars) {
|
||||
if (target === '$instance_id') {
|
||||
return 'i-45678901';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
getVariableName: function(e) {
|
||||
return 'instance_id';
|
||||
},
|
||||
variableExists: function(e) {
|
||||
return true;
|
||||
},
|
||||
containsVariable: function(str, variableName) {
|
||||
return str.indexOf('$' + variableName) !== -1;
|
||||
},
|
||||
};
|
||||
|
||||
var targets = [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: '$instance_id',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
period: 300,
|
||||
},
|
||||
];
|
||||
|
||||
var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv);
|
||||
expect(result[0].dimensions.InstanceId).to.be('i-45678901');
|
||||
});
|
||||
});
|
||||
|
||||
@ -228,21 +154,21 @@ describe('CloudWatchDatasource', function() {
|
||||
it('should return the datasource region if empty or "default"', function() {
|
||||
var defaultRegion = instanceSettings.jsonData.defaultRegion;
|
||||
|
||||
expect(ctx.ds.getActualRegion()).to.be(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion('')).to.be(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion('default')).to.be(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion()).toBe(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion('')).toBe(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion);
|
||||
});
|
||||
|
||||
it('should return the specified region if specified', function() {
|
||||
expect(ctx.ds.getActualRegion('some-fake-region-1')).to.be('some-fake-region-1');
|
||||
expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
|
||||
});
|
||||
|
||||
var requestParams;
|
||||
beforeEach(function() {
|
||||
ctx.ds.performTimeSeriesQuery = function(request) {
|
||||
ctx.ds.performTimeSeriesQuery = jest.fn(request => {
|
||||
requestParams = request;
|
||||
return ctx.$q.when({ data: {} });
|
||||
};
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('should query for the datasource region if empty or "default"', function(done) {
|
||||
@ -264,10 +190,9 @@ describe('CloudWatchDatasource', function() {
|
||||
};
|
||||
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(requestParams.queries[0].region).to.be(instanceSettings.jsonData.defaultRegion);
|
||||
expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
});
|
||||
|
||||
@ -311,18 +236,17 @@ describe('CloudWatchDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(params) {
|
||||
return ctx.$q.when({ data: response });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(params => {
|
||||
return Promise.resolve({ data: response });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return series list', function(done) {
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(result.data[0].target).to.be(response.results.A.series[0].name);
|
||||
expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
|
||||
expect(result.data[0].target).toBe(response.results.A.series[0].name);
|
||||
expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
});
|
||||
|
||||
@ -332,14 +256,13 @@ describe('CloudWatchDatasource', function() {
|
||||
scenario.setup = setupCallback => {
|
||||
beforeEach(() => {
|
||||
setupCallback();
|
||||
ctx.backendSrv.datasourceRequest = args => {
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(args => {
|
||||
scenario.request = args.data;
|
||||
return ctx.$q.when({ data: scenario.requestResponse });
|
||||
};
|
||||
return Promise.resolve({ data: scenario.requestResponse });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(args => {
|
||||
scenario.result = args;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
@ -359,9 +282,9 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
|
||||
it('should call __GetRegions and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('us-east-1');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('regions');
|
||||
expect(scenario.result[0].text).toContain('us-east-1');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('regions');
|
||||
});
|
||||
});
|
||||
|
||||
@ -377,9 +300,9 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
|
||||
it('should call __GetNamespaces and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('AWS/EC2');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('namespaces');
|
||||
expect(scenario.result[0].text).toContain('AWS/EC2');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('namespaces');
|
||||
});
|
||||
});
|
||||
|
||||
@ -395,9 +318,9 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
|
||||
it('should call __GetMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('CPUUtilization');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('metrics');
|
||||
expect(scenario.result[0].text).toBe('CPUUtilization');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('metrics');
|
||||
});
|
||||
});
|
||||
|
||||
@ -413,9 +336,9 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
|
||||
it('should call __GetDimensions and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('InstanceId');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_keys');
|
||||
expect(scenario.result[0].text).toBe('InstanceId');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('dimension_keys');
|
||||
});
|
||||
});
|
||||
|
||||
@ -431,9 +354,9 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
|
||||
it('should call __ListMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_values');
|
||||
expect(scenario.result[0].text).toContain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('dimension_values');
|
||||
});
|
||||
});
|
||||
|
||||
@ -449,9 +372,9 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
|
||||
it('should call __ListMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_values');
|
||||
expect(scenario.result[0].text).toContain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('dimension_values');
|
||||
});
|
||||
});
|
||||
|
||||
@ -544,7 +467,7 @@ describe('CloudWatchDatasource', function() {
|
||||
let now = new Date(options.range.from.valueOf() + t[2] * 1000);
|
||||
let expected = t[3];
|
||||
let actual = ctx.ds.getPeriod(target, options, now);
|
||||
expect(actual).to.be(expected);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,28 +1,21 @@
|
||||
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
|
||||
import moment from 'moment';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import { MysqlDatasource } from '../datasource';
|
||||
import { CustomVariable } from 'app/features/templating/custom_variable';
|
||||
|
||||
describe('MySQLDatasource', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var instanceSettings = { name: 'mysql' };
|
||||
let instanceSettings = { name: 'mysql' };
|
||||
let backendSrv = {};
|
||||
let templateSrv = {
|
||||
replace: jest.fn(text => text),
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(ctx.providePhase(['backendSrv']));
|
||||
let ctx = <any>{
|
||||
backendSrv,
|
||||
};
|
||||
|
||||
beforeEach(
|
||||
angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
|
||||
ctx.$q = $q;
|
||||
ctx.$httpBackend = $httpBackend;
|
||||
ctx.$rootScope = $rootScope;
|
||||
ctx.ds = $injector.instantiate(MysqlDatasource, {
|
||||
instanceSettings: instanceSettings,
|
||||
});
|
||||
$httpBackend.when('GET', /\.html$/).respond('');
|
||||
})
|
||||
);
|
||||
beforeEach(() => {
|
||||
ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv);
|
||||
});
|
||||
|
||||
describe('When performing annotationQuery', function() {
|
||||
let results;
|
||||
@ -59,26 +52,25 @@ describe('MySQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.annotationQuery(options).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return annotation list', function() {
|
||||
expect(results.length).to.be(3);
|
||||
expect(results.length).toBe(3);
|
||||
|
||||
expect(results[0].text).to.be('some text');
|
||||
expect(results[0].tags[0]).to.be('TagA');
|
||||
expect(results[0].tags[1]).to.be('TagB');
|
||||
expect(results[0].text).toBe('some text');
|
||||
expect(results[0].tags[0]).toBe('TagA');
|
||||
expect(results[0].tags[1]).toBe('TagB');
|
||||
|
||||
expect(results[1].tags[0]).to.be('TagB');
|
||||
expect(results[1].tags[1]).to.be('TagC');
|
||||
expect(results[1].tags[0]).toBe('TagB');
|
||||
expect(results[1].tags[1]).toBe('TagC');
|
||||
|
||||
expect(results[2].tags.length).to.be(0);
|
||||
expect(results[2].tags.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -103,19 +95,18 @@ describe('MySQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return list of all column values', function() {
|
||||
expect(results.length).to.be(6);
|
||||
expect(results[0].text).to.be('aTitle');
|
||||
expect(results[5].text).to.be('some text3');
|
||||
expect(results.length).toBe(6);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[5].text).toBe('some text3');
|
||||
});
|
||||
});
|
||||
|
||||
@ -140,21 +131,20 @@ describe('MySQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return list of as text, value', function() {
|
||||
expect(results.length).to.be(3);
|
||||
expect(results[0].text).to.be('aTitle');
|
||||
expect(results[0].value).to.be('value1');
|
||||
expect(results[2].text).to.be('aTitle3');
|
||||
expect(results[2].value).to.be('value3');
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[0].value).toBe('value1');
|
||||
expect(results[2].text).toBe('aTitle3');
|
||||
expect(results[2].value).toBe('value3');
|
||||
});
|
||||
});
|
||||
|
||||
@ -179,19 +169,18 @@ describe('MySQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return list of unique keys', function() {
|
||||
expect(results.length).to.be(1);
|
||||
expect(results[0].text).to.be('aTitle');
|
||||
expect(results[0].value).to.be('same');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[0].value).toBe('same');
|
||||
});
|
||||
});
|
||||
|
||||
@ -202,33 +191,33 @@ describe('MySQLDatasource', function() {
|
||||
|
||||
describe('and value is a string', () => {
|
||||
it('should return an unquoted value', () => {
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc');
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and value is a number', () => {
|
||||
it('should return an unquoted value', () => {
|
||||
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000);
|
||||
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and value is an array of strings', () => {
|
||||
it('should return comma separated quoted values', () => {
|
||||
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'");
|
||||
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable allows multi-value and value is a string', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.multi = true;
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable allows all and value is a string', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.includeAll = true;
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
|
||||
});
|
||||
});
|
||||
});
|
@ -1,28 +1,21 @@
|
||||
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
|
||||
import moment from 'moment';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import { PostgresDatasource } from '../datasource';
|
||||
import { CustomVariable } from 'app/features/templating/custom_variable';
|
||||
|
||||
describe('PostgreSQLDatasource', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var instanceSettings = { name: 'postgresql' };
|
||||
let instanceSettings = { name: 'postgresql' };
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(ctx.providePhase(['backendSrv']));
|
||||
let backendSrv = {};
|
||||
let templateSrv = {
|
||||
replace: jest.fn(text => text),
|
||||
};
|
||||
let ctx = <any>{
|
||||
backendSrv,
|
||||
};
|
||||
|
||||
beforeEach(
|
||||
angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
|
||||
ctx.$q = $q;
|
||||
ctx.$httpBackend = $httpBackend;
|
||||
ctx.$rootScope = $rootScope;
|
||||
ctx.ds = $injector.instantiate(PostgresDatasource, {
|
||||
instanceSettings: instanceSettings,
|
||||
});
|
||||
$httpBackend.when('GET', /\.html$/).respond('');
|
||||
})
|
||||
);
|
||||
beforeEach(() => {
|
||||
ctx.ds = new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv);
|
||||
});
|
||||
|
||||
describe('When performing annotationQuery', function() {
|
||||
let results;
|
||||
@ -59,26 +52,25 @@ describe('PostgreSQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.annotationQuery(options).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return annotation list', function() {
|
||||
expect(results.length).to.be(3);
|
||||
expect(results.length).toBe(3);
|
||||
|
||||
expect(results[0].text).to.be('some text');
|
||||
expect(results[0].tags[0]).to.be('TagA');
|
||||
expect(results[0].tags[1]).to.be('TagB');
|
||||
expect(results[0].text).toBe('some text');
|
||||
expect(results[0].tags[0]).toBe('TagA');
|
||||
expect(results[0].tags[1]).toBe('TagB');
|
||||
|
||||
expect(results[1].tags[0]).to.be('TagB');
|
||||
expect(results[1].tags[1]).to.be('TagC');
|
||||
expect(results[1].tags[0]).toBe('TagB');
|
||||
expect(results[1].tags[1]).toBe('TagC');
|
||||
|
||||
expect(results[2].tags.length).to.be(0);
|
||||
expect(results[2].tags.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -103,19 +95,18 @@ describe('PostgreSQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return list of all column values', function() {
|
||||
expect(results.length).to.be(6);
|
||||
expect(results[0].text).to.be('aTitle');
|
||||
expect(results[5].text).to.be('some text3');
|
||||
expect(results.length).toBe(6);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[5].text).toBe('some text3');
|
||||
});
|
||||
});
|
||||
|
||||
@ -140,21 +131,20 @@ describe('PostgreSQLDatasource', function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return list of as text, value', function() {
|
||||
expect(results.length).to.be(3);
|
||||
expect(results[0].text).to.be('aTitle');
|
||||
expect(results[0].value).to.be('value1');
|
||||
expect(results[2].text).to.be('aTitle3');
|
||||
expect(results[2].value).to.be('value3');
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[0].value).toBe('value1');
|
||||
expect(results[2].text).toBe('aTitle3');
|
||||
expect(results[2].value).toBe('value3');
|
||||
});
|
||||
});
|
||||
|
||||
@ -178,20 +168,20 @@ describe('PostgreSQLDatasource', function() {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query).then(function(data) {
|
||||
results = data;
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
//ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return list of unique keys', function() {
|
||||
expect(results.length).to.be(1);
|
||||
expect(results[0].text).to.be('aTitle');
|
||||
expect(results[0].value).to.be('same');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[0].value).toBe('same');
|
||||
});
|
||||
});
|
||||
|
||||
@ -202,33 +192,33 @@ describe('PostgreSQLDatasource', function() {
|
||||
|
||||
describe('and value is a string', () => {
|
||||
it('should return an unquoted value', () => {
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc');
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and value is a number', () => {
|
||||
it('should return an unquoted value', () => {
|
||||
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000);
|
||||
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and value is an array of strings', () => {
|
||||
it('should return comma separated quoted values', () => {
|
||||
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'");
|
||||
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable allows multi-value and is a string', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.multi = true;
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable allows all and is a string', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.includeAll = true;
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
|
||||
});
|
||||
});
|
||||
});
|
@ -196,7 +196,7 @@ export class PrometheusDatasource {
|
||||
interval = adjustedInterval;
|
||||
scopedVars = Object.assign({}, options.scopedVars, {
|
||||
__interval: { text: interval + 's', value: interval + 's' },
|
||||
__interval_ms: { text: interval * 1000, value: interval * 1000 },
|
||||
__interval_ms: { text: String(interval * 1000), value: String(interval * 1000) },
|
||||
});
|
||||
}
|
||||
query.step = interval;
|
||||
|
@ -452,7 +452,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '10s',
|
||||
scopedVars: {
|
||||
__interval: { text: '10s', value: '10s' },
|
||||
__interval_ms: { text: 10 * 1000, value: 10 * 1000 },
|
||||
__interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@ -463,8 +463,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('10s');
|
||||
expect(query.scopedVars.__interval.value).to.be('10s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
|
||||
});
|
||||
it('should be min interval when it is greater than auto interval', function() {
|
||||
var query = {
|
||||
@ -479,7 +479,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@ -490,8 +490,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
});
|
||||
it('should account for intervalFactor', function() {
|
||||
var query = {
|
||||
@ -507,7 +507,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '10s',
|
||||
scopedVars: {
|
||||
__interval: { text: '10s', value: '10s' },
|
||||
__interval_ms: { text: 10 * 1000, value: 10 * 1000 },
|
||||
__interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@ -518,8 +518,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('10s');
|
||||
expect(query.scopedVars.__interval.value).to.be('10s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
|
||||
});
|
||||
it('should be interval * intervalFactor when greater than min interval', function() {
|
||||
var query = {
|
||||
@ -535,7 +535,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@ -546,8 +546,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
});
|
||||
it('should be min interval when greater than interval * intervalFactor', function() {
|
||||
var query = {
|
||||
@ -563,7 +563,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@ -574,8 +574,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
});
|
||||
it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
|
||||
var query = {
|
||||
@ -590,7 +590,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
},
|
||||
};
|
||||
var end = 7 * 24 * 60 * 60;
|
||||
@ -609,8 +609,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import * as d3ScaleChromatic from 'd3-scale-chromatic';
|
||||
|
||||
export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any {
|
||||
let colorInterpolator = d3ScaleChromatic[colorScheme.value];
|
||||
let colorScaleInverted = colorScheme.invert === 'always' || (colorScheme.invert === 'dark' && !lightTheme);
|
||||
let colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark');
|
||||
|
||||
let start = colorScaleInverted ? maxValue : minValue;
|
||||
let end = colorScaleInverted ? minValue : maxValue;
|
||||
|
@ -76,6 +76,13 @@ let colorSchemes = [
|
||||
{ name: 'Reds', value: 'interpolateReds', invert: 'dark' },
|
||||
|
||||
// Sequential (Multi-Hue)
|
||||
{ name: 'Viridis', value: 'interpolateViridis', invert: 'light' },
|
||||
{ name: 'Magma', value: 'interpolateMagma', invert: 'light' },
|
||||
{ name: 'Inferno', value: 'interpolateInferno', invert: 'light' },
|
||||
{ name: 'Plasma', value: 'interpolatePlasma', invert: 'light' },
|
||||
{ name: 'Warm', value: 'interpolateWarm', invert: 'light' },
|
||||
{ name: 'Cool', value: 'interpolateCool', invert: 'light' },
|
||||
{ name: 'Cubehelix', value: 'interpolateCubehelixDefault', invert: 'light' },
|
||||
{ name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' },
|
||||
{ name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' },
|
||||
{ name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' },
|
||||
@ -87,7 +94,7 @@ let colorSchemes = [
|
||||
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
|
||||
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
|
||||
{ name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' },
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' },
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
|
||||
];
|
||||
|
||||
const ds_support_histogram_sort = ['prometheus', 'elasticsearch'];
|
||||
|
@ -5,6 +5,8 @@ import ServerStats from 'app/containers/ServerStats/ServerStats';
|
||||
import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList';
|
||||
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
||||
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
||||
import TeamPages from 'app/containers/Teams/TeamPages';
|
||||
import TeamList from 'app/containers/Teams/TeamList';
|
||||
|
||||
/** @ngInject **/
|
||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
@ -140,19 +142,23 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
controller: 'OrgApiKeysCtrl',
|
||||
})
|
||||
.when('/org/teams', {
|
||||
templateUrl: 'public/app/features/org/partials/teams.html',
|
||||
controller: 'TeamsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
roles: () => ['Editor', 'Admin'],
|
||||
component: () => TeamList,
|
||||
},
|
||||
})
|
||||
.when('/org/teams/new', {
|
||||
templateUrl: 'public/app/features/org/partials/create_team.html',
|
||||
controller: 'CreateTeamCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/org/teams/edit/:id', {
|
||||
templateUrl: 'public/app/features/org/partials/team_details.html',
|
||||
controller: 'TeamDetailsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
.when('/org/teams/edit/:id/:page?', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
roles: () => ['Admin'],
|
||||
component: () => TeamPages,
|
||||
},
|
||||
})
|
||||
.when('/profile', {
|
||||
templateUrl: 'public/app/features/org/partials/profile.html',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { types } from 'mobx-state-tree';
|
||||
import { types } from 'mobx-state-tree';
|
||||
|
||||
export const NavItem = types.model('NavItem', {
|
||||
id: types.identifier(types.string),
|
||||
@ -8,6 +8,7 @@ export const NavItem = types.model('NavItem', {
|
||||
icon: types.optional(types.string, ''),
|
||||
img: types.optional(types.string, ''),
|
||||
active: types.optional(types.boolean, false),
|
||||
hideFromTabs: types.optional(types.boolean, false),
|
||||
breadcrumbs: types.optional(types.array(types.late(() => Breadcrumb)), []),
|
||||
children: types.optional(types.array(types.late(() => NavItem)), []),
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { types, getEnv } from 'mobx-state-tree';
|
||||
import { NavItem } from './NavItem';
|
||||
import { ITeam } from '../TeamsStore/TeamsStore';
|
||||
|
||||
export const NavStore = types
|
||||
.model('NavStore', {
|
||||
@ -115,4 +116,43 @@ export const NavStore = types
|
||||
|
||||
self.main = NavItem.create(main);
|
||||
},
|
||||
|
||||
initTeamPage(team: ITeam, tab: string, isSyncEnabled: boolean) {
|
||||
let 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);
|
||||
},
|
||||
}));
|
||||
|
@ -6,6 +6,7 @@ import { AlertListStore } from './../AlertListStore/AlertListStore';
|
||||
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({
|
||||
search: types.optional(SearchStore, {
|
||||
@ -28,6 +29,9 @@ export const RootStore = types.model({
|
||||
routeParams: {},
|
||||
}),
|
||||
folder: types.optional(FolderStore, {}),
|
||||
teams: types.optional(TeamsStore, {
|
||||
map: {},
|
||||
}),
|
||||
});
|
||||
|
||||
type IRootStoreType = typeof RootStore.Type;
|
||||
|
156
public/app/stores/TeamsStore/TeamsStore.ts
Normal file
156
public/app/stores/TeamsStore/TeamsStore.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { types, getEnv, flow } from 'mobx-state-tree';
|
||||
|
||||
export const TeamMember = types.model('TeamMember', {
|
||||
userId: types.identifier(types.number),
|
||||
teamId: types.number,
|
||||
avatarUrl: types.string,
|
||||
email: types.string,
|
||||
login: types.string,
|
||||
});
|
||||
|
||||
type TeamMemberType = typeof TeamMember.Type;
|
||||
export interface ITeamMember extends TeamMemberType {}
|
||||
|
||||
export const TeamGroup = types.model('TeamGroup', {
|
||||
groupId: types.identifier(types.string),
|
||||
teamId: types.number,
|
||||
});
|
||||
|
||||
type TeamGroupType = typeof TeamGroup.Type;
|
||||
export interface ITeamGroup extends TeamGroupType {}
|
||||
|
||||
export const Team = 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(TeamMember), {}),
|
||||
groups: types.optional(types.map(TeamGroup), {}),
|
||||
})
|
||||
.views(self => ({
|
||||
get filteredMembers() {
|
||||
let members = this.members.values();
|
||||
let 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 (let member of rsp) {
|
||||
self.members.set(member.userId.toString(), TeamMember.create(member));
|
||||
}
|
||||
}),
|
||||
|
||||
removeMember: flow(function* load(member: ITeamMember) {
|
||||
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 (let group of rsp) {
|
||||
self.groups.set(group.groupId, TeamGroup.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,
|
||||
TeamGroup.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 Team.Type;
|
||||
export interface ITeam extends TeamType {}
|
||||
|
||||
export const TeamsStore = types
|
||||
.model('TeamsStore', {
|
||||
map: types.map(Team),
|
||||
search: types.optional(types.string, ''),
|
||||
})
|
||||
.views(self => ({
|
||||
get filteredTeams() {
|
||||
let teams = this.map.values();
|
||||
let 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 (let team of rsp.teams) {
|
||||
self.map.set(team.id.toString(), Team.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, Team.create(team));
|
||||
}),
|
||||
}));
|
@ -403,9 +403,9 @@ select.gf-form-input ~ .gf-form-help-icon {
|
||||
|
||||
.cta-form {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
padding: 1.5rem;
|
||||
background-color: $empty-list-cta-bg;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-top: 3px solid $green;
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,14 @@
|
||||
// icon hidden on smaller screens
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--folder {
|
||||
color: $text-color-weak;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-buttons {
|
||||
|
@ -1,6 +1,17 @@
|
||||
declare var global: NodeJS.Global;
|
||||
|
||||
(<any>global).requestAnimationFrame = (callback) => {
|
||||
(<any>global).requestAnimationFrame = callback => {
|
||||
setTimeout(callback, 0);
|
||||
};
|
||||
|
||||
(<any>Promise.prototype).finally = function(onFinally) {
|
||||
return this.then(
|
||||
/* onFulfilled */
|
||||
res => Promise.resolve(onFinally()).then(() => res),
|
||||
/* onRejected */
|
||||
err =>
|
||||
Promise.resolve(onFinally()).then(() => {
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -13,9 +13,6 @@ function exit_if_fail {
|
||||
echo "running go fmt"
|
||||
exit_if_fail test -z "$(gofmt -s -l ./pkg | tee /dev/stderr)"
|
||||
|
||||
echo "running go vet"
|
||||
exit_if_fail test -z "$(go vet ./pkg/... | tee /dev/stderr)"
|
||||
|
||||
echo "building backend with install to cache pkgs"
|
||||
exit_if_fail time go install ./pkg/cmd/grafana-server
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user