Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Benjamin Schweizer 2018-09-04 17:42:15 +02:00
commit 4bf8f80657
114 changed files with 3931 additions and 1139 deletions

View File

@ -5,6 +5,7 @@
* **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz)
* **Dashboard**: TV & Kiosk mode changes, new cycle view mode button in dashboard toolbar [#13025](https://github.com/grafana/grafana/pull/13025)
* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
* **Postgres**: Graphical query builder [#10095](https://github.com/grafana/grafana/issues/10095), thx [svenklemm](https://github.com/svenklemm)
### New Features
@ -19,6 +20,9 @@
### Minor
* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125)
* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme)
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)

View File

@ -538,3 +538,8 @@ container_name =
[external_image_storage.local]
# does not require any configuration
[rendering]
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
server_url =
callback_url =

View File

@ -460,3 +460,8 @@ log_queries =
[external_image_storage.local]
# does not require any configuration
[rendering]
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
;server_url =
;callback_url =

View File

@ -155,7 +155,7 @@ Since not all datasources have the same configuration settings we only have the
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version |
| timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
| esVersion | number | Elastic | Elasticsearch version as an number (2/5/56) |
| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) |
| timeField | string | Elastic | Which field that should be used as timestamp |
| interval | string | Elastic | Index date time format |
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
@ -165,6 +165,8 @@ Since not all datasources have the same configuration settings we only have the
| tsdbVersion | string | OpenTSDB | Version |
| tsdbResolution | string | OpenTSDB | Resolution |
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 |
| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
#### Secure Json Data

View File

@ -31,7 +31,9 @@ Name | Description
*User* | Database user's login/username
*Password* | Database user's password
*SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+).
*Version* | This option determines which functions are available in the query builder (only available in Grafana 5.3+).
*TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+).
### Database User Permissions (Important!)
@ -292,5 +294,6 @@ datasources:
password: "Password!"
jsonData:
sslmode: "disable" # disable/require/verify-ca/verify-full
postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
timescaledb: false
```

View File

@ -152,7 +152,7 @@ func downloadFile(pluginName, filePath, url string) (err error) {
return err
}
r, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength)
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return err
}

View File

@ -96,13 +96,17 @@ func main() {
func listenToSystemSignals(server *GrafanaServerImpl) {
signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
sighupChan := make(chan os.Signal, 1)
signal.Notify(ignoreChan, syscall.SIGHUP)
signal.Notify(sighupChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
select {
case sig := <-signalChan:
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
for {
select {
case _ = <-sighupChan:
log.Reload()
case sig := <-signalChan:
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
}
}
}

View File

@ -236,3 +236,20 @@ func (w *FileLogWriter) Close() {
func (w *FileLogWriter) Flush() {
w.mw.fd.Sync()
}
// Reload file logger
func (w *FileLogWriter) Reload() {
// block Logger's io.Writer
w.mw.Lock()
defer w.mw.Unlock()
// Close
fd := w.mw.fd
fd.Close()
// Open again
err := w.StartLogger()
if err != nil {
fmt.Fprintf(os.Stderr, "Reload StartLogger: %s\n", err)
}
}

View File

@ -3,3 +3,7 @@ package log
type DisposableHandler interface {
Close()
}
type ReloadableHandler interface {
Reload()
}

View File

@ -21,10 +21,12 @@ import (
var Root log15.Logger
var loggersToClose []DisposableHandler
var loggersToReload []ReloadableHandler
var filters map[string]log15.Lvl
func init() {
loggersToClose = make([]DisposableHandler, 0)
loggersToReload = make([]ReloadableHandler, 0)
Root = log15.Root()
Root.SetHandler(log15.DiscardHandler())
}
@ -115,6 +117,12 @@ func Close() {
loggersToClose = make([]DisposableHandler, 0)
}
func Reload() {
for _, logger := range loggersToReload {
logger.Reload()
}
}
func GetLogLevelFor(name string) Lvl {
if level, ok := filters[name]; ok {
switch level {
@ -230,6 +238,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
fileHandler.Init()
loggersToClose = append(loggersToClose, fileHandler)
loggersToReload = append(loggersToReload, fileHandler)
handler = fileHandler
case "syslog":
sysLogHandler := NewSyslog(sec, format)

View File

@ -2,6 +2,7 @@ package rendering
import (
"context"
"fmt"
"io"
"net"
"net/http"
@ -20,14 +21,13 @@ var netTransport = &http.Transport{
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Transport: netTransport,
}
func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) {
filePath := rs.getFilePathForNewImage()
var netClient = &http.Client{
Timeout: opts.Timeout,
Transport: netTransport,
}
rendererUrl, err := url.Parse(rs.Cfg.RendererUrl)
if err != nil {
return nil, err
@ -35,10 +35,10 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
queryParams := rendererUrl.Query()
queryParams.Add("url", rs.getURL(opts.Path))
queryParams.Add("renderKey", rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole))
queryParams.Add("renderKey", rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole))
queryParams.Add("width", strconv.Itoa(opts.Width))
queryParams.Add("height", strconv.Itoa(opts.Height))
queryParams.Add("domain", rs.getLocalDomain())
queryParams.Add("domain", rs.domain)
queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone))
queryParams.Add("encoding", opts.Encoding)
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
@ -49,20 +49,48 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
return nil, err
}
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
defer cancel()
req = req.WithContext(reqContext)
// make request to renderer server
resp, err := netClient.Do(req)
if err != nil {
return nil, err
rs.log.Error("Failed to send request to remote rendering service.", "error", err)
return nil, fmt.Errorf("Failed to send request to remote rendering service. %s", err)
}
// save response to file
defer resp.Body.Close()
// check for timeout first
if reqContext.Err() == context.DeadlineExceeded {
rs.log.Info("Rendering timed out")
return nil, ErrTimeout
}
// if we didnt get a 200 response, something went wrong.
if resp.StatusCode != http.StatusOK {
rs.log.Error("Remote rendering request failed", "error", resp.Status)
return nil, fmt.Errorf("Remote rendering request failed. %d: %s", resp.StatusCode, resp.Status)
}
out, err := os.Create(filePath)
if err != nil {
return nil, err
}
defer out.Close()
io.Copy(out, resp.Body)
_, err = io.Copy(out, resp.Body)
if err != nil {
// check that we didnt timeout while receiving the response.
if reqContext.Err() == context.DeadlineExceeded {
rs.log.Info("Rendering timed out")
return nil, ErrTimeout
}
rs.log.Error("Remote rendering request failed", "error", err)
return nil, fmt.Errorf("Remote rendering request failed. %s", err)
}
return &RenderResult{FilePath: filePath}, err
}

View File

@ -49,7 +49,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
fmt.Sprintf("width=%v", opts.Width),
fmt.Sprintf("height=%v", opts.Height),
fmt.Sprintf("png=%v", pngPath),
fmt.Sprintf("domain=%v", rs.getLocalDomain()),
fmt.Sprintf("domain=%v", rs.domain),
fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()),
fmt.Sprintf("renderKey=%v", renderKey),
}

View File

@ -77,10 +77,10 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, opts Opts) (*Re
Height: int32(opts.Height),
FilePath: pngPath,
Timeout: int32(opts.Timeout.Seconds()),
RenderKey: rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole),
RenderKey: rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole),
Encoding: opts.Encoding,
Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
Domain: rs.getLocalDomain(),
Domain: rs.domain,
})
if err != nil {

View File

@ -3,6 +3,8 @@ package rendering
import (
"context"
"fmt"
"net/url"
"os"
"path/filepath"
plugin "github.com/hashicorp/go-plugin"
@ -27,12 +29,31 @@ type RenderingService struct {
grpcPlugin pluginModel.RendererPlugin
pluginInfo *plugins.RendererPlugin
renderAction renderFunc
domain string
Cfg *setting.Cfg `inject:""`
}
func (rs *RenderingService) Init() error {
rs.log = log.New("rendering")
// ensure ImagesDir exists
err := os.MkdirAll(rs.Cfg.ImagesDir, 0700)
if err != nil {
return err
}
// set value used for domain attribute of renderKey cookie
if rs.Cfg.RendererUrl != "" {
// RendererCallbackUrl has already been passed, it wont generate an error.
u, _ := url.Parse(rs.Cfg.RendererCallbackUrl)
rs.domain = u.Hostname()
} else if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
rs.domain = setting.HttpAddr
} else {
rs.domain = "localhost"
}
return nil
}
@ -82,16 +103,17 @@ func (rs *RenderingService) getFilePathForNewImage() string {
}
func (rs *RenderingService) getURL(path string) string {
// &render=1 signals to the legacy redirect layer to
return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.getLocalDomain(), setting.HttpPort, path)
}
if rs.Cfg.RendererUrl != "" {
// The backend rendering service can potentially be remote.
// So we need to use the root_url to ensure the rendering service
// can reach this Grafana instance.
// &render=1 signals to the legacy redirect layer to
return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
func (rs *RenderingService) getLocalDomain() string {
if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
return setting.HttpAddr
}
return "localhost"
// &render=1 signals to the legacy redirect layer to
return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.domain, setting.HttpPort, path)
}
func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) string {

View File

@ -197,6 +197,7 @@ type Cfg struct {
ImagesDir string
PhantomDir string
RendererUrl string
RendererCallbackUrl string
DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration
@ -641,6 +642,18 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// Rendering
renderSec := iniFile.Section("rendering")
cfg.RendererUrl = renderSec.Key("server_url").String()
cfg.RendererCallbackUrl = renderSec.Key("callback_url").String()
if cfg.RendererCallbackUrl == "" {
cfg.RendererCallbackUrl = AppUrl
} else {
if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' {
cfg.RendererCallbackUrl += "/"
}
_, err := url.Parse(cfg.RendererCallbackUrl)
if err != nil {
log.Fatal(4, "Invalid callback_url(%s): %s", cfg.RendererCallbackUrl, err)
}
}
cfg.ImagesDir = filepath.Join(DataPath, "png")
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)

View File

@ -20,6 +20,7 @@ func TestLoadingSettings(t *testing.T) {
So(err, ShouldBeNil)
So(AdminUser, ShouldEqual, "admin")
So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/")
})
Convey("Should be able to override via environment variables", func() {
@ -178,5 +179,15 @@ func TestLoadingSettings(t *testing.T) {
So(InstanceName, ShouldEqual, hostname)
})
Convey("Reading callback_url should add trailing slash", func() {
cfg := NewCfg()
cfg.Load(&CommandLineArgs{
HomePath: "../../",
Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"},
})
So(cfg.RendererCallbackUrl, ShouldEqual, "http://myserver/renderer/")
})
})
}

View File

@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore';
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
import { BackendSrv } from 'app/core/services/backend_srv';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
interface Props {
nav: typeof NavStore.Type;
@ -61,48 +62,81 @@ export class TeamList extends React.Component<Props, any> {
);
}
renderTeamList(teams) {
return (
<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>
);
}
renderEmptyList() {
return (
<div className="page-container page-body">
<EmptyListCTA
model={{
title: "You haven't created any teams yet.",
buttonIcon: 'fa fa-plus',
buttonLink: 'org/teams/new',
buttonTitle: ' New team',
proTip: 'Assign folder and dashboard permissions to teams instead of users to ease administration.',
proTipLink: '',
proTipLinkTitle: '',
proTipTarget: '_blank',
}}
/>
</div>
);
}
render() {
const { nav, teams } = this.props;
let view;
if (teams.filteredTeams.length > 0) {
view = this.renderTeamList(teams);
} else {
view = this.renderEmptyList();
}
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>
{view}
</div>
);
}

View File

@ -4,10 +4,12 @@ import PageHeader from './components/PageHeader/PageHeader';
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import { SearchResult } from './components/search/SearchResult';
import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import DashboardPermissions from './components/Permissions/DashboardPermissions';
export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
react2AngularDirective('sidemenu', SideMenu, []);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
react2AngularDirective('searchResult', SearchResult, []);

View File

@ -0,0 +1,96 @@
import React from 'react';
import { shallow } from 'enzyme';
import BottomNavLinks from './BottomNavLinks';
import appEvents from '../../app_events';
jest.mock('../../app_events', () => ({
emit: jest.fn(),
}));
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
link: {},
user: {
isGrafanaAdmin: false,
isSignedIn: false,
orgCount: 2,
orgRole: '',
orgId: 1,
orgName: 'Grafana',
timezone: 'UTC',
helpFlags1: 1,
lightTheme: false,
hasEditPermissionInFolders: false,
},
},
propOverrides
);
return shallow(<BottomNavLinks {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render organisation switcher', () => {
const wrapper = setup({
link: {
showOrgSwitcher: true,
},
});
expect(wrapper).toMatchSnapshot();
});
it('should render subtitle', () => {
const wrapper = setup({
link: {
subTitle: 'subtitle',
},
});
expect(wrapper).toMatchSnapshot();
});
it('should render children', () => {
const wrapper = setup({
link: {
children: [
{
id: '1',
},
{
id: '2',
},
{
id: '3',
},
{
id: '4',
hideFromMenu: true,
},
],
},
});
expect(wrapper).toMatchSnapshot();
});
});
describe('Functions', () => {
describe('item clicked', () => {
const wrapper = setup();
const mockEvent = { preventDefault: jest.fn() };
it('should emit show modal event if url matches shortcut', () => {
const child = { url: '/shortcuts' };
const instance = wrapper.instance() as BottomNavLinks;
instance.itemClicked(mockEvent, child);
expect(appEvents.emit).toHaveBeenCalledWith('show-modal', { templateHtml: '<help-modal></help-modal>' });
});
});
});

View File

@ -0,0 +1,78 @@
import React, { PureComponent } from 'react';
import appEvents from '../../app_events';
import { User } from '../../services/context_srv';
export interface Props {
link: any;
user: User;
}
class BottomNavLinks extends PureComponent<Props> {
itemClicked = (event, child) => {
if (child.url === '/shortcuts') {
event.preventDefault();
appEvents.emit('show-modal', {
templateHtml: '<help-modal></help-modal>',
});
}
};
switchOrg = () => {
appEvents.emit('show-modal', {
templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
});
};
render() {
const { link, user } = this.props;
return (
<div className="sidemenu-item dropdown dropup">
<a href={link.url} className="sidemenu-link" target={link.target}>
<span className="icon-circle sidemenu-icon">
{link.icon && <i className={link.icon} />}
{link.img && <img src={link.img} />}
</span>
</a>
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
{link.subTitle && (
<li className="sidemenu-subtitle">
<span className="sidemenu-item-text">{link.subTitle}</span>
</li>
)}
{link.showOrgSwitcher && (
<li className="sidemenu-org-switcher">
<a onClick={this.switchOrg}>
<div>
<div className="sidemenu-org-switcher__org-name">{user.orgName}</div>
<div className="sidemenu-org-switcher__org-current">Current Org:</div>
</div>
<div className="sidemenu-org-switcher__switch">
<i className="fa fa-fw fa-random" />Switch
</div>
</a>
</li>
)}
{link.children &&
link.children.map((child, index) => {
if (!child.hideFromMenu) {
return (
<li className={child.divider} key={`${child.text}-${index}`}>
<a href={child.url} target={child.target} onClick={event => this.itemClicked(event, child)}>
{child.icon && <i className={child.icon} />}
{child.text}
</a>
</li>
);
}
return null;
})}
<li className="side-menu-header">
<span className="sidemenu-item-text">{link.text}</span>
</li>
</ul>
</div>
);
}
}
export default BottomNavLinks;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { shallow } from 'enzyme';
import BottomSection from './BottomSection';
jest.mock('../../config', () => ({
bootData: {
navTree: [
{
id: 'profile',
hideFromMenu: true,
},
{
hideFromMenu: true,
},
{
hideFromMenu: false,
},
{
hideFromMenu: true,
},
],
},
user: {
orgCount: 5,
orgName: 'Grafana',
},
}));
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
sidemenu: true,
isSignedIn: false,
isGrafanaAdmin: false,
hasEditPermissionFolders: false,
},
}));
describe('Render', () => {
it('should render component', () => {
const wrapper = shallow(<BottomSection />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,29 @@
import React from 'react';
import _ from 'lodash';
import SignIn from './SignIn';
import BottomNavLinks from './BottomNavLinks';
import { contextSrv } from 'app/core/services/context_srv';
import config from '../../config';
export default function BottomSection() {
const navTree = _.cloneDeep(config.bootData.navTree);
const bottomNav = _.filter(navTree, item => item.hideFromMenu);
const isSignedIn = contextSrv.isSignedIn;
const user = contextSrv.user;
if (user && user.orgCount > 1) {
const profileNode = _.find(bottomNav, { id: 'profile' });
if (profileNode) {
profileNode.showOrgSwitcher = true;
}
}
return (
<div className="sidemenu__bottom">
{!isSignedIn && <SignIn />}
{bottomNav.map((link, index) => {
return <BottomNavLinks link={link} user={user} key={`${link.url}-${index}`} />;
})}
</div>
);
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { shallow } from 'enzyme';
import DropDownChild from './DropDownChild';
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
child: {
divider: true,
},
},
propOverrides
);
return shallow(<DropDownChild {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render icon if exists', () => {
const wrapper = setup({
child: {
divider: false,
icon: 'icon-test',
},
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,21 @@
import React, { SFC } from 'react';
export interface Props {
child: any;
}
const DropDownChild: SFC<Props> = props => {
const { child } = props;
const listItemClassName = child.divider ? 'divider' : '';
return (
<li className={listItemClassName}>
<a href={child.url}>
{child.icon && <i className={child.icon} />}
{child.text}
</a>
</li>
);
};
export default DropDownChild;

View File

@ -0,0 +1,70 @@
import React from 'react';
import { shallow } from 'enzyme';
import { SideMenu } from './SideMenu';
import appEvents from '../../app_events';
import { contextSrv } from 'app/core/services/context_srv';
jest.mock('../../app_events', () => ({
emit: jest.fn(),
}));
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
sidemenu: true,
user: {},
isSignedIn: false,
isGrafanaAdmin: false,
isEditor: false,
hasEditPermissionFolders: false,
toggleSideMenu: jest.fn(),
},
}));
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
loginUrl: '',
user: {},
mainLinks: [],
bottomeLinks: [],
isSignedIn: false,
},
propOverrides
);
return shallow(<SideMenu {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});
describe('Functions', () => {
describe('toggle side menu', () => {
const wrapper = setup();
const instance = wrapper.instance() as SideMenu;
instance.toggleSideMenu();
it('should call contextSrv.toggleSideMenu', () => {
expect(contextSrv.toggleSideMenu).toHaveBeenCalled();
});
it('should emit toggle sidemenu event', () => {
expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu');
});
});
describe('toggle side menu on mobile', () => {
const wrapper = setup();
const instance = wrapper.instance() as SideMenu;
instance.toggleSideMenuSmallBreakpoint();
it('should emit toggle sidemenu event', () => {
expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu-mobile');
});
});
});

View File

@ -0,0 +1,32 @@
import React, { PureComponent } from 'react';
import appEvents from '../../app_events';
import { contextSrv } from 'app/core/services/context_srv';
import TopSection from './TopSection';
import BottomSection from './BottomSection';
export class SideMenu extends PureComponent {
toggleSideMenu = () => {
contextSrv.toggleSideMenu();
appEvents.emit('toggle-sidemenu');
};
toggleSideMenuSmallBreakpoint = () => {
appEvents.emit('toggle-sidemenu-mobile');
};
render() {
return [
<div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
<img src="public/img/grafana_icon.svg" alt="graphana_logo" />
</div>,
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
<i className="fa fa-bars" />
<span className="sidemenu__close">
<i className="fa fa-times" />&nbsp;Close
</span>
</div>,
<TopSection key="topsection" />,
<BottomSection key="bottomsection" />,
];
}
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { shallow } from 'enzyme';
import SideMenuDropDown from './SideMenuDropDown';
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
link: {
text: 'link',
},
},
propOverrides
);
return shallow(<SideMenuDropDown {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render children', () => {
const wrapper = setup({
link: {
text: 'link',
children: [{ id: 1 }, { id: 2 }, { id: 3 }],
},
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
import React, { SFC } from 'react';
import DropDownChild from './DropDownChild';
interface Props {
link: any;
}
const SideMenuDropDown: SFC<Props> = props => {
const { link } = props;
return (
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li className="side-menu-header">
<span className="sidemenu-item-text">{link.text}</span>
</li>
{link.children &&
link.children.map((child, index) => {
return <DropDownChild child={child} key={`${child.url}-${index}`} />;
})}
</ul>
);
};
export default SideMenuDropDown;

View File

@ -0,0 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import SignIn from './SignIn';
describe('Render', () => {
it('should render component', () => {
const wrapper = shallow(<SignIn />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
import React, { SFC } from 'react';
const SignIn: SFC<any> = () => {
const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
return (
<div className="sidemenu-item">
<a href={loginUrl} className="sidemenu-link" target="_self">
<span className="icon-circle sidemenu-icon">
<i className="fa fa-fw fa-sign-in" />
</span>
</a>
<a href={loginUrl} target="_self">
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li className="side-menu-header">
<span className="sidemenu-item-text">Sign In</span>
</li>
</ul>
</a>
</div>
);
};
export default SignIn;

View File

@ -0,0 +1,41 @@
import React from 'react';
import { shallow } from 'enzyme';
import TopSection from './TopSection';
jest.mock('../../config', () => ({
bootData: {
navTree: [
{ id: '1', hideFromMenu: true },
{ id: '2', hideFromMenu: true },
{ id: '3', hideFromMenu: false },
{ id: '4', hideFromMenu: true },
],
},
}));
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
mainLinks: [],
},
propOverrides
);
return shallow(<TopSection {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render items', () => {
const wrapper = setup({
mainLinks: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,19 @@
import React, { SFC } from 'react';
import _ from 'lodash';
import TopSectionItem from './TopSectionItem';
import config from '../../config';
const TopSection: SFC<any> = () => {
const navTree = _.cloneDeep(config.bootData.navTree);
const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
return (
<div className="sidemenu__top">
{mainLinks.map((link, index) => {
return <TopSectionItem link={link} key={`${link.id}-${index}`} />;
})}
</div>
);
};
export default TopSection;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { shallow } from 'enzyme';
import TopSectionItem from './TopSectionItem';
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
link: {},
},
propOverrides
);
return shallow(<TopSectionItem {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
import React, { SFC } from 'react';
import SideMenuDropDown from './SideMenuDropDown';
export interface Props {
link: any;
}
const TopSectionItem: SFC<Props> = props => {
const { link } = props;
return (
<div className="sidemenu-item dropdown">
<a className="sidemenu-link" href={link.url} target={link.target}>
<span className="icon-circle sidemenu-icon">
<i className={link.icon} />
{link.img && <img src={link.img} />}
</span>
</a>
{link.children && <SideMenuDropDown link={link} />}
</div>
);
};
export default TopSectionItem;

View File

@ -0,0 +1,163 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render children 1`] = `
<div
className="sidemenu-item dropdown dropup"
>
<a
className="sidemenu-link"
>
<span
className="icon-circle sidemenu-icon"
/>
</a>
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
key="undefined-0"
>
<a
onClick={[Function]}
/>
</li>
<li
key="undefined-1"
>
<a
onClick={[Function]}
/>
</li>
<li
key="undefined-2"
>
<a
onClick={[Function]}
/>
</li>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
/>
</li>
</ul>
</div>
`;
exports[`Render should render component 1`] = `
<div
className="sidemenu-item dropdown dropup"
>
<a
className="sidemenu-link"
>
<span
className="icon-circle sidemenu-icon"
/>
</a>
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
/>
</li>
</ul>
</div>
`;
exports[`Render should render organisation switcher 1`] = `
<div
className="sidemenu-item dropdown dropup"
>
<a
className="sidemenu-link"
>
<span
className="icon-circle sidemenu-icon"
/>
</a>
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
className="sidemenu-org-switcher"
>
<a
onClick={[Function]}
>
<div>
<div
className="sidemenu-org-switcher__org-name"
>
Grafana
</div>
<div
className="sidemenu-org-switcher__org-current"
>
Current Org:
</div>
</div>
<div
className="sidemenu-org-switcher__switch"
>
<i
className="fa fa-fw fa-random"
/>
Switch
</div>
</a>
</li>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
/>
</li>
</ul>
</div>
`;
exports[`Render should render subtitle 1`] = `
<div
className="sidemenu-item dropdown dropup"
>
<a
className="sidemenu-link"
>
<span
className="icon-circle sidemenu-icon"
/>
</a>
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
className="sidemenu-subtitle"
>
<span
className="sidemenu-item-text"
>
subtitle
</span>
</li>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
/>
</li>
</ul>
</div>
`;

View File

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="sidemenu__bottom"
>
<SignIn />
<BottomNavLinks
key="undefined-0"
link={
Object {
"hideFromMenu": true,
"id": "profile",
}
}
/>
<BottomNavLinks
key="undefined-1"
link={
Object {
"hideFromMenu": true,
}
}
/>
<BottomNavLinks
key="undefined-2"
link={
Object {
"hideFromMenu": true,
}
}
/>
</div>
`;

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<li
className="divider"
>
<a />
</li>
`;
exports[`Render should render icon if exists 1`] = `
<li
className=""
>
<a>
<i
className="icon-test"
/>
</a>
</li>
`;

View File

@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
Array [
<div
className="sidemenu__logo"
key="logo"
onClick={[Function]}
/>,
<div
className="sidemenu__logo_small_breakpoint"
key="hamburger"
onClick={[Function]}
/>,
<TopSection
key="topsection"
/>,
<BottomSection
key="bottomsection"
/>,
]
`;

View File

@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render children 1`] = `
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
>
link
</span>
</li>
<DropDownChild
child={
Object {
"id": 1,
}
}
key="undefined-0"
/>
<DropDownChild
child={
Object {
"id": 2,
}
}
key="undefined-1"
/>
<DropDownChild
child={
Object {
"id": 3,
}
}
key="undefined-2"
/>
</ul>
`;
exports[`Render should render component 1`] = `
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
>
link
</span>
</li>
</ul>
`;

View File

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="sidemenu-item"
>
<a
className="sidemenu-link"
href="login?redirect=blank"
target="_self"
>
<span
className="icon-circle sidemenu-icon"
>
<i
className="fa fa-fw fa-sign-in"
/>
</span>
</a>
<a
href="login?redirect=blank"
target="_self"
>
<ul
className="dropdown-menu dropdown-menu--sidemenu"
role="menu"
>
<li
className="side-menu-header"
>
<span
className="sidemenu-item-text"
>
Sign In
</span>
</li>
</ul>
</a>
</div>
`;

View File

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="sidemenu__top"
>
<TopSectionItem
key="3-0"
link={
Object {
"hideFromMenu": false,
"id": "3",
}
}
/>
</div>
`;
exports[`Render should render items 1`] = `
<div
className="sidemenu__top"
>
<TopSectionItem
key="3-0"
link={
Object {
"hideFromMenu": false,
"id": "3",
}
}
/>
</div>
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="sidemenu-item dropdown"
>
<a
className="sidemenu-link"
>
<span
className="icon-circle sidemenu-icon"
>
<i />
</span>
</a>
</div>
`;

View File

@ -1,81 +0,0 @@
<a class="sidemenu__logo" ng-click="ctrl.toggleSideMenu()">
<img src="public/img/grafana_icon.svg"></img>
</a>
<a class="sidemenu__logo_small_breakpoint" ng-click="ctrl.toggleSideMenuSmallBreakpoint()">
<i class="fa fa-bars"></i>
<span class="sidemenu__close">
<i class="fa fa-times"></i>&nbsp;Close</span>
</a>
<div class="sidemenu__top">
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
<span class="icon-circle sidemenu-icon">
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
<img ng-src="{{::item.img}}" ng-show="::item.img">
</span>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
<li class="side-menu-header">
<span class="sidemenu-item-text">{{::item.text}}</span>
</li>
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
<a href="{{::child.url}}">
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
{{::child.text}}
</a>
</li>
</ul>
</div>
</div>
<div class="sidemenu__bottom">
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
<a href="{{ctrl.loginUrl}}" class="sidemenu-link" target="_self">
<span class="icon-circle sidemenu-icon">
<i class="fa fa-fw fa-sign-in"></i>
</span>
</a>
<a href="{{ctrl.loginUrl}}" target="_self">
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li class="side-menu-header">
<span class="sidemenu-item-text">Sign In</span>
</li>
</ul>
</a>
</div>
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
<span class="icon-circle sidemenu-icon">
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
<img ng-src="{{::item.img}}" ng-show="::item.img">
</span>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li ng-if="item.subTitle" class="sidemenu-subtitle">
<span class="sidemenu-item-text">{{::item.subTitle}}</span>
</li>
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
<a ng-click="ctrl.switchOrg()">
<div>
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
</div>
<div class="sidemenu-org-switcher__switch">
<i class="fa fa-fw fa-random"></i>Switch</div>
</a>
</li>
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
<a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
{{::child.text}}
</a>
</li>
<li class="side-menu-header">
<span class="sidemenu-item-text">{{::item.text}}</span>
</li>
</ul>
</div>
</div>

View File

@ -1,89 +0,0 @@
import _ from 'lodash';
import config from 'app/core/config';
import $ from 'jquery';
import coreModule from '../../core_module';
import appEvents from 'app/core/app_events';
export class SideMenuCtrl {
user: any;
mainLinks: any;
bottomNav: any;
loginUrl: string;
isSignedIn: boolean;
isOpenMobile: boolean;
/** @ngInject */
constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
this.isSignedIn = contextSrv.isSignedIn;
this.user = contextSrv.user;
const navTree = _.cloneDeep(config.bootData.navTree);
this.mainLinks = _.filter(navTree, item => !item.hideFromMenu);
this.bottomNav = _.filter(navTree, item => item.hideFromMenu);
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
if (contextSrv.user.orgCount > 1) {
const profileNode = _.find(this.bottomNav, { id: 'profile' });
if (profileNode) {
profileNode.showOrgSwitcher = true;
}
}
this.$scope.$on('$routeChangeSuccess', () => {
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
});
}
toggleSideMenu() {
this.contextSrv.toggleSideMenu();
appEvents.emit('toggle-sidemenu');
this.$timeout(() => {
this.$rootScope.$broadcast('render');
});
}
toggleSideMenuSmallBreakpoint() {
appEvents.emit('toggle-sidemenu-mobile');
}
switchOrg() {
this.$rootScope.appEvent('show-modal', {
templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
});
}
itemClicked(item, evt) {
if (item.url === '/shortcuts') {
appEvents.emit('show-modal', {
templateHtml: '<help-modal></help-modal>',
});
evt.preventDefault();
}
}
}
export function sideMenuDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/core/components/sidemenu/sidemenu.html',
controller: SideMenuCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {},
link: function(scope, elem) {
// hack to hide dropdown menu
elem.on('click.dropdown', '.dropdown-menu a', function(evt) {
const menu = $(evt.target).parents('.dropdown-menu');
const parent = menu.parent();
menu.detach();
setTimeout(function() {
parent.append(menu);
}, 100);
});
},
};
}
coreModule.directive('sidemenu', sideMenuDirective);

View File

@ -0,0 +1,74 @@
import _ from 'lodash';
export class SqlPartDef {
type: string;
style: string;
label: string;
params: any[];
defaultParams: any[];
wrapOpen: string;
wrapClose: string;
separator: string;
constructor(options: any) {
this.type = options.type;
if (options.label) {
this.label = options.label;
} else {
this.label = this.type[0].toUpperCase() + this.type.substring(1) + ':';
}
this.style = options.style;
if (this.style === 'function') {
this.wrapOpen = '(';
this.wrapClose = ')';
this.separator = ', ';
} else {
this.wrapOpen = ' ';
this.wrapClose = ' ';
this.separator = ' ';
}
this.params = options.params;
this.defaultParams = options.defaultParams;
}
}
export class SqlPart {
part: any;
def: SqlPartDef;
params: any[];
label: string;
name: string;
datatype: string;
constructor(part: any, def: any) {
this.part = part;
this.def = def;
if (!this.def) {
throw { message: 'Could not find sql part ' + part.type };
}
this.datatype = part.datatype;
if (part.name) {
this.name = part.name;
this.label = def.label + ' ' + part.name;
} else {
this.name = '';
this.label = def.label;
}
part.params = part.params || _.clone(this.def.defaultParams);
this.params = part.params;
}
updateParam(strValue, index) {
// handle optional parameters
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
} else {
this.params[index] = strValue;
}
this.part.params = this.params;
}
}

View File

@ -0,0 +1,199 @@
import _ from 'lodash';
import $ from 'jquery';
import coreModule from 'app/core/core_module';
const template = `
<div class="dropdown cascade-open">
<a ng-click="showActionsMenu()" class="query-part-name pointer dropdown-toggle" data-toggle="dropdown">{{part.label}}</a>
<span>{{part.def.wrapOpen}}</span><span class="query-part-parameters"></span><span>{{part.def.wrapClose}}</span>
<ul class="dropdown-menu">
<li ng-repeat="action in partActions">
<a ng-click="triggerPartAction(action)">{{action.text}}</a>
</li>
</ul>
`;
/** @ngInject */
export function sqlPartEditorDirective($compile, templateSrv) {
const paramTemplate = '<input type="text" class="hide input-mini"></input>';
return {
restrict: 'E',
template: template,
scope: {
part: '=',
handleEvent: '&',
debounce: '@',
},
link: function postLink($scope, elem) {
const part = $scope.part;
const partDef = part.def;
const $paramsContainer = elem.find('.query-part-parameters');
const debounceLookup = $scope.debounce;
let cancelBlur = null;
$scope.partActions = [];
function clickFuncParam(this: any, paramIndex) {
/*jshint validthis:true */
const $link = $(this);
const $input = $link.next();
$input.val(part.params[paramIndex]);
$input.css('width', $link.width() + 16 + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
const typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function inputBlur($input, paramIndex) {
cancelBlur = setTimeout(function() {
switchToLink($input, paramIndex);
}, 200);
}
function switchToLink($input, paramIndex) {
/*jshint validthis:true */
const $link = $input.prev();
const newValue = $input.val();
if (newValue !== '' || part.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
part.updateParam($input.val(), paramIndex);
$scope.$apply(() => {
$scope.handleEvent({ $event: { name: 'part-param-changed' } });
});
}
$input.hide();
$link.show();
}
function inputKeyPress(this: any, paramIndex, e) {
/*jshint validthis:true */
if (e.which === 13) {
switchToLink($(this), paramIndex);
}
}
function inputKeyDown(this: any) {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, param, paramIndex) {
if (!param.options && !param.dynamicLookup) {
return;
}
const typeaheadSource = function(query, callback) {
if (param.options) {
let options = param.options;
if (param.type === 'int') {
options = _.map(options, function(val) {
return val.toString();
});
}
return options;
}
$scope.$apply(function() {
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(function(result) {
const dynamicOptions = _.map(result, function(op) {
return op.value;
});
// add current value to dropdown if it's not in dynamicOptions
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
dynamicOptions.unshift(part.params[paramIndex]);
}
callback(dynamicOptions);
});
});
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadSource,
minLength: 0,
items: 1000,
updater: function(value) {
if (value === part.params[paramIndex]) {
clearTimeout(cancelBlur);
$input.focus();
return value;
}
return value;
},
});
const typeahead = $input.data('typeahead');
typeahead.lookup = function() {
this.query = this.$element.val() || '';
const items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
if (debounceLookup) {
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
}
}
$scope.showActionsMenu = function() {
$scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => {
$scope.partActions = res;
});
};
$scope.triggerPartAction = function(action) {
$scope.handleEvent({ $event: { name: 'action', action: action } });
};
function addElementsAndCompile() {
_.each(partDef.params, function(param, index) {
if (param.optional && part.params.length <= index) {
return;
}
if (index > 0) {
$('<span>' + partDef.separator + '</span>').appendTo($paramsContainer);
}
const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
const $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
const $input = $(paramTemplate);
$paramLink.appendTo($paramsContainer);
$input.appendTo($paramsContainer);
$input.blur(_.partial(inputBlur, $input, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
addTypeahead($input, param, index);
});
}
function relink() {
$paramsContainer.empty();
addElementsAndCompile();
}
relink();
},
};
}
coreModule.directive('sqlPartEditor', sqlPartEditorDirective);

View File

@ -33,9 +33,9 @@ export class Settings {
constructor(options) {
const defaults = {
datasources: {},
window_title_prefix: 'Grafana - ',
windowTitlePrefix: 'Grafana - ',
panels: {},
new_panel_title: 'Panel Title',
newPanelTitle: 'Panel Title',
playlist_timespan: '1m',
unsaved_changes_warning: true,
appSubUrl: '',

View File

@ -20,7 +20,6 @@ import './services/search_srv';
import './services/ng_react';
import { grafanaAppDirective } from './components/grafana_app';
import { sideMenuDirective } from './components/sidemenu/sidemenu';
import { searchDirective } from './components/search/search';
import { infoPopover } from './components/info_popover';
import { navbarDirective } from './components/navbar/navbar';
@ -31,6 +30,7 @@ import { layoutSelector } from './components/layout_selector/layout_selector';
import { switchDirective } from './components/switch';
import { dashboardSelector } from './components/dashboard_selector';
import { queryPartEditorDirective } from './components/query_part/query_part_editor';
import { sqlPartEditorDirective } from './components/sql_part/sql_part_editor';
import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
import 'app/core/controllers/all';
import 'app/core/services/all';
@ -61,7 +61,6 @@ export {
arrayJoin,
coreModule,
grafanaAppDirective,
sideMenuDirective,
navbarDirective,
searchDirective,
liveSrv,
@ -72,6 +71,7 @@ export {
appEvents,
dashboardSelector,
queryPartEditorDirective,
sqlPartEditorDirective,
colors,
formDropdownDirective,
assignModelProperties,

View File

@ -8,6 +8,8 @@ export class User {
isSignedIn: any;
orgRole: any;
orgId: number;
orgName: string;
orgCount: number;
timezone: string;
helpFlags1: number;
lightTheme: boolean;

View File

@ -450,6 +450,7 @@ kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
kbn.valueFormats.currencyBTC = kbn.formatBuilders.currency('฿');
// Data (Binary)
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@ -882,6 +883,7 @@ kbn.getUnitFormats = function() {
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
{ text: 'Swiss franc (CHF)', value: 'currencyCHF' },
{ text: 'Polish Złoty (PLN)', value: 'currencyPLN' },
{ text: 'Bitcoin (฿)', value: 'currencyBTC' },
],
},
{

View File

@ -12,7 +12,7 @@ class AdminSettingsCtrl {
constructor($scope, backendSrv, navModelSrv) {
this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings', 1);
backendSrv.get('/api/admin/settings').then(function(settings) {
backendSrv.get('/api/admin/settings').then(settings => {
$scope.settings = settings;
});
}

View File

@ -3,7 +3,7 @@ import angular from 'angular';
export class AdminEditOrgCtrl {
/** @ngInject */
constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
$scope.init = function() {
$scope.init = () => {
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
if ($routeParams.id) {
@ -12,34 +12,34 @@ export class AdminEditOrgCtrl {
}
};
$scope.getOrg = function(id) {
backendSrv.get('/api/orgs/' + id).then(function(org) {
$scope.getOrg = id => {
backendSrv.get('/api/orgs/' + id).then(org => {
$scope.org = org;
});
};
$scope.getOrgUsers = function(id) {
backendSrv.get('/api/orgs/' + id + '/users').then(function(orgUsers) {
$scope.getOrgUsers = id => {
backendSrv.get('/api/orgs/' + id + '/users').then(orgUsers => {
$scope.orgUsers = orgUsers;
});
};
$scope.update = function() {
$scope.update = () => {
if (!$scope.orgDetailsForm.$valid) {
return;
}
backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(function() {
backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(() => {
$location.path('/admin/orgs');
});
};
$scope.updateOrgUser = function(orgUser) {
$scope.updateOrgUser = orgUser => {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser);
};
$scope.removeOrgUser = function(orgUser) {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(function() {
$scope.removeOrgUser = orgUser => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(() => {
$scope.getOrgUsers($scope.org.id);
});
};

View File

@ -9,72 +9,72 @@ export class AdminEditUserCtrl {
$scope.permissions = {};
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
$scope.init = function() {
$scope.init = () => {
if ($routeParams.id) {
$scope.getUser($routeParams.id);
$scope.getUserOrgs($routeParams.id);
}
};
$scope.getUser = function(id) {
backendSrv.get('/api/users/' + id).then(function(user) {
$scope.getUser = id => {
backendSrv.get('/api/users/' + id).then(user => {
$scope.user = user;
$scope.user_id = id;
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
});
};
$scope.setPassword = function() {
$scope.setPassword = () => {
if (!$scope.passwordForm.$valid) {
return;
}
const payload = { password: $scope.password };
backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() {
backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(() => {
$location.path('/admin/users');
});
};
$scope.updatePermissions = function() {
$scope.updatePermissions = () => {
const payload = $scope.permissions;
backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() {
backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(() => {
$location.path('/admin/users');
});
};
$scope.create = function() {
$scope.create = () => {
if (!$scope.userForm.$valid) {
return;
}
backendSrv.post('/api/admin/users', $scope.user).then(function() {
backendSrv.post('/api/admin/users', $scope.user).then(() => {
$location.path('/admin/users');
});
};
$scope.getUserOrgs = function(id) {
backendSrv.get('/api/users/' + id + '/orgs').then(function(orgs) {
$scope.getUserOrgs = id => {
backendSrv.get('/api/users/' + id + '/orgs').then(orgs => {
$scope.orgs = orgs;
});
};
$scope.update = function() {
$scope.update = () => {
if (!$scope.userForm.$valid) {
return;
}
backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(function() {
backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(() => {
$location.path('/admin/users');
});
};
$scope.updateOrgUser = function(orgUser) {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(function() {});
$scope.updateOrgUser = orgUser => {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(() => {});
};
$scope.removeOrgUser = function(orgUser) {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() {
$scope.removeOrgUser = orgUser => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(() => {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
});
@ -82,19 +82,19 @@ export class AdminEditUserCtrl {
$scope.orgsSearchCache = [];
$scope.searchOrgs = function(queryStr, callback) {
$scope.searchOrgs = (queryStr, callback) => {
if ($scope.orgsSearchCache.length > 0) {
callback(_.map($scope.orgsSearchCache, 'name'));
return;
}
backendSrv.get('/api/orgs', { query: '' }).then(function(result) {
backendSrv.get('/api/orgs', { query: '' }).then(result => {
$scope.orgsSearchCache = result;
callback(_.map(result, 'name'));
});
};
$scope.addOrgUser = function() {
$scope.addOrgUser = () => {
if (!$scope.addOrgForm.$valid) {
return;
}
@ -108,7 +108,7 @@ export class AdminEditUserCtrl {
$scope.newOrg.loginOrEmail = $scope.user.login;
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() {
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(() => {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
});

View File

@ -3,26 +3,26 @@ import angular from 'angular';
export class AdminListOrgsCtrl {
/** @ngInject */
constructor($scope, backendSrv, navModelSrv) {
$scope.init = function() {
$scope.init = () => {
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
$scope.getOrgs();
};
$scope.getOrgs = function() {
backendSrv.get('/api/orgs').then(function(orgs) {
$scope.getOrgs = () => {
backendSrv.get('/api/orgs').then(orgs => {
$scope.orgs = orgs;
});
};
$scope.deleteOrg = function(org) {
$scope.deleteOrg = org => {
$scope.appEvent('confirm-modal', {
title: 'Delete',
text: 'Do you want to delete organization ' + org.name + '?',
text2: 'All dashboards for this organization will be removed!',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: function() {
backendSrv.delete('/api/orgs/' + org.id).then(function() {
onConfirm: () => {
backendSrv.delete('/api/orgs/' + org.id).then(() => {
$scope.getOrgs();
});
},

View File

@ -2,7 +2,7 @@ import '../annotations_srv';
import 'app/features/dashboard/time_srv';
import { AnnotationsSrv } from '../annotations_srv';
describe('AnnotationsSrv', function() {
describe('AnnotationsSrv', () => {
const $rootScope = {
onAppEvent: jest.fn(),
};

View File

@ -1,7 +1,7 @@
import { DashboardImportCtrl } from '../dashboard_import_ctrl';
import config from '../../../core/config';
describe('DashboardImportCtrl', function() {
describe('DashboardImportCtrl', () => {
const ctx: any = {};
let navModelSrv;
@ -26,8 +26,8 @@ describe('DashboardImportCtrl', function() {
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {});
});
describe('when uploading json', function() {
beforeEach(function() {
describe('when uploading json', () => {
beforeEach(() => {
config.datasources = {
ds: {
type: 'test-db',
@ -46,19 +46,19 @@ describe('DashboardImportCtrl', function() {
});
});
it('should build input model', function() {
it('should build input model', () => {
expect(ctx.ctrl.inputs.length).toBe(1);
expect(ctx.ctrl.inputs[0].name).toBe('ds');
expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source');
});
it('should set inputValid to false', function() {
it('should set inputValid to false', () => {
expect(ctx.ctrl.inputsValid).toBe(false);
});
});
describe('when specifying grafana.com url', function() {
beforeEach(function() {
describe('when specifying grafana.com url', () => {
beforeEach(() => {
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
// setup api mock
backendSrv.get = jest.fn(() => {
@ -69,13 +69,13 @@ describe('DashboardImportCtrl', function() {
return ctx.ctrl.checkGnetDashboard();
});
it('should call gnet api with correct dashboard id', function() {
it('should call gnet api with correct dashboard id', () => {
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123');
});
});
describe('when specifying dashboard id', function() {
beforeEach(function() {
describe('when specifying dashboard id', () => {
beforeEach(() => {
ctx.ctrl.gnetUrl = '2342';
// setup api mock
backendSrv.get = jest.fn(() => {
@ -86,7 +86,7 @@ describe('DashboardImportCtrl', function() {
return ctx.ctrl.checkGnetDashboard();
});
it('should call gnet api with correct dashboard id', function() {
it('should call gnet api with correct dashboard id', () => {
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342');
});
});

View File

@ -6,14 +6,14 @@ import { expect } from 'test/lib/common';
jest.mock('app/core/services/context_srv', () => ({}));
describe('DashboardModel', function() {
describe('when creating dashboard with old schema', function() {
describe('DashboardModel', () => {
describe('when creating dashboard with old schema', () => {
let model;
let graph;
let singlestat;
let table;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
services: {
filter: { time: { from: 'now-1d', to: 'now' }, list: [{}] },
@ -65,52 +65,52 @@ describe('DashboardModel', function() {
table = model.panels[2];
});
it('should have title', function() {
it('should have title', () => {
expect(model.title).toBe('No Title');
});
it('should have panel id', function() {
it('should have panel id', () => {
expect(graph.id).toBe(1);
});
it('should move time and filtering list', function() {
it('should move time and filtering list', () => {
expect(model.time.from).toBe('now-1d');
expect(model.templating.list[0].allFormat).toBe('glob');
});
it('graphite panel should change name too graph', function() {
it('graphite panel should change name too graph', () => {
expect(graph.type).toBe('graph');
});
it('single stat panel should have two thresholds', function() {
it('single stat panel should have two thresholds', () => {
expect(singlestat.thresholds).toBe('20,30');
});
it('queries without refId should get it', function() {
it('queries without refId should get it', () => {
expect(graph.targets[1].refId).toBe('B');
});
it('update legend setting', function() {
it('update legend setting', () => {
expect(graph.legend.show).toBe(true);
});
it('move aliasYAxis to series override', function() {
it('move aliasYAxis to series override', () => {
expect(graph.seriesOverrides[0].alias).toBe('test');
expect(graph.seriesOverrides[0].yaxis).toBe(2);
});
it('should move pulldowns to new schema', function() {
it('should move pulldowns to new schema', () => {
expect(model.annotations.list[1].name).toBe('old');
});
it('table panel should only have two thresholds values', function() {
it('table panel should only have two thresholds values', () => {
expect(table.styles[0].thresholds[0]).toBe('20');
expect(table.styles[0].thresholds[1]).toBe('30');
expect(table.styles[1].thresholds[0]).toBe('200');
expect(table.styles[1].thresholds[1]).toBe('300');
});
it('graph grid to yaxes options', function() {
it('graph grid to yaxes options', () => {
expect(graph.yaxes[0].min).toBe(1);
expect(graph.yaxes[0].max).toBe(10);
expect(graph.yaxes[0].format).toBe('kbyte');
@ -126,11 +126,11 @@ describe('DashboardModel', function() {
expect(graph.y_formats).toBe(undefined);
});
it('dashboard schema version should be set to latest', function() {
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(16);
});
it('graph thresholds should be migrated', function() {
it('graph thresholds should be migrated', () => {
expect(graph.thresholds.length).toBe(2);
expect(graph.thresholds[0].op).toBe('gt');
expect(graph.thresholds[0].value).toBe(200);
@ -140,16 +140,16 @@ describe('DashboardModel', function() {
});
});
describe('when migrating to the grid layout', function() {
describe('when migrating to the grid layout', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = {
rows: [],
};
});
it('should create proper grid', function() {
it('should create proper grid', () => {
model.rows = [createRow({ collapse: false, height: 8 }, [[6], [6]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -158,7 +158,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should add special "row" panel if row is collapsed', function() {
it('should add special "row" panel if row is collapsed', () => {
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -171,7 +171,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should add special "row" panel if row has visible title', function() {
it('should add special "row" panel if row has visible title', () => {
model.rows = [
createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]]),
createRow({ height: 8 }, [[12]]),
@ -189,7 +189,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should not add "row" panel if row has not visible title or not collapsed', function() {
it('should not add "row" panel if row has not visible title or not collapsed', () => {
model.rows = [
createRow({ collapse: true, height: 8 }, [[12]]),
createRow({ height: 8 }, [[12]]),
@ -212,7 +212,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should add all rows if even one collapsed or titled row is present', function() {
it('should add all rows if even one collapsed or titled row is present', () => {
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -225,7 +225,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should properly place panels with fixed height', function() {
it('should properly place panels with fixed height', () => {
model.rows = [
createRow({ height: 6 }, [[6], [6, 3], [6, 3]]),
createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]]),
@ -245,7 +245,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should place panel to the right side of panel having bigger height', function() {
it('should place panel to the right side of panel having bigger height', () => {
model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -260,7 +260,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should fill current row if it possible', function() {
it('should fill current row if it possible', () => {
model.rows = [createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -276,7 +276,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should fill current row if it possible (2)', function() {
it('should fill current row if it possible (2)', () => {
model.rows = [createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -292,7 +292,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should fill current row if panel height more than row height', function() {
it('should fill current row if panel height more than row height', () => {
model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -307,7 +307,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should wrap panels to multiple rows', function() {
it('should wrap panels to multiple rows', () => {
model.rows = [createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])];
const dashboard = new DashboardModel(model);
const panelGridPos = getGridPositions(dashboard);
@ -323,7 +323,7 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid);
});
it('should add repeated row if repeat set', function() {
it('should add repeated row if repeat set', () => {
model.rows = [
createRow({ showTitle: true, title: 'Row', height: 8, repeat: 'server' }, [[6]]),
createRow({ height: 8 }, [[12]]),
@ -344,7 +344,7 @@ describe('DashboardModel', function() {
expect(dashboard.panels[3].repeat).toBeUndefined();
});
it('should ignore repeated row', function() {
it('should ignore repeated row', () => {
model.rows = [
createRow({ showTitle: true, title: 'Row1', height: 8, repeat: 'server' }, [[6]]),
createRow(
@ -364,7 +364,7 @@ describe('DashboardModel', function() {
expect(dashboard.panels.length).toBe(2);
});
it('minSpan should be twice', function() {
it('minSpan should be twice', () => {
model.rows = [createRow({ height: 8 }, [[6]])];
model.rows[0].panels[0] = { minSpan: 12 };
@ -372,7 +372,7 @@ describe('DashboardModel', function() {
expect(dashboard.panels[0].minSpan).toBe(24);
});
it('should assign id', function() {
it('should assign id', () => {
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
model.rows[0].panels[0] = {};

View File

@ -4,43 +4,43 @@ import { PanelModel } from '../panel_model';
jest.mock('app/core/services/context_srv', () => ({}));
describe('DashboardModel', function() {
describe('when creating new dashboard model defaults only', function() {
describe('DashboardModel', () => {
describe('when creating new dashboard model defaults only', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({}, {});
});
it('should have title', function() {
it('should have title', () => {
expect(model.title).toBe('No Title');
});
it('should have meta', function() {
it('should have meta', () => {
expect(model.meta.canSave).toBe(true);
expect(model.meta.canShare).toBe(true);
});
it('should have default properties', function() {
it('should have default properties', () => {
expect(model.panels.length).toBe(0);
});
});
describe('when getting next panel id', function() {
describe('when getting next panel id', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
panels: [{ id: 5 }],
});
});
it('should return max id + 1', function() {
it('should return max id + 1', () => {
expect(model.getNextPanelId()).toBe(6);
});
});
describe('getSaveModelClone', function() {
describe('getSaveModelClone', () => {
it('should sort keys', () => {
const model = new DashboardModel({});
const saveModel = model.getSaveModelClone();
@ -68,20 +68,20 @@ describe('DashboardModel', function() {
});
});
describe('row and panel manipulation', function() {
describe('row and panel manipulation', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({});
});
it('adding panel should new up panel model', function() {
it('adding panel should new up panel model', () => {
dashboard.addPanel({ type: 'test', title: 'test' });
expect(dashboard.panels[0] instanceof PanelModel).toBe(true);
});
it('duplicate panel should try to add to the right if there is space', function() {
it('duplicate panel should try to add to the right if there is space', () => {
const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } };
dashboard.addPanel(panel);
@ -95,7 +95,7 @@ describe('DashboardModel', function() {
});
});
it('duplicate panel should remove repeat data', function() {
it('duplicate panel should remove repeat data', () => {
const panel = {
id: 10,
gridPos: { x: 0, y: 0, w: 6, h: 2 },
@ -111,29 +111,29 @@ describe('DashboardModel', function() {
});
});
describe('Given editable false dashboard', function() {
describe('Given editable false dashboard', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({ editable: false });
});
it('Should set meta canEdit and canSave to false', function() {
it('Should set meta canEdit and canSave to false', () => {
expect(model.meta.canSave).toBe(false);
expect(model.meta.canEdit).toBe(false);
});
it('getSaveModelClone should remove meta', function() {
it('getSaveModelClone should remove meta', () => {
const clone = model.getSaveModelClone();
expect(clone.meta).toBe(undefined);
});
});
describe('when loading dashboard with old influxdb query schema', function() {
describe('when loading dashboard with old influxdb query schema', () => {
let model;
let target;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
panels: [
{
@ -185,7 +185,7 @@ describe('DashboardModel', function() {
target = model.panels[0].targets[0];
});
it('should update query schema', function() {
it('should update query schema', () => {
expect(target.fields).toBe(undefined);
expect(target.select.length).toBe(2);
expect(target.select[0].length).toBe(4);
@ -196,10 +196,10 @@ describe('DashboardModel', function() {
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
describe('when creating dashboard model with missing list for annoations or templating', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
annotations: {
enable: true,
@ -210,54 +210,54 @@ describe('DashboardModel', function() {
});
});
it('should add empty list', function() {
it('should add empty list', () => {
expect(model.annotations.list.length).toBe(1);
expect(model.templating.list.length).toBe(0);
});
it('should add builtin annotation query', function() {
it('should add builtin annotation query', () => {
expect(model.annotations.list[0].builtIn).toBe(1);
expect(model.templating.list.length).toBe(0);
});
});
describe('Formatting epoch timestamp when timezone is set as utc', function() {
describe('Formatting epoch timestamp when timezone is set as utc', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({ timezone: 'utc' });
});
it('Should format timestamp with second resolution by default', function() {
it('Should format timestamp with second resolution by default', () => {
expect(dashboard.formatDate(1234567890000)).toBe('2009-02-13 23:31:30');
});
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
it('Should format timestamp with second resolution even if second format is passed as parameter', () => {
expect(dashboard.formatDate(1234567890007, 'YYYY-MM-DD HH:mm:ss')).toBe('2009-02-13 23:31:30');
});
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
it('Should format timestamp with millisecond resolution if format is passed as parameter', () => {
expect(dashboard.formatDate(1234567890007, 'YYYY-MM-DD HH:mm:ss.SSS')).toBe('2009-02-13 23:31:30.007');
});
});
describe('updateSubmenuVisibility with empty lists', function() {
describe('updateSubmenuVisibility with empty lists', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({});
model.updateSubmenuVisibility();
});
it('should not enable submmenu', function() {
it('should not enable submmenu', () => {
expect(model.meta.submenuEnabled).toBe(false);
});
});
describe('updateSubmenuVisibility with annotation', function() {
describe('updateSubmenuVisibility with annotation', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
annotations: {
list: [{}],
@ -266,15 +266,15 @@ describe('DashboardModel', function() {
model.updateSubmenuVisibility();
});
it('should enable submmenu', function() {
it('should enable submmenu', () => {
expect(model.meta.submenuEnabled).toBe(true);
});
});
describe('updateSubmenuVisibility with template var', function() {
describe('updateSubmenuVisibility with template var', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
templating: {
list: [{}],
@ -283,15 +283,15 @@ describe('DashboardModel', function() {
model.updateSubmenuVisibility();
});
it('should enable submmenu', function() {
it('should enable submmenu', () => {
expect(model.meta.submenuEnabled).toBe(true);
});
});
describe('updateSubmenuVisibility with hidden template var', function() {
describe('updateSubmenuVisibility with hidden template var', () => {
let model;
beforeEach(function() {
beforeEach(() => {
model = new DashboardModel({
templating: {
list: [{ hide: 2 }],
@ -300,15 +300,15 @@ describe('DashboardModel', function() {
model.updateSubmenuVisibility();
});
it('should not enable submmenu', function() {
it('should not enable submmenu', () => {
expect(model.meta.submenuEnabled).toBe(false);
});
});
describe('updateSubmenuVisibility with hidden annotation toggle', function() {
describe('updateSubmenuVisibility with hidden annotation toggle', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({
annotations: {
list: [{ hide: true }],
@ -317,15 +317,15 @@ describe('DashboardModel', function() {
dashboard.updateSubmenuVisibility();
});
it('should not enable submmenu', function() {
it('should not enable submmenu', () => {
expect(dashboard.meta.submenuEnabled).toBe(false);
});
});
describe('When collapsing row', function() {
describe('When collapsing row', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({
panels: [
{ id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 2 } },
@ -338,36 +338,36 @@ describe('DashboardModel', function() {
dashboard.toggleRow(dashboard.panels[1]);
});
it('should remove panels and put them inside collapsed row', function() {
it('should remove panels and put them inside collapsed row', () => {
expect(dashboard.panels.length).toBe(3);
expect(dashboard.panels[1].panels.length).toBe(2);
});
describe('and when removing row and its panels', function() {
beforeEach(function() {
describe('and when removing row and its panels', () => {
beforeEach(() => {
dashboard.removeRow(dashboard.panels[1], true);
});
it('should remove row and its panels', function() {
it('should remove row and its panels', () => {
expect(dashboard.panels.length).toBe(2);
});
});
describe('and when removing only the row', function() {
beforeEach(function() {
describe('and when removing only the row', () => {
beforeEach(() => {
dashboard.removeRow(dashboard.panels[1], false);
});
it('should only remove row', function() {
it('should only remove row', () => {
expect(dashboard.panels.length).toBe(4);
});
});
});
describe('When expanding row', function() {
describe('When expanding row', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({
panels: [
{ id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 6 } },
@ -387,16 +387,16 @@ describe('DashboardModel', function() {
dashboard.toggleRow(dashboard.panels[1]);
});
it('should add panels back', function() {
it('should add panels back', () => {
expect(dashboard.panels.length).toBe(5);
});
it('should add them below row in array', function() {
it('should add them below row in array', () => {
expect(dashboard.panels[2].id).toBe(3);
expect(dashboard.panels[3].id).toBe(4);
});
it('should position them below row', function() {
it('should position them below row', () => {
expect(dashboard.panels[2].gridPos).toMatchObject({
x: 0,
y: 7,
@ -405,7 +405,7 @@ describe('DashboardModel', function() {
});
});
it('should move panels below down', function() {
it('should move panels below down', () => {
expect(dashboard.panels[4].gridPos).toMatchObject({
x: 0,
y: 9,
@ -414,22 +414,22 @@ describe('DashboardModel', function() {
});
});
describe('and when removing row and its panels', function() {
beforeEach(function() {
describe('and when removing row and its panels', () => {
beforeEach(() => {
dashboard.removeRow(dashboard.panels[1], true);
});
it('should remove row and its panels', function() {
it('should remove row and its panels', () => {
expect(dashboard.panels.length).toBe(2);
});
});
describe('and when removing only the row', function() {
beforeEach(function() {
describe('and when removing only the row', () => {
beforeEach(() => {
dashboard.removeRow(dashboard.panels[1], false);
});
it('should only remove row', function() {
it('should only remove row', () => {
expect(dashboard.panels.length).toBe(4);
});
});

View File

@ -4,7 +4,7 @@ import { HistorySrv } from '../history/history_srv';
import { DashboardModel } from '../dashboard_model';
jest.mock('app/core/store');
describe('historySrv', function() {
describe('historySrv', () => {
const versionsResponse = versions();
const restoreResponse = restore;
@ -19,35 +19,35 @@ describe('historySrv', function() {
const emptyDash = new DashboardModel({});
const historyListOpts = { limit: 10, start: 0 };
describe('getHistoryList', function() {
it('should return a versions array for the given dashboard id', function() {
describe('getHistoryList', () => {
it('should return a versions array for the given dashboard id', () => {
backendSrv.get = jest.fn(() => Promise.resolve(versionsResponse));
historySrv = new HistorySrv(backendSrv);
return historySrv.getHistoryList(dash, historyListOpts).then(function(versions) {
return historySrv.getHistoryList(dash, historyListOpts).then(versions => {
expect(versions).toEqual(versionsResponse);
});
});
it('should return an empty array when not given an id', function() {
return historySrv.getHistoryList(emptyDash, historyListOpts).then(function(versions) {
it('should return an empty array when not given an id', () => {
return historySrv.getHistoryList(emptyDash, historyListOpts).then(versions => {
expect(versions).toEqual([]);
});
});
it('should return an empty array when not given a dashboard', function() {
return historySrv.getHistoryList(null, historyListOpts).then(function(versions) {
it('should return an empty array when not given a dashboard', () => {
return historySrv.getHistoryList(null, historyListOpts).then(versions => {
expect(versions).toEqual([]);
});
});
});
describe('restoreDashboard', () => {
it('should return a success response given valid parameters', function() {
it('should return a success response given valid parameters', () => {
const version = 6;
backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version)));
historySrv = new HistorySrv(backendSrv);
return historySrv.restoreDashboard(dash, version).then(function(response) {
return historySrv.restoreDashboard(dash, version).then(response => {
expect(response).toEqual(restoreResponse(version));
});
});

View File

@ -4,10 +4,10 @@ import { expect } from 'test/lib/common';
jest.mock('app/core/services/context_srv', () => ({}));
describe('given dashboard with panel repeat', function() {
describe('given dashboard with panel repeat', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
const dashboardJSON = {
panels: [
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
@ -35,7 +35,7 @@ describe('given dashboard with panel repeat', function() {
dashboard.processRepeats();
});
it('should repeat panels when row is expanding', function() {
it('should repeat panels when row is expanding', () => {
expect(dashboard.panels.length).toBe(4);
// toggle row
@ -55,10 +55,10 @@ describe('given dashboard with panel repeat', function() {
});
});
describe('given dashboard with panel repeat in horizontal direction', function() {
describe('given dashboard with panel repeat in horizontal direction', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({
panels: [
{
@ -89,22 +89,22 @@ describe('given dashboard with panel repeat in horizontal direction', function()
dashboard.processRepeats();
});
it('should repeat panel 3 times', function() {
it('should repeat panel 3 times', () => {
expect(dashboard.panels.length).toBe(3);
});
it('should mark panel repeated', function() {
it('should mark panel repeated', () => {
expect(dashboard.panels[0].repeat).toBe('apps');
expect(dashboard.panels[1].repeatPanelId).toBe(2);
});
it('should set scopedVars on panels', function() {
it('should set scopedVars on panels', () => {
expect(dashboard.panels[0].scopedVars.apps.value).toBe('se1');
expect(dashboard.panels[1].scopedVars.apps.value).toBe('se2');
expect(dashboard.panels[2].scopedVars.apps.value).toBe('se3');
});
it('should place on first row and adjust width so all fit', function() {
it('should place on first row and adjust width so all fit', () => {
expect(dashboard.panels[0].gridPos).toMatchObject({
x: 0,
y: 0,
@ -125,23 +125,23 @@ describe('given dashboard with panel repeat in horizontal direction', function()
});
});
describe('After a second iteration', function() {
beforeEach(function() {
describe('After a second iteration', () => {
beforeEach(() => {
dashboard.panels[0].fill = 10;
dashboard.processRepeats();
});
it('reused panel should copy properties from source', function() {
it('reused panel should copy properties from source', () => {
expect(dashboard.panels[1].fill).toBe(10);
});
it('should have same panel count', function() {
it('should have same panel count', () => {
expect(dashboard.panels.length).toBe(3);
});
});
describe('After a second iteration with different variable', function() {
beforeEach(function() {
describe('After a second iteration with different variable', () => {
beforeEach(() => {
dashboard.templating.list.push({
name: 'server',
current: { text: 'se1, se2, se3', value: ['se1'] },
@ -151,46 +151,46 @@ describe('given dashboard with panel repeat in horizontal direction', function()
dashboard.processRepeats();
});
it('should remove scopedVars value for last variable', function() {
it('should remove scopedVars value for last variable', () => {
expect(dashboard.panels[0].scopedVars.apps).toBe(undefined);
});
it('should have new variable value in scopedVars', function() {
it('should have new variable value in scopedVars', () => {
expect(dashboard.panels[0].scopedVars.server.value).toBe('se1');
});
});
describe('After a second iteration and selected values reduced', function() {
beforeEach(function() {
describe('After a second iteration and selected values reduced', () => {
beforeEach(() => {
dashboard.templating.list[0].options[1].selected = false;
dashboard.processRepeats();
});
it('should clean up repeated panel', function() {
it('should clean up repeated panel', () => {
expect(dashboard.panels.length).toBe(2);
});
});
describe('After a second iteration and panel repeat is turned off', function() {
beforeEach(function() {
describe('After a second iteration and panel repeat is turned off', () => {
beforeEach(() => {
dashboard.panels[0].repeat = null;
dashboard.processRepeats();
});
it('should clean up repeated panel', function() {
it('should clean up repeated panel', () => {
expect(dashboard.panels.length).toBe(1);
});
it('should remove scoped vars from reused panel', function() {
it('should remove scoped vars from reused panel', () => {
expect(dashboard.panels[0].scopedVars).toBe(undefined);
});
});
});
describe('given dashboard with panel repeat in vertical direction', function() {
describe('given dashboard with panel repeat in vertical direction', () => {
let dashboard;
beforeEach(function() {
beforeEach(() => {
dashboard = new DashboardModel({
panels: [
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
@ -218,7 +218,7 @@ describe('given dashboard with panel repeat in vertical direction', function() {
dashboard.processRepeats();
});
it('should place on items on top of each other and keep witdh', function() {
it('should place on items on top of each other and keep witdh', () => {
expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // first row
expect(dashboard.panels[1].gridPos).toMatchObject({ x: 5, y: 1, h: 2, w: 8 });
@ -290,7 +290,7 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi
]);
});
it('should be placed in their places', function() {
it('should be placed in their places', () => {
expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // 1st row
expect(dashboard.panels[1].gridPos).toMatchObject({ x: 0, y: 1, h: 2, w: 6 });
@ -311,10 +311,10 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi
});
});
describe('given dashboard with row repeat', function() {
describe('given dashboard with row repeat', () => {
let dashboard, dashboardJSON;
beforeEach(function() {
beforeEach(() => {
dashboardJSON = {
panels: [
{
@ -349,12 +349,12 @@ describe('given dashboard with row repeat', function() {
dashboard.processRepeats();
});
it('should not repeat only row', function() {
it('should not repeat only row', () => {
const panelTypes = _.map(dashboard.panels, 'type');
expect(panelTypes).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph', 'row', 'graph']);
});
it('should set scopedVars for each panel', function() {
it('should set scopedVars for each panel', () => {
dashboardJSON.templating.list[0].options[2].selected = true;
dashboard = new DashboardModel(dashboardJSON);
dashboard.processRepeats();
@ -375,12 +375,12 @@ describe('given dashboard with row repeat', function() {
expect(scopedVars).toEqual(['se1', 'se1', 'se1', 'se2', 'se2', 'se2', 'se3', 'se3', 'se3']);
});
it('should repeat only configured row', function() {
it('should repeat only configured row', () => {
expect(dashboard.panels[6].id).toBe(4);
expect(dashboard.panels[7].id).toBe(5);
});
it('should repeat only row if it is collapsed', function() {
it('should repeat only row if it is collapsed', () => {
dashboardJSON.panels = [
{
id: 1,
@ -405,7 +405,7 @@ describe('given dashboard with row repeat', function() {
expect(dashboard.panels[1].panels).toHaveLength(2);
});
it('should properly repeat multiple rows', function() {
it('should properly repeat multiple rows', () => {
dashboardJSON.panels = [
{
id: 1,
@ -469,7 +469,7 @@ describe('given dashboard with row repeat', function() {
expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02');
});
it('should assign unique ids for repeated panels', function() {
it('should assign unique ids for repeated panels', () => {
dashboardJSON.panels = [
{
id: 1,
@ -501,7 +501,7 @@ describe('given dashboard with row repeat', function() {
expect(panelIds.length).toEqual(_.uniq(panelIds).length);
});
it('should place new panels in proper order', function() {
it('should place new panels in proper order', () => {
dashboardJSON.panels = [
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 }, repeat: 'apps' },
{ id: 2, type: 'graph', gridPos: { x: 0, y: 1, h: 3, w: 12 } },
@ -646,7 +646,7 @@ describe('given dashboard with row and panel repeat', () => {
});
});
it('should repeat panels when row is expanding', function() {
it('should repeat panels when row is expanding', () => {
dashboard = new DashboardModel(dashboardJSON);
dashboard.processRepeats();

View File

@ -10,11 +10,11 @@ describe('saving dashboard as', () => {
};
const mockDashboardSrv = {
getCurrent: function() {
getCurrent: () => {
return {
id: 5,
meta: {},
getSaveModelClone: function() {
getSaveModelClone: () => {
return json;
},
};

View File

@ -7,11 +7,11 @@ describe('SaveProvisionedDashboardModalCtrl', () => {
};
const mockDashboardSrv = {
getCurrent: function() {
getCurrent: () => {
return {
id: 5,
meta: {},
getSaveModelClone: function() {
getSaveModelClone: () => {
return json;
},
};

View File

@ -136,7 +136,7 @@ describe('ShareModalCtrl', () => {
ctx.$location.absUrl = () => 'http://server/#!/test';
ctx.scope.options.includeTemplateVars = true;
ctx.templateSrv.fillVariableValuesForUrl = function(params) {
ctx.templateSrv.fillVariableValuesForUrl = params => {
params['var-app'] = 'mupp';
params['var-server'] = 'srv-01';
};

View File

@ -2,7 +2,7 @@ import { TimeSrv } from '../time_srv';
import '../time_srv';
import moment from 'moment';
describe('timeSrv', function() {
describe('timeSrv', () => {
const rootScope = {
$on: jest.fn(),
onAppEvent: jest.fn(),
@ -26,20 +26,20 @@ describe('timeSrv', function() {
getTimezone: jest.fn(() => 'browser'),
};
beforeEach(function() {
beforeEach(() => {
timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() });
timeSrv.init(_dashboard);
});
describe('timeRange', function() {
it('should return unparsed when parse is false', function() {
describe('timeRange', () => {
it('should return unparsed when parse is false', () => {
timeSrv.setTime({ from: 'now', to: 'now-1h' });
const time = timeSrv.timeRange();
expect(time.raw.from).toBe('now');
expect(time.raw.to).toBe('now-1h');
});
it('should return parsed when parse is true', function() {
it('should return parsed when parse is true', () => {
timeSrv.setTime({ from: 'now', to: 'now-1h' });
const time = timeSrv.timeRange();
expect(moment.isMoment(time.from)).toBe(true);
@ -47,8 +47,8 @@ describe('timeSrv', function() {
});
});
describe('init time from url', function() {
it('should handle relative times', function() {
describe('init time from url', () => {
it('should handle relative times', () => {
location = {
search: jest.fn(() => ({
from: 'now-2d',
@ -63,7 +63,7 @@ describe('timeSrv', function() {
expect(time.raw.to).toBe('now');
});
it('should handle formatted dates', function() {
it('should handle formatted dates', () => {
location = {
search: jest.fn(() => ({
from: '20140410T052010',
@ -79,7 +79,7 @@ describe('timeSrv', function() {
expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime());
});
it('should handle formatted dates without time', function() {
it('should handle formatted dates without time', () => {
location = {
search: jest.fn(() => ({
from: '20140410',
@ -95,7 +95,7 @@ describe('timeSrv', function() {
expect(time.to.valueOf()).toEqual(new Date('2014-05-20T00:00:00Z').getTime());
});
it('should handle epochs', function() {
it('should handle epochs', () => {
location = {
search: jest.fn(() => ({
from: '1410337646373',
@ -111,7 +111,7 @@ describe('timeSrv', function() {
expect(time.to.valueOf()).toEqual(1410337665699);
});
it('should handle bad dates', function() {
it('should handle bad dates', () => {
location = {
search: jest.fn(() => ({
from: '20151126T00010%3C%2Fp%3E%3Cspan%20class',
@ -128,22 +128,22 @@ describe('timeSrv', function() {
});
});
describe('setTime', function() {
it('should return disable refresh if refresh is disabled for any range', function() {
describe('setTime', () => {
it('should return disable refresh if refresh is disabled for any range', () => {
_dashboard.refresh = false;
timeSrv.setTime({ from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.refresh).toBe(false);
});
it('should restore refresh for absolute time range', function() {
it('should restore refresh for absolute time range', () => {
_dashboard.refresh = '30s';
timeSrv.setTime({ from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.refresh).toBe('30s');
});
it('should restore refresh after relative time range is set', function() {
it('should restore refresh after relative time range is set', () => {
_dashboard.refresh = '10s';
timeSrv.setTime({
from: moment([2011, 1, 1]),
@ -154,7 +154,7 @@ describe('timeSrv', function() {
expect(_dashboard.refresh).toBe('10s');
});
it('should keep refresh after relative time range is changed and now delay exists', function() {
it('should keep refresh after relative time range is changed and now delay exists', () => {
_dashboard.refresh = '10s';
timeSrv.setTime({ from: 'now-1h', to: 'now-10s' });
expect(_dashboard.refresh).toBe('10s');

View File

@ -3,20 +3,20 @@ import angular from 'angular';
export class OrgDetailsCtrl {
/** @ngInject */
constructor($scope, $http, backendSrv, contextSrv, navModelSrv) {
$scope.init = function() {
$scope.init = () => {
$scope.getOrgInfo();
$scope.navModel = navModelSrv.getNav('cfg', 'org-settings', 0);
};
$scope.getOrgInfo = function() {
backendSrv.get('/api/org').then(function(org) {
$scope.getOrgInfo = () => {
backendSrv.get('/api/org').then(org => {
$scope.org = org;
$scope.address = org.address;
contextSrv.user.orgName = org.name;
});
};
$scope.update = function() {
$scope.update = () => {
if (!$scope.orgForm.$valid) {
return;
}
@ -24,7 +24,7 @@ export class OrgDetailsCtrl {
backendSrv.put('/api/org', data).then($scope.getOrgInfo);
};
$scope.updateAddress = function() {
$scope.updateAddress = () => {
if (!$scope.addressForm.$valid) {
return;
}

View File

@ -6,7 +6,7 @@ jest.mock('angular', () => {
return new AngularJSMock();
});
describe('linkSrv', function() {
describe('linkSrv', () => {
let linkSrv;
const templateSrvMock = {};
const timeSrvMock = {};
@ -15,24 +15,24 @@ describe('linkSrv', function() {
linkSrv = new LinkSrv(templateSrvMock, timeSrvMock);
});
describe('when appending query strings', function() {
it('add ? to URL if not present', function() {
describe('when appending query strings', () => {
it('add ? to URL if not present', () => {
const url = linkSrv.appendToQueryString('http://example.com', 'foo=bar');
expect(url).toBe('http://example.com?foo=bar');
});
it('do not add & to URL if ? is present but query string is empty', function() {
it('do not add & to URL if ? is present but query string is empty', () => {
const url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar');
expect(url).toBe('http://example.com?foo=bar');
});
it('add & to URL if query string is present', function() {
it('add & to URL if query string is present', () => {
const url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world');
expect(url).toBe('http://example.com?foo=bar&hello=world');
});
it('do not change the URL if there is nothing to append', function() {
_.each(['', undefined, null], function(toAppend) {
it('do not change the URL if there is nothing to append', () => {
_.each(['', undefined, null], toAppend => {
const url1 = linkSrv.appendToQueryString('http://example.com', toAppend);
expect(url1).toBe('http://example.com');

View File

@ -15,7 +15,7 @@ const templateSrv = {
],
};
describe('datasource_srv', function() {
describe('datasource_srv', () => {
const _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
describe('when loading explore sources', () => {

View File

@ -1,8 +1,8 @@
import { AdhocVariable } from '../adhoc_variable';
describe('AdhocVariable', function() {
describe('when serializing to url', function() {
it('should set return key value and op separated by pipe', function() {
describe('AdhocVariable', () => {
describe('when serializing to url', () => {
it('should set return key value and op separated by pipe', () => {
const variable = new AdhocVariable({
filters: [
{ key: 'key1', operator: '=', value: 'value1' },
@ -15,8 +15,8 @@ describe('AdhocVariable', function() {
});
});
describe('when deserializing from url', function() {
it('should restore filters', function() {
describe('when deserializing from url', () => {
it('should restore filters', () => {
const variable = new AdhocVariable({});
variable.setValueFromUrl(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']);

View File

@ -1,6 +1,6 @@
import { TemplateSrv } from '../template_srv';
describe('templateSrv', function() {
describe('templateSrv', () => {
let _templateSrv;
function initTemplateSrv(variables) {
@ -8,58 +8,58 @@ describe('templateSrv', function() {
_templateSrv.init(variables);
}
describe('init', function() {
beforeEach(function() {
describe('init', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should initialize template data', function() {
it('should initialize template data', () => {
const target = _templateSrv.replace('this.[[test]].filters');
expect(target).toBe('this.oogle.filters');
});
});
describe('replace can pass scoped vars', function() {
beforeEach(function() {
describe('replace can pass scoped vars', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should replace $test with scoped value', function() {
it('should replace $test with scoped value', () => {
const target = _templateSrv.replace('this.$test.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.mupp.filters');
});
it('should replace ${test} with scoped value', function() {
it('should replace ${test} with scoped value', () => {
const target = _templateSrv.replace('this.${test}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.mupp.filters');
});
it('should replace ${test:glob} with scoped value', function() {
it('should replace ${test:glob} with scoped value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.mupp.filters');
});
it('should replace $test with scoped text', function() {
it('should replace $test with scoped text', () => {
const target = _templateSrv.replaceWithText('this.$test.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.asd.filters');
});
it('should replace ${test} with scoped text', function() {
it('should replace ${test} with scoped text', () => {
const target = _templateSrv.replaceWithText('this.${test}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.asd.filters');
});
it('should replace ${test:glob} with scoped text', function() {
it('should replace ${test:glob} with scoped text', () => {
const target = _templateSrv.replaceWithText('this.${test:glob}.filters', {
test: { value: 'mupp', text: 'asd' },
});
@ -67,8 +67,8 @@ describe('templateSrv', function() {
});
});
describe('getAdhocFilters', function() {
beforeEach(function() {
describe('getAdhocFilters', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'datasource',
@ -80,24 +80,24 @@ describe('templateSrv', function() {
]);
});
it('should return filters if datasourceName match', function() {
it('should return filters if datasourceName match', () => {
const filters = _templateSrv.getAdhocFilters('oogle');
expect(filters).toMatchObject([1]);
});
it('should return empty array if datasourceName does not match', function() {
it('should return empty array if datasourceName does not match', () => {
const filters = _templateSrv.getAdhocFilters('oogleasdasd');
expect(filters).toMatchObject([]);
});
it('should return filters when datasourceName match via data source variable', function() {
it('should return filters when datasourceName match via data source variable', () => {
const filters = _templateSrv.getAdhocFilters('logstash');
expect(filters).toMatchObject([2]);
});
});
describe('replace can pass multi / all format', function() {
beforeEach(function() {
describe('replace can pass multi / all format', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
@ -107,44 +107,44 @@ describe('templateSrv', function() {
]);
});
it('should replace $test with globbed value', function() {
it('should replace $test with globbed value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test} with globbed value', function() {
it('should replace ${test} with globbed value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test:glob} with globbed value', function() {
it('should replace ${test:glob} with globbed value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {});
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace $test with piped value', function() {
it('should replace $test with piped value', () => {
const target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).toBe('this=value1|value2');
});
it('should replace ${test} with piped value', function() {
it('should replace ${test} with piped value', () => {
const target = _templateSrv.replace('this=${test}', {}, 'pipe');
expect(target).toBe('this=value1|value2');
});
it('should replace ${test:pipe} with piped value', function() {
it('should replace ${test:pipe} with piped value', () => {
const target = _templateSrv.replace('this=${test:pipe}', {});
expect(target).toBe('this=value1|value2');
});
it('should replace ${test:pipe} with piped value and $test with globbed value', function() {
it('should replace ${test:pipe} with piped value and $test with globbed value', () => {
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
expect(target).toBe('value1|value2,{value1,value2}');
});
});
describe('variable with all option', function() {
beforeEach(function() {
describe('variable with all option', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
@ -155,29 +155,29 @@ describe('templateSrv', function() {
]);
});
it('should replace $test with formatted all value', function() {
it('should replace $test with formatted all value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test} with formatted all value', function() {
it('should replace ${test} with formatted all value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test:glob} with formatted all value', function() {
it('should replace ${test:glob} with formatted all value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {});
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test:pipe} with piped value and $test with globbed value', function() {
it('should replace ${test:pipe} with piped value and $test with globbed value', () => {
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
expect(target).toBe('value1|value2,{value1,value2}');
});
});
describe('variable with all option and custom value', function() {
beforeEach(function() {
describe('variable with all option and custom value', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
@ -189,148 +189,148 @@ describe('templateSrv', function() {
]);
});
it('should replace $test with formatted all value', function() {
it('should replace $test with formatted all value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.*.filters');
});
it('should replace ${test} with formatted all value', function() {
it('should replace ${test} with formatted all value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.*.filters');
});
it('should replace ${test:glob} with formatted all value', function() {
it('should replace ${test:glob} with formatted all value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {});
expect(target).toBe('this.*.filters');
});
it('should not escape custom all value', function() {
it('should not escape custom all value', () => {
const target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).toBe('this.*');
});
});
describe('lucene format', function() {
it('should properly escape $test with lucene escape sequences', function() {
describe('lucene format', () => {
it('should properly escape $test with lucene escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).toBe('this:value\\/4');
});
it('should properly escape ${test} with lucene escape sequences', function() {
it('should properly escape ${test} with lucene escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:${test}', {}, 'lucene');
expect(target).toBe('this:value\\/4');
});
it('should properly escape ${test:lucene} with lucene escape sequences', function() {
it('should properly escape ${test:lucene} with lucene escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:${test:lucene}', {});
expect(target).toBe('this:value\\/4');
});
});
describe('format variable to string values', function() {
it('single value should return value', function() {
describe('format variable to string values', () => {
it('single value should return value', () => {
const result = _templateSrv.formatValue('test');
expect(result).toBe('test');
});
it('multi value and glob format should render glob string', function() {
it('multi value and glob format should render glob string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'glob');
expect(result).toBe('{test,test2}');
});
it('multi value and lucene should render as lucene expr', function() {
it('multi value and lucene should render as lucene expr', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'lucene');
expect(result).toBe('("test" OR "test2")');
});
it('multi value and regex format should render regex string', function() {
it('multi value and regex format should render regex string', () => {
const result = _templateSrv.formatValue(['test.', 'test2'], 'regex');
expect(result).toBe('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', function() {
it('multi value and pipe should render pipe string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'pipe');
expect(result).toBe('test|test2');
});
it('multi value and distributed should render distributed string', function() {
it('multi value and distributed should render distributed string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', {
name: 'build',
});
expect(result).toBe('test,build=test2');
});
it('multi value and distributed should render when not string', function() {
it('multi value and distributed should render when not string', () => {
const result = _templateSrv.formatValue(['test'], 'distributed', {
name: 'build',
});
expect(result).toBe('test');
});
it('multi value and csv format should render csv string', function() {
it('multi value and csv format should render csv string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'csv');
expect(result).toBe('test,test2');
});
it('multi value and percentencode format should render percent-encoded string', function() {
it('multi value and percentencode format should render percent-encoded string', () => {
const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode');
expect(result).toBe('%7bfoo%28%29bar%20BAZ%2ctest2%7d');
});
it('slash should be properly escaped in regex format', function() {
it('slash should be properly escaped in regex format', () => {
const result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).toBe('Gi3\\/14');
});
});
describe('can check if variable exists', function() {
beforeEach(function() {
describe('can check if variable exists', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if exists', function() {
it('should return true if exists', () => {
const result = _templateSrv.variableExists('$test');
expect(result).toBe(true);
});
});
describe('can highlight variables in string', function() {
beforeEach(function() {
describe('can highlight variables in string', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', function() {
it('should insert html', () => {
const result = _templateSrv.highlightVariablesAsHtml('$test');
expect(result).toBe('<span class="template-variable">$test</span>');
});
it('should insert html anywhere in string', function() {
it('should insert html anywhere in string', () => {
const result = _templateSrv.highlightVariablesAsHtml('this $test ok');
expect(result).toBe('this <span class="template-variable">$test</span> ok');
});
it('should ignore if variables does not exist', function() {
it('should ignore if variables does not exist', () => {
const result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).toBe('this $google ok');
});
});
describe('updateTemplateData with simple value', function() {
beforeEach(function() {
describe('updateTemplateData with simple value', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', function() {
it('should set current value and update template data', () => {
const target = _templateSrv.replace('this.[[test]].filters');
expect(target).toBe('this.muuuu.filters');
});
});
describe('fillVariableValuesForUrl with multi value', function() {
beforeEach(function() {
describe('fillVariableValuesForUrl with multi value', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
@ -343,15 +343,15 @@ describe('templateSrv', function() {
]);
});
it('should set multiple url params', function() {
it('should set multiple url params', () => {
const params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).toMatchObject(['val1', 'val2']);
});
});
describe('fillVariableValuesForUrl skip url sync', function() {
beforeEach(function() {
describe('fillVariableValuesForUrl skip url sync', () => {
beforeEach(() => {
initTemplateSrv([
{
name: 'test',
@ -364,15 +364,15 @@ describe('templateSrv', function() {
]);
});
it('should not include template variable value in url', function() {
it('should not include template variable value in url', () => {
const params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).toBe(undefined);
});
});
describe('fillVariableValuesForUrl with multi value with skip url sync', function() {
beforeEach(function() {
describe('fillVariableValuesForUrl with multi value with skip url sync', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
@ -386,19 +386,19 @@ describe('templateSrv', function() {
]);
});
it('should not include template variable value in url', function() {
it('should not include template variable value in url', () => {
const params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).toBe(undefined);
});
});
describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
beforeEach(function() {
describe('fillVariableValuesForUrl with multi value and scopedVars', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
});
it('should set scoped value as url params', function() {
it('should set scoped value as url params', () => {
const params = {};
_templateSrv.fillVariableValuesForUrl(params, {
test: { value: 'val1' },
@ -407,12 +407,12 @@ describe('templateSrv', function() {
});
});
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', function() {
beforeEach(function() {
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
});
it('should not set scoped value as url params', function() {
it('should not set scoped value as url params', () => {
const params = {};
_templateSrv.fillVariableValuesForUrl(params, {
test: { name: 'test', value: 'val1', skipUrlSync: true },
@ -421,8 +421,8 @@ describe('templateSrv', function() {
});
});
describe('replaceWithText', function() {
beforeEach(function() {
describe('replaceWithText', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
@ -439,18 +439,18 @@ describe('templateSrv', function() {
_templateSrv.updateTemplateData();
});
it('should replace with text except for grafanaVariables', function() {
it('should replace with text except for grafanaVariables', () => {
const target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).toBe('Server: All, period: 13m');
});
});
describe('built in interval variables', function() {
beforeEach(function() {
describe('built in interval variables', () => {
beforeEach(() => {
initTemplateSrv([]);
});
it('should replace $__interval_ms with interval milliseconds', function() {
it('should replace $__interval_ms with interval milliseconds', () => {
const target = _templateSrv.replace('10 * $__interval_ms', {
__interval_ms: { text: '100', value: '100' },
});

View File

@ -1,53 +1,53 @@
import { containsVariable, assignModelProperties } from '../variable';
describe('containsVariable', function() {
describe('when checking if a string contains a variable', function() {
it('should find it with $const syntax', function() {
describe('containsVariable', () => {
describe('when checking if a string contains a variable', () => {
it('should find it with $const syntax', () => {
const contains = containsVariable('this.$test.filters', 'test');
expect(contains).toBe(true);
});
it('should not find it if only part matches with $const syntax', function() {
it('should not find it if only part matches with $const syntax', () => {
const contains = containsVariable('this.$serverDomain.filters', 'server');
expect(contains).toBe(false);
});
it('should find it if it ends with variable and passing multiple test strings', function() {
it('should find it if it ends with variable and passing multiple test strings', () => {
const contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric');
expect(contains).toBe(true);
});
it('should find it with [[var]] syntax', function() {
it('should find it with [[var]] syntax', () => {
const contains = containsVariable('this.[[test]].filters', 'test');
expect(contains).toBe(true);
});
it('should find it when part of segment', function() {
it('should find it when part of segment', () => {
const contains = containsVariable('metrics.$env.$group-*', 'group');
expect(contains).toBe(true);
});
it('should find it its the only thing', function() {
it('should find it its the only thing', () => {
const contains = containsVariable('$env', 'env');
expect(contains).toBe(true);
});
it('should be able to pass in multiple test strings', function() {
it('should be able to pass in multiple test strings', () => {
const contains = containsVariable('asd', 'asd2.$env', 'env');
expect(contains).toBe(true);
});
});
});
describe('assignModelProperties', function() {
it('only set properties defined in defaults', function() {
describe('assignModelProperties', () => {
it('only set properties defined in defaults', () => {
const target: any = { test: 'asd' };
assignModelProperties(target, { propA: 1, propB: 2 }, { propB: 0 });
expect(target.propB).toBe(2);
expect(target.test).toBe('asd');
});
it('use default value if not found on source', function() {
it('use default value if not found on source', () => {
const target: any = { test: 'asd' };
assignModelProperties(target, { propA: 1, propB: 2 }, { propC: 10 });
expect(target.propC).toBe(10);

View File

@ -34,7 +34,7 @@ describe('VariableSrv', function(this: any) {
function describeUpdateVariable(desc, fn) {
describe(desc, () => {
const scenario: any = {};
scenario.setup = function(setupFn) {
scenario.setup = setupFn => {
scenario.setupFn = setupFn;
};
@ -135,7 +135,7 @@ describe('VariableSrv', function(this: any) {
//
// Query variable update
//
describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
describeUpdateVariable('query variable with empty current object and refresh', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -154,7 +154,7 @@ describe('VariableSrv', function(this: any) {
describeUpdateVariable(
'query variable with multi select and new options does not contain some selected values',
function(scenario) {
scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -177,7 +177,7 @@ describe('VariableSrv', function(this: any) {
describeUpdateVariable(
'query variable with multi select and new options does not contain any selected values',
function(scenario) {
scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -198,7 +198,7 @@ describe('VariableSrv', function(this: any) {
}
);
describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
describeUpdateVariable('query variable with multi select and $__all selected', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -219,7 +219,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('query variable with numeric results', function(scenario) {
describeUpdateVariable('query variable with numeric results', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -237,7 +237,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('basic query variable', function(scenario) {
describeUpdateVariable('basic query variable', scenario => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
@ -255,7 +255,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('and existing value still exists in options', function(scenario) {
describeUpdateVariable('and existing value still exists in options', scenario => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.current = { value: 'backend2', text: 'backend2' };
@ -267,7 +267,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('and regex pattern exists', function(scenario) {
describeUpdateVariable('and regex pattern exists', scenario => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
@ -282,7 +282,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
describeUpdateVariable('and regex pattern exists and no match', scenario => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
@ -298,7 +298,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('regex pattern without slashes', function(scenario) {
describeUpdateVariable('regex pattern without slashes', scenario => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = 'backend_01';
@ -313,7 +313,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
describeUpdateVariable('regex pattern remove duplicates', scenario => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = '/backend_01/';
@ -328,7 +328,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('with include All', function(scenario) {
describeUpdateVariable('with include All', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -345,7 +345,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('with include all and custom value', function(scenario) {
describeUpdateVariable('with include all and custom value', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -362,7 +362,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('without sort', function(scenario) {
describeUpdateVariable('without sort', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -380,7 +380,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
describeUpdateVariable('with alphabetical sort (asc)', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -398,7 +398,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
describeUpdateVariable('with alphabetical sort (desc)', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -416,7 +416,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('with numerical sort (asc)', function(scenario) {
describeUpdateVariable('with numerical sort (asc)', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -434,7 +434,7 @@ describe('VariableSrv', function(this: any) {
});
});
describeUpdateVariable('with numerical sort (desc)', function(scenario) {
describeUpdateVariable('with numerical sort (desc)', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
@ -455,7 +455,7 @@ describe('VariableSrv', function(this: any) {
//
// datasource variable update
//
describeUpdateVariable('datasource variable with regex filter', function(scenario) {
describeUpdateVariable('datasource variable with regex filter', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'datasource',
@ -486,7 +486,7 @@ describe('VariableSrv', function(this: any) {
//
// Custom variable update
//
describeUpdateVariable('update custom variable', function(scenario) {
describeUpdateVariable('update custom variable', scenario => {
scenario.setup(() => {
scenario.variableModel = {
type: 'custom',

View File

@ -122,12 +122,13 @@ export class VariableSrv {
}
const g = this.createGraph();
const promises = g
.getNode(variable.name)
.getOptimizedInputEdges()
.map(e => {
const node = g.getNode(variable.name);
let promises = [];
if (node) {
promises = node.getOptimizedInputEdges().map(e => {
return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
});
}
return this.$q.all(promises).then(() => {
if (emitChangeEvents) {

View File

@ -3,7 +3,7 @@ import CloudWatchDatasource from '../datasource';
import * as dateMath from 'app/core/utils/datemath';
import _ from 'lodash';
describe('CloudWatchDatasource', function() {
describe('CloudWatchDatasource', () => {
const instanceSettings = {
jsonData: { defaultRegion: 'us-east-1', access: 'proxy' },
};
@ -34,7 +34,7 @@ describe('CloudWatchDatasource', function() {
ctx.ds = new CloudWatchDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv);
});
describe('When performing CloudWatch query', function() {
describe('When performing CloudWatch query', () => {
let requestParams;
const query = {
@ -80,8 +80,8 @@ describe('CloudWatchDatasource', function() {
});
});
it('should generate the correct query', function(done) {
ctx.ds.query(query).then(function() {
it('should generate the correct query', done => {
ctx.ds.query(query).then(() => {
const params = requestParams.queries[0];
expect(params.namespace).toBe(query.targets[0].namespace);
expect(params.metricName).toBe(query.targets[0].metricName);
@ -92,7 +92,7 @@ describe('CloudWatchDatasource', function() {
});
});
it('should generate the correct query with interval variable', function(done) {
it('should generate the correct query with interval variable', done => {
ctx.templateSrv.data = {
period: '10m',
};
@ -114,14 +114,14 @@ describe('CloudWatchDatasource', function() {
],
};
ctx.ds.query(query).then(function() {
ctx.ds.query(query).then(() => {
const params = requestParams.queries[0];
expect(params.period).toBe('600');
done();
});
});
it('should cancel query for invalid extended statistics', function() {
it('should cancel query for invalid extended statistics', () => {
const query = {
range: { from: 'now-1h', to: 'now' },
rangeRaw: { from: 1483228800, to: 1483232400 },
@ -141,8 +141,8 @@ describe('CloudWatchDatasource', function() {
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) {
it('should return series list', done => {
ctx.ds.query(query).then(result => {
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();
@ -150,8 +150,8 @@ describe('CloudWatchDatasource', function() {
});
});
describe('When query region is "default"', function() {
it('should return the datasource region if empty or "default"', function() {
describe('When query region is "default"', () => {
it('should return the datasource region if empty or "default"', () => {
const defaultRegion = instanceSettings.jsonData.defaultRegion;
expect(ctx.ds.getActualRegion()).toBe(defaultRegion);
@ -159,19 +159,19 @@ describe('CloudWatchDatasource', function() {
expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion);
});
it('should return the specified region if specified', function() {
it('should return the specified region if specified', () => {
expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
});
let requestParams;
beforeEach(function() {
beforeEach(() => {
ctx.ds.performTimeSeriesQuery = jest.fn(request => {
requestParams = request;
return Promise.resolve({ data: {} });
});
});
it('should query for the datasource region if empty or "default"', function(done) {
it('should query for the datasource region if empty or "default"', done => {
const query = {
range: { from: 'now-1h', to: 'now' },
rangeRaw: { from: 1483228800, to: 1483232400 },
@ -189,14 +189,14 @@ describe('CloudWatchDatasource', function() {
],
};
ctx.ds.query(query).then(function(result) {
ctx.ds.query(query).then(result => {
expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion);
done();
});
});
});
describe('When performing CloudWatch query for extended statistics', function() {
describe('When performing CloudWatch query for extended statistics', () => {
const query = {
range: { from: 'now-1h', to: 'now' },
rangeRaw: { from: 1483228800, to: 1483232400 },
@ -235,14 +235,14 @@ describe('CloudWatchDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
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) {
it('should return series list', done => {
ctx.ds.query(query).then(result => {
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();
@ -378,7 +378,7 @@ describe('CloudWatchDatasource', function() {
});
});
it('should caclculate the correct period', function() {
it('should caclculate the correct period', () => {
const hourSec = 60 * 60;
const daySec = hourSec * 24;
const start = 1483196400 * 1000;

View File

@ -43,8 +43,8 @@ describe('ElasticDatasource', function(this: any) {
ctx.ds = new ElasticDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv);
}
describe('When testing datasource with index pattern', function() {
beforeEach(function() {
describe('When testing datasource with index pattern', () => {
beforeEach(() => {
createDatasource({
url: 'http://es.com',
index: '[asd-]YYYY.MM.DD',
@ -52,7 +52,7 @@ describe('ElasticDatasource', function(this: any) {
});
});
it('should translate index pattern to current day', function() {
it('should translate index pattern to current day', () => {
let requestOptions;
ctx.backendSrv.datasourceRequest = jest.fn(options => {
requestOptions = options;
@ -66,7 +66,7 @@ describe('ElasticDatasource', function(this: any) {
});
});
describe('When issuing metric query with interval pattern', function() {
describe('When issuing metric query with interval pattern', () => {
let requestOptions, parts, header;
beforeEach(() => {
@ -99,20 +99,20 @@ describe('ElasticDatasource', function(this: any) {
header = angular.fromJson(parts[0]);
});
it('should translate index pattern to current day', function() {
it('should translate index pattern to current day', () => {
expect(header.index).toEqual(['asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']);
});
it('should json escape lucene query', function() {
it('should json escape lucene query', () => {
const body = angular.fromJson(parts[1]);
expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test');
});
});
describe('When issuing document query', function() {
describe('When issuing document query', () => {
let requestOptions, parts, header;
beforeEach(function() {
beforeEach(() => {
createDatasource({
url: 'http://es.com',
index: 'test',
@ -142,17 +142,17 @@ describe('ElasticDatasource', function(this: any) {
header = angular.fromJson(parts[0]);
});
it('should set search type to query_then_fetch', function() {
it('should set search type to query_then_fetch', () => {
expect(header.search_type).toEqual('query_then_fetch');
});
it('should set size', function() {
it('should set size', () => {
const body = angular.fromJson(parts[1]);
expect(body.size).toBe(500);
});
});
describe('When getting fields', function() {
describe('When getting fields', () => {
beforeEach(() => {
createDatasource({ url: 'http://es.com', index: 'metricbeat' });
@ -203,7 +203,7 @@ describe('ElasticDatasource', function(this: any) {
});
});
it('should return nested fields', function() {
it('should return nested fields', () => {
ctx.ds
.getFields({
find: 'fields',
@ -224,7 +224,7 @@ describe('ElasticDatasource', function(this: any) {
});
});
it('should return fields related to query type', function() {
it('should return fields related to query type', () => {
ctx.ds
.getFields({
find: 'fields',
@ -249,10 +249,10 @@ describe('ElasticDatasource', function(this: any) {
});
});
describe('When issuing aggregation query on es5.x', function() {
describe('When issuing aggregation query on es5.x', () => {
let requestOptions, parts, header;
beforeEach(function() {
beforeEach(() => {
createDatasource({
url: 'http://es.com',
index: 'test',
@ -282,17 +282,17 @@ describe('ElasticDatasource', function(this: any) {
header = angular.fromJson(parts[0]);
});
it('should not set search type to count', function() {
it('should not set search type to count', () => {
expect(header.search_type).not.toEqual('count');
});
it('should set size to 0', function() {
it('should set size to 0', () => {
const body = angular.fromJson(parts[1]);
expect(body.size).toBe(0);
});
});
describe('When issuing metricFind query on es5.x', function() {
describe('When issuing metricFind query on es5.x', () => {
let requestOptions, parts, header, body, results;
beforeEach(() => {

View File

@ -12,12 +12,12 @@ describe('graphiteDatasource', () => {
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
};
beforeEach(function() {
beforeEach(() => {
ctx.instanceSettings.url = '/api/datasources/proxy/1';
ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
});
describe('When querying graphite with one target using query editor target spec', function() {
describe('When querying graphite with one target using query editor target spec', () => {
const query = {
panelId: 3,
dashboardId: 5,
@ -30,14 +30,14 @@ describe('graphiteDatasource', () => {
let requestOptions;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = function(options) {
ctx.backendSrv.datasourceRequest = options => {
requestOptions = options;
return ctx.$q.when({
data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }],
});
};
await ctx.ds.query(query).then(function(data) {
await ctx.ds.query(query).then(data => {
results = data;
});
});
@ -47,15 +47,15 @@ describe('graphiteDatasource', () => {
expect(requestOptions.headers['X-Panel-Id']).toBe(3);
});
it('should generate the correct query', function() {
it('should generate the correct query', () => {
expect(requestOptions.url).toBe('/api/datasources/proxy/1/render');
});
it('should set unique requestId', function() {
it('should set unique requestId', () => {
expect(requestOptions.requestId).toBe('graphiteProd.panelId.3');
});
it('should query correctly', function() {
it('should query correctly', () => {
const params = requestOptions.data.split('&');
expect(params).toContain('target=prod1.count');
expect(params).toContain('target=prod2.count');
@ -63,17 +63,17 @@ describe('graphiteDatasource', () => {
expect(params).toContain('until=now');
});
it('should exclude undefined params', function() {
it('should exclude undefined params', () => {
const params = requestOptions.data.split('&');
expect(params).not.toContain('cacheTimeout=undefined');
});
it('should return series list', function() {
it('should return series list', () => {
expect(results.data.length).toBe(1);
expect(results.data[0].target).toBe('prod1.count');
});
it('should convert to millisecond resolution', function() {
it('should convert to millisecond resolution', () => {
expect(results.data[0].datapoints[0][0]).toBe(10);
});
});
@ -106,11 +106,11 @@ describe('graphiteDatasource', () => {
};
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = function(options) {
ctx.backendSrv.datasourceRequest = options => {
return ctx.$q.when(response);
};
await ctx.ds.annotationQuery(options).then(function(data) {
await ctx.ds.annotationQuery(options).then(data => {
results = data;
});
});
@ -136,11 +136,11 @@ describe('graphiteDatasource', () => {
],
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = function(options) {
ctx.backendSrv.datasourceRequest = options => {
return ctx.$q.when(response);
};
ctx.ds.annotationQuery(options).then(function(data) {
ctx.ds.annotationQuery(options).then(data => {
results = data;
});
// ctx.$rootScope.$apply();
@ -155,29 +155,29 @@ describe('graphiteDatasource', () => {
});
});
describe('building graphite params', function() {
it('should return empty array if no targets', function() {
describe('building graphite params', () => {
it('should return empty array if no targets', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{}],
});
expect(results.length).toBe(0);
});
it('should uri escape targets', function() {
it('should uri escape targets', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }],
});
expect(results).toContain('target=prod1.%7Btest%2Ctest2%7D');
});
it('should replace target placeholder', function() {
it('should replace target placeholder', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }],
});
expect(results[2]).toBe('target=asPercent(series1%2Cseries2)');
});
it('should replace target placeholder for hidden series', function() {
it('should replace target placeholder for hidden series', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [
{ target: 'series1', hide: true },
@ -188,28 +188,28 @@ describe('graphiteDatasource', () => {
expect(results[0]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
});
it('should replace target placeholder when nesting query references', function() {
it('should replace target placeholder when nesting query references', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }],
});
expect(results[2]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
});
it('should fix wrong minute interval parameters', function() {
it('should fix wrong minute interval parameters', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }],
});
expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')"));
});
it('should fix wrong month interval parameters', function() {
it('should fix wrong month interval parameters', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }],
});
expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')"));
});
it('should ignore empty targets', function() {
it('should ignore empty targets', () => {
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: 'series1' }, { target: '' }],
});
@ -222,7 +222,7 @@ describe('graphiteDatasource', () => {
let requestOptions;
beforeEach(() => {
ctx.backendSrv.datasourceRequest = function(options) {
ctx.backendSrv.datasourceRequest = options => {
requestOptions = options;
return ctx.$q.when({
data: ['backend_01', 'backend_02'],
@ -307,7 +307,7 @@ describe('graphiteDatasource', () => {
});
function accessScenario(name, url, fn) {
describe('access scenario ' + name, function() {
describe('access scenario ' + name, () => {
const ctx: any = {
backendSrv: {},
$q: $q,
@ -332,12 +332,12 @@ function accessScenario(name, url, fn) {
});
}
accessScenario('with proxy access', '/api/datasources/proxy/1', function(httpOptions) {
accessScenario('with proxy access', '/api/datasources/proxy/1', httpOptions => {
expect(httpOptions.headers['X-Dashboard-Id']).toBe(1);
expect(httpOptions.headers['X-Panel-Id']).toBe(2);
});
accessScenario('with direct access', 'http://localhost:8080', function(httpOptions) {
accessScenario('with direct access', 'http://localhost:8080', httpOptions => {
expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined);
expect(httpOptions.headers['X-Panel-Id']).toBe(undefined);
});

View File

@ -1,7 +1,7 @@
import gfunc from '../gfunc';
describe('when creating func instance from func names', function() {
it('should return func instance', function() {
describe('when creating func instance from func names', () => {
it('should return func instance', () => {
const func = gfunc.createFuncInstance('sumSeries');
expect(func).toBeTruthy();
expect(func.def.name).toEqual('sumSeries');
@ -10,18 +10,18 @@ describe('when creating func instance from func names', function() {
expect(func.def.defaultParams.length).toEqual(1);
});
it('should return func instance with shortName', function() {
it('should return func instance with shortName', () => {
const func = gfunc.createFuncInstance('sum');
expect(func).toBeTruthy();
});
it('should return func instance from funcDef', function() {
it('should return func instance from funcDef', () => {
const func = gfunc.createFuncInstance('sum');
const func2 = gfunc.createFuncInstance(func.def);
expect(func2).toBeTruthy();
});
it('func instance should have text representation', function() {
it('func instance should have text representation', () => {
const func = gfunc.createFuncInstance('groupByNode');
func.params[0] = 5;
func.params[1] = 'avg';
@ -30,78 +30,78 @@ describe('when creating func instance from func names', function() {
});
});
describe('when rendering func instance', function() {
it('should handle single metric param', function() {
describe('when rendering func instance', () => {
it('should handle single metric param', () => {
const func = gfunc.createFuncInstance('sumSeries');
expect(func.render('hello.metric')).toEqual('sumSeries(hello.metric)');
});
it('should include default params if options enable it', function() {
it('should include default params if options enable it', () => {
const func = gfunc.createFuncInstance('scaleToSeconds', {
withDefaultParams: true,
});
expect(func.render('hello')).toEqual('scaleToSeconds(hello, 1)');
});
it('should handle int or interval params with number', function() {
it('should handle int or interval params with number', () => {
const func = gfunc.createFuncInstance('movingMedian');
func.params[0] = '5';
expect(func.render('hello')).toEqual('movingMedian(hello, 5)');
});
it('should handle int or interval params with interval string', function() {
it('should handle int or interval params with interval string', () => {
const func = gfunc.createFuncInstance('movingMedian');
func.params[0] = '5min';
expect(func.render('hello')).toEqual("movingMedian(hello, '5min')");
});
it('should never quote boolean paramater', function() {
it('should never quote boolean paramater', () => {
const func = gfunc.createFuncInstance('sortByName');
func.params[0] = '$natural';
expect(func.render('hello')).toEqual('sortByName(hello, $natural)');
});
it('should never quote int paramater', function() {
it('should never quote int paramater', () => {
const func = gfunc.createFuncInstance('maximumAbove');
func.params[0] = '$value';
expect(func.render('hello')).toEqual('maximumAbove(hello, $value)');
});
it('should never quote node paramater', function() {
it('should never quote node paramater', () => {
const func = gfunc.createFuncInstance('aliasByNode');
func.params[0] = '$node';
expect(func.render('hello')).toEqual('aliasByNode(hello, $node)');
});
it('should handle metric param and int param and string param', function() {
it('should handle metric param and int param and string param', () => {
const func = gfunc.createFuncInstance('groupByNode');
func.params[0] = 5;
func.params[1] = 'avg';
expect(func.render('hello.metric')).toEqual("groupByNode(hello.metric, 5, 'avg')");
});
it('should handle function with no metric param', function() {
it('should handle function with no metric param', () => {
const func = gfunc.createFuncInstance('randomWalk');
func.params[0] = 'test';
expect(func.render(undefined)).toEqual("randomWalk('test')");
});
it('should handle function multiple series params', function() {
it('should handle function multiple series params', () => {
const func = gfunc.createFuncInstance('asPercent');
func.params[0] = '#B';
expect(func.render('#A')).toEqual('asPercent(#A, #B)');
});
});
describe('when requesting function definitions', function() {
it('should return function definitions', function() {
describe('when requesting function definitions', () => {
it('should return function definitions', () => {
const funcIndex = gfunc.getFuncDefs('1.0');
expect(Object.keys(funcIndex).length).toBeGreaterThan(8);
});
});
describe('when updating func param', function() {
it('should update param value and update text representation', function() {
describe('when updating func param', () => {
it('should update param value and update text representation', () => {
const func = gfunc.createFuncInstance('summarize', {
withDefaultParams: true,
});
@ -110,21 +110,21 @@ describe('when updating func param', function() {
expect(func.text).toBe('summarize(1h, sum, false)');
});
it('should parse numbers as float', function() {
it('should parse numbers as float', () => {
const func = gfunc.createFuncInstance('scale');
func.updateParam('0.001', 0);
expect(func.params[0]).toBe('0.001');
});
});
describe('when updating func param with optional second parameter', function() {
it('should update value and text', function() {
describe('when updating func param with optional second parameter', () => {
it('should update value and text', () => {
const func = gfunc.createFuncInstance('aliasByNode');
func.updateParam('1', 0);
expect(func.params[0]).toBe('1');
});
it('should slit text and put value in second param', function() {
it('should slit text and put value in second param', () => {
const func = gfunc.createFuncInstance('aliasByNode');
func.updateParam('4,-5', 0);
expect(func.params[0]).toBe('4');
@ -132,7 +132,7 @@ describe('when updating func param with optional second parameter', function() {
expect(func.text).toBe('aliasByNode(4, -5)');
});
it('should remove second param when empty string is set', function() {
it('should remove second param when empty string is set', () => {
const func = gfunc.createFuncInstance('aliasByNode');
func.updateParam('4,-5', 0);
func.updateParam('', 1);

View File

@ -1,7 +1,7 @@
import { Lexer } from '../lexer';
describe('when lexing graphite expression', function() {
it('should tokenize metric expression', function() {
describe('when lexing graphite expression', () => {
it('should tokenize metric expression', () => {
const lexer = new Lexer('metric.test.*.asd.count');
const tokens = lexer.tokenize();
expect(tokens[0].value).toBe('metric');
@ -11,27 +11,27 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].pos).toBe(13);
});
it('should tokenize metric expression with dash', function() {
it('should tokenize metric expression with dash', () => {
const lexer = new Lexer('metric.test.se1-server-*.asd.count');
const tokens = lexer.tokenize();
expect(tokens[4].type).toBe('identifier');
expect(tokens[4].value).toBe('se1-server-*');
});
it('should tokenize metric expression with dash2', function() {
it('should tokenize metric expression with dash2', () => {
const lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*');
const tokens = lexer.tokenize();
expect(tokens[0].value).toBe('net');
expect(tokens[2].value).toBe('192-168-1-1');
});
it('should tokenize metric expression with equal sign', function() {
it('should tokenize metric expression with equal sign', () => {
const lexer = new Lexer('apps=test');
const tokens = lexer.tokenize();
expect(tokens[0].value).toBe('apps=test');
});
it('simple function2', function() {
it('simple function2', () => {
const lexer = new Lexer('offset(test.metric, -100)');
const tokens = lexer.tokenize();
expect(tokens[2].type).toBe('identifier');
@ -39,7 +39,7 @@ describe('when lexing graphite expression', function() {
expect(tokens[6].type).toBe('number');
});
it('should tokenize metric expression with curly braces', function() {
it('should tokenize metric expression with curly braces', () => {
const lexer = new Lexer('metric.se1-{first, second}.count');
const tokens = lexer.tokenize();
expect(tokens.length).toBe(10);
@ -49,7 +49,7 @@ describe('when lexing graphite expression', function() {
expect(tokens[6].value).toBe('second');
});
it('should tokenize metric expression with number segments', function() {
it('should tokenize metric expression with number segments', () => {
const lexer = new Lexer('metric.10.12_10.test');
const tokens = lexer.tokenize();
expect(tokens[0].type).toBe('identifier');
@ -59,7 +59,7 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].type).toBe('identifier');
});
it('should tokenize metric expression with segment that start with number', function() {
it('should tokenize metric expression with segment that start with number', () => {
const lexer = new Lexer('metric.001-server');
const tokens = lexer.tokenize();
expect(tokens[0].type).toBe('identifier');
@ -67,7 +67,7 @@ describe('when lexing graphite expression', function() {
expect(tokens.length).toBe(3);
});
it('should tokenize func call with numbered metric and number arg', function() {
it('should tokenize func call with numbered metric and number arg', () => {
const lexer = new Lexer('scale(metric.10, 15)');
const tokens = lexer.tokenize();
expect(tokens[0].type).toBe('identifier');
@ -78,7 +78,7 @@ describe('when lexing graphite expression', function() {
expect(tokens[6].type).toBe('number');
});
it('should tokenize metric with template parameter', function() {
it('should tokenize metric with template parameter', () => {
const lexer = new Lexer('metric.[[server]].test');
const tokens = lexer.tokenize();
expect(tokens[2].type).toBe('identifier');
@ -86,7 +86,7 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].type).toBe('identifier');
});
it('should tokenize metric with question mark', function() {
it('should tokenize metric with question mark', () => {
const lexer = new Lexer('metric.server_??.test');
const tokens = lexer.tokenize();
expect(tokens[2].type).toBe('identifier');
@ -94,7 +94,7 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].type).toBe('identifier');
});
it('should handle error with unterminated string', function() {
it('should handle error with unterminated string', () => {
const lexer = new Lexer("alias(metric, 'asd)");
const tokens = lexer.tokenize();
expect(tokens[0].value).toBe('alias');
@ -106,14 +106,14 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].pos).toBe(20);
});
it('should handle float parameters', function() {
it('should handle float parameters', () => {
const lexer = new Lexer('alias(metric, 0.002)');
const tokens = lexer.tokenize();
expect(tokens[4].type).toBe('number');
expect(tokens[4].value).toBe('0.002');
});
it('should handle bool parameters', function() {
it('should handle bool parameters', () => {
const lexer = new Lexer('alias(metric, true, false)');
const tokens = lexer.tokenize();
expect(tokens[4].type).toBe('bool');

View File

@ -1,7 +1,7 @@
import { Parser } from '../parser';
describe('when parsing', function() {
it('simple metric expression', function() {
describe('when parsing', () => {
it('simple metric expression', () => {
const parser = new Parser('metric.test.*.asd.count');
const rootNode = parser.getAst();
@ -10,7 +10,7 @@ describe('when parsing', function() {
expect(rootNode.segments[0].value).toBe('metric');
});
it('simple metric expression with numbers in segments', function() {
it('simple metric expression with numbers in segments', () => {
const parser = new Parser('metric.10.15_20.5');
const rootNode = parser.getAst();
@ -21,7 +21,7 @@ describe('when parsing', function() {
expect(rootNode.segments[3].value).toBe('5');
});
it('simple metric expression with curly braces', function() {
it('simple metric expression with curly braces', () => {
const parser = new Parser('metric.se1-{count, max}');
const rootNode = parser.getAst();
@ -30,7 +30,7 @@ describe('when parsing', function() {
expect(rootNode.segments[1].value).toBe('se1-{count,max}');
});
it('simple metric expression with curly braces at start of segment and with post chars', function() {
it('simple metric expression with curly braces at start of segment and with post chars', () => {
const parser = new Parser('metric.{count, max}-something.count');
const rootNode = parser.getAst();
@ -39,14 +39,14 @@ describe('when parsing', function() {
expect(rootNode.segments[1].value).toBe('{count,max}-something');
});
it('simple function', function() {
it('simple function', () => {
const parser = new Parser('sum(test)');
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
expect(rootNode.params.length).toBe(1);
});
it('simple function2', function() {
it('simple function2', () => {
const parser = new Parser('offset(test.metric, -100)');
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
@ -54,7 +54,7 @@ describe('when parsing', function() {
expect(rootNode.params[1].type).toBe('number');
});
it('simple function with string arg', function() {
it('simple function with string arg', () => {
const parser = new Parser("randomWalk('test')");
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
@ -62,7 +62,7 @@ describe('when parsing', function() {
expect(rootNode.params[0].type).toBe('string');
});
it('function with multiple args', function() {
it('function with multiple args', () => {
const parser = new Parser("sum(test, 1, 'test')");
const rootNode = parser.getAst();
@ -73,7 +73,7 @@ describe('when parsing', function() {
expect(rootNode.params[2].type).toBe('string');
});
it('function with nested function', function() {
it('function with nested function', () => {
const parser = new Parser('sum(scaleToSeconds(test, 1))');
const rootNode = parser.getAst();
@ -86,7 +86,7 @@ describe('when parsing', function() {
expect(rootNode.params[0].params[1].type).toBe('number');
});
it('function with multiple series', function() {
it('function with multiple series', () => {
const parser = new Parser('sum(test.test.*.count, test.timers.*.count)');
const rootNode = parser.getAst();
@ -96,7 +96,7 @@ describe('when parsing', function() {
expect(rootNode.params[1].type).toBe('metric');
});
it('function with templated series', function() {
it('function with templated series', () => {
const parser = new Parser('sum(test.[[server]].count)');
const rootNode = parser.getAst();
@ -106,7 +106,7 @@ describe('when parsing', function() {
expect(rootNode.params[0].segments[1].value).toBe('[[server]]');
});
it('invalid metric expression', function() {
it('invalid metric expression', () => {
const parser = new Parser('metric.test.*.asd.');
const rootNode = parser.getAst();
@ -114,7 +114,7 @@ describe('when parsing', function() {
expect(rootNode.pos).toBe(19);
});
it('invalid function expression missing closing parenthesis', function() {
it('invalid function expression missing closing parenthesis', () => {
const parser = new Parser('sum(test');
const rootNode = parser.getAst();
@ -122,7 +122,7 @@ describe('when parsing', function() {
expect(rootNode.pos).toBe(9);
});
it('unclosed string in function', function() {
it('unclosed string in function', () => {
const parser = new Parser("sum('test)");
const rootNode = parser.getAst();
@ -130,13 +130,13 @@ describe('when parsing', function() {
expect(rootNode.pos).toBe(11);
});
it('handle issue #69', function() {
it('handle issue #69', () => {
const parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
});
it('handle float function arguments', function() {
it('handle float function arguments', () => {
const parser = new Parser('scale(test, 0.002)');
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
@ -144,7 +144,7 @@ describe('when parsing', function() {
expect(rootNode.params[1].value).toBe(0.002);
});
it('handle curly brace pattern at start', function() {
it('handle curly brace pattern at start', () => {
const parser = new Parser('{apps}.test');
const rootNode = parser.getAst();
expect(rootNode.type).toBe('metric');
@ -152,7 +152,7 @@ describe('when parsing', function() {
expect(rootNode.segments[1].value).toBe('test');
});
it('series parameters', function() {
it('series parameters', () => {
const parser = new Parser('asPercent(#A, #B)');
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
@ -161,7 +161,7 @@ describe('when parsing', function() {
expect(rootNode.params[1].value).toBe('#B');
});
it('series parameters, issue 2788', function() {
it('series parameters, issue 2788', () => {
const parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)");
const rootNode = parser.getAst();
expect(rootNode.type).toBe('function');
@ -170,7 +170,7 @@ describe('when parsing', function() {
expect(rootNode.params[3].type).toBe('bool');
});
it('should parse metric expression with ip number segments', function() {
it('should parse metric expression with ip number segments', () => {
const parser = new Parser('5.10.123.5');
const rootNode = parser.getAst();
expect(rootNode.segments[0].value).toBe('5');

View File

@ -137,7 +137,7 @@ describe('GraphiteQueryCtrl', () => {
ctx.ctrl.target.target = 'test.count';
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
ctx.ctrl.parseTarget();
ctx.ctrl.getAltSegments(1).then(function(results) {
ctx.ctrl.getAltSegments(1).then(results => {
ctx.altSegments = results;
});
});

View File

@ -10,7 +10,7 @@ describe('InfluxDataSource', () => {
instanceSettings: { url: 'url', name: 'influxDb', jsonData: {} },
};
beforeEach(function() {
beforeEach(() => {
ctx.instanceSettings.url = '/api/datasources/proxy/1';
ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
});
@ -26,7 +26,7 @@ describe('InfluxDataSource', () => {
let requestQuery;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = function(req) {
ctx.backendSrv.datasourceRequest = req => {
requestQuery = req.params.q;
return ctx.$q.when({
results: [
@ -43,7 +43,7 @@ describe('InfluxDataSource', () => {
});
};
await ctx.ds.metricFindQuery(query, queryOptions).then(function(_) {});
await ctx.ds.metricFindQuery(query, queryOptions).then(_ => {});
});
it('should replace $timefilter', () => {

View File

@ -1,10 +1,10 @@
import InfluxQuery from '../influx_query';
describe('InfluxQuery', function() {
describe('InfluxQuery', () => {
const templateSrv = { replace: val => val };
describe('render series with mesurement only', function() {
it('should generate correct query', function() {
describe('render series with mesurement only', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -18,8 +18,8 @@ describe('InfluxQuery', function() {
});
});
describe('render series with policy only', function() {
it('should generate correct query', function() {
describe('render series with policy only', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -36,8 +36,8 @@ describe('InfluxQuery', function() {
});
});
describe('render series with math and alias', function() {
it('should generate correct query', function() {
describe('render series with math and alias', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -61,8 +61,8 @@ describe('InfluxQuery', function() {
});
});
describe('series with single tag only', function() {
it('should generate correct query', function() {
describe('series with single tag only', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -81,7 +81,7 @@ describe('InfluxQuery', function() {
);
});
it('should switch regex operator with tag value is regex', function() {
it('should switch regex operator with tag value is regex', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -99,8 +99,8 @@ describe('InfluxQuery', function() {
});
});
describe('series with multiple tags only', function() {
it('should generate correct query', function() {
describe('series with multiple tags only', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -119,8 +119,8 @@ describe('InfluxQuery', function() {
});
});
describe('series with tags OR condition', function() {
it('should generate correct query', function() {
describe('series with tags OR condition', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -139,8 +139,8 @@ describe('InfluxQuery', function() {
});
});
describe('query with value condition', function() {
it('should not quote value', function() {
describe('query with value condition', () => {
it('should not quote value', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -156,8 +156,8 @@ describe('InfluxQuery', function() {
});
});
describe('series with groupByTag', function() {
it('should generate correct query', function() {
describe('series with groupByTag', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -173,8 +173,8 @@ describe('InfluxQuery', function() {
});
});
describe('render series without group by', function() {
it('should generate correct query', function() {
describe('render series without group by', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -189,8 +189,8 @@ describe('InfluxQuery', function() {
});
});
describe('render series without group by and fill', function() {
it('should generate correct query', function() {
describe('render series without group by and fill', () => {
it('should generate correct query', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -205,8 +205,8 @@ describe('InfluxQuery', function() {
});
});
describe('when adding group by part', function() {
it('should add tag before fill', function() {
describe('when adding group by part', () => {
it('should add tag before fill', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -223,7 +223,7 @@ describe('InfluxQuery', function() {
expect(query.target.groupBy[2].type).toBe('fill');
});
it('should add tag last if no fill', function() {
it('should add tag last if no fill', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -239,8 +239,8 @@ describe('InfluxQuery', function() {
});
});
describe('when adding select part', function() {
it('should add mean after after field', function() {
describe('when adding select part', () => {
it('should add mean after after field', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -255,7 +255,7 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][1].type).toBe('mean');
});
it('should replace sum by mean', function() {
it('should replace sum by mean', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -270,7 +270,7 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][1].type).toBe('sum');
});
it('should add math before alias', function() {
it('should add math before alias', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -285,7 +285,7 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][2].type).toBe('math');
});
it('should add math last', function() {
it('should add math last', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -300,7 +300,7 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][2].type).toBe('math');
});
it('should replace math', function() {
it('should replace math', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -315,7 +315,7 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][2].type).toBe('math');
});
it('should add math when one only query part', function() {
it('should add math when one only query part', () => {
const query = new InfluxQuery(
{
measurement: 'cpu',
@ -330,8 +330,8 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][1].type).toBe('math');
});
describe('when render adhoc filters', function() {
it('should generate correct query segment', function() {
describe('when render adhoc filters', () => {
it('should generate correct query segment', () => {
const query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {});
const queryText = query.renderAdhocFilters([

View File

@ -1,7 +1,7 @@
import InfluxSeries from '../influx_series';
describe('when generating timeseries from influxdb response', function() {
describe('given multiple fields for series', function() {
describe('when generating timeseries from influxdb response', () => {
describe('given multiple fields for series', () => {
const options = {
alias: '',
series: [
@ -13,8 +13,8 @@ describe('when generating timeseries from influxdb response', function() {
},
],
};
describe('and no alias', function() {
it('should generate multiple datapoints for each column', function() {
describe('and no alias', () => {
it('should generate multiple datapoints for each column', () => {
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -39,8 +39,8 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('and simple alias', function() {
it('should use alias', function() {
describe('and simple alias', () => {
it('should use alias', () => {
options.alias = 'new series';
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -51,8 +51,8 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('and alias patterns', function() {
it('should replace patterns', function() {
describe('and alias patterns', () => {
it('should replace patterns', () => {
options.alias = 'alias: $m -> $tag_server ([[measurement]])';
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -64,7 +64,7 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given measurement with default fieldname', function() {
describe('given measurement with default fieldname', () => {
const options = {
series: [
{
@ -82,8 +82,8 @@ describe('when generating timeseries from influxdb response', function() {
],
};
describe('and no alias', function() {
it('should generate label with no field', function() {
describe('and no alias', () => {
it('should generate label with no field', () => {
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -93,7 +93,7 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given two series', function() {
describe('given two series', () => {
const options = {
alias: '',
series: [
@ -112,8 +112,8 @@ describe('when generating timeseries from influxdb response', function() {
],
};
describe('and no alias', function() {
it('should generate two time series', function() {
describe('and no alias', () => {
it('should generate two time series', () => {
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -132,8 +132,8 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('and simple alias', function() {
it('should use alias', function() {
describe('and simple alias', () => {
it('should use alias', () => {
options.alias = 'new series';
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -142,8 +142,8 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('and alias patterns', function() {
it('should replace patterns', function() {
describe('and alias patterns', () => {
it('should replace patterns', () => {
options.alias = 'alias: $m -> $tag_server ([[measurement]])';
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -154,7 +154,7 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given measurement with dots', function() {
describe('given measurement with dots', () => {
const options = {
alias: '',
series: [
@ -167,7 +167,7 @@ describe('when generating timeseries from influxdb response', function() {
],
};
it('should replace patterns', function() {
it('should replace patterns', () => {
options.alias = 'alias: $1 -> [[3]]';
const series = new InfluxSeries(options);
const result = series.getTimeSeries();
@ -176,7 +176,7 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given table response', function() {
describe('given table response', () => {
const options = {
alias: '',
series: [
@ -189,7 +189,7 @@ describe('when generating timeseries from influxdb response', function() {
],
};
it('should return table', function() {
it('should return table', () => {
const series = new InfluxSeries(options);
const table = series.getTable();
@ -200,7 +200,7 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given table response from SHOW CARDINALITY', function() {
describe('given table response from SHOW CARDINALITY', () => {
const options = {
alias: '',
series: [
@ -212,7 +212,7 @@ describe('when generating timeseries from influxdb response', function() {
],
};
it('should return table', function() {
it('should return table', () => {
const series = new InfluxSeries(options);
const table = series.getTable();
@ -223,8 +223,8 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given annotation response', function() {
describe('with empty tagsColumn', function() {
describe('given annotation response', () => {
describe('with empty tagsColumn', () => {
const options = {
alias: '',
annotation: {},
@ -238,7 +238,7 @@ describe('when generating timeseries from influxdb response', function() {
],
};
it('should multiple tags', function() {
it('should multiple tags', () => {
const series = new InfluxSeries(options);
const annotations = series.getAnnotations();
@ -246,7 +246,7 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given annotation response', function() {
describe('given annotation response', () => {
const options = {
alias: '',
annotation: {
@ -262,7 +262,7 @@ describe('when generating timeseries from influxdb response', function() {
],
};
it('should multiple tags', function() {
it('should multiple tags', () => {
const series = new InfluxSeries(options);
const annotations = series.getAnnotations();

View File

@ -1,14 +1,14 @@
import { InfluxQueryBuilder } from '../query_builder';
describe('InfluxQueryBuilder', function() {
describe('when building explore queries', function() {
it('should only have measurement condition in tag keys query given query with measurement', function() {
describe('InfluxQueryBuilder', () => {
describe('when building explore queries', () => {
it('should only have measurement condition in tag keys query given query with measurement', () => {
const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] });
const query = builder.buildExploreQuery('TAG_KEYS');
expect(query).toBe('SHOW TAG KEYS FROM "cpu"');
});
it('should handle regex measurement in tag keys query', function() {
it('should handle regex measurement in tag keys query', () => {
const builder = new InfluxQueryBuilder({
measurement: '/.*/',
tags: [],
@ -17,13 +17,13 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG KEYS FROM /.*/');
});
it('should have no conditions in tags keys query given query with no measurement or tag', function() {
it('should have no conditions in tags keys query given query with no measurement or tag', () => {
const builder = new InfluxQueryBuilder({ measurement: '', tags: [] });
const query = builder.buildExploreQuery('TAG_KEYS');
expect(query).toBe('SHOW TAG KEYS');
});
it('should have where condition in tag keys query with tags', function() {
it('should have where condition in tag keys query with tags', () => {
const builder = new InfluxQueryBuilder({
measurement: '',
tags: [{ key: 'host', value: 'se1' }],
@ -32,25 +32,25 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\'');
});
it('should have no conditions in measurement query for query with no tags', function() {
it('should have no conditions in measurement query for query with no tags', () => {
const builder = new InfluxQueryBuilder({ measurement: '', tags: [] });
const query = builder.buildExploreQuery('MEASUREMENTS');
expect(query).toBe('SHOW MEASUREMENTS LIMIT 100');
});
it('should have no conditions in measurement query for query with no tags and empty query', function() {
it('should have no conditions in measurement query for query with no tags and empty query', () => {
const builder = new InfluxQueryBuilder({ measurement: '', tags: [] });
const query = builder.buildExploreQuery('MEASUREMENTS', undefined, '');
expect(query).toBe('SHOW MEASUREMENTS LIMIT 100');
});
it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', function() {
it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', () => {
const builder = new InfluxQueryBuilder({ measurement: '', tags: [] });
const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something');
expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ LIMIT 100');
});
it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', function() {
it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => {
const builder = new InfluxQueryBuilder({
measurement: '',
tags: [{ key: 'app', value: 'email' }],
@ -59,7 +59,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ WHERE "app" = \'email\' LIMIT 100');
});
it('should have where condition in measurement query for query with tags', function() {
it('should have where condition in measurement query for query with tags', () => {
const builder = new InfluxQueryBuilder({
measurement: '',
tags: [{ key: 'app', value: 'email' }],
@ -68,7 +68,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW MEASUREMENTS WHERE "app" = \'email\' LIMIT 100');
});
it('should have where tag name IN filter in tag values query for query with one tag', function() {
it('should have where tag name IN filter in tag values query for query with one tag', () => {
const builder = new InfluxQueryBuilder({
measurement: '',
tags: [{ key: 'app', value: 'asdsadsad' }],
@ -77,7 +77,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES WITH KEY = "app"');
});
it('should have measurement tag condition and tag name IN filter in tag values query', function() {
it('should have measurement tag condition and tag name IN filter in tag values query', () => {
const builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [{ key: 'app', value: 'email' }, { key: 'host', value: 'server1' }],
@ -86,7 +86,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\'');
});
it('should select from policy correctly if policy is specified', function() {
it('should select from policy correctly if policy is specified', () => {
const builder = new InfluxQueryBuilder({
measurement: 'cpu',
policy: 'one_week',
@ -96,7 +96,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\'');
});
it('should not include policy when policy is default', function() {
it('should not include policy when policy is default', () => {
const builder = new InfluxQueryBuilder({
measurement: 'cpu',
policy: 'default',
@ -106,7 +106,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app"');
});
it('should switch to regex operator in tag condition', function() {
it('should switch to regex operator in tag condition', () => {
const builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [{ key: 'host', value: '/server.*/' }],
@ -115,7 +115,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/');
});
it('should build show field query', function() {
it('should build show field query', () => {
const builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [{ key: 'app', value: 'email' }],
@ -124,7 +124,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW FIELD KEYS FROM "cpu"');
});
it('should build show field query with regexp', function() {
it('should build show field query with regexp', () => {
const builder = new InfluxQueryBuilder({
measurement: '/$var/',
tags: [{ key: 'app', value: 'email' }],
@ -133,7 +133,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW FIELD KEYS FROM /$var/');
});
it('should build show retention policies query', function() {
it('should build show retention policies query', () => {
const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site');
const query = builder.buildExploreQuery('RETENTION POLICIES');
expect(query).toBe('SHOW RETENTION POLICIES on "site"');

View File

@ -4,20 +4,20 @@ import { TemplateSrvStub } from 'test/specs/helpers';
import { CustomVariable } from 'app/features/templating/custom_variable';
import q from 'q';
describe('MSSQLDatasource', function() {
describe('MSSQLDatasource', () => {
const ctx: any = {
backendSrv: {},
templateSrv: new TemplateSrvStub(),
};
beforeEach(function() {
beforeEach(() => {
ctx.$q = q;
ctx.instanceSettings = { name: 'mssql' };
ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv);
});
describe('When performing annotationQuery', function() {
describe('When performing annotationQuery', () => {
let results;
const annotationName = 'MyAnno';
@ -61,7 +61,7 @@ describe('MSSQLDatasource', function() {
});
});
it('should return annotation list', function() {
it('should return annotation list', () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('some text');
@ -75,7 +75,7 @@ describe('MSSQLDatasource', function() {
});
});
describe('When performing metricFindQuery', function() {
describe('When performing metricFindQuery', () => {
let results;
const query = 'select * from atable';
const response = {
@ -95,24 +95,24 @@ describe('MSSQLDatasource', function() {
},
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
return ctx.$q.when({ data: response, status: 200 });
};
return ctx.ds.metricFindQuery(query).then(function(data) {
return ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of all column values', function() {
it('should return list of all column values', () => {
expect(results.length).toBe(6);
expect(results[0].text).toBe('aTitle');
expect(results[5].text).toBe('some text3');
});
});
describe('When performing metricFindQuery with key, value columns', function() {
describe('When performing metricFindQuery with key, value columns', () => {
let results;
const query = 'select * from atable';
const response = {
@ -132,17 +132,17 @@ describe('MSSQLDatasource', function() {
},
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
return ctx.$q.when({ data: response, status: 200 });
};
return ctx.ds.metricFindQuery(query).then(function(data) {
return ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of as text, value', function() {
it('should return list of as text, value', () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('aTitle');
expect(results[0].value).toBe('value1');
@ -151,7 +151,7 @@ describe('MSSQLDatasource', function() {
});
});
describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() {
describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => {
let results;
const query = 'select * from atable';
const response = {
@ -171,17 +171,17 @@ describe('MSSQLDatasource', function() {
},
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
return ctx.$q.when({ data: response, status: 200 });
};
return ctx.ds.metricFindQuery(query).then(function(data) {
return ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of unique keys', function() {
it('should return list of unique keys', () => {
expect(results.length).toBe(1);
expect(results[0].text).toBe('aTitle');
expect(results[0].value).toBe('same');
@ -189,7 +189,7 @@ describe('MSSQLDatasource', function() {
});
describe('When interpolating variables', () => {
beforeEach(function() {
beforeEach(() => {
ctx.variable = new CustomVariable({}, {});
});

View File

@ -2,7 +2,7 @@ import moment from 'moment';
import { MysqlDatasource } from '../datasource';
import { CustomVariable } from 'app/features/templating/custom_variable';
describe('MySQLDatasource', function() {
describe('MySQLDatasource', () => {
const instanceSettings = { name: 'mysql' };
const backendSrv = {};
const templateSrv = {
@ -17,7 +17,7 @@ describe('MySQLDatasource', function() {
ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv);
});
describe('When performing annotationQuery', function() {
describe('When performing annotationQuery', () => {
let results;
const annotationName = 'MyAnno';
@ -51,16 +51,16 @@ describe('MySQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.annotationQuery(options).then(function(data) {
ctx.ds.annotationQuery(options).then(data => {
results = data;
});
});
it('should return annotation list', function() {
it('should return annotation list', () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('some text');
@ -74,7 +74,7 @@ describe('MySQLDatasource', function() {
});
});
describe('When performing metricFindQuery', function() {
describe('When performing metricFindQuery', () => {
let results;
const query = 'select * from atable';
const response = {
@ -94,23 +94,23 @@ describe('MySQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.metricFindQuery(query).then(function(data) {
ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of all column values', function() {
it('should return list of all column values', () => {
expect(results.length).toBe(6);
expect(results[0].text).toBe('aTitle');
expect(results[5].text).toBe('some text3');
});
});
describe('When performing metricFindQuery with key, value columns', function() {
describe('When performing metricFindQuery with key, value columns', () => {
let results;
const query = 'select * from atable';
const response = {
@ -130,16 +130,16 @@ describe('MySQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.metricFindQuery(query).then(function(data) {
ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of as text, value', function() {
it('should return list of as text, value', () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('aTitle');
expect(results[0].value).toBe('value1');
@ -148,7 +148,7 @@ describe('MySQLDatasource', function() {
});
});
describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() {
describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => {
let results;
const query = 'select * from atable';
const response = {
@ -168,16 +168,16 @@ describe('MySQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.metricFindQuery(query).then(function(data) {
ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of unique keys', function() {
it('should return list of unique keys', () => {
expect(results.length).toBe(1);
expect(results[0].text).toBe('aTitle');
expect(results[0].value).toBe('same');
@ -185,7 +185,7 @@ describe('MySQLDatasource', function() {
});
describe('When interpolating variables', () => {
beforeEach(function() {
beforeEach(() => {
ctx.variable = new CustomVariable({}, {});
});

View File

@ -0,0 +1,63 @@
import _ from 'lodash';
export class PostgresConfigCtrl {
static templateUrl = 'partials/config.html';
current: any;
datasourceSrv: any;
showTimescaleDBHelp: boolean;
/** @ngInject */
constructor($scope, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
this.showTimescaleDBHelp = false;
this.autoDetectFeatures();
}
autoDetectFeatures() {
if (!this.current.id) {
return;
}
this.datasourceSrv.loadDatasource(this.current.name).then(ds => {
return ds.getVersion().then(version => {
version = Number(version[0].text);
// timescaledb is only available for 9.6+
if (version >= 906) {
ds.getTimescaleDBVersion().then(version => {
if (version.length === 1) {
this.current.jsonData.timescaledb = true;
}
});
}
const major = Math.trunc(version / 100);
const minor = version % 100;
let name = String(major);
if (version < 1000) {
name = String(major) + '.' + String(minor);
}
if (!_.find(this.postgresVersions, (p: any) => p.value === version)) {
this.postgresVersions.push({ name: name, value: version });
}
this.current.jsonData.postgresVersion = version;
});
});
}
toggleTimescaleDBHelp() {
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
}
// the value portion is derived from postgres server_version_num/100
postgresVersions = [
{ name: '9.3', value: 903 },
{ name: '9.4', value: 904 },
{ name: '9.5', value: 905 },
{ name: '9.6', value: 906 },
{ name: '10', value: 1000 },
];
}

View File

@ -1,22 +1,27 @@
import _ from 'lodash';
import ResponseParser from './response_parser';
import PostgresQuery from 'app/plugins/datasource/postgres/postgres_query';
export class PostgresDatasource {
id: any;
name: any;
jsonData: any;
responseParser: ResponseParser;
queryModel: PostgresQuery;
/** @ngInject */
constructor(instanceSettings, private backendSrv, private $q, private templateSrv) {
constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) {
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.jsonData = instanceSettings.jsonData;
this.responseParser = new ResponseParser(this.$q);
this.queryModel = new PostgresQuery({});
}
interpolateVariable(value, variable) {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
return "'" + value.replace(/'/g, `''`) + "'";
return this.queryModel.quoteLiteral(value);
} else {
return value;
}
@ -26,23 +31,25 @@ export class PostgresDatasource {
return value;
}
const quotedValues = _.map(value, function(val) {
return "'" + val.replace(/'/g, `''`) + "'";
const quotedValues = _.map(value, v => {
return this.queryModel.quoteLiteral(v);
});
return quotedValues.join(',');
}
query(options) {
const queries = _.filter(options.targets, item => {
return item.hide !== true;
}).map(item => {
const queries = _.filter(options.targets, target => {
return target.hide !== true;
}).map(target => {
const queryModel = new PostgresQuery(target, this.templateSrv, options.scopedVars);
return {
refId: item.refId,
refId: target.refId,
intervalMs: options.intervalMs,
maxDataPoints: options.maxDataPoints,
datasourceId: this.id,
rawSql: this.templateSrv.replace(item.rawSql, options.scopedVars, this.interpolateVariable),
format: item.format,
rawSql: queryModel.render(this.interpolateVariable),
format: target.format,
};
});
@ -103,17 +110,13 @@ export class PostgresDatasource {
format: 'table',
};
const range = this.timeSrv.timeRange();
const data = {
queries: [interpolatedQuery],
from: range.from.valueOf().toString(),
to: range.to.valueOf().toString(),
};
if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
data['from'] = optionalOptions.range.from.valueOf().toString();
}
if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
data['to'] = optionalOptions.range.to.valueOf().toString();
}
return this.backendSrv
.datasourceRequest({
url: '/api/tsdb/query',
@ -123,6 +126,14 @@ export class PostgresDatasource {
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
}
getVersion() {
return this.metricFindQuery("SELECT current_setting('server_version_num')::int/100", {});
}
getTimescaleDBVersion() {
return this.metricFindQuery("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'", {});
}
testDatasource() {
return this.metricFindQuery('SELECT 1', {})
.then(res => {

View File

@ -0,0 +1,176 @@
export class PostgresMetaQuery {
constructor(private target, private queryModel) {}
getOperators(datatype: string) {
switch (datatype) {
case 'float4':
case 'float8': {
return ['=', '!=', '<', '<=', '>', '>='];
}
case 'text':
case 'varchar':
case 'char': {
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE', '~', '~*', '!~', '!~*'];
}
default: {
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN'];
}
}
}
// quote identifier as literal to use in metadata queries
quoteIdentAsLiteral(value) {
return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value));
}
findMetricTable() {
// query that returns first table found that has a timestamp(tz) column and a float column
const query = `
SELECT
quote_ident(table_name) as table_name,
( SELECT
quote_ident(column_name) as column_name
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
udt_name IN ('timestamptz','timestamp')
ORDER BY ordinal_position LIMIT 1
) AS time_column,
( SELECT
quote_ident(column_name) AS column_name
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
udt_name='float8'
ORDER BY ordinal_position LIMIT 1
) AS value_column
FROM information_schema.tables t
WHERE
table_schema IN (
SELECT CASE WHEN trim(unnest) = '"$user"' THEN user ELSE trim(unnest) END
FROM unnest(string_to_array(current_setting('search_path'),','))
) AND
EXISTS
( SELECT 1
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
udt_name IN ('timestamptz','timestamp')
) AND
EXISTS
( SELECT 1
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
udt_name='float8'
)
LIMIT 1
;`;
return query;
}
buildSchemaConstraint() {
const query = `
table_schema IN (
SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END
FROM unnest(string_to_array(current_setting(\'search_path\'),\',\'))
)`;
return query;
}
buildTableConstraint(table: string) {
let query = '';
// check for schema qualified table
if (table.includes('.')) {
const parts = table.split('.');
query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]);
query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]);
return query;
} else {
query = `
table_schema IN (
SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END
FROM unnest(string_to_array(current_setting(\'search_path\'),\',\'))
)`;
query += ' AND table_name = ' + this.quoteIdentAsLiteral(table);
return query;
}
}
buildTableQuery() {
let query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE ';
query += this.buildSchemaConstraint();
query += ' ORDER BY table_name';
return query;
}
buildColumnQuery(type?: string) {
let query = 'SELECT quote_ident(column_name) FROM information_schema.columns WHERE ';
query += this.buildTableConstraint(this.target.table);
switch (type) {
case 'time': {
query +=
" AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')";
break;
}
case 'metric': {
query += " AND data_type IN ('text','character','character varying')";
break;
}
case 'value': {
query += " AND data_type IN ('bigint','integer','double precision','real')";
query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn);
break;
}
case 'group': {
query += " AND data_type IN ('text','character','character varying')";
break;
}
}
query += ' ORDER BY column_name';
return query;
}
buildValueQuery(column: string) {
let query = 'SELECT DISTINCT quote_literal(' + column + ')';
query += ' FROM ' + this.target.table;
query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')';
query += ' ORDER BY 1 LIMIT 100';
return query;
}
buildDatatypeQuery(column: string) {
let query = `
SELECT udt_name
FROM information_schema.columns
WHERE
table_schema IN (
SELECT schema FROM (
SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END as schema
FROM unnest(string_to_array(current_setting(\'search_path\'),\',\'))
) s
WHERE EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = s.schema)
)
`;
query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table);
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
return query;
}
buildAggregateQuery() {
let query = 'SELECT DISTINCT proname FROM pg_aggregate ';
query += 'INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid ';
query += 'INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype ';
query += "WHERE pronargs=1 AND typname IN ('float8') AND aggkind='n' ORDER BY 1";
return query;
}
}

View File

@ -1,16 +1,6 @@
import { PostgresDatasource } from './datasource';
import { PostgresQueryCtrl } from './query_ctrl';
class PostgresConfigCtrl {
static templateUrl = 'partials/config.html';
current: any;
/** @ngInject */
constructor($scope) {
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
}
}
import { PostgresConfigCtrl } from './config_ctrl';
const defaultQuery = `SELECT
extract(epoch from time_column) AS time,

View File

@ -42,10 +42,36 @@
<div class="gf-form-group">
<div class="gf-form">
<gf-form-switch class="gf-form" label="TimescaleDB" tooltip="Use TimescaleDB features (e.g., time_bucket) in Grafana" label-class="width-9" checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
<span class="gf-form-label width-9">
Version
<info-popover mode="right-normal" position="top center">
This option controls what functions are available in the PostgreSQL query builder.
</info-popover>
</span>
<span class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.postgresVersion" ng-options="f.value as f.name for f in ctrl.postgresVersions"></select>
</span>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label="TimescaleDB" label-class="width-9" checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleTimescaleDBHelp()">
Help&nbsp;
<i class="fa fa-caret-down" ng-show="ctrl.showTimescaleDBHelp"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showTimescaleDBHelp">&nbsp;</i>
</label>
</div>
<div class="grafana-info-box alert alert-info" ng-show="ctrl.showTimescaleDBHelp">
<div class="alert-body">
<p>
<a href="https://github.com/timescale/timescaledb" class="pointer" target="_blank">TimescaleDB</a> is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use <code>time_bucket</code> in the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate functions in the query builder.
</p>
</div>
</div>
</div>
<div class="gf-form-group">
<div class="grafana-info-box">
<h5>User Permission</h5>

View File

@ -1,43 +1,141 @@
<query-editor-row query-ctrl="ctrl" can-collapse="false">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sql">
</code-editor>
</div>
</div>
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
<div ng-if="ctrl.target.rawQuery">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sql">
</code-editor>
</div>
</div>
</div>
<div ng-if="!ctrl.target.rawQuery">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">FROM</label>
<metric-segment segment="ctrl.tableSegment" get-options="ctrl.getTableSegments()" on-change="ctrl.tableChanged()"></metric-segment>
<label class="gf-form-label query-keyword width-7">Time column</label>
<metric-segment segment="ctrl.timeColumnSegment" get-options="ctrl.getTimeColumnSegments()" on-change="ctrl.timeColumnChanged()"></metric-segment>
<label class="gf-form-label query-keyword width-9">
Metric column
<info-popover mode="right-normal">Column to be used as metric name for the value column.</info-popover>
</label>
<metric-segment segment="ctrl.metricColumnSegment" get-options="ctrl.getMetricColumnSegments()" on-change="ctrl.metricColumnChanged()"></metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-repeat="selectParts in ctrl.selectParts">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">
<span ng-show="$index === 0">SELECT</span>&nbsp;
</label>
</div>
<div class="gf-form" ng-repeat="part in selectParts">
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleSelectPartEvent(selectParts, part, $event)">
</sql-part-editor>
</div>
<div class="gf-form">
<label class="dropdown"
dropdown-typeahead="ctrl.selectMenu"
dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)">
</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">WHERE</label>
</div>
<div class="gf-form" ng-repeat="part in ctrl.whereParts">
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleWherePartEvent(ctrl.whereParts, part, $event, $index)">
</sql-part-editor>
</div>
<div class="gf-form">
<metric-segment segment="ctrl.whereAdd" get-options="ctrl.getWhereOptions()" on-change="ctrl.addWhereAction(part, $index)"></metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">
<span>GROUP BY</span>
</label>
<sql-part-editor ng-repeat="part in ctrl.groupParts"
part="part" class="gf-form-label sql-part"
handle-event="ctrl.handleGroupPartEvent(part, $index, $event)">
</sql-part-editor>
</div>
<div class="gf-form">
<metric-segment segment="ctrl.groupAdd" get-options="ctrl.getGroupOptions()" on-change="ctrl.addGroupAction(part, $index)"></metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword">Format as</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
<label class="gf-form-label query-keyword">Format as</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleEditorMode()" ng-show="ctrl.panelCtrl.panel.type !== 'table'">
<span ng-show="ctrl.target.rawQuery">Query Builder</span>
<span ng-hide="ctrl.target.rawQuery">Edit SQL</span>
</label>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showHelp = !ctrl.showHelp">
Show Help
<i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
</label>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
Generated SQL
<i class="fa fa-caret-down" ng-show="ctrl.showLastQuerySQL"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showLastQuerySQL"></i>
</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.sql}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.sql}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info">Time series:
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info">Time series:
- return column named <i>time</i> (UTC in seconds or timestamp)
- return column(s) with numeric datatype as values
Optional:
@ -73,13 +171,13 @@ Or build your own conditionals using these macros which just return the values:
- $__timeTo() -&gt; '2017-04-21T05:01:17Z'
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
</pre>
</div>
</pre>
</div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryError">
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryError">
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
</div>
</query-editor-row>

View File

@ -19,4 +19,5 @@
"alerting": true,
"annotations": true,
"metrics": true
}

View File

@ -0,0 +1,285 @@
import _ from 'lodash';
export default class PostgresQuery {
target: any;
templateSrv: any;
scopedVars: any;
/** @ngInject */
constructor(target, templateSrv?, scopedVars?) {
this.target = target;
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
target.format = target.format || 'time_series';
target.timeColumn = target.timeColumn || 'time';
target.metricColumn = target.metricColumn || 'none';
target.group = target.group || [];
target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }];
target.select = target.select || [[{ type: 'column', params: ['value'] }]];
// handle pre query gui panels gracefully
if (!('rawQuery' in this.target)) {
if ('rawSql' in target) {
// pre query gui panel
target.rawQuery = true;
} else {
// new panel
target.rawQuery = false;
}
}
// give interpolateQueryStr access to this
this.interpolateQueryStr = this.interpolateQueryStr.bind(this);
}
// remove identifier quoting from identifier to use in metadata queries
unquoteIdentifier(value) {
if (value[0] === '"' && value[value.length - 1] === '"') {
return value.substring(1, value.length - 1).replace(/""/g, '"');
} else {
return value;
}
}
quoteIdentifier(value) {
return '"' + value.replace(/"/g, '""') + '"';
}
quoteLiteral(value) {
return "'" + value.replace(/'/g, "''") + "'";
}
escapeLiteral(value) {
return value.replace(/'/g, "''");
}
hasTimeGroup() {
return _.find(this.target.group, (g: any) => g.type === 'time');
}
hasMetricColumn() {
return this.target.metricColumn !== 'none';
}
interpolateQueryStr(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return this.escapeLiteral(value);
}
if (typeof value === 'string') {
return this.quoteLiteral(value);
}
const escapedValues = _.map(value, this.quoteLiteral);
return escapedValues.join(',');
}
render(interpolate?) {
const target = this.target;
// new query with no table set yet
if (!this.target.rawQuery && !('table' in this.target)) {
return '';
}
if (!target.rawQuery) {
target.rawSql = this.buildQuery();
}
if (interpolate) {
return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr);
} else {
return target.rawSql;
}
}
hasUnixEpochTimecolumn() {
return ['int4', 'int8', 'float4', 'float8', 'numeric'].indexOf(this.target.timeColumnType) > -1;
}
buildTimeColumn(alias = true) {
const timeGroup = this.hasTimeGroup();
let query;
let macro = '$__timeGroup';
if (timeGroup) {
let args;
if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') {
args = timeGroup.params.join(',');
} else {
args = timeGroup.params[0];
}
if (this.hasUnixEpochTimecolumn()) {
macro = '$__unixEpochGroup';
}
if (alias) {
macro += 'Alias';
}
query = macro + '(' + this.target.timeColumn + ',' + args + ')';
} else {
query = this.target.timeColumn;
if (alias) {
query += ' AS "time"';
}
}
return query;
}
buildMetricColumn() {
if (this.hasMetricColumn()) {
return this.target.metricColumn + ' AS metric';
}
return '';
}
buildValueColumns() {
let query = '';
for (const column of this.target.select) {
query += ',\n ' + this.buildValueColumn(column);
}
return query;
}
buildValueColumn(column) {
let query = '';
const columnName = _.find(column, (g: any) => g.type === 'column');
query = columnName.params[0];
const aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile');
const windows = _.find(column, (g: any) => g.type === 'window' || g.type === 'moving_window');
if (aggregate) {
const func = aggregate.params[0];
switch (aggregate.type) {
case 'aggregate':
if (func === 'first' || func === 'last') {
query = func + '(' + query + ',' + this.target.timeColumn + ')';
} else {
query = func + '(' + query + ')';
}
break;
case 'percentile':
query = func + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')';
break;
}
}
if (windows) {
const overParts = [];
if (this.hasMetricColumn()) {
overParts.push('PARTITION BY ' + this.target.metricColumn);
}
overParts.push('ORDER BY ' + this.buildTimeColumn(false));
const over = overParts.join(' ');
let curr: string;
let prev: string;
switch (windows.type) {
case 'window':
switch (windows.params[0]) {
case 'increase':
curr = query;
prev = 'lag(' + curr + ') OVER (' + over + ')';
query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)';
break;
case 'rate':
let timeColumn = this.target.timeColumn;
if (aggregate) {
timeColumn = 'min(' + timeColumn + ')';
}
curr = query;
prev = 'lag(' + curr + ') OVER (' + over + ')';
query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)';
query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))';
break;
default:
query = windows.params[0] + '(' + query + ') OVER (' + over + ')';
break;
}
break;
case 'moving_window':
query = windows.params[0] + '(' + query + ') OVER (' + over + ' ROWS ' + windows.params[1] + ' PRECEDING)';
break;
}
}
const alias = _.find(column, (g: any) => g.type === 'alias');
if (alias) {
query += ' AS ' + this.quoteIdentifier(alias.params[0]);
}
return query;
}
buildWhereClause() {
let query = '';
const conditions = _.map(this.target.where, (tag, index) => {
switch (tag.type) {
case 'macro':
return tag.name + '(' + this.target.timeColumn + ')';
break;
case 'expression':
return tag.params.join(' ');
break;
}
});
if (conditions.length > 0) {
query = '\nWHERE\n ' + conditions.join(' AND\n ');
}
return query;
}
buildGroupClause() {
let query = '';
let groupSection = '';
for (let i = 0; i < this.target.group.length; i++) {
const part = this.target.group[i];
if (i > 0) {
groupSection += ', ';
}
if (part.type === 'time') {
groupSection += '1';
} else {
groupSection += part.params[0];
}
}
if (groupSection.length) {
query = '\nGROUP BY ' + groupSection;
if (this.hasMetricColumn()) {
query += ',2';
}
}
return query;
}
buildQuery() {
let query = 'SELECT';
query += '\n ' + this.buildTimeColumn();
if (this.hasMetricColumn()) {
query += ',\n ' + this.buildMetricColumn();
}
query += this.buildValueColumns();
query += '\nFROM ' + this.target.table;
query += this.buildWhereClause();
query += this.buildGroupClause();
query += '\nORDER BY 1';
return query;
}
}

View File

@ -1,12 +1,10 @@
import _ from 'lodash';
import appEvents from 'app/core/app_events';
import { PostgresMetaQuery } from './meta_query';
import { QueryCtrl } from 'app/plugins/sdk';
export interface PostgresQuery {
refId: string;
format: string;
alias: string;
rawSql: string;
}
import { SqlPart } from 'app/core/components/sql_part/sql_part';
import PostgresQuery from './postgres_query';
import sqlPart from './sql_part';
export interface QueryMeta {
sql: string;
@ -26,17 +24,29 @@ export class PostgresQueryCtrl extends QueryCtrl {
showLastQuerySQL: boolean;
formats: any[];
target: PostgresQuery;
queryModel: PostgresQuery;
metaBuilder: PostgresMetaQuery;
lastQueryMeta: QueryMeta;
lastQueryError: string;
showHelp: boolean;
tableSegment: any;
whereAdd: any;
timeColumnSegment: any;
metricColumnSegment: any;
selectMenu: any[];
selectParts: SqlPart[][];
groupParts: SqlPart[];
whereParts: SqlPart[];
groupAdd: any;
/** @ngInject */
constructor($scope, $injector) {
constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
super($scope, $injector);
this.target = this.target;
this.queryModel = new PostgresQuery(this.target, templateSrv, this.panel.scopedVars);
this.metaBuilder = new PostgresMetaQuery(this.target, this.queryModel);
this.updateProjection();
this.target.format = this.target.format || 'time_series';
this.target.alias = '';
this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
if (!this.target.rawSql) {
@ -44,15 +54,231 @@ export class PostgresQueryCtrl extends QueryCtrl {
if (this.panelCtrl.panel.type === 'table') {
this.target.format = 'table';
this.target.rawSql = 'SELECT 1';
this.target.rawQuery = true;
} else {
this.target.rawSql = defaultQuery;
this.datasource.metricFindQuery(this.metaBuilder.findMetricTable()).then(result => {
if (result.length > 0) {
this.target.table = result[0].text;
let segment = this.uiSegmentSrv.newSegment(this.target.table);
this.tableSegment.html = segment.html;
this.tableSegment.value = segment.value;
this.target.timeColumn = result[1].text;
segment = this.uiSegmentSrv.newSegment(this.target.timeColumn);
this.timeColumnSegment.html = segment.html;
this.timeColumnSegment.value = segment.value;
this.target.timeColumnType = 'timestamp';
this.target.select = [[{ type: 'column', params: [result[2].text] }]];
this.updateProjection();
this.panelCtrl.refresh();
}
});
}
}
if (!this.target.table) {
this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true });
} else {
this.tableSegment = uiSegmentSrv.newSegment(this.target.table);
}
this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn);
this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn);
this.buildSelectMenu();
this.whereAdd = this.uiSegmentSrv.newPlusButton();
this.groupAdd = this.uiSegmentSrv.newPlusButton();
this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
}
updateProjection() {
this.selectParts = _.map(this.target.select, function(parts: any) {
return _.map(parts, sqlPart.create).filter(n => n);
});
this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n);
this.groupParts = _.map(this.target.group, sqlPart.create).filter(n => n);
}
updatePersistedParts() {
this.target.select = _.map(this.selectParts, function(selectParts) {
return _.map(selectParts, function(part: any) {
return { type: part.def.type, datatype: part.datatype, params: part.params };
});
});
this.target.where = _.map(this.whereParts, function(part: any) {
return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params };
});
this.target.group = _.map(this.groupParts, function(part: any) {
return { type: part.def.type, datatype: part.datatype, params: part.params };
});
}
buildSelectMenu() {
this.selectMenu = [];
const aggregates = {
text: 'Aggregate Functions',
value: 'aggregate',
submenu: [
{ text: 'Average', value: 'avg' },
{ text: 'Count', value: 'count' },
{ text: 'Maximum', value: 'max' },
{ text: 'Minimum', value: 'min' },
{ text: 'Sum', value: 'sum' },
{ text: 'Standard deviation', value: 'stddev' },
{ text: 'Variance', value: 'variance' },
],
};
// first and last aggregate are timescaledb specific
if (this.datasource.jsonData.timescaledb === true) {
aggregates.submenu.push({ text: 'First', value: 'first' });
aggregates.submenu.push({ text: 'Last', value: 'last' });
}
this.selectMenu.push(aggregates);
// ordered set aggregates require postgres 9.4+
if (this.datasource.jsonData.postgresVersion >= 904) {
const aggregates2 = {
text: 'Ordered-Set Aggregate Functions',
value: 'percentile',
submenu: [
{ text: 'Percentile (continuous)', value: 'percentile_cont' },
{ text: 'Percentile (discrete)', value: 'percentile_disc' },
],
};
this.selectMenu.push(aggregates2);
}
const windows = {
text: 'Window Functions',
value: 'window',
submenu: [
{ text: 'Increase', value: 'increase' },
{ text: 'Rate', value: 'rate' },
{ text: 'Sum', value: 'sum' },
{ text: 'Moving Average', value: 'avg', type: 'moving_window' },
],
};
this.selectMenu.push(windows);
this.selectMenu.push({ text: 'Alias', value: 'alias' });
this.selectMenu.push({ text: 'Column', value: 'column' });
}
toggleEditorMode() {
if (this.target.rawQuery) {
appEvents.emit('confirm-modal', {
title: 'Warning',
text2: 'Switching to query builder may overwrite your raw SQL.',
icon: 'fa-exclamation',
yesText: 'Switch',
onConfirm: () => {
this.target.rawQuery = !this.target.rawQuery;
},
});
} else {
this.target.rawQuery = !this.target.rawQuery;
}
}
resetPlusButton(button) {
const plusButton = this.uiSegmentSrv.newPlusButton();
button.html = plusButton.html;
button.value = plusButton.value;
}
getTableSegments() {
return this.datasource
.metricFindQuery(this.metaBuilder.buildTableQuery())
.then(this.transformToSegments({}))
.catch(this.handleQueryError.bind(this));
}
tableChanged() {
this.target.table = this.tableSegment.value;
this.target.where = [];
this.target.group = [];
this.updateProjection();
const segment = this.uiSegmentSrv.newSegment('none');
this.metricColumnSegment.html = segment.html;
this.metricColumnSegment.value = segment.value;
this.target.metricColumn = 'none';
const task1 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then(result => {
// check if time column is still valid
if (result.length > 0 && !_.find(result, (r: any) => r.text === this.target.timeColumn)) {
const segment = this.uiSegmentSrv.newSegment(result[0].text);
this.timeColumnSegment.html = segment.html;
this.timeColumnSegment.value = segment.value;
}
return this.timeColumnChanged(false);
});
const task2 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('value')).then(result => {
if (result.length > 0) {
this.target.select = [[{ type: 'column', params: [result[0].text] }]];
this.updateProjection();
}
});
this.$q.all([task1, task2]).then(() => {
this.panelCtrl.refresh();
});
}
getTimeColumnSegments() {
return this.datasource
.metricFindQuery(this.metaBuilder.buildColumnQuery('time'))
.then(this.transformToSegments({}))
.catch(this.handleQueryError.bind(this));
}
timeColumnChanged(refresh?: boolean) {
this.target.timeColumn = this.timeColumnSegment.value;
return this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn)).then(result => {
if (result.length === 1) {
if (this.target.timeColumnType !== result[0].text) {
this.target.timeColumnType = result[0].text;
}
let partModel;
if (this.queryModel.hasUnixEpochTimecolumn()) {
partModel = sqlPart.create({ type: 'macro', name: '$__unixEpochFilter', params: [] });
} else {
partModel = sqlPart.create({ type: 'macro', name: '$__timeFilter', params: [] });
}
if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') {
// replace current macro
this.whereParts[0] = partModel;
} else {
this.whereParts.splice(0, 0, partModel);
}
}
this.updatePersistedParts();
if (refresh !== false) {
this.panelCtrl.refresh();
}
});
}
getMetricColumnSegments() {
return this.datasource
.metricFindQuery(this.metaBuilder.buildColumnQuery('metric'))
.then(this.transformToSegments({ addNone: true }))
.catch(this.handleQueryError.bind(this));
}
metricColumnChanged() {
this.target.metricColumn = this.metricColumnSegment.value;
this.panelCtrl.refresh();
}
onDataReceived(dataList) {
this.lastQueryMeta = null;
this.lastQueryError = null;
@ -72,4 +298,356 @@ export class PostgresQueryCtrl extends QueryCtrl {
}
}
}
transformToSegments(config) {
return results => {
const segments = _.map(results, segment => {
return this.uiSegmentSrv.newSegment({
value: segment.text,
expandable: segment.expandable,
});
});
if (config.addTemplateVars) {
for (const variable of this.templateSrv.variables) {
let value;
value = '$' + variable.name;
if (config.templateQuoter && variable.multi === false) {
value = config.templateQuoter(value);
}
segments.unshift(
this.uiSegmentSrv.newSegment({
type: 'template',
value: value,
expandable: true,
})
);
}
}
if (config.addNone) {
segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'none', expandable: true }));
}
return segments;
};
}
findAggregateIndex(selectParts) {
return _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile');
}
findWindowIndex(selectParts) {
return _.findIndex(selectParts, (p: any) => p.def.type === 'window' || p.def.type === 'moving_window');
}
addSelectPart(selectParts, item, subItem) {
let partType = item.value;
if (subItem && subItem.type) {
partType = subItem.type;
}
let partModel = sqlPart.create({ type: partType });
if (subItem) {
partModel.params[0] = subItem.value;
}
let addAlias = false;
switch (partType) {
case 'column':
const parts = _.map(selectParts, function(part: any) {
return sqlPart.create({ type: part.def.type, params: _.clone(part.params) });
});
this.selectParts.push(parts);
break;
case 'percentile':
case 'aggregate':
// add group by if no group by yet
if (this.target.group.length === 0) {
this.addGroup('time', '$__interval');
}
const aggIndex = this.findAggregateIndex(selectParts);
if (aggIndex !== -1) {
// replace current aggregation
selectParts[aggIndex] = partModel;
} else {
selectParts.splice(1, 0, partModel);
}
if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) {
addAlias = true;
}
break;
case 'moving_window':
case 'window':
const windowIndex = this.findWindowIndex(selectParts);
if (windowIndex !== -1) {
// replace current window function
selectParts[windowIndex] = partModel;
} else {
const aggIndex = this.findAggregateIndex(selectParts);
if (aggIndex !== -1) {
selectParts.splice(aggIndex + 1, 0, partModel);
} else {
selectParts.splice(1, 0, partModel);
}
}
if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) {
addAlias = true;
}
break;
case 'alias':
addAlias = true;
break;
}
if (addAlias) {
// set initial alias name to column name
partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace(/"/g, '')] });
if (selectParts[selectParts.length - 1].def.type === 'alias') {
selectParts[selectParts.length - 1] = partModel;
} else {
selectParts.push(partModel);
}
}
this.updatePersistedParts();
this.panelCtrl.refresh();
}
removeSelectPart(selectParts, part) {
if (part.def.type === 'column') {
// remove all parts of column unless its last column
if (this.selectParts.length > 1) {
const modelsIndex = _.indexOf(this.selectParts, selectParts);
this.selectParts.splice(modelsIndex, 1);
}
} else {
const partIndex = _.indexOf(selectParts, part);
selectParts.splice(partIndex, 1);
}
this.updatePersistedParts();
}
handleSelectPartEvent(selectParts, part, evt) {
switch (evt.name) {
case 'get-param-options': {
switch (part.def.type) {
case 'aggregate':
return this.datasource
.metricFindQuery(this.metaBuilder.buildAggregateQuery())
.then(this.transformToSegments({}))
.catch(this.handleQueryError.bind(this));
case 'column':
return this.datasource
.metricFindQuery(this.metaBuilder.buildColumnQuery('value'))
.then(this.transformToSegments({}))
.catch(this.handleQueryError.bind(this));
}
}
case 'part-param-changed': {
this.updatePersistedParts();
this.panelCtrl.refresh();
break;
}
case 'action': {
this.removeSelectPart(selectParts, part);
this.panelCtrl.refresh();
break;
}
case 'get-part-actions': {
return this.$q.when([{ text: 'Remove', value: 'remove-part' }]);
}
}
}
handleGroupPartEvent(part, index, evt) {
switch (evt.name) {
case 'get-param-options': {
return this.datasource
.metricFindQuery(this.metaBuilder.buildColumnQuery())
.then(this.transformToSegments({}))
.catch(this.handleQueryError.bind(this));
}
case 'part-param-changed': {
this.updatePersistedParts();
this.panelCtrl.refresh();
break;
}
case 'action': {
this.removeGroup(part, index);
this.panelCtrl.refresh();
break;
}
case 'get-part-actions': {
return this.$q.when([{ text: 'Remove', value: 'remove-part' }]);
}
}
}
addGroup(partType, value) {
let params = [value];
if (partType === 'time') {
params = ['$__interval', 'none'];
}
const partModel = sqlPart.create({ type: partType, params: params });
if (partType === 'time') {
// put timeGroup at start
this.groupParts.splice(0, 0, partModel);
} else {
this.groupParts.push(partModel);
}
// add aggregates when adding group by
for (const selectParts of this.selectParts) {
if (!selectParts.some(part => part.def.type === 'aggregate')) {
const aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] });
selectParts.splice(1, 0, aggregate);
if (!selectParts.some(part => part.def.type === 'alias')) {
const alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] });
selectParts.push(alias);
}
}
}
this.updatePersistedParts();
}
removeGroup(part, index) {
if (part.def.type === 'time') {
// remove aggregations
this.selectParts = _.map(this.selectParts, (s: any) => {
return _.filter(s, (part: any) => {
if (part.def.type === 'aggregate' || part.def.type === 'percentile') {
return false;
}
return true;
});
});
}
this.groupParts.splice(index, 1);
this.updatePersistedParts();
}
handleWherePartEvent(whereParts, part, evt, index) {
switch (evt.name) {
case 'get-param-options': {
switch (evt.param.name) {
case 'left':
return this.datasource
.metricFindQuery(this.metaBuilder.buildColumnQuery())
.then(this.transformToSegments({}))
.catch(this.handleQueryError.bind(this));
case 'right':
if (['int4', 'int8', 'float4', 'float8', 'timestamp', 'timestamptz'].indexOf(part.datatype) > -1) {
// don't do value lookups for numerical fields
return this.$q.when([]);
} else {
return this.datasource
.metricFindQuery(this.metaBuilder.buildValueQuery(part.params[0]))
.then(
this.transformToSegments({
addTemplateVars: true,
templateQuoter: (v: string) => {
return this.queryModel.quoteLiteral(v);
},
})
)
.catch(this.handleQueryError.bind(this));
}
case 'op':
return this.$q.when(this.uiSegmentSrv.newOperators(this.metaBuilder.getOperators(part.datatype)));
default:
return this.$q.when([]);
}
}
case 'part-param-changed': {
this.updatePersistedParts();
this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(part.params[0])).then((d: any) => {
if (d.length === 1) {
part.datatype = d[0].text;
}
});
this.panelCtrl.refresh();
break;
}
case 'action': {
// remove element
whereParts.splice(index, 1);
this.updatePersistedParts();
this.panelCtrl.refresh();
break;
}
case 'get-part-actions': {
return this.$q.when([{ text: 'Remove', value: 'remove-part' }]);
}
}
}
getWhereOptions() {
const options = [];
if (this.queryModel.hasUnixEpochTimecolumn()) {
options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__unixEpochFilter' }));
} else {
options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__timeFilter' }));
}
options.push(this.uiSegmentSrv.newSegment({ type: 'expression', value: 'Expression' }));
return this.$q.when(options);
}
addWhereAction(part, index) {
switch (this.whereAdd.type) {
case 'macro': {
const partModel = sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] });
if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') {
// replace current macro
this.whereParts[0] = partModel;
} else {
this.whereParts.splice(0, 0, partModel);
}
break;
}
default: {
this.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] }));
}
}
this.updatePersistedParts();
this.resetPlusButton(this.whereAdd);
this.panelCtrl.refresh();
}
getGroupOptions() {
return this.datasource
.metricFindQuery(this.metaBuilder.buildColumnQuery('group'))
.then(tags => {
const options = [];
if (!this.queryModel.hasTimeGroup()) {
options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' }));
}
for (const tag of tags) {
options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text }));
}
return options;
})
.catch(this.handleQueryError.bind(this));
}
addGroupAction() {
switch (this.groupAdd.value) {
default: {
this.addGroup(this.groupAdd.type, this.groupAdd.value);
}
}
this.resetPlusButton(this.groupAdd);
this.panelCtrl.refresh();
}
handleQueryError(err) {
this.error = err.message || 'Failed to issue metric query';
return [];
}
}

View File

@ -2,22 +2,33 @@ import moment from 'moment';
import { PostgresDatasource } from '../datasource';
import { CustomVariable } from 'app/features/templating/custom_variable';
describe('PostgreSQLDatasource', function() {
describe('PostgreSQLDatasource', () => {
const instanceSettings = { name: 'postgresql' };
const backendSrv = {};
const templateSrv = {
replace: jest.fn(text => text),
};
const raw = {
from: moment.utc('2018-04-25 10:00'),
to: moment.utc('2018-04-25 11:00'),
};
const ctx = {
backendSrv,
timeSrvMock: {
timeRange: () => ({
from: raw.from,
to: raw.to,
raw: raw,
}),
},
} as any;
beforeEach(() => {
ctx.ds = new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv);
ctx.ds = new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv, ctx.timeSrvMock);
});
describe('When performing annotationQuery', function() {
describe('When performing annotationQuery', () => {
let results;
const annotationName = 'MyAnno';
@ -51,16 +62,16 @@ describe('PostgreSQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.annotationQuery(options).then(function(data) {
ctx.ds.annotationQuery(options).then(data => {
results = data;
});
});
it('should return annotation list', function() {
it('should return annotation list', () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('some text');
@ -74,7 +85,7 @@ describe('PostgreSQLDatasource', function() {
});
});
describe('When performing metricFindQuery', function() {
describe('When performing metricFindQuery', () => {
let results;
const query = 'select * from atable';
const response = {
@ -94,23 +105,23 @@ describe('PostgreSQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.metricFindQuery(query).then(function(data) {
ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of all column values', function() {
it('should return list of all column values', () => {
expect(results.length).toBe(6);
expect(results[0].text).toBe('aTitle');
expect(results[5].text).toBe('some text3');
});
});
describe('When performing metricFindQuery with key, value columns', function() {
describe('When performing metricFindQuery with key, value columns', () => {
let results;
const query = 'select * from atable';
const response = {
@ -130,16 +141,16 @@ describe('PostgreSQLDatasource', function() {
},
};
beforeEach(function() {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.metricFindQuery(query).then(function(data) {
ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
});
it('should return list of as text, value', function() {
it('should return list of as text, value', () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('aTitle');
expect(results[0].value).toBe('value1');
@ -148,7 +159,7 @@ describe('PostgreSQLDatasource', function() {
});
});
describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() {
describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => {
let results;
const query = 'select * from atable';
const response = {
@ -172,13 +183,13 @@ describe('PostgreSQLDatasource', function() {
ctx.backendSrv.datasourceRequest = jest.fn(options => {
return Promise.resolve({ data: response, status: 200 });
});
ctx.ds.metricFindQuery(query).then(function(data) {
ctx.ds.metricFindQuery(query).then(data => {
results = data;
});
//ctx.$rootScope.$apply();
});
it('should return list of unique keys', function() {
it('should return list of unique keys', () => {
expect(results.length).toBe(1);
expect(results[0].text).toBe('aTitle');
expect(results[0].value).toBe('same');
@ -186,7 +197,7 @@ describe('PostgreSQLDatasource', function() {
});
describe('When interpolating variables', () => {
beforeEach(function() {
beforeEach(() => {
ctx.variable = new CustomVariable({}, {});
});
@ -219,6 +230,7 @@ describe('PostgreSQLDatasource', function() {
it('should return a quoted value', () => {
ctx.variable.multi = true;
expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'");
expect(ctx.ds.interpolateVariable("a'b'c", ctx.variable)).toEqual("'a''b''c'");
});
});

View File

@ -0,0 +1,155 @@
import PostgresQuery from '../postgres_query';
describe('PostgresQuery', function() {
const templateSrv = {
replace: jest.fn(text => text),
};
describe('When initializing', function() {
it('should not be in SQL mode', function() {
const query = new PostgresQuery({}, templateSrv);
expect(query.target.rawQuery).toBe(false);
});
it('should be in SQL mode for pre query builder queries', function() {
const query = new PostgresQuery({ rawSql: 'SELECT 1' }, templateSrv);
expect(query.target.rawQuery).toBe(true);
});
});
describe('When generating time column SQL', function() {
const query = new PostgresQuery({}, templateSrv);
query.target.timeColumn = 'time';
expect(query.buildTimeColumn()).toBe('time AS "time"');
query.target.timeColumn = '"time"';
expect(query.buildTimeColumn()).toBe('"time" AS "time"');
});
describe('When generating time column SQL with group by time', function() {
let query = new PostgresQuery(
{ timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'none'] }] },
templateSrv
);
expect(query.buildTimeColumn()).toBe('$__timeGroupAlias(time,5m)');
expect(query.buildTimeColumn(false)).toBe('$__timeGroup(time,5m)');
query = new PostgresQuery({ timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'NULL'] }] }, templateSrv);
expect(query.buildTimeColumn()).toBe('$__timeGroupAlias(time,5m,NULL)');
query = new PostgresQuery(
{ timeColumn: 'time', timeColumnType: 'int4', group: [{ type: 'time', params: ['5m', 'none'] }] },
templateSrv
);
expect(query.buildTimeColumn()).toBe('$__unixEpochGroupAlias(time,5m)');
expect(query.buildTimeColumn(false)).toBe('$__unixEpochGroup(time,5m)');
});
describe('When generating metric column SQL', function() {
const query = new PostgresQuery({}, templateSrv);
query.target.metricColumn = 'host';
expect(query.buildMetricColumn()).toBe('host AS metric');
query.target.metricColumn = '"host"';
expect(query.buildMetricColumn()).toBe('"host" AS metric');
});
describe('When generating value column SQL', function() {
const query = new PostgresQuery({}, templateSrv);
let column = [{ type: 'column', params: ['value'] }];
expect(query.buildValueColumn(column)).toBe('value');
column = [{ type: 'column', params: ['value'] }, { type: 'alias', params: ['alias'] }];
expect(query.buildValueColumn(column)).toBe('value AS "alias"');
column = [
{ type: 'column', params: ['v'] },
{ type: 'alias', params: ['a'] },
{ type: 'aggregate', params: ['max'] },
];
expect(query.buildValueColumn(column)).toBe('max(v) AS "a"');
column = [
{ type: 'column', params: ['v'] },
{ type: 'alias', params: ['a'] },
{ type: 'window', params: ['increase'] },
];
expect(query.buildValueColumn(column)).toBe(
'(CASE WHEN v >= lag(v) OVER (ORDER BY time) THEN v - lag(v) OVER (ORDER BY time) ELSE v END) AS "a"'
);
});
describe('When generating value column SQL with metric column', function() {
const query = new PostgresQuery({}, templateSrv);
query.target.metricColumn = 'host';
let column = [{ type: 'column', params: ['value'] }];
expect(query.buildValueColumn(column)).toBe('value');
column = [{ type: 'column', params: ['value'] }, { type: 'alias', params: ['alias'] }];
expect(query.buildValueColumn(column)).toBe('value AS "alias"');
column = [
{ type: 'column', params: ['v'] },
{ type: 'alias', params: ['a'] },
{ type: 'aggregate', params: ['max'] },
];
expect(query.buildValueColumn(column)).toBe('max(v) AS "a"');
column = [
{ type: 'column', params: ['v'] },
{ type: 'alias', params: ['a'] },
{ type: 'window', params: ['increase'] },
];
expect(query.buildValueColumn(column)).toBe(
'(CASE WHEN v >= lag(v) OVER (PARTITION BY host ORDER BY time) THEN v - lag(v) OVER (PARTITION BY host ORDER BY time) ELSE v END) AS "a"'
);
column = [
{ type: 'column', params: ['v'] },
{ type: 'alias', params: ['a'] },
{ type: 'aggregate', params: ['max'] },
{ type: 'window', params: ['increase'] },
];
expect(query.buildValueColumn(column)).toBe(
'(CASE WHEN max(v) >= lag(max(v)) OVER (PARTITION BY host ORDER BY time) ' +
'THEN max(v) - lag(max(v)) OVER (PARTITION BY host ORDER BY time) ELSE max(v) END) AS "a"'
);
});
describe('When generating WHERE clause', function() {
const query = new PostgresQuery({ where: [] }, templateSrv);
expect(query.buildWhereClause()).toBe('');
query.target.timeColumn = 't';
query.target.where = [{ type: 'macro', name: '$__timeFilter' }];
expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t)');
query.target.where = [{ type: 'expression', params: ['v', '=', '1'] }];
expect(query.buildWhereClause()).toBe('\nWHERE\n v = 1');
query.target.where = [{ type: 'macro', name: '$__timeFilter' }, { type: 'expression', params: ['v', '=', '1'] }];
expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t) AND\n v = 1');
});
describe('When generating GROUP BY clause', function() {
const query = new PostgresQuery({ group: [], metricColumn: 'none' }, templateSrv);
expect(query.buildGroupClause()).toBe('');
query.target.group = [{ type: 'time', params: ['5m'] }];
expect(query.buildGroupClause()).toBe('\nGROUP BY 1');
query.target.metricColumn = 'm';
expect(query.buildGroupClause()).toBe('\nGROUP BY 1,2');
});
describe('When generating complete statement', function() {
const target = {
timeColumn: 't',
table: 'table',
select: [[{ type: 'column', params: ['value'] }]],
where: [],
};
let result = 'SELECT\n t AS "time",\n value\nFROM table\nORDER BY 1';
const query = new PostgresQuery(target, templateSrv);
expect(query.buildQuery()).toBe(result);
query.target.metricColumn = 'm';
result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM table\nORDER BY 1';
expect(query.buildQuery()).toBe(result);
});
});

View File

@ -0,0 +1,137 @@
import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part';
const index = [];
function createPart(part): any {
const def = index[part.type];
if (!def) {
return null;
}
return new SqlPart(part, def);
}
function register(options: any) {
index[options.type] = new SqlPartDef(options);
}
register({
type: 'column',
style: 'label',
params: [{ type: 'column', dynamicLookup: true }],
defaultParams: ['value'],
});
register({
type: 'expression',
style: 'expression',
label: 'Expr:',
params: [
{ name: 'left', type: 'string', dynamicLookup: true },
{ name: 'op', type: 'string', dynamicLookup: true },
{ name: 'right', type: 'string', dynamicLookup: true },
],
defaultParams: ['value', '=', 'value'],
});
register({
type: 'macro',
style: 'label',
label: 'Macro:',
params: [],
defaultParams: [],
});
register({
type: 'aggregate',
style: 'label',
params: [
{
name: 'name',
type: 'string',
options: ['avg', 'count', 'min', 'max', 'sum', 'stddev', 'variance'],
},
],
defaultParams: ['avg'],
});
register({
type: 'percentile',
label: 'Aggregate:',
style: 'label',
params: [
{
name: 'name',
type: 'string',
options: ['percentile_cont', 'percentile_disc'],
},
{
name: 'fraction',
type: 'number',
options: ['0.5', '0.75', '0.9', '0.95', '0.99'],
},
],
defaultParams: ['percentile_cont', '0.95'],
});
register({
type: 'alias',
style: 'label',
params: [{ name: 'name', type: 'string', quote: 'double' }],
defaultParams: ['alias'],
});
register({
type: 'time',
style: 'function',
label: 'time',
params: [
{
name: 'interval',
type: 'interval',
options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'],
},
{
name: 'fill',
type: 'string',
options: ['none', 'NULL', 'previous', '0'],
},
],
defaultParams: ['$__interval', 'none'],
});
register({
type: 'window',
style: 'label',
params: [
{
name: 'function',
type: 'string',
options: ['increase', 'rate', 'sum'],
},
],
defaultParams: ['increase'],
});
register({
type: 'moving_window',
style: 'label',
label: 'Moving Window:',
params: [
{
name: 'function',
type: 'string',
options: ['avg'],
},
{
name: 'window_size',
type: 'number',
options: ['3', '5', '7', '10', '20'],
},
],
defaultParams: ['avg', '5'],
});
export default {
create: createPart,
};

View File

@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv';
jest.mock('../datasource');
jest.mock('app/core/services/backend_srv');
describe('Prometheus editor completer', function() {
describe('Prometheus editor completer', () => {
function getSessionStub(data) {
return {
getTokenAt: jest.fn(() => data.currentToken),

View File

@ -437,7 +437,7 @@ describe('PrometheusDatasource', () => {
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query).then(function(data) {
await ctx.ds.query(query).then(data => {
results = data;
});
});
@ -487,7 +487,7 @@ describe('PrometheusDatasource', () => {
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query).then(function(data) {
await ctx.ds.query(query).then(data => {
results = data;
});
});
@ -548,7 +548,7 @@ describe('PrometheusDatasource', () => {
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query).then(function(data) {
await ctx.ds.query(query).then(data => {
results = data;
});
});
@ -603,7 +603,7 @@ describe('PrometheusDatasource', () => {
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.annotationQuery(options).then(function(data) {
await ctx.ds.annotationQuery(options).then(data => {
results = data;
});
});
@ -642,7 +642,7 @@ describe('PrometheusDatasource', () => {
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query).then(function(data) {
await ctx.ds.query(query).then(data => {
results = data;
});
});
@ -1156,7 +1156,7 @@ describe('PrometheusDatasource for POST', () => {
};
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query).then(function(data) {
await ctx.ds.query(query).then(data => {
results = data;
});
});

View File

@ -3,7 +3,7 @@ import { PrometheusDatasource } from '../datasource';
import PrometheusMetricFindQuery from '../metric_find_query';
import q from 'q';
describe('PrometheusMetricFindQuery', function() {
describe('PrometheusMetricFindQuery', () => {
const instanceSettings = {
url: 'proxied',
directUrl: 'direct',

Some files were not shown because too many files have changed in this diff Show More