mirror of https://github.com/grafana/grafana
drclau/unistor/namespace_authorizer
commit
6993f108a2
@ -1,7 +1,7 @@ |
||||
module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT |
||||
|
||||
go 1.22 |
||||
go 1.22.1 |
||||
|
||||
toolchain go1.22.4 |
||||
toolchain go1.23.0 |
||||
|
||||
require github.com/golangci/golangci-lint v1.59.1 // cmd/golangci-lint |
||||
require github.com/golangci/golangci-lint v1.60.1 // cmd/golangci-lint |
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@ |
||||
FROM golang:latest AS builder |
||||
|
||||
ADD main.go / |
||||
ADD go.mod / |
||||
ADD go.sum / |
||||
WORKDIR / |
||||
|
||||
RUN go mod download |
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o main . |
||||
|
||||
FROM scratch |
||||
WORKDIR / |
||||
EXPOSE 9111 |
||||
COPY --from=builder /main /main |
||||
ENTRYPOINT ["/main"] |
@ -0,0 +1,6 @@ |
||||
prometheus_high_card: |
||||
build: docker/blocks/prometheus_high_card |
||||
ports: |
||||
- "3012:3012" |
||||
extra_hosts: |
||||
- "host.docker.internal:host-gateway" |
@ -0,0 +1,20 @@ |
||||
module high-card |
||||
|
||||
go 1.22.4 |
||||
|
||||
require ( |
||||
github.com/prometheus/client_golang v1.20.2 |
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 |
||||
) |
||||
|
||||
require ( |
||||
github.com/beorn7/perks v1.0.1 // indirect |
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect |
||||
github.com/klauspost/compress v1.17.9 // indirect |
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect |
||||
github.com/prometheus/client_model v0.6.1 // indirect |
||||
github.com/prometheus/common v0.55.0 // indirect |
||||
github.com/prometheus/procfs v0.15.1 // indirect |
||||
golang.org/x/sys v0.22.0 // indirect |
||||
google.golang.org/protobuf v1.34.2 // indirect |
||||
) |
@ -0,0 +1,26 @@ |
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= |
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= |
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= |
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= |
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= |
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= |
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= |
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= |
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= |
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= |
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= |
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= |
||||
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= |
||||
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= |
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= |
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= |
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= |
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= |
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= |
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= |
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= |
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= |
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= |
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= |
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= |
@ -0,0 +1,123 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"net/http" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/prometheus/client_golang/prometheus" |
||||
"github.com/prometheus/client_golang/prometheus/promauto" |
||||
"github.com/prometheus/client_golang/prometheus/promhttp" |
||||
"golang.org/x/exp/rand" |
||||
) |
||||
|
||||
func randomValues(max int) func() (string, bool) { |
||||
i := 0 |
||||
return func() (string, bool) { |
||||
i++ |
||||
return strconv.Itoa(i), i < max+1 |
||||
} |
||||
} |
||||
|
||||
func staticList(input []string) func() string { |
||||
return func() string { |
||||
i := rand.Intn(len(input)) |
||||
|
||||
return input[i] |
||||
} |
||||
} |
||||
|
||||
type dimension struct { |
||||
label string |
||||
getNextValue func() string |
||||
} |
||||
|
||||
func main() { |
||||
|
||||
fakeMetrics := []dimension{ |
||||
{ |
||||
label: "cluster", |
||||
getNextValue: staticList([]string{"prod-uk1", "prod-eu1", "prod-uk2", "prod-eu2", "prod-uk3", "prod-eu3", "prod-uk4", "prod-eu4", "prod-uk5", "prod-eu5"}), |
||||
}, |
||||
{ |
||||
label: "namespace", |
||||
getNextValue: staticList([]string{"default", "kube-api", "kube-system", "kube-public", "kube-node-lease", "kube-ingress", "kube-logging", "kube-metrics", "kube-monitoring", "kube-network", "kube-storage"}), |
||||
}, |
||||
{ |
||||
label: "pod", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
{ |
||||
label: "container", |
||||
getNextValue: staticList([]string{"container"}), |
||||
}, |
||||
{ |
||||
label: "method", |
||||
getNextValue: staticList([]string{"GET", "POST", "DELETE", "PUT", "PATCH"}), |
||||
}, |
||||
{ |
||||
label: "address", |
||||
getNextValue: staticList([]string{"/", "/api", "/api/dashboard", "/api/dashboard/:uid", "/api/dashboard/:uid/overview", "/api/dashboard/:uid/overview/:id", "/api/dashboard/:uid/overview/:id/summary", "/api/dashboard/:uid/overview/:id/summary/:type", "/api/dashboard/:uid/overview/:id/summary/:type/:subtype", "/api/dashboard/:uid/overview/:id/summary/:type/:subtype/:id"}), |
||||
}, |
||||
{ |
||||
label: "extra_label_name1", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
{ |
||||
label: "extra_label_name2", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
{ |
||||
label: "extra_label_name3", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
{ |
||||
label: "extra_label_name4", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
{ |
||||
label: "extra_label_name5", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
{ |
||||
label: "extra_label_name6", |
||||
getNextValue: staticList([]string{"default"}), |
||||
}, |
||||
} |
||||
|
||||
dimensions := []string{} |
||||
for _, dim := range fakeMetrics { |
||||
dimensions = append(dimensions, dim.label) |
||||
} |
||||
|
||||
opsProcessed := promauto.NewCounterVec(prometheus.CounterOpts{ |
||||
Name: "fakedata_highcard_http_requests_total", |
||||
Help: "a high cardinality counter", |
||||
}, dimensions) |
||||
|
||||
http.Handle("/metrics", promhttp.Handler()) |
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
w.WriteHeader(http.StatusOK) |
||||
w.Write([]byte("Hello, is it me you're looking for?")) |
||||
}) |
||||
|
||||
go func() { |
||||
for { |
||||
labels := []string{} |
||||
for _, dim := range fakeMetrics { |
||||
value := dim.getNextValue() |
||||
labels = append(labels, value) |
||||
} |
||||
|
||||
opsProcessed.WithLabelValues(labels...).Inc() |
||||
|
||||
time.Sleep(time.Millisecond) |
||||
} |
||||
}() |
||||
|
||||
fmt.Printf("Server started at :9111\n") |
||||
|
||||
log.Fatal(http.ListenAndServe(":9111", nil)) |
||||
} |
@ -1,27 +1,19 @@ |
||||
apiVersion: 1 |
||||
|
||||
apps: |
||||
- type: myorg-extensions-app |
||||
- type: grafana-extensionstest-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-a-app |
||||
jsonData: |
||||
apiUrl: http://default-url.com |
||||
secureJsonData: |
||||
apiKey: secret-key |
||||
- type: grafana-extensionexample1-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-b-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-extensionpoint-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-componentconsumer-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-componentexposer-app |
||||
- type: grafana-extensionexample2-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
|
@ -0,0 +1,45 @@ |
||||
--- |
||||
canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-alerts-panels/ |
||||
description: Create alert rules from panels. Reuse the queries in the panel and create alert rules based on them. |
||||
keywords: |
||||
- grafana |
||||
- alerting |
||||
- panels |
||||
- create |
||||
- grafana-managed |
||||
- data source-managed |
||||
labels: |
||||
products: |
||||
- cloud |
||||
- enterprise |
||||
- oss |
||||
title: Create alert rules from panels |
||||
weight: 400 |
||||
--- |
||||
|
||||
## Create alert rules from panels |
||||
|
||||
Create alert rules from time series panels. By doing so, you can reuse the queries in the panel and create alert rules based on them. |
||||
|
||||
1. Navigate to a dashboard in the **Dashboards** section. |
||||
2. Hover over the top-right corner of a time series panel and click the panel menu icon. |
||||
3. From the dropdown menu, select **More...** > **New alert rule**. |
||||
|
||||
The New alert rule form opens where you can configure and create your alert rule based on the query used in the panel. |
||||
|
||||
{{% admonition type="note" %}} |
||||
Changes to the panel aren't reflected on the linked alert rules. If you change a query, you have to update it in both the panel and the alert rule. |
||||
|
||||
Alert rules are only supported in [time series](ref:time-series) visualizations. |
||||
{{% /admonition %}} |
||||
|
||||
{{< docs/play title="visualizations with linked alerts in Grafana" url="https://play.grafana.org/d/000000074/" >}} |
||||
|
||||
## View alert rules from panels |
||||
|
||||
To view alert rules associated with a time series panel, complete the following steps. |
||||
|
||||
1. Open the panel editor by hovering over the top-right corner of any panel |
||||
1. Click the panel menu icon that appears. |
||||
1. Click **Edit**. |
||||
1. Click the **Alert** tab to view existing alert rules or create a new one. |
@ -0,0 +1,57 @@ |
||||
--- |
||||
canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns/ |
||||
description: Configure the Grafana Alerting - Amazon SNS integration to receive alert notifications when your alerts are firing. |
||||
keywords: |
||||
- grafana |
||||
- alerting |
||||
- Amazon SNS |
||||
- integration |
||||
labels: |
||||
products: |
||||
- cloud |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Amazon SNS |
||||
title: Configure Amazon SNS for Alerting |
||||
weight: 0 |
||||
--- |
||||
|
||||
# Configure Amazon SNS for Alerting |
||||
|
||||
Use the Grafana Alerting - Amazon SNS integration to send notifications to Amazon SNS when your alerts are firing. |
||||
|
||||
## Before you begin |
||||
|
||||
To configure Amazon SNS to receive alert notifications, complete the following steps. |
||||
|
||||
1. Create a new topic in https://console.aws.amazon.com/sns. |
||||
1. Open the topic and create a new subscription. |
||||
1. Choose the protocol HTTPS. |
||||
1. Copy the URL. |
||||
|
||||
For more information, refer to [Amazon SNS documentation](https://docs.aws.amazon.com/sns/latest/dg/welcome.html). |
||||
|
||||
## Procedure |
||||
|
||||
To create your Amazon SNS integration in Grafana Alerting, complete the following steps. |
||||
|
||||
1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. |
||||
1. Click **+ Add contact point**. |
||||
1. Enter a contact point name. |
||||
1. From the Integration list, select **AWS SNS**. |
||||
1. Copy in the URL from above into the **The Amazon SNS API URL** field. |
||||
1. Click **Test** to check that your integration works. |
||||
1. Click **Save contact point**. |
||||
|
||||
## Next steps |
||||
|
||||
The Amazon SNS contact point is ready to receive alert notifications. |
||||
|
||||
To add this contact point to your alert, complete the following steps. |
||||
|
||||
1. In Grafana, navigate to **Alerting** > **Alert rules**. |
||||
1. Edit or create a new alert rule. |
||||
1. Scroll down to the **Configure labels and notifications** section. |
||||
1. Under Notifications click **Select contact point**. |
||||
1. From the drop-down menu, select the previously created contact point. |
||||
1. **Click Save rule and exit**. |
@ -0,0 +1,154 @@ |
||||
--- |
||||
canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt/ |
||||
description: Configure the MQTT notifier integration for Alerting |
||||
keywords: |
||||
- grafana |
||||
- alerting |
||||
- guide |
||||
- contact point |
||||
- mqtt |
||||
labels: |
||||
products: |
||||
- cloud |
||||
- enterprise |
||||
- oss |
||||
menuTitle: MQTT notifier |
||||
title: Configure the MQTT notifier for Alerting |
||||
weight: 80 |
||||
--- |
||||
|
||||
# Configure the MQTT notifier for Alerting |
||||
|
||||
Use the Grafana Alerting - MQTT integration to send notifications to an MQTT broker when your alerts are firing. |
||||
|
||||
## Procedure |
||||
|
||||
To configure the MQTT integration for Alerting, 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. 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. |
||||
A test alert notification should be sent to the MQTT broker. |
||||
1. Click **Save** contact point. |
||||
|
||||
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: |
||||
|
||||
- **JSON**: Sends the alert notification in JSON format. |
||||
- **Text**: Sends the rendered alert notification message in plain text format. |
||||
|
||||
## MQTT JSON payload |
||||
|
||||
If the JSON message format is selected in **Optional MQTT settings**, the payload is sent in the following structure. |
||||
|
||||
```json |
||||
{ |
||||
"receiver": "My MQTT integration", |
||||
"status": "firing", |
||||
"orgId": 1, |
||||
"alerts": [ |
||||
{ |
||||
"status": "firing", |
||||
"labels": { |
||||
"alertname": "High memory usage", |
||||
"team": "blue", |
||||
"zone": "us-1" |
||||
}, |
||||
"annotations": { |
||||
"description": "The system has high memory usage", |
||||
"runbook_url": "https://myrunbook.com/runbook/1234", |
||||
"summary": "This alert was triggered for zone us-1" |
||||
}, |
||||
"startsAt": "2021-10-12T09:51:03.157076+02:00", |
||||
"endsAt": "0001-01-01T00:00:00Z", |
||||
"generatorURL": "https://play.grafana.org/alerting/1afz29v7z/edit", |
||||
"fingerprint": "c6eadffa33fcdf37", |
||||
"silenceURL": "https://play.grafana.org/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DT2%2Cteam%3Dblue%2Czone%3Dus-1", |
||||
"dashboardURL": "", |
||||
"panelURL": "", |
||||
"values": { |
||||
"B": 44.23943737541908, |
||||
"C": 1 |
||||
} |
||||
}, |
||||
{ |
||||
"status": "firing", |
||||
"labels": { |
||||
"alertname": "High CPU usage", |
||||
"team": "blue", |
||||
"zone": "eu-1" |
||||
}, |
||||
"annotations": { |
||||
"description": "The system has high CPU usage", |
||||
"runbook_url": "https://myrunbook.com/runbook/1234", |
||||
"summary": "This alert was triggered for zone eu-1" |
||||
}, |
||||
"startsAt": "2021-10-12T09:56:03.157076+02:00", |
||||
"endsAt": "0001-01-01T00:00:00Z", |
||||
"generatorURL": "https://play.grafana.org/alerting/d1rdpdv7k/edit", |
||||
"fingerprint": "bc97ff14869b13e3", |
||||
"silenceURL": "https://play.grafana.org/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DT1%2Cteam%3Dblue%2Czone%3Deu-1", |
||||
"dashboardURL": "", |
||||
"panelURL": "", |
||||
"values": { |
||||
"B": 44.23943737541908, |
||||
"C": 1 |
||||
} |
||||
} |
||||
], |
||||
"groupLabels": {}, |
||||
"commonLabels": { |
||||
"team": "blue" |
||||
}, |
||||
"commonAnnotations": {}, |
||||
"externalURL": "https://play.grafana.org/", |
||||
"version": "1", |
||||
"groupKey": "{}:{}", |
||||
"message": "**Firing**\n\nLabels:\n - alertname = T2\n - team = blue\n - zone = us-1\nAnnotations:\n - description = This is the alert rule checking the second system\n - runbook_url = https://myrunbook.com\n - summary = This is my summary\nSource: https://play.grafana.org/alerting/1afz29v7z/edit\nSilence: https://play.grafana.org/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DT2%2Cteam%3Dblue%2Czone%3Dus-1\n\nLabels:\n - alertname = T1\n - team = blue\n - zone = eu-1\nAnnotations:\nSource: https://play.grafana.org/alerting/d1rdpdv7k/edit\nSilence: https://play.grafana.org/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DT1%2Cteam%3Dblue%2Czone%3Deu-1\n" |
||||
} |
||||
``` |
||||
|
||||
### Payload fields |
||||
|
||||
Each notification 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 | **Deprecated. It will be removed in a future release.** | |
||||
| panelURL | string | **Deprecated. It will be removed in a future release.** | |
||||
| imageURL | string | URL of a screenshot of a panel assigned to the rule that created this notification | |
||||
|
||||
{{< admonition type="note" >}} |
||||
Alert rules are not coupled to dashboards anymore. The fields related to dashboards `dashboardId` and `panelId` have been removed. |
||||
{{< /admonition >}} |
@ -0,0 +1,22 @@ |
||||
--- |
||||
description: Guide for upgrading to Grafana v11.2 |
||||
keywords: |
||||
- grafana |
||||
- configuration |
||||
- documentation |
||||
- upgrade |
||||
- '11.2' |
||||
title: Upgrade to Grafana v11.2 |
||||
menuTitle: Upgrade to v11.2 |
||||
weight: 1000 |
||||
--- |
||||
|
||||
# Upgrade to Grafana v11.2 |
||||
|
||||
{{< docs/shared lookup="upgrade/intro.md" source="grafana" version="<GRAFANA_VERSION>" >}} |
||||
|
||||
{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}} |
||||
|
||||
{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="<GRAFANA_VERSION>" >}} |
||||
|
||||
## Technical notes |
@ -1,5 +0,0 @@ |
||||
# Custom plugins |
||||
|
||||
Plugins in this directory will be installed when the e2e [test server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) is started. Optionally, you can provision the plugin by adding configuration to the [datasources.yaml](https://github.com/grafana/grafana/blob/extensions/add-e2e-tests/devenv/datasources.yaml) or to the [plugins.yaml](https://github.com/grafana/grafana/blob/extensions/add-e2e-tests/devenv/plugins.yaml). |
||||
|
||||
These plugins are not being built as part of CI. Plugins in this directory are being version controlled, so make sure the bundle size is small. Only use dependencies provided by the runtime (see list of runtime dependencies [here](https://github.com/grafana/plugin-tools/blob/08b67179bdbf8847788c54aadb22654aa1a7c060/packages/create-plugin/templates/common/.config/webpack/webpack.config.ts#L36)). |
@ -1,22 +0,0 @@ |
||||
# App with exposed components |
||||
|
||||
This directory contains two apps - `myorg-componentconsumer-app` and `myorg-componentexposer-app` which is nested inside `myorg-componentconsumer-app`. |
||||
|
||||
`myorg-componentconsumer-app` exposes a simple React component using the [`exposeComponent`](https://grafana.com/developers/plugin-tools/reference/ui-extensions#exposecomponent) api. `myorg-componentconsumer-app` in turn, consumes this compoment using the [`https://grafana.com/developers/plugin-tools/reference/ui-extensions#useplugincomponent`](https://grafana.com/developers/plugin-tools/reference/ui-extensions#useplugincomponent) hook. |
||||
|
||||
To test this app: |
||||
|
||||
```sh |
||||
# start e2e test instance (it will install this plugin) |
||||
PORT=3000 ./scripts/grafana-server/start-server |
||||
# run Playwright tests using Playwright VSCode extension or with the following script |
||||
yarn e2e:playwright |
||||
``` |
||||
|
||||
or |
||||
|
||||
``` |
||||
PORT=3000 ./scripts/grafana-server/start-server |
||||
yarn start |
||||
yarn e2e |
||||
``` |
@ -1,28 +0,0 @@ |
||||
define(['@grafana/data', '@grafana/runtime', 'react'], function (grafanaData, grafanaRuntime, React) { |
||||
var AppPlugin = grafanaData.AppPlugin; |
||||
var usePluginComponent = grafanaRuntime.usePluginComponent; |
||||
|
||||
var MyComponent = function () { |
||||
var plugin = usePluginComponent('myorg-componentexposer-app/reusable-component/v1'); |
||||
var TestComponent = plugin.component; |
||||
var isLoading = plugin.isLoading; |
||||
|
||||
if (!TestComponent) { |
||||
return null; |
||||
} |
||||
|
||||
return React.createElement( |
||||
React.Fragment, |
||||
null, |
||||
React.createElement('div', null, 'Exposed component:'), |
||||
isLoading ? 'Loading..' : React.createElement(TestComponent, { name: 'World' }) |
||||
); |
||||
}; |
||||
|
||||
var App = function () { |
||||
return React.createElement('div', null, 'Hello Grafana!', React.createElement(MyComponent, null)); |
||||
}; |
||||
|
||||
var plugin = new AppPlugin().setRootPage(App); |
||||
return { plugin: plugin }; |
||||
}); |
@ -1,35 +0,0 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "Extensions exposed component App", |
||||
"id": "myorg-componentconsumer-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Example on how to extend grafana ui from a plugin", |
||||
"author": { |
||||
"name": "Myorg" |
||||
}, |
||||
"logos": { |
||||
"small": "img/logo.svg", |
||||
"large": "img/logo.svg" |
||||
}, |
||||
"screenshots": [], |
||||
"version": "1.0.0", |
||||
"updated": "2024-08-09" |
||||
}, |
||||
"includes": [ |
||||
{ |
||||
"type": "page", |
||||
"name": "Default", |
||||
"path": "/a/myorg-componentconsumer-app", |
||||
"role": "Admin", |
||||
"addToNav": true, |
||||
"defaultNav": true |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
} |
||||
} |
@ -1,14 +0,0 @@ |
||||
define(['@grafana/data', 'module', 'react'], function (grafanaData, amdModule, React) { |
||||
const plugin = new grafanaData.AppPlugin().exposeComponent({ |
||||
id: 'myorg-componentexposer-app/reusable-component/v1', |
||||
title: 'Reusable component', |
||||
description: 'A component that can be reused by other app plugins.', |
||||
component: function ({ name }) { |
||||
return React.createElement('div', { 'data-testid': 'exposed-component' }, 'Hello ', name, '!'); |
||||
}, |
||||
}); |
||||
|
||||
return { |
||||
plugin: plugin, |
||||
}; |
||||
}); |
@ -1,12 +0,0 @@ |
||||
# App with extension point |
||||
|
||||
This app was initially copied from the [app-with-extension-point](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-extension-point) example plugin. The plugin bundle is using AMD, but it's not minified and the plugin feature set is small so it should be possible to make changes in this file if necessary. |
||||
|
||||
To test this app: |
||||
|
||||
```sh |
||||
# start e2e test instance (it will install this plugin) |
||||
PORT=3000 ./scripts/grafana-server/start-server |
||||
# run Playwright tests using Playwright VSCode extension or with the following script |
||||
yarn e2e:playwright |
||||
``` |
@ -1,141 +0,0 @@ |
||||
define(['@grafana/data', 'react', '@grafana/ui', '@grafana/runtime'], function (data, React, UI, runtime) { |
||||
'use strict'; |
||||
|
||||
const styles = { |
||||
container: 'main-app-body', |
||||
actions: { button: 'action-button' }, |
||||
modal: { container: 'container', open: 'open-link' }, |
||||
appA: { container: 'a-app-body' }, |
||||
appB: { modal: 'b-app-modal' }, |
||||
}; |
||||
|
||||
function ModalComponent({ onDismiss, title, path }) { |
||||
return React.createElement( |
||||
UI.Modal, |
||||
{ 'data-testid': styles.modal.container, title, isOpen: true, onDismiss }, |
||||
React.createElement( |
||||
UI.VerticalGroup, |
||||
{ spacing: 'sm' }, |
||||
React.createElement('p', null, 'Do you want to proceed in the current tab or open a new tab?') |
||||
), |
||||
React.createElement( |
||||
UI.Modal.ButtonRow, |
||||
null, |
||||
React.createElement(UI.Button, { onClick: onDismiss, fill: 'outline', variant: 'secondary' }, 'Cancel'), |
||||
React.createElement( |
||||
UI.Button, |
||||
{ |
||||
type: 'submit', |
||||
variant: 'secondary', |
||||
onClick: function () { |
||||
window.open(data.locationUtil.assureBaseUrl(path), '_blank'); |
||||
onDismiss(); |
||||
}, |
||||
icon: 'external-link-alt', |
||||
}, |
||||
'Open in new tab' |
||||
), |
||||
React.createElement( |
||||
UI.Button, |
||||
{ |
||||
'data-testid': styles.modal.open, |
||||
type: 'submit', |
||||
variant: 'primary', |
||||
onClick: function () { |
||||
runtime.locationService.push(path); |
||||
}, |
||||
icon: 'apps', |
||||
}, |
||||
'Open' |
||||
) |
||||
) |
||||
); |
||||
} |
||||
|
||||
function ActionComponent({ extensions }) { |
||||
const options = React.useMemo( |
||||
function () { |
||||
return extensions.reduce(function (acc, extension) { |
||||
if (runtime.isPluginExtensionLink(extension)) { |
||||
acc.push({ label: extension.title, title: extension.title, value: extension }); |
||||
} |
||||
return acc; |
||||
}, []); |
||||
}, |
||||
[extensions] |
||||
); |
||||
|
||||
const [selected, setSelected] = React.useState(); |
||||
|
||||
return options.length === 0 |
||||
? React.createElement(UI.Button, null, 'Run default action') |
||||
: React.createElement( |
||||
React.Fragment, |
||||
null, |
||||
React.createElement( |
||||
UI.ButtonGroup, |
||||
null, |
||||
React.createElement( |
||||
UI.ToolbarButton, |
||||
{ |
||||
key: 'default-action', |
||||
variant: 'canvas', |
||||
onClick: function () { |
||||
alert('You triggered the default action'); |
||||
}, |
||||
}, |
||||
'Run default action' |
||||
), |
||||
React.createElement(UI.ButtonSelect, { |
||||
'data-testid': styles.actions.button, |
||||
key: 'select-extension', |
||||
variant: 'canvas', |
||||
options: options, |
||||
onChange: function (e) { |
||||
const extension = e.value; |
||||
if (runtime.isPluginExtensionLink(extension)) { |
||||
if (extension.path) setSelected(extension); |
||||
if (extension.onClick) extension.onClick(); |
||||
} |
||||
}, |
||||
}) |
||||
), |
||||
selected && |
||||
selected.path && |
||||
React.createElement(ModalComponent, { |
||||
title: selected.title, |
||||
path: selected.path, |
||||
onDismiss: function () { |
||||
setSelected(undefined); |
||||
}, |
||||
}) |
||||
); |
||||
} |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
const { extensions } = runtime.getPluginExtensions({ |
||||
extensionPointId: 'plugins/myorg-extensionpoint-app/actions', |
||||
context: {}, |
||||
}); |
||||
|
||||
return React.createElement( |
||||
'div', |
||||
{ 'data-testid': styles.container, style: { marginTop: '5%' } }, |
||||
React.createElement( |
||||
UI.HorizontalGroup, |
||||
{ align: 'flex-start', justify: 'center' }, |
||||
React.createElement( |
||||
UI.HorizontalGroup, |
||||
null, |
||||
React.createElement('span', null, 'Hello Grafana! These are the actions you can trigger from this plugin'), |
||||
React.createElement(ActionComponent, { extensions: extensions }) |
||||
) |
||||
) |
||||
); |
||||
} |
||||
} |
||||
|
||||
const plugin = new data.AppPlugin().setRootPage(RootComponent); |
||||
return { plugin: plugin }; |
||||
}); |
@ -1,36 +0,0 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "Extension Point App", |
||||
"id": "myorg-extensionpoint-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Show case how to add an extension point to your plugin", |
||||
"author": { |
||||
"name": "Myorg" |
||||
}, |
||||
"logos": { |
||||
"small": "img/logo.svg", |
||||
"large": "img/logo.svg" |
||||
}, |
||||
"screenshots": [], |
||||
"version": "1.0.0", |
||||
"updated": "2024-06-11" |
||||
}, |
||||
"includes": [ |
||||
{ |
||||
"type": "page", |
||||
"name": "Default", |
||||
"path": "/a/myorg-extensionpoint-app", |
||||
"role": "Admin", |
||||
"addToNav": true, |
||||
"defaultNav": true |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
}, |
||||
"extensions": [] |
||||
} |
@ -1,26 +0,0 @@ |
||||
define(['@grafana/data', 'react'], function (data, React) { |
||||
'use strict'; |
||||
|
||||
const styles = { |
||||
container: 'a-app-body', |
||||
}; |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
return React.createElement( |
||||
'div', |
||||
{ 'data-testid': styles.container, className: 'page-container' }, |
||||
'Hello Grafana!' |
||||
); |
||||
} |
||||
} |
||||
|
||||
const plugin = new data.AppPlugin().setRootPage(RootComponent).configureExtensionLink({ |
||||
title: 'Go to A', |
||||
description: 'Navigating to plugin A', |
||||
extensionPointId: 'plugins/myorg-extensionpoint-app/actions', |
||||
path: '/a/myorg-a-app/', |
||||
}); |
||||
|
||||
return { plugin: plugin }; |
||||
}); |
@ -1,45 +0,0 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "A App", |
||||
"id": "myorg-a-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Will extend root app with ui extensions", |
||||
"author": { |
||||
"name": "Myorg" |
||||
}, |
||||
"logos": { |
||||
"small": "img/logo.svg", |
||||
"large": "img/logo.svg" |
||||
}, |
||||
"screenshots": [], |
||||
"version": "%VERSION%", |
||||
"updated": "%TODAY%" |
||||
}, |
||||
"includes": [ |
||||
{ |
||||
"type": "page", |
||||
"name": "Default", |
||||
"path": "/a/myorg-a-app", |
||||
"role": "Admin", |
||||
"addToNav": false, |
||||
"defaultNav": false |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
}, |
||||
"generated": { |
||||
"extensions": [ |
||||
{ |
||||
"extensionPointId": "plugins/myorg-extensionpoint-app/actions", |
||||
"title": "Go to A", |
||||
"description": "Navigating to pluging A", |
||||
"type": "link" |
||||
} |
||||
] |
||||
} |
||||
} |
@ -1,27 +0,0 @@ |
||||
define(['react', '@grafana/data'], function (React, data) { |
||||
'use strict'; |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
return React.createElement('div', { className: 'page-container' }, 'Hello Grafana!'); |
||||
} |
||||
} |
||||
|
||||
const modalId = 'b-app-modal'; |
||||
|
||||
const plugin = new data.AppPlugin().setRootPage(RootComponent).configureExtensionLink({ |
||||
title: 'Open from B', |
||||
description: 'Open a modal from plugin B', |
||||
extensionPointId: 'plugins/myorg-extensionpoint-app/actions', |
||||
onClick: function (e, { openModal }) { |
||||
openModal({ |
||||
title: 'Modal from app B', |
||||
body: function () { |
||||
return React.createElement('div', { 'data-testid': modalId }, 'From plugin B'); |
||||
}, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
return { plugin: plugin }; |
||||
}); |
@ -1,12 +0,0 @@ |
||||
# App with extensions |
||||
|
||||
This app was initially copied from the [app-with-extensions](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-extensions) example plugin. The plugin bundle is using AMD, but it's not minified and the plugin feature set is small so it should be possible to make changes in this file if necessary. |
||||
|
||||
To test this app: |
||||
|
||||
```sh |
||||
# start e2e test instance (it will install this plugin) |
||||
PORT=3000 ./scripts/grafana-server/start-server |
||||
# run Playwright tests using Playwright VSCode extension or with the following script |
||||
yarn e2e:playwright |
||||
``` |
@ -1,216 +0,0 @@ |
||||
define(['react', '@grafana/data', '@grafana/ui', '@grafana/runtime', '@emotion/css', 'rxjs'], function ( |
||||
React, |
||||
data, |
||||
ui, |
||||
runtime, |
||||
css, |
||||
rxjs |
||||
) { |
||||
'use strict'; |
||||
|
||||
const styles = { |
||||
modalBody: 'ape-modal-body', |
||||
mainPageContainer: 'ape-main-page-container', |
||||
}; |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
return React.createElement( |
||||
'div', |
||||
{ 'data-testid': styles.mainPageContainer, className: 'page-container' }, |
||||
'Hello Grafana!' |
||||
); |
||||
} |
||||
} |
||||
|
||||
const asyncWrapper = (fn) => { |
||||
return function () { |
||||
const gen = fn.apply(this, arguments); |
||||
return new Promise((resolve, reject) => { |
||||
function step(key, arg) { |
||||
let info, value; |
||||
try { |
||||
info = gen[key](arg); |
||||
value = info.value; |
||||
} catch (error) { |
||||
reject(error); |
||||
return; |
||||
} |
||||
if (info.done) { |
||||
resolve(value); |
||||
} else { |
||||
Promise.resolve(value).then(next, throw_); |
||||
} |
||||
} |
||||
function next(value) { |
||||
step('next', value); |
||||
} |
||||
function throw_(value) { |
||||
step('throw', value); |
||||
} |
||||
next(); |
||||
}); |
||||
}; |
||||
}; |
||||
|
||||
const getStyles = (theme) => ({ |
||||
colorWeak: css.css`color: ${theme.colors.text.secondary};`, |
||||
marginTop: css.css`margin-top: ${theme.spacing(3)};`, |
||||
}); |
||||
|
||||
const updatePlugin = asyncWrapper(function* (pluginId, settings) { |
||||
const response = runtime |
||||
.getBackendSrv() |
||||
.fetch({ url: `/api/plugins/${pluginId}/settings`, method: 'POST', data: settings }); |
||||
return rxjs.lastValueFrom(response); |
||||
}); |
||||
|
||||
const handleUpdate = asyncWrapper(function* (pluginId, settings) { |
||||
try { |
||||
yield updatePlugin(pluginId, settings); |
||||
window.location.reload(); |
||||
} catch (error) { |
||||
console.error('Error while updating the plugin', error); |
||||
} |
||||
}); |
||||
|
||||
const configPageBody = ({ plugin }) => { |
||||
const styles = getStyles(ui.useStyles2()); |
||||
const { enabled, jsonData } = plugin.meta; |
||||
return React.createElement( |
||||
'div', |
||||
null, |
||||
React.createElement(ui.Legend, null, 'Enable / Disable '), |
||||
!enabled && |
||||
React.createElement( |
||||
React.Fragment, |
||||
null, |
||||
React.createElement('div', { className: styles.colorWeak }, 'The plugin is currently not enabled.'), |
||||
React.createElement( |
||||
ui.Button, |
||||
{ |
||||
className: styles.marginTop, |
||||
variant: 'primary', |
||||
onClick: () => handleUpdate(plugin.meta.id, { enabled: true, pinned: true, jsonData: jsonData }), |
||||
}, |
||||
'Enable plugin' |
||||
) |
||||
), |
||||
enabled && |
||||
React.createElement( |
||||
React.Fragment, |
||||
null, |
||||
React.createElement('div', { className: styles.colorWeak }, 'The plugin is currently enabled.'), |
||||
React.createElement( |
||||
ui.Button, |
||||
{ |
||||
className: styles.marginTop, |
||||
variant: 'destructive', |
||||
onClick: () => handleUpdate(plugin.meta.id, { enabled: false, pinned: false, jsonData: jsonData }), |
||||
}, |
||||
'Disable plugin' |
||||
) |
||||
) |
||||
); |
||||
}; |
||||
|
||||
const selectQueryModal = ({ targets = [], onDismiss }) => { |
||||
const [selectedQuery, setSelectedQuery] = React.useState(targets[0]); |
||||
return React.createElement( |
||||
'div', |
||||
{ 'data-testid': styles.modalBody }, |
||||
React.createElement( |
||||
'p', |
||||
null, |
||||
'Please select the query you would like to use to create "something" in the plugin.' |
||||
), |
||||
React.createElement( |
||||
ui.HorizontalGroup, |
||||
null, |
||||
targets.map((query) => |
||||
React.createElement(ui.FilterPill, { |
||||
key: query.refId, |
||||
label: query.refId, |
||||
selected: query.refId === (selectedQuery ? selectedQuery.refId : null), |
||||
onClick: () => setSelectedQuery(query), |
||||
}) |
||||
) |
||||
), |
||||
React.createElement( |
||||
ui.Modal.ButtonRow, |
||||
null, |
||||
React.createElement(ui.Button, { variant: 'secondary', fill: 'outline', onClick: onDismiss }, 'Cancel'), |
||||
React.createElement( |
||||
ui.Button, |
||||
{ |
||||
disabled: !Boolean(selectedQuery), |
||||
onClick: () => { |
||||
onDismiss && onDismiss(); |
||||
alert(`You selected query "${selectedQuery.refId}"`); |
||||
}, |
||||
}, |
||||
'OK' |
||||
) |
||||
) |
||||
); |
||||
}; |
||||
|
||||
const plugin = new data.AppPlugin() |
||||
.setRootPage(RootComponent) |
||||
.addConfigPage({ |
||||
title: 'Configuration', |
||||
icon: 'cog', |
||||
body: configPageBody, |
||||
id: 'configuration', |
||||
}) |
||||
.configureExtensionLink({ |
||||
title: 'Open from time series or pie charts (path)', |
||||
description: 'This link will only be visible on time series and pie charts', |
||||
extensionPointId: data.PluginExtensionPoints.DashboardPanelMenu, |
||||
path: `/a/myorg-extensions-app/`, |
||||
configure: (context) => { |
||||
if (context.dashboard?.title === 'Link Extensions (path)') { |
||||
switch (context.pluginId) { |
||||
case 'timeseries': |
||||
return {}; |
||||
case 'piechart': |
||||
return { title: `Open from ${context.pluginId}` }; |
||||
default: |
||||
return; |
||||
} |
||||
} |
||||
}, |
||||
}) |
||||
.configureExtensionLink({ |
||||
title: 'Open from time series or pie charts (onClick)', |
||||
description: 'This link will only be visible on time series and pie charts', |
||||
extensionPointId: data.PluginExtensionPoints.DashboardPanelMenu, |
||||
onClick: (_, { context, openModal }) => { |
||||
const targets = context?.targets || []; |
||||
const title = context?.title; |
||||
if (!targets.length) return; |
||||
if (targets.length > 1) { |
||||
openModal({ |
||||
title: `Select query from "${title}"`, |
||||
body: (props) => React.createElement(selectQueryModal, { ...props, targets: targets }), |
||||
}); |
||||
} else { |
||||
alert(`You selected query "${targets[0].refId}"`); |
||||
} |
||||
}, |
||||
configure: (context) => { |
||||
if (context.dashboard?.title === 'Link Extensions (onClick)') { |
||||
switch (context.pluginId) { |
||||
case 'timeseries': |
||||
return {}; |
||||
case 'piechart': |
||||
return { title: `Open from ${context.pluginId}` }; |
||||
default: |
||||
return; |
||||
} |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
return { plugin: plugin }; |
||||
}); |
@ -1,49 +0,0 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "Extensions App", |
||||
"id": "myorg-extensions-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Example on how to extend grafana ui from a plugin", |
||||
"author": { |
||||
"name": "Myorg" |
||||
}, |
||||
"logos": { |
||||
"small": "img/logo.svg", |
||||
"large": "img/logo.svg" |
||||
}, |
||||
"screenshots": [], |
||||
"version": "1.0.0", |
||||
"updated": "2024-06-11" |
||||
}, |
||||
"includes": [ |
||||
{ |
||||
"type": "page", |
||||
"name": "Default", |
||||
"path": "/a/myorg-extensions-app", |
||||
"role": "Admin", |
||||
"addToNav": true, |
||||
"defaultNav": true |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
}, |
||||
"extensions": [ |
||||
{ |
||||
"extensionPointId": "grafana/dashboard/panel/menu", |
||||
"type": "link", |
||||
"title": "Open from time series or pie charts (path)", |
||||
"description": "This link will only be visible on time series and pie charts" |
||||
}, |
||||
{ |
||||
"extensionPointId": "grafana/dashboard/panel/menu", |
||||
"type": "link", |
||||
"title": "Open from time series or pie charts (onClick)", |
||||
"description": "This link will only be visible on time series and pie charts" |
||||
} |
||||
] |
||||
} |
@ -1,35 +0,0 @@ |
||||
import { test, expect } from '@grafana/plugin-e2e'; |
||||
|
||||
const testIds = { |
||||
container: 'main-app-body', |
||||
actions: { |
||||
button: 'action-button', |
||||
}, |
||||
modal: { |
||||
container: 'container', |
||||
open: 'open-link', |
||||
}, |
||||
appA: { |
||||
container: 'a-app-body', |
||||
}, |
||||
appB: { |
||||
modal: 'b-app-modal', |
||||
}, |
||||
}; |
||||
|
||||
const pluginId = 'myorg-extensionpoint-app'; |
||||
|
||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { |
||||
await page.goto(`/a/${pluginId}/one`); |
||||
await page.getByTestId(testIds.actions.button).click(); |
||||
await page.getByTestId(testIds.container).getByText('Go to A').click(); |
||||
await page.getByTestId(testIds.modal.open).click(); |
||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); |
||||
}); |
||||
|
||||
test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => { |
||||
await page.goto(`/a/${pluginId}/one`); |
||||
await page.getByTestId(testIds.actions.button).click(); |
||||
await page.getByTestId(testIds.container).getByText('Open from B').click(); |
||||
await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); |
||||
}); |
@ -1,38 +0,0 @@ |
||||
import { expect, test } from '@grafana/plugin-e2e'; |
||||
|
||||
const panelTitle = 'Link with defaults'; |
||||
const extensionTitle = 'Open from time series...'; |
||||
const testIds = { |
||||
modal: { |
||||
container: 'ape-modal-body', |
||||
}, |
||||
mainPage: { |
||||
container: 'ape-main-page-container', |
||||
}, |
||||
}; |
||||
|
||||
const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; |
||||
const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; |
||||
|
||||
test('should add link extension (path) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { |
||||
const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); |
||||
const panel = await dashboardPage.getPanelByTitle(panelTitle); |
||||
await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); |
||||
await expect(page.getByTestId(testIds.mainPage.container)).toBeVisible(); |
||||
}); |
||||
|
||||
test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { |
||||
const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); |
||||
const panel = await dashboardPage.getPanelByTitle(panelTitle); |
||||
await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); |
||||
await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); |
||||
}); |
||||
|
||||
test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { |
||||
const panelTitle = 'Link with new name'; |
||||
const extensionTitle = 'Open from piechart'; |
||||
const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); |
||||
const panel = await dashboardPage.getPanelByTitle(panelTitle); |
||||
await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); |
||||
await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); |
||||
}); |
@ -1,9 +0,0 @@ |
||||
import { test, expect } from '@grafana/plugin-e2e'; |
||||
|
||||
const pluginId = 'myorg-componentconsumer-app'; |
||||
const exposedComponentTestId = 'exposed-component'; |
||||
|
||||
test('should display component exposed by another app', async ({ page }) => { |
||||
await page.goto(`/a/${pluginId}`); |
||||
await expect(await page.getByTestId(exposedComponentTestId)).toHaveText('Hello World!'); |
||||
}); |
@ -0,0 +1,33 @@ |
||||
# Test plugins |
||||
|
||||
The [e2e test server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) automatically scans and looks for plugins in this directory. |
||||
|
||||
### To add a new test plugin: |
||||
|
||||
1. If provisioning is required you may update the YAML config file in [`/devenv`](https://github.com/grafana/grafana/tree/main/devenv). |
||||
2. Add the plugin ID to the `allow_loading_unsigned_plugins` setting in the test server's [configuration file](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/custom.ini). |
||||
|
||||
### Building a test plugin with webpack |
||||
|
||||
If you wish to build a test plugin with webpack, you may take a look at how the [grafana-extensionstest-app](./grafana-extensionstest-app/) is wired. A few things to keep in mind: |
||||
|
||||
- the package name needs to be prefixed with `@test-plugins/` |
||||
- extend the webpack config from [`@grafana/plugin-configs`](../../packages/grafana-plugin-configs/) and use custom webpack config to only copy the necessary files (see example [here](./grafana-extensionstest-app/webpack.config.ts)) |
||||
- keep dependency versions in sync with what's in core |
||||
|
||||
#### Local development |
||||
|
||||
1: Install frontend dependencies: |
||||
`yarn install --immutable` |
||||
|
||||
2: Build and watch the core frontend |
||||
`yarn start` |
||||
|
||||
3: Build and watch the test plugins |
||||
`yarn e2e:plugin:build:dev` |
||||
|
||||
4: Build the backend |
||||
`make build-go` |
||||
|
||||
5: Start the Grafana e2e test server with the provisioned test plugin |
||||
`PORT=3000 ./scripts/grafana-server/start-server` |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue