Merge branch 'main' into kristina/rtk-corr

This commit is contained in:
Kristina Durivage
2025-10-23 14:10:23 -05:00
441 changed files with 9779 additions and 3487 deletions
+4 -4
View File
@@ -561,6 +561,7 @@
/packages/grafana-data/src/themes/ @grafana/grafana-frontend-platform
/packages/grafana-data/src/transformations/ @grafana/datapro
/packages/grafana-data/src/types/ @grafana/grafana-frontend-platform
/packages/grafana-data/src/types/scopes.ts @grafana/grafana-operator-experience-squad
/packages/grafana-data/src/utils/__snapshots__/ @grafanabot
/packages/grafana-data/src/utils/anyToNumber.ts @grafana/grafana-frontend-platform
/packages/grafana-data/src/utils/arrayUtils* @grafana/grafana-frontend-platform
@@ -740,6 +741,9 @@
# @grafana/test-utils
/packages/grafana-test-utils @grafana/grafana-frontend-platform
# @grafana/api-clients
/packages/grafana-api-clients/ @grafana/grafana-frontend-platform @grafana/grafana-search-navigate-organise
# root files, mostly frontend
/.browserslistrc @grafana/frontend-ops
/package.json @grafana/frontend-ops
@@ -958,7 +962,6 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
/public/app/features/transformers/timeSeriesTable/ @grafana/dataviz-squad @grafana/app-o11y-visualizations
/public/app/features/users/ @grafana/access-squad
/public/app/features/variables/ @grafana/dashboards-squad
/public/app/features/preferences/ @grafana/grafana-frontend-platform
/public/app/features/bookmarks/ @grafana/grafana-search-navigate-organise
/public/app/plugins/panel/* @grafana/dataviz-squad
/public/app/plugins/panel/alertlist/ @grafana/alerting-frontend
@@ -1054,13 +1057,10 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
/scripts/trigger_windows_build.sh @grafana/grafana-developer-enablement-squad
/scripts/cleanup-husky.sh @grafana/frontend-ops
/scripts/verify-repo-update/ @grafana/grafana-developer-enablement-squad
/scripts/generate-rtk-apis.ts @grafana/grafana-frontend-platform
/scripts/process-specs.ts @grafana/grafana-frontend-platform
/scripts/generate-alerting-rtk-apis.ts @grafana/alerting-frontend
/scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend
/scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend
/scripts/codemods/explicit-barrel-imports.cjs @grafana/frontend-ops
/scripts/rtk-client-generator/ @grafana/grafana-search-navigate-organise
/scripts/codeowners-manifest/ @grafana/dataviz-squad
/scripts/test-coverage-by-codeowner.js @grafana/dataviz-squad
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
pull-requests: read
steps:
- uses: actions/checkout@v5
- uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v1.0.0
- uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v0.2.1
with:
dry-run: true
max-date: "1 month ago"
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
id: detect-changes
uses: ./.github/actions/change-detection
with:
self: .github/workflows/pr-test-integration.yml
self: .github/workflows/pr-test-docker.yml
build-dockerfile:
needs: detect-changes
+2 -1
View File
@@ -101,7 +101,8 @@ jobs:
run: |
set -euo pipefail
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | ./scripts/ci/backend-tests/shard.sh -N"$SHARD" -d-)"
CGO_ENABLED=0 go test -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}"
# ionice since tests are IO intensive
CGO_ENABLED=0 ionice -c2 -n7 go test -p=4 -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}"
mysql:
needs: detect-changes
if: needs.detect-changes.outputs.changed == 'true'
+4 -4
View File
@@ -156,16 +156,16 @@ jobs:
artifacts: targz:grafana:linux/arm/v6,deb:grafana:linux/arm/v6
verify: true
- name: windows-amd64
artifacts: targz:grafana:windows/amd64,zip:grafana:windows/amd64,msi:grafana:windows/amd64
artifacts: targz:grafana:windows/amd64:nocgo,zip:grafana:windows/amd64:nocgo,msi:grafana:windows/amd64:nocgo
verify: true
- name: windows-arm64
artifacts: targz:grafana:windows/arm64,zip:grafana:windows/arm64
artifacts: targz:grafana:windows/arm64:nocgo,zip:grafana:windows/arm64:nocgo
verify: true
- name: darwin-amd64
artifacts: targz:grafana:darwin/amd64
artifacts: targz:grafana:darwin/amd64:nocgo
verify: true
- name: darwin-arm64
artifacts: targz:grafana:darwin/arm64
artifacts: targz:grafana:darwin/arm64:nocgo
verify: true
steps:
- uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -25,6 +25,6 @@ plugins:
path: .yarn/plugins/@yarnpkg/plugin-licenses.cjs
spec: "https://raw.githubusercontent.com/mhassan1/yarn-plugin-licenses/v0.15.0/bundles/@yarnpkg/plugin-licenses.js"
yarnPath: .yarn/releases/yarn-4.9.4.cjs
yarnPath: .yarn/releases/yarn-4.10.3.cjs
enableScripts: false
+6 -6
View File
@@ -69,12 +69,12 @@ func New(cfg app.Config) (app.App, error) {
go func() {
logger := log.WithContext(ctx).With("check", check.ID())
logger.Debug("Processing check", "namespace", req.Object.GetNamespace())
requester, err := identity.GetRequester(ctx)
orgID, err := getOrgIDFromNamespace(req.Object.GetNamespace())
if err != nil {
logger.Error("Error getting requester", "error", err)
logger.Error("Error getting org ID from namespace", "error", err)
return
}
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), requester.GetOrgID())
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), orgID)
err = processCheck(ctx, logger, client, typesClient, req.Object, check)
if err != nil {
logger.Error("Error processing check", "error", err)
@@ -85,12 +85,12 @@ func New(cfg app.Config) (app.App, error) {
go func() {
logger := log.WithContext(ctx).With("check", check.ID())
logger.Debug("Updating check", "namespace", req.Object.GetNamespace(), "name", req.Object.GetName())
requester, err := identity.GetRequester(ctx)
orgID, err := getOrgIDFromNamespace(req.Object.GetNamespace())
if err != nil {
logger.Error("Error getting requester", "error", err)
logger.Error("Error getting org ID from namespace", "error", err)
return
}
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), requester.GetOrgID())
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), orgID)
err = processCheckRetry(ctx, logger, client, typesClient, req.Object, check)
if err != nil {
logger.Error("Error processing check retry", "error", err)
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@@ -91,4 +92,5 @@ type AdvisorAppConfig struct {
CheckRegistry CheckService
PluginConfig map[string]string
StackID string
OrgService org.Service
}
@@ -54,7 +54,13 @@ func New(
}
func (c *check) Items(ctx context.Context) ([]any, error) {
dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{})
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
dss, err := c.DatasourceSvc.GetDataSources(ctx, &datasources.GetDataSourcesQuery{
OrgID: requester.GetOrgID(),
})
if err != nil {
return nil, err
}
@@ -347,7 +347,7 @@ type MockDatasourceSvc struct {
dss []*datasources.DataSource
}
func (m *MockDatasourceSvc) GetAllDataSources(context.Context, *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) {
func (m *MockDatasourceSvc) GetDataSources(context.Context, *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
return m.dss, nil
}
+20 -9
View File
@@ -9,7 +9,7 @@ import (
"github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/resource"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/services/org"
)
const (
@@ -57,15 +57,26 @@ func NewCheckReportFailureWithMoreInfo(
}
}
func GetNamespace(stackID string) (string, error) {
if stackID == "" {
return metav1.NamespaceDefault, nil
func GetNamespaces(ctx context.Context, stackID string, orgService org.Service) ([]string, error) {
var namespaces []string
if stackID != "" {
// Single namespace for cloud stack
stackId, err := strconv.ParseInt(stackID, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid stack id: %s", stackID)
}
namespaces = []string{types.CloudNamespaceFormatter(stackId)}
} else {
// Multiple namespaces for each org
orgs, err := orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return nil, fmt.Errorf("failed to fetch orgs: %w", err)
}
for _, o := range orgs {
namespaces = append(namespaces, types.OrgNamespaceFormatter(o.ID))
}
}
stackId, err := strconv.ParseInt(stackID, 10, 64)
if err != nil {
return "", fmt.Errorf("invalid stack id: %s", stackID)
}
return types.CloudNamespaceFormatter(stackId), nil
return namespaces, nil
}
func GetStatusAnnotation(obj resource.Object) string {
+40 -10
View File
@@ -1,40 +1,61 @@
package checks
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/services/org"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestGetNamespace(t *testing.T) {
func TestGetNamespaces(t *testing.T) {
tests := []struct {
name string
input string
expected string
stackID string
orgs []string
expected []string
expectedErr string
}{
{
name: "empty stack ID",
input: "",
expected: metav1.NamespaceDefault,
stackID: "",
orgs: []string{"default"},
expected: []string{metav1.NamespaceDefault},
},
{
name: "valid stack ID",
input: "1234567890",
expected: "stacks-1234567890",
stackID: "1234567890",
orgs: []string{"default"},
expected: []string{"stacks-1234567890"},
},
{
name: "invalid stack ID",
input: "invalid",
expected: "",
stackID: "invalid",
orgs: []string{"default"},
expected: nil,
expectedErr: "invalid stack id: invalid",
},
{
name: "multiple orgs and no stack ID",
stackID: "",
orgs: []string{"default", "org-2"},
expected: []string{"default", "org-2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GetNamespace(tt.input)
fakeOrgService := &mockOrgService{
SearchFunc: func(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
orgs := make([]*org.OrgDTO, len(tt.orgs))
for i, o := range tt.orgs {
orgs[i] = &org.OrgDTO{ID: int64(i + 1), Name: o}
}
return orgs, nil
},
}
result, err := GetNamespaces(context.Background(), tt.stackID, fakeOrgService)
if tt.expectedErr != "" {
assert.EqualError(t, err, tt.expectedErr)
} else {
@@ -44,3 +65,12 @@ func TestGetNamespace(t *testing.T) {
})
}
}
type mockOrgService struct {
org.Service
SearchFunc func(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error)
}
func (m *mockOrgService) Search(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
return m.SearchFunc(ctx, query)
}
@@ -16,6 +16,7 @@ import (
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/services/org"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -37,8 +38,9 @@ type Runner struct {
typesClient resource.Client
defaultEvalInterval time.Duration
maxHistory int
namespace string
log logging.Logger
orgService org.Service
stackID string
}
// NewRunner creates a new Runner.
@@ -49,6 +51,7 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
return nil, fmt.Errorf("invalid config type")
}
checkRegistry := specificConfig.CheckRegistry
orgService := specificConfig.OrgService
evalInterval, err := getEvaluationInterval(specificConfig.PluginConfig)
if err != nil {
return nil, err
@@ -57,10 +60,6 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
if err != nil {
return nil, err
}
namespace, err := checks.GetNamespace(specificConfig.StackID)
if err != nil {
return nil, err
}
// Prepare storage client
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
@@ -79,8 +78,9 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
typesClient: typesClient,
defaultEvalInterval: evalInterval,
maxHistory: maxHistory,
namespace: namespace,
log: log.With("runner", "advisor.checkscheduler"),
orgService: orgService,
stackID: specificConfig.StackID,
}, nil
}
@@ -89,49 +89,82 @@ func (r *Runner) Run(ctx context.Context) error {
// We still need the context to eventually be cancelled to exit this function
// but we don't want the requests to fail because of it
ctxWithoutCancel := context.WithoutCancel(ctx)
lastCreated, err := r.checkLastCreated(ctxWithoutCancel, logger)
// Determine namespaces based on StackID or OrgID
namespaces, err := checks.GetNamespaces(ctxWithoutCancel, r.stackID, r.orgService)
if err != nil {
return fmt.Errorf("failed to get namespaces: %w", err)
}
logger.Debug("Scheduling checks", "namespaces", len(namespaces))
// Get the last created time for this specific namespace
lastCreatedMap, err := r.checkLastCreated(ctx, logger, namespaces)
if err != nil {
logger.Error("Error getting last check creation time", "error", err)
return err
}
// If there are checks already created, run an initial cleanup to remove old checks
if !lastCreated.IsZero() {
err = r.cleanupChecks(ctxWithoutCancel, logger)
if err != nil {
logger.Error("Error cleaning up old check reports", "error", err)
return err
// If there are checks already created, run an initial cleanup
for _, namespace := range namespaces {
logger = logger.With("namespace", namespace)
lastCreated := lastCreatedMap[namespace]
if !lastCreated.IsZero() {
err = r.cleanupChecks(ctx, logger, namespace)
if err != nil {
logger.Error("Error cleaning up old check reports", "error", err)
return err
}
err = r.markUnprocessedChecks(ctx, logger, namespace)
if err != nil {
logger.Error("Error marking unprocessed checks", "error", err)
return err
}
}
}
nextEvalTime := r.getNextEvalTime(r.defaultEvalInterval, lastCreated)
nextEvalTime := r.getNextEvalTime(r.defaultEvalInterval, lastCreatedMap)
ticker := time.NewTicker(nextEvalTime)
defer ticker.Stop()
for {
select {
case <-ticker.C:
lastCreated, err := r.checkLastCreated(ctxWithoutCancel, logger)
// Get the current last created time for this namespace
lastCreatedMap, err := r.checkLastCreated(ctx, logger, namespaces)
if err != nil {
logger.Error("Error getting last check creation time", "error", err)
return err
}
// If there are checks already created, then we can automatically create more
if !lastCreated.IsZero() {
err = r.createChecks(ctxWithoutCancel, logger)
if err != nil {
logger.Error("Error creating new check reports", "error", err)
}
for _, namespace := range namespaces {
logger = logger.With("namespace", namespace)
lastCreated := lastCreatedMap[namespace]
// Clean up old checks to avoid going over the limit
err = r.cleanupChecks(ctxWithoutCancel, logger)
if err != nil {
logger.Error("Error cleaning up old check reports", "error", err)
// If there are checks already created and they are older than the evaluation interval
// then we can automatically create more
if !lastCreated.IsZero() && lastCreated.Before(time.Now().Add(-r.defaultEvalInterval)) {
err = r.createChecks(ctx, logger, namespace)
if err != nil {
logger.Error("Error creating new check reports", "error", err)
return err
}
// Clean up old checks to avoid going over the limit
err = r.cleanupChecks(ctx, logger, namespace)
if err != nil {
logger.Error("Error cleaning up old check reports", "error", err)
return err
}
// Update the last created time with the new created checks
lastCreatedMap[namespace] = time.Now()
}
}
// Reset the ticker to the next send interval
nextEvalTime = r.getNextEvalTime(r.defaultEvalInterval, lastCreated)
nextEvalTime = r.getNextEvalTime(r.defaultEvalInterval, lastCreatedMap)
ticker.Reset(nextEvalTime)
case <-ctx.Done():
return ctx.Err()
@@ -139,8 +172,8 @@ func (r *Runner) Run(ctx context.Context) error {
}
}
func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resource.Object, error) {
list, err := r.checksClient.List(ctx, r.namespace, resource.ListOptions{
func (r *Runner) listChecks(ctx context.Context, logger logging.Logger, namespace string) ([]resource.Object, error) {
list, err := r.checksClient.List(ctx, namespace, resource.ListOptions{
Limit: 1000, // Avoid pagination for normal uses cases, which is a costly operation
})
if err != nil {
@@ -150,7 +183,7 @@ func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resou
checks := list.GetItems()
for list.GetContinue() != "" {
logger.Debug("List has continue token, listing next page", "continue", list.GetContinue())
list, err = r.checksClient.List(ctx, r.namespace, resource.ListOptions{Continue: list.GetContinue(), Limit: 1000})
list, err = r.checksClient.List(ctx, namespace, resource.ListOptions{Continue: list.GetContinue(), Limit: 1000})
if err != nil {
return nil, err
}
@@ -159,38 +192,48 @@ func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resou
return checks, nil
}
// checkLastCreated returns the creation time of the last check created
// regardless of its ID. This assumes that the checks are created in batches
// so a batch will have a similar creation time.
// checkLastCreated returns the creation time of the last check created for a specific namespace.
// This assumes that the checks are created in batches so a batch will have a similar creation time.
// In case it finds an unprocessed check from a previous run, it will set it to error.
func (r *Runner) checkLastCreated(ctx context.Context, log logging.Logger) (time.Time, error) {
checkList, err := r.listChecks(ctx, log)
if err != nil {
return time.Time{}, err
}
lastCreated := time.Time{}
for _, item := range checkList {
itemCreated := item.GetCreationTimestamp().Time
if itemCreated.After(lastCreated) {
lastCreated = itemCreated
func (r *Runner) checkLastCreated(ctx context.Context, log logging.Logger, namespaces []string) (map[string]time.Time, error) {
lastCreated := map[string]time.Time{}
for _, namespace := range namespaces {
checkList, err := r.listChecks(ctx, log, namespace)
if err != nil {
return nil, err
}
// If the check is unprocessed, set it to error
if checks.GetStatusAnnotation(item) == "" {
log.Info("Check is unprocessed, marking as error", "check", item.GetStaticMetadata().Identifier())
err := checks.SetStatusAnnotation(ctx, r.checksClient, item, checks.StatusAnnotationError)
if err != nil {
log.Error("Error setting check status to error", "error", err)
for _, item := range checkList {
itemCreated := item.GetCreationTimestamp().Time
if itemCreated.After(lastCreated[namespace]) {
lastCreated[namespace] = itemCreated
}
}
}
return lastCreated, nil
}
func (r *Runner) markUnprocessedChecks(ctx context.Context, log logging.Logger, namespace string) error {
checkList, err := r.listChecks(ctx, log, namespace)
if err != nil {
return err
}
for _, item := range checkList {
if checks.GetStatusAnnotation(item) == "" {
log.Info("Check is unprocessed, marking as error", "check", item.GetStaticMetadata().Identifier())
err := checks.SetStatusAnnotation(ctx, r.checksClient, item, checks.StatusAnnotationError)
if err != nil {
log.Error("Error setting check status to error", "error", err)
return err
}
}
}
return nil
}
// createChecks creates a new check for each check type in the registry.
func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error {
func (r *Runner) createChecks(ctx context.Context, logger logging.Logger, namespace string) error {
// List existing CheckType objects
list, err := r.typesClient.List(ctx, r.namespace, resource.ListOptions{})
list, err := r.typesClient.List(ctx, namespace, resource.ListOptions{})
if err != nil {
return fmt.Errorf("error listing check types: %w", err)
}
@@ -200,7 +243,7 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error
for !allChecksRegistered && retryCount < waitMaxRetries {
logger.Info("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{})
list, err = r.typesClient.List(ctx, namespace, resource.ListOptions{})
if err != nil {
return fmt.Errorf("error listing check types: %w", err)
}
@@ -218,7 +261,7 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error
obj := &advisorv0alpha1.Check{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "check-",
Namespace: r.namespace,
Namespace: namespace,
Labels: map[string]string{
checks.TypeLabel: checkType.Spec.Name,
},
@@ -235,13 +278,13 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error
}
// cleanupChecks deletes the olders checks if the number of checks exceeds the limit.
func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger) error {
checkList, err := r.listChecks(ctx, logger)
func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger, namespace string) error {
checkList, err := r.listChecks(ctx, logger, namespace)
if err != nil {
return err
}
logger.Debug("Cleaning up checks", "numChecks", len(checkList))
logger.Debug("Cleaning up checks", "namespace", namespace, "numChecks", len(checkList))
// organize checks by type
checksByType := map[string][]resource.Object{}
@@ -294,12 +337,15 @@ func getEvaluationInterval(pluginConfig map[string]string) (time.Duration, error
return evaluationInterval, nil
}
func (r *Runner) getNextEvalTime(defaultEvaluationInterval time.Duration, lastCreated time.Time) time.Duration {
func (r *Runner) getNextEvalTime(defaultEvaluationInterval time.Duration, lastCreated map[string]time.Time) time.Duration {
nextEvalTime := defaultEvaluationInterval
baseTime := lastCreated
if lastCreated.IsZero() {
baseTime = time.Now()
// Get the oldest last created time
baseTime := time.Now()
for _, lastNamespacedCreated := range lastCreated {
if !lastNamespacedCreated.IsZero() && lastNamespacedCreated.Before(baseTime) {
baseTime = lastNamespacedCreated
}
}
// Calculate the next evaluation time and add random variation
@@ -12,6 +12,7 @@ import (
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/services/org"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -212,7 +213,8 @@ func TestRunner_Run_UnprocessedChecks(t *testing.T) {
Items: []advisorv0alpha1.Check{
{
ObjectMeta: metav1.ObjectMeta{
Name: "unprocessed-check",
Name: "unprocessed-check",
CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)),
// No status annotation - unprocessed
},
},
@@ -331,8 +333,9 @@ func createTestRunnerWithRegistry(checkClient, typesClient *MockClient, checkReg
typesClient: typesClient,
defaultEvalInterval: 5 * time.Millisecond,
maxHistory: defaultMaxHistory,
namespace: "test-namespace",
log: &logging.NoOpLogger{},
orgService: &mockOrgService{orgs: []*org.OrgDTO{{ID: 1}}},
stackID: "",
}
}
@@ -383,3 +386,12 @@ func (m *mockCheck) ID() string {
func (m *mockCheck) Steps() []checks.Step {
return m.steps
}
type mockOrgService struct {
org.Service
orgs []*org.OrgDTO
}
func (m *mockOrgService) Search(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
return m.orgs, nil
}
@@ -16,6 +16,7 @@ import (
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/services/org"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -26,7 +27,8 @@ import (
type Runner struct {
checkRegistry checkregistry.CheckService
client resource.Client
namespace string
orgService org.Service
stackID string
log logging.Logger
retryAttempts int
retryDelay time.Duration
@@ -40,10 +42,7 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
return nil, fmt.Errorf("invalid config type")
}
checkRegistry := specificConfig.CheckRegistry
namespace, err := checks.GetNamespace(specificConfig.StackID)
if err != nil {
return nil, err
}
orgService := specificConfig.OrgService
// Prepare storage client
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
@@ -55,7 +54,8 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
return &Runner{
checkRegistry: checkRegistry,
client: client,
namespace: namespace,
orgService: orgService,
stackID: specificConfig.StackID,
log: log.With("runner", "advisor.checktyperegisterer"),
retryAttempts: 5,
retryDelay: time.Second * 10,
@@ -64,36 +64,47 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
func (r *Runner) Run(ctx context.Context) error {
logger := r.log.WithContext(ctx)
for _, t := range r.checkRegistry.Checks() {
steps := t.Steps()
stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps))
for i, s := range steps {
stepTypes[i] = advisorv0alpha1.CheckTypeStep{
Title: s.Title(),
Description: s.Description(),
StepID: s.ID(),
Resolution: s.Resolution(),
// Determine namespaces based on StackID or OrgID
namespaces, err := checks.GetNamespaces(ctx, r.stackID, r.orgService)
if err != nil {
return fmt.Errorf("failed to get namespaces: %w", err)
}
logger.Debug("Registering check types", "namespaces", len(namespaces))
// Register check types in each namespace
for _, namespace := range namespaces {
for _, t := range r.checkRegistry.Checks() {
steps := t.Steps()
stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps))
for i, s := range steps {
stepTypes[i] = advisorv0alpha1.CheckTypeStep{
Title: s.Title(),
Description: s.Description(),
StepID: s.ID(),
Resolution: s.Resolution(),
}
}
}
obj := &advisorv0alpha1.CheckType{
ObjectMeta: metav1.ObjectMeta{
Name: t.ID(),
Namespace: r.namespace,
Annotations: map[string]string{
checks.NameAnnotation: t.Name(),
// Flag to indicate feature availability
checks.RetryAnnotation: "1",
checks.IgnoreStepsAnnotation: "1",
obj := &advisorv0alpha1.CheckType{
ObjectMeta: metav1.ObjectMeta{
Name: t.ID(),
Namespace: namespace,
Annotations: map[string]string{
checks.NameAnnotation: t.Name(),
// Flag to indicate feature availability
checks.RetryAnnotation: "1",
checks.IgnoreStepsAnnotation: "1",
},
},
},
Spec: advisorv0alpha1.CheckTypeSpec{
Name: t.ID(),
Steps: stepTypes,
},
}
err := r.registerCheckType(ctx, logger, t.ID(), obj)
if err != nil {
return err
Spec: advisorv0alpha1.CheckTypeSpec{
Name: t.ID(),
Steps: stepTypes,
},
}
err := r.registerCheckType(ctx, logger, t.ID(), obj)
if err != nil {
return err
}
}
}
return nil
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana-app-sdk/resource"
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/services/org"
k8sErrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -67,14 +68,17 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
tests := []struct {
name string
checks []checks.Check
stackID string
orgService org.Service
getFunc func(ctx context.Context, id resource.Identifier) (resource.Object, error)
createFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error)
updateFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error)
expectedErr error
}{
{
name: "successful create",
checks: []checks.Check{newMockCheck},
name: "successful create",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
},
@@ -85,8 +89,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: nil,
},
{
name: "resource exists with different annotations, should update",
checks: []checks.Check{newMockCheck},
name: "resource exists with different annotations, should update",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectDifferentAnnotations, nil
},
@@ -96,8 +101,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: nil,
},
{
name: "resource exists with different steps, should update",
checks: []checks.Check{newMockCheck},
name: "resource exists with different steps, should update",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectDifferentSteps, nil
},
@@ -107,8 +113,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: nil,
},
{
name: "resource exists with same annotations and steps, should not update",
checks: []checks.Check{newMockCheck},
name: "resource exists with same annotations and steps, should not update",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectSameContent, nil
},
@@ -118,8 +125,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: nil,
},
{
name: "resource exists, with custom annotations preserved",
checks: []checks.Check{newMockCheck},
name: "resource exists, with custom annotations preserved",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectDifferentAnnotations, nil
},
@@ -132,8 +140,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: nil,
},
{
name: "create error",
checks: []checks.Check{newMockCheck},
name: "create error",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
},
@@ -144,8 +153,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: errors.New("create error"),
},
{
name: "update error",
checks: []checks.Check{newMockCheck},
name: "update error",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectDifferentAnnotations, nil
},
@@ -155,8 +165,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: errors.New("update error"),
},
{
name: "shutting down error",
checks: []checks.Check{newMockCheck},
name: "shutting down error",
checks: []checks.Check{newMockCheck},
stackID: "123",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectDifferentAnnotations, nil
},
@@ -166,14 +177,33 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
expectedErr: nil,
},
{
name: "custom namespace",
checks: []checks.Check{newMockCheck},
name: "cloud stack namespace",
checks: []checks.Check{newMockCheck},
stackID: "456",
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return existingObjectDifferentAnnotations, nil
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
},
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
if obj.GetNamespace() != "custom-namespace" {
return nil, fmt.Errorf("expected namespace %s, got %s", "custom-namespace", obj.GetNamespace())
if obj.GetNamespace() != "stack-456" {
return nil, fmt.Errorf("expected namespace %s, got %s", "stack-456", obj.GetNamespace())
}
return obj, nil
},
expectedErr: nil,
},
{
name: "multiple orgs",
checks: []checks.Check{newMockCheck},
stackID: "",
orgService: &mockOrgService{orgs: []*org.OrgDTO{{ID: 1, Name: "Org1"}, {ID: 2, Name: "Org2"}}},
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
},
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
// Should create in both org-1 and org-2 namespaces
ns := obj.GetNamespace()
if ns != "org-1" && ns != "org-2" {
return nil, fmt.Errorf("expected namespace org-1 or org-2, got %s", ns)
}
return obj, nil
},
@@ -183,6 +213,10 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orgSvc := tt.orgService
if orgSvc == nil {
orgSvc = &mockOrgService{orgs: []*org.OrgDTO{}}
}
r := &Runner{
checkRegistry: &mockCheckRegistry{checks: tt.checks},
client: &mockClient{
@@ -190,7 +224,8 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
createFunc: tt.createFunc,
updateFunc: tt.updateFunc,
},
namespace: "custom-namespace",
orgService: orgSvc,
stackID: tt.stackID,
log: logging.DefaultLogger,
retryAttempts: 1,
retryDelay: 0,
@@ -298,3 +333,12 @@ func (m *mockClient) Update(ctx context.Context, id resource.Identifier, obj res
}
return nil, errors.New("not implemented")
}
type mockOrgService struct {
org.Service
orgs []*org.OrgDTO
}
func (m *mockOrgService) Search(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
return m.orgs, nil
}
+10
View File
@@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/resource"
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
@@ -317,3 +318,12 @@ func waitForRetryAnnotation(ctx context.Context, log logging.Logger, client reso
log.Debug("Retry annotation persisted", "check", obj.GetName(), "item", itemToRetry)
return nil
}
// getOrgIDFromNamespace extracts the org ID from a namespace using the standard authlib parser.
func getOrgIDFromNamespace(namespace string) (int64, error) {
info, err := types.ParseNamespace(namespace)
if err != nil {
return 0, fmt.Errorf("failed to parse namespace %s: %w", namespace, err)
}
return info.OrgID, nil
}
+1 -1
View File
@@ -6,7 +6,7 @@ UserSpec: {
emailVerified: bool
grafanaAdmin: bool
login: string
name: string
title: string
provisioned: bool
role: string
}
@@ -9,7 +9,7 @@ type UserSpec struct {
EmailVerified bool `json:"emailVerified"`
GrafanaAdmin bool `json:"grafanaAdmin"`
Login string `json:"login"`
Name string `json:"name"`
Title string `json:"title"`
Provisioned bool `json:"provisioned"`
Role string `json:"role"`
}
@@ -2591,7 +2591,7 @@ func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common.
Format: "",
},
},
"name": {
"title": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
@@ -2613,7 +2613,7 @@ func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common.
},
},
},
Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "name", "provisioned", "role"},
Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "title", "provisioned", "role"},
},
},
}
+2 -1
View File
@@ -150,7 +150,8 @@ When you log in for the first time, Grafana asks you to change your password.
#### Build on Windows
The Grafana backend includes SQLite, a database which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). Eventually, if you use [Scoop](https://scoop.sh), you can install GCC through that.
The Grafana backend includes SQLite, a database which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC with binutils version 2.37 or later.
We recommend [MinGW x64](https://www.mingw-w64.org/downloads). Eventually, if you use [Scoop](https://scoop.sh), you can install GCC through that.
You can build the back-end as follows:
@@ -548,12 +548,11 @@ The following table outlines the various Entra ID configuration options. You can
| `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | |
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` |
| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` |
| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
@@ -248,7 +248,7 @@ IdP-initiated SSO has some security risks, so make sure you understand the risks
For advanced configuration and troubleshooting, please refer to the one of the following pages:
- [Configure SAML request signing](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-request-signing/)
- [Configure SAML signing and encryption](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-signing-encryption/)
- [Configure SAML single logout](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-single-logout/)
- [Configure Organization mapping](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-org-mapping/)
- [Configure Role and Team sync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/)
@@ -68,7 +68,7 @@ Sign in to Grafana and navigate to **Administration > Authentication > Configure
Use the [PKCS #8](https://en.wikipedia.org/wiki/PKCS_8) format to issue the private key.
For more information, refer to an [example on how to generate SAML credentials](../configure-saml-request-signing/#example-of-private-key-generation-for-saml-authentication).
For more information, refer to an [example on how to generate SAML credentials](../configure-saml-signing-encryption/#example-of-private-key-generation-for-saml-authentication).
Alternatively, you can generate a new private key and certificate pair directly from the UI. Click on the `Generate key and certificate` button to open a form where you enter some information you want to be embedded into the new certificate.
@@ -48,10 +48,17 @@ test.describe(
}) => {
// Handle query and query_range API calls
await page.route(/\/api\/ds\/query/, async (route) => {
const fixture = require('../fixtures/prometheus-response.json');
// during the test, we select the "inner_eval" slice to filter; this simulates the behavior
// of prometheus applying that filter and removing dataframes from the response.
if (route.request().postData()?.includes('{slice=\\\"inner_eval\\\"}')) {
delete fixture.results.A.frames[1];
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(require('../fixtures/prometheus-response.json')),
body: JSON.stringify(fixture),
});
});
@@ -73,6 +80,11 @@ test.describe(
const labelValue = await labelValueCell.textContent();
expect(labelValue).toBeTruthy();
const otherValueCell = await getCell(panel, 2, 1);
const otherValueLabel = await otherValueCell.textContent();
expect(otherValueLabel).toBeTruthy();
expect(otherValueLabel).not.toBe(labelValue);
// Hover over the first cell to trigger the appearance of filter actions
await labelValueCell.hover();
@@ -92,6 +104,9 @@ test.describe(
const hasFilterValue = await submenuItems.filter({ hasText: labelValue! }).count();
expect(hasFilterValue).toBeGreaterThan(0);
const hasOtherValue = await submenuItems.filter({ hasText: otherValueLabel! }).count();
expect(hasOtherValue).toBe(0);
// Check if the URL contains the var-PromAdHoc parameter with the filtered value
const currentUrl = page.url();
expect(currentUrl).toContain('var-PromAdHoc');
@@ -101,6 +116,10 @@ test.describe(
const promAdHocParam = urlParams.get('var-PromAdHoc');
expect(promAdHocParam).toBeTruthy();
expect(promAdHocParam).toContain(labelValue!);
expect(promAdHocParam).not.toContain(otherValueLabel!);
// finally, let's check that the table was updated and that the value was filtered out when the query was re-run
await expect(otherValueCell).toBeHidden();
});
}
);
+14 -1
View File
@@ -55,7 +55,20 @@
]
}
},
"overrides": []
"overrides": [
{
"matcher": {
"id": "byName",
"options": "slice"
},
"properties": [
{
"id": "displayName",
"value": "Slice"
}
]
}
]
},
"gridPos": {
"h": 8,
-155
View File
@@ -1194,11 +1194,6 @@
"count": 1
}
},
"public/app/core/components/AccessControl/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/core/components/DynamicImports/SafeDynamicImport.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -1462,11 +1457,6 @@
"count": 1
}
},
"public/app/core/utils/richHistory.ts": {
"no-barrel-files/no-barrel-files": {
"count": 3
}
},
"public/app/core/utils/ticks.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 2
@@ -1993,11 +1983,6 @@
"count": 1
}
},
"public/app/features/auth-config/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/auth-config/utils/data.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -2038,21 +2023,6 @@
"count": 1
}
},
"public/app/features/connections/components/ConnectionsRedirectNotice/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/connections/tabs/ConnectData/CardGrid/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/connections/tabs/ConnectData/CategoryHeader/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/connections/tabs/ConnectData/ConnectData.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -2064,21 +2034,6 @@
"count": 2
}
},
"public/app/features/connections/tabs/ConnectData/NoAccessModal/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/connections/tabs/ConnectData/Search/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/connections/tabs/ConnectData/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/correlations/CorrelationsPage.tsx": {
"no-restricted-syntax": {
"count": 1
@@ -2425,11 +2380,6 @@
"count": 3
}
},
"public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx": {
"no-restricted-syntax": {
"count": 7
@@ -2448,21 +2398,11 @@
"count": 7
}
},
"public/app/features/dashboard/components/DashExportModal/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx": {
"react-prefer-function-component/react-prefer-function-component": {
"count": 1
}
},
"public/app/features/dashboard/components/DashNav/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.test.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -2486,11 +2426,6 @@
"count": 1
}
},
"public/app/features/dashboard/components/DashboardRow/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx": {
"no-restricted-syntax": {
"count": 1
@@ -2514,11 +2449,6 @@
"count": 1
}
},
"public/app/features/dashboard/components/DashboardSettings/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx": {
"no-restricted-syntax": {
"count": 3
@@ -3037,11 +2967,6 @@
"count": 1
}
},
"public/app/features/explore/TraceView/components/TracePageHeader/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx": {
"react-prefer-function-component/react-prefer-function-component": {
"count": 1
@@ -3082,11 +3007,6 @@
"count": 1
}
},
"public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx": {
"react-prefer-function-component/react-prefer-function-component": {
"count": 1
@@ -3097,11 +3017,6 @@
"count": 1
}
},
"public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/explore/TraceView/components/common/SearchBarInput.tsx": {
"react-prefer-function-component/react-prefer-function-component": {
"count": 1
@@ -3112,11 +3027,6 @@
"count": 1
}
},
"public/app/features/explore/TraceView/components/model/ddg/types.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/explore/TraceView/components/model/link-patterns.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 2
@@ -3145,11 +3055,6 @@
"count": 1
}
},
"public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/explore/TraceView/components/utils/sort.ts": {
"no-restricted-syntax": {
"count": 1
@@ -3338,11 +3243,6 @@
"count": 4
}
},
"public/app/features/migrate-to-cloud/api/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/CreateTokenModal.tsx": {
"no-restricted-syntax": {
"count": 1
@@ -3426,11 +3326,6 @@
"count": 1
}
},
"public/app/features/plugins/admin/components/GetStartedWithPlugin/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx": {
"react/no-unescaped-entities": {
"count": 1
@@ -3947,11 +3842,6 @@
"count": 1
}
},
"public/app/features/variables/pickers/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/variables/pickers/shared/VariableInput.tsx": {
"no-restricted-syntax": {
"count": 1
@@ -4084,11 +3974,6 @@
"count": 2
}
},
"public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx": {
"no-restricted-syntax": {
"count": 5
@@ -4124,41 +4009,16 @@
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/ResourceField/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/ResourcePicker/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx": {
"no-restricted-syntax": {
"count": 7
@@ -4258,21 +4118,11 @@
"count": 2
}
},
"public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LegacyLogGroupNamesSelection.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/plugins/datasource/cloudwatch/components/shared/MetricStatEditor/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/datasource/cloudwatch/datasource.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -5108,11 +4958,6 @@
"count": 1
}
},
"public/app/plugins/panel/nodeGraph/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/plugins/panel/nodeGraph/layout.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
+34 -10
View File
@@ -1,17 +1,18 @@
const fs = require('fs');
const open = require('open').default;
const path = require('path');
const baseConfig = require('./jest.config.js');
const CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH = 'codeowners-manifest/filenames-by-team.json';
const teamName = process.env.TEAM_NAME;
if (!teamName) {
console.error('ERROR: TEAM_NAME environment variable is required');
const codeownerName = process.env.CODEOWNER_NAME;
if (!codeownerName) {
console.error('ERROR: CODEOWNER_NAME environment variable is required');
process.exit(1);
}
const outputDir = `./coverage/by-team/${createOwnerDirectory(teamName)}`;
const outputDir = `./coverage/by-team/${createOwnerDirectory(codeownerName)}`;
const codeownersFilePath = path.join(__dirname, CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH);
@@ -22,10 +23,10 @@ if (!fs.existsSync(codeownersFilePath)) {
}
const codeownersData = JSON.parse(fs.readFileSync(codeownersFilePath, 'utf8'));
const teamFiles = codeownersData[teamName] || [];
const teamFiles = codeownersData[codeownerName] || [];
if (teamFiles.length === 0) {
console.error(`ERROR: No files found for team "${teamName}"`);
console.error(`ERROR: No files found for team "${codeownerName}"`);
console.error('Available teams:', Object.keys(codeownersData).join(', '));
process.exit(1);
}
@@ -34,10 +35,15 @@ const sourceFiles = teamFiles.filter((file) => {
const ext = path.extname(file);
return (
['.ts', '.tsx', '.js', '.jsx'].includes(ext) &&
// exclude all tests
!path.matchesGlob(file, '**/test/**/*') &&
!file.includes('.test.') &&
!file.includes('.spec.') &&
// and storybook stories
!file.includes('.story.') &&
// and generated files
!file.includes('.gen.ts') &&
// and type definitions
!file.includes('.d.ts') &&
!file.endsWith('/types.ts')
);
@@ -49,12 +55,12 @@ const testFiles = teamFiles.filter((file) => {
});
if (testFiles.length === 0) {
console.log(`No test files found for team ${teamName}`);
console.log(`No test files found for team ${codeownerName}`);
process.exit(0);
}
console.log(
`🧪 Collecting coverage for ${sourceFiles.length} testable files and running ${testFiles.length} test files of ${teamFiles.length} files owned by ${teamName}.`
`🧪 Collecting coverage for ${sourceFiles.length} testable files and running ${testFiles.length} test files of ${teamFiles.length} files owned by ${codeownerName}.`
);
module.exports = {
@@ -71,7 +77,7 @@ module.exports = {
[
'jest-monocart-coverage',
{
name: `Coverage Report - ${teamName} owned files`,
name: `Coverage Report - ${codeownerName} owned files`,
outputDir: outputDir,
reports: ['console-summary', 'v8', 'json', 'lcov'],
sourceFilter: (coveredFile) => sourceFiles.includes(coveredFile),
@@ -84,7 +90,13 @@ module.exports = {
},
cleanCache: true,
onEnd: (coverageResults) => {
console.log(`📄 Coverage report saved to file://${path.resolve(outputDir)}/index.html`);
const reportURL = `file://${path.resolve(outputDir)}/index.html`;
console.log(`📄 Coverage report saved to ${reportURL}`);
if (process.env.SHOULD_OPEN_COVERAGE_REPORT === 'true') {
openCoverageReport(reportURL);
}
// TODO: Emit coverage metrics https://github.com/grafana/grafana/issues/111208
},
},
@@ -115,3 +127,15 @@ function createOwnerDirectory(owner) {
return `emails/${user}-at-${domain}`;
}
}
/**
* Open the given file URL in the default browser safely, without shell injection risk.
* @param {string} reportURL
*/
async function openCoverageReport(reportURL) {
try {
await open(reportURL);
} catch (err) {
console.error(`Failed to open coverage report: ${err}`);
}
}
+8 -7
View File
@@ -36,8 +36,8 @@
"test": "jest --notify --watch",
"test:coverage": "jest --coverage",
"test:coverage:by-codeowner": "yarn codeowners-manifest && node scripts/test-coverage-by-codeowner.js",
"test:storybook": "yarn workspace @grafana/ui storybook:test",
"test:coverage:changes": "jest --coverage --changedSince=origin/main",
"test:storybook": "yarn workspace @grafana/ui storybook:test",
"test:accessibility-report": "./scripts/generate-a11y-report.sh",
"test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%} --shard=${TEST_SHARD:-1}/${TEST_SHARD_TOTAL:-1}",
"lint": "yarn run lint:ts && yarn run lint:sass",
@@ -75,9 +75,8 @@
"plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100",
"plugin:test:ci": "nx run-many -t test:ci --projects='tag:scope:plugin' --maxParallel=2",
"plugin:i18n-extract": "nx run-many -t i18n-extract --projects='tag:scope:plugin'",
"process-specs": "node --experimental-strip-types scripts/process-specs.ts",
"generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts",
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types --disable-warning=ExperimentalWarning' plop --plopfile scripts/rtk-client-generator/plopfile.ts"
"generate-apis": "yarn workspace @grafana/api-clients generate-apis",
"generate:api-client": "yarn workspace @grafana/api-clients generate:api-client"
},
"grafana": {
"whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v%[1]s-%[2]s/",
@@ -183,6 +182,7 @@
"cypress": "14.3.2",
"cypress-file-upload": "5.0.8",
"cypress-recurse": "^1.35.3",
"enquirer": "^2.4.1",
"esbuild": "0.25.8",
"esbuild-loader": "4.3.0",
"esbuild-plugin-browserslist": "^1.0.0",
@@ -229,10 +229,10 @@
"mutationobserver-shim": "0.3.7",
"node-notifier": "10.0.1",
"nx": "21.3.11",
"openapi-types": "^12.1.3",
"open": "^10.2.0",
"ora": "^9.0.0",
"pa11y-ci": "^4.0.0",
"pdf-parse": "^1.1.1",
"plop": "^4.0.1",
"postcss": "8.5.6",
"postcss-loader": "8.1.1",
"postcss-reporter": "7.1.0",
@@ -277,6 +277,7 @@
"@formatjs/intl-durationformat": "^0.7.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/alerting": "workspace:*",
"@grafana/api-clients": "workspace:*",
"@grafana/assistant": "0.1.0",
"@grafana/aws-sdk": "0.7.1",
"@grafana/azure-sdk": "0.0.7",
@@ -468,7 +469,7 @@
"engines": {
"node": ">= 22 <23"
},
"packageManager": "yarn@4.9.4",
"packageManager": "yarn@4.10.3",
"dependenciesMeta": {
"cypress": {
"built": true
+1 -1
View File
@@ -20,7 +20,7 @@ All packages are versioned according to the current Grafana version:
- Grafana v6.2.5 -> @grafana/\* packages @ 6.2.5
- Grafana - main branch version (based on package.json, i.e. 6.4.0-pre) -> @grafana/\* packages @ 6.4.0-pre-<COMMIT-SHA> (see details below about packages publishing channels)
> Please note that @grafana/ui, @grafana/data, and @grafana/runtime packages are considered ALPHA even though they are not released as alpha versions.
> Please note that the @grafana/api-clients package is considered ALPHA even though it is not released as an alpha version.
### Stable releases
@@ -0,0 +1,7 @@
# Changelog
All notable changes to this project will be documented in this file.
## Unreleased
- Initial scaffold
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Grafana Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+32
View File
@@ -0,0 +1,32 @@
# Grafana API Clients
> **@grafana/api-clients is currently in BETA**.
This package hosts reusable clients for Grafana APIs. The implementation is currently in BETA and not yet published.
## Generating RTK Query API Clients
This guide explains how to generate API clients for Grafana APIs using this package.
### 1. Generate an OpenAPI snapshot
First, check if the `group` and the `version` are already present in [openapi_test.go](../../pkg/tests/apis/openapi_test.go). If so, move on to the next step. If you need to add a new block, you can check for the right `group` and `version` in the backend API call that you want to replicate in the frontend.
```go
{
Group: "dashboard.grafana.app",
Version: "v0alpha1",
}
```
Afterwards, you need to run the `TestIntegrationOpenAPIs` test. Note that it will fail the first time you run it. On the second run, it will generate the corresponding OpenAPI spec, which you can find in [openapi_snapshots](../../pkg/tests/apis/openapi_snapshots).
> Note: You don't need to follow these two steps if the `group` you're working with is already in the `openapi_test.go` file.
### 2. Run the API generator script
Run `yarn generate:api-client` and follow the prompts. See [API Client Generator](./src/generator/README.md) for details.
## Updating generated clients
To update the existing clients, for example, when the OpenAPI spec has changed, run `yarn generate-apis`. This will regenerate all the clients based on the current OpenAPI snapshots.
+111
View File
@@ -0,0 +1,111 @@
{
"author": "Grafana Labs",
"license": "Apache-2.0",
"private": true,
"name": "@grafana/api-clients",
"version": "12.3.0-pre",
"description": "Grafana API client utilities",
"keywords": [
"grafana",
"typescript"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/grafana/grafana.git",
"directory": "packages/grafana-api-clients"
},
"main": "src/index.ts",
"module": "src/index.ts",
"types": "src/index.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
},
"./rtkq": {
"import": "./src/clients/rtkq/index.ts",
"require": "./src/clients/rtkq/index.ts"
},
"./rtkq/advisor/v0alpha1": {
"import": "./src/clients/rtkq/advisor/v0alpha1/index.ts",
"require": "./src/clients/rtkq/advisor/v0alpha1/index.ts"
},
"./rtkq/dashboard/v0alpha1": {
"import": "./src/clients/rtkq/dashboard/v0alpha1/index.ts",
"require": "./src/clients/rtkq/dashboard/v0alpha1/index.ts"
},
"./rtkq/folder/v1beta1": {
"import": "./src/clients/rtkq/folder/v1beta1/index.ts",
"require": "./src/clients/rtkq/folder/v1beta1/index.ts"
},
"./rtkq/iam/v0alpha1": {
"import": "./src/clients/rtkq/iam/v0alpha1/index.ts",
"require": "./src/clients/rtkq/iam/v0alpha1/index.ts"
},
"./rtkq/legacy/migrate-to-cloud": {
"import": "./src/clients/rtkq/migrate-to-cloud/index.ts",
"require": "./src/clients/rtkq/migrate-to-cloud/index.ts"
},
"./rtkq/legacy/preferences": {
"import": "./src/clients/rtkq/preferences/user/index.ts",
"require": "./src/clients/rtkq/preferences/user/index.ts"
},
"./rtkq/legacy/user": {
"import": "./src/clients/rtkq/user/index.ts",
"require": "./src/clients/rtkq/user/index.ts"
},
"./rtkq/playlist/v0alpha1": {
"import": "./src/clients/rtkq/playlist/v0alpha1/index.ts",
"require": "./src/clients/rtkq/playlist/v0alpha1/index.ts"
},
"./rtkq/preferences/v1alpha1": {
"import": "./src/clients/rtkq/preferences/v1alpha1/index.ts",
"require": "./src/clients/rtkq/preferences/v1alpha1/index.ts"
},
"./rtkq/provisioning/v0alpha1": {
"import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts",
"require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts"
},
"./rtkq/shorturl/v1alpha1": {
"import": "./src/clients/rtkq/shorturl/v1alpha1/index.ts",
"require": "./src/clients/rtkq/shorturl/v1alpha1/index.ts"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
"./dist",
"./README.md",
"./CHANGELOG.md",
"./LICENSE_APACHE2"
],
"scripts": {
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json",
"process-specs": "NODE_OPTIONS='--disable-warning=ExperimentalWarning' node --experimental-strip-types src/scripts/process-specs.ts",
"generate-apis": "yarn process-specs && NODE_OPTIONS='--disable-warning=ExperimentalWarning' rtk-query-codegen-openapi ./src/scripts/generate-rtk-apis.ts",
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types --disable-warning=ExperimentalWarning' plop --plopfile src/generator/plopfile.ts"
},
"devDependencies": {
"@rtk-query/codegen-openapi": "^2.0.0",
"openapi-types": "^12.1.3",
"plop": "^4.0.1",
"rollup": "^4.22.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"@grafana/runtime": ">=11.6 <= 12.x",
"@reduxjs/toolkit": "^2.8.0",
"rxjs": "7.8.2"
}
}
@@ -0,0 +1,8 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"tags": ["scope:package", "type:runtime"],
"targets": {
"build": {}
}
}
@@ -0,0 +1,39 @@
import { createRequire } from 'node:module';
import { cjsOutput, entryPoint, esmOutput, plugins } from '../rollup.config.parts';
const rq = createRequire(import.meta.url);
const pkg = rq('./package.json');
const apiClients = Object.entries<{ import: string; require: string }>(pkg.exports).filter(([key]) =>
key.startsWith('./rtkq/')
);
const apiClientConfigs = apiClients.map(([name, { import: importPath }]) => {
const baseCjsOutput = cjsOutput(pkg);
const entryFileNames = name.replace('./', '') + '.cjs';
const cjsOutputConfig = { ...baseCjsOutput, entryFileNames };
return {
input: importPath.replace('./', ''),
plugins,
output: [cjsOutputConfig, esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
};
});
export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
},
{
input: 'src/clients/rtkq/index.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
},
...apiClientConfigs,
];
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'advisor.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'advisorAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'correlations.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'correlationsAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,6 +1,8 @@
import { generatedAPI } from './endpoints.gen';
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export const correlationAPIv0alpha1 = generatedAPI.enhanceEndpoints({
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
endpoints: {
createCorrelation: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
@@ -1,9 +1,11 @@
import { BaseQueryFn } from '@reduxjs/toolkit/query';
import { lastValueFrom } from 'rxjs';
import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime';
import { type BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
interface RequestOptions extends BackendSrvRequest {
import { handleRequestError } from '../../utils/utils';
export interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
body?: BackendSrvRequest['data'];
}
@@ -49,11 +51,3 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
return backendSrvBaseQuery;
}
export function handleRequestError(error: unknown) {
if (isFetchError(error) || error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'dashboard.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'dashboardAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,7 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { addTagTypes, generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
addTagTypes: [...addTagTypes, 'Folder', 'Dashboard'],
});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'folder.grafana.app' as const;
export const API_VERSION = 'v1beta1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'folderAPIv1beta1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'iam.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'iamAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,52 @@
/**
* Don't manually add to this file! Use the `generate:api-client` command to add new API clients.
*/
import { generatedAPI as advisorAPIv0alpha1 } from './advisor/v0alpha1';
import { generatedAPI as correlationsAPIv0alpha1 } from './correlations/v0alpha1';
import { generatedAPI as dashboardAPIv0alpha1 } from './dashboard/v0alpha1';
import { generatedAPI as folderAPIv1beta1 } from './folder/v1beta1';
import { generatedAPI as iamAPIv0alpha1 } from './iam/v0alpha1';
import { generatedAPI as migrateToCloudAPI } from './migrate-to-cloud';
import { generatedAPI as playlistAPIv0alpha1 } from './playlist/v0alpha1';
import { generatedAPI as preferencesUserAPI } from './preferences/user';
import { generatedAPI as preferencesAPIv1alpha1 } from './preferences/v1alpha1';
import { generatedAPI as provisioningAPIv0alpha1 } from './provisioning/v0alpha1';
import { generatedAPI as shortURLAPIv1alpha1 } from './shorturl/v1alpha1';
import { generatedAPI as legacyUserAPI } from './user';
// PLOP_INJECT_IMPORT
/** RTK Query middleware for all API clients */
export const allMiddleware = [
advisorAPIv0alpha1.middleware,
dashboardAPIv0alpha1.middleware,
folderAPIv1beta1.middleware,
iamAPIv0alpha1.middleware,
migrateToCloudAPI.middleware,
playlistAPIv0alpha1.middleware,
preferencesAPIv1alpha1.middleware,
preferencesUserAPI.middleware,
provisioningAPIv0alpha1.middleware,
shortURLAPIv1alpha1.middleware,
correlationsAPIv0alpha1.middleware,
legacyUserAPI.middleware,
// PLOP_INJECT_MIDDLEWARE
] as const;
/** RTK Query reducers for all API clients */
export const allReducers = {
[advisorAPIv0alpha1.reducerPath]: advisorAPIv0alpha1.reducer,
[dashboardAPIv0alpha1.reducerPath]: dashboardAPIv0alpha1.reducer,
[folderAPIv1beta1.reducerPath]: folderAPIv1beta1.reducer,
[iamAPIv0alpha1.reducerPath]: iamAPIv0alpha1.reducer,
[migrateToCloudAPI.reducerPath]: migrateToCloudAPI.reducer,
[playlistAPIv0alpha1.reducerPath]: playlistAPIv0alpha1.reducer,
[preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer,
[preferencesUserAPI.reducerPath]: preferencesUserAPI.reducer,
[provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer,
[shortURLAPIv1alpha1.reducerPath]: shortURLAPIv1alpha1.reducer,
[correlationsAPIv0alpha1.reducerPath]: correlationsAPIv0alpha1.reducer,
[legacyUserAPI.reducerPath]: legacyUserAPI.reducer,
// PLOP_INJECT_REDUCER
};
export * from './createBaseQuery';
@@ -1,8 +1,8 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { createBaseQuery } from '../createBaseQuery';
export const baseAPI = createApi({
export const api = createApi({
reducerPath: 'migrateToCloudGeneratedAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
@@ -1,4 +1,4 @@
import { baseAPI as api } from './baseAPI';
import { api } from './baseAPI';
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
getSessionList: build.query<GetSessionListApiResponse, GetSessionListApiArg>({
@@ -0,0 +1,89 @@
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
import { generatedAPI as rawAPI } from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
addTagTypes: [
'cloud-migration-token',
'cloud-migration-session',
'cloud-migration-snapshot',
'cloud-migration-resource-dependencies',
],
endpoints: {
// Cloud-side - create token
getCloudMigrationToken: {
providesTags: ['cloud-migration-token'],
},
createCloudMigrationToken: {
invalidatesTags: ['cloud-migration-token'],
},
deleteCloudMigrationToken: {
invalidatesTags: ['cloud-migration-token'],
},
// On-prem session management (entering token)
getSessionList: {
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
},
getSession: {
providesTags: ['cloud-migration-session'],
},
createSession: {
invalidatesTags: ['cloud-migration-session'],
},
deleteSession: {
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
},
// Snapshot management
getShapshotList: {
providesTags: ['cloud-migration-snapshot'],
},
getSnapshot: {
providesTags: ['cloud-migration-snapshot'],
},
createSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
uploadSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
// Resource dependencies
getResourceDependencies: {
providesTags: ['cloud-migration-resource-dependencies'],
},
getDashboardByUid: suppressErrorsOnQuery,
getLibraryElementByUid: suppressErrorsOnQuery,
},
});
function suppressErrorsOnQuery<
QueryArg,
BaseQuery extends BaseQueryFn,
TagTypes extends string,
ResultType,
ReducerPath extends string,
PageParam,
>(endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath, PageParam>) {
if (!endpoint.query) {
return;
}
// internal type from rtk-query that isn't exported
type InfiniteQueryCombinedArg<QueryArg, PageParam> = {
queryArg: QueryArg;
pageParam: PageParam;
};
const originalQuery = endpoint.query;
endpoint.query = (arg: QueryArg & InfiniteQueryCombinedArg<QueryArg, PageParam>) => {
const baseQuery = originalQuery(arg);
baseQuery.showErrorAlert = false;
return baseQuery;
};
}
export * from './endpoints.gen';
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'playlist.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'playlistAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -1,8 +1,8 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { createBaseQuery } from '../../createBaseQuery';
export const baseAPI = createApi({
export const api = createApi({
reducerPath: 'userPreferencesAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
@@ -1,4 +1,4 @@
import { baseAPI as api } from './baseAPI';
import { api } from './baseAPI';
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
getUserPreferences: build.query<GetUserPreferencesApiResponse, GetUserPreferencesApiArg>({
@@ -1,9 +1,6 @@
import { generatedAPI } from './user/endpoints.gen';
import { generatedAPI as rawAPI } from './endpoints.gen';
export const { useGetUserPreferencesQuery, usePatchUserPreferencesMutation, useUpdateUserPreferencesMutation } =
generatedAPI;
export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
export const generatedAPI = rawAPI.enhanceEndpoints({
addTagTypes: ['UserPreferences'],
endpoints: {
getUserPreferences: {
@@ -17,3 +14,5 @@ export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
},
},
});
export * from './endpoints.gen';
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'preferences.grafana.app' as const;
export const API_VERSION = 'v1alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'preferencesAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'provisioning.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'provisioningAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'shorturl.grafana.app' as const;
export const API_VERSION = 'v1alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'shortURLAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,9 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from '../createBaseQuery';
export const api = createApi({
reducerPath: 'legacyUserAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
});
@@ -0,0 +1,37 @@
import { api } from './baseAPI';
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
unstarDashboardByUid: build.mutation<UnstarDashboardByUidApiResponse, UnstarDashboardByUidApiArg>({
query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'DELETE' }),
}),
starDashboardByUid: build.mutation<StarDashboardByUidApiResponse, StarDashboardByUidApiArg>({
query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'POST' }),
}),
}),
overrideExisting: false,
});
export { injectedRtkApi as generatedAPI };
export type UnstarDashboardByUidApiResponse =
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
export type UnstarDashboardByUidApiArg = {
dashboardUid: string;
};
export type StarDashboardByUidApiResponse =
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
export type StarDashboardByUidApiArg = {
dashboardUid: string;
};
export type SuccessResponseBody = {
message?: string;
};
export type ErrorResponseBody = {
/** Error An optional detailed description of the actual error. Only included if running in developer mode. */
error?: string;
/** a human readable version of the error */
message: string;
/** Status An optional status to denote the cause of the error.
For example, a 412 Precondition Failed error may include additional information of why that error happened. */
status?: string;
};
export const { useUnstarDashboardByUidMutation, useStarDashboardByUidMutation } = injectedRtkApi;
@@ -0,0 +1,25 @@
import { generatedAPI as rawAPI } from './endpoints.gen';
export const generatedAPI = rawAPI
.enhanceEndpoints({
addTagTypes: ['dashboardStars'],
endpoints: {
starDashboardByUid: {
invalidatesTags: ['dashboardStars'],
},
unstarDashboardByUid: {
invalidatesTags: ['dashboardStars'],
},
},
})
.injectEndpoints({
endpoints: (build) => ({
getStars: build.query<string[], void>({
query: () => ({ url: '/user/stars' }),
providesTags: ['dashboardStars'],
}),
}),
});
export const { useGetStarsQuery } = generatedAPI;
export * from './endpoints.gen';
@@ -26,7 +26,9 @@ The generator automates the following:
- `scripts/generate-rtk-apis.ts` for OSS APIs
- `local/generate-enterprise-apis.ts` for Enterprise APIs
3. Creates the `index.ts` file with proper exports
4. For OSS APIs only: Registers Redux reducers and middleware in the store. For Enterprise this needs to be done manually
4. For OSS APIs only:
- Exports Redux reducers and middleware in `rtkq.ts`
- Updates `package.json` exports to include the new API client
5. Formats all generated files using Prettier and ESLint
6. Automatically runs the appropriate command to generate endpoints from the OpenAPI schema
@@ -1,4 +1,6 @@
import { execSync } from 'child_process';
import fs from 'fs';
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
type PlopActionFunction = (
@@ -34,14 +36,24 @@ export const formatEndpoints = () => (endpointsInput: string | string[]) => {
// List of created or modified files
export const getFilesToFormat = (groupName: string, version: string, isEnterprise = false) => {
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients';
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts';
const apiClientBasePath = isEnterprise
? 'public/app/extensions/api/clients'
: 'packages/grafana-api-clients/src/clients/rtkq';
const generateScriptPath = isEnterprise
? 'local/generate-enterprise-apis.ts'
: 'packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts';
return [
`${apiClientBasePath}/${groupName}/${version}/baseAPI.ts`,
`${apiClientBasePath}/${groupName}/${version}/index.ts`,
generateScriptPath,
...(isEnterprise ? [] : [`public/app/core/reducers/root.ts`, `public/app/store/configureStore.ts`]),
...(isEnterprise
? []
: [
`packages/grafana-api-clients/src/index.ts`,
`packages/grafana-api-clients/src/clients/rtkq/index.ts`,
`packages/grafana-api-clients/package.json`,
]),
];
};
@@ -53,9 +65,10 @@ export const runGenerateApis =
let command;
if (isEnterprise) {
command = 'yarn process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
command =
'yarn workspace @grafana/api-clients process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
} else {
command = 'yarn generate-apis';
command = 'yarn workspace @grafana/api-clients generate-apis';
}
console.log(`⏳ Running ${command} to generate endpoints...`);
@@ -113,3 +126,62 @@ export const validateGroup = (group: string) => {
export const validateVersion = (version: string) => {
return version && /^v\d+[a-z]*\d+$/.test(version) ? true : 'Version should be in format: v0alpha1, v1beta2, etc.';
};
export const updatePackageJsonExports =
(basePath: string): PlopActionFunction =>
(answers) => {
try {
const { groupName, version } = answers;
if (!groupName || !version) {
return '❌ Missing groupName or version for package.json update';
}
const packageJsonPath = path.join(basePath, 'packages/grafana-api-clients/package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Create the new export entry
const newExportKey = `./rtkq/${groupName}/${version}`;
const newExportValue = {
import: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
require: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
};
// Check if export already exists
if (packageJson.exports[newExportKey]) {
return `✅ Export for ${newExportKey} already exists in package.json`;
}
// Add the new export entry
packageJson.exports[newExportKey] = newExportValue;
// Write the updated package.json back to file
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
return `✅ Added export for ${newExportKey} to package.json`;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Failed to update package.json exports:', errorMessage);
return '❌ Failed to update package.json exports. See error above.';
}
};
/**
* TODO: Make this work more generically with endpoints that don't have operationIds defined -
* then we can allow selection of endpoints in the generator
*/
export const getOperationIds = (apiSpec: OpenAPIV3.Document) => {
return (
Object.values(apiSpec.paths).flatMap((path) => {
return Object.entries(path || {})
.filter(([name]) => name !== 'parameters')
.flatMap(([_, pathItem]) => {
if (typeof pathItem === 'object' && 'operationId' in pathItem && pathItem.operationId) {
return pathItem.operationId;
}
return null;
})
.filter((id) => id !== null);
}) || []
);
};
@@ -1,27 +1,26 @@
import fs from 'fs';
import path from 'path';
import type { NodePlopAPI, PlopGeneratorConfig } from 'plop';
import {
formatEndpoints,
validateGroup,
validateVersion,
formatFiles,
getFilesToFormat,
runGenerateApis,
formatFiles,
// The file extension is necessary to make the imports
// work with the '--experimental-strip-types' flag
// @ts-ignore
updatePackageJsonExports,
validateGroup,
validateVersion,
} from './helpers.ts';
// @ts-ignore
import { type ActionConfig, type PlopData, isPlopData } from './types.ts';
export default function plopGenerator(plop: NodePlopAPI) {
// Grafana root path
const basePath = path.resolve(import.meta.dirname, '../..');
const basePath = path.resolve(import.meta.dirname, '../../../..');
// Register custom action types
plop.setActionType('runGenerateApis', runGenerateApis(basePath));
plop.setActionType('formatFiles', formatFiles(basePath));
plop.setActionType('updatePackageJsonExports', updatePackageJsonExports(basePath));
// Used in templates to format endpoints
plop.setHelper('formatEndpoints', formatEndpoints());
@@ -29,13 +28,16 @@ export default function plopGenerator(plop: NodePlopAPI) {
const generateRtkApiActions = (data: PlopData) => {
const { reducerPath, groupName, version, isEnterprise } = data;
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients';
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts';
const apiClientBasePath = isEnterprise
? 'public/app/extensions/api/clients'
: 'packages/grafana-api-clients/src/clients/rtkq';
const generateScriptPath = isEnterprise
? 'local/generate-enterprise-apis.ts'
: 'packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts';
// Using app path, so the imports work on any file level
const clientImportPath = isEnterprise ? '../extensions/api/clients' : 'app/api/clients';
const clientImportPath = isEnterprise ? '../extensions/api/clients' : '.';
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../public/app/api/clients';
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../clients';
const templateData = {
...data,
@@ -68,27 +70,24 @@ export default function plopGenerator(plop: NodePlopAPI) {
actions.push(
{
type: 'modify',
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
path: '../clients/rtkq/index.ts',
pattern: '// PLOP_INJECT_IMPORT',
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
template: `import { generatedAPI as ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
},
{
type: 'modify',
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
path: '../clients/rtkq/index.ts',
pattern: '// PLOP_INJECT_REDUCER',
template: `[${reducerPath}.reducerPath]: ${reducerPath}.reducer,\n // PLOP_INJECT_REDUCER`,
},
{
type: 'modify',
path: path.join(basePath, 'public/app/store/configureStore.ts'),
pattern: '// PLOP_INJECT_IMPORT',
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
},
{
type: 'modify',
path: path.join(basePath, 'public/app/store/configureStore.ts'),
path: '../clients/rtkq/index.ts',
pattern: '// PLOP_INJECT_MIDDLEWARE',
template: `${reducerPath}.middleware,\n // PLOP_INJECT_MIDDLEWARE`,
},
{
type: 'updatePackageJsonExports',
}
);
}
@@ -108,6 +107,16 @@ export default function plopGenerator(plop: NodePlopAPI) {
return actions;
};
// Read files from data/openapi directory and use as an array of API clients that the user can select
const openapiDir = path.join(basePath, 'data/openapi');
let possibleOpenAPISpecs: string[] = [];
try {
possibleOpenAPISpecs = fs.readdirSync(openapiDir).filter((file: string) => file.endsWith('.json'));
} catch (e) {
possibleOpenAPISpecs = [];
}
const generator: PlopGeneratorConfig = {
description: 'Generate RTK Query API client for a Grafana API group',
prompts: [
@@ -118,30 +127,43 @@ export default function plopGenerator(plop: NodePlopAPI) {
default: false,
},
{
type: 'input',
name: 'groupName',
message: 'API group name (e.g. dashboard):',
type: 'list',
loop: false,
choices: possibleOpenAPISpecs,
pageSize: 50,
name: 'apiInfo',
message: 'OpenAPI spec:',
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
},
{
type: 'input',
name: 'groupName',
message: 'API group name:',
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
default: (answers: { apiInfo?: string }) => answers.apiInfo?.split('.grafana.app-')[0],
},
{
type: 'input',
name: 'group',
message: 'API group (e.g. dashboard.grafana.app):',
message: 'API group:',
default: (answers: { groupName?: string }) => `${answers.groupName}.grafana.app`,
validate: validateGroup,
},
{
type: 'input',
name: 'version',
message: 'API version (e.g. v0alpha1):',
default: 'v0alpha1',
message: 'API version:',
default: (answers: { apiInfo?: string }) => answers.apiInfo?.split('.grafana.app-')[1].replace(/\.json$/, ''),
validate: validateVersion,
},
{
type: 'input',
name: 'reducerPath',
message: 'Reducer path (e.g. dashboardAPIv0alpha1):',
default: (answers: { groupName?: string; version?: string }) => `${answers.groupName}API${answers.version}`,
message: 'Reducer path:',
default: (answers: { groupName?: string; version?: string }) => {
const groupNameParsed = answers.groupName?.replace(/\.([a-z])/g, (_, letter) => letter.toUpperCase());
return `${groupNameParsed}API${answers.version}`;
},
validate: (input: string) =>
input?.endsWith('API') || input?.match(/API[a-z]\d+[a-z]*\d*$/)
? true
@@ -150,8 +172,8 @@ export default function plopGenerator(plop: NodePlopAPI) {
{
type: 'input',
name: 'endpoints',
default: undefined,
message: 'Endpoints to include (comma-separated, optional):',
validate: () => true,
},
],
actions: function (data) {
@@ -0,0 +1,21 @@
import { createApi } from '@reduxjs/toolkit/query/react';
{{#if isEnterprise}}
import { getAPIBaseURL } from '@grafana/api-clients';
import { createBaseQuery } from '@grafana/api-clients/rtkq';
{{else}}
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
{{/if}}
export const API_GROUP = '{{group}}' as const;
export const API_VERSION = '{{version}}' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: '{{reducerPath}}',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,2 @@
...createAPIConfig('{{groupName}}', '{{version}}'{{#if endpoints}}, [{{{formatEndpoints endpoints}}}]{{/if}}),
// PLOP_INJECT_API_CLIENT - Used by the API client generator
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -10,8 +10,17 @@ export interface RunGenerateApisActionConfig {
isEnterprise: boolean;
}
export interface UpdatePackageJsonExportsActionConfig {
type: 'updatePackageJsonExports';
}
// Union type of all possible action configs
export type ActionConfig = AddActionConfig | ModifyActionConfig | FormatFilesActionConfig | RunGenerateApisActionConfig;
export type ActionConfig =
| AddActionConfig
| ModifyActionConfig
| FormatFilesActionConfig
| RunGenerateApisActionConfig
| UpdatePackageJsonExportsActionConfig;
export interface PlopData {
groupName: string;
@@ -0,0 +1 @@
export { getAPINamespace, getAPIBaseURL, normalizeError, handleRequestError } from './utils/utils';
@@ -0,0 +1,111 @@
// Generates Redux Toolkit API slices for certain APIs from the OpenAPI spec
import type { ConfigFile } from '@rtk-query/codegen-openapi';
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
// Grafana root path - navigate up from this script's directory
const basePath = path.resolve(__dirname, '../../../..');
// Include some types that are used inside the @rtk-query/codegen-openapi package
// but not exported
declare const operationKeys: readonly ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
type OperationDefinition = {
path: string;
verb: (typeof operationKeys)[number];
pathItem: OpenAPIV3.PathItemObject;
operation: OpenAPIV3.OperationObject;
};
type EndpointMatcher = string[] | ((operationName: string, operationDefinition: OperationDefinition) => boolean);
/**
* Helper to return consistent base API generation config
*/
const createAPIConfig = (app: string, version: string, filterEndpoints?: EndpointMatcher, additional = {}) => {
const filePath = `../clients/rtkq/${app}/${version}/endpoints.gen.ts`;
return {
[filePath]: {
schemaFile: path.join(basePath, `data/openapi/${app}.grafana.app-${version}.json`),
apiFile: `../clients/rtkq/${app}/${version}/baseAPI.ts`,
filterEndpoints,
tag: true,
...additional,
},
};
};
const config: ConfigFile = {
schemaFile: '', // leave this empty, and instead populate the outputFiles object below
apiFile: '', // leave this empty, and instead populate the outputFiles object below
exportName: 'generatedAPI',
outputFiles: {
'../clients/rtkq/migrate-to-cloud/endpoints.gen.ts': {
schemaFile: path.join(basePath, 'public/openapi3.json'),
apiFile: '../clients/rtkq/migrate-to-cloud/baseAPI.ts',
hooks: true,
filterEndpoints: [
'getSessionList',
'getSession',
'deleteSession',
'createSession',
'getShapshotList',
'getSnapshot',
'uploadSnapshot',
'createSnapshot',
'cancelSnapshot',
'createCloudMigrationToken',
'deleteCloudMigrationToken',
'getCloudMigrationToken',
'getDashboardByUid',
'getLibraryElementByUid',
'getResourceDependencies',
],
},
'../clients/rtkq/preferences/user/endpoints.gen.ts': {
schemaFile: path.join(basePath, 'public/openapi3.json'),
hooks: true,
apiFile: '../clients/rtkq/preferences/user/baseAPI.ts',
filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'],
},
'../clients/rtkq/user/endpoints.gen.ts': {
schemaFile: path.join(basePath, 'public/openapi3.json'),
hooks: true,
apiFile: '../clients/rtkq/user/baseAPI.ts',
filterEndpoints: ['starDashboardByUid', 'unstarDashboardByUid'],
},
...createAPIConfig('iam', 'v0alpha1', ['getDisplayMapping']),
...createAPIConfig('provisioning', 'v0alpha1', filterEndpoints, { hooks: true }),
...createAPIConfig('folder', 'v1beta1', undefined),
...createAPIConfig('advisor', 'v0alpha1', [
'createCheck',
'getCheck',
'listCheck',
'deleteCheck',
'updateCheck',
'listCheckType',
'updateCheckType',
]),
...createAPIConfig('playlist', 'v0alpha1', [
'listPlaylist',
'getPlaylist',
'createPlaylist',
'deletePlaylist',
'replacePlaylist',
]),
...createAPIConfig('shorturl', 'v1alpha1'),
...createAPIConfig('preferences', 'v1alpha1', undefined, { hooks: true }),
...createAPIConfig('dashboard', 'v0alpha1', ['getSearch']),
...createAPIConfig('correlations', 'v0alpha1'),
// PLOP_INJECT_API_CLIENT - Used by the API client generator
},
};
function filterEndpoints(name: string) {
return !name.toLowerCase().includes('getapiresources') && !name.toLowerCase().includes('update');
}
export default config;
@@ -172,11 +172,14 @@ function processDirectory(sourceDir: string, outputDir: string) {
}
}
// Grafana root path - navigate up from this script's directory
const basePath = path.resolve(import.meta.dirname, '../../../..');
const sourceDirs = [
path.resolve(import.meta.dirname, '../pkg/tests/apis/openapi_snapshots'),
path.resolve(import.meta.dirname, '../pkg/extensions/apiserver/tests/openapi_snapshots'),
path.join(basePath, 'pkg/tests/apis/openapi_snapshots'),
path.join(basePath, 'pkg/extensions/apiserver/tests/openapi_snapshots'),
];
const outputDir = path.resolve(import.meta.dirname, '../data/openapi');
const outputDir = path.join(basePath, 'data/openapi');
for (const sourceDir of sourceDirs) {
processDirectory(sourceDir, outputDir);
@@ -0,0 +1,62 @@
import { config, isFetchError } from '@grafana/runtime';
export const getAPINamespace = () => config.namespace;
/**
* Get a base URL for a k8s API endpoint with parameterised namespace given it's group and version
* @param group the k8s group, e.g. dashboard.grafana.app
* @param version e.g. v0alpha1
* @returns
*/
export const getAPIBaseURL = (group: string, version: string) => {
return `/apis/${group}/${version}/namespaces/${getAPINamespace()}` as const;
};
/**
* Normalize error from various error formats into a string message
* @param e the raw error
* @returns string error message
*/
export const normalizeError = (e: unknown): string => {
if (!e) {
return 'Unknown error';
}
if (typeof e === 'object') {
if ('data' in e && e.data && typeof e.data === 'object' && 'message' in e.data) {
return String(e.data.message);
}
// Direct error with message property
if ('message' in e) {
return String(e.message);
}
// Nested error wrapper (RTK Query format)
if ('error' in e) {
if (e.error instanceof Error) {
return e.error.message;
} else if (isFetchError(e.error)) {
if (e.error.data && typeof e.error.data === 'object' && 'message' in e.error.data) {
return String(e.error.data.message);
}
if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) {
return e.error.data.errors.join('\n');
}
}
}
}
if (e instanceof Error) {
return e.message;
}
return String(e);
};
export function handleRequestError(error: unknown) {
if (isFetchError(error) || error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "node_modules", "**/*.test.ts*"]
}
@@ -0,0 +1,21 @@
{
"extends": "../../scripts/tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"declaration": true,
"jsx": "react-jsx",
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."],
"allowImportingTsExtensions": true
},
"exclude": ["dist/**/*"],
"include": [
"../../public/app/types/window.d.ts",
"../../public/app/types/images.d.ts",
"../grafana-ui/src/types/*.d.ts",
"../grafana-i18n/src/types/*.d.ts",
"src/**/*.ts*"
]
}
@@ -25,6 +25,7 @@ export interface Action {
*/
export interface ActionModel<T = any> {
title: string;
type?: ActionType;
onClick: (event: any, origin?: any, actionVars?: ActionVariableInput) => void;
confirmation: (actionVars?: ActionVariableInput) => ReactNode;
oneClick?: boolean;
@@ -712,10 +712,6 @@ export interface FeatureToggles {
*/
unifiedStorageSearchSprinkles?: boolean;
/**
* Use full n-gram indexing instead of edge n-gram for unified storage search
*/
unifiedStorageUseFullNgram?: boolean;
/**
* Pick the dual write mode from database configs
*/
managedDualWriter?: boolean;
@@ -1055,11 +1051,6 @@ export interface FeatureToggles {
*/
restoreDashboards?: boolean;
/**
* Skip token rotation if it was already rotated less than 5 seconds ago
* @default true
*/
skipTokenRotationIfRecent?: boolean;
/**
* Enable configuration of alert enrichments in Grafana Cloud.
* @default false
*/
@@ -1119,11 +1110,6 @@ export interface FeatureToggles {
*/
foldersAppPlatformAPI?: boolean;
/**
* Set this to true to use the new PluginImporter functionality
* @default false
*/
enablePluginImporter?: boolean;
/**
* Applies OTel formatting templates to displayed logs
*/
otelLogsFormatting?: boolean;
@@ -1232,7 +1218,7 @@ export interface FeatureToggles {
*/
preventPanelChromeOverflow?: boolean;
/**
* Load plugins during store service startup instead of wire provider
* Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded
* @default false
*/
pluginStoreServiceLoading?: boolean;
+1 -1
View File
@@ -33,5 +33,5 @@
"webpack-bundle-analyzer": "^4.10.2",
"webpack-virtual-modules": "^0.6.2"
},
"packageManager": "yarn@4.9.4"
"packageManager": "yarn@4.10.3"
}
@@ -40,6 +40,7 @@ export interface Options {
setDisplayedFields?: unknown;
showCommonLabels: boolean;
showControls?: boolean;
showFieldSelector?: boolean;
showLabels: boolean;
showLogAttributes?: boolean;
showLogContextToggle: boolean;
@@ -93,6 +93,7 @@ export const getModalStyles = (theme: GrafanaTheme2) => {
position: 'sticky',
bottom: 0,
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(0.5),
zIndex: 1,
}),
};
@@ -293,10 +293,6 @@ function getStyles(theme: GrafanaTheme2) {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
// Adds subtle shadow in light themes to help bar stand out
'.radial-arc-path': {
filter: theme.isLight ? `drop-shadow(0px 0px 1px #888);` : '',
},
}),
clearButton: css({
background: 'transparent',
@@ -59,7 +59,7 @@ export const TableCellActions = memo(
aria-label={t('grafana-ui.table.cell-filter-on', 'Filter for value')}
onClick={() => {
onCellFilterAdded?.({
key: displayName,
key: field.name,
operator: FILTER_FOR_OPERATOR,
value: String(value ?? ''),
});
@@ -70,7 +70,7 @@ export const TableCellActions = memo(
aria-label={t('grafana-ui.table.cell-filter-out', 'Filter out value')}
onClick={() => {
onCellFilterAdded?.({
key: displayName,
key: field.name,
operator: FILTER_OUT_OPERATOR,
value: String(value ?? ''),
});
+1 -1
View File
@@ -1262,7 +1262,7 @@ type GetHomeDashboardResponseBody struct {
// swagger:response dashboardVersionsResponse
type DashboardVersionsResponse struct {
// in: body
Body []dashver.DashboardVersionMeta `json:"body"`
Body *dashver.DashboardVersionResponseMeta `json:"body"`
}
// swagger:response dashboardVersionResponse
+10 -1
View File
@@ -132,6 +132,7 @@ type NewBackendOpts struct {
Tags []string
Static bool
WireTag string
CGOEnabled bool
GoBuildCache *dagger.CacheVolume
GoModCache *dagger.CacheVolume
}
@@ -198,6 +199,12 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
goCacheProg = val
}
cgoDisabled, err := options.Bool(flags.CGODisabled)
if err != nil {
return nil, err
}
cgoEnabled := !cgoDisabled
bopts := &backend.BuildOpts{
Version: p.Version,
Enterprise: p.Enterprise,
@@ -206,6 +213,7 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
Static: static,
WireTag: wireTag,
Tags: tags,
CGOEnabled: cgoEnabled,
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
@@ -233,9 +241,10 @@ func NewBackend(ctx context.Context, log *slog.Logger, artifact string, opts *Ne
Tags: opts.Tags,
Static: opts.Static,
WireTag: opts.WireTag,
CGOEnabled: opts.CGOEnabled,
}
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution)
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution, "cgo enabled", opts.CGOEnabled)
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
@@ -119,7 +119,12 @@ func NewTarballFromString(ctx context.Context, log *slog.Logger, artifact string
if err != nil {
return nil, err
}
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments)
cgoDisabled, err := options.Bool(flags.CGODisabled)
if err != nil {
return nil, err
}
cgoEnabled := !cgoDisabled
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments, cgoEnabled)
}
// NewTarball returns a properly initialized Tarball artifact.
@@ -143,6 +148,7 @@ func NewTarball(
goVersion string,
viceroyVersion string,
experiments []string,
cgoEnabled bool,
) (*pipeline.Artifact, error) {
backendArtifact, err := NewBackend(ctx, log, artifact, &NewBackendOpts{
Name: name,
@@ -158,6 +164,7 @@ func NewTarball(
Enterprise: enterprise,
GoBuildCache: goBuildCache,
GoModCache: goModCache,
CGOEnabled: cgoEnabled,
})
if err != nil {
return nil, err
+1 -1
View File
@@ -62,7 +62,7 @@ func Build(
ldflags := LDFlagsDynamic(vcsinfo)
if opts.Static {
if opts.Static && opts.CGOEnabled {
ldflags = LDFlagsStatic(vcsinfo)
}
+34 -17
View File
@@ -19,9 +19,23 @@ type BuildOpts struct {
GoCacheProg string
Static bool
Enterprise bool
CGOEnabled bool
}
func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc, error) {
func distroOptsFunc(log *slog.Logger, distro Distribution, opts *BuildOpts) (DistroBuildOptsFunc, error) {
if !opts.CGOEnabled {
return func(distro Distribution, experiments, tags []string) *GoBuildOpts {
os, arch := OSAndArch(distro)
archv := ArchVersion(distro)
return &GoBuildOpts{
OS: os,
Arch: arch,
GoARM: GoARM(archv),
CGOEnabled: false,
}
}, nil
}
if val, ok := DistributionGoOpts[distro]; ok {
return DistroOptsLogger(log, val), nil
}
@@ -29,7 +43,7 @@ func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc,
}
func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
fn, err := distroOptsFunc(log, distro)
fn, err := distroOptsFunc(log, distro, opts)
if err != nil {
return nil, err
}
@@ -39,7 +53,7 @@ func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distributio
}
func WithViceroyEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
fn, err := distroOptsFunc(log, distro)
fn, err := distroOptsFunc(log, distro, opts)
if err != nil {
return nil, err
}
@@ -92,24 +106,27 @@ func GolangContainer(
) (*dagger.Container, error) {
os, _ := OSAndArch(distro)
// Only use viceroy for all darwin and only windows/amd64
if os == "darwin" || distro == DistWindowsAMD64 {
if opts.CGOEnabled && (os == "darwin" || distro == DistWindowsAMD64) {
return ViceroyContainer(d, log, distro, goVersion, viceroyVersion, opts)
}
container := golang.Container(d, platform, goVersion).
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"mv", "/zig", "/bin/zig"}).
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
WithExec([]string{"mkdir", "/toolchain"}).
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
container := golang.Container(d, platform, goVersion)
if opts.CGOEnabled {
container = container.
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"mv", "/zig", "/bin/zig"}).
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
WithExec([]string{"mkdir", "/toolchain"}).
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
}
return WithGoEnv(log, container, distro, opts)
}

Some files were not shown because too many files have changed in this diff Show More