Merge branch 'main' into secret-service/feature-branch
This commit is contained in:
@@ -501,7 +501,7 @@
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"name": "area/area/expressions/sql",
|
||||
"name": "area/expressions/sql",
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/908"
|
||||
|
||||
@@ -413,5 +413,71 @@
|
||||
],
|
||||
"action": "updateLabel",
|
||||
"addLabel": "area/panel/table"
|
||||
},
|
||||
{
|
||||
"type": "changedfiles",
|
||||
"matches": [
|
||||
"public/app/plugins/datasource/azuremonitor/**/*",
|
||||
"pkg/tsdb/azuremonitor/**/*"
|
||||
],
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "changedfiles",
|
||||
"matches": [
|
||||
"public/app/plugins/datasource/graphite/**/*",
|
||||
"pkg/tsdb/graphite/**/*"
|
||||
],
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "changedfiles",
|
||||
"matches": [
|
||||
"public/app/plugins/datasource/influxdb/**/*",
|
||||
"pkg/tsdb/influx/**/*"
|
||||
],
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "changedfiles",
|
||||
"matches": [
|
||||
"public/app/plugins/datasource/elasticsearch/**/*",
|
||||
"pkg/tsdb/elasticsearch/**/*"
|
||||
],
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "changedfiles",
|
||||
"matches": [
|
||||
"public/app/plugins/datasource/cloud-monitoring/**/*",
|
||||
"pkg/tsdb/cloud-monitoring/**/*"
|
||||
],
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "changedfiles",
|
||||
"matches": [
|
||||
"public/app/plugins/datasource/opentsdb/**/*",
|
||||
"pkg/tsdb/opentsdb/**/*"
|
||||
],
|
||||
"action": "addToProject",
|
||||
"addToProject": {
|
||||
"url": "https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,6 +16,10 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
description: The version of Grafana that is being released (without the `v` prefix)`
|
||||
target:
|
||||
required: false
|
||||
type: string
|
||||
description: 'Unused: left here for backwards compatibility'
|
||||
changelog:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
@@ -1,3 +1,70 @@
|
||||
<!-- 12.0.1+security-01 START -->
|
||||
|
||||
# 12.0.1+security-01 (2025-06-13)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 12.0.1+security-01 END -->
|
||||
<!-- 11.6.2+security-01 START -->
|
||||
|
||||
# 11.6.2+security-01 (2025-06-13)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 11.6.2+security-01 END -->
|
||||
<!-- 11.5.5+security-01 START -->
|
||||
|
||||
# 11.5.5+security-01 (2025-06-13)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 11.5.5+security-01 END -->
|
||||
<!-- 11.4.5+security-01 START -->
|
||||
|
||||
# 11.4.5+security-01 (2025-06-12)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 11.4.5+security-01 END -->
|
||||
<!-- 11.3.7+security-01 START -->
|
||||
|
||||
# 11.3.7+security-01 (2025-06-12)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 11.3.7+security-01 END -->
|
||||
<!-- 11.2.10+security-01 START -->
|
||||
|
||||
# 11.2.10+security-01 (2025-06-12)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 11.2.10+security-01 END -->
|
||||
<!-- 10.4.19+security-01 START -->
|
||||
|
||||
# 10.4.19+security-01 (2025-06-12)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security:** Fixed CVE-2025-3415
|
||||
|
||||
<!-- 10.4.19+security-01 END -->
|
||||
<!-- 12.0.1 START -->
|
||||
|
||||
# 12.0.1 (2025-05-22)
|
||||
|
||||
@@ -71,7 +71,7 @@ func New(cfg app.Config) (app.App, error) {
|
||||
logger.Error("Error getting requester", "error", err)
|
||||
return
|
||||
}
|
||||
ctx = identity.WithRequester(context.Background(), requester)
|
||||
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), requester.GetOrgID())
|
||||
err = processCheck(ctx, logger, client, typesClient, req.Object, check)
|
||||
if err != nil {
|
||||
logger.Error("Error processing check", "error", err)
|
||||
@@ -87,7 +87,7 @@ func New(cfg app.Config) (app.App, error) {
|
||||
logger.Error("Error getting requester", "error", err)
|
||||
return
|
||||
}
|
||||
ctx = identity.WithRequester(context.Background(), requester)
|
||||
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), requester.GetOrgID())
|
||||
err = processCheckRetry(ctx, logger, client, typesClient, req.Object, check)
|
||||
if err != nil {
|
||||
logger.Error("Error processing check retry", "error", err)
|
||||
|
||||
@@ -22,10 +22,16 @@ import (
|
||||
const defaultEvaluationInterval = 7 * 24 * time.Hour // 7 days
|
||||
const defaultMaxHistory = 10
|
||||
|
||||
var (
|
||||
waitInterval = 5 * time.Second
|
||||
waitMaxRetries = 3
|
||||
)
|
||||
|
||||
// Runner is a "runnable" app used to be able to expose and API endpoint
|
||||
// with the existing checks types. This does not need to be a CRUD resource, but it is
|
||||
// the only way existing at the moment to expose the check types.
|
||||
type Runner struct {
|
||||
checkRegistry checkregistry.CheckService
|
||||
client resource.Client
|
||||
typesClient resource.Client
|
||||
evaluationInterval time.Duration
|
||||
@@ -41,6 +47,7 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid config type")
|
||||
}
|
||||
checkRegistry := specificConfig.CheckRegistry
|
||||
evalInterval, err := getEvaluationInterval(specificConfig.PluginConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,6 +73,7 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
}
|
||||
|
||||
return &Runner{
|
||||
checkRegistry: checkRegistry,
|
||||
client: client,
|
||||
typesClient: typesClient,
|
||||
evaluationInterval: evalInterval,
|
||||
@@ -85,7 +93,7 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
} else {
|
||||
// do an initial creation if necessary
|
||||
if lastCreated.IsZero() {
|
||||
err = r.createChecks(ctx)
|
||||
err = r.createChecks(ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error("Error creating new check reports", "error", err)
|
||||
} else {
|
||||
@@ -101,7 +109,7 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
err = r.createChecks(ctx)
|
||||
err = r.createChecks(ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error("Error creating new check reports", "error", err)
|
||||
}
|
||||
@@ -150,12 +158,25 @@ func (r *Runner) checkLastCreated(ctx context.Context, log logging.Logger) (time
|
||||
}
|
||||
|
||||
// createChecks creates a new check for each check type in the registry.
|
||||
func (r *Runner) createChecks(ctx context.Context) error {
|
||||
func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error {
|
||||
// List existing CheckType objects
|
||||
list, err := r.typesClient.List(ctx, r.namespace, resource.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing check types: %w", err)
|
||||
}
|
||||
// This may be run before the check types are registered, so we need to wait for them to be registered.
|
||||
allChecksRegistered := len(list.GetItems()) == len(r.checkRegistry.Checks())
|
||||
retryCount := 0
|
||||
for !allChecksRegistered && retryCount < waitMaxRetries {
|
||||
logger.Error("Waiting for all check types to be registered", "retryCount", retryCount, "waitInterval", waitInterval)
|
||||
time.Sleep(waitInterval)
|
||||
list, err = r.typesClient.List(ctx, r.namespace, resource.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing check types: %w", err)
|
||||
}
|
||||
allChecksRegistered = len(list.GetItems()) == len(r.checkRegistry.Checks())
|
||||
retryCount++
|
||||
}
|
||||
|
||||
// Create checks for each CheckType
|
||||
for _, item := range list.GetItems() {
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestRunner_Run(t *testing.T) {
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
typesClient: mockTypesClient,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
evaluationInterval: 1 * time.Hour,
|
||||
}
|
||||
|
||||
@@ -56,10 +56,10 @@ func TestRunner_checkLastCreated_ErrorOnList(t *testing.T) {
|
||||
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
|
||||
lastCreated, err := runner.checkLastCreated(context.Background(), logging.DefaultLogger)
|
||||
lastCreated, err := runner.checkLastCreated(context.Background(), &logging.NoOpLogger{})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, lastCreated.IsZero())
|
||||
}
|
||||
@@ -89,10 +89,10 @@ func TestRunner_checkLastCreated_UnprocessedCheck(t *testing.T) {
|
||||
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
|
||||
lastCreated, err := runner.checkLastCreated(context.Background(), logging.DefaultLogger)
|
||||
lastCreated, err := runner.checkLastCreated(context.Background(), &logging.NoOpLogger{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, lastCreated.IsZero())
|
||||
assert.Equal(t, "check-1", identifier.Name)
|
||||
@@ -104,6 +104,8 @@ func TestRunner_checkLastCreated_UnprocessedCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunner_createChecks_ErrorOnCreate(t *testing.T) {
|
||||
mockCheckService := &MockCheckService{checks: []checks.Check{&mockCheck{id: "check-1"}}}
|
||||
|
||||
mockClient := &MockClient{
|
||||
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
|
||||
return nil, errors.New("create error")
|
||||
@@ -121,16 +123,19 @@ func TestRunner_createChecks_ErrorOnCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
typesClient: mockTypesClient,
|
||||
log: logging.DefaultLogger,
|
||||
checkRegistry: mockCheckService,
|
||||
client: mockClient,
|
||||
typesClient: mockTypesClient,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
|
||||
err := runner.createChecks(context.Background())
|
||||
err := runner.createChecks(context.Background(), &logging.NoOpLogger{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRunner_createChecks_Success(t *testing.T) {
|
||||
mockCheckService := &MockCheckService{checks: []checks.Check{&mockCheck{id: "check-1"}}}
|
||||
|
||||
mockClient := &MockClient{
|
||||
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
|
||||
return &advisorv0alpha1.Check{}, nil
|
||||
@@ -148,12 +153,13 @@ func TestRunner_createChecks_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
typesClient: mockTypesClient,
|
||||
log: logging.DefaultLogger,
|
||||
checkRegistry: mockCheckService,
|
||||
client: mockClient,
|
||||
typesClient: mockTypesClient,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
|
||||
err := runner.createChecks(context.Background())
|
||||
err := runner.createChecks(context.Background(), &logging.NoOpLogger{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -166,10 +172,10 @@ func TestRunner_cleanupChecks_ErrorOnList(t *testing.T) {
|
||||
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
|
||||
err := runner.cleanupChecks(context.Background(), logging.DefaultLogger)
|
||||
err := runner.cleanupChecks(context.Background(), &logging.NoOpLogger{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -187,10 +193,10 @@ func TestRunner_cleanupChecks_WithinMax(t *testing.T) {
|
||||
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
|
||||
err := runner.cleanupChecks(context.Background(), logging.DefaultLogger)
|
||||
err := runner.cleanupChecks(context.Background(), &logging.NoOpLogger{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -217,9 +223,9 @@ func TestRunner_cleanupChecks_ErrorOnDelete(t *testing.T) {
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
maxHistory: defaultMaxHistory,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
err := runner.cleanupChecks(context.Background(), logging.DefaultLogger)
|
||||
err := runner.cleanupChecks(context.Background(), &logging.NoOpLogger{})
|
||||
assert.ErrorContains(t, err, "delete error")
|
||||
}
|
||||
|
||||
@@ -253,9 +259,9 @@ func TestRunner_cleanupChecks_Success(t *testing.T) {
|
||||
runner := &Runner{
|
||||
client: mockClient,
|
||||
maxHistory: defaultMaxHistory,
|
||||
log: logging.DefaultLogger,
|
||||
log: &logging.NoOpLogger{},
|
||||
}
|
||||
err := runner.cleanupChecks(context.Background(), logging.DefaultLogger)
|
||||
err := runner.cleanupChecks(context.Background(), &logging.NoOpLogger{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"check-0"}, itemsDeleted)
|
||||
}
|
||||
@@ -334,3 +340,26 @@ func (m *MockClient) Delete(ctx context.Context, identifier resource.Identifier,
|
||||
func (m *MockClient) PatchInto(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions, into resource.Object) error {
|
||||
return m.patchFunc(ctx, identifier, patch, options, into)
|
||||
}
|
||||
|
||||
type MockCheckService struct {
|
||||
checks []checks.Check
|
||||
}
|
||||
|
||||
func (m *MockCheckService) Checks() []checks.Check {
|
||||
return m.checks
|
||||
}
|
||||
|
||||
type mockCheck struct {
|
||||
checks.Check
|
||||
|
||||
id string
|
||||
steps []checks.Step
|
||||
}
|
||||
|
||||
func (m *mockCheck) ID() string {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func (m *mockCheck) Steps() []checks.Step {
|
||||
return m.steps
|
||||
}
|
||||
|
||||
@@ -692,8 +692,7 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
|
||||
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
|
||||
|
||||
// Determine the origin of the adhoc variable filter
|
||||
// Accepted values are `dashboard` (filter originated from dashboard), or `scope` (filter originated from scope).
|
||||
FilterOrigin: "dashboard" | "scope"
|
||||
FilterOrigin: "dashboard"
|
||||
|
||||
// FIXME: should we introduce this? --- Variable value option
|
||||
VariableValueOption: {
|
||||
|
||||
@@ -696,8 +696,7 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
|
||||
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
|
||||
|
||||
// Determine the origin of the adhoc variable filter
|
||||
// Accepted values are `dashboard` (filter originated from dashboard), or `scope` (filter originated from scope).
|
||||
FilterOrigin: "dashboard" | "scope"
|
||||
FilterOrigin: "dashboard"
|
||||
|
||||
// FIXME: should we introduce this? --- Variable value option
|
||||
VariableValueOption: {
|
||||
|
||||
@@ -1673,14 +1673,14 @@ func NewDashboardAdhocVariableSpec() *DashboardAdhocVariableSpec {
|
||||
// Define the AdHocFilterWithLabels type
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardAdHocFilterWithLabels struct {
|
||||
Key string `json:"key"`
|
||||
Operator string `json:"operator"`
|
||||
Value string `json:"value"`
|
||||
Values []string `json:"values,omitempty"`
|
||||
KeyLabel *string `json:"keyLabel,omitempty"`
|
||||
ValueLabels []string `json:"valueLabels,omitempty"`
|
||||
ForceEdit *bool `json:"forceEdit,omitempty"`
|
||||
Origin *DashboardFilterOrigin `json:"origin,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Operator string `json:"operator"`
|
||||
Value string `json:"value"`
|
||||
Values []string `json:"values,omitempty"`
|
||||
KeyLabel *string `json:"keyLabel,omitempty"`
|
||||
ValueLabels []string `json:"valueLabels,omitempty"`
|
||||
ForceEdit *bool `json:"forceEdit,omitempty"`
|
||||
Origin string `json:"origin,omitempty"`
|
||||
// @deprecated
|
||||
Condition *string `json:"condition,omitempty"`
|
||||
}
|
||||
@@ -1691,14 +1691,8 @@ func NewDashboardAdHocFilterWithLabels() *DashboardAdHocFilterWithLabels {
|
||||
}
|
||||
|
||||
// Determine the origin of the adhoc variable filter
|
||||
// Accepted values are `dashboard` (filter originated from dashboard), or `scope` (filter originated from scope).
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardFilterOrigin string
|
||||
|
||||
const (
|
||||
DashboardFilterOriginDashboard DashboardFilterOrigin = "dashboard"
|
||||
DashboardFilterOriginScope DashboardFilterOrigin = "scope"
|
||||
)
|
||||
const DashboardFilterOrigin = "dashboard"
|
||||
|
||||
// Define the MetricFindValue type
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -7,13 +7,17 @@ package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
v1beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
v2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "dashboard",
|
||||
Group: "dashboard.grafana.app",
|
||||
|
||||
@@ -1987,6 +1987,11 @@ provider = static
|
||||
# Default system date format used in time range picker and other places where full time is displayed
|
||||
full_date = YYYY-MM-DD HH:mm:ss
|
||||
|
||||
[time_picker]
|
||||
# Custom quick ranges for the time picker. Each quick range has a display name, a from value, and a to value.
|
||||
# Format: [{"from":"now-5m","to":"now","display":"Last 5 minutes"},{"from":"now-15m","to":"now","display":"Last 15 minutes"}]
|
||||
quick_ranges =
|
||||
|
||||
# Used by graph and other places where we only show small intervals
|
||||
interval_second = HH:mm:ss
|
||||
interval_minute = HH:mm
|
||||
|
||||
@@ -1924,6 +1924,11 @@ default_datasource_uid =
|
||||
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
|
||||
;default_timezone = browser
|
||||
|
||||
[time_picker]
|
||||
# Custom quick ranges for the time picker. Each quick range has a display name, a from value, and a to value.
|
||||
# Format: [{"from":"now-5m","to":"now","display":"Last 5 minutes"},{"from":"now-15m","to":"now","display":"Last 15 minutes"}]
|
||||
;quick_ranges =
|
||||
|
||||
[expressions]
|
||||
# Enable or disable the expressions functionality.
|
||||
;enabled = true
|
||||
|
||||
@@ -24,19 +24,27 @@
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 1263,
|
||||
"id": 15116,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -45,7 +53,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -74,10 +83,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -93,14 +104,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -109,7 +129,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -146,10 +167,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -165,14 +188,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -181,7 +213,8 @@
|
||||
"mode": "percentage",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -218,10 +251,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -237,14 +272,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -253,7 +297,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -275,7 +320,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -314,10 +360,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -333,14 +381,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -394,7 +451,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -416,13 +474,15 @@
|
||||
"showLegend": true
|
||||
},
|
||||
"mergeValues": true,
|
||||
"rowHeight": 0.9,
|
||||
"rowHeight": 0.47,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -438,14 +498,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -454,7 +523,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -537,10 +607,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -556,14 +628,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -572,7 +653,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -601,10 +683,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -620,14 +704,23 @@
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "testdata" },
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
@@ -636,7 +729,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -665,10 +759,12 @@
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -709,10 +805,192 @@
|
||||
}
|
||||
],
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"match": "null",
|
||||
"result": {
|
||||
"color": "purple",
|
||||
"index": 0,
|
||||
"text": "null"
|
||||
}
|
||||
},
|
||||
"type": "special"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"match": "nan",
|
||||
"result": {
|
||||
"color": "red",
|
||||
"index": 1,
|
||||
"text": "NaN"
|
||||
}
|
||||
},
|
||||
"type": "special"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"alignValue": "center",
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"mergeValues": true,
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000\n ],\n [\n 5,\n null,\n 20,\n null,\n 40\n ]\n ],\n \"entities\": [null, { \"NaN\": [3]}]\n }\n }\n]",
|
||||
"refId": "A",
|
||||
"scenarioId": "raw_frame"
|
||||
}
|
||||
],
|
||||
"title": "special null | NaN value mapping from data",
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"match": "null+nan",
|
||||
"result": {
|
||||
"color": "super-light-red",
|
||||
"index": 0,
|
||||
"text": "null + NaN"
|
||||
}
|
||||
},
|
||||
"type": "special"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 18
|
||||
},
|
||||
"id": 13,
|
||||
"options": {
|
||||
"alignValue": "center",
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"mergeValues": true,
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000\n ],\n [\n 5,\n null,\n 20,\n null,\n 40\n ]\n ],\n \"entities\": [null, { \"NaN\": [1]}]\n }\n }\n]",
|
||||
"refId": "A",
|
||||
"scenarioId": "raw_frame"
|
||||
}
|
||||
],
|
||||
"title": "special null + NaN value mapping from data",
|
||||
"type": "state-timeline"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 38,
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 41,
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
@@ -723,13 +1001,12 @@
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "2023-01-26T11:33:55.000Z",
|
||||
"to": "2023-01-26T14:33:55.000Z"
|
||||
"from": "2023-01-26T11:29:47.180Z",
|
||||
"to": "2023-01-26T16:29:39.205Z"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"timezone": "utc",
|
||||
"title": "StateTimeline - Thresholds & Mappings",
|
||||
"uid": "Kce7z9TVz",
|
||||
"version": 14,
|
||||
"weekStart": ""
|
||||
"version": 13
|
||||
}
|
||||
|
||||
@@ -186,6 +186,10 @@ The migration assistant can migrate the majority of Grafana Alerting resources t
|
||||
- Notification policy tree
|
||||
- Notification templates
|
||||
|
||||
{{< admonition type="note">}}
|
||||
The `grafana-default-email` contact point that's provisioned with every new Grafana instance doesn't have a UID by default and won't be migrated unless you edit or update and save it. You do not need to change the contact point for a UID to be generated when saved.
|
||||
{{< /admonition >}}
|
||||
|
||||
This is sufficient to have your Alerting configuration up and running in Grafana Cloud with minimal effort.
|
||||
|
||||
Migration of Silences is not supported by the migration assistant and needs to be configured manually. Alert History is also not available for migration.
|
||||
|
||||
+81
-54
@@ -13,41 +13,69 @@ labels:
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: MQTT
|
||||
title: Configure the MQTT notifier for Alerting
|
||||
title: Configure MQTT notifications
|
||||
weight: 140
|
||||
refs:
|
||||
notification-template-examples:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/template-notifications/examples/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/examples/
|
||||
notification-templates:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/template-notifications/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/
|
||||
configure-contact-points:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/manage-contact-points/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/
|
||||
---
|
||||
|
||||
# Configure the MQTT notifier for Alerting
|
||||
# Configure MQTT notifications
|
||||
|
||||
Use the Grafana Alerting - MQTT integration to send notifications to an MQTT broker when your alerts are firing.
|
||||
Use the MQTT integration in contact points to send alert notifications to your MQTT broker.
|
||||
|
||||
## Procedure
|
||||
## Configure MQTT for a contact point
|
||||
|
||||
To configure the MQTT integration for Alerting, complete the following steps.
|
||||
To create a contact point with MQTT integration, complete the following steps.
|
||||
|
||||
1. In the left-side menu, click **Alerts & IRM** and then **Alerting**.
|
||||
1. On the **Contact Points** tab, click **+ Add contact point**.
|
||||
1. Enter a descriptive name for the contact point.
|
||||
1. From the Integration list, select **MQTT**.
|
||||
1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**.
|
||||
1. Click **+ Add contact point**.
|
||||
1. Enter a name for the contact point.
|
||||
1. From the **Integration** list, select **MQTT**.
|
||||
1. Enter your broker URL in the **Broker URL** field. Supports `tcp`, `ssl`, `mqtt`, `mqtts`, `ws`, `wss` schemes. For example: `tcp://127.0.0.1:1883`.
|
||||
1. Enter the MQTT topic name in the **Topic** field.
|
||||
1. In **Optional MQTT settings**, specify additional settings for the MQTT integration if needed.
|
||||
1. Click **Test** to check that your integration works.
|
||||
1. (Optional) Configure [additional settings](#optional-settings).
|
||||
1. Click **Save contact point**.
|
||||
|
||||
** For Grafana Alertmanager only.**
|
||||
For more details on contact points, including how to test them and enable notifications, refer to [Configure contact points](ref:configure-contact-points).
|
||||
|
||||
A test alert notification should be sent to the MQTT broker.
|
||||
### Required Settings
|
||||
|
||||
1. Click **Save** contact point.
|
||||
| Option | Description |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| Broker URL | The URL of the MQTT broker. |
|
||||
| Topic | The topic to which the message will be sent. |
|
||||
|
||||
The integration sends data in JSON format by default. You can change that using **Message format** field in the **Optional MQTT settings** section. There are two supported formats:
|
||||
### Optional Settings
|
||||
|
||||
- **JSON**: Sends the alert notification in JSON format.
|
||||
- **Text**: Sends the rendered alert notification message in plain text format.
|
||||
| Option | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Message format | If set to `json` (default), the notification message uses the [default JSON payload](#default-json-payload). <br/> If set to `text`, the notification message is fully customizable. |
|
||||
| Message | Depends on the **Message format** option. <br/> In `json` format, defines only the `message` field of the [default JSON payload](#default-json-payload). <br/> In `text` format, defines the [entire custom payload](#custom-payload). <br/> This field supports [notification templates](ref:notification-templates). |
|
||||
| Client ID | The client ID to use when connecting to the MQTT broker. If blank, a random client ID is used. |
|
||||
| Username | The username to use when connecting to the MQTT broker. |
|
||||
| Password | The password to use when connecting to the MQTT broker. |
|
||||
| QoS | The quality of service to use when sending the message. Options are `At most once`, `At least once`, and `Exactly once`. |
|
||||
| Retain | If set to true, the message will be retained by the broker. |
|
||||
| TLS | TLS configuration options, including CA certificate, client certificate, and client key, and disable certificate verification. |
|
||||
| Disable resolved message | Enable this option to prevent notifications when an alert resolves. |
|
||||
|
||||
## MQTT JSON payload
|
||||
## Default JSON payload
|
||||
|
||||
If the JSON message format is selected in **Optional MQTT settings**, the payload is sent in the following structure.
|
||||
If the **Message format** option is `json` (the default), the payload is like this example.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -116,43 +144,42 @@ If the JSON message format is selected in **Optional MQTT settings**, the payloa
|
||||
}
|
||||
```
|
||||
|
||||
### Payload fields
|
||||
### Body
|
||||
|
||||
Each notification payload contains the following fields.
|
||||
If the **Message format** option is `json` (the default), the payload contains the following fields.
|
||||
|
||||
| Key | Type | Description |
|
||||
| ----------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| receiver | string | Name of the contact point |
|
||||
| status | string | Current status of the alert, `firing` or `resolved` |
|
||||
| orgId | number | ID of the organization related to the payload |
|
||||
| alerts | array of [alert instances](#alert-instance) | Alerts that are triggering |
|
||||
| groupLabels | object | Labels that are used for grouping, map of string keys to string values |
|
||||
| commonLabels | object | Labels that all alarms have in common, map of string keys to string values |
|
||||
| commonAnnotations | object | Annotations that all alarms have in common, map of string keys to string values |
|
||||
| externalURL | string | External URL to the Grafana instance sending this webhook |
|
||||
| version | string | Version of the payload |
|
||||
| groupKey | string | Key that is used for grouping |
|
||||
| message | string | Rendered message of the alerts |
|
||||
|
||||
### Alert instance
|
||||
|
||||
Each alert instance in the `alerts` array has the following fields.
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------ | ------ | ---------------------------------------------------------------------------------- |
|
||||
| status | string | Current status of the alert, `firing` or `resolved` |
|
||||
| labels | object | Labels that are part of this alert, map of string keys to string values |
|
||||
| annotations | object | Annotations that are part of this alert, map of string keys to string values |
|
||||
| startsAt | string | Start time of the alert |
|
||||
| endsAt | string | End time of the alert, default value when not resolved is `0001-01-01T00:00:00Z` |
|
||||
| values | object | Values that triggered the current status |
|
||||
| generatorURL | string | URL of the alert rule in the Grafana UI |
|
||||
| fingerprint | string | The labels fingerprint, alarms with the same labels will have the same fingerprint |
|
||||
| silenceURL | string | URL to silence the alert rule in the Grafana UI |
|
||||
| dashboardURL | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation |
|
||||
| panelURL | string | A link to the panel if the alert has a Panel ID annotation |
|
||||
| imageURL | string | URL of a screenshot of a panel assigned to the rule that created this notification |
|
||||
| Key | Type | Description |
|
||||
| ------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `receiver` | string | Name of the contact point |
|
||||
| `status` | string | Current status of the alert, `firing` or `resolved` |
|
||||
| `orgId` | number | ID of the organization related to the payload |
|
||||
| `alerts` | array of [alerts](#alert-object) | Alerts that are triggering |
|
||||
| `groupLabels` | object | Labels that are used for grouping, map of string keys to string values |
|
||||
| `commonLabels` | object | Labels that all alarms have in common, map of string keys to string values |
|
||||
| `commonAnnotations` | object | Annotations that all alarms have in common, map of string keys to string values |
|
||||
| `externalURL` | string | External URL to the Grafana instance sending this webhook |
|
||||
| `version` | string | Version of the payload |
|
||||
| `groupKey` | string | Key that is used for grouping |
|
||||
| `message` | string | Custom message configured in **Message** (**Optional Settings**). <br/> Supports [notification templates](ref:notification-templates); the output is formatted as a string. |
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Alert rules are not coupled to dashboards anymore. The fields related to dashboards `dashboardId` and `panelId` have been removed.
|
||||
|
||||
When using the `json` **Message format**, only the **message** field of the JSON payload is customizable, and its output is formatted as a string.
|
||||
|
||||
To customize the full payload in text or JSON format, use the `text` format and define a [custom payload](#custom-payload).
|
||||
|
||||
{{< /admonition >}}
|
||||
|
||||
### Alert object
|
||||
|
||||
The Alert object represents an alert included in the notification group, as provided by the [`alerts` field](#body).
|
||||
|
||||
{{< docs/shared lookup="alerts/table-for-json-alert-object.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Custom payload
|
||||
|
||||
When you set the **Message format** option to `text`, you can customize the entire payload of the MQTT message.
|
||||
|
||||
In this mode, the **Message** option defines the entire payload. It supports [notification templates](ref:notification-templates) and can generate notification messages in plain text, JSON, or any custom format.
|
||||
|
||||
For examples of templates that produce plain text or JSON messages, refer to [notification template examples](ref:notification-template-examples).
|
||||
|
||||
+1
-14
@@ -237,20 +237,7 @@ The following key-value pairs are also included in the JSON payload and can be c
|
||||
|
||||
The Alert object represents an alert included in the notification group, as provided by the [`alerts` field](#body).
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------- | ------ | ----------------------------------------------------------------------------------- |
|
||||
| `status` | string | Current status of the alert, `firing` or `resolved`. |
|
||||
| `labels` | object | Labels that are part of this alert, map of string keys to string values. |
|
||||
| `annotations` | object | Annotations that are part of this alert, map of string keys to string values. |
|
||||
| `startsAt` | string | Start time of the alert. |
|
||||
| `endsAt` | string | End time of the alert, default value when not resolved is `0001-01-01T00:00:00Z`. |
|
||||
| `values` | object | Values that triggered the current status. |
|
||||
| `generatorURL` | string | URL of the alert rule in the Grafana UI. |
|
||||
| `fingerprint` | string | The labels fingerprint, alarms with the same labels will have the same fingerprint. |
|
||||
| `silenceURL` | string | URL to silence the alert rule in the Grafana UI. |
|
||||
| `dashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. |
|
||||
| `panelURL` | string | A link to the panel if the alert has a Panel ID annotation. |
|
||||
| `imageURL` | string | URL of a screenshot of a panel assigned to the rule that created this notification. |
|
||||
{{< docs/shared lookup="alerts/table-for-json-alert-object.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Custom Payload
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ Aggregates time series values within the selected time range into a single numbe
|
||||
|
||||
Reduce takes one or more time series and transform each series into a single number, which can then be compared in the alert condition.
|
||||
|
||||
The following aggregations functions are included: `Min`, `Max`, `Mean`, `Mediam`, `Sum`, `Count`, and `Last`. For more details, refer to the [Reduce documentation](ref:reduce-operation).
|
||||
The following aggregations functions are included: `Min`, `Max`, `Mean`, `Median`, `Sum`, `Count`, and `Last`. For more details, refer to the [Reduce documentation](ref:reduce-operation).
|
||||
|
||||
### Math
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ weight: 2
|
||||
|
||||
# Breaking changes in Grafana
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
As of Grafana v12.0 we no longer publish a dedicated breaking changes page and we, instead, publish breaking changes information in our [What's new](../whatsnew/) page.
|
||||
{{< /admonition >}}
|
||||
|
||||
In some cases, major releases that introduce many new features also introduce breaking changes. These changes are described, along with information about what to do, in the breaking changes pages specific to each release.
|
||||
|
||||
For our purposes, a breaking change is any change that requires users or operators to do something. This includes:
|
||||
|
||||
@@ -54,7 +54,7 @@ Query Parameters:
|
||||
- `to`: epoch datetime in milliseconds. Optional.
|
||||
- `limit`: number. Optional - default is 100. Max limit for results returned.
|
||||
- `alertId`: number. Optional. Find annotations for a specified alert.
|
||||
- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard
|
||||
- `dashboardId`: Deprecated. Use dashboardUID instead.
|
||||
- `dashboardUID`: string. Optional. Find annotations that are scoped to a specific dashboard, when dashboardUID presents, dashboardId would be ignored.
|
||||
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel
|
||||
- `userId`: number. Optional. Find annotations created by a specific user
|
||||
@@ -113,7 +113,7 @@ Content-Type: application/json
|
||||
|
||||
## Create Annotation
|
||||
|
||||
Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional.
|
||||
Creates an annotation in the Grafana database. The `dashboardUid` and `panelId` fields are optional.
|
||||
If they are not specified then an organization annotation is created and can be queried in any dashboard that adds
|
||||
the Grafana annotations data source. When creating a region annotation include the timeEnd property.
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ title: 'Preferences API'
|
||||
Keys:
|
||||
|
||||
- **theme** - One of: `light`, `dark`, or an empty string for the default theme
|
||||
- **homeDashboardId** - The numerical `:id` of a favorited dashboard, default: `0`
|
||||
- **homeDashboardId** - Deprecated. Use `homeDashboardUID` instead.
|
||||
- **homeDashboardUID**: The `:uid` of a dashboard
|
||||
- **timezone** - One of: `utc`, `browser`, or an empty string for the default
|
||||
|
||||
Omitting a key will cause the current value to be replaced with the
|
||||
@@ -139,6 +140,7 @@ Content-Type: application/json
|
||||
{
|
||||
"theme": "",
|
||||
"homeDashboardId": 0,
|
||||
"homeDashboardUID": "",
|
||||
"timezone": "",
|
||||
"weekStart": "",
|
||||
"navbar": {
|
||||
|
||||
@@ -477,6 +477,7 @@ Content-Type: application/json
|
||||
{
|
||||
"theme": "",
|
||||
"homeDashboardId": 0,
|
||||
"homeDashboardUID": "",
|
||||
"timezone": ""
|
||||
}
|
||||
```
|
||||
@@ -504,6 +505,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
{
|
||||
"theme": "dark",
|
||||
"homeDashboardId": 39,
|
||||
"homeDashboardUID": "jcIIG-07z",
|
||||
"timezone": "utc"
|
||||
}
|
||||
```
|
||||
@@ -511,7 +513,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
JSON Body Schema:
|
||||
|
||||
- **theme** - One of: `light`, `dark`, or an empty string for the default theme
|
||||
- **homeDashboardId** - The numerical `:id` of a dashboard, default: `0`
|
||||
- **homeDashboardId** - Deprecated. Use `homeDashboardUID` instead.
|
||||
- **homeDashboardUID** - The `:uid` of a dashboard
|
||||
- **timezone** - One of: `utc`, `browser`, or an empty string for the default
|
||||
|
||||
Omitting a key will cause the current value to be replaced with the system default value.
|
||||
|
||||
@@ -54,6 +54,10 @@ By default, the configuration file is located at `/opt/homebrew/etc/grafana/graf
|
||||
For a Grafana instance installed using Homebrew, edit the `grafana.ini` file directly.
|
||||
Otherwise, add a configuration file named `custom.ini` to the `conf` directory to override the settings defined in `conf/defaults.ini`.
|
||||
|
||||
### Grafana Cloud
|
||||
|
||||
There is no local configuration file for Grafana Cloud stacks, but many of these settings are still configurable. To edit configurable settings, open a support ticket.
|
||||
|
||||
## Remove comments in the .ini files
|
||||
|
||||
Grafana uses semicolons (`;`) to comment out lines in the INI file.
|
||||
@@ -2101,7 +2105,7 @@ Setting `0` means the short links are cleaned up approximately every 10 minutes.
|
||||
A negative value such as `-1` disables expiry.
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
Short links without an expiration increase the size of the database and can't be deleted.
|
||||
Short links without an expiration increase the size of the database and can't be deleted. Grafana recommends setting a duration based on your specific use case
|
||||
{{< /admonition >}}
|
||||
|
||||
<hr>
|
||||
@@ -2810,6 +2814,40 @@ Used as the default time zone for user preferences. Can be either `browser` for
|
||||
|
||||
Set the default start of the week, valid values are: `saturday`, `sunday`, `monday` or `browser` to use the browser locale to define the first day of the week. Default is `browser`.
|
||||
|
||||
### `[time_picker]`
|
||||
|
||||
This section controls system-wide defaults for the time picker, such as the default quick ranges.
|
||||
|
||||
#### `quick_ranges`
|
||||
|
||||
Set the default set of quick relative offset time ranges that show up in the right column of the time picker. Each configuration entry must have a `from`, `to`, and `display` field. Any configuration for this field must be in valid JSON format made up of a list of quick range configurations.
|
||||
|
||||
The `from` and `to` fields should be valid relative time ranges. For more information the relative time formats, refer to [Time units and relative ranges.](/docs/grafana/<GRAFANA_VERSION>/dashboards/use-dashboards/#time-units-and-relative-ranges). The `from` field is required, but omitting `to` will result in the `from` value being used in both fields.
|
||||
|
||||
If no configuration is provided, the default time ranges will be used.
|
||||
|
||||
For example:
|
||||
|
||||
```ini
|
||||
[time_picker]
|
||||
quick_ranges = [
|
||||
{
|
||||
"display": "Last 5 minutes",
|
||||
"from": "now-5m",
|
||||
"to": "now",
|
||||
},
|
||||
{
|
||||
"display": "Yesterday",
|
||||
"from": "now-1d/d",
|
||||
},
|
||||
{
|
||||
"display": "Today so far",
|
||||
"from": "now/d",
|
||||
"to": "now",
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `[expressions]`
|
||||
|
||||
#### `enabled`
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: 'JSON alert object'
|
||||
---
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------- | ------ | ----------------------------------------------------------------------------------- |
|
||||
| `status` | string | Current status of the alert, `firing` or `resolved`. |
|
||||
| `labels` | object | Labels that are part of this alert, map of string keys to string values. |
|
||||
| `annotations` | object | Annotations that are part of this alert, map of string keys to string values. |
|
||||
| `startsAt` | string | Start time of the alert. |
|
||||
| `endsAt` | string | End time of the alert, default value when not resolved is `0001-01-01T00:00:00Z`. |
|
||||
| `values` | object | Values that triggered the current status. |
|
||||
| `generatorURL` | string | URL of the alert rule in the Grafana UI. |
|
||||
| `fingerprint` | string | The labels fingerprint, alarms with the same labels will have the same fingerprint. |
|
||||
| `silenceURL` | string | URL to silence the alert rule in the Grafana UI. |
|
||||
| `dashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. |
|
||||
| `panelURL` | string | A link to the panel if the alert has a Panel ID annotation. |
|
||||
| `imageURL` | string | URL of a screenshot of a panel assigned to the rule that created this notification. |
|
||||
@@ -77,7 +77,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20250605155607-02235095d018 // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20250610043455-3e20fda3b872 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
|
||||
@@ -1569,8 +1569,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20250605155607-02235095d018 h1:sGEBflMw3DUsfV4SCl1at+QRwGi5h29iiAuk8ZZn5Mw=
|
||||
github.com/grafana/alerting v0.0.0-20250605155607-02235095d018/go.mod h1:e2nDocz4yTgLYGJFMrlL3DTjdMTEYPABorKc191b8/w=
|
||||
github.com/grafana/alerting v0.0.0-20250610043455-3e20fda3b872 h1:bhUBPxMHtQB+AJaPcASElgwC7WFjWQvEiD3FoMEAv0E=
|
||||
github.com/grafana/alerting v0.0.0-20250610043455-3e20fda3b872/go.mod h1:VANfU4LMnOR4Gv4hkCN9fGyQ/8Lf2DKOvzG70vI2IS4=
|
||||
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb h1:oTl2j6/4miQUYmXANp2pBuYCWA5f8NVYFfCWpczpFso=
|
||||
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
|
||||
@@ -22,6 +22,8 @@ const esModules = [
|
||||
'vscode-languageserver-types',
|
||||
'@bsull/augurs',
|
||||
'react-data-grid',
|
||||
'@grafana/llm',
|
||||
'pkce-challenge',
|
||||
].join('|');
|
||||
|
||||
module.exports = {
|
||||
|
||||
+8
-8
@@ -280,14 +280,14 @@
|
||||
"@grafana/google-sdk": "0.1.2",
|
||||
"@grafana/i18n": "workspace:*",
|
||||
"@grafana/lezer-logql": "0.2.7",
|
||||
"@grafana/llm": "0.19.2",
|
||||
"@grafana/llm": "0.22.0",
|
||||
"@grafana/monaco-logql": "^0.0.8",
|
||||
"@grafana/o11y-ds-frontend": "workspace:*",
|
||||
"@grafana/plugin-ui": "0.10.6",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "^6.19.0",
|
||||
"@grafana/scenes-react": "^6.19.0",
|
||||
"@grafana/scenes": "^6.20.1",
|
||||
"@grafana/scenes-react": "^6.20.1",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
@@ -306,10 +306,10 @@
|
||||
"@opentelemetry/exporter-collector": "0.25.0",
|
||||
"@opentelemetry/semantic-conventions": "1.34.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@react-aria/dialog": "3.5.25",
|
||||
"@react-aria/focus": "3.20.3",
|
||||
"@react-aria/overlays": "3.27.1",
|
||||
"@react-aria/utils": "3.29.0",
|
||||
"@react-aria/dialog": "3.5.27",
|
||||
"@react-aria/focus": "3.20.5",
|
||||
"@react-aria/overlays": "3.27.3",
|
||||
"@react-aria/utils": "3.29.1",
|
||||
"@react-awesome-query-builder/ui": "6.6.15",
|
||||
"@reduxjs/toolkit": "2.5.1",
|
||||
"@visx/event": "3.12.0",
|
||||
@@ -368,7 +368,7 @@
|
||||
"nanoid": "^5.0.9",
|
||||
"node-forge": "^1.3.1",
|
||||
"ol": "7.4.0",
|
||||
"ol-ext": "4.0.31",
|
||||
"ol-ext": "4.0.32",
|
||||
"pluralize": "^8.0.0",
|
||||
"prismjs": "1.30.0",
|
||||
"rc-slider": "11.1.8",
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"./unstable": {
|
||||
"import": "./src/unstable.ts",
|
||||
"require": "./src/unstable.ts"
|
||||
},
|
||||
"./testing": {
|
||||
"import": "./src/testing.ts",
|
||||
"require": "./src/testing.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// export MSW handlers for testing
|
||||
export * from './grafana/api/v0alpha1/mocks/handlers';
|
||||
|
||||
// export mocks and factories
|
||||
export * from './grafana/api/v0alpha1/mocks/fakes/common';
|
||||
export * from './grafana/api/v0alpha1/mocks/fakes/Receivers';
|
||||
@@ -142,20 +142,20 @@ describe('align frames', () => {
|
||||
{
|
||||
name: 'gender',
|
||||
type: FieldType.string,
|
||||
values: ['NON-BINARY', 'MALE', 'MALE', 'FEMALE', 'FEMALE', 'NON-BINARY'],
|
||||
values: ['NON-BINARY', 'MALE', 'MALE', 'FEMALE', 'FEMALE', 'NON-BINARY', 'COW'],
|
||||
},
|
||||
{
|
||||
name: 'day',
|
||||
type: FieldType.string,
|
||||
values: ['Wednesday', 'Tuesday', 'Monday', 'Wednesday', 'Tuesday', 'Monday'],
|
||||
values: ['Wednesday', 'Tuesday', 'Monday', 'Wednesday', 'Tuesday', 'Monday', 'Monday'],
|
||||
},
|
||||
{ name: 'count', type: FieldType.number, values: [18, 72, 13, 17, 71, 7] },
|
||||
{ name: 'count', type: FieldType.number, values: [18, 72, 13, 17, 71, 7, 1] },
|
||||
],
|
||||
});
|
||||
const tableData2 = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'gender', type: FieldType.string, values: ['MALE', 'NON-BINARY', 'FEMALE'] },
|
||||
{ name: 'count', type: FieldType.number, values: [103, 95, 201] },
|
||||
{ name: 'gender', type: FieldType.string, values: ['MALE', 'NON-BINARY', 'FEMALE', 'DOG'] },
|
||||
{ name: 'count', type: FieldType.number, values: [103, 95, 201, 6] },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -181,6 +181,8 @@ describe('align frames', () => {
|
||||
"FEMALE",
|
||||
"FEMALE",
|
||||
"NON-BINARY",
|
||||
"COW",
|
||||
"DOG",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -192,6 +194,8 @@ describe('align frames', () => {
|
||||
"Wednesday",
|
||||
"Tuesday",
|
||||
"Monday",
|
||||
"Monday",
|
||||
null,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -203,6 +207,8 @@ describe('align frames', () => {
|
||||
17,
|
||||
71,
|
||||
7,
|
||||
1,
|
||||
null,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -214,6 +220,8 @@ describe('align frames', () => {
|
||||
201,
|
||||
201,
|
||||
95,
|
||||
null,
|
||||
6,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -167,10 +167,6 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined {
|
||||
const nullModes: JoinNullMode[][] = [];
|
||||
const allData: AlignedData[] = [];
|
||||
const originalFields: Field[] = [];
|
||||
// store frame field order for tabular data join
|
||||
const originalFieldsOrderByFrame: number[][] = [];
|
||||
// all other fields that are not the join on are in the 1+ position (join is always the 0)
|
||||
let fieldsOrder = 1;
|
||||
const joinFieldMatcher = getJoinMatcher(options);
|
||||
|
||||
for (let frameIndex = 0; frameIndex < options.frames.length; frameIndex++) {
|
||||
@@ -183,7 +179,6 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined {
|
||||
const nullModesFrame: JoinNullMode[] = [NULL_REMOVE];
|
||||
let join: Field | undefined = undefined;
|
||||
let fields: Field[] = [];
|
||||
let frameFieldsOrder = [];
|
||||
|
||||
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
|
||||
const field = frame.fields[fieldIndex];
|
||||
@@ -243,21 +238,16 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined {
|
||||
// clear field displayName state
|
||||
delete field.state?.displayName;
|
||||
}
|
||||
// store frame field order for tabular data join
|
||||
frameFieldsOrder.push(fieldsOrder);
|
||||
fieldsOrder++;
|
||||
}
|
||||
// store frame field order for tabular data join
|
||||
originalFieldsOrderByFrame.push(frameFieldsOrder);
|
||||
allData.push(a);
|
||||
}
|
||||
|
||||
let joined: Array<Array<number | string | null | undefined>> = [];
|
||||
|
||||
if (options.mode === JoinMode.outerTabular) {
|
||||
joined = joinOuterTabular(allData, originalFieldsOrderByFrame, originalFields.length, nullModes);
|
||||
joined = joinTabular(allData, true);
|
||||
} else if (options.mode === JoinMode.inner) {
|
||||
joined = joinInner(allData);
|
||||
joined = joinTabular(allData);
|
||||
} else {
|
||||
joined = join(allData, nullModes, options.mode);
|
||||
}
|
||||
@@ -272,165 +262,201 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
// The following full outer join allows for multiple/duplicated joined fields values where as the performant join from uplot creates a unique set of field values to be joined on
|
||||
// http://www.silota.com/docs/recipes/sql-join-tutorial-javascript-examples.html
|
||||
// The frame field value which is used join on is sorted to the 0 position of each table data in both tables and nullModes
|
||||
// (not sure if we need nullModes) for nullModes, the field to join on is given NULL_REMOVE and all other fields are given NULL_EXPAND
|
||||
function joinOuterTabular(
|
||||
tables: AlignedData[],
|
||||
originalFieldsOrderByFrame: number[][],
|
||||
numberOfFields: number,
|
||||
nullModes?: number[][]
|
||||
) {
|
||||
// we will iterate through all frames and check frames for matches preventing duplicates.
|
||||
// we will store each matched frame "row" or field values at the same index in the following hash.
|
||||
let duplicateHash: { [key: string]: Array<number | string | null | undefined> } = {};
|
||||
|
||||
// iterate through the tables (frames)
|
||||
// for each frame we get the field data where the data in the 0 pos is the value to join on
|
||||
for (let tableIdx = 0; tableIdx < tables.length; tableIdx++) {
|
||||
// the table (frame) to check for matches in other tables
|
||||
let table = tables[tableIdx];
|
||||
// the field value to join on (the join value is always in the 0 position)
|
||||
let joinOnTableField = table[0];
|
||||
|
||||
// now we iterate through the other table (frame) data to look for matches
|
||||
for (let otherTablesIdx = 0; otherTablesIdx < tables.length; otherTablesIdx++) {
|
||||
// do not match on the same table
|
||||
if (otherTablesIdx === tableIdx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let otherTable = tables[otherTablesIdx];
|
||||
let otherTableJoinOnField = otherTable[0];
|
||||
|
||||
// iterate through the field to join on from the first table
|
||||
for (
|
||||
let joinTableFieldValuesIdx = 0;
|
||||
joinTableFieldValuesIdx < joinOnTableField.length;
|
||||
joinTableFieldValuesIdx++
|
||||
) {
|
||||
// create the joined data
|
||||
// this has the orignalFields length and should start out undefined
|
||||
// joined row + number of other fields in each frame
|
||||
// the order of each field is important in how we
|
||||
// 1 check for duplicates
|
||||
// 2 transform the row back into fields for the joined frame
|
||||
// 3 when there is no match for the row we keep the vals undefined
|
||||
const tableJoinOnValue = joinOnTableField[joinTableFieldValuesIdx];
|
||||
const allOtherFields = numberOfFields - 1;
|
||||
let joinedRow: Array<number | string | null | undefined> = [tableJoinOnValue].concat(new Array(allOtherFields));
|
||||
|
||||
let tableFieldValIdx = 0;
|
||||
for (let fieldsIdx = 1; fieldsIdx < table.length; fieldsIdx++) {
|
||||
const joinRowIdx = originalFieldsOrderByFrame[tableIdx][tableFieldValIdx];
|
||||
joinedRow[joinRowIdx] = table[fieldsIdx][joinTableFieldValuesIdx];
|
||||
tableFieldValIdx++;
|
||||
}
|
||||
|
||||
for (let otherTableValuesIdx = 0; otherTableValuesIdx < otherTableJoinOnField.length; otherTableValuesIdx++) {
|
||||
if (joinOnTableField[joinTableFieldValuesIdx] === otherTableJoinOnField[otherTableValuesIdx]) {
|
||||
let tableFieldValIdx = 0;
|
||||
for (let fieldsIdx = 1; fieldsIdx < otherTable.length; fieldsIdx++) {
|
||||
const joinRowIdx = originalFieldsOrderByFrame[otherTablesIdx][tableFieldValIdx];
|
||||
joinedRow[joinRowIdx] = otherTable[fieldsIdx][otherTableValuesIdx];
|
||||
tableFieldValIdx++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent duplicates by entering rows in a hash where keys are the rows
|
||||
duplicateHash[JSON.stringify(joinedRow)] = joinedRow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transform the joined rows into data for a dataframe
|
||||
let data: Array<Array<number | string | null | undefined>> = [];
|
||||
for (let field = 0; field < numberOfFields; field++) {
|
||||
data.push(new Array(0));
|
||||
}
|
||||
|
||||
for (let key in duplicateHash) {
|
||||
const row = duplicateHash[key];
|
||||
|
||||
for (let valIdx = 0; valIdx < row.length; valIdx++) {
|
||||
data[valIdx].push(row[valIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function performs a sql-style inner join on tabular data;
|
||||
* it will combine records from two tables whenever there are matching
|
||||
* values in a field common to both tables.
|
||||
*
|
||||
* NOTE: This function implicitly assumes that the first array in each AlignedData
|
||||
* contains the values to join on. It doesn't explicitly specify a column field to join on,
|
||||
* but rather uses the 0th position of the arrays (AlignedData[0]) to determine the joining keys.
|
||||
* Then, when processing the tables, the function iterates over the values in the `xValues`
|
||||
* (the joining keys) array and checks if the current row `currentRow` already includes the value.
|
||||
* If a matching value is found, it joins the corresponding values from the remaining arrays `yValues`
|
||||
* (all other non-joining key arrays) to create a new row in the joined table.
|
||||
*
|
||||
* @param {AlignedData[]} tables - The tables to join.
|
||||
*
|
||||
* @returns {Array<Array<string | number | null | undefined>>} The joined tables as an array of arrays, where each array represents a row in the joined table.
|
||||
* SQL-style join of tables, using the first column in each
|
||||
*/
|
||||
function joinInner(tables: AlignedData[]): Array<Array<string | number | null | undefined>> {
|
||||
const joinedTables: Array<Array<string | number | null | undefined>> = [];
|
||||
function joinTabular(tables: AlignedData[], outer = false) {
|
||||
// console.time('joinTabular');
|
||||
|
||||
// Recursive function to perform the inner join.
|
||||
const joinTables = (
|
||||
currentTables: AlignedData[],
|
||||
currentIndex: number,
|
||||
currentRow: Array<string | number | null | undefined>
|
||||
) => {
|
||||
if (currentIndex === currentTables.length) {
|
||||
// Base case: all tables have been joined, add the current row to the final result.
|
||||
joinedTables.push(currentRow);
|
||||
return;
|
||||
let ltable = tables[0];
|
||||
let lfield = ltable[0];
|
||||
|
||||
// iterate tables, merging right table with left, with the result becoming the new left
|
||||
// rinse and repeat for each tables in the array
|
||||
for (let ti = 1; ti < tables.length; ti++) {
|
||||
let rtable = tables[ti];
|
||||
let rfield = rtable[0];
|
||||
|
||||
/**
|
||||
* Build an inverted index of the right table's join column like { "foo": [1,2,3], "bar": [7,12], ... }
|
||||
* where the keys are unique values and the arrays are indices where these values were found
|
||||
*/
|
||||
// console.time('index right');
|
||||
let index: Record<string | number, number[]> = {};
|
||||
|
||||
for (let i = 0; i < rfield.length; i++) {
|
||||
let val = rfield[i];
|
||||
|
||||
let idxs = index[val];
|
||||
|
||||
if (idxs == null) {
|
||||
idxs = index[val] = [];
|
||||
}
|
||||
|
||||
idxs.push(i);
|
||||
}
|
||||
// console.timeEnd('index right');
|
||||
|
||||
const currentTable = currentTables[currentIndex];
|
||||
const [xValues, ...yValues] = currentTable;
|
||||
/**
|
||||
* Loop over the left table's join column and match each non-null value to the right index,
|
||||
* copying the matched ridxs array into new matched list, like [33, [45,79,233]], where first
|
||||
* value is left idx and second value is right idxs
|
||||
*
|
||||
* Also keep track of unmatched or null left values for outer join, since we'll need to include these
|
||||
*/
|
||||
let matchedKeys = new Set();
|
||||
let unmatchedLeft = [];
|
||||
let unmatchedRight = [];
|
||||
|
||||
for (let i = 0; i < xValues.length; i++) {
|
||||
const value = xValues[i];
|
||||
// console.time('match left');
|
||||
let matched: Array<[lidx: number, ridxs: number[]]> = [];
|
||||
|
||||
if (currentIndex === 0 || currentRow.includes(value)) {
|
||||
const newRow = [...currentRow];
|
||||
// count of total number of output rows, so we can
|
||||
// pre-allocate the final array size during materialization
|
||||
let count = 0;
|
||||
|
||||
if (currentIndex === 0) {
|
||||
newRow.push(value);
|
||||
for (let i = 0; i < lfield.length; i++) {
|
||||
let v = lfield[i];
|
||||
|
||||
if (v != null) {
|
||||
let idxs = index[v];
|
||||
|
||||
if (idxs != null) {
|
||||
matched.push([i, idxs]);
|
||||
count += idxs.length;
|
||||
outer && matchedKeys.add(v);
|
||||
} else if (outer) {
|
||||
unmatchedLeft.push(i);
|
||||
}
|
||||
|
||||
for (let j = 0; j < yValues.length; j++) {
|
||||
newRow.push(yValues[j][i]);
|
||||
}
|
||||
|
||||
// Recursive call for the next table
|
||||
joinTables(currentTables, currentIndex + 1, newRow);
|
||||
} else if (outer) {
|
||||
unmatchedLeft.push(i);
|
||||
}
|
||||
}
|
||||
};
|
||||
count += unmatchedLeft.length;
|
||||
// console.timeEnd('match left');
|
||||
|
||||
// Start the recursive join process.
|
||||
joinTables(tables, 0, []);
|
||||
/**
|
||||
* For outer joins, also loop over the right index to record unmatched values
|
||||
*/
|
||||
// console.time('unmatched right');
|
||||
if (outer) {
|
||||
for (let k in index) {
|
||||
if (!matchedKeys.has(k)) {
|
||||
unmatchedRight.push(...index[k]);
|
||||
}
|
||||
}
|
||||
count += unmatchedRight.length;
|
||||
}
|
||||
// console.timeEnd('unmatched right');
|
||||
|
||||
// Check if joinedTables is empty before transposing. No need to transpose if there are no joined tables.
|
||||
if (joinedTables.length === 0) {
|
||||
const fieldCount = tables.reduce((count, table) => count + (table.length - 1), 1);
|
||||
return Array.from({ length: fieldCount }, () => []);
|
||||
/**
|
||||
* Now we can use matched, unmatchedLeft, unmatchedRight, ltable, and rtable to assemble the final table
|
||||
* Instead of using 3-deep nested loops, we eliminate the loops over the known column structure
|
||||
* For this we compile a new function using the schemas from both tables, and filling that struct by looping
|
||||
* over the matched lookup array, then appending the unmatched left rows (and null-filling the right values),
|
||||
* then appending the unmatched right rows (and null-filling the left values).
|
||||
*
|
||||
* The assembled function looks something like this when joining 2-col left + 2-col right:
|
||||
*
|
||||
* function anonymous(matched, unmatchedLeft, unmatchedRight, ltable, rtable) {
|
||||
* const joined = [Array(99991),Array(99991),Array(99991)];
|
||||
*
|
||||
* let rowIdx = 0;
|
||||
*
|
||||
* for (let i = 0; i < matched.length; i++) {
|
||||
* let [lidx, ridxs] = matched[i];
|
||||
*
|
||||
* for (let j = 0; j < ridxs.length; j++, rowIdx++) {
|
||||
* let ridx = ridxs[j];
|
||||
* joined[0][rowIdx] = ltable[0][lidx];
|
||||
* joined[1][rowIdx] = ltable[1][lidx];
|
||||
* joined[2][rowIdx] = rtable[1][ridx];
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* for (let i = 0; i < unmatchedLeft.length; i++, rowIdx++) {
|
||||
* let lidx = unmatchedLeft[i];
|
||||
* joined[0][rowIdx] = ltable[0][lidx];
|
||||
* joined[1][rowIdx] = ltable[1][lidx];
|
||||
* joined[2][rowIdx] = null;
|
||||
* }
|
||||
*
|
||||
* for (let i = 0; i < unmatchedRight.length; i++, rowIdx++) {
|
||||
* let ridx = unmatchedRight[i];
|
||||
* joined[0][rowIdx] = rtable[0][ridx];
|
||||
* joined[1][rowIdx] = null;
|
||||
* joined[2][rowIdx] = rtable[1][ridx];
|
||||
* }
|
||||
*
|
||||
* return joined;
|
||||
* }
|
||||
*/
|
||||
// console.time('materialize');
|
||||
let outFieldsTpl = Array.from({ length: ltable.length + rtable.length - 1 }, () => `Array(${count})`).join(',');
|
||||
let copyLeftRowTpl = ltable.map((c, i) => `joined[${i}][rowIdx] = ltable[${i}][lidx]`).join(';');
|
||||
// (skips join field in right table)
|
||||
let copyRightRowTpl = rtable
|
||||
.slice(1)
|
||||
.map((c, i) => `joined[${ltable.length + i}][rowIdx] = rtable[${i + 1}][ridx]`)
|
||||
.join(';');
|
||||
|
||||
// for outer joins, when we null-fill the left row values, we still populate the first (join) column
|
||||
// with the right row's join column value, rather than omitting it as we do for matched left/right where
|
||||
// that value is already filled by the left row
|
||||
let nullLeftRowTpl = ltable
|
||||
.map((c, i) => `joined[${i}][rowIdx] = ${i === 0 ? `rtable[${i}][ridx]` : `null`}`)
|
||||
.join(';');
|
||||
// (skips join field in right table)
|
||||
let nullRightRowTpl = rtable.slice(1).map((c, i) => `joined[${ltable.length + i}][rowIdx] = null`);
|
||||
|
||||
let materialize = new Function(
|
||||
'matched',
|
||||
'unmatchedLeft',
|
||||
'unmatchedRight',
|
||||
'ltable',
|
||||
'rtable',
|
||||
`
|
||||
const joined = [${outFieldsTpl}];
|
||||
|
||||
let rowIdx = 0;
|
||||
|
||||
for (let i = 0; i < matched.length; i++) {
|
||||
let [lidx, ridxs] = matched[i];
|
||||
|
||||
for (let j = 0; j < ridxs.length; j++, rowIdx++) {
|
||||
let ridx = ridxs[j];
|
||||
${copyLeftRowTpl};
|
||||
${copyRightRowTpl};
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < unmatchedLeft.length; i++, rowIdx++) {
|
||||
let lidx = unmatchedLeft[i];
|
||||
${copyLeftRowTpl};
|
||||
${nullRightRowTpl};
|
||||
}
|
||||
|
||||
for (let i = 0; i < unmatchedRight.length; i++, rowIdx++) {
|
||||
let ridx = unmatchedRight[i];
|
||||
${nullLeftRowTpl};
|
||||
${copyRightRowTpl};
|
||||
}
|
||||
|
||||
return joined;
|
||||
`
|
||||
);
|
||||
|
||||
let joined = materialize(matched, unmatchedLeft, unmatchedRight, ltable, rtable);
|
||||
// console.timeEnd('materialize');
|
||||
|
||||
ltable = joined;
|
||||
lfield = ltable[0];
|
||||
}
|
||||
|
||||
// Transpose the joined tables to get the desired output format.
|
||||
// This essentially flips the rows and columns back to the stucture of the original `tables`.
|
||||
return joinedTables[0].map((_, colIndex) => joinedTables.map((row) => row[colIndex]));
|
||||
// console.timeEnd('joinTabular');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return ltable as Array<Array<string | number | null | undefined>>;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NavLinkDTO } from './navModel';
|
||||
import { OrgRole } from './orgs';
|
||||
import { PanelPluginMeta } from './panel';
|
||||
import { GrafanaTheme } from './theme';
|
||||
import { TimeOption } from './time';
|
||||
|
||||
/**
|
||||
* Describes the build information that will be available via the Grafana configuration.
|
||||
@@ -240,6 +241,7 @@ export interface GrafanaConfig {
|
||||
reportingStaticContext?: Record<string, string>;
|
||||
exploreDefaultTimeOffset?: string;
|
||||
exploreHideLogsDownload?: boolean;
|
||||
quickRanges?: TimeOption[];
|
||||
|
||||
// The namespace to use for kubernetes apiserver requests
|
||||
namespace: string;
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface FieldConfig<TOptions = any> {
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* An explict path to the field in the datasource. When the frame meta includes a path,
|
||||
* An explicit path to the field in the datasource. When the frame meta includes a path,
|
||||
* This will default to `${frame.meta.path}/${field.name}
|
||||
*
|
||||
* When defined, this value can be used as an identifier within the datasource scope, and
|
||||
@@ -158,7 +158,7 @@ export interface Field<T = any> {
|
||||
|
||||
/**
|
||||
* When type === FieldType.Time, this can optionally store
|
||||
* the nanosecond-precison fractions as integers between
|
||||
* the nanosecond-precision fractions as integers between
|
||||
* 0 and 999999.
|
||||
*/
|
||||
nanos?: number[];
|
||||
|
||||
@@ -954,10 +954,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingRuleRecoverDeleted?: boolean;
|
||||
/**
|
||||
* Support Application Signals queries in the X-Ray datasource
|
||||
*/
|
||||
xrayApplicationSignals?: boolean;
|
||||
/**
|
||||
* use multi-tenant path for awsTempCredentials
|
||||
*/
|
||||
multiTenantTempCredentials?: boolean;
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface AdHocVariableFilter {
|
||||
operator: string;
|
||||
value: string;
|
||||
values?: string[];
|
||||
origin?: 'dashboard' | string;
|
||||
/** @deprecated */
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
@@ -281,3 +281,95 @@ describe('isNumeric', () => {
|
||||
expect(isNumeric(value)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('null and NaN special mapping', () => {
|
||||
it('should return null for NaN', () => {
|
||||
const value = Number.NaN;
|
||||
expect(
|
||||
getValueMappingResult(
|
||||
[
|
||||
{
|
||||
type: MappingType.SpecialValue,
|
||||
options: {
|
||||
match: SpecialValueMatch.NullAndNaN,
|
||||
result: { text: 'it is null or nan' },
|
||||
},
|
||||
},
|
||||
],
|
||||
value
|
||||
)
|
||||
).toEqual({ text: 'it is null or nan' });
|
||||
});
|
||||
|
||||
it('should return null for null', () => {
|
||||
const value = null;
|
||||
expect(
|
||||
getValueMappingResult(
|
||||
[
|
||||
{
|
||||
type: MappingType.SpecialValue,
|
||||
options: {
|
||||
match: SpecialValueMatch.NullAndNaN,
|
||||
result: { text: 'it is null or nan' },
|
||||
},
|
||||
},
|
||||
],
|
||||
value
|
||||
)
|
||||
).toEqual({ text: 'it is null or nan' });
|
||||
});
|
||||
|
||||
it('should return null for undefined', () => {
|
||||
const value = undefined;
|
||||
expect(
|
||||
getValueMappingResult(
|
||||
[
|
||||
{
|
||||
type: MappingType.SpecialValue,
|
||||
options: {
|
||||
match: SpecialValueMatch.NullAndNaN,
|
||||
result: { text: 'it is null or nan' },
|
||||
},
|
||||
},
|
||||
],
|
||||
value
|
||||
)
|
||||
).toEqual({ text: 'it is null or nan' });
|
||||
});
|
||||
|
||||
it('should return null for numeric non-NaN', () => {
|
||||
const value = 42;
|
||||
expect(
|
||||
getValueMappingResult(
|
||||
[
|
||||
{
|
||||
type: MappingType.SpecialValue,
|
||||
options: {
|
||||
match: SpecialValueMatch.NullAndNaN,
|
||||
result: { text: 'it is null or nan' },
|
||||
},
|
||||
},
|
||||
],
|
||||
value
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for string', () => {
|
||||
const value = 'foo';
|
||||
expect(
|
||||
getValueMappingResult(
|
||||
[
|
||||
{
|
||||
type: MappingType.SpecialValue,
|
||||
options: {
|
||||
match: SpecialValueMatch.NullAndNaN,
|
||||
result: { text: 'it is null or nan' },
|
||||
},
|
||||
},
|
||||
],
|
||||
value
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useRef, useCallback } from 'react';
|
||||
|
||||
import { createDataFrame, createTheme } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import { data } from './FlameGraph/testData/dataNestedSet';
|
||||
import FlameGraphContainer from './FlameGraphContainer';
|
||||
import FlameGraphContainer, { labelSearch } from './FlameGraphContainer';
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
@@ -99,21 +100,117 @@ describe('FlameGraphContainer', () => {
|
||||
render(<FlameGraphContainerWithProps />);
|
||||
|
||||
// Checking for presence of this function before filter
|
||||
const matchingText = 'net/http.HandlerFunc.ServeHTTP';
|
||||
const matchingText1 = 'net/http.HandlerFunc.ServeHTTP';
|
||||
const matchingText2 = 'runtime.gcBgMarkWorker';
|
||||
const nonMatchingText = 'runtime.systemstack';
|
||||
|
||||
expect(screen.queryAllByText(matchingText).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText1).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText2).length).toBe(1);
|
||||
expect(screen.queryAllByText(nonMatchingText).length).toBe(1);
|
||||
|
||||
// Apply the filter
|
||||
const searchInput = await screen.getByPlaceholderText('Search...');
|
||||
await userEvent.type(searchInput, 'Handler serve');
|
||||
const searchInput = screen.getByPlaceholderText('Search...');
|
||||
await userEvent.type(searchInput, 'Handler serve,gcBgMarkWorker');
|
||||
|
||||
// We have to wait for filter to take effect
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByText(nonMatchingText).length).toBe(0);
|
||||
});
|
||||
// Check we didn't lose the one that should match
|
||||
expect(screen.queryAllByText(matchingText).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText1).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText2).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('labelSearch', () => {
|
||||
let container: FlameGraphDataContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
const df = createDataFrame(data);
|
||||
df.meta = {
|
||||
custom: {
|
||||
ProfileTypeID: 'cpu:foo:bar',
|
||||
},
|
||||
};
|
||||
|
||||
container = new FlameGraphDataContainer(df, { collapsing: false });
|
||||
});
|
||||
|
||||
describe('fuzzy', () => {
|
||||
it('single term', () => {
|
||||
const search = 'test pkg';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(45);
|
||||
});
|
||||
|
||||
it('multiple terms', () => {
|
||||
const search = 'test pkg,compress';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(107);
|
||||
});
|
||||
|
||||
it('falls back to fuzzy with malformed regex', () => {
|
||||
const search = 'deduplicatingSlice[.';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(1);
|
||||
});
|
||||
|
||||
it('no results', () => {
|
||||
const search = 'term_not_found';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regex', () => {
|
||||
it('single pattern', () => {
|
||||
const term = '\\d$';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(61);
|
||||
});
|
||||
|
||||
it('multiple patterns', () => {
|
||||
const term = '\\d$,^go';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(62);
|
||||
});
|
||||
|
||||
it('no results', () => {
|
||||
const term = 'pattern_not_found';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fuzzy and regex', () => {
|
||||
it('regex found, fuzzy found', () => {
|
||||
const term = '\\d$,test pkg';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(98);
|
||||
});
|
||||
|
||||
it('regex not found, fuzzy found', () => {
|
||||
const term = 'not_found_suffix$,test pkg';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(45);
|
||||
});
|
||||
|
||||
it('regex found, fuzzy not found', () => {
|
||||
const term = '\\d$,not_found_fuzzy';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(61);
|
||||
});
|
||||
|
||||
it('regex not found, fuzzy not found', () => {
|
||||
const term = 'not_found_suffix$,not_found_fuzzy';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
|
||||
it('does not match empty terms', () => {
|
||||
const search = ',,,,,';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,26 +317,71 @@ function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
|
||||
/**
|
||||
* Based on the search string it does a fuzzy search over all the unique labels, so we can highlight them later.
|
||||
*/
|
||||
function useLabelSearch(
|
||||
export function useLabelSearch(
|
||||
search: string | undefined,
|
||||
data: FlameGraphDataContainer | undefined
|
||||
): Set<string> | undefined {
|
||||
return useMemo(() => {
|
||||
if (search && data) {
|
||||
const foundLabels = new Set<string>();
|
||||
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
|
||||
if (!search || !data) {
|
||||
// In this case undefined means there was no search so no attempt to
|
||||
// highlighting anything should be made.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (idxs) {
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(data.getUniqueLabels()[idx]);
|
||||
}
|
||||
return labelSearch(search, data);
|
||||
}, [search, data]);
|
||||
}
|
||||
|
||||
export function labelSearch(search: string, data: FlameGraphDataContainer): Set<string> {
|
||||
const foundLabels = new Set<string>();
|
||||
const terms = search.split(',');
|
||||
|
||||
const regexFilter = (labels: string[], pattern: string): boolean => {
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(pattern);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let foundMatch = false;
|
||||
for (let label of labels) {
|
||||
if (!regex.test(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
foundLabels.add(label);
|
||||
foundMatch = true;
|
||||
}
|
||||
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
|
||||
return undefined;
|
||||
}, [search, data]);
|
||||
return foundMatch;
|
||||
};
|
||||
|
||||
const fuzzyFilter = (labels: string[], term: string): boolean => {
|
||||
let idxs = ufuzzy.filter(labels, term);
|
||||
if (!idxs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let foundMatch = false;
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(labels[idx]);
|
||||
foundMatch = true;
|
||||
}
|
||||
return foundMatch;
|
||||
};
|
||||
|
||||
for (let term of terms) {
|
||||
if (!term) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const found = regexFilter(data.getUniqueLabels(), term);
|
||||
if (!found) {
|
||||
fuzzyFilter(data.getUniqueLabels(), term);
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
|
||||
@@ -11,6 +11,17 @@ import { ResourceLoader, Resources, TFunction, TransProps, TransType } from './t
|
||||
let tFunc: I18NextTFunction<string[], undefined> | undefined;
|
||||
let transComponent: TransType;
|
||||
|
||||
function initTFuncAndTransComponent({ id, ns }: { id?: string; ns?: string[] } = {}) {
|
||||
if (id) {
|
||||
tFunc = getI18nInstance().getFixedT(null, id);
|
||||
transComponent = (props: TransProps) => <I18NextTrans shouldUnescape ns={id} {...props} />;
|
||||
return;
|
||||
}
|
||||
|
||||
tFunc = getI18nInstance().t;
|
||||
transComponent = (props: TransProps) => <I18NextTrans shouldUnescape ns={ns} {...props} />;
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export async function loadPluginResources(id: string, language: string, loaders?: ResourceLoader[]) {
|
||||
if (!loaders?.length) {
|
||||
@@ -41,8 +52,7 @@ export function initDefaultI18nInstance() {
|
||||
returnEmptyString: false,
|
||||
lng: DEFAULT_LANGUAGE, // this should be the locale of the phrases in our source JSX
|
||||
});
|
||||
tFunc = getI18nInstance().t;
|
||||
transComponent = (props: TransProps) => <I18NextTrans shouldUnescape {...props} />;
|
||||
initTFuncAndTransComponent();
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
@@ -63,8 +73,7 @@ export async function initPluginTranslations(id: string, loaders?: ResourceLoade
|
||||
initDefaultReactI18nInstance();
|
||||
|
||||
const language = getResolvedLanguage();
|
||||
tFunc = getI18nInstance().getFixedT(null, id);
|
||||
transComponent = (props: TransProps) => <I18NextTrans shouldUnescape ns={id} {...props} />;
|
||||
initTFuncAndTransComponent({ id });
|
||||
|
||||
await loadPluginResources(id, language, loaders);
|
||||
|
||||
@@ -136,8 +145,7 @@ async function initTranslations({
|
||||
|
||||
await getI18nInstance().init(options);
|
||||
|
||||
tFunc = getI18nInstance().t;
|
||||
transComponent = (props: TransProps) => <I18NextTrans shouldUnescape ns={ns} {...props} />;
|
||||
initTFuncAndTransComponent({ ns });
|
||||
|
||||
return {
|
||||
language: getResolvedLanguage(),
|
||||
|
||||
@@ -46,6 +46,10 @@ interface TransProps {
|
||||
* Values to interpolate into the translation
|
||||
*/
|
||||
values?: Record<string, unknown>;
|
||||
/**
|
||||
* Class name for the Trans component
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
PluginLoadingStrategy,
|
||||
PluginDependencies,
|
||||
PluginExtensions,
|
||||
TimeOption,
|
||||
} from '@grafana/data';
|
||||
|
||||
export interface AzureSettings {
|
||||
@@ -203,6 +204,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
reportingStaticContext?: Record<string, string>;
|
||||
exploreDefaultTimeOffset = '1h';
|
||||
exploreHideLogsDownload: boolean | undefined;
|
||||
quickRanges?: TimeOption[];
|
||||
|
||||
/**
|
||||
* Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface Options {
|
||||
showLogContextToggle: boolean;
|
||||
showTime: boolean;
|
||||
sortOrder: common.LogsSortOrder;
|
||||
syntaxHighlighting?: boolean;
|
||||
wrapLogMessage: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1327,7 +1327,7 @@ export interface AdHocFilterWithLabels {
|
||||
keyLabel?: string;
|
||||
valueLabels?: string[];
|
||||
forceEdit?: boolean;
|
||||
origin?: FilterOrigin;
|
||||
origin?: "dashboard";
|
||||
// @deprecated
|
||||
condition?: string;
|
||||
}
|
||||
@@ -1339,10 +1339,7 @@ export const defaultAdHocFilterWithLabels = (): AdHocFilterWithLabels => ({
|
||||
});
|
||||
|
||||
// Determine the origin of the adhoc variable filter
|
||||
// Accepted values are `dashboard` (filter originated from dashboard), or `scope` (filter originated from scope).
|
||||
export type FilterOrigin = "dashboard" | "scope";
|
||||
|
||||
export const defaultFilterOrigin = (): FilterOrigin => ("dashboard");
|
||||
export const FilterOrigin = "dashboard";
|
||||
|
||||
// Define the MetricFindValue type
|
||||
export interface MetricFindValue {
|
||||
|
||||
@@ -75,10 +75,10 @@
|
||||
"@leeoniya/ufuzzy": "1.0.18",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@react-aria/dialog": "3.5.25",
|
||||
"@react-aria/focus": "3.20.3",
|
||||
"@react-aria/overlays": "3.27.1",
|
||||
"@react-aria/utils": "3.29.0",
|
||||
"@react-aria/dialog": "3.5.27",
|
||||
"@react-aria/focus": "3.20.5",
|
||||
"@react-aria/overlays": "3.27.3",
|
||||
"@react-aria/utils": "3.29.1",
|
||||
"@tanstack/react-virtual": "^3.5.1",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/lodash": "4.17.15",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ActionModel, Field, ActionVariableInput } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Button, ButtonProps } from '../Button/Button';
|
||||
import { ConfirmModal } from '../ConfirmModal/ConfirmModal';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { ActionModel, ActionVariableInput } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Field } from '../Forms/Field';
|
||||
import { FieldSet } from '../Forms/FieldSet';
|
||||
|
||||
@@ -4,10 +4,10 @@ import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Box } from '../Layout/Box/Box';
|
||||
|
||||
@@ -3,8 +3,9 @@ import { debounce } from 'lodash';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { Trans } from '../../utils/i18n';
|
||||
import { Field, FieldProps } from '../Forms/Field';
|
||||
import { InlineToast } from '../InlineToast/InlineToast';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { memo, cloneElement, FC, useMemo, useContext, ReactNode } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFocusStyles } from '../../themes/mixins';
|
||||
import { t } from '../../utils/i18n';
|
||||
|
||||
import { CardContainer, CardContainerProps, getCardContainerStyles } from './CardContainer';
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { OverlayContainer, useOverlay } from '@react-aria/overlays';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Alert } from '../Alert/Alert';
|
||||
import { clearButtonStyles } from '../Button/Button';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
@@ -5,10 +5,10 @@ import { PureComponent } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { Themeable2 } from '../../types/theme';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { Input } from '../Input/Input';
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Button, ButtonProps } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { InlineToast } from '../InlineToast/InlineToast';
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { Themeable2 } from '../../types/theme';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Tab } from '../Tabs/Tab';
|
||||
import { TabsBar } from '../Tabs/TabsBar';
|
||||
import { PopoverContentProps } from '../Tooltip/types';
|
||||
|
||||
@@ -5,9 +5,9 @@ import tinycolor from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
|
||||
/** @internal */
|
||||
export enum ColorSwatchVariant {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
import NamedColorsGroup from './NamedColorsGroup';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
import { InlineSwitch } from '../Switch/Switch';
|
||||
import { PopoverContentProps } from '../Tooltip/types';
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useVirtualizer, type Range } from '@tanstack/react-virtual';
|
||||
import { useCombobox } from 'downshift';
|
||||
import React, { useCallback, useId, useMemo } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { AutoSizeInput } from '../Input/AutoSizeInput';
|
||||
import { Input, Props as InputProps } from '../Input/Input';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Trans } from '../../utils/i18n';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Box } from '../Layout/Box/Box';
|
||||
import { Stack } from '../Layout/Stack/Stack';
|
||||
|
||||
@@ -2,8 +2,9 @@ import { cx } from '@emotion/css';
|
||||
import { useCombobox, useMultipleSelection } from 'downshift';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Box } from '../Layout/Box/Box';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
|
||||
@@ -2,9 +2,9 @@ import { css } from '@emotion/css';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
interface ValuePillProps {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { debounce } from 'lodash';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { t } from '../../utils/i18n';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { fuzzyFind, itemToString } from './filter';
|
||||
import { ComboboxOption } from './types';
|
||||
|
||||
@@ -3,10 +3,10 @@ import { ReactElement, useEffect, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { ComponentSize } from '../../types/size';
|
||||
import { Trans } from '../../utils/i18n';
|
||||
import { Button, ButtonVariant } from '../Button/Button';
|
||||
|
||||
export interface Props {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Button, ButtonVariant } from '../Button/Button';
|
||||
import { Field } from '../Forms/Field';
|
||||
import { Input } from '../Input/Input';
|
||||
|
||||
@@ -2,9 +2,9 @@ import { css } from '@emotion/css';
|
||||
import { memo, ChangeEvent } from 'react';
|
||||
|
||||
import { VariableSuggestion, GrafanaTheme2, DataLink } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { Field } from '../Forms/Field';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { DataFrame, DataLink, VariableSuggestion } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { Trans } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button/Button';
|
||||
import { Modal } from '../../Modal/Modal';
|
||||
import { DataLinkEditor } from '../DataLinkEditor';
|
||||
|
||||
+1
-1
@@ -4,9 +4,9 @@ import { cloneDeep } from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Action, DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { t } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button/Button';
|
||||
import { Modal } from '../../Modal/Modal';
|
||||
|
||||
|
||||
+1
-1
@@ -2,9 +2,9 @@ import { css, cx } from '@emotion/css';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
import { Action, DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { t } from '../../../utils/i18n';
|
||||
import { Badge } from '../../Badge/Badge';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { IconButton } from '../../IconButton/IconButton';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { Field, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { Trans } from '../../utils/i18n';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { DataLinkButton } from './DataLinkButton';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { InlineSwitch } from '../../components/Switch/Switch';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
|
||||
export interface Props<T extends DataSourceJsonData>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { InlineField } from '../../components/Forms/InlineField';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { FormField } from '../FormField/FormField';
|
||||
import { SecretFormField } from '../SecretFormField/SecretFormField';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent, MouseEvent } from 'react';
|
||||
|
||||
import { Trans } from '../../utils/i18n';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { Button } from '../Button/Button';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
import { InlineFieldRow } from '../Forms/InlineFieldRow';
|
||||
|
||||
@@ -3,9 +3,9 @@ import { uniqueId } from 'lodash';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { Button } from '../Button/Button';
|
||||
import { FormField } from '../FormField/FormField';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useState, useCallback, useId, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { Alert } from '../Alert/Alert';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Field } from '../Forms/Field';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
import { Stack } from '../Layout/Stack/Stack';
|
||||
import { InlineSwitch } from '../Switch/Switch';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { InlineSwitch } from '../../components/Switch/Switch';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
|
||||
export interface Props<T extends DataSourceJsonData>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { css, cx } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { KeyValue } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { FormField } from '../FormField/FormField';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
+1
-1
@@ -19,9 +19,9 @@ import {
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { Components } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button/Button';
|
||||
import { InlineField } from '../../Forms/InlineField';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
|
||||
+1
-1
@@ -6,9 +6,9 @@ import { useOverlay } from '@react-aria/overlays';
|
||||
import { FormEvent, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { Trans, t } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button/Button';
|
||||
import { Field } from '../../Forms/Field';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
getTimeZoneInfo,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { ButtonGroup } from '../Button/ButtonGroup';
|
||||
import { getModalStyles } from '../Modal/getModalStyles';
|
||||
import { getPortalContainer } from '../Portal/Portal';
|
||||
|
||||
+37
-28
@@ -3,9 +3,9 @@ import { useCallback } from 'react';
|
||||
import Calendar, { CalendarType } from 'react-calendar';
|
||||
|
||||
import { GrafanaTheme2, dateTimeParse, DateTime, TimeZone } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { t } from '../../../utils/i18n';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { getWeekStart, WeekStart } from '../WeekStartPicker';
|
||||
import { adjustDateForReactCalendar } from '../utils/adjustDateForReactCalendar';
|
||||
@@ -92,6 +92,7 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
|
||||
// If a time range is part of only 1 day but does not encompass the whole day,
|
||||
// the class that react-calendar uses is '--hasActive' by itself (without being part of a '--range')
|
||||
const hasActiveSelector = `.react-calendar__tile--hasActive:not(.react-calendar__tile--range)`;
|
||||
|
||||
return {
|
||||
title: css({
|
||||
color: theme.colors.text.primary,
|
||||
@@ -154,43 +155,51 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
|
||||
outline: 0,
|
||||
},
|
||||
|
||||
[`${hasActiveSelector}, .react-calendar__tile--active`]: {
|
||||
color: theme.colors.primary.contrastText,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
background: theme.colors.primary.main,
|
||||
border: '0px',
|
||||
// The --hover modifier is active when the user is selecting a range and hovering over a tile - it shows the pending range.
|
||||
// It is applied to all dates between the clicked date and the hovered date.
|
||||
// The *clicked* date should have primary bg, while *pending* range dates should have hover bg.
|
||||
'.react-calendar__tile--hover': {
|
||||
backgroundColor: theme.colors.action.hover,
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: 0,
|
||||
},
|
||||
|
||||
'.react-calendar__tile:hover:not(.react-calendar__tile--active):not(.react-calendar__tile--rangeEnd):not(.react-calendar__tile--rangeStart)':
|
||||
'.react-calendar__tile--hoverStart': {
|
||||
borderTopLeftRadius: theme.shape.radius.pill,
|
||||
borderBottomLeftRadius: theme.shape.radius.pill,
|
||||
},
|
||||
|
||||
'.react-calendar__tile--hoverEnd': {
|
||||
borderTopRightRadius: theme.shape.radius.pill,
|
||||
borderBottomRightRadius: theme.shape.radius.pill,
|
||||
},
|
||||
|
||||
// Addiitonally, when hovering a date before clicking any, it should show the hover bg.
|
||||
'.react-calendar__tile:hover:not(.react-calendar__tile--hover):not(.react-calendar__tile--active):not(.react-calendar__tile--hasActive)':
|
||||
{
|
||||
backgroundColor: theme.colors.action.hover,
|
||||
},
|
||||
|
||||
'.react-calendar__tile--rangeEnd, .react-calendar__tile--rangeStart': {
|
||||
padding: 0,
|
||||
border: '0px',
|
||||
color: theme.colors.primary.contrastText,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
background: theme.colors.primary.main,
|
||||
|
||||
abbr: {
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
display: 'block',
|
||||
paddingTop: '2px',
|
||||
height: '26px',
|
||||
},
|
||||
|
||||
// When the user is selecting a range (they've clicked one date, tiles have --hover), both --rangeStart and --rangeEnd are on the tile.
|
||||
// The --hover classes above handle the rounding of the tiles so they're contigious with the range
|
||||
[`${hasActiveSelector}, .react-calendar__tile--rangeStart:not(.react-calendar__tile--hover)`]: {
|
||||
borderTopLeftRadius: theme.shape.radius.pill,
|
||||
borderBottomLeftRadius: theme.shape.radius.pill,
|
||||
},
|
||||
|
||||
[`${hasActiveSelector}, .react-calendar__tile--rangeStart`]: {
|
||||
borderTopLeftRadius: '20px',
|
||||
borderBottomLeftRadius: '20px',
|
||||
[`${hasActiveSelector}, .react-calendar__tile--rangeEnd:not(.react-calendar__tile--hover)`]: {
|
||||
borderTopRightRadius: theme.shape.radius.pill,
|
||||
borderBottomRightRadius: theme.shape.radius.pill,
|
||||
},
|
||||
|
||||
[`${hasActiveSelector}, .react-calendar__tile--rangeEnd`]: {
|
||||
borderTopRightRadius: '20px',
|
||||
borderBottomRightRadius: '20px',
|
||||
},
|
||||
[`${hasActiveSelector}, .react-calendar__tile--active, .react-calendar__tile--rangeEnd, .react-calendar__tile--rangeStart`]:
|
||||
{
|
||||
color: theme.colors.primary.contrastText,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
background: theme.colors.primary.main,
|
||||
border: '0px',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { Trans } from '../../../utils/i18n';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { Button } from '../../Button/Button';
|
||||
import { Stack } from '../../Layout/Stack/Stack';
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { Trans, t } from '../../../utils/i18n';
|
||||
import { IconButton } from '../../IconButton/IconButton';
|
||||
import { Stack } from '../../Layout/Stack/Stack';
|
||||
|
||||
|
||||
+1
-1
@@ -3,10 +3,10 @@ import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, isDateTime, rangeUtil, RawTimeRange, TimeOption, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
|
||||
import { getFocusStyles } from '../../../themes/mixins';
|
||||
import { t, Trans } from '../../../utils/i18n';
|
||||
import { FilterInput } from '../../FilterInput/FilterInput';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { WeekStart } from '../WeekStartPicker';
|
||||
|
||||
+1
-1
@@ -5,9 +5,9 @@ import * as React from 'react';
|
||||
|
||||
import { getTimeZoneInfo, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button/Button';
|
||||
import { Combobox } from '../../Combobox/Combobox';
|
||||
import { Field } from '../../Forms/Field';
|
||||
|
||||
+1
-1
@@ -14,9 +14,9 @@ import {
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button/Button';
|
||||
import { Field } from '../../Forms/Field';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
|
||||
+1
-1
@@ -2,9 +2,9 @@ import { css } from '@emotion/css';
|
||||
import { useRef, ReactNode } from 'react';
|
||||
|
||||
import { GrafanaTheme2, TimeOption } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||
import { t } from '../../../utils/i18n';
|
||||
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { TimeRangeOption } from './TimeRangeOption';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t } from '../../utils/i18n';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
TimeZone,
|
||||
InternalTimeZones,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Select } from '../Select/Select';
|
||||
|
||||
import { TimeZoneGroup } from './TimeZonePicker/TimeZoneGroup';
|
||||
|
||||
@@ -8,9 +8,9 @@ import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { getDragStyles } from '../DragHandle/DragHandle';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FeatureState } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Badge, BadgeProps } from '../Badge/Badge';
|
||||
|
||||
export interface FeatureBadgeProps {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { ReactNode, useCallback, useState } from 'react';
|
||||
import { Accept, DropEvent, DropzoneOptions, FileError, FileRejection, useDropzone, ErrorCode } from 'react-dropzone';
|
||||
|
||||
import { formattedValueToString, getValueFormat, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { Alert } from '../Alert/Alert';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { formattedValueToString, getValueFormat, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { trimFileName } from '../../utils/file';
|
||||
import { t, Trans } from '../../utils/i18n';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
@@ -5,12 +5,12 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFocusStyles } from '../../themes/mixins';
|
||||
import { ComponentSize } from '../../types/size';
|
||||
import { trimFileName } from '../../utils/file';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { getButtonStyles } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { forwardRef, useRef, HTMLProps } from 'react';
|
||||
|
||||
import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
import { Trans } from '../../utils/i18n';
|
||||
import { useCombinedRefs } from '../../utils/useCombinedRefs';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { CellProps, HeaderProps } from 'react-table';
|
||||
|
||||
import { t } from '../../../utils/i18n';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { IconButton } from '../../IconButton/IconButton';
|
||||
|
||||
const expanderContainerStyles = css({
|
||||
|
||||
@@ -2,8 +2,8 @@ import { memo, useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Input } from '../Input/Input';
|
||||
|
||||
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
SelectableValue,
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ComparisonOperation } from '@grafana/schema';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Select } from '../Select/Select';
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { ReactElement, useCallback, useState, useRef, useImperativeHandle, CSSPr
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFocusStyles } from '../../themes/mixins';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Stack } from '../Layout/Stack/Stack';
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user