Commit 90d162e6 by Hugo Häggmark

Merge with master

parents 222482b6 ef723643
# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine
version: '2'
services:
elasticsearch5: elasticsearch5:
image: elasticsearch:5 image: elasticsearch:5
command: elasticsearch command: elasticsearch
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
"html-loader": "^0.5.1", "html-loader": "^0.5.1",
"html-webpack-harddisk-plugin": "^0.2.0", "html-webpack-harddisk-plugin": "^0.2.0",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3", "husky": "^1.3.1",
"jest": "^23.6.0", "jest": "^23.6.0",
"jest-date-mock": "^1.0.6", "jest-date-mock": "^1.0.6",
"lint-staged": "^8.1.3", "lint-staged": "^8.1.3",
...@@ -120,7 +120,6 @@ ...@@ -120,7 +120,6 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"jest": "jest --notify --watch", "jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js", "api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "grunt precommit",
"storybook": "cd packages/grafana-ui && yarn storybook" "storybook": "cd packages/grafana-ui && yarn storybook"
}, },
"husky": { "husky": {
...@@ -151,6 +150,7 @@ ...@@ -151,6 +150,7 @@
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"@torkelo/react-select": "2.1.1", "@torkelo/react-select": "2.1.1",
"@types/reselect": "^2.2.0",
"angular": "1.6.6", "angular": "1.6.6",
"angular-bindonce": "0.3.1", "angular-bindonce": "0.3.1",
"angular-native-dragdrop": "1.2.2", "angular-native-dragdrop": "1.2.2",
...@@ -187,6 +187,7 @@ ...@@ -187,6 +187,7 @@
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"remarkable": "^1.7.1", "remarkable": "^1.7.1",
"reselect": "^4.0.0",
"rst2html": "github:thoward/rst2html#990cb89", "rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^6.3.3", "rxjs": "^6.3.3",
"slate": "^0.33.4", "slate": "^0.33.4",
......
import React from 'react'; import React, { ChangeEvent } from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { ThresholdsEditor, Props } from './ThresholdsEditor'; import { ThresholdsEditor, Props } from './ThresholdsEditor';
...@@ -118,7 +118,7 @@ describe('change threshold value', () => { ...@@ -118,7 +118,7 @@ describe('change threshold value', () => {
]; ];
const instance = setup({ thresholds }); const instance = setup({ thresholds });
const mockEvent = { target: { value: 12 } }; const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, thresholds[0]); instance.onChangeThresholdValue(mockEvent, thresholds[0]);
...@@ -137,7 +137,7 @@ describe('change threshold value', () => { ...@@ -137,7 +137,7 @@ describe('change threshold value', () => {
thresholds, thresholds,
}; };
const mockEvent = { target: { value: 78 } }; const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, thresholds[1]); instance.onChangeThresholdValue(mockEvent, thresholds[1]);
......
import React, { PureComponent } from 'react'; import React, { PureComponent, ChangeEvent } from 'react';
import { Threshold } from '../../types'; import { Threshold } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker'; import { ColorPicker } from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
...@@ -94,14 +94,15 @@ export class ThresholdsEditor extends PureComponent<Props, State> { ...@@ -94,14 +94,15 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
); );
}; };
onChangeThresholdValue = (event: any, threshold: Threshold) => { onChangeThresholdValue = (event: ChangeEvent<HTMLInputElement>, threshold: Threshold) => {
if (threshold.index === 0) { if (threshold.index === 0) {
return; return;
} }
const { thresholds } = this.state; const { thresholds } = this.state;
const parsedValue = parseInt(event.target.value, 10); const cleanValue = event.target.value.replace(/,/g, '.');
const value = isNaN(parsedValue) ? null : parsedValue; const parsedValue = parseFloat(cleanValue);
const value = isNaN(parsedValue) ? '' : parsedValue;
const newThresholds = thresholds.map(t => { const newThresholds = thresholds.map(t => {
if (t === threshold && t.index !== 0) { if (t === threshold && t.index !== 0) {
...@@ -164,16 +165,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> { ...@@ -164,16 +165,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
<div className="thresholds-row-input-inner-color"> <div className="thresholds-row-input-inner-color">
{threshold.color && ( {threshold.color && (
<div className="thresholds-row-input-inner-color-colorpicker"> <div className="thresholds-row-input-inner-color-colorpicker">
<ColorPicker <ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
color={threshold.color}
onChange={color => this.onChangeThresholdColor(threshold, color)}
/>
</div> </div>
)} )}
</div> </div>
<div className="thresholds-row-input-inner-value"> <div className="thresholds-row-input-inner-value">
<input <input
type="text" type="number"
step="0.0001"
onChange={event => this.onChangeThresholdValue(event, threshold)} onChange={event => this.onChangeThresholdValue(event, threshold)}
value={value} value={value}
onBlur={this.onBlur} onBlur={this.onBlur}
......
...@@ -31,7 +31,7 @@ $popper-margin-from-ref: 5px; ...@@ -31,7 +31,7 @@ $popper-margin-from-ref: 5px;
// Themes // Themes
&.popper__background--error { &.popper__background--error {
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError); @include popper-theme($tooltipBackgroundError, $white);
} }
&.popper__background--info { &.popper__background--info {
......
...@@ -16,7 +16,7 @@ func (hs *HTTPServer) registerRoutes() { ...@@ -16,7 +16,7 @@ func (hs *HTTPServer) registerRoutes() {
reqOrgAdmin := middleware.ReqOrgAdmin reqOrgAdmin := middleware.ReqOrgAdmin
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
quota := middleware.Quota quota := middleware.Quota(hs.QuotaService)
bind := binding.Bind bind := binding.Bind
r := hs.RouteRegister r := hs.RouteRegister
...@@ -286,7 +286,7 @@ func (hs *HTTPServer) registerRoutes() { ...@@ -286,7 +286,7 @@ func (hs *HTTPServer) registerRoutes() {
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(PostDashboard)) dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(hs.PostDashboard))
dashboardRoute.Get("/home", Wrap(GetHomeDashboard)) dashboardRoute.Get("/home", Wrap(GetHomeDashboard))
dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Get("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard)) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
...@@ -294,7 +294,7 @@ func (hs *HTTPServer) registerRoutes() { ...@@ -294,7 +294,7 @@ func (hs *HTTPServer) registerRoutes() {
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) { dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", Wrap(GetDashboardVersions)) dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion)) dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion)) dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(hs.RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList)) dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
......
...@@ -18,7 +18,6 @@ import ( ...@@ -18,7 +18,6 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
...@@ -208,14 +207,14 @@ func DeleteDashboardByUID(c *m.ReqContext) Response { ...@@ -208,14 +207,14 @@ func DeleteDashboardByUID(c *m.ReqContext) Response {
}) })
} }
func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
cmd.UserId = c.UserId cmd.UserId = c.UserId
dash := cmd.GetDashboardModel() dash := cmd.GetDashboardModel()
if dash.Id == 0 && dash.Uid == "" { if dash.Id == 0 && dash.Uid == "" {
limitReached, err := quota.QuotaReached(c, "dashboard") limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
if err != nil { if err != nil {
return Error(500, "failed to get quota", err) return Error(500, "failed to get quota", err)
} }
...@@ -463,7 +462,7 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption ...@@ -463,7 +462,7 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
} }
// RestoreDashboardVersion restores a dashboard to the given version. // RestoreDashboardVersion restores a dashboard to the given version.
func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response { func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "") dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
if rsp != nil { if rsp != nil {
return rsp return rsp
...@@ -490,7 +489,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio ...@@ -490,7 +489,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
saveCmd.Dashboard.Set("uid", dash.Uid) saveCmd.Dashboard.Set("uid", dash.Uid)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
return PostDashboard(c, saveCmd) return hs.PostDashboard(c, saveCmd)
} }
func GetDashboardTags(c *m.ReqContext) { func GetDashboardTags(c *m.ReqContext) {
......
...@@ -881,12 +881,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d ...@@ -881,12 +881,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
Convey(desc+" "+url, func() { Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
hs := HTTPServer{
Bus: bus.GetBus(),
}
sc := setupScenarioContext(url) sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId} sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
return PostDashboard(c, cmd) return hs.PostDashboard(c, cmd)
}) })
origNewDashboardService := dashboards.NewService origNewDashboardService := dashboards.NewService
......
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
...@@ -55,6 +56,7 @@ type HTTPServer struct { ...@@ -55,6 +56,7 @@ type HTTPServer struct {
CacheService *cache.CacheService `inject:""` CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""`
AuthTokenService models.UserTokenService `inject:""` AuthTokenService models.UserTokenService `inject:""`
QuotaService *quota.QuotaService `inject:""`
} }
func (hs *HTTPServer) Init() error { func (hs *HTTPServer) Init() error {
......
...@@ -4,18 +4,30 @@ import ( ...@@ -4,18 +4,30 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
) )
func init() { func init() {
bus.AddHandler("auth", UpsertUser) registry.RegisterService(&LoginService{})
} }
var ( var (
logger = log.New("login.ext_user") logger = log.New("login.ext_user")
) )
func UpsertUser(cmd *m.UpsertUserCommand) error { type LoginService struct {
Bus bus.Bus `inject:""`
QuotaService *quota.QuotaService `inject:""`
}
func (ls *LoginService) Init() error {
ls.Bus.AddHandler(ls.UpsertUser)
return nil
}
func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
extUser := cmd.ExternalUser extUser := cmd.ExternalUser
userQuery := &m.GetUserByAuthInfoQuery{ userQuery := &m.GetUserByAuthInfoQuery{
...@@ -37,7 +49,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { ...@@ -37,7 +49,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
return ErrInvalidCredentials return ErrInvalidCredentials
} }
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user") limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user")
if err != nil { if err != nil {
log.Warn("Error getting user quota. error: %v", err) log.Warn("Error getting user quota. error: %v", err)
return ErrGettingUserQuota return ErrGettingUserQuota
...@@ -57,7 +69,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { ...@@ -57,7 +69,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
AuthModule: extUser.AuthModule, AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId, AuthId: extUser.AuthId,
} }
if err := bus.Dispatch(cmd2); err != nil { if err := ls.Bus.Dispatch(cmd2); err != nil {
return err return err
} }
} }
...@@ -78,12 +90,12 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { ...@@ -78,12 +90,12 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
// Sync isGrafanaAdmin permission // Sync isGrafanaAdmin permission
if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin { if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil { if err := ls.Bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
return err return err
} }
} }
err = bus.Dispatch(&m.SyncTeamsCommand{ err = ls.Bus.Dispatch(&m.SyncTeamsCommand{
User: cmd.Result, User: cmd.Result,
ExternalUser: extUser, ExternalUser: extUser,
}) })
......
...@@ -395,8 +395,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { ...@@ -395,8 +395,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
sc := &scenarioContext{} sc := &scenarioContext{}
loginService := &LoginService{
Bus: bus.GetBus(),
}
bus.AddHandler("test", UpsertUser) bus.AddHandler("test", loginService.UpsertUser)
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error { bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error {
return nil return nil
......
...@@ -682,6 +682,7 @@ type fakeUserAuthTokenService struct { ...@@ -682,6 +682,7 @@ type fakeUserAuthTokenService struct {
tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error) tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
lookupTokenProvider func(unhashedToken string) (*m.UserToken, error) lookupTokenProvider func(unhashedToken string) (*m.UserToken, error)
revokeTokenProvider func(token *m.UserToken) error revokeTokenProvider func(token *m.UserToken) error
activeAuthTokenCount func() (int64, error)
} }
func newFakeUserAuthTokenService() *fakeUserAuthTokenService { func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
...@@ -704,6 +705,9 @@ func newFakeUserAuthTokenService() *fakeUserAuthTokenService { ...@@ -704,6 +705,9 @@ func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
revokeTokenProvider: func(token *m.UserToken) error { revokeTokenProvider: func(token *m.UserToken) error {
return nil return nil
}, },
activeAuthTokenCount: func() (int64, error) {
return 10, nil
},
} }
} }
...@@ -722,3 +726,7 @@ func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, ...@@ -722,3 +726,7 @@ func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP,
func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error { func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
return s.revokeTokenProvider(token) return s.revokeTokenProvider(token)
} }
func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
return s.activeAuthTokenCount()
}
...@@ -9,16 +9,20 @@ import ( ...@@ -9,16 +9,20 @@ import (
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
) )
func Quota(target string) macaron.Handler { // Quota returns a function that returns a function used to call quotaservice based on target name
return func(c *m.ReqContext) { func Quota(quotaService *quota.QuotaService) func(target string) macaron.Handler {
limitReached, err := quota.QuotaReached(c, target) //https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky
if err != nil { return func(target string) macaron.Handler {
c.JsonApiErr(500, "failed to get quota", err) return func(c *m.ReqContext) {
return limitReached, err := quotaService.QuotaReached(c, target)
} if err != nil {
if limitReached { c.JsonApiErr(500, "failed to get quota", err)
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil) return
return }
if limitReached {
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
return
}
} }
} }
} }
...@@ -3,9 +3,10 @@ package middleware ...@@ -3,9 +3,10 @@ package middleware
import ( import (
"testing" "testing"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
...@@ -13,10 +14,6 @@ import ( ...@@ -13,10 +14,6 @@ import (
func TestMiddlewareQuota(t *testing.T) { func TestMiddlewareQuota(t *testing.T) {
Convey("Given the grafana quota middleware", t, func() { Convey("Given the grafana quota middleware", t, func() {
session.GetSessionCount = func() int {
return 4
}
setting.AnonymousEnabled = false setting.AnonymousEnabled = false
setting.Quota = setting.QuotaSettings{ setting.Quota = setting.QuotaSettings{
Enabled: true, Enabled: true,
...@@ -39,6 +36,12 @@ func TestMiddlewareQuota(t *testing.T) { ...@@ -39,6 +36,12 @@ func TestMiddlewareQuota(t *testing.T) {
}, },
} }
fakeAuthTokenService := newFakeUserAuthTokenService()
qs := &quota.QuotaService{
AuthTokenService: fakeAuthTokenService,
}
QuotaFn := Quota(qs)
middlewareScenario("with user not logged in", func(sc *scenarioContext) { middlewareScenario("with user not logged in", func(sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error { bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
query.Result = &m.GlobalQuotaDTO{ query.Result = &m.GlobalQuotaDTO{
...@@ -48,26 +51,30 @@ func TestMiddlewareQuota(t *testing.T) { ...@@ -48,26 +51,30 @@ func TestMiddlewareQuota(t *testing.T) {
} }
return nil return nil
}) })
Convey("global quota not reached", func() { Convey("global quota not reached", func() {
sc.m.Get("/user", Quota("user"), sc.defaultHandler) sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec() sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
}) })
Convey("global quota reached", func() { Convey("global quota reached", func() {
setting.Quota.Global.User = 4 setting.Quota.Global.User = 4
sc.m.Get("/user", Quota("user"), sc.defaultHandler) sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec() sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 403) So(sc.resp.Code, ShouldEqual, 403)
}) })
Convey("global session quota not reached", func() { Convey("global session quota not reached", func() {
setting.Quota.Global.Session = 10 setting.Quota.Global.Session = 10
sc.m.Get("/user", Quota("session"), sc.defaultHandler) sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec() sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
}) })
Convey("global session quota reached", func() { Convey("global session quota reached", func() {
setting.Quota.Global.Session = 1 setting.Quota.Global.Session = 1
sc.m.Get("/user", Quota("session"), sc.defaultHandler) sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec() sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 403) So(sc.resp.Code, ShouldEqual, 403)
}) })
...@@ -95,6 +102,7 @@ func TestMiddlewareQuota(t *testing.T) { ...@@ -95,6 +102,7 @@ func TestMiddlewareQuota(t *testing.T) {
} }
return nil return nil
}) })
bus.AddHandler("userQuota", func(query *m.GetUserQuotaByTargetQuery) error { bus.AddHandler("userQuota", func(query *m.GetUserQuotaByTargetQuery) error {
query.Result = &m.UserQuotaDTO{ query.Result = &m.UserQuotaDTO{
Target: query.Target, Target: query.Target,
...@@ -103,6 +111,7 @@ func TestMiddlewareQuota(t *testing.T) { ...@@ -103,6 +111,7 @@ func TestMiddlewareQuota(t *testing.T) {
} }
return nil return nil
}) })
bus.AddHandler("orgQuota", func(query *m.GetOrgQuotaByTargetQuery) error { bus.AddHandler("orgQuota", func(query *m.GetOrgQuotaByTargetQuery) error {
query.Result = &m.OrgQuotaDTO{ query.Result = &m.OrgQuotaDTO{
Target: query.Target, Target: query.Target,
...@@ -111,45 +120,49 @@ func TestMiddlewareQuota(t *testing.T) { ...@@ -111,45 +120,49 @@ func TestMiddlewareQuota(t *testing.T) {
} }
return nil return nil
}) })
Convey("global datasource quota reached", func() { Convey("global datasource quota reached", func() {
setting.Quota.Global.DataSource = 4 setting.Quota.Global.DataSource = 4
sc.m.Get("/ds", Quota("data_source"), sc.defaultHandler) sc.m.Get("/ds", QuotaFn("data_source"), sc.defaultHandler)
sc.fakeReq("GET", "/ds").exec() sc.fakeReq("GET", "/ds").exec()
So(sc.resp.Code, ShouldEqual, 403) So(sc.resp.Code, ShouldEqual, 403)
}) })
Convey("user Org quota not reached", func() { Convey("user Org quota not reached", func() {
setting.Quota.User.Org = 5 setting.Quota.User.Org = 5
sc.m.Get("/org", Quota("org"), sc.defaultHandler) sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
sc.fakeReq("GET", "/org").exec() sc.fakeReq("GET", "/org").exec()
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
}) })
Convey("user Org quota reached", func() { Convey("user Org quota reached", func() {
setting.Quota.User.Org = 4 setting.Quota.User.Org = 4
sc.m.Get("/org", Quota("org"), sc.defaultHandler) sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
sc.fakeReq("GET", "/org").exec() sc.fakeReq("GET", "/org").exec()
So(sc.resp.Code, ShouldEqual, 403) So(sc.resp.Code, ShouldEqual, 403)
}) })
Convey("org dashboard quota not reached", func() { Convey("org dashboard quota not reached", func() {
setting.Quota.Org.Dashboard = 10 setting.Quota.Org.Dashboard = 10
sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler) sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec() sc.fakeReq("GET", "/dashboard").exec()
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
}) })
Convey("org dashboard quota reached", func() { Convey("org dashboard quota reached", func() {
setting.Quota.Org.Dashboard = 4 setting.Quota.Org.Dashboard = 4
sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler) sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec() sc.fakeReq("GET", "/dashboard").exec()
So(sc.resp.Code, ShouldEqual, 403) So(sc.resp.Code, ShouldEqual, 403)
}) })
Convey("org dashboard quota reached but quotas disabled", func() { Convey("org dashboard quota reached but quotas disabled", func() {
setting.Quota.Org.Dashboard = 4 setting.Quota.Org.Dashboard = 4
setting.Quota.Enabled = false setting.Quota.Enabled = false
sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler) sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec() sc.fakeReq("GET", "/dashboard").exec()
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
}) })
}) })
}) })
} }
...@@ -6,7 +6,6 @@ import ( ...@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
macaron "gopkg.in/macaron.v1" macaron "gopkg.in/macaron.v1"
...@@ -66,7 +65,6 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { ...@@ -66,7 +65,6 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
sc.userAuthTokenService = newFakeUserAuthTokenService() sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService)) sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine // mock out gc goroutine
session.StartSessionGC = func() {}
sc.m.Use(OrgRedirect()) sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders()) sc.m.Use(AddDefaultResponseHeaders())
......
...@@ -29,4 +29,5 @@ type UserTokenService interface { ...@@ -29,4 +29,5 @@ type UserTokenService interface {
LookupToken(unhashedToken string) (*UserToken, error) LookupToken(unhashedToken string) (*UserToken, error)
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error) TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
RevokeToken(token *UserToken) error RevokeToken(token *UserToken) error
ActiveTokenCount() (int64, error)
} }
...@@ -35,6 +35,13 @@ func (s *UserAuthTokenService) Init() error { ...@@ -35,6 +35,13 @@ func (s *UserAuthTokenService) Init() error {
return nil return nil
} }
func (s *UserAuthTokenService) ActiveTokenCount() (int64, error) {
var model userAuthToken
count, err := s.SQLStore.NewSession().Where(`created_at > ? AND rotated_at > ?`, s.createdAfterParam(), s.rotatedAfterParam()).Count(&model)
return count, err
}
func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) { func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
clientIP = util.ParseIPAddress(clientIP) clientIP = util.ParseIPAddress(clientIP)
token, err := util.RandomHex(16) token, err := util.RandomHex(16)
...@@ -79,13 +86,8 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo ...@@ -79,13 +86,8 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
} }
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
createdAfter := getTime().Add(-tokenMaxLifetime).Unix()
rotatedAfter := getTime().Add(-tokenMaxInactiveLifetime).Unix()
var model userAuthToken var model userAuthToken
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, createdAfter, rotatedAfter).Get(&model) exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, s.createdAfterParam(), s.rotatedAfterParam()).Get(&model)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -219,6 +221,16 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error { ...@@ -219,6 +221,16 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
return nil return nil
} }
func (s *UserAuthTokenService) createdAfterParam() int64 {
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
return getTime().Add(-tokenMaxLifetime).Unix()
}
func (s *UserAuthTokenService) rotatedAfterParam() int64 {
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
return getTime().Add(-tokenMaxInactiveLifetime).Unix()
}
func hashToken(token string) string { func hashToken(token string) string {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:]) return hex.EncodeToString(hashBytes[:])
......
...@@ -31,6 +31,12 @@ func TestUserAuthToken(t *testing.T) { ...@@ -31,6 +31,12 @@ func TestUserAuthToken(t *testing.T) {
So(userToken, ShouldNotBeNil) So(userToken, ShouldNotBeNil)
So(userToken.AuthTokenSeen, ShouldBeFalse) So(userToken.AuthTokenSeen, ShouldBeFalse)
Convey("Can count active tokens", func() {
count, err := userAuthTokenService.ActiveTokenCount()
So(err, ShouldBeNil)
So(count, ShouldEqual, 1)
})
Convey("When lookup unhashed token should return user auth token", func() { Convey("When lookup unhashed token should return user auth token", func() {
userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
...@@ -114,6 +120,12 @@ func TestUserAuthToken(t *testing.T) { ...@@ -114,6 +120,12 @@ func TestUserAuthToken(t *testing.T) {
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound) So(err, ShouldEqual, models.ErrUserTokenNotFound)
So(notGood, ShouldBeNil) So(notGood, ShouldBeNil)
Convey("should not find active token when expired", func() {
count, err := userAuthTokenService.ActiveTokenCount()
So(err, ShouldBeNil)
So(count, ShouldEqual, 0)
})
}) })
Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() { Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() {
......
...@@ -3,11 +3,23 @@ package quota ...@@ -3,11 +3,23 @@ package quota
import ( import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func QuotaReached(c *m.ReqContext, target string) (bool, error) { func init() {
registry.RegisterService(&QuotaService{})
}
type QuotaService struct {
AuthTokenService m.UserTokenService `inject:""`
}
func (qs *QuotaService) Init() error {
return nil
}
func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, error) {
if !setting.Quota.Enabled { if !setting.Quota.Enabled {
return false, nil return false, nil
} }
...@@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) { ...@@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) {
return true, nil return true, nil
} }
if target == "session" { if target == "session" {
usedSessions := session.GetSessionCount()
usedSessions, err := qs.AuthTokenService.ActiveTokenCount()
if err != nil {
return false, err
}
if int64(usedSessions) > scope.DefaultLimit { if int64(usedSessions) > scope.DefaultLimit {
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit) c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
return true, nil return true, nil
......
...@@ -19,7 +19,7 @@ const ( ...@@ -19,7 +19,7 @@ const (
var sessionManager *ms.Manager var sessionManager *ms.Manager
var sessionOptions *ms.Options var sessionOptions *ms.Options
var StartSessionGC func() var StartSessionGC func() = func() {}
var GetSessionCount func() int var GetSessionCount func() int
var sessionLogger = log.New("session") var sessionLogger = log.New("session")
var sessionConnMaxLifetime int64 var sessionConnMaxLifetime int64
......
import { memoize } from 'lodash';
import { createSelectorCreator } from 'reselect';
const hashFn = (...args) => args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '');
export const createLodashMemoizedSelector = createSelectorCreator(memoize, hashFn);
...@@ -38,7 +38,7 @@ export class SettingsCtrl { ...@@ -38,7 +38,7 @@ export class SettingsCtrl {
}); });
}); });
this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders; this.canSaveAs = contextSrv.hasEditPermissionInFolders;
this.canSave = this.dashboard.meta.canSave; this.canSave = this.dashboard.meta.canSave;
this.canDelete = this.dashboard.meta.canSave; this.canDelete = this.dashboard.meta.canSave;
......
...@@ -36,31 +36,29 @@ class NewDataSourcePage extends PureComponent<Props> { ...@@ -36,31 +36,29 @@ class NewDataSourcePage extends PureComponent<Props> {
return ( return (
<Page navModel={navModel}> <Page navModel={navModel}>
<Page.Contents isLoading={isLoading}> <Page.Contents isLoading={isLoading}>
<div className="page-container page-body"> <h2 className="add-data-source-header">Choose data source type</h2>
<h2 className="add-data-source-header">Choose data source type</h2> <div className="add-data-source-search">
<div className="add-data-source-search"> <FilterInput
<FilterInput labelClassName="gf-form--has-input-icon"
labelClassName="gf-form--has-input-icon" inputClassName="gf-form-input width-20"
inputClassName="gf-form-input width-20" value={dataSourceTypeSearchQuery}
value={dataSourceTypeSearchQuery} onChange={this.onSearchQueryChange}
onChange={this.onSearchQueryChange} placeholder="Filter by name or type"
placeholder="Filter by name or type" />
/> </div>
</div> <div className="add-data-source-grid">
<div className="add-data-source-grid"> {dataSourceTypes.map((plugin, index) => {
{dataSourceTypes.map((plugin, index) => { return (
return ( <div
<div onClick={() => this.onDataSourceTypeClicked(plugin)}
onClick={() => this.onDataSourceTypeClicked(plugin)} className="add-data-source-grid-item"
className="add-data-source-grid-item" key={`${plugin.id}-${index}`}
key={`${plugin.id}-${index}`} >
> <img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} />
<img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} /> <span className="add-data-source-grid-item-text">{plugin.name}</span>
<span className="add-data-source-grid-item-text">{plugin.name}</span> </div>
</div> );
); })}
})}
</div>
</div> </div>
</Page.Contents> </Page.Contents>
</Page> </Page>
......
...@@ -7,6 +7,7 @@ const setup = (propOverrides?: object) => { ...@@ -7,6 +7,7 @@ const setup = (propOverrides?: object) => {
isReadOnly: true, isReadOnly: true,
onSubmit: jest.fn(), onSubmit: jest.fn(),
onDelete: jest.fn(), onDelete: jest.fn(),
onTest: jest.fn(),
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
......
...@@ -4,14 +4,22 @@ export interface Props { ...@@ -4,14 +4,22 @@ export interface Props {
isReadOnly: boolean; isReadOnly: boolean;
onDelete: () => void; onDelete: () => void;
onSubmit: (event) => void; onSubmit: (event) => void;
onTest: (event) => void;
} }
const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit }) => { const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
return ( return (
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<button type="submit" className="btn btn-primary" disabled={isReadOnly} onClick={event => onSubmit(event)}> {!isReadOnly && (
Save &amp; Test <button type="submit" className="btn btn-primary" disabled={isReadOnly} onClick={event => onSubmit(event)}>
</button> Save &amp; Test
</button>
)}
{isReadOnly && (
<button type="submit" className="btn btn-success" onClick={onTest}>
Test
</button>
)}
<button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}> <button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
Delete Delete
</button> </button>
......
...@@ -72,6 +72,12 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> { ...@@ -72,6 +72,12 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
this.testDataSource(); this.testDataSource();
}; };
onTest = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
this.testDataSource();
};
onDelete = () => { onDelete = () => {
appEvents.emit('confirm-modal', { appEvents.emit('confirm-modal', {
title: 'Delete', title: 'Delete',
...@@ -180,7 +186,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> { ...@@ -180,7 +186,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
return ( return (
<Page navModel={navModel}> <Page navModel={navModel}>
<Page.Contents isLoading={!this.hasDataSource}> <Page.Contents isLoading={!this.hasDataSource}>
{this.hasDataSource && <div className="page-container page-body"> {this.hasDataSource && (
<div> <div>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
{this.isReadOnly() && this.renderIsReadOnlyMessage()} {this.isReadOnly() && this.renderIsReadOnlyMessage()}
...@@ -201,7 +207,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> { ...@@ -201,7 +207,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
/> />
)} )}
<div className="gf-form-group section"> <div className="gf-form-group">
{testingMessage && ( {testingMessage && (
<div className={`alert-${testingStatus} alert`}> <div className={`alert-${testingStatus} alert`}>
<div className="alert-icon"> <div className="alert-icon">
...@@ -222,10 +228,11 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> { ...@@ -222,10 +228,11 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
onSubmit={event => this.onSubmit(event)} onSubmit={event => this.onSubmit(event)}
isReadOnly={this.isReadOnly()} isReadOnly={this.isReadOnly()}
onDelete={this.onDelete} onDelete={this.onDelete}
onTest={event => this.onTest(event)}
/> />
</form> </form>
</div> </div>
</div>} )}
</Page.Contents> </Page.Contents>
</Page> </Page>
); );
......
...@@ -5,12 +5,11 @@ exports[`Render should render component 1`] = ` ...@@ -5,12 +5,11 @@ exports[`Render should render component 1`] = `
className="gf-form-button-row" className="gf-form-button-row"
> >
<button <button
className="btn btn-primary" className="btn btn-success"
disabled={true} onClick={[MockFunction]}
onClick={[Function]}
type="submit" type="submit"
> >
Save & Test Test
</button> </button>
<button <button
className="btn btn-danger" className="btn btn-danger"
......
...@@ -5,15 +5,7 @@ import * as rangeUtil from 'app/core/utils/rangeutil'; ...@@ -5,15 +5,7 @@ import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch } from '@grafana/ui'; import { RawTimeRange, Switch } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import { import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogLevel, LogsMetaKind } from 'app/core/logs_model';
LogsDedupDescription,
LogsDedupStrategy,
LogsModel,
dedupLogRows,
filterLogLevels,
LogLevel,
LogsMetaKind,
} from 'app/core/logs_model';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
...@@ -51,6 +43,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { ...@@ -51,6 +43,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
interface Props { interface Props {
data?: LogsModel; data?: LogsModel;
dedupedData?: LogsModel;
width: number; width: number;
exploreId: string; exploreId: string;
highlighterExpressions: string[]; highlighterExpressions: string[];
...@@ -59,16 +52,17 @@ interface Props { ...@@ -59,16 +52,17 @@ interface Props {
scanning?: boolean; scanning?: boolean;
scanRange?: RawTimeRange; scanRange?: RawTimeRange;
dedupStrategy: LogsDedupStrategy; dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
onChangeTime?: (range: RawTimeRange) => void; onChangeTime?: (range: RawTimeRange) => void;
onClickLabel?: (label: string, value: string) => void; onClickLabel?: (label: string, value: string) => void;
onStartScanning?: () => void; onStartScanning?: () => void;
onStopScanning?: () => void; onStopScanning?: () => void;
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void; onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
onToggleLogLevel: (hiddenLogLevels: Set<LogLevel>) => void;
} }
interface State { interface State {
deferLogs: boolean; deferLogs: boolean;
hiddenLogLevels: Set<LogLevel>;
renderAll: boolean; renderAll: boolean;
showLabels: boolean | null; // Tristate: null means auto showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean; showLocalTime: boolean;
...@@ -81,7 +75,6 @@ export default class Logs extends PureComponent<Props, State> { ...@@ -81,7 +75,6 @@ export default class Logs extends PureComponent<Props, State> {
state = { state = {
deferLogs: true, deferLogs: true,
hiddenLogLevels: new Set(),
renderAll: false, renderAll: false,
showLabels: null, showLabels: null,
showLocalTime: true, showLocalTime: true,
...@@ -142,7 +135,7 @@ export default class Logs extends PureComponent<Props, State> { ...@@ -142,7 +135,7 @@ export default class Logs extends PureComponent<Props, State> {
onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set<string>) => { onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set<string>) => {
const hiddenLogLevels: Set<LogLevel> = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level])); const hiddenLogLevels: Set<LogLevel> = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level]));
this.setState({ hiddenLogLevels }); this.props.onToggleLogLevel(hiddenLogLevels);
}; };
onClickScan = (event: React.SyntheticEvent) => { onClickScan = (event: React.SyntheticEvent) => {
...@@ -166,21 +159,18 @@ export default class Logs extends PureComponent<Props, State> { ...@@ -166,21 +159,18 @@ export default class Logs extends PureComponent<Props, State> {
scanning, scanning,
scanRange, scanRange,
width, width,
dedupedData,
} = this.props; } = this.props;
if (!data) { if (!data) {
return null; return null;
} }
const { deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc, } = this.state; const { deferLogs, renderAll, showLocalTime, showUtc } = this.state;
let { showLabels } = this.state; let { showLabels } = this.state;
const { dedupStrategy } = this.props; const { dedupStrategy } = this.props;
const hasData = data && data.rows && data.rows.length > 0; const hasData = data && data.rows && data.rows.length > 0;
const showDuplicates = dedupStrategy !== LogsDedupStrategy.none; const showDuplicates = dedupStrategy !== LogsDedupStrategy.none;
// Filtering
const filteredData = filterLogLevels(data, hiddenLogLevels);
const dedupedData = dedupLogRows(filteredData, dedupStrategy);
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
const meta = [...data.meta]; const meta = [...data.meta];
......
...@@ -4,18 +4,21 @@ import { connect } from 'react-redux'; ...@@ -4,18 +4,21 @@ import { connect } from 'react-redux';
import { RawTimeRange, TimeRange } from '@grafana/ui'; import { RawTimeRange, TimeRange } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { toggleLogs, changeDedupStrategy } from './state/actions'; import { toggleLogs, changeDedupStrategy } from './state/actions';
import Logs from './Logs'; import Logs from './Logs';
import Panel from './Panel'; import Panel from './Panel';
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors';
interface LogsContainerProps { interface LogsContainerProps {
exploreId: ExploreId; exploreId: ExploreId;
loading: boolean; loading: boolean;
logsHighlighterExpressions?: string[]; logsHighlighterExpressions?: string[];
logsResult?: LogsModel; logsResult?: LogsModel;
dedupedResult?: LogsModel;
onChangeTime: (range: TimeRange) => void; onChangeTime: (range: TimeRange) => void;
onClickLabel: (key: string, value: string) => void; onClickLabel: (key: string, value: string) => void;
onStartScanning: () => void; onStartScanning: () => void;
...@@ -25,8 +28,10 @@ interface LogsContainerProps { ...@@ -25,8 +28,10 @@ interface LogsContainerProps {
scanRange?: RawTimeRange; scanRange?: RawTimeRange;
showingLogs: boolean; showingLogs: boolean;
toggleLogs: typeof toggleLogs; toggleLogs: typeof toggleLogs;
toggleLogLevelAction: typeof toggleLogLevelAction;
changeDedupStrategy: typeof changeDedupStrategy; changeDedupStrategy: typeof changeDedupStrategy;
dedupStrategy: LogsDedupStrategy; dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
width: number; width: number;
} }
...@@ -39,12 +44,21 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -39,12 +44,21 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy); this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy);
}; };
hangleToggleLogLevel = (hiddenLogLevels: Set<LogLevel>) => {
const { exploreId } = this.props;
this.props.toggleLogLevelAction({
exploreId,
hiddenLogLevels,
});
};
render() { render() {
const { const {
exploreId, exploreId,
loading, loading,
logsHighlighterExpressions, logsHighlighterExpressions,
logsResult, logsResult,
dedupedResult,
onChangeTime, onChangeTime,
onClickLabel, onClickLabel,
onStartScanning, onStartScanning,
...@@ -54,6 +68,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -54,6 +68,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
scanning, scanning,
scanRange, scanRange,
width, width,
hiddenLogLevels,
} = this.props; } = this.props;
return ( return (
...@@ -61,6 +76,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -61,6 +76,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
<Logs <Logs
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none} dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
data={logsResult} data={logsResult}
dedupedData={dedupedResult}
exploreId={exploreId} exploreId={exploreId}
key={logsResult && logsResult.id} key={logsResult && logsResult.id}
highlighterExpressions={logsHighlighterExpressions} highlighterExpressions={logsHighlighterExpressions}
...@@ -70,32 +86,26 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -70,32 +86,26 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
onStartScanning={onStartScanning} onStartScanning={onStartScanning}
onStopScanning={onStopScanning} onStopScanning={onStopScanning}
onDedupStrategyChange={this.handleDedupStrategyChange} onDedupStrategyChange={this.handleDedupStrategyChange}
onToggleLogLevel={this.hangleToggleLogLevel}
range={range} range={range}
scanning={scanning} scanning={scanning}
scanRange={scanRange} scanRange={scanRange}
width={width} width={width}
hiddenLogLevels={hiddenLogLevels}
/> />
</Panel> </Panel>
); );
} }
} }
const selectItemUIState = (itemState: ExploreItemState) => {
const { showingGraph, showingLogs, showingTable, showingStartPage, dedupStrategy } = itemState;
return {
showingGraph,
showingLogs,
showingTable,
showingStartPage,
dedupStrategy,
};
};
function mapStateToProps(state: StoreState, { exploreId }) { function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore; const explore = state.explore;
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId];
const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item; const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item;
const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
const {showingLogs, dedupStrategy} = selectItemUIState(item); const { showingLogs, dedupStrategy } = exploreItemUIStateSelector(item);
const hiddenLogLevels = new Set(item.hiddenLogLevels);
const dedupedResult = deduplicatedLogsSelector(item);
return { return {
loading, loading,
...@@ -106,12 +116,15 @@ function mapStateToProps(state: StoreState, { exploreId }) { ...@@ -106,12 +116,15 @@ function mapStateToProps(state: StoreState, { exploreId }) {
showingLogs, showingLogs,
range, range,
dedupStrategy, dedupStrategy,
hiddenLogLevels,
dedupedResult,
}; };
} }
const mapDispatchToProps = { const mapDispatchToProps = {
toggleLogs, toggleLogs,
changeDedupStrategy, changeDedupStrategy,
toggleLogLevelAction,
}; };
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
ExploreUIState, ExploreUIState,
} from 'app/types/explore'; } from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
import { LogLevel } from 'app/core/logs_model';
/** Higher order actions /** Higher order actions
* *
...@@ -192,7 +193,7 @@ export interface ToggleLogsPayload { ...@@ -192,7 +193,7 @@ export interface ToggleLogsPayload {
exploreId: ExploreId; exploreId: ExploreId;
} }
export interface UpdateUIStatePayload extends Partial<ExploreUIState>{ export interface UpdateUIStatePayload extends Partial<ExploreUIState> {
exploreId: ExploreId; exploreId: ExploreId;
} }
...@@ -201,6 +202,11 @@ export interface UpdateDatasourceInstancePayload { ...@@ -201,6 +202,11 @@ export interface UpdateDatasourceInstancePayload {
datasourceInstance: DataSourceApi; datasourceInstance: DataSourceApi;
} }
export interface ToggleLogLevelPayload {
exploreId: ExploreId;
hiddenLogLevels: Set<LogLevel>;
}
export interface QueriesImportedPayload { export interface QueriesImportedPayload {
exploreId: ExploreId; exploreId: ExploreId;
queries: DataQuery[]; queries: DataQuery[];
...@@ -397,6 +403,8 @@ export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasou ...@@ -397,6 +403,8 @@ export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasou
'explore/UPDATE_DATASOURCE_INSTANCE' 'explore/UPDATE_DATASOURCE_INSTANCE'
).create(); ).create();
export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>('explore/TOGGLE_LOG_LEVEL').create();
/** /**
* Resets state for explore. * Resets state for explore.
*/ */
...@@ -436,4 +444,5 @@ export type Action = ...@@ -436,4 +444,5 @@ export type Action =
| ActionOf<ToggleGraphPayload> | ActionOf<ToggleGraphPayload>
| ActionOf<ToggleLogsPayload> | ActionOf<ToggleLogsPayload>
| ActionOf<UpdateDatasourceInstancePayload> | ActionOf<UpdateDatasourceInstancePayload>
| ActionOf<QueriesImportedPayload>; | ActionOf<QueriesImportedPayload>
| ActionOf<ToggleLogLevelPayload>;
...@@ -38,6 +38,7 @@ import { ...@@ -38,6 +38,7 @@ import {
toggleTableAction, toggleTableAction,
queriesImportedAction, queriesImportedAction,
updateUIStateAction, updateUIStateAction,
toggleLogLevelAction,
} from './actionTypes'; } from './actionTypes';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
...@@ -467,6 +468,16 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -467,6 +468,16 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
}; };
}, },
}) })
.addMapper({
filter: toggleLogLevelAction,
mapper: (state, action): ExploreItemState => {
const { hiddenLogLevels } = action.payload;
return {
...state,
hiddenLogLevels: Array.from(hiddenLogLevels),
};
},
})
.create(); .create();
/** /**
......
import { createLodashMemoizedSelector } from 'app/core/utils/reselect';
import { ExploreItemState } from 'app/types';
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
export const exploreItemUIStateSelector = (itemState: ExploreItemState) => {
const { showingGraph, showingLogs, showingTable, showingStartPage, dedupStrategy } = itemState;
return {
showingGraph,
showingLogs,
showingTable,
showingStartPage,
dedupStrategy,
};
};
const logsSelector = (state: ExploreItemState) => state.logsResult;
const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels;
const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy;
export const deduplicatedLogsSelector = createLodashMemoizedSelector(
logsSelector,
hiddenLogLevelsSelector,
dedupStrategySelector,
(logs, hiddenLogLevels, dedupStrategy) => {
if (!logs) {
return null;
}
const filteredData = filterLogLevels(logs, new Set(hiddenLogLevels));
return dedupLogRows(filteredData, dedupStrategy);
}
);
...@@ -26,7 +26,7 @@ export class FolderSettingsPage extends PureComponent<Props, State> { ...@@ -26,7 +26,7 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
isLoading: false isLoading: false,
}; };
} }
...@@ -41,9 +41,9 @@ export class FolderSettingsPage extends PureComponent<Props, State> { ...@@ -41,9 +41,9 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
onSave = async (evt: React.FormEvent<HTMLFormElement>) => { onSave = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this.setState({isLoading: true}); this.setState({ isLoading: true });
await this.props.saveFolder(this.props.folder); await this.props.saveFolder(this.props.folder);
this.setState({isLoading: false}); this.setState({ isLoading: false });
}; };
onDelete = (evt: React.MouseEvent<HTMLButtonElement>) => { onDelete = (evt: React.MouseEvent<HTMLButtonElement>) => {
...@@ -67,30 +67,28 @@ export class FolderSettingsPage extends PureComponent<Props, State> { ...@@ -67,30 +67,28 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
return ( return (
<Page navModel={navModel}> <Page navModel={navModel}>
<Page.Contents isLoading={this.state.isLoading}> <Page.Contents isLoading={this.state.isLoading}>
<div className="page-container page-body"> <h2 className="page-sub-heading">Folder Settings</h2>
<h2 className="page-sub-heading">Folder Settings</h2>
<div className="section gf-form-group"> <div className="section gf-form-group">
<form name="folderSettingsForm" onSubmit={this.onSave}> <form name="folderSettingsForm" onSubmit={this.onSave}>
<div className="gf-form"> <div className="gf-form">
<label className="gf-form-label width-7">Name</label> <label className="gf-form-label width-7">Name</label>
<input <input
type="text" type="text"
className="gf-form-input width-30" className="gf-form-input width-30"
value={folder.title} value={folder.title}
onChange={this.onTitleChange} onChange={this.onTitleChange}
/> />
</div> </div>
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<button type="submit" className="btn btn-primary" disabled={!folder.canSave || !folder.hasChanged}> <button type="submit" className="btn btn-primary" disabled={!folder.canSave || !folder.hasChanged}>
<i className="fa fa-save" /> Save <i className="fa fa-save" /> Save
</button> </button>
<button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}> <button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
<i className="fa fa-trash" /> Delete <i className="fa fa-trash" /> Delete
</button> </button>
</div> </div>
</form> </form>
</div>
</div> </div>
</Page.Contents> </Page.Contents>
</Page> </Page>
......
...@@ -7,62 +7,58 @@ exports[`Render should enable save button 1`] = ` ...@@ -7,62 +7,58 @@ exports[`Render should enable save button 1`] = `
<PageContents <PageContents
isLoading={false} isLoading={false}
> >
<h2
className="page-sub-heading"
>
Folder Settings
</h2>
<div <div
className="page-container page-body" className="section gf-form-group"
> >
<h2 <form
className="page-sub-heading" name="folderSettingsForm"
> onSubmit={[Function]}
Folder Settings
</h2>
<div
className="section gf-form-group"
> >
<form <div
name="folderSettingsForm" className="gf-form"
onSubmit={[Function]}
> >
<div <label
className="gf-form" className="gf-form-label width-7"
> >
<label Name
className="gf-form-label width-7" </label>
> <input
Name className="gf-form-input width-30"
</label> onChange={[Function]}
<input type="text"
className="gf-form-input width-30" value="loading"
onChange={[Function]} />
type="text" </div>
value="loading" <div
className="gf-form-button-row"
>
<button
className="btn btn-primary"
disabled={false}
type="submit"
>
<i
className="fa fa-save"
/> />
</div> Save
<div </button>
className="gf-form-button-row" <button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
> >
<button <i
className="btn btn-primary" className="fa fa-trash"
disabled={false} />
type="submit" Delete
> </button>
<i </div>
className="fa fa-save" </form>
/>
Save
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-trash"
/>
Delete
</button>
</div>
</form>
</div>
</div> </div>
</PageContents> </PageContents>
</Page> </Page>
...@@ -75,62 +71,58 @@ exports[`Render should render component 1`] = ` ...@@ -75,62 +71,58 @@ exports[`Render should render component 1`] = `
<PageContents <PageContents
isLoading={false} isLoading={false}
> >
<h2
className="page-sub-heading"
>
Folder Settings
</h2>
<div <div
className="page-container page-body" className="section gf-form-group"
> >
<h2 <form
className="page-sub-heading" name="folderSettingsForm"
> onSubmit={[Function]}
Folder Settings
</h2>
<div
className="section gf-form-group"
> >
<form <div
name="folderSettingsForm" className="gf-form"
onSubmit={[Function]}
> >
<div <label
className="gf-form" className="gf-form-label width-7"
> >
<label Name
className="gf-form-label width-7" </label>
> <input
Name className="gf-form-input width-30"
</label> onChange={[Function]}
<input type="text"
className="gf-form-input width-30" value="loading"
onChange={[Function]} />
type="text" </div>
value="loading" <div
className="gf-form-button-row"
>
<button
className="btn btn-primary"
disabled={true}
type="submit"
>
<i
className="fa fa-save"
/> />
</div> Save
<div </button>
className="gf-form-button-row" <button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
> >
<button <i
className="btn btn-primary" className="fa fa-trash"
disabled={true} />
type="submit" Delete
> </button>
<i </div>
className="fa fa-save" </form>
/>
Save
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-trash"
/>
Delete
</button>
</div>
</form>
</div>
</div> </div>
</PageContents> </PageContents>
</Page> </Page>
......
...@@ -49,9 +49,9 @@ export class TeamPages extends PureComponent<Props, State> { ...@@ -49,9 +49,9 @@ export class TeamPages extends PureComponent<Props, State> {
async fetchTeam() { async fetchTeam() {
const { loadTeam, teamId } = this.props; const { loadTeam, teamId } = this.props;
this.setState({isLoading: true}); this.setState({ isLoading: true });
const team = await loadTeam(teamId); const team = await loadTeam(teamId);
this.setState({isLoading: false}); this.setState({ isLoading: false });
return team; return team;
} }
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
} from '@grafana/ui'; } from '@grafana/ui';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
export interface CompletionItem { export interface CompletionItem {
...@@ -242,6 +242,11 @@ export interface ExploreItemState { ...@@ -242,6 +242,11 @@ export interface ExploreItemState {
* Current logs deduplication strategy * Current logs deduplication strategy
*/ */
dedupStrategy?: LogsDedupStrategy; dedupStrategy?: LogsDedupStrategy;
/**
* Currently hidden log series
*/
hiddenLogLevels?: LogLevel[];
} }
export interface ExploreUIState { export interface ExploreUIState {
......
...@@ -1833,6 +1833,13 @@ ...@@ -1833,6 +1833,13 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^2.2.0" csstype "^2.2.0"
"@types/reselect@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/reselect/-/reselect-2.2.0.tgz#c667206cfdc38190e1d379babe08865b2288575f"
integrity sha1-xmcgbP3DgZDh03m6vgiGWyKIV18=
dependencies:
reselect "*"
"@types/storybook__addon-actions@^3.4.1": "@types/storybook__addon-actions@^3.4.1":
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.1.tgz#8f90d76b023b58ee794170f2fe774a3fddda2c1d" resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.1.tgz#8f90d76b023b58ee794170f2fe774a3fddda2c1d"
...@@ -4712,6 +4719,11 @@ ci-info@^1.5.0: ...@@ -4712,6 +4719,11 @@ ci-info@^1.5.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
cidr-regex@1.0.6: cidr-regex@1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
...@@ -7922,6 +7934,11 @@ get-stdin@^4.0.1: ...@@ -7922,6 +7934,11 @@ get-stdin@^4.0.1:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
get-stdin@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
get-stream@3.0.0, get-stream@^3.0.0: get-stream@3.0.0, get-stream@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
...@@ -8886,14 +8903,21 @@ humanize-ms@^1.2.1: ...@@ -8886,14 +8903,21 @@ humanize-ms@^1.2.1:
dependencies: dependencies:
ms "^2.0.0" ms "^2.0.0"
husky@^0.14.3: husky@^1.3.1:
version "0.14.3" version "1.3.1"
resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" resolved "https://registry.yarnpkg.com/husky/-/husky-1.3.1.tgz#26823e399300388ca2afff11cfa8a86b0033fae0"
integrity sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA== integrity sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==
dependencies: dependencies:
is-ci "^1.0.10" cosmiconfig "^5.0.7"
normalize-path "^1.0.0" execa "^1.0.0"
strip-indent "^2.0.0" find-up "^3.0.0"
get-stdin "^6.0.0"
is-ci "^2.0.0"
pkg-dir "^3.0.0"
please-upgrade-node "^3.1.1"
read-pkg "^4.0.1"
run-node "^1.0.0"
slash "^2.0.0"
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
version "0.4.24" version "0.4.24"
...@@ -9279,6 +9303,13 @@ is-ci@^1.0.10: ...@@ -9279,6 +9303,13 @@ is-ci@^1.0.10:
dependencies: dependencies:
ci-info "^1.5.0" ci-info "^1.5.0"
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
dependencies:
ci-info "^2.0.0"
is-cidr@~1.0.0: is-cidr@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc" resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc"
...@@ -11925,11 +11956,6 @@ normalize-path@2.0.1: ...@@ -11925,11 +11956,6 @@ normalize-path@2.0.1:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
integrity sha1-R4hqwWYnYNQmG32XnSQXCdPOP3o= integrity sha1-R4hqwWYnYNQmG32XnSQXCdPOP3o=
normalize-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
integrity sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=
normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
...@@ -12948,7 +12974,7 @@ pkg-up@^1.0.0: ...@@ -12948,7 +12974,7 @@ pkg-up@^1.0.0:
dependencies: dependencies:
find-up "^1.0.0" find-up "^1.0.0"
please-upgrade-node@^3.0.2: please-upgrade-node@^3.0.2, please-upgrade-node@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac"
integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ== integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==
...@@ -14336,6 +14362,15 @@ read-pkg@^3.0.0: ...@@ -14336,6 +14362,15 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2" normalize-package-data "^2.3.2"
path-type "^3.0.0" path-type "^3.0.0"
read-pkg@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237"
integrity sha1-ljYlN48+HE1IyFhytabsfV0JMjc=
dependencies:
normalize-package-data "^2.3.2"
parse-json "^4.0.0"
pify "^3.0.0"
read@1, read@~1.0.1, read@~1.0.7: read@1, read@~1.0.1, read@~1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
...@@ -14823,6 +14858,11 @@ requires-port@^1.0.0: ...@@ -14823,6 +14858,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
reselect@*, reselect@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
resolve-cwd@^2.0.0: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
...@@ -14990,6 +15030,11 @@ run-async@^2.0.0, run-async@^2.2.0: ...@@ -14990,6 +15030,11 @@ run-async@^2.0.0, run-async@^2.2.0:
dependencies: dependencies:
is-promise "^2.1.0" is-promise "^2.1.0"
run-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e"
integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==
run-queue@^1.0.0, run-queue@^1.0.3: run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
...@@ -15448,6 +15493,11 @@ slash@^1.0.0: ...@@ -15448,6 +15493,11 @@ slash@^1.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
slash@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
slate-base64-serializer@^0.2.36: slate-base64-serializer@^0.2.36:
version "0.2.94" version "0.2.94"
resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.94.tgz#b908c3af481b9a0ead78f313653414c4b2b4b2d5" resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.94.tgz#b908c3af481b9a0ead78f313653414c4b2b4b2d5"
...@@ -16147,11 +16197,6 @@ strip-indent@^1.0.1: ...@@ -16147,11 +16197,6 @@ strip-indent@^1.0.1:
dependencies: dependencies:
get-stdin "^4.0.1" get-stdin "^4.0.1"
strip-indent@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
strip-json-comments@~1.0.1: strip-json-comments@~1.0.1:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment