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:
image: elasticsearch:5
command: elasticsearch
......
......@@ -65,7 +65,7 @@
"html-loader": "^0.5.1",
"html-webpack-harddisk-plugin": "^0.2.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3",
"husky": "^1.3.1",
"jest": "^23.6.0",
"jest-date-mock": "^1.0.6",
"lint-staged": "^8.1.3",
......@@ -120,7 +120,6 @@
"typecheck": "tsc --noEmit",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "grunt precommit",
"storybook": "cd packages/grafana-ui && yarn storybook"
},
"husky": {
......@@ -151,6 +150,7 @@
"dependencies": {
"@babel/polyfill": "^7.0.0",
"@torkelo/react-select": "2.1.1",
"@types/reselect": "^2.2.0",
"angular": "1.6.6",
"angular-bindonce": "0.3.1",
"angular-native-dragdrop": "1.2.2",
......@@ -187,6 +187,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"remarkable": "^1.7.1",
"reselect": "^4.0.0",
"rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^6.3.3",
"slate": "^0.33.4",
......
import React from 'react';
import React, { ChangeEvent } from 'react';
import { shallow } from 'enzyme';
import { ThresholdsEditor, Props } from './ThresholdsEditor';
......@@ -118,7 +118,7 @@ describe('change threshold value', () => {
];
const instance = setup({ thresholds });
const mockEvent = { target: { value: 12 } };
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, thresholds[0]);
......@@ -137,7 +137,7 @@ describe('change threshold value', () => {
thresholds,
};
const mockEvent = { target: { value: 78 } };
const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
......
import React, { PureComponent } from 'react';
import React, { PureComponent, ChangeEvent } from 'react';
import { Threshold } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
......@@ -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) {
return;
}
const { thresholds } = this.state;
const parsedValue = parseInt(event.target.value, 10);
const value = isNaN(parsedValue) ? null : parsedValue;
const cleanValue = event.target.value.replace(/,/g, '.');
const parsedValue = parseFloat(cleanValue);
const value = isNaN(parsedValue) ? '' : parsedValue;
const newThresholds = thresholds.map(t => {
if (t === threshold && t.index !== 0) {
......@@ -164,16 +165,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
<div className="thresholds-row-input-inner-color">
{threshold.color && (
<div className="thresholds-row-input-inner-color-colorpicker">
<ColorPicker
color={threshold.color}
onChange={color => this.onChangeThresholdColor(threshold, color)}
/>
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
</div>
)}
</div>
<div className="thresholds-row-input-inner-value">
<input
type="text"
type="number"
step="0.0001"
onChange={event => this.onChangeThresholdValue(event, threshold)}
value={value}
onBlur={this.onBlur}
......
......@@ -31,7 +31,7 @@ $popper-margin-from-ref: 5px;
// Themes
&.popper__background--error {
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
@include popper-theme($tooltipBackgroundError, $white);
}
&.popper__background--info {
......
......@@ -16,7 +16,7 @@ func (hs *HTTPServer) registerRoutes() {
reqOrgAdmin := middleware.ReqOrgAdmin
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
quota := middleware.Quota
quota := middleware.Quota(hs.QuotaService)
bind := binding.Bind
r := hs.RouteRegister
......@@ -286,7 +286,7 @@ func (hs *HTTPServer) registerRoutes() {
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("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
......@@ -294,7 +294,7 @@ func (hs *HTTPServer) registerRoutes() {
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
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) {
dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
......
......@@ -18,7 +18,6 @@ import (
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"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/util"
)
......@@ -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.UserId = c.UserId
dash := cmd.GetDashboardModel()
if dash.Id == 0 && dash.Uid == "" {
limitReached, err := quota.QuotaReached(c, "dashboard")
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
if err != nil {
return Error(500, "failed to get quota", err)
}
......@@ -463,7 +462,7 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
}
// 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"), "")
if rsp != nil {
return rsp
......@@ -490,7 +489,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
saveCmd.Dashboard.Set("uid", dash.Uid)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
return PostDashboard(c, saveCmd)
return hs.PostDashboard(c, saveCmd)
}
func GetDashboardTags(c *m.ReqContext) {
......
......@@ -881,12 +881,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
hs := HTTPServer{
Bus: bus.GetBus(),
}
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
return PostDashboard(c, cmd)
return hs.PostDashboard(c, cmd)
})
origNewDashboardService := dashboards.NewService
......
......@@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources"
"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/session"
"github.com/grafana/grafana/pkg/setting"
......@@ -55,6 +56,7 @@ type HTTPServer struct {
CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
AuthTokenService models.UserTokenService `inject:""`
QuotaService *quota.QuotaService `inject:""`
}
func (hs *HTTPServer) Init() error {
......
......@@ -4,18 +4,30 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/quota"
)
func init() {
bus.AddHandler("auth", UpsertUser)
registry.RegisterService(&LoginService{})
}
var (
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
userQuery := &m.GetUserByAuthInfoQuery{
......@@ -37,7 +49,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
return ErrInvalidCredentials
}
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user")
if err != nil {
log.Warn("Error getting user quota. error: %v", err)
return ErrGettingUserQuota
......@@ -57,7 +69,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
}
if err := bus.Dispatch(cmd2); err != nil {
if err := ls.Bus.Dispatch(cmd2); err != nil {
return err
}
}
......@@ -78,12 +90,12 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
// Sync isGrafanaAdmin permission
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
}
}
err = bus.Dispatch(&m.SyncTeamsCommand{
err = ls.Bus.Dispatch(&m.SyncTeamsCommand{
User: cmd.Result,
ExternalUser: extUser,
})
......
......@@ -395,8 +395,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
defer bus.ClearBusHandlers()
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 {
return nil
......
......@@ -682,6 +682,7 @@ type fakeUserAuthTokenService struct {
tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
lookupTokenProvider func(unhashedToken string) (*m.UserToken, error)
revokeTokenProvider func(token *m.UserToken) error
activeAuthTokenCount func() (int64, error)
}
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
......@@ -704,6 +705,9 @@ func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
revokeTokenProvider: func(token *m.UserToken) error {
return nil
},
activeAuthTokenCount: func() (int64, error) {
return 10, nil
},
}
}
......@@ -722,3 +726,7 @@ func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP,
func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
return s.revokeTokenProvider(token)
}
func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
return s.activeAuthTokenCount()
}
......@@ -9,16 +9,20 @@ import (
"github.com/grafana/grafana/pkg/services/quota"
)
func Quota(target string) macaron.Handler {
return func(c *m.ReqContext) {
limitReached, err := quota.QuotaReached(c, target)
if err != nil {
c.JsonApiErr(500, "failed to get quota", err)
return
}
if limitReached {
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
return
// Quota returns a function that returns a function used to call quotaservice based on target name
func Quota(quotaService *quota.QuotaService) func(target string) macaron.Handler {
//https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky
return func(target string) macaron.Handler {
return func(c *m.ReqContext) {
limitReached, err := quotaService.QuotaReached(c, target)
if err != nil {
c.JsonApiErr(500, "failed to get quota", err)
return
}
if limitReached {
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
return
}
}
}
}
......@@ -3,9 +3,10 @@ package middleware
import (
"testing"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
......@@ -13,10 +14,6 @@ import (
func TestMiddlewareQuota(t *testing.T) {
Convey("Given the grafana quota middleware", t, func() {
session.GetSessionCount = func() int {
return 4
}
setting.AnonymousEnabled = false
setting.Quota = setting.QuotaSettings{
Enabled: true,
......@@ -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) {
bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
query.Result = &m.GlobalQuotaDTO{
......@@ -48,26 +51,30 @@ func TestMiddlewareQuota(t *testing.T) {
}
return nil
})
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()
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("global quota reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 403)
})
Convey("global session quota not reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("global session quota reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 403)
})
......@@ -95,6 +102,7 @@ func TestMiddlewareQuota(t *testing.T) {
}
return nil
})
bus.AddHandler("userQuota", func(query *m.GetUserQuotaByTargetQuery) error {
query.Result = &m.UserQuotaDTO{
Target: query.Target,
......@@ -103,6 +111,7 @@ func TestMiddlewareQuota(t *testing.T) {
}
return nil
})
bus.AddHandler("orgQuota", func(query *m.GetOrgQuotaByTargetQuery) error {
query.Result = &m.OrgQuotaDTO{
Target: query.Target,
......@@ -111,45 +120,49 @@ func TestMiddlewareQuota(t *testing.T) {
}
return nil
})
Convey("global datasource quota reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 403)
})
Convey("user Org quota not reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("user Org quota reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 403)
})
Convey("org dashboard quota not reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("org dashboard quota reached", func() {
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()
So(sc.resp.Code, ShouldEqual, 403)
})
Convey("org dashboard quota reached but quotas disabled", func() {
setting.Quota.Org.Dashboard = 4
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()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
}
......@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
macaron "gopkg.in/macaron.v1"
......@@ -66,7 +65,6 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
......
......@@ -29,4 +29,5 @@ type UserTokenService interface {
LookupToken(unhashedToken string) (*UserToken, error)
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
RevokeToken(token *UserToken) error
ActiveTokenCount() (int64, error)
}
......@@ -35,6 +35,13 @@ func (s *UserAuthTokenService) Init() error {
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) {
clientIP = util.ParseIPAddress(clientIP)
token, err := util.RandomHex(16)
......@@ -79,13 +86,8 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo
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
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 {
return nil, err
}
......@@ -219,6 +221,16 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
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 {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:])
......
......@@ -31,6 +31,12 @@ func TestUserAuthToken(t *testing.T) {
So(userToken, ShouldNotBeNil)
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() {
userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil)
......@@ -114,6 +120,12 @@ func TestUserAuthToken(t *testing.T) {
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
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() {
......
......@@ -3,11 +3,23 @@ package quota
import (
"github.com/grafana/grafana/pkg/bus"
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"
)
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 {
return false, nil
}
......@@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) {
return true, nil
}
if target == "session" {
usedSessions := session.GetSessionCount()
usedSessions, err := qs.AuthTokenService.ActiveTokenCount()
if err != nil {
return false, err
}
if int64(usedSessions) > scope.DefaultLimit {
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
return true, nil
......
......@@ -19,7 +19,7 @@ const (
var sessionManager *ms.Manager
var sessionOptions *ms.Options
var StartSessionGC func()
var StartSessionGC func() = func() {}
var GetSessionCount func() int
var sessionLogger = log.New("session")
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 {
});
});
this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders;
this.canSaveAs = contextSrv.hasEditPermissionInFolders;
this.canSave = this.dashboard.meta.canSave;
this.canDelete = this.dashboard.meta.canSave;
......
......@@ -36,31 +36,29 @@ class NewDataSourcePage extends PureComponent<Props> {
return (
<Page navModel={navModel}>
<Page.Contents isLoading={isLoading}>
<div className="page-container page-body">
<h2 className="add-data-source-header">Choose data source type</h2>
<div className="add-data-source-search">
<FilterInput
labelClassName="gf-form--has-input-icon"
inputClassName="gf-form-input width-20"
value={dataSourceTypeSearchQuery}
onChange={this.onSearchQueryChange}
placeholder="Filter by name or type"
/>
</div>
<div className="add-data-source-grid">
{dataSourceTypes.map((plugin, index) => {
return (
<div
onClick={() => this.onDataSourceTypeClicked(plugin)}
className="add-data-source-grid-item"
key={`${plugin.id}-${index}`}
>
<img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} />
<span className="add-data-source-grid-item-text">{plugin.name}</span>
</div>
);
})}
</div>
<h2 className="add-data-source-header">Choose data source type</h2>
<div className="add-data-source-search">
<FilterInput
labelClassName="gf-form--has-input-icon"
inputClassName="gf-form-input width-20"
value={dataSourceTypeSearchQuery}
onChange={this.onSearchQueryChange}
placeholder="Filter by name or type"
/>
</div>
<div className="add-data-source-grid">
{dataSourceTypes.map((plugin, index) => {
return (
<div
onClick={() => this.onDataSourceTypeClicked(plugin)}
className="add-data-source-grid-item"
key={`${plugin.id}-${index}`}
>
<img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} />
<span className="add-data-source-grid-item-text">{plugin.name}</span>
</div>
);
})}
</div>
</Page.Contents>
</Page>
......
......@@ -7,6 +7,7 @@ const setup = (propOverrides?: object) => {
isReadOnly: true,
onSubmit: jest.fn(),
onDelete: jest.fn(),
onTest: jest.fn(),
};
Object.assign(props, propOverrides);
......
......@@ -4,14 +4,22 @@ export interface Props {
isReadOnly: boolean;
onDelete: () => void;
onSubmit: (event) => void;
onTest: (event) => void;
}
const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
return (
<div className="gf-form-button-row">
<button type="submit" className="btn btn-primary" disabled={isReadOnly} onClick={event => onSubmit(event)}>
Save &amp; Test
</button>
{!isReadOnly && (
<button type="submit" className="btn btn-primary" disabled={isReadOnly} onClick={event => onSubmit(event)}>
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}>
Delete
</button>
......
......@@ -72,6 +72,12 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
this.testDataSource();
};
onTest = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
this.testDataSource();
};
onDelete = () => {
appEvents.emit('confirm-modal', {
title: 'Delete',
......@@ -180,7 +186,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
return (
<Page navModel={navModel}>
<Page.Contents isLoading={!this.hasDataSource}>
{this.hasDataSource && <div className="page-container page-body">
{this.hasDataSource && (
<div>
<form onSubmit={this.onSubmit}>
{this.isReadOnly() && this.renderIsReadOnlyMessage()}
......@@ -201,7 +207,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
/>
)}
<div className="gf-form-group section">
<div className="gf-form-group">
{testingMessage && (
<div className={`alert-${testingStatus} alert`}>
<div className="alert-icon">
......@@ -222,10 +228,11 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
onSubmit={event => this.onSubmit(event)}
isReadOnly={this.isReadOnly()}
onDelete={this.onDelete}
onTest={event => this.onTest(event)}
/>
</form>
</div>
</div>}
)}
</Page.Contents>
</Page>
);
......
......@@ -5,12 +5,11 @@ exports[`Render should render component 1`] = `
className="gf-form-button-row"
>
<button
className="btn btn-primary"
disabled={true}
onClick={[Function]}
className="btn btn-success"
onClick={[MockFunction]}
type="submit"
>
Save & Test
Test
</button>
<button
className="btn btn-danger"
......
......@@ -5,15 +5,7 @@ import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import {
LogsDedupDescription,
LogsDedupStrategy,
LogsModel,
dedupLogRows,
filterLogLevels,
LogLevel,
LogsMetaKind,
} from 'app/core/logs_model';
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogLevel, LogsMetaKind } from 'app/core/logs_model';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
......@@ -51,6 +43,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
interface Props {
data?: LogsModel;
dedupedData?: LogsModel;
width: number;
exploreId: string;
highlighterExpressions: string[];
......@@ -59,16 +52,17 @@ interface Props {
scanning?: boolean;
scanRange?: RawTimeRange;
dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
onChangeTime?: (range: RawTimeRange) => void;
onClickLabel?: (label: string, value: string) => void;
onStartScanning?: () => void;
onStopScanning?: () => void;
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
onToggleLogLevel: (hiddenLogLevels: Set<LogLevel>) => void;
}
interface State {
deferLogs: boolean;
hiddenLogLevels: Set<LogLevel>;
renderAll: boolean;
showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean;
......@@ -81,7 +75,6 @@ export default class Logs extends PureComponent<Props, State> {
state = {
deferLogs: true,
hiddenLogLevels: new Set(),
renderAll: false,
showLabels: null,
showLocalTime: true,
......@@ -142,7 +135,7 @@ export default class Logs extends PureComponent<Props, State> {
onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set<string>) => {
const hiddenLogLevels: Set<LogLevel> = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level]));
this.setState({ hiddenLogLevels });
this.props.onToggleLogLevel(hiddenLogLevels);
};
onClickScan = (event: React.SyntheticEvent) => {
......@@ -166,21 +159,18 @@ export default class Logs extends PureComponent<Props, State> {
scanning,
scanRange,
width,
dedupedData,
} = this.props;
if (!data) {
return null;
}
const { deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc, } = this.state;
const { deferLogs, renderAll, showLocalTime, showUtc } = this.state;
let { showLabels } = this.state;
const { dedupStrategy } = this.props;
const hasData = data && data.rows && data.rows.length > 0;
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 meta = [...data.meta];
......
......@@ -4,18 +4,21 @@ import { connect } from 'react-redux';
import { RawTimeRange, TimeRange } from '@grafana/ui';
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 { toggleLogs, changeDedupStrategy } from './state/actions';
import Logs from './Logs';
import Panel from './Panel';
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors';
interface LogsContainerProps {
exploreId: ExploreId;
loading: boolean;
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
dedupedResult?: LogsModel;
onChangeTime: (range: TimeRange) => void;
onClickLabel: (key: string, value: string) => void;
onStartScanning: () => void;
......@@ -25,8 +28,10 @@ interface LogsContainerProps {
scanRange?: RawTimeRange;
showingLogs: boolean;
toggleLogs: typeof toggleLogs;
toggleLogLevelAction: typeof toggleLogLevelAction;
changeDedupStrategy: typeof changeDedupStrategy;
dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
width: number;
}
......@@ -39,12 +44,21 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy);
};
hangleToggleLogLevel = (hiddenLogLevels: Set<LogLevel>) => {
const { exploreId } = this.props;
this.props.toggleLogLevelAction({
exploreId,
hiddenLogLevels,
});
};
render() {
const {
exploreId,
loading,
logsHighlighterExpressions,
logsResult,
dedupedResult,
onChangeTime,
onClickLabel,
onStartScanning,
......@@ -54,6 +68,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
scanning,
scanRange,
width,
hiddenLogLevels,
} = this.props;
return (
......@@ -61,6 +76,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
<Logs
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
data={logsResult}
dedupedData={dedupedResult}
exploreId={exploreId}
key={logsResult && logsResult.id}
highlighterExpressions={logsHighlighterExpressions}
......@@ -70,32 +86,26 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
onStartScanning={onStartScanning}
onStopScanning={onStopScanning}
onDedupStrategyChange={this.handleDedupStrategyChange}
onToggleLogLevel={this.hangleToggleLogLevel}
range={range}
scanning={scanning}
scanRange={scanRange}
width={width}
hiddenLogLevels={hiddenLogLevels}
/>
</Panel>
);
}
}
const selectItemUIState = (itemState: ExploreItemState) => {
const { showingGraph, showingLogs, showingTable, showingStartPage, dedupStrategy } = itemState;
return {
showingGraph,
showingLogs,
showingTable,
showingStartPage,
dedupStrategy,
};
};
function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item;
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 {
loading,
......@@ -106,12 +116,15 @@ function mapStateToProps(state: StoreState, { exploreId }) {
showingLogs,
range,
dedupStrategy,
hiddenLogLevels,
dedupedResult,
};
}
const mapDispatchToProps = {
toggleLogs,
changeDedupStrategy,
toggleLogLevelAction,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
......@@ -18,6 +18,7 @@ import {
ExploreUIState,
} from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
import { LogLevel } from 'app/core/logs_model';
/** Higher order actions
*
......@@ -192,7 +193,7 @@ export interface ToggleLogsPayload {
exploreId: ExploreId;
}
export interface UpdateUIStatePayload extends Partial<ExploreUIState>{
export interface UpdateUIStatePayload extends Partial<ExploreUIState> {
exploreId: ExploreId;
}
......@@ -201,6 +202,11 @@ export interface UpdateDatasourceInstancePayload {
datasourceInstance: DataSourceApi;
}
export interface ToggleLogLevelPayload {
exploreId: ExploreId;
hiddenLogLevels: Set<LogLevel>;
}
export interface QueriesImportedPayload {
exploreId: ExploreId;
queries: DataQuery[];
......@@ -397,6 +403,8 @@ export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasou
'explore/UPDATE_DATASOURCE_INSTANCE'
).create();
export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>('explore/TOGGLE_LOG_LEVEL').create();
/**
* Resets state for explore.
*/
......@@ -436,4 +444,5 @@ export type Action =
| ActionOf<ToggleGraphPayload>
| ActionOf<ToggleLogsPayload>
| ActionOf<UpdateDatasourceInstancePayload>
| ActionOf<QueriesImportedPayload>;
| ActionOf<QueriesImportedPayload>
| ActionOf<ToggleLogLevelPayload>;
......@@ -38,6 +38,7 @@ import {
toggleTableAction,
queriesImportedAction,
updateUIStateAction,
toggleLogLevelAction,
} from './actionTypes';
export const DEFAULT_RANGE = {
......@@ -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();
/**
......
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> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false
isLoading: false,
};
}
......@@ -41,9 +41,9 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
onSave = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
evt.stopPropagation();
this.setState({isLoading: true});
this.setState({ isLoading: true });
await this.props.saveFolder(this.props.folder);
this.setState({isLoading: false});
this.setState({ isLoading: false });
};
onDelete = (evt: React.MouseEvent<HTMLButtonElement>) => {
......@@ -67,30 +67,28 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
return (
<Page navModel={navModel}>
<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">
<form name="folderSettingsForm" onSubmit={this.onSave}>
<div className="gf-form">
<label className="gf-form-label width-7">Name</label>
<input
type="text"
className="gf-form-input width-30"
value={folder.title}
onChange={this.onTitleChange}
/>
</div>
<div className="gf-form-button-row">
<button type="submit" className="btn btn-primary" disabled={!folder.canSave || !folder.hasChanged}>
<i className="fa fa-save" /> Save
</button>
<button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
<i className="fa fa-trash" /> Delete
</button>
</div>
</form>
</div>
<div className="section gf-form-group">
<form name="folderSettingsForm" onSubmit={this.onSave}>
<div className="gf-form">
<label className="gf-form-label width-7">Name</label>
<input
type="text"
className="gf-form-input width-30"
value={folder.title}
onChange={this.onTitleChange}
/>
</div>
<div className="gf-form-button-row">
<button type="submit" className="btn btn-primary" disabled={!folder.canSave || !folder.hasChanged}>
<i className="fa fa-save" /> Save
</button>
<button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
<i className="fa fa-trash" /> Delete
</button>
</div>
</form>
</div>
</Page.Contents>
</Page>
......
......@@ -7,62 +7,58 @@ exports[`Render should enable save button 1`] = `
<PageContents
isLoading={false}
>
<h2
className="page-sub-heading"
>
Folder Settings
</h2>
<div
className="page-container page-body"
className="section gf-form-group"
>
<h2
className="page-sub-heading"
>
Folder Settings
</h2>
<div
className="section gf-form-group"
<form
name="folderSettingsForm"
onSubmit={[Function]}
>
<form
name="folderSettingsForm"
onSubmit={[Function]}
<div
className="gf-form"
>
<div
className="gf-form"
<label
className="gf-form-label width-7"
>
<label
className="gf-form-label width-7"
>
Name
</label>
<input
className="gf-form-input width-30"
onChange={[Function]}
type="text"
value="loading"
Name
</label>
<input
className="gf-form-input width-30"
onChange={[Function]}
type="text"
value="loading"
/>
</div>
<div
className="gf-form-button-row"
>
<button
className="btn btn-primary"
disabled={false}
type="submit"
>
<i
className="fa fa-save"
/>
</div>
<div
className="gf-form-button-row"
Save
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
>
<button
className="btn btn-primary"
disabled={false}
type="submit"
>
<i
className="fa fa-save"
/>
Save
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-trash"
/>
Delete
</button>
</div>
</form>
</div>
<i
className="fa fa-trash"
/>
Delete
</button>
</div>
</form>
</div>
</PageContents>
</Page>
......@@ -75,62 +71,58 @@ exports[`Render should render component 1`] = `
<PageContents
isLoading={false}
>
<h2
className="page-sub-heading"
>
Folder Settings
</h2>
<div
className="page-container page-body"
className="section gf-form-group"
>
<h2
className="page-sub-heading"
>
Folder Settings
</h2>
<div
className="section gf-form-group"
<form
name="folderSettingsForm"
onSubmit={[Function]}
>
<form
name="folderSettingsForm"
onSubmit={[Function]}
<div
className="gf-form"
>
<div
className="gf-form"
<label
className="gf-form-label width-7"
>
<label
className="gf-form-label width-7"
>
Name
</label>
<input
className="gf-form-input width-30"
onChange={[Function]}
type="text"
value="loading"
Name
</label>
<input
className="gf-form-input width-30"
onChange={[Function]}
type="text"
value="loading"
/>
</div>
<div
className="gf-form-button-row"
>
<button
className="btn btn-primary"
disabled={true}
type="submit"
>
<i
className="fa fa-save"
/>
</div>
<div
className="gf-form-button-row"
Save
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
>
<button
className="btn btn-primary"
disabled={true}
type="submit"
>
<i
className="fa fa-save"
/>
Save
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-trash"
/>
Delete
</button>
</div>
</form>
</div>
<i
className="fa fa-trash"
/>
Delete
</button>
</div>
</form>
</div>
</PageContents>
</Page>
......
......@@ -49,9 +49,9 @@ export class TeamPages extends PureComponent<Props, State> {
async fetchTeam() {
const { loadTeam, teamId } = this.props;
this.setState({isLoading: true});
this.setState({ isLoading: true });
const team = await loadTeam(teamId);
this.setState({isLoading: false});
this.setState({ isLoading: false });
return team;
}
......
......@@ -11,7 +11,7 @@ import {
} from '@grafana/ui';
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';
export interface CompletionItem {
......@@ -242,6 +242,11 @@ export interface ExploreItemState {
* Current logs deduplication strategy
*/
dedupStrategy?: LogsDedupStrategy;
/**
* Currently hidden log series
*/
hiddenLogLevels?: LogLevel[];
}
export interface ExploreUIState {
......
......@@ -1833,6 +1833,13 @@
"@types/prop-types" "*"
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":
version "3.4.1"
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:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
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:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
......@@ -7922,6 +7934,11 @@ get-stdin@^4.0.1:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
......@@ -8886,14 +8903,21 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
husky@^0.14.3:
version "0.14.3"
resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3"
integrity sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==
husky@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/husky/-/husky-1.3.1.tgz#26823e399300388ca2afff11cfa8a86b0033fae0"
integrity sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==
dependencies:
is-ci "^1.0.10"
normalize-path "^1.0.0"
strip-indent "^2.0.0"
cosmiconfig "^5.0.7"
execa "^1.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:
version "0.4.24"
......@@ -9279,6 +9303,13 @@ is-ci@^1.0.10:
dependencies:
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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc"
......@@ -11925,11 +11956,6 @@ normalize-path@2.0.1:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
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:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
......@@ -12948,7 +12974,7 @@ pkg-up@^1.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac"
integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==
......@@ -14336,6 +14362,15 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2"
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:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
......@@ -14823,6 +14858,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
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:
version "2.0.0"
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:
dependencies:
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:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
......@@ -15448,6 +15493,11 @@ slash@^1.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
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:
version "0.2.94"
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:
dependencies:
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:
version "1.0.4"
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