Commit a4dd63e2 by Torkel Ödegaard

Merge branch 'master' into ui-new-red-green-blue

parents 2a2b242e ba2f698b
......@@ -11,6 +11,7 @@
* **MSSQL**: Timerange are now passed for template variable queries [#13324](https://github.com/grafana/grafana/issues/13324), thx [@thatsparesh](https://github.com/thatsparesh)
* **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh)
* **Templating**: Add json formatting to variable interpolation [#15291](https://github.com/grafana/grafana/issues/15291), thx [@mtanda](https://github.com/mtanda)
* **Login**: Anonymous usage stats for token auth [#15288](https://github.com/grafana/grafana/issues/15288)
### 6.0.0-beta1 fixes
......
......@@ -7,13 +7,18 @@
Grafana is an open source, feature rich metrics dashboard and graph editor for
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
![](https://www.grafanacon.org/2019/images/grafanacon_la_nav-logo.png)
Join us Feb 25-26 in Los Angeles, California for GrafanaCon - a two-day event with talks focused on Grafana and the surrounding open source monitoring ecosystem. Get deep dives into Loki, the Explore workflow and all of the new features of Grafana 6, plus participate in hands on workshops to help you get the most out of your data.
Time is running out - grab your ticket now! http://grafanacon.org
<!---
![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png)
-->
## Installation
Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get)
the latest release.
If you have any problems please read the [troubleshooting guide](http://docs.grafana.org/installation/troubleshooting/).
Head to [docs.grafana.org](http://docs.grafana.org/installation/) for documentation or [download](https://grafana.com/get) to get the latest release.
## Documentation & Support
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
......
......@@ -27,6 +27,7 @@
"@types/react-dom": "^16.0.9",
"@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-transition-group": "^2.0.15",
"@types/react-virtualized": "^9.18.12",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
......
package usagestats
import (
"context"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/social"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
var metricsLogger log.Logger = log.New("metrics")
func init() {
registry.RegisterService(&UsageStatsService{})
}
type UsageStatsService struct {
Cfg *setting.Cfg `inject:""`
Bus bus.Bus `inject:""`
SQLStore *sqlstore.SqlStore `inject:""`
oauthProviders map[string]bool
}
func (uss *UsageStatsService) Init() error {
uss.oauthProviders = social.GetOAuthProviders(uss.Cfg)
return nil
}
func (uss *UsageStatsService) Run(ctx context.Context) error {
uss.updateTotalStats()
onceEveryDayTick := time.NewTicker(time.Hour * 24)
everyMinuteTicker := time.NewTicker(time.Minute)
defer onceEveryDayTick.Stop()
defer everyMinuteTicker.Stop()
for {
select {
case <-onceEveryDayTick.C:
uss.sendUsageStats(uss.oauthProviders)
case <-everyMinuteTicker.C:
uss.updateTotalStats()
case <-ctx.Done():
return ctx.Err()
}
}
}
package usagestats
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"runtime"
"strings"
"time"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) {
if !setting.ReportingEnabled {
return
}
metricsLogger.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL))
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
metrics := map[string]interface{}{}
report := map[string]interface{}{
"version": version,
"metrics": metrics,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"edition": getEdition(),
"packaging": setting.Packaging,
}
statsQuery := models.GetSystemStatsQuery{}
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
metrics["stats.users.count"] = statsQuery.Result.Users
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
metrics["stats.stars.count"] = statsQuery.Result.Stars
metrics["stats.folders.count"] = statsQuery.Result.Folders
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
metrics["stats.teams.count"] = statsQuery.Result.Teams
metrics["stats.total_sessions.count"] = statsQuery.Result.Sessions
userCount := statsQuery.Result.Users
avgSessionsPerUser := statsQuery.Result.Sessions
if userCount != 0 {
avgSessionsPerUser = avgSessionsPerUser / userCount
}
metrics["stats.avg_sessions_per_user.count"] = avgSessionsPerUser
dsStats := models.GetDataSourceStatsQuery{}
if err := uss.Bus.Dispatch(&dsStats); err != nil {
metricsLogger.Error("Failed to get datasource stats", "error", err)
return
}
// send counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsOtherCount := 0
for _, dsStat := range dsStats.Result {
if models.IsKnownDataSourcePlugin(dsStat.Type) {
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
} else {
dsOtherCount += dsStat.Count
}
}
metrics["stats.ds.other.count"] = dsOtherCount
metrics["stats.packaging."+setting.Packaging+".count"] = 1
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := uss.Bus.Dispatch(&dsAccessStats); err != nil {
metricsLogger.Error("Failed to get datasource access stats", "error", err)
return
}
// send access counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsAccessOtherCount := make(map[string]int64)
for _, dsAccessStat := range dsAccessStats.Result {
if dsAccessStat.Access == "" {
continue
}
access := strings.ToLower(dsAccessStat.Access)
if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
} else {
old := dsAccessOtherCount[access]
dsAccessOtherCount[access] = old + dsAccessStat.Count
}
}
for access, count := range dsAccessOtherCount {
metrics["stats.ds_access.other."+access+".count"] = count
}
anStats := models.GetAlertNotifierUsageStatsQuery{}
if err := uss.Bus.Dispatch(&anStats); err != nil {
metricsLogger.Error("Failed to get alert notification stats", "error", err)
return
}
for _, stats := range anStats.Result {
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
}
authTypes := map[string]bool{}
authTypes["anonymous"] = setting.AnonymousEnabled
authTypes["basic_auth"] = setting.BasicAuthEnabled
authTypes["ldap"] = setting.LdapEnabled
authTypes["auth_proxy"] = setting.AuthProxyEnabled
for provider, enabled := range oauthProviders {
authTypes["oauth_"+provider] = enabled
}
for authType, enabled := range authTypes {
enabledValue := 0
if enabled {
enabledValue = 1
}
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
}
out, _ := json.MarshalIndent(report, "", " ")
data := bytes.NewBuffer(out)
client := http.Client{Timeout: 5 * time.Second}
go client.Post(usageStatsURL, "application/json", data)
}
func (uss *UsageStatsService) updateTotalStats() {
statsQuery := models.GetSystemStatsQuery{}
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
metrics.M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
metrics.M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
metrics.M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
metrics.M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
metrics.M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
}
func getEdition() string {
if setting.IsEnterprise {
return "enterprise"
} else {
return "oss"
}
}
package metrics
package usagestats
import (
"bytes"
......@@ -15,14 +15,21 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestMetrics(t *testing.T) {
Convey("Test send usage stats", t, func() {
uss := &UsageStatsService{
Bus: bus.New(),
SQLStore: sqlstore.InitTestDB(t),
}
var getSystemStatsQuery *models.GetSystemStatsQuery
bus.AddHandler("test", func(query *models.GetSystemStatsQuery) error {
uss.Bus.AddHandler(func(query *models.GetSystemStatsQuery) error {
query.Result = &models.SystemStats{
Dashboards: 1,
Datasources: 2,
......@@ -38,13 +45,14 @@ func TestMetrics(t *testing.T) {
ProvisionedDashboards: 12,
Snapshots: 13,
Teams: 14,
Sessions: 15,
}
getSystemStatsQuery = query
return nil
})
var getDataSourceStatsQuery *models.GetDataSourceStatsQuery
bus.AddHandler("test", func(query *models.GetDataSourceStatsQuery) error {
uss.Bus.AddHandler(func(query *models.GetDataSourceStatsQuery) error {
query.Result = []*models.DataSourceStats{
{
Type: models.DS_ES,
......@@ -68,7 +76,7 @@ func TestMetrics(t *testing.T) {
})
var getDataSourceAccessStatsQuery *models.GetDataSourceAccessStatsQuery
bus.AddHandler("test", func(query *models.GetDataSourceAccessStatsQuery) error {
uss.Bus.AddHandler(func(query *models.GetDataSourceAccessStatsQuery) error {
query.Result = []*models.DataSourceAccessStats{
{
Type: models.DS_ES,
......@@ -116,7 +124,7 @@ func TestMetrics(t *testing.T) {
})
var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error {
uss.Bus.AddHandler(func(query *models.GetAlertNotifierUsageStatsQuery) error {
query.Result = []*models.NotifierUsageStats{
{
Type: "slack",
......@@ -155,11 +163,11 @@ func TestMetrics(t *testing.T) {
"grafana_com": true,
}
sendUsageStats(oauthProviders)
uss.sendUsageStats(oauthProviders)
Convey("Given reporting not enabled and sending usage stats", func() {
setting.ReportingEnabled = false
sendUsageStats(oauthProviders)
uss.sendUsageStats(oauthProviders)
Convey("Should not gather stats or call http endpoint", func() {
So(getSystemStatsQuery, ShouldBeNil)
......@@ -179,7 +187,7 @@ func TestMetrics(t *testing.T) {
setting.Packaging = "deb"
wg.Add(1)
sendUsageStats(oauthProviders)
uss.sendUsageStats(oauthProviders)
Convey("Should gather stats and call http endpoint", func() {
if waitTimeout(&wg, 2*time.Second) {
......@@ -221,6 +229,8 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.provisioned_dashboards.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.ProvisionedDashboards)
So(metrics.Get("stats.snapshots.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Snapshots)
So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams)
So(metrics.Get("stats.total_sessions.count").MustInt64(), ShouldEqual, 15)
So(metrics.Get("stats.avg_sessions_per_user.count").MustInt64(), ShouldEqual, 5)
So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9)
So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10)
......@@ -246,6 +256,7 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
})
})
......
package metrics
import (
"bytes"
"encoding/json"
"net/http"
"runtime"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
......@@ -68,23 +59,6 @@ var (
grafanaBuildVersion *prometheus.GaugeVec
)
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
counter := prometheus.NewCounterVec(opts, labels)
for _, label := range labelValues {
counter.WithLabelValues(label).Add(0)
}
return counter
}
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
counter := prometheus.NewCounter(opts)
counter.Add(0)
return counter
}
func init() {
M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{
Name: "instance_start_total",
......@@ -362,154 +336,19 @@ func initMetricVars() {
}
func updateTotalStats() {
statsQuery := models.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
}
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
func getEdition() string {
if setting.IsEnterprise {
return "enterprise"
} else {
return "oss"
}
}
func sendUsageStats(oauthProviders map[string]bool) {
if !setting.ReportingEnabled {
return
}
metricsLogger.Debug("Sending anonymous usage stats to stats.grafana.org")
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
metrics := map[string]interface{}{}
report := map[string]interface{}{
"version": version,
"metrics": metrics,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"edition": getEdition(),
"packaging": setting.Packaging,
}
statsQuery := models.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
metrics["stats.users.count"] = statsQuery.Result.Users
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
metrics["stats.stars.count"] = statsQuery.Result.Stars
metrics["stats.folders.count"] = statsQuery.Result.Folders
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
metrics["stats.teams.count"] = statsQuery.Result.Teams
dsStats := models.GetDataSourceStatsQuery{}
if err := bus.Dispatch(&dsStats); err != nil {
metricsLogger.Error("Failed to get datasource stats", "error", err)
return
}
// send counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsOtherCount := 0
for _, dsStat := range dsStats.Result {
if models.IsKnownDataSourcePlugin(dsStat.Type) {
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
} else {
dsOtherCount += dsStat.Count
}
}
metrics["stats.ds.other.count"] = dsOtherCount
metrics["stats.packaging."+setting.Packaging+".count"] = 1
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := bus.Dispatch(&dsAccessStats); err != nil {
metricsLogger.Error("Failed to get datasource access stats", "error", err)
return
}
// send access counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsAccessOtherCount := make(map[string]int64)
for _, dsAccessStat := range dsAccessStats.Result {
if dsAccessStat.Access == "" {
continue
}
access := strings.ToLower(dsAccessStat.Access)
if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
} else {
old := dsAccessOtherCount[access]
dsAccessOtherCount[access] = old + dsAccessStat.Count
}
}
for access, count := range dsAccessOtherCount {
metrics["stats.ds_access.other."+access+".count"] = count
}
anStats := models.GetAlertNotifierUsageStatsQuery{}
if err := bus.Dispatch(&anStats); err != nil {
metricsLogger.Error("Failed to get alert notification stats", "error", err)
return
}
for _, stats := range anStats.Result {
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
}
authTypes := map[string]bool{}
authTypes["anonymous"] = setting.AnonymousEnabled
authTypes["basic_auth"] = setting.BasicAuthEnabled
authTypes["ldap"] = setting.LdapEnabled
authTypes["auth_proxy"] = setting.AuthProxyEnabled
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
counter := prometheus.NewCounterVec(opts, labels)
for provider, enabled := range oauthProviders {
authTypes["oauth_"+provider] = enabled
for _, label := range labelValues {
counter.WithLabelValues(label).Add(0)
}
for authType, enabled := range authTypes {
enabledValue := 0
if enabled {
enabledValue = 1
}
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
}
return counter
}
out, _ := json.MarshalIndent(report, "", " ")
data := bytes.NewBuffer(out)
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
counter := prometheus.NewCounter(opts)
counter.Add(0)
client := http.Client{Timeout: 5 * time.Second}
go client.Post(usageStatsURL, "application/json", data)
return counter
}
......@@ -2,7 +2,6 @@ package metrics
import (
"context"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
......@@ -30,7 +29,6 @@ type InternalMetricsService struct {
intervalSeconds int64
graphiteCfg *graphitebridge.Config
oauthProviders map[string]bool
}
func (im *InternalMetricsService) Init() error {
......@@ -50,22 +48,6 @@ func (im *InternalMetricsService) Run(ctx context.Context) error {
M_Instance_Start.Inc()
// set the total stats gauges before we publishing metrics
updateTotalStats()
onceEveryDayTick := time.NewTicker(time.Hour * 24)
everyMinuteTicker := time.NewTicker(time.Minute)
defer onceEveryDayTick.Stop()
defer everyMinuteTicker.Stop()
for {
select {
case <-onceEveryDayTick.C:
sendUsageStats(im.oauthProviders)
case <-everyMinuteTicker.C:
updateTotalStats()
case <-ctx.Done():
return ctx.Err()
}
}
<-ctx.Done()
return ctx.Err()
}
......@@ -5,8 +5,6 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/social"
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
......@@ -24,8 +22,6 @@ func (im *InternalMetricsService) readSettings() error {
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
}
im.oauthProviders = social.GetOAuthProviders(im.Cfg)
return nil
}
......
......@@ -15,6 +15,7 @@ type SystemStats struct {
FolderPermissions int64
Folders int64
ProvisionedDashboards int64
Sessions int64
}
type DataSourceStats struct {
......
......@@ -74,7 +74,8 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS sessions`)
var stats m.SystemStats
_, err := x.SQL(sb.GetSqlString(), sb.params...).Get(&stats)
......
import React, { FC } from 'react';
import Transition from 'react-transition-group/Transition';
import Transition, { ExitHandler } from 'react-transition-group/Transition';
interface Props {
duration: number;
children: JSX.Element;
in: boolean;
unmountOnExit?: boolean;
onExited?: ExitHandler;
}
export const FadeIn: FC<Props> = props => {
......@@ -22,7 +23,12 @@ export const FadeIn: FC<Props> = props => {
};
return (
<Transition in={props.in} timeout={props.duration} unmountOnExit={props.unmountOnExit || false}>
<Transition
in={props.in}
timeout={props.duration}
unmountOnExit={props.unmountOnExit || false}
onExited={props.onExited}
>
{state => (
<div
style={{
......
......@@ -8,6 +8,16 @@ jest.mock('../../app_events', () => ({
emit: jest.fn(),
}));
jest.mock('app/store/store', () => ({
store: {
getState: jest.fn().mockReturnValue({
location: {
lastUpdated: 0,
}
})
}
}));
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
sidemenu: true,
......
......@@ -3,9 +3,16 @@ import appEvents from '../../app_events';
import { contextSrv } from 'app/core/services/context_srv';
import TopSection from './TopSection';
import BottomSection from './BottomSection';
import { store } from 'app/store/store';
export class SideMenu extends PureComponent {
toggleSideMenu = () => {
// ignore if we just made a location change, stops hiding sidemenu on double clicks of back button
const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated;
if (timeSinceLocationChanged < 1000) {
return;
}
contextSrv.toggleSideMenu();
appEvents.emit('toggle-sidemenu');
};
......
import './directives/dash_class';
import './directives/dropdown_typeahead';
import './directives/autofill_event_fix';
import './directives/metric_segment';
......
import $ from 'jquery';
import _ from 'lodash';
import coreModule from '../core_module';
/** @ngInject */
function dashClass($timeout) {
return {
link: ($scope, elem) => {
const body = $('body');
$scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
console.log('view-mode-changed', panel.fullscreen);
if (panel.fullscreen) {
body.addClass('panel-in-fullscreen');
} else {
$timeout(() => {
body.removeClass('panel-in-fullscreen');
});
}
});
body.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
if (newValue) {
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
setTimeout(() => {
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
}, 10);
} else {
elem.removeClass('dashboard-page--settings-opening');
elem.removeClass('dashboard-page--settings-open');
}
});
},
};
}
coreModule.directive('dashClass', dashClass);
......@@ -340,6 +340,11 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
return a[1] - b[1];
});
return { datapoints: series.datapoints, target: series.alias, color: series.color };
return {
datapoints: series.datapoints,
target: series.alias,
alias: series.alias,
color: series.color
};
});
}
......@@ -9,6 +9,7 @@ export const initialState: LocationState = {
query: {},
routeParams: {},
replace: false,
lastUpdated: 0,
};
export const locationReducer = (state = initialState, action: Action): LocationState => {
......@@ -28,6 +29,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
query: { ...query },
routeParams: routeParams || state.routeParams,
replace: replace === true,
lastUpdated: new Date().getTime(),
};
}
}
......
......@@ -9,12 +9,13 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
// Components
import { DashNavButton } from './DashNavButton';
import { Tooltip } from '@grafana/ui';
// State
import { updateLocation } from 'app/core/actions';
// Types
import { DashboardModel } from '../../state/DashboardModel';
import { DashboardModel } from '../../state';
export interface Props {
dashboard: DashboardModel;
......@@ -33,7 +34,6 @@ export class DashNav extends PureComponent<Props> {
constructor(props: Props) {
super(props);
this.playlistSrv = this.props.$injector.get('playlistSrv');
}
......@@ -123,26 +123,54 @@ export class DashNav extends PureComponent<Props> {
});
};
render() {
const { dashboard, isFullscreen, editview, onAddPanel } = this.props;
const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta;
const { snapshot } = dashboard;
renderDashboardTitleSearchButton() {
const { dashboard } = this.props;
const folderTitle = dashboard.meta.folderTitle;
const haveFolder = dashboard.meta.folderId > 0;
const snapshotUrl = snapshot && snapshot.originalUrl;
return (
<div className="navbar">
<>
<div>
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
<i className="gicon gicon-dashboard" />
{!this.isInFullscreenOrSettings && <i className="gicon gicon-dashboard" />}
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
{dashboard.title}
<i className="fa fa-caret-down" />
</a>
</div>
<div className="navbar__spacer" />
</>
);
}
get isInFullscreenOrSettings() {
return this.props.editview || this.props.isFullscreen;
}
renderBackButton() {
return (
<div className="navbar-edit">
<Tooltip content="Go back (Esc)">
<button className="navbar-edit__back-btn" onClick={this.onClose}>
<i className="fa fa-arrow-left" />
</button>
</Tooltip>
</div>
);
}
render() {
const { dashboard, onAddPanel } = this.props;
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
const { snapshot } = dashboard;
const snapshotUrl = snapshot && snapshot.originalUrl;
return (
<div className="navbar">
{this.isInFullscreenOrSettings && this.renderBackButton()}
{this.renderDashboardTitleSearchButton()}
{this.playlistSrv.isPlaying && (
<div className="navbar-buttons navbar-buttons--playlist">
......@@ -228,17 +256,6 @@ export class DashNav extends PureComponent<Props> {
</div>
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
{(isFullscreen || editview) && (
<div className="navbar-buttons navbar-buttons--close">
<DashNavButton
tooltip="Back to dashboard"
classSuffix="primary"
icon="fa fa-reply"
onClick={this.onClose}
/>
</div>
)}
</div>
);
}
......
......@@ -149,6 +149,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
}
}
clearQuery = () => {
this.setState({ searchQuery: '' });
};
onPanelOptionsChanged = (options: any) => {
this.props.panel.updateOptions(options);
this.forceUpdate();
......@@ -241,7 +245,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
setScrollTop={this.setScrollTop}
>
<>
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
<VizTypePicker
current={plugin}
onTypeChanged={this.onTypeChanged}
......
......@@ -220,6 +220,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
{supportsLogs && (
<LogsContainer
width={width}
exploreId={exploreId}
onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
......
......@@ -51,6 +51,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
interface Props {
data?: LogsModel;
width: number;
exploreId: string;
highlighterExpressions: string[];
loading: boolean;
......@@ -165,6 +166,7 @@ export default class Logs extends PureComponent<Props, State> {
range,
scanning,
scanRange,
width,
} = this.props;
if (!data) {
......@@ -215,6 +217,7 @@ export default class Logs extends PureComponent<Props, State> {
<Graph
data={timeSeries}
height={100}
width={width}
range={range}
id={`explore-logs-graph-${exploreId}`}
onChangeTime={this.props.onChangeTime}
......
......@@ -25,6 +25,7 @@ interface LogsContainerProps {
scanRange?: RawTimeRange;
showingLogs: boolean;
toggleLogs: typeof toggleLogs;
width: number;
}
export class LogsContainer extends PureComponent<LogsContainerProps> {
......@@ -46,6 +47,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
showingLogs,
scanning,
scanRange,
width,
} = this.props;
return (
......@@ -63,6 +65,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
range={range}
scanning={scanning}
scanRange={scanRange}
width={width}
/>
</Panel>
);
......
......@@ -2,61 +2,65 @@
import React, { PureComponent } from 'react';
// Components
import { Select, SelectOptionItem } from '@grafana/ui';
// import { Select, SelectOptionItem } from '@grafana/ui';
// Types
import { QueryEditorProps } from '@grafana/ui/src/types';
import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types';
import { LokiQueryField } from './LokiQueryField';
// import { LokiQueryField } from './LokiQueryField';
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
interface State {
query: LokiQuery;
}
// interface State {
// query: LokiQuery;
// }
export class LokiQueryEditor extends PureComponent<Props> {
state: State = {
query: this.props.query,
};
onRunQuery = () => {
const { query } = this.state;
this.props.onChange(query);
this.props.onRunQuery();
};
onFieldChange = (query: LokiQuery, override?) => {
this.setState({
query: {
...this.state.query,
expr: query.expr,
},
});
};
onFormatChanged = (option: SelectOptionItem) => {
this.props.onChange({
...this.state.query,
resultFormat: option.value,
});
};
// state: State = {
// query: this.props.query,
// };
//
// onRunQuery = () => {
// const { query } = this.state;
//
// this.props.onChange(query);
// this.props.onRunQuery();
// };
//
// onFieldChange = (query: LokiQuery, override?) => {
// this.setState({
// query: {
// ...this.state.query,
// expr: query.expr,
// },
// });
// };
//
// onFormatChanged = (option: SelectOptionItem) => {
// this.props.onChange({
// ...this.state.query,
// resultFormat: option.value,
// });
// };
render() {
const { query } = this.state;
const { datasource } = this.props;
const formatOptions: SelectOptionItem[] = [
{ label: 'Time Series', value: 'time_series' },
{ label: 'Table', value: 'table' },
];
query.resultFormat = query.resultFormat || 'time_series';
const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
// const { query } = this.state;
// const { datasource } = this.props;
// const formatOptions: SelectOptionItem[] = [
// { label: 'Time Series', value: 'time_series' },
// { label: 'Table', value: 'table' },
// ];
//
// query.resultFormat = query.resultFormat || 'time_series';
// const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
return (
<div>
<div className="gf-form">
<div className="gf-form-label">Loki is currently not supported as dashboard data source. We are working on it!</div>
</div>
{/*
<LokiQueryField
datasource={datasource}
query={query}
......@@ -78,6 +82,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
*/}
</div>
);
}
......
......@@ -150,8 +150,8 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl',
})
.when('/dashboards/f/:uid', {
templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
controller: 'FolderDashboardsCtrl',
templateUrl: 'public/app/features/folders/partials/folder_dashboards.html',
controller: FolderDashboardsCtrl,
controllerAs: 'ctrl',
})
.when('/explore', {
......
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
// import { createLogger } from 'redux-logger';
import sharedReducers from 'app/core/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/state/reducers';
......@@ -41,7 +41,7 @@ export function configureStore() {
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
} else {
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
}
......
......@@ -15,6 +15,7 @@ export interface LocationState {
query: UrlQueryMap;
routeParams: UrlQueryMap;
replace: boolean;
lastUpdated: number;
}
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
......
.navbar {
position: relative;
padding-left: 40px;
padding-left: 20px;
z-index: $zindex-navbar-fixed;
height: $navbarHeight;
padding-right: 20px;
......@@ -41,15 +41,12 @@
.panel-in-fullscreen {
.navbar {
padding-left: 15px;
padding-left: 20px;
}
.navbar-button--add-panel,
.navbar-button--star,
.navbar-button--tv,
.navbar-page-btn .fa-caret-down {
display: none;
}
.navbar-buttons--close {
display: flex;
......@@ -179,3 +176,33 @@
}
}
}
.navbar-edit {
display: flex;
height: $navbarHeight;
align-items: center;
padding-left: 7px;
}
.navbar-edit__back-btn {
background: transparent;
border: 2px solid $text-color;
border-radius: 50%;
width: 34px;
height: 34px;
transition: transform 0.1s ease 0.1s;
color: $text-color;
i {
font-size: $font-size-lg;
position: relative;
top: 2px;
}
&:hover {
color: $text-color-strong;
border-color: $text-color-strong;
}
}
......@@ -86,6 +86,10 @@
.panel-editor-container__panel {
margin: 0 $dashboard-padding;
}
.search-container {
left: 0 !important;
}
}
.panel-editor-container__resizer {
......
......@@ -21,9 +21,9 @@
// Search
.search-field-wrapper {
width: 100%;
height: $navbarHeight;
display: flex;
background-color: $navbarBackground;
box-shadow: $navbarShadow;
position: relative;
& > input {
......
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