Compare commits

..

51 Commits

Author SHA1 Message Date
Torkel Ödegaard
3f4c2e7957 Fixed issue with alert links in alert list panel causing panel not found errors, fixes #15680
(cherry picked from commit 91fc73bf82)
2019-03-19 14:06:06 +01:00
Torkel Ödegaard
b7ad897c46 Improved error handling when rendering dashboard panels, fixes #15913
(cherry picked from commit 2333cf3fd1)
2019-03-19 14:06:06 +01:00
Marcus Efraimsson
74c89ecbb9 fix allow anonymous server bind for ldap search
(cherry picked from commit c242d38301)
2019-03-19 14:06:06 +01:00
Marcus Efraimsson
3a206c5493 add nil/length check when delete old login attempts
(cherry picked from commit e3b3062107)
2019-03-19 14:06:06 +01:00
Marcus Efraimsson
3ed4d91aee fix discord notifier so it doesn't crash when there are no image generated
(cherry picked from commit f21c976b27)
2019-03-19 14:06:06 +01:00
Marcus Efraimsson
a5c4dff3b3 fix only users that can edit a dashboard should be able to update panel json
(cherry picked from commit 1a588dadbe)
2019-03-19 14:06:06 +01:00
Peter Holmberg
444a8ecc36 move to new component to handle focus
(cherry picked from commit 98ff39173f)
2019-03-19 14:06:06 +01:00
Peter Holmberg
14666f00f8 added state to not set focus on search every render
(cherry picked from commit 52c2b7606d)
2019-03-19 14:06:06 +01:00
Dominik Prokop
145b1aa0f6 Snapshots update
(cherry picked from commit 9c51912827)
2019-03-19 14:06:06 +01:00
Dominik Prokop
b22d269e9c Use app config directly in ButtonRow instead of passing datasources page URL via prop
(cherry picked from commit 12a868a999)
2019-03-19 14:06:06 +01:00
Dominik Prokop
9c6a72a0c3 Update snapshots
(cherry picked from commit 7fb3cbd72f)
2019-03-19 14:06:06 +01:00
Dominik Prokop
1b667055dc Fixed url of back button in datasource edit page, when root_url configured
(cherry picked from commit d50a7ef6ac)
2019-03-19 14:06:06 +01:00
Hugo Häggmark
1bc6bbfc17 release: Bumped version 2019-03-19 14:06:06 +01:00
Hugo Häggmark
0c44a04ba8 Merge pull request #15824 from grafana/cp-6.0.1
Cherry picks for v6.0.1
2019-03-06 15:21:49 +01:00
Torkel Ödegaard
ae4bdf9403 Bumped version to 6.0.1 2019-03-06 14:32:38 +01:00
Torkel Ödegaard
3d4f08bea5 Temp fix for scrollbar issue PR that was tricky to cherry pick (#15713) 2019-03-06 14:31:05 +01:00
Marcus Efraimsson
97a193d7a5 log phantomjs output even if it timeout and include orgId when render alert
(cherry picked from commit 36f3accf0d)
2019-03-06 14:27:31 +01:00
Torkel Ödegaard
177bee85c6 Fixed image rendering issue for dashboards with auto refresh, casued by missing reloadOnSearch flag on route, fixes #15631
(cherry picked from commit 70f1abbe37)
2019-03-06 14:26:06 +01:00
Marcus Efraimsson
ef3531312c fix allow anonymous initial bind for ldap search
(cherry picked from commit 3b9f0e6ef2)
2019-03-06 14:25:31 +01:00
Jon Ferreira
24da153147 Expose onQueryChange to angular plugins
(cherry picked from commit a3da8dc673)
2019-03-06 14:24:33 +01:00
Torkel Ödegaard
816e81ac0a Fixed scrollbar not visible due to content being added a bit after mount, fixes #15711
(cherry picked from commit cd78f0bef2)
2019-03-06 14:24:16 +01:00
Hugo Häggmark
20d7d4b8c3 fix: update datasource in componentDidUpdate
Closes #15751

(cherry picked from commit 09b036dc93)
2019-03-06 14:22:06 +01:00
Torkel Ödegaard
3e243adc29 Fixed scrolling issue that caused scroll to be locked to the bottom of a long dashboard, fixes #15712
(cherry picked from commit e6a83bf0e1)
2019-03-06 14:21:23 +01:00
Johannes Schill
94e21de199 Viewers with viewers_can_edit should be able to access /explore (#15787)
* fix: Viewers with viewers_can_edit should be able to access /explore #15773

* refactoring initial PR a bit to simplify function and reduce duplication

(cherry picked from commit a81d5486b0)
2019-03-06 14:18:22 +01:00
Daniel Lee
eff00e8cc4 utils: show string errors. Fixes #15782
(cherry picked from commit 8b1e25b50a)
2019-03-06 14:14:27 +01:00
Hugo Häggmark
78d614ab9c Made sure that DataSourceOption displays value and fires onChange/onBlur events (#15757)
* Fixed #15682 

* fix: Add hideTimeOverride to state since we need to control the Switch

* fix: Back the maxDataPoints change, we need to keep it as a string

Co-authored-by:johannes.schill@polyester.se
(cherry picked from commit 48570c6272)
2019-03-06 14:13:37 +01:00
Marcus Efraimsson
073186bd3e org admins should only be able to access org admin pages
(cherry picked from commit 5638c67be8)
2019-03-06 14:12:49 +01:00
Marcus Efraimsson
9044b269a3 only editor/admin should have access to alert list/notifications pages
(cherry picked from commit a29b99b96b)
2019-03-06 14:12:41 +01:00
Johannes Schill
18ac0824bd fix: When in tv-mode, autofitpanel should not take space from the navbar #15650
(cherry picked from commit cc40a515be)
2019-03-06 14:10:12 +01:00
Johannes Schill
967089d179 fix: Kiosk mode should have &kiosk appended to the url #15765
(cherry picked from commit 92ec8757d3)
2019-03-06 14:09:38 +01:00
Jon Ferreira
1dadcb0a5b Toggle stack should trigger a render, not a refresh
(cherry picked from commit 0bdca7957a)
2019-03-06 14:08:07 +01:00
Johannes Schill
e610e4ca41 fix: Return url when query dashboards by tag
(cherry picked from commit 8d5ccc7831)
2019-03-06 14:07:36 +01:00
Peter Holmberg
c946a1fe2f fix
(cherry picked from commit a9ca8e9dec)
2019-03-06 14:06:41 +01:00
Leonard Gram
1a48d82133 service: fix for disabled internal metrics.
Update of the internal metrics for Grafana was
disabled by mistake when refactoring the code.

Fixes #15651

(cherry picked from commit 36788183d8)
2019-03-06 14:05:00 +01:00
Daniel Lee
acedf16a8c stackdriver: fix for float64 bounds for distribution metrics
Adds support for explicit distribution metrics and float64 bounds

Fixes #14509

(cherry picked from commit d1e249a803)
2019-03-06 13:59:30 +01:00
Daniel Lee
3344c53cd5 stackdriver: change reducer mapping for distribution metrics
- Distribution metrics are now mapped to more reducers
when the metric kind is cumulative.
- The witdth of the metrics dropdown is now much wider.
- Changed the text from Select Aggregation to Select Reducer
to line up with the UI in Stackdriver.

(cherry picked from commit 35fc0c5329)
2019-03-06 13:58:28 +01:00
Hugo Häggmark
5854eddb3d Fixed bug with getting teams for user
(cherry picked from commit dafcfd70a7)
2019-03-06 13:55:38 +01:00
Leonard Gram
34a9a621b6 Merge pull request #15567 from grafana/cp-6.0-stable
Cherry picks for 6.0 stable
2019-02-25 15:47:26 +01:00
Leonard Gram
0655a23091 release 6.0.0 2019-02-25 15:24:39 +01:00
Daniel Lee
0f63051a3b graph: fixes click after scroll in series override menu
Makes changes to dropdown-typeahead2 so that a css
class for the button can be passed in. Means it can
be used instead of dropdown-typeahead.

Switches to using dropdown-typeahead2 for series_overrides
directive and for the influxdb, mysql and postgres datasources
as it already contains a fix for this issue.

This commit also fixes the index property which
was set using an incorrectly spelled length property in the
series_overrides directive.

Closes #15621

(cherry picked from commit e76655df43)
2019-02-25 12:50:23 +01:00
Torkel Ödegaard
4a2852d0ed Fixed value dropdown not updating when it's current value updates, fixes #15566
(cherry picked from commit f768808b6e)
2019-02-25 12:39:09 +01:00
Dominik Prokop
682baf8d46 Bring back plugins page styles
(cherry picked from commit f471be2453)
2019-02-25 12:37:26 +01:00
Torkel Ödegaard
9c2adc115c Minor fix/polish to gauge panel and threshold editor
(cherry picked from commit b4627ec302)
2019-02-25 10:38:23 +01:00
Daniel Lee
f93cd857cb panel: defensive coding that fixes #15563
If a plugin incorrectly uses an attribute in the
query-editor-row directive, it should not throw
an exception.

(cherry picked from commit bd0f55cbb8)
2019-02-25 10:23:28 +01:00
Johannes Schill
28d5737221 fix: Filter out values not supported by Explore yet #15281
(cherry picked from commit 6f9edf4a22)
2019-02-25 10:03:40 +01:00
Torkel Ödegaard
d85a4c3dd0 Fixed problem with prettier formatting after cherry picks 2019-02-21 15:24:09 +01:00
Daniel Lee
115215d317 Pass dashboardModel to PanelCtrl class. Fixes #15541
(cherry picked from commit c5a70e9b97)
2019-02-21 12:49:50 +01:00
Marcus Efraimsson
99fecdcf46 fix: mysql query using __interval_ms variable throws error
fixes #14507

(cherry picked from commit d8e655bbcf)
2019-02-21 12:49:31 +01:00
Dominik Prokop
9bfcfe271f Fix build
(cherry picked from commit f28cc871e1)
2019-02-21 12:49:10 +01:00
Dominik Prokop
251596aed4 Display graphite function name editor in a tooltip
(cherry picked from commit 1069d7f5b1)
2019-02-21 12:48:40 +01:00
Dominik Prokop
daa6819e0c Bump Prettier version (#15532)
* Fix prettier version to 1.16.4

(cherry picked from commit 88a46e6dd4)
2019-02-21 12:44:45 +01:00
151 changed files with 1998 additions and 888 deletions

View File

@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.0.0-beta3",
"version": "6.0.2",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -83,7 +83,7 @@
"postcss-browser-reporter": "^0.5.0",
"postcss-loader": "^2.0.6",
"postcss-reporter": "^5.0.0",
"prettier": "1.9.2",
"prettier": "1.16.4",
"react-hot-loader": "^4.3.6",
"react-test-renderer": "^16.5.0",
"redux-mock-store": "^1.5.3",
@@ -129,8 +129,14 @@
}
},
"lint-staged": {
"*.{ts,tsx,json,scss}": ["prettier --write", "git add"],
"*pkg/**/*.go": ["gofmt -w -s", "git add"]
"*.{ts,tsx,json,scss}": [
"prettier --write",
"git add"
],
"*pkg/**/*.go": [
"gofmt -w -s",
"git add"
]
},
"prettier": {
"trailingComma": "es5",
@@ -195,7 +201,12 @@
"**/@types/react": "16.7.6"
},
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/@types/*", "**/@types/*/**"]
"packages": [
"packages/*"
],
"nohoist": [
"**/@types/*",
"**/@types/*/**"
]
}
}

View File

@@ -1,6 +1,6 @@
import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper';
import { PopperController } from '../Tooltip/PopperController';
import { Popper } from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';

View File

@@ -4,7 +4,7 @@ import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsP
import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
import { PopperContentProps } from '../Tooltip/PopperController';
import SpectrumPalette from './SpectrumPalette';
import { GrafanaThemeType } from '@grafana/ui';
import { GrafanaThemeType } from '../../types/theme';
export interface Props<T> extends ColorPickerProps, PopperContentProps {
customPickers?: T;

View File

@@ -14,6 +14,7 @@ interface Props {
scrollTop?: number;
setScrollTop: (event: any) => void;
autoHeightMin?: number | string;
updateAfterMountMs?: number;
}
/**
@@ -42,16 +43,26 @@ export class CustomScrollbar extends PureComponent<Props> {
const ref = this.ref.current;
if (ref && !_.isNil(this.props.scrollTop)) {
if (this.props.scrollTop > 10000) {
ref.scrollToBottom();
} else {
ref.scrollTop(this.props.scrollTop);
}
ref.scrollTop(this.props.scrollTop);
}
}
componentDidMount() {
this.updateScroll();
// this logic is to make scrollbar visible when content is added body after mount
if (this.props.updateAfterMountMs) {
setTimeout(() => this.updateAfterMount(), this.props.updateAfterMountMs);
}
}
updateAfterMount() {
if (this.ref && this.ref.current) {
const scrollbar = this.ref.current as any;
if (scrollbar.update) {
scrollbar.update();
}
}
}
componentDidUpdate() {

View File

@@ -1,4 +1,4 @@
.custom-scrollbars {
.custom-scrollbars {
// Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to
// make scroll working it should fit outer container size (scroll appears only when inner container size is
// greater than outer one).
@@ -14,7 +14,7 @@
.track-vertical {
border-radius: 3px;
width: 6px !important;
right: 2px;
right: 0px;
bottom: 2px;
top: 2px;
}

View File

@@ -1,6 +1,6 @@
import React, { FunctionComponent } from 'react';
import { storiesOf } from '@storybook/react';
import { DeleteButton } from '@grafana/ui';
import { DeleteButton } from './DeleteButton';
const CenteredStory: FunctionComponent<{}> = ({ children }) => {
return (

View File

@@ -0,0 +1,8 @@
.empty-search-result {
border-left: 3px solid $info-box-border-color;
background-color: $empty-list-cta-bg;
padding: $spacer;
min-width: 350px;
border-radius: $border-radius;
margin-bottom: $spacer * 2;
}

View File

@@ -115,9 +115,9 @@ export class Gauge extends PureComponent<Props> {
getFontScale(length: number): number {
if (length > 12) {
return FONT_SCALE - length * 5 / 120;
return FONT_SCALE - (length * 5) / 110;
}
return FONT_SCALE - length * 5 / 105;
return FONT_SCALE - (length * 5) / 100;
}
draw() {

View File

@@ -3,7 +3,8 @@ import { Threshold } from '../../types';
import { ColorPicker } from '..';
import { PanelOptionsGroup } from '..';
import { colors } from '../../utils';
import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui';
import { ThemeContext } from '../../themes/ThemeContext';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
export interface Props {
thresholds: Threshold[];
@@ -54,7 +55,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
// Set a color
const color = colors.filter(c => !newThresholds.some(t => t.color === c))[0];
const color = colors.filter(c => !newThresholds.some(t => t.color === c))[1];
this.setState(
{

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import * as PopperJS from 'popper.js';
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
import { Portal } from '@grafana/ui';
import { Portal } from '../Portal/Portal';
import Transition from 'react-transition-group/Transition';
import { PopperContent } from './PopperController';
@@ -17,12 +17,7 @@ const transitionStyles: { [key: string]: object } = {
exiting: { opacity: 0, transitionDelay: '500ms' },
};
export type RenderPopperArrowFn = (
props: {
arrowProps: PopperArrowProps;
placement: string;
}
) => JSX.Element;
export type RenderPopperArrowFn = (props: { arrowProps: PopperArrowProps; placement: string }) => JSX.Element;
interface Props extends React.HTMLAttributes<HTMLDivElement> {
show: boolean;
@@ -58,7 +53,7 @@ class Popper extends PureComponent<Props> {
// TODO: move modifiers config to popper controller
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
>
{({ ref, style, placement, arrowProps }) => {
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
return (
<div
onMouseEnter={onMouseEnter}
@@ -73,7 +68,12 @@ class Popper extends PureComponent<Props> {
className={`${wrapperClassName}`}
>
<div className={className}>
{typeof content === 'string' ? content : React.cloneElement(content)}
{typeof content === 'string' && content}
{React.isValidElement(content) && React.cloneElement(content)}
{typeof content === 'function' &&
content({
updatePopperPosition: scheduleUpdate,
})}
{renderArrow &&
renderArrow({
arrowProps,
@@ -93,4 +93,4 @@ class Popper extends PureComponent<Props> {
}
}
export default Popper;
export { Popper };

View File

@@ -7,7 +7,7 @@ export interface PopperContentProps {
updatePopperPosition?: () => void;
}
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T>;
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T> | ((props: T) => JSX.Element);
export interface UsingPopperProps {
show?: boolean;
@@ -101,4 +101,4 @@ class PopperController extends React.Component<Props, State> {
}
}
export default PopperController;
export { PopperController };

View File

@@ -1,7 +1,7 @@
import React, { createRef } from 'react';
import * as PopperJS from 'popper.js';
import Popper from './Popper';
import PopperController, { UsingPopperProps } from './PopperController';
import { Popper } from './Popper';
import { PopperController, UsingPopperProps } from './PopperController';
interface TooltipProps extends UsingPopperProps {
theme?: 'info' | 'error';

View File

@@ -1,5 +1,7 @@
export { DeleteButton } from './DeleteButton/DeleteButton';
export { Tooltip } from './Tooltip/Tooltip';
export { PopperController } from './Tooltip/PopperController';
export { Popper } from './Tooltip/Popper';
export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';

View File

@@ -125,7 +125,7 @@ export const getCategories = (): ValueFormatCategory[] => [
{
name: 'Data (Metric)',
formats: [
{ name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') },
{ name: 'bits', id: 'decbits', fn: decimalSIPrefix('b') },
{ name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') },
{ name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
{ name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },

View File

@@ -33,17 +33,17 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/profile/", reqSignedIn, hs.Index)
r.Get("/profile/password", reqSignedIn, hs.Index)
r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
r.Get("/org/", reqSignedIn, hs.Index)
r.Get("/org/new", reqSignedIn, hs.Index)
r.Get("/datasources/", reqSignedIn, hs.Index)
r.Get("/datasources/new", reqSignedIn, hs.Index)
r.Get("/datasources/edit/*", reqSignedIn, hs.Index)
r.Get("/org/users", reqSignedIn, hs.Index)
r.Get("/org/users/new", reqSignedIn, hs.Index)
r.Get("/org/users/invite", reqSignedIn, hs.Index)
r.Get("/org/teams", reqSignedIn, hs.Index)
r.Get("/org/teams/*", reqSignedIn, hs.Index)
r.Get("/org/apikeys/", reqSignedIn, hs.Index)
r.Get("/org/", reqOrgAdmin, hs.Index)
r.Get("/org/new", reqGrafanaAdmin, hs.Index)
r.Get("/datasources/", reqOrgAdmin, hs.Index)
r.Get("/datasources/new", reqOrgAdmin, hs.Index)
r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index)
r.Get("/org/users", reqOrgAdmin, hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
r.Get("/org/teams", reqOrgAdmin, hs.Index)
r.Get("/org/teams/*", reqOrgAdmin, hs.Index)
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
r.Get("/admin", reqGrafanaAdmin, hs.Index)
@@ -73,12 +73,12 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboards/", reqSignedIn, hs.Index)
r.Get("/dashboards/*", reqSignedIn, hs.Index)
r.Get("/explore", reqEditorRole, hs.Index)
r.Get("/explore", reqSignedIn, middleware.EnsureEditorOrViewerCanEdit, hs.Index)
r.Get("/playlists/", reqSignedIn, hs.Index)
r.Get("/playlists/*", reqSignedIn, hs.Index)
r.Get("/alerting/", reqSignedIn, hs.Index)
r.Get("/alerting/*", reqSignedIn, hs.Index)
r.Get("/alerting/", reqEditorRole, hs.Index)
r.Get("/alerting/*", reqEditorRole, hs.Index)
// sign up
r.Get("/signup", hs.Index)

View File

@@ -52,8 +52,10 @@ func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboar
for _, item := range searchQuery.Result {
result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
Uri: item.Uri,
Url: m.GetDashboardUrl(item.Uid, item.Slug),
Order: dashboardTagOrder[tag],
})
}

View File

@@ -121,7 +121,7 @@ func GetUserTeams(c *m.ReqContext) Response {
return getUserTeamList(c.OrgId, c.ParamsInt64(":id"))
}
func getUserTeamList(userID int64, orgID int64) Response {
func getUserTeamList(orgID int64, userID int64) Response {
query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID}
if err := bus.Dispatch(&query); err != nil {

View File

@@ -29,6 +29,8 @@ import (
// self registering services
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/infra/serverlock"
_ "github.com/grafana/grafana/pkg/infra/usagestats"
_ "github.com/grafana/grafana/pkg/metrics"
_ "github.com/grafana/grafana/pkg/plugins"
_ "github.com/grafana/grafana/pkg/services/alerting"

View File

@@ -25,6 +25,7 @@ var filters map[string]log15.Lvl
func init() {
loggersToClose = make([]DisposableHandler, 0)
loggersToReload = make([]ReloadableHandler, 0)
filters = map[string]log15.Lvl{}
Root = log15.Root()
Root.SetHandler(log15.DiscardHandler())
}
@@ -197,7 +198,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
// Log level.
_, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
filters := getFilters(util.SplitString(sec.Key("filters").String()))
modeFilters := getFilters(util.SplitString(sec.Key("filters").String()))
format := getLogFormat(sec.Key("format").MustString(""))
var handler log15.Handler
@@ -230,12 +231,18 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
}
for key, value := range defaultFilters {
if _, exist := modeFilters[key]; !exist {
modeFilters[key] = value
}
}
for key, value := range modeFilters {
if _, exist := filters[key]; !exist {
filters[key] = value
}
}
handler = LogFilterHandler(level, filters, handler)
handler = LogFilterHandler(level, modeFilters, handler)
handlers = append(handlers, handler)
}

View File

@@ -18,6 +18,7 @@ import (
type ILdapConn interface {
Bind(username, password string) error
UnauthenticatedBind(username string) error
Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
StartTLS(*tls.Config) error
Close()
@@ -218,8 +219,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
}
func (a *ldapAuther) serverBind() error {
bindFn := func() error {
return a.conn.Bind(a.server.BindDN, a.server.BindPassword)
}
if a.server.BindPassword == "" {
bindFn = func() error {
return a.conn.UnauthenticatedBind(a.server.BindDN)
}
}
// bind_dn and bind_password to bind
if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil {
if err := bindFn(); err != nil {
a.log.Info("LDAP initial bind failed, %v", err)
if ldapErr, ok := err.(*ldap.Error); ok {
@@ -259,7 +270,17 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
bindPath = fmt.Sprintf(a.server.BindDN, username)
}
if err := a.conn.Bind(bindPath, userPassword); err != nil {
bindFn := func() error {
return a.conn.Bind(bindPath, userPassword)
}
if userPassword == "" {
bindFn = func() error {
return a.conn.UnauthenticatedBind(bindPath)
}
}
if err := bindFn(); err != nil {
a.log.Info("Initial bind failed", "error", err)
if ldapErr, ok := err.(*ldap.Error); ok {

View File

@@ -13,6 +13,133 @@ import (
)
func TestLdapAuther(t *testing.T) {
Convey("initialBind", t, func() {
Convey("Given bind dn and password configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := ldapAuther.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(ldapAuther.requireSecondBind, ShouldBeTrue)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
},
}
err := ldapAuther.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(ldapAuther.requireSecondBind, ShouldBeFalse)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "pwd")
})
Convey("Given empty bind dn and password", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{},
}
err := ldapAuther.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(ldapAuther.requireSecondBind, ShouldBeTrue)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("serverBind", t, func() {
Convey("Given bind dn and password configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := ldapAuther.serverBind()
So(err, ShouldBeNil)
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "o=users,dc=grafana,dc=org",
},
}
err := ldapAuther.serverBind()
So(err, ShouldBeNil)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
})
Convey("Given empty bind dn and password", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{},
}
err := ldapAuther.serverBind()
So(err, ShouldBeNil)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("When translating ldap user to grafana user", t, func() {
@@ -365,12 +492,26 @@ func TestLdapAuther(t *testing.T) {
}
type mockLdapConn struct {
result *ldap.SearchResult
searchCalled bool
searchAttributes []string
result *ldap.SearchResult
searchCalled bool
searchAttributes []string
bindProvider func(username, password string) error
unauthenticatedBindProvider func(username string) error
}
func (c *mockLdapConn) Bind(username, password string) error {
if c.bindProvider != nil {
return c.bindProvider(username, password)
}
return nil
}
func (c *mockLdapConn) UnauthenticatedBind(username string) error {
if c.unauthenticatedBindProvider != nil {
return c.unauthenticatedBindProvider(username)
}
return nil
}

View File

@@ -4,7 +4,7 @@ import (
"net/url"
"strings"
"gopkg.in/macaron.v1"
macaron "gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@@ -52,6 +52,12 @@ func notAuthorized(c *m.ReqContext) {
c.Redirect(setting.AppSubUrl + "/login")
}
func EnsureEditorOrViewerCanEdit(c *m.ReqContext) {
if !c.SignedInUser.HasRole(m.ROLE_EDITOR) && !setting.ViewersCanEdit {
accessForbidden(c)
}
}
func RoleAuth(roles ...m.RoleType) macaron.Handler {
return func(c *m.ReqContext) {
ok := false

View File

@@ -138,7 +138,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return err
}
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, context.Rule.OrgId, context.Rule.PanelId)
result, err := n.renderService.Render(context.Ctx, renderOpts)
if err != nil {

View File

@@ -111,57 +111,20 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
json, _ := bodyJSON.MarshalJSON()
content_type := "application/json"
var body []byte
if embeddedImage {
var b bytes.Buffer
w := multipart.NewWriter(&b)
f, err := os.Open(evalContext.ImageOnDiskPath)
if err != nil {
this.log.Error("Can't open graph file", err)
return err
}
defer f.Close()
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(json))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
body = b.Bytes()
content_type = w.FormDataContentType()
} else {
body = json
}
cmd := &m.SendWebhookSync{
Url: this.WebhookURL,
Body: string(body),
HttpMethod: "POST",
ContentType: content_type,
ContentType: "application/json",
}
if !embeddedImage {
cmd.Body = string(json)
} else {
err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
if err != nil {
this.log.Error("failed to embed image", "error", err)
return err
}
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
@@ -171,3 +134,45 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
f, err := os.Open(imagePath)
defer f.Close()
if err != nil {
if os.IsNotExist(err) {
cmd.Body = string(existingJSONBody)
return nil
}
if !os.IsNotExist(err) {
return err
}
}
var b bytes.Buffer
w := multipart.NewWriter(&b)
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(existingJSONBody))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
cmd.Body = string(b.Bytes())
cmd.ContentType = w.FormDataContentType()
return nil
}

View File

@@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
defer middleware.RemoveRenderAuthKey(renderKey)
phantomDebugArg := "--debug=false"
if log.GetLogLevelFor("renderer") >= log.LvlDebug {
if log.GetLogLevelFor("rendering") >= log.LvlDebug {
phantomDebugArg = "--debug=true"
}
@@ -64,13 +64,26 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
cmd.Stderr = cmd.Stdout
timezone := ""
if opts.Timezone != "" {
timezone = isoTimeOffsetToPosixTz(opts.Timezone)
baseEnviron := os.Environ()
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(opts.Timezone))
cmd.Env = appendEnviron(baseEnviron, "TZ", timezone)
}
rs.log.Debug("executing Phantomjs", "binPath", binPath, "cmdArgs", cmdArgs, "timezone", timezone)
out, err := cmd.Output()
if out != nil {
rs.log.Debug("Phantomjs output", "out", string(out))
}
if err != nil {
rs.log.Debug("Phantomjs error", "error", err)
}
// check for timeout first
if commandCtx.Err() == context.DeadlineExceeded {
rs.log.Info("Rendering timed out")
@@ -82,8 +95,6 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
return nil, err
}
rs.log.Debug("Phantomjs output", "out", string(out))
rs.log.Debug("Image rendered", "path", pngPath)
return &RenderResult{FilePath: pngPath}, nil
}

View File

@@ -17,6 +17,7 @@ type Hit struct {
Title string `json:"title"`
Uri string `json:"uri"`
Url string `json:"url"`
Slug string `json:"slug"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`

View File

@@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
return err
}
if result == nil || len(result) == 0 || result[0] == nil {
return nil
}
maxId = toInt64(result[0]["id"])
if maxId == 0 {

View File

@@ -336,6 +336,8 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
return StackdriverResponse{}, err
}
// slog.Info("stackdriver", "response", string(body))
if res.StatusCode/100 != 2 {
slog.Error("Request failed", "status", res.Status, "body", string(body))
return StackdriverResponse{}, fmt.Errorf(string(body))
@@ -559,7 +561,7 @@ func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string {
} else if bucketOptions.ExponentialBuckets != nil {
bucketBound = strconv.FormatInt(int64(bucketOptions.ExponentialBuckets.Scale*math.Pow(bucketOptions.ExponentialBuckets.GrowthFactor, float64(n-1))), 10)
} else if bucketOptions.ExplicitBuckets != nil {
bucketBound = strconv.FormatInt(bucketOptions.ExplicitBuckets.Bounds[(n-1)], 10)
bucketBound = fmt.Sprintf("%g", bucketOptions.ExplicitBuckets.Bounds[n])
}
return bucketBound
}

View File

@@ -344,8 +344,8 @@ func TestStackdriver(t *testing.T) {
})
})
Convey("when data from query is distribution", func() {
data, err := loadTestFile("./test-data/3-series-response-distribution.json")
Convey("when data from query is distribution with exponential bounds", func() {
data, err := loadTestFile("./test-data/3-series-response-distribution-exponential.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 1)
@@ -370,6 +370,14 @@ func TestStackdriver(t *testing.T) {
So(res.Series[0].Points[2][1].Float64, ShouldEqual, 1536669060000)
})
Convey("bucket bounds should be correct", func() {
So(res.Series[0].Name, ShouldEqual, "0")
So(res.Series[1].Name, ShouldEqual, "1")
So(res.Series[2].Name, ShouldEqual, "2")
So(res.Series[3].Name, ShouldEqual, "4")
So(res.Series[4].Name, ShouldEqual, "8")
})
Convey("value should be correct", func() {
So(res.Series[8].Points[0][0].Float64, ShouldEqual, 1)
So(res.Series[9].Points[0][0].Float64, ShouldEqual, 1)
@@ -383,6 +391,45 @@ func TestStackdriver(t *testing.T) {
})
})
Convey("when data from query is distribution with explicit bounds", func() {
data, err := loadTestFile("./test-data/4-series-response-distribution-explicit.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 1)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{bucket}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 33)
for i := 0; i < 33; i++ {
if i == 0 {
So(res.Series[i].Name, ShouldEqual, "0")
}
So(len(res.Series[i].Points), ShouldEqual, 2)
}
Convey("timestamps should be in ascending order", func() {
So(res.Series[0].Points[0][1].Float64, ShouldEqual, 1550859086000)
So(res.Series[0].Points[1][1].Float64, ShouldEqual, 1550859146000)
})
Convey("bucket bounds should be correct", func() {
So(res.Series[0].Name, ShouldEqual, "0")
So(res.Series[1].Name, ShouldEqual, "0.01")
So(res.Series[2].Name, ShouldEqual, "0.05")
So(res.Series[3].Name, ShouldEqual, "0.1")
})
Convey("value should be correct", func() {
So(res.Series[8].Points[0][0].Float64, ShouldEqual, 381)
So(res.Series[9].Points[0][0].Float64, ShouldEqual, 212)
So(res.Series[10].Points[0][0].Float64, ShouldEqual, 56)
So(res.Series[8].Points[1][0].Float64, ShouldEqual, 375)
So(res.Series[9].Points[1][0].Float64, ShouldEqual, 213)
So(res.Series[10].Points[1][0].Float64, ShouldEqual, 56)
})
})
})
Convey("when interpolating filter wildcards", func() {

View File

@@ -0,0 +1,209 @@
{
"timeSeries": [
{
"metric": {
"type": "custom.googleapis.com\/opencensus\/grpc.io\/client\/roundtrip_latency"
},
"resource": {
"type": "global",
"labels": {
"project_id": "grafana-demo"
}
},
"metricKind": "DELTA",
"valueType": "DISTRIBUTION",
"points": [
{
"interval": {
"startTime": "2019-02-22T18:11:26Z",
"endTime": "2019-02-22T18:12:26Z"
},
"value": {
"distributionValue": {
"count": "1878",
"mean": 17.813718392255,
"sumOfSquaredDeviation": 7141630.651914,
"bucketOptions": {
"explicitBuckets": {
"bounds": [
0,
0.01,
0.05,
0.1,
0.3,
0.6,
0.8,
1,
2,
3,
4,
5,
6,
8,
10,
13,
16,
20,
25,
30,
40,
50,
65,
80,
100,
130,
160,
200,
250,
300,
400,
500,
650,
800,
1000,
2000,
5000,
10000,
20000,
50000,
100000
]
}
},
"bucketCounts": [
"0",
"0",
"0",
"0",
"8",
"403",
"297",
"184",
"375",
"213",
"56",
"31",
"15",
"13",
"4",
"1",
"5",
"2",
"8",
"13",
"26",
"13",
"45",
"48",
"61",
"10",
"3",
"6",
"7",
"4",
"7",
"12",
"8"
]
}
}
},
{
"interval": {
"startTime": "2019-02-22T18:10:26Z",
"endTime": "2019-02-22T18:11:26Z"
},
"value": {
"distributionValue": {
"count": "1887",
"mean": 17.654277577766,
"sumOfSquaredDeviation": 7082587.2133073,
"bucketOptions": {
"explicitBuckets": {
"bounds": [
0,
0.01,
0.05,
0.1,
0.3,
0.6,
0.8,
1,
2,
3,
4,
5,
6,
8,
10,
13,
16,
20,
25,
30,
40,
50,
65,
80,
100,
130,
160,
200,
250,
300,
400,
500,
650,
800,
1000,
2000,
5000,
10000,
20000,
50000,
100000
]
}
},
"bucketCounts": [
"0",
"0",
"0",
"0",
"8",
"404",
"298",
"187",
"381",
"212",
"56",
"31",
"15",
"14",
"4",
"1",
"4",
"2",
"9",
"13",
"24",
"13",
"46",
"46",
"61",
"11",
"3",
"6",
"7",
"5",
"7",
"11",
"8"
]
}
}
}
]
}
]
}

View File

@@ -26,7 +26,7 @@ type StackdriverBucketOptions struct {
Scale float64 `json:"scale"`
} `json:"exponentialBuckets"`
ExplicitBuckets *struct {
Bounds []int64 `json:"bounds"`
Bounds []float64 `json:"bounds"`
} `json:"explicitBuckets"`
}

View File

@@ -10,10 +10,12 @@ import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
react2AngularDirective('sidemenu', SideMenu, []);
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
react2AngularDirective('appNotificationsList', AppNotificationList, []);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);

View File

@@ -61,15 +61,14 @@ export default class PermissionsListItem extends PureComponent<Props> {
{item.name} <ItemDescription item={item} />
</td>
<td>
{item.inherited &&
folderInfo && (
<em className="muted no-wrap">
Inherited from folder{' '}
<a className="text-link" href={`${folderInfo.url}/permissions`}>
{folderInfo.title}
</a>{' '}
</em>
)}
{item.inherited && folderInfo && (
<em className="muted no-wrap">
Inherited from folder{' '}
<a className="text-link" href={`${folderInfo.url}/permissions`}>
{folderInfo.title}
</a>{' '}
</em>
)}
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
</td>
<td className="query-keyword">Can</td>

View File

@@ -3,14 +3,14 @@
/*
* Escapes `"` characters from string
*/
*/
function escapeString(str: string): string {
return str.replace('"', '"');
}
/*
* Determines if a value is an object
*/
*/
export function isObject(value: any): boolean {
const type = typeof value;
return !!value && type === 'object';
@@ -20,7 +20,7 @@ export function isObject(value: any): boolean {
* Gets constructor name of an object.
* From http://stackoverflow.com/a/332429
*
*/
*/
export function getObjectName(object: object): string {
if (object === undefined) {
return '';
@@ -43,7 +43,7 @@ export function getObjectName(object: object): string {
/*
* Gets type of an object. Returns "null" for null objects
*/
*/
export function getType(object: object): string {
if (object === null) {
return 'null';
@@ -53,7 +53,7 @@ export function getType(object: object): string {
/*
* Generates inline preview for a JavaScript object based on a value
*/
*/
export function getValuePreview(object: object, value: string): string {
const type = getType(object);
@@ -78,7 +78,7 @@ export function getValuePreview(object: object, value: string): string {
/*
* Generates inline preview for a JavaScript object
*/
*/
let value = '';
export function getPreview(obj: object): string {
if (isObject(obj)) {
@@ -94,15 +94,15 @@ export function getPreview(obj: object): string {
/*
* Generates a prefixed CSS class name
*/
*/
export function cssClass(className: string): string {
return `json-formatter-${className}`;
}
/*
* Creates a new DOM element with given type and class
* TODO: move me to helpers
*/
* Creates a new DOM element with given type and class
* TODO: move me to helpers
*/
export function createElement(type: string, className?: string, content?: Element | string): Element {
const el = document.createElement(type);
if (className) {

View File

@@ -83,7 +83,7 @@ export class JsonExplorer {
/*
* is formatter open?
*/
*/
private get isOpen(): boolean {
if (this._isOpen !== null) {
return this._isOpen;
@@ -94,14 +94,14 @@ export class JsonExplorer {
/*
* set open state (from toggler)
*/
*/
private set isOpen(value: boolean) {
this._isOpen = value;
}
/*
* is this a date string?
*/
*/
private get isDate(): boolean {
return (
this.type === 'string' &&
@@ -111,14 +111,14 @@ export class JsonExplorer {
/*
* is this a URL string?
*/
*/
private get isUrl(): boolean {
return this.type === 'string' && this.json.indexOf('http') === 0;
}
/*
* is this an array?
*/
*/
private get isArray(): boolean {
return Array.isArray(this.json);
}
@@ -126,21 +126,21 @@ export class JsonExplorer {
/*
* is this an object?
* Note: In this context arrays are object as well
*/
*/
private get isObject(): boolean {
return isObject(this.json);
}
/*
* is this an empty object with no properties?
*/
*/
private get isEmptyObject(): boolean {
return !this.keys.length && !this.isArray;
}
/*
* is this an empty object or array?
*/
*/
private get isEmpty(): boolean {
return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
}
@@ -148,14 +148,14 @@ export class JsonExplorer {
/*
* did we receive a key argument?
* This means that the formatter was called as a sub formatter of a parent formatter
*/
*/
private get hasKey(): boolean {
return typeof this.key !== 'undefined';
}
/*
* if this is an object, get constructor function name
*/
*/
private get constructorName(): string {
return getObjectName(this.json);
}
@@ -163,7 +163,7 @@ export class JsonExplorer {
/*
* get type of this value
* Possible values: all JavaScript primitive types plus "array" and "null"
*/
*/
private get type(): string {
return getType(this.json);
}
@@ -171,7 +171,7 @@ export class JsonExplorer {
/*
* get object keys
* If there is an empty key we pad it wit quotes to make it visible
*/
*/
private get keys(): string[] {
if (this.isObject) {
return Object.keys(this.json).map(key => (key ? key : '""'));

View File

@@ -47,7 +47,8 @@ class BottomNavLinks extends PureComponent<Props> {
<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
<i className="fa fa-fw fa-random" />
Switch
</div>
</a>
</li>

View File

@@ -29,7 +29,8 @@ export class SideMenu extends PureComponent {
<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
<i className="fa fa-times" />
&nbsp;Close
</span>
</div>,
<TopSection key="topsection" />,

View File

@@ -5,7 +5,7 @@ export class JsonEditorCtrl {
/** @ngInject */
constructor($scope) {
$scope.json = angular.toJson($scope.model.object, true);
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.contextSrv.isEditor;
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.model.canUpdate;
$scope.canCopy = $scope.model.enableCopy;
$scope.update = () => {

View File

@@ -128,7 +128,7 @@ export function dropdownTypeahead2($compile) {
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
const buttonTemplate =
'<a class="gf-form-input dropdown-toggle"' +
'<a class="{{buttonTemplateClass}} dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' ><i class="fa fa-plus"></i></a>';
@@ -137,9 +137,15 @@ export function dropdownTypeahead2($compile) {
menuItems: '=dropdownTypeahead2',
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
model: '=ngModel',
buttonTemplateClass: '@',
},
link: ($scope, elem, attrs) => {
const $input = $(inputTemplate);
if (!$scope.buttonTemplateClass) {
$scope.buttonTemplateClass = 'gf-form-input';
}
const $button = $(buttonTemplate);
const timeoutId = {
blur: null,

View File

@@ -240,7 +240,7 @@ export class ValueSelectDropdownCtrl {
/** @ngInject */
export function valueSelectDropdown($compile, $window, $timeout, $rootScope) {
return {
scope: { variable: '=', onUpdated: '&' },
scope: { dashboard: '=', variable: '=', onUpdated: '&' },
templateUrl: 'public/app/partials/valueSelectDropdown.html',
controller: 'ValueSelectDropdownCtrl',
controllerAs: 'vm',
@@ -288,13 +288,13 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) {
}
});
const cleanUp = $rootScope.$on('template-variable-value-updated', () => {
scope.vm.updateLinkText();
});
scope.$on('$destroy', () => {
cleanUp();
});
scope.vm.dashboard.on(
'template-variable-value-updated',
() => {
scope.vm.updateLinkText();
},
scope
);
scope.vm.init();
},

View File

@@ -0,0 +1,55 @@
import { getMessageFromError } from 'app/core/utils/errors';
describe('errors functions', () => {
let message;
describe('when getMessageFromError gets an error string', () => {
beforeEach(() => {
message = getMessageFromError('error string');
});
it('should return the string', () => {
expect(message).toBe('error string');
});
});
describe('when getMessageFromError gets an error object with message field', () => {
beforeEach(() => {
message = getMessageFromError({ message: 'error string' });
});
it('should return the message text', () => {
expect(message).toBe('error string');
});
});
describe('when getMessageFromError gets an error object with data.message field', () => {
beforeEach(() => {
message = getMessageFromError({ data: { message: 'error string' } });
});
it('should return the message text', () => {
expect(message).toBe('error string');
});
});
describe('when getMessageFromError gets an error object with statusText field', () => {
beforeEach(() => {
message = getMessageFromError({ statusText: 'error string' });
});
it('should return the statusText text', () => {
expect(message).toBe('error string');
});
});
describe('when getMessageFromError gets an error object', () => {
beforeEach(() => {
message = getMessageFromError({ customError: 'error string' });
});
it('should return the stringified error', () => {
expect(message).toBe('{"customError":"error string"}');
});
});
});

View File

@@ -13,5 +13,5 @@ export function getMessageFromError(err: any): string | null {
}
}
return null;
return err;
}

View File

@@ -153,7 +153,7 @@ export function buildQueryTransaction(
};
}
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');

View File

@@ -143,7 +143,7 @@ kbn.secondsToHhmmss = seconds => {
};
kbn.to_percent = (nr, outof) => {
return Math.floor(nr / outof * 10000) / 100 + '%';
return Math.floor((nr / outof) * 10000) / 100 + '%';
};
kbn.addslashes = str => {

View File

@@ -149,4 +149,9 @@ const mapDispatchToProps = {
togglePauseAlertRule,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(AlertRuleList)
);

View File

@@ -263,4 +263,9 @@ const mapDispatchToProps = {
addApiKey,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(ApiKeysPage)
);

View File

@@ -32,7 +32,7 @@
.add-panel-widget__title {
font-size: $font-size-md;
font-weight: $font-weight-semi-bold;
margin-right: $spacer*2;
margin-right: $spacer * 2;
}
.add-panel-widget__link {

View File

@@ -267,4 +267,7 @@ const mapDispatchToProps = {
updateLocation,
};
export default connect(mapStateToProps, mapDispatchToProps)(DashNav);
export default connect(
mapStateToProps,
mapDispatchToProps
)(DashNav);

View File

@@ -4,7 +4,7 @@
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
{{variable.label || variable.name}}
</label>
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" dashboard="ctrl.dashboard" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
</div>
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable" dashboard="ctrl.dashboard"></ad-hoc-filters>

View File

@@ -92,11 +92,12 @@ export class DashboardPage extends PureComponent<Props, State> {
componentWillUnmount() {
if (this.props.dashboard) {
this.props.cleanUpDashboard();
this.setPanelFullscreenClass(false);
}
}
componentDidUpdate(prevProps: Props) {
const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props;
const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId, urlUid } = this.props;
if (!dashboard) {
return;
@@ -107,6 +108,12 @@ export class DashboardPage extends PureComponent<Props, State> {
document.title = dashboard.title + ' - Grafana';
}
// Due to the angular -> react url bridge we can ge an update here with new uid before the container unmounts
// Can remove this condition after we switch to react router
if (prevProps.urlUid !== urlUid) {
return;
}
// handle animation states when opening dashboard settings
if (!prevProps.editview && editview) {
this.setState({ isSettingsOpening: true });
@@ -163,14 +170,20 @@ export class DashboardPage extends PureComponent<Props, State> {
fullscreenPanel: null,
scrollTop: this.state.rememberScrollTop,
},
() => {
dashboard.render();
}
this.triggerPanelsRendering.bind(this)
);
this.setPanelFullscreenClass(false);
}
triggerPanelsRendering() {
try {
this.props.dashboard.render();
} catch (err) {
this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
}
}
handleFullscreenPanelNotFound(urlPanelId: string) {
// Panel not found
this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));
@@ -268,7 +281,12 @@ export class DashboardPage extends PureComponent<Props, State> {
onAddPanel={this.onAddPanel}
/>
<div className="scroll-canvas scroll-canvas--dashboard">
<CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}>
<CustomScrollbar
autoHeightMin={'100%'}
setScrollTop={this.setScrollTop}
scrollTop={scrollTop}
updateAfterMountMs={500}
>
{editview && <DashboardSettings dashboard={dashboard} />}
{initError && this.renderInitFailedState()}
@@ -306,4 +324,9 @@ const mapDispatchToProps = {
updateLocation,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(DashboardPage)
);

View File

@@ -107,4 +107,9 @@ const mapDispatchToProps = {
initDashboard,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(SoloPanelPage)
);

View File

@@ -113,6 +113,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
hideTracksWhenNotNeeded={false}
scrollTop={0}
setScrollTop={[Function]}
updateAfterMountMs={500}
>
<div
className="dashboard-container"
@@ -349,6 +350,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
hideTracksWhenNotNeeded={false}
scrollTop={0}
setScrollTop={[Function]}
updateAfterMountMs={500}
>
<DashboardSettings
dashboard={

View File

@@ -49,21 +49,20 @@ export class PanelHeaderCorner extends Component<Props> {
return (
<div className="markdown-html">
<div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
{panel.links &&
panel.links.length > 0 && (
<ul className="text-left">
{panel.links.map((link, idx) => {
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
return (
<li key={idx}>
<a className="panel-menu-link" href={info.href} target={info.target}>
{info.title}
</a>
</li>
);
})}
</ul>
)}
{panel.links && panel.links.length > 0 && (
<ul className="text-left">
{panel.links.map((link, idx) => {
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
return (
<li key={idx}>
<a className="panel-menu-link" href={info.href} target={info.target}>
{info.title}
</a>
</li>
);
})}
</ul>
)}
</div>
);
};

View File

@@ -1,16 +1,17 @@
import React, { FC } from 'react';
import React, { FC, ChangeEvent } from 'react';
import { FormLabel } from '@grafana/ui';
interface Props {
label: string;
placeholder?: string;
name?: string;
value?: string;
onChange?: (evt: any) => void;
name: string;
value: string;
onBlur: (event: ChangeEvent<HTMLInputElement>) => void;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
tooltipInfo?: any;
}
export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
export const DataSourceOption: FC<Props> = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => {
return (
<div className="gf-form gf-form--flex-end">
<FormLabel tooltip={tooltipInfo}>{label}</FormLabel>
@@ -20,10 +21,10 @@ export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value,
placeholder={placeholder}
name={name}
spellCheck={false}
onBlur={evt => onChange(evt.target.value)}
onBlur={onBlur}
onChange={onChange}
value={value}
/>
</div>
);
};
export default DataSourceOptions;

View File

@@ -118,7 +118,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
{toolbarItems.map(item => this.renderButton(item))}
</div>
<div className="panel-editor__scroll">
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop} updateAfterMountMs={300}>
<div className="panel-editor__content">
<FadeIn in={isOpen} duration={200} unmountOnExit={true}>
{openView && this.renderOpenView(openView)}

View File

@@ -176,7 +176,7 @@ export class QueriesTab extends PureComponent<Props, State> {
};
render() {
const { panel } = this.props;
const { panel, dashboard } = this.props;
const { currentDS, scrollTop } = this.state;
const queryInspector: EditorToolbarView = {
@@ -205,6 +205,7 @@ export class QueriesTab extends PureComponent<Props, State> {
dataSourceValue={query.datasource || panel.datasource}
key={query.refId}
panel={panel}
dashboard={dashboard}
query={query}
onChange={query => this.onQueryChange(query, index)}
onRemoveQuery={this.onRemoveQuery}

View File

@@ -12,10 +12,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
// Types
import { PanelModel } from '../state/PanelModel';
import { DataQuery, DataSourceApi, TimeRange } from '@grafana/ui';
import { DashboardModel } from '../state/DashboardModel';
interface Props {
panel: PanelModel;
query: DataQuery;
dashboard: DashboardModel;
onAddQuery: (query?: DataQuery) => void;
onRemoveQuery: (query: DataQuery) => void;
onMoveQuery: (query: DataQuery, direction: number) => void;
@@ -83,13 +85,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
};
getAngularQueryComponentScope(): AngularQueryComponentScope {
const { panel, query } = this.props;
const { panel, query, dashboard } = this.props;
const { datasource } = this.state;
return {
datasource: datasource,
target: query,
panel: panel,
dashboard: dashboard,
refresh: () => panel.refresh(),
render: () => panel.render(),
events: panel.events,
@@ -265,6 +268,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
export interface AngularQueryComponentScope {
target: DataQuery;
panel: PanelModel;
dashboard: DashboardModel;
events: Emitter;
refresh: () => void;
render: () => void;

View File

@@ -1,5 +1,5 @@
// Libraries
import React, { PureComponent } from 'react';
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
// Utils
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
@@ -9,7 +9,7 @@ import { Switch } from '@grafana/ui';
import { Input } from 'app/core/components/Form';
import { EventsWithValidation } from 'app/core/components/Form/Input';
import { InputStatus } from 'app/core/components/Form/Input';
import DataSourceOption from './DataSourceOption';
import { DataSourceOption } from './DataSourceOption';
import { FormLabel } from '@grafana/ui';
// Types
@@ -43,32 +43,79 @@ interface Props {
interface State {
relativeTime: string;
timeShift: string;
cacheTimeout: string;
maxDataPoints: string;
interval: string;
hideTimeOverride: boolean;
}
export class QueryOptions extends PureComponent<Props, State> {
allOptions = {
cacheTimeout: {
label: 'Cache timeout',
placeholder: '60',
name: 'cacheTimeout',
tooltipInfo: (
<>
If your time series store has a query cache this option can override the default cache timeout. Specify a
numeric value in seconds.
</>
),
},
maxDataPoints: {
label: 'Max data points',
placeholder: 'auto',
name: 'maxDataPoints',
tooltipInfo: (
<>
The maximum data points the query should return. For graphs this is automatically set to one data point per
pixel.
</>
),
},
minInterval: {
label: 'Min time interval',
placeholder: '0',
name: 'minInterval',
panelKey: 'interval',
tooltipInfo: (
<>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
<code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
be used in math expressions.
</>
),
},
};
constructor(props) {
super(props);
this.state = {
relativeTime: props.panel.timeFrom || '',
timeShift: props.panel.timeShift || '',
cacheTimeout: props.panel.cacheTimeout || '',
maxDataPoints: props.panel.maxDataPoints || '',
interval: props.panel.interval || '',
hideTimeOverride: props.panel.hideTimeOverride || false,
};
}
onRelativeTimeChange = event => {
onRelativeTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
relativeTime: event.target.value,
});
};
onTimeShiftChange = event => {
onTimeShiftChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
timeShift: event.target.value,
});
};
onOverrideTime = (evt, status: InputStatus) => {
const { value } = evt.target;
onOverrideTime = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
const { value } = event.target;
const { panel } = this.props;
const emptyToNullValue = emptyToNull(value);
if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
@@ -77,8 +124,8 @@ export class QueryOptions extends PureComponent<Props, State> {
}
};
onTimeShift = (evt, status: InputStatus) => {
const { value } = evt.target;
onTimeShift = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
const { value } = event.target;
const { panel } = this.props;
const emptyToNullValue = emptyToNull(value);
if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
@@ -89,77 +136,49 @@ export class QueryOptions extends PureComponent<Props, State> {
onToggleTimeOverride = () => {
const { panel } = this.props;
panel.hideTimeOverride = !panel.hideTimeOverride;
this.setState({ hideTimeOverride: !this.state.hideTimeOverride }, () => {
panel.hideTimeOverride = this.state.hideTimeOverride;
panel.refresh();
});
};
onDataSourceOptionBlur = (panelKey: string) => () => {
const { panel } = this.props;
panel[panelKey] = this.state[panelKey];
panel.refresh();
};
renderOptions() {
const { datasource, panel } = this.props;
onDataSourceOptionChange = (panelKey: string) => (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ ...this.state, [panelKey]: event.target.value });
};
renderOptions = () => {
const { datasource } = this.props;
const { queryOptions } = datasource.meta;
if (!queryOptions) {
return null;
}
const onChangeFn = (panelKey: string) => {
return (value: string | number) => {
panel[panelKey] = value;
panel.refresh();
};
};
const allOptions = {
cacheTimeout: {
label: 'Cache timeout',
placeholder: '60',
name: 'cacheTimeout',
value: panel.cacheTimeout,
tooltipInfo: (
<>
If your time series store has a query cache this option can override the default cache timeout. Specify a
numeric value in seconds.
</>
),
},
maxDataPoints: {
label: 'Max data points',
placeholder: 'auto',
name: 'maxDataPoints',
value: panel.maxDataPoints,
tooltipInfo: (
<>
The maximum data points the query should return. For graphs this is automatically set to one data point per
pixel.
</>
),
},
minInterval: {
label: 'Min time interval',
placeholder: '0',
name: 'minInterval',
value: panel.interval,
panelKey: 'interval',
tooltipInfo: (
<>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
<code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
be used in math expressions.
</>
),
},
};
return Object.keys(queryOptions).map(key => {
const options = allOptions[key];
return <DataSourceOption key={key} {...options} onChange={onChangeFn(allOptions[key].panelKey || key)} />;
const options = this.allOptions[key];
const panelKey = options.panelKey || key;
return (
<DataSourceOption
key={key}
{...options}
onChange={this.onDataSourceOptionChange(panelKey)}
onBlur={this.onDataSourceOptionBlur(panelKey)}
value={this.state[panelKey]}
/>
);
});
}
};
render() {
const hideTimeOverride = this.props.panel.hideTimeOverride;
const { hideTimeOverride } = this.state;
const { relativeTime, timeShift } = this.state;
return (
<div className="gf-form-inline">
{this.renderOptions()}

View File

@@ -14,10 +14,10 @@ import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
// Types
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { PanelModel } from '../state';
import { DashboardModel } from '../state';
import { PanelPlugin } from 'app/types/plugins';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { VizPickerSearch } from './VizPickerSearch';
interface Props {
panel: PanelModel;
@@ -33,18 +33,19 @@ interface State {
isVizPickerOpen: boolean;
searchQuery: string;
scrollTop: number;
hasBeenFocused: boolean;
}
export class VisualizationTab extends PureComponent<Props, State> {
element: HTMLElement;
angularOptions: AngularComponent;
searchInput: HTMLElement;
constructor(props) {
super(props);
this.state = {
isVizPickerOpen: this.props.urlOpenVizPicker,
hasBeenFocused: false,
searchQuery: '',
scrollTop: 0,
};
@@ -162,7 +163,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
this.props.updateLocation({ query: { openVizPicker: null }, partial: true });
}
this.setState({ isVizPickerOpen: false });
this.setState({ isVizPickerOpen: false, hasBeenFocused: false });
};
onSearchQueryChange = (value: string) => {
@@ -173,23 +174,16 @@ export class VisualizationTab extends PureComponent<Props, State> {
renderToolbar = (): JSX.Element => {
const { plugin } = this.props;
const { searchQuery } = this.state;
const { isVizPickerOpen, searchQuery } = this.state;
if (this.state.isVizPickerOpen) {
if (isVizPickerOpen) {
return (
<>
<FilterInput
labelClassName="gf-form--has-input-icon"
inputClassName="gf-form-input width-13"
placeholder=""
onChange={this.onSearchQueryChange}
value={searchQuery}
ref={elem => elem && elem.focus()}
/>
<button className="btn btn-link toolbar__close" onClick={this.onCloseVizPicker}>
<i className="fa fa-chevron-up" />
</button>
</>
<VizPickerSearch
plugin={plugin}
searchQuery={searchQuery}
onChange={this.onSearchQueryChange}
onClose={this.onCloseVizPicker}
/>
);
} else {
return (

View File

@@ -0,0 +1,33 @@
import React, { PureComponent } from 'react';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { PanelPlugin } from 'app/types';
interface Props {
plugin: PanelPlugin;
searchQuery: string;
onChange: (query: string) => void;
onClose: () => void;
}
export class VizPickerSearch extends PureComponent<Props> {
render() {
const { searchQuery, onChange, onClose } = this.props;
return (
<>
<FilterInput
labelClassName="gf-form--has-input-icon"
inputClassName="gf-form-input width-13"
placeholder=""
onChange={onChange}
value={searchQuery}
ref={element => element && element.focus()}
/>
<button className="btn btn-link toolbar__close" onClick={onClose}>
<i className="fa fa-chevron-up" />
</button>
</>
);
}
}

View File

@@ -227,8 +227,8 @@ export class TimeSrv {
const timespan = range.to.valueOf() - range.from.valueOf();
const center = range.to.valueOf() - timespan / 2;
const to = center + timespan * factor / 2;
const from = center - timespan * factor / 2;
const to = center + (timespan * factor) / 2;
const from = center - (timespan * factor) / 2;
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
}

View File

@@ -487,7 +487,7 @@ export class DashboardMigrator {
for (const panel of row.panels) {
panel.span = panel.span || DEFAULT_PANEL_SPAN;
if (panel.minSpan) {
panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan);
panel.minSpan = Math.min(GRID_COLUMN_COUNT, (GRID_COLUMN_COUNT / 12) * panel.minSpan);
}
const panelWidth = Math.floor(panel.span) * widthFactor;
const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight;

View File

@@ -887,8 +887,8 @@ export class DashboardModel {
}
// add back navbar height
if (kioskMode === KIOSK_MODE_TV) {
visibleHeight += 55;
if (kioskMode && kioskMode !== KIOSK_MODE_TV) {
visibleHeight += navbarHeight;
}
const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));

View File

@@ -70,6 +70,7 @@ export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
updateHandler: (newPanel: PanelModel, oldPanel: PanelModel) => {
replacePanel(dashboard, newPanel, oldPanel);
},
canUpdate: dashboard.meta.canEdit,
enableCopy: true,
};

View File

@@ -98,4 +98,9 @@ const mapDispatchToProps = {
removeDashboard,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceDashboards));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(DataSourceDashboards)
);

View File

@@ -115,4 +115,9 @@ const mapDispatchToProps = {
setDataSourcesLayoutMode,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(DataSourcesListPage)
);

View File

@@ -80,4 +80,9 @@ const mapDispatchToProps = {
setDataSourceTypeSearchQuery,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(NewDataSourcePage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(NewDataSourcePage)
);

View File

@@ -1,4 +1,5 @@
import React, { FC } from 'react';
import config from 'app/core/config';
export interface Props {
isReadOnly: boolean;
@@ -23,7 +24,7 @@ const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
<button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
Delete
</button>
<a className="btn btn-inverse" href="/datasources">
<a className="btn btn-inverse" href={`${config.appSubUrl}/datasources`}>
Back
</a>
</div>

View File

@@ -64,6 +64,14 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
await loadDataSource(pageId);
}
componentDidUpdate(prevProps: Props) {
const { dataSource } = this.props;
if (prevProps.dataSource !== dataSource) {
this.setState({ dataSource });
}
}
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
@@ -95,9 +103,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
};
onModelChange = (dataSource: DataSourceSettings) => {
this.setState({
dataSource: dataSource,
});
this.setState({ dataSource });
};
isReadOnly() {
@@ -259,4 +265,9 @@ const mapDispatchToProps = {
setIsDefault,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettingsPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(DataSourceSettingsPage)
);

View File

@@ -200,43 +200,42 @@ export class Explore extends React.PureComponent<ExploreProps> {
</div>
)}
{datasourceInstance &&
!datasourceError && (
<div className="explore-container">
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
<AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
{datasourceInstance && !datasourceError && (
<div className="explore-container">
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
<AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
return (
<main className="m-t-2" style={{ width }}>
<ErrorBoundary>
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
{supportsLogs && (
<LogsContainer
width={width}
exploreId={exploreId}
onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning}
/>
)}
</>
)}
</ErrorBoundary>
</main>
);
}}
</AutoSizer>
</div>
)}
return (
<main className="m-t-2" style={{ width }}>
<ErrorBoundary>
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
{supportsLogs && (
<LogsContainer
width={width}
exploreId={exploreId}
onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning}
/>
)}
</>
)}
</ErrorBoundary>
</main>
);
}}
</AutoSizer>
</div>
)}
</div>
);
}
@@ -287,4 +286,9 @@ const mapDispatchToProps = {
setQueries,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(Explore)
);

View File

@@ -193,4 +193,9 @@ const mapDispatchToProps: DispatchProps = {
split: splitOpen,
};
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
export const ExploreToolbar = hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(UnConnectedExploreToolbar)
);

View File

@@ -217,11 +217,13 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
let series = [{ data: [[0, 0]] }];
if (data && data.length > 0) {
series = data.filter((ts: TimeSeries) => !hiddenSeries.has(ts.alias)).map((ts: TimeSeries) => ({
color: ts.color,
label: ts.label,
data: ts.getFlotPairs('null'),
}));
series = data
.filter((ts: TimeSeries) => !hiddenSeries.has(ts.alias))
.map((ts: TimeSeries) => ({
color: ts.color,
label: ts.label,
data: ts.getFlotPairs('null'),
}));
}
this.dynamicOptions = this.getDynamicOptions();
@@ -242,17 +244,15 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
return (
<>
{this.props.data &&
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
!this.state.showAllTimeSeries && (
<div className="time-series-disclaimer">
<i className="fa fa-fw fa-warning disclaimer-icon" />
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
<span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
this.props.data.length
}`}</span>
</div>
)}
{this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && (
<div className="time-series-disclaimer">
<i className="fa fa-fw fa-warning disclaimer-icon" />
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
<span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
this.props.data.length
}`}</span>
</div>
)}
<div id={id} className="explore-graph" style={{ height }} />
<Legend data={data} hiddenSeries={hiddenSeries} onToggleSeries={this.onToggleSeries} />
</>

View File

@@ -70,4 +70,9 @@ const mapDispatchToProps = {
changeTime,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(GraphContainer)
);

View File

@@ -60,7 +60,9 @@ export class LogLabelStats extends PureComponent<Props> {
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
</div>
<div className="logs-stats__body">
{topRows.map(stat => <LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />)}
{topRows.map(stat => (
<LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />
))}
{insertActiveRow && activeRow && <LogLabelStatsRow key={activeRow.value} {...activeRow} active />}
{otherCount > 0 && (
<LogLabelStatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />

View File

@@ -166,15 +166,14 @@ export class LogRow extends PureComponent<Props, State> {
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed &&
needsHighlighter && (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{!parsed && needsHighlighter && (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{!parsed && !needsHighlighter && row.entry}
{showFieldStats && (
<div className="logs-row__stats">

View File

@@ -237,17 +237,16 @@ export default class Logs extends PureComponent<Props, State> {
</div>
</div>
{hasData &&
meta && (
<div className="logs-panel-meta">
{meta.map(item => (
<div className="logs-panel-meta__item" key={item.label}>
<span className="logs-panel-meta__label">{item.label}:</span>
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
</div>
))}
</div>
)}
{hasData && meta && (
<div className="logs-panel-meta">
{meta.map(item => (
<div className="logs-panel-meta__item" key={item.label}>
<span className="logs-panel-meta__label">{item.label}:</span>
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
</div>
))}
</div>
)}
<div className="logs-rows">
{hasData &&
@@ -282,16 +281,14 @@ export default class Logs extends PureComponent<Props, State> {
))}
{hasData && deferLogs && <span>Rendering {dedupedData.rows.length} rows...</span>}
</div>
{!loading &&
!hasData &&
!scanning && (
<div className="logs-panel-nodata">
No logs found.
<a className="link" onClick={this.onClickScan}>
Scan for older logs
</a>
</div>
)}
{!loading && !hasData && !scanning && (
<div className="logs-panel-nodata">
No logs found.
<a className="link" onClick={this.onClickScan}>
Scan for older logs
</a>
</div>
)}
{scanning && (
<div className="logs-panel-nodata">

View File

@@ -127,4 +127,9 @@ const mapDispatchToProps = {
toggleLogLevelAction,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(LogsContainer)
);

View File

@@ -43,6 +43,9 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
this.props.onQueryChange(target);
this.props.onExecuteQuery();
},
onQueryChange: () => {
this.props.onQueryChange(target);
},
events: exploreEvents,
panel: { datasource, targets: [target] },
dashboard: {},

View File

@@ -169,4 +169,9 @@ const mapDispatchToProps = {
runQueries,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(QueryRow)
);

View File

@@ -51,4 +51,9 @@ const mapDispatchToProps = {
toggleTable,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(TableContainer)
);

View File

@@ -82,4 +82,9 @@ const mapDispatchToProps = {
resetExploreAction,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(Wrapper)
);

View File

@@ -132,4 +132,9 @@ const mapDispatchToProps = {
addFolderPermission,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(FolderPermissions)
);

View File

@@ -113,4 +113,9 @@ const mapDispatchToProps = {
deleteFolder,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(FolderSettingsPage)
);

View File

@@ -65,4 +65,9 @@ const mapDispatchToProps = {
updateOrganization,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(OrgDetailsPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(OrgDetailsPage)
);

View File

@@ -14,7 +14,7 @@ export class QueryRowCtrl {
this.target = this.queryCtrl.target;
this.panel = this.panelCtrl.panel;
if (this.hasTextEditMode) {
if (this.hasTextEditMode && this.queryCtrl.toggleEditorMode) {
// expose this function to react parent component
this.panelCtrl.toggleEditorMode = this.queryCtrl.toggleEditorMode.bind(this.queryCtrl);
}

View File

@@ -81,4 +81,9 @@ const mapDispatchToProps = {
setPluginsSearchQuery,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(PluginListPage)
);

View File

@@ -136,27 +136,29 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
// Datasource ConfigCtrl
case 'datasource-config-ctrl': {
const dsMeta = scope.ctrl.datasourceMeta;
return importPluginModule(dsMeta.module).then((dsModule): any => {
if (!dsModule.ConfigCtrl) {
return { notFound: true };
return importPluginModule(dsMeta.module).then(
(dsModule): any => {
if (!dsModule.ConfigCtrl) {
return { notFound: true };
}
scope.$watch(
'ctrl.current',
() => {
scope.onModelChanged(scope.ctrl.current);
},
true
);
return {
baseUrl: dsMeta.baseUrl,
name: 'ds-config-' + dsMeta.id,
bindings: { meta: '=', current: '=' },
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
Component: dsModule.ConfigCtrl,
};
}
scope.$watch(
'ctrl.current',
() => {
scope.onModelChanged(scope.ctrl.current);
},
true
);
return {
baseUrl: dsMeta.baseUrl,
name: 'ds-config-' + dsMeta.id,
bindings: { meta: '=', current: '=' },
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
Component: dsModule.ConfigCtrl,
};
});
);
}
// AppConfigCtrl
case 'app-config-ctrl': {

View File

@@ -116,26 +116,25 @@ export class TeamGroupSync extends PureComponent<Props, State> {
</div>
</SlideDown>
{groups.length === 0 &&
!isAdding && (
<div className="empty-list-cta">
<div className="empty-list-cta__title">There are no external groups to sync with</div>
<button onClick={this.onToggleAdding} className="empty-list-cta__button btn btn-xlarge btn-primary">
<i className="gicon gicon-add-team" />
Add Group
</button>
<div className="empty-list-cta__pro-tip">
<i className="fa fa-rocket" /> {headerTooltip}
<a
className="text-link empty-list-cta__pro-tip-link"
href="http://docs.grafana.org/auth/enhanced_ldap/"
target="_blank"
>
Learn more
</a>
</div>
{groups.length === 0 && !isAdding && (
<div className="empty-list-cta">
<div className="empty-list-cta__title">There are no external groups to sync with</div>
<button onClick={this.onToggleAdding} className="empty-list-cta__button btn btn-xlarge btn-primary">
<i className="gicon gicon-add-team" />
Add Group
</button>
<div className="empty-list-cta__pro-tip">
<i className="fa fa-rocket" /> {headerTooltip}
<a
className="text-link empty-list-cta__pro-tip-link"
href="http://docs.grafana.org/auth/enhanced_ldap/"
target="_blank"
>
Learn more
</a>
</div>
)}
</div>
)}
{groups.length > 0 && (
<div className="admin-list-table">
@@ -167,4 +166,7 @@ const mapDispatchToProps = {
removeTeamGroup,
};
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);
export default connect(
mapStateToProps,
mapDispatchToProps
)(TeamGroupSync);

View File

@@ -161,4 +161,9 @@ const mapDispatchToProps = {
setSearchQuery,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(TeamList)
);

View File

@@ -62,7 +62,9 @@ export class TeamMembers extends PureComponent<Props, State> {
return (
<td>
{labels.map(label => <TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />)}
{labels.map(label => (
<TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />
))}
</td>
);
}
@@ -156,4 +158,7 @@ const mapDispatchToProps = {
setSearchMemberQuery,
};
export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers);
export default connect(
mapStateToProps,
mapDispatchToProps
)(TeamMembers);

View File

@@ -108,4 +108,9 @@ const mapDispatchToProps = {
loadTeam,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(TeamPages)
);

View File

@@ -98,4 +98,7 @@ const mapDispatchToProps = {
updateTeam,
};
export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings);
export default connect(
mapStateToProps,
mapDispatchToProps
)(TeamSettings);

View File

@@ -54,15 +54,17 @@ export class VariableSrv {
onTimeRangeUpdated(timeRange: TimeRange) {
this.templateSrv.updateTimeRange(timeRange);
const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
const previousOptions = variable.options.slice();
const promises = this.variables
.filter(variable => variable.refresh === 2)
.map(variable => {
const previousOptions = variable.options.slice();
return variable.updateOptions().then(() => {
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
this.dashboard.templateVariableValueUpdated();
}
return variable.updateOptions().then(() => {
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
this.dashboard.templateVariableValueUpdated();
}
});
});
});
return this.$q.all(promises).then(() => {
this.dashboard.startRefresh();

View File

@@ -52,6 +52,9 @@ const mapDispatchToProps = {
revokeInvite,
};
export default connect(() => {
return {};
}, mapDispatchToProps)(InviteeRow);
export default connect(
() => {
return {};
},
mapDispatchToProps
)(InviteeRow);

View File

@@ -92,4 +92,7 @@ const mapDispatchToProps = {
setUsersSearchQuery,
};
export default connect(mapStateToProps, mapDispatchToProps)(UsersActionBar);
export default connect(
mapStateToProps,
mapDispatchToProps
)(UsersActionBar);

View File

@@ -138,4 +138,9 @@ const mapDispatchToProps = {
removeUser,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage));
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(UsersListPage)
);

View File

@@ -4,7 +4,8 @@ export class AzureMonitorAnnotationsQueryCtrl {
annotation: any;
workspaces: any[];
defaultQuery = '<your table>\n| where $__timeFilter() \n| project TimeGenerated, Text=YourTitleColumn, Tags="tag1,tag2"';
defaultQuery =
'<your table>\n| where $__timeFilter() \n| project TimeGenerated, Text=YourTitleColumn, Tags="tag1,tag2"';
/** @ngInject */
constructor() {

View File

@@ -311,8 +311,8 @@ class QueryField extends React.Component<any, any> {
let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
const flattenedSuggestions = flattenSuggestions(suggestions);
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
i => (typeof i === 'object' ? i.text : i)
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(i =>
typeof i === 'object' ? i.text : i
);
// Create typeahead in DOM root so we can later position it absolutely

View File

@@ -0,0 +1,110 @@
import React from 'react';
import { PopperController, Popper } from '@grafana/ui';
import rst2html from 'rst2html';
import { FunctionDescriptor, FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls';
interface FunctionEditorProps extends FunctionEditorControlsProps {
func: FunctionDescriptor;
}
interface FunctionEditorState {
showingDescription: boolean;
}
class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEditorState> {
private triggerRef = React.createRef<HTMLSpanElement>();
constructor(props: FunctionEditorProps) {
super(props);
this.state = {
showingDescription: false,
};
}
renderContent = ({ updatePopperPosition }) => {
const {
onMoveLeft,
onMoveRight,
func: {
def: { name, description },
},
} = this.props;
const { showingDescription } = this.state;
if (showingDescription) {
return (
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
<h4 style={{ color: 'white' }}> {name} </h4>
<div
dangerouslySetInnerHTML={{
__html: rst2html(description),
}}
/>
</div>
);
}
return (
<FunctionEditorControls
{...this.props}
onMoveLeft={() => {
onMoveLeft(this.props.func);
updatePopperPosition();
}}
onMoveRight={() => {
onMoveRight(this.props.func);
updatePopperPosition();
}}
onDescriptionShow={() => {
this.setState({ showingDescription: true }, () => {
updatePopperPosition();
});
}}
/>
);
};
render() {
return (
<PopperController content={this.renderContent} placement="top" hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.triggerRef && (
<Popper
{...popperProps}
referenceElement={this.triggerRef.current}
wrapperClassName="popper"
className="popper__background"
onMouseLeave={() => {
this.setState({ showingDescription: false });
hidePopper();
}}
onMouseEnter={showPopper}
renderArrow={({ arrowProps, placement }) => (
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
)}
/>
)}
<span
ref={this.triggerRef}
onClick={popperProps.show ? hidePopper : showPopper}
onMouseLeave={() => {
hidePopper();
this.setState({ showingDescription: false });
}}
style={{ cursor: 'pointer' }}
>
{this.props.func.def.name}
</span>
</>
);
}}
</PopperController>
);
}
}
export { FunctionEditor };

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