diff --git a/.betterer.results b/.betterer.results index 73851418191..c35064cb70b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1769,9 +1769,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], - "public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -1803,25 +1800,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], [0, 0, 0, "No untranslated strings. Wrap text with ", "13"] ], - "public/app/features/alerting/unified/components/notification-policies/Policy.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "13"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "14"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "15"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "16"] - ], "public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -2864,9 +2842,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/scene/Scopes/ScopesInput.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -4870,17 +4845,12 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], "public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/plugins/admin/components/InstallControls/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./InstallControlsWarning\`)", "0"], @@ -5184,6 +5154,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], + "public/app/features/scopes/index.ts:5381": [ + [0, 0, 0, "Do not re-export imported variable (\`./instance\`)", "0"], + [0, 0, 0, "Do not re-export imported variable (\`./ScopesDashboards\`)", "1"] + ], "public/app/features/search/page/components/ActionRow.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -7326,6 +7300,23 @@ exports[`better eslint`] = { "public/app/types/unified-alerting-dto.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/swagger/K8sNameLookup.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/swagger/SwaggerPage.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/swagger/index.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"] + ], + "public/swagger/plugins.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], "public/test/core/redux/reduxTester.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -7592,6 +7583,34 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], + "packages/grafana-ui/src/themes/GlobalStyles/forms.ts:5381": [ + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], + [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] + ], "packages/grafana-ui/src/themes/GlobalStyles/legacySelect.ts:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], diff --git a/.bra.toml b/.bra.toml index 5c293408100..b32010204b6 100644 --- a/.bra.toml +++ b/.bra.toml @@ -2,7 +2,7 @@ init_cmds = [ ["GO_BUILD_DEV=1", "make", "build-go"], ["make", "gen-jsonnet"], - ["./bin/grafana", "server", "-profile", "-profile-addr=0.0.0.0", "-profile-port=6000", "-profile-block-rate=1", "-profile-mutex-rate=5", "-packaging=dev", "cfg:app_mode=development"] + ["./bin/grafana", "server", "-profile", "-profile-addr=127.0.0.1", "-profile-port=6000", "-profile-block-rate=1", "-profile-mutex-rate=5", "-packaging=dev", "cfg:app_mode=development"] ] watch_all = true follow_symlinks = true @@ -18,5 +18,5 @@ build_delay = 1500 cmds = [ ["GO_BUILD_DEV=1", "make", "build-go"], ["make", "gen-jsonnet"], - ["./bin/grafana", "server", "-profile", "-profile-addr=0.0.0.0", "-profile-port=6000", "-profile-block-rate=1", "-profile-mutex-rate=5", "-packaging=dev", "cfg:app_mode=development"] + ["./bin/grafana", "server", "-profile", "-profile-addr=127.0.0.1", "-profile-port=6000", "-profile-block-rate=1", "-profile-mutex-rate=5", "-packaging=dev", "cfg:app_mode=development"] ] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b6664d8dbf..c92f3f3d701 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -106,6 +106,7 @@ /pkg/semconv/ @grafana/grafana-backend-group /pkg/server/ @grafana/grafana-backend-group /pkg/apiserver @grafana/grafana-app-platform-squad +/pkg/aggregator @grafana/grafana-app-platform-squad /pkg/apimachinery @grafana/grafana-app-platform-squad /pkg/apimachinery/identity/ @grafana/identity-squad /pkg/apimachinery/errutil/ @grafana/grafana-backend-group @@ -344,6 +345,7 @@ /packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad +/packages/grafana-ui/src/components/PluginSignatureBadge/ @grafana/plugins-platform-frontend /packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations /packages/grafana-ui/src/components/Table/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Table/SparklineCell.tsx @grafana/dataviz-squad @grafana/app-o11y-visualizations @@ -417,6 +419,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/dashboard/ @grafana/dashboards-squad /public/app/features/dashboard/components/TransformationsEditor/ @grafana/dataviz-squad /public/app/features/dashboard-scene/ @grafana/dashboards-squad +/public/app/features/scopes/ @grafana/dashboards-squad /public/app/features/datasources/ @grafana/plugins-platform-frontend /public/app/features/dimensions/ @grafana/dataviz-squad /public/app/features/dataframe-import/ @grafana/dataviz-squad @@ -503,7 +506,8 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/test/ @grafana/grafana-frontend-platform /public/test/helpers/alertingRuleEditor.tsx @grafana/alerting-frontend /public/views/ @grafana/grafana-frontend-platform -/public/views/swagger.html @grafana/grafana-backend-group +/public/views/swagger.html @grafana/grafana-app-platform-squad +/public/swagger/ @grafana/grafana-app-platform-squad /public/app/features/explore/Logs/ @grafana/observability-logs @@ -664,7 +668,7 @@ embed.go @grafana/grafana-as-code # GitHub Workflows and Templates /.github/CODEOWNERS @tolzhabayev -/.github/ISSUE_TEMPLATE/ @torkelo +/.github/ISSUE_TEMPLATE/ @torkelo @sympatheticmoose /.github/PULL_REQUEST_TEMPLATE.md @torkelo /.github/bot.md @torkelo /.github/commands.json @torkelo @@ -721,6 +725,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/i18n-crowdin-upload.yml @grafana/grafana-frontend-platform /.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform /.github/workflows/pr-go-workspace-check.yml @grafana/grafana-app-platform-squad +/.github/workflows/pr-k8s-codegen-check.yml @grafana/grafana-app-platform-squad /.github/workflows/run-scenes-e2e.yml @grafana/dashboards-squad /.github/workflows/go_lint.yml @grafana/grafana-backend-services-squad /.github/workflows/trivy-scan.yml @grafana/grafana-backend-services-squad diff --git a/.github/workflows/pr-k8s-codegen-check.yml b/.github/workflows/pr-k8s-codegen-check.yml new file mode 100644 index 00000000000..71dd29b6354 --- /dev/null +++ b/.github/workflows/pr-k8s-codegen-check.yml @@ -0,0 +1,38 @@ +name: "K8s Codegen Check" + +on: + workflow_dispatch: + pull_request: + branches: [main] + paths: + - "pkg/apis/**" + - "pkg/aggregator/apis/**" + - "pkg/apimachinery/apis/**" + - "hack/**" + - "*.sum" + +jobs: + check: + name: K8s Codegen Check + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set go version + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + - name: Update k8s codegen + run: ./hack/update-codegen.sh + + - name: Check for k8s codegen changes + run: | + if ! git diff --exit-code --quiet; then + echo "Changes detected:" + git diff + echo "Please run './hack/update-codegen.sh' and commit the changes." + exit 1 + fi \ No newline at end of file diff --git a/.golangci.toml b/.golangci.toml index b96e063439c..f45ec1af4b7 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -98,6 +98,21 @@ files = [ "**/pkg/apimachinery/**/*" ] +[linters-settings.depguard.rules.aggregator] +list-mode = "lax" +allow = [ + "github.com/grafana/grafana/pkg/aggregator", + "github.com/grafana/grafana/pkg/semconv", + "github.com/grafana/grafana/pkg/apimachinery", +] +deny = [ + { pkg = "github.com/grafana/grafana/pkg", desc = "apimachinery is not allowed to import grafana core" } +] +files = [ + "./pkg/aggregator/*", + "./pkg/aggregator/**/*" +] + [linters-settings.depguard.rules.promlib] list-mode = "lax" # allow unless explicitely denied deny = [ @@ -107,7 +122,6 @@ allow = [ "github.com/grafana/grafana/pkg/promlib" ] files = [ - "**/pkg/promlib/*", "**/pkg/promlib/**/*" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index bec6b577d4e..099b03ce0a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ + + +# 11.1.4 (2024-08-14) + +### Bug fixes + +- **Swagger:** Fixed CVE-2024-6837. + + + + +# 11.0.3 (2024-08-14) + +### Bug fixes + +- **Swagger:** Fixed CVE-2024-6837. + + + + +# 10.4.7 (2024-08-14) + +### Bug fixes + +- **Swagger:** Fixed CVE-2024-6837. + + # 11.1.3 (2024-07-26) diff --git a/Dockerfile b/Dockerfile index 416b2a5b5b8..5fe3fc478aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ COPY packages packages COPY plugins-bundled plugins-bundled COPY public public COPY LICENSE ./ +COPY conf/defaults.ini ./conf/defaults.ini RUN apk add --no-cache make build-base python3 @@ -44,6 +45,7 @@ RUN if grep -i -q alpine /etc/issue; then \ apk add --no-cache \ # This is required to allow building on arm64 due to https://github.com/golang/go/issues/22040 binutils-gold \ + bash \ # Install build dependencies gcc g++ make git; \ fi @@ -62,6 +64,7 @@ COPY pkg/build/wire/go.* pkg/build/wire/ COPY pkg/promlib/go.* pkg/promlib/ COPY pkg/storage/unified/resource/go.* pkg/storage/unified/resource/ COPY pkg/semconv/go.* pkg/semconv/ +COPY pkg/aggregator/go.* pkg/aggregator/ RUN go mod download RUN if [[ "$BINGO" = "true" ]]; then \ diff --git a/conf/defaults.ini b/conf/defaults.ini index 02ff494a9f8..b14e1d5ea49 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -579,6 +579,13 @@ oauth_auto_login = false # OAuth state max age cookie duration in seconds. Defaults to 600 seconds. oauth_state_cookie_max_age = 600 +# Minimum wait time in milliseconds for the server lock retry mechanism. +# The server lock retry mechanism is used to prevent multiple Grafana instances from +# simultaneously refreshing OAuth tokens. This mechanism waits at least this amount +# of time before retrying to acquire the server lock. There are 5 retries in total. +# The wait time between retries is calculated as random(n, n + 500) +oauth_refresh_token_server_lock_min_wait_ms = 1000 + # limit of api_key seconds to live before expiration api_key_max_seconds_to_live = -1 @@ -1431,6 +1438,9 @@ max_age = max_annotations_to_keep = [recording_rules] +# Enable recording rules. You must provide write credentials below. +enabled = false + # Target URL (including write path) for recording rules. url = @@ -1973,3 +1983,10 @@ feedback_url = https://docs.google.com/forms/d/e/1FAIpQLSeEE33vhbSpR8A8S1A1ocZ1B # How frequently should the frontend UI poll for changes while resources are migrating frontend_poll_interval = 2s +################################## Frontend development configuration ################################### +# Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code +# Any values placed here may be accessible to the UI. Do not place sensitive information here. +[frontend_dev] +# Should UI tests fail when console log/warn/erroring? +# Does not affect the result when running on CI - only for allowing devs to choose this behaviour locally +fail_tests_on_console = true diff --git a/conf/sample.ini b/conf/sample.ini index b1c05b984b3..f94888ae679 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -583,6 +583,13 @@ # OAuth state max age cookie duration in seconds. Defaults to 600 seconds. ;oauth_state_cookie_max_age = 600 +# Minimum wait time in milliseconds for the server lock retry mechanism. +# The server lock retry mechanism is used to prevent multiple Grafana instances from +# simultaneously refreshing OAuth tokens. This mechanism waits at least this amount +# of time before retrying to acquire the server lock. There are 5 retries in total. +# The wait time between retries is calculated as random(n, n + 500) +; oauth_refresh_token_server_lock_min_wait_ms = 1000 + # limit of api_key seconds to live before expiration ;api_key_max_seconds_to_live = -1 @@ -1425,6 +1432,9 @@ max_annotations_to_keep = #################################### Recording Rules ##################### [recording_rules] +# Enable recording rules. You must provide write credentials below. +enabled = false + # Target URL (including write path) for recording rules. url = @@ -1905,3 +1915,11 @@ timeout = 30s ;feedback_url = "" # How frequently should the frontend UI poll for changes while resources are migrating ;frontend_poll_interval = 2s + +################################## Frontend development configuration ################################### +# Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code +# Any values placed here may be accessible to the UI. Do not place sensitive information here. +[frontend_dev] +# Should UI tests fail when console log/warn/erroring? +# Does not affect the result when running on CI - only for allowing devs to choose this behaviour locally +; fail_tests_on_console = true diff --git a/devenv/plugins.yaml b/devenv/plugins.yaml index 9e488cc065f..097428a6ca9 100644 --- a/devenv/plugins.yaml +++ b/devenv/plugins.yaml @@ -17,3 +17,11 @@ apps: org_id: 1 org_name: Main Org. disabled: false + - type: myorg-componentconsumer-app + org_id: 1 + org_name: Main Org. + disabled: false + - type: myorg-componentexposer-app + org_id: 1 + org_name: Main Org. + disabled: false diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 8c5b82c7efe..f4c29165647 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -15,31 +15,34 @@ weight: 600 # Provision Grafana -In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. But that required the service to be running before you started creating dashboards and you also needed to set up credentials for the HTTP API. In v5.0 we decided to improve this experience by adding a new active provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can be defined via files that can be version controlled. We hope to extend this system to later add support for users and orgs as well. +Grafana has an active provisioning system that uses configuration files. +This makes GitOps more natural since data sources and dashboards can be defined using files that can be version controlled. -## Config File +## Configuration file -See [Configuration]({{< relref "../../setup-grafana/configure-grafana/" >}}) for more information on what you can configure in `grafana.ini`. +Refer to [Configuration]({{< relref "../../setup-grafana/configure-grafana/" >}}) for more information on what you can configure in `grafana.ini`. -### Config File Locations +### Configuration file locations - Default configuration from `$WORKING_DIR/conf/defaults.ini` - Custom configuration from `$WORKING_DIR/conf/custom.ini` - The custom configuration file path can be overridden using the `--config` parameter -{{% admonition type="note" %}} +{{< admonition type="note" >}} If you have installed Grafana using the `deb` or `rpm` packages, then your configuration file is located at `/etc/grafana/grafana.ini`. This path is specified in the Grafana -init.d script using `--config` file parameter. -{{% /admonition %}} +`init.d` script using the `--config` file parameter. +{{< /admonition >}} -### Using Environment Variables +### Environment variables -It is possible to use environment variable interpolation in all 3 provisioning configuration types. Allowed syntax -is either `$ENV_VAR_NAME` or `${ENV_VAR_NAME}` and can be used only for values not for keys or bigger parts -of the configurations. It is not available in the dashboard's definition files just the dashboard provisioning +You can use environment variable interpolation in all three provisioning configuration types. +The allowed syntax is either `$ENV_VAR_NAME` or `${ENV_VAR_NAME}`, and it can be used only for values, not for keys or larger parts +of the configurations. +It's not available in the dashboard's definition files, just the dashboard provisioning configuration. + Example: ```yaml @@ -51,13 +54,13 @@ datasources: password: $PASSWORD ``` -If you have a literal `$` in your value and want to avoid interpolation, `$$` can be used. +You can use `$$` if you have a literal `$` in your value and want to avoid interpolation. -
+## Configuration management tools -## Configuration Management Tools - -Currently we do not provide any scripts/manifests for configuring Grafana. Rather than spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefore, we heavily rely on the expertise of the community. +Currently, we don't provide any scripts or manifests for configuring Grafana. +Rather than spending time learning and creating scripts or manifests for each tool, we think our time is better spent making Grafana easier to provision. +Therefore, we heavily rely on the expertise of the community. | Tool | Project | | --------- | ------------------------------------------------------------------------------------------------------------------------------- | @@ -70,12 +73,8 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe ## Data sources -{{% admonition type="note" %}} -Available in Grafana v5.0 and higher. -{{% /admonition %}} - -You can manage data sources in Grafana by adding YAML configuration files in the [`provisioning/datasources`]({{< relref "../../setup-grafana/configure-grafana#provisioning" >}}) directory. -Each config file can contain a list of `datasources` to add or update during startup. +You can manage data sources in Grafana by adding YAML configuration files in the [`provisioning/data sources`]({{< relref "../../setup-grafana/configure-grafana#provisioning" >}}) directory. +Each configuration file can contain a list of `datasources` to add or update during startup. If the data source already exists, Grafana reconfigures it to match the provisioned configuration file. The configuration file can also list data sources to automatically delete, called `deleteDatasources`. @@ -85,17 +84,17 @@ You can configure Grafana to automatically delete provisioned data sources when To do so, add `prune: true` to the root of your provisioning file. With this configuration, Grafana also removes the provisioned data sources if you remove the provisioning file entirely. -{{% admonition type="note" %}} +{{< admonition type="note" >}} The `prune` parameter is available in Grafana v11.1 and higher. -{{% /admonition %}} +{{< /admonition >}} ### Running multiple Grafana instances If you run multiple instances of Grafana, add a version number to each data source in the configuration and increase it when you update the configuration. -Grafana updates only data sources with the same or lower version number than specified in the config. +Grafana updates only data sources with the same or lower version number than specified in the configuration. This prevents old configurations from overwriting newer ones if you have different versions of the `datasource.yaml` file that don't define version numbers, and then restart instances at the same time. -### Example data source config file +### Example data source configuration file This example provisions a [Graphite data source]({{< relref "../../datasources/graphite" >}}): @@ -179,16 +178,16 @@ datasources: For provisioning examples of specific data sources, refer to that [data source's documentation]({{< relref "../../datasources" >}}). -#### JSON Data +#### JSON data -Since not all data sources have the same configuration settings, we include only the most common ones as fields. +Not all data sources have the same configuration settings. Only the most common fields are included in examples. To provision the rest of a data source's settings, include them as a JSON blob in the `jsonData` field. Common settings in the [built-in core data sources]({{< relref "../../datasources#built-in-core-data-sources" >}}) include: -{{% admonition type="note" %}} +{{< admonition type="note" >}} Data sources tagged with _HTTP\*_ communicate using the HTTP protocol, which includes all core data source plugins except MySQL, PostgreSQL, and MSSQL. -{{% /admonition %}} +{{< /admonition >}} | Name | Type | Data source | Description | | ----------------------------- | ------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -249,11 +248,14 @@ For examples of specific data sources' JSON data, refer to that [data source's d #### Secure JSON Data -Secure JSON data is a map of settings that will be encrypted with [secret key]({{< relref "../../setup-grafana/configure-grafana#secret_key" >}}) from the Grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to the request on the server side. All of these settings are optional. +Secure JSON data is a map of settings that are encrypted with a [secret key]({{< relref "../../setup-grafana/configure-grafana#secret_key" >}}) from the Grafana configuration. +The encryption hides content from the users of the application. +This should be used for storing the TLS Cert and password that Grafana appends to the request on the server side. +All of these settings are optional. -{{% admonition type="note" %}} -The _HTTP\*_ tag denotes data sources that communicate using the HTTP protocol, including all core data source plugins except MySQL, PostgreSQL, and MSSQL. -{{% /admonition %}} +{{< admonition type="note" >}} +The _HTTP\*_ tag denotes data sources that communicate using the HTTP protocol, including all core data source plugins except MySQL, PostgreSQL, and MS SQL. +{{< /admonition >}} | Name | Type | Data source | Description | | ----------------- | ------ | ---------------------------------- | -------------------------------------------------------- | @@ -270,7 +272,7 @@ The _HTTP\*_ tag denotes data sources that communicate using the HTTP protocol, #### Custom HTTP headers for data sources Data sources managed with provisioning can be configured to add HTTP headers to all requests. -The header name is configured in the `jsonData` field and the header value is configured in `secureJsonData`. +Configure the header name in the `jsonData` field and the header value in `secureJsonData`. ```yaml apiVersion: 1 @@ -287,16 +289,14 @@ datasources: ## Plugins -{{% admonition type="note" %}} -Available in Grafana v7.1 and higher. -{{% /admonition %}} +You can manage plugin applications in Grafana by adding one or more YAML configuration files in the [`provisioning/plugins`]({{< relref "../../setup-grafana/configure-grafana#provisioning" >}}) directory. +Each configuration file can contain a list of `apps` that update during start up. +Grafana updates each app to match the configuration file. -You can manage plugin applications in Grafana by adding one or more YAML config files in the [`provisioning/plugins`]({{< relref "../../setup-grafana/configure-grafana#provisioning" >}}) directory. Each config file can contain a list of `apps` that will be updated during start up. Grafana updates each app to match the configuration file. - -{{% admonition type="note" %}} +{{< admonition type="note" >}} This feature enables you to provision plugin configurations, not the plugins themselves. The plugins must already be installed on the Grafana instance. -{{% /admonition %}} +{{< /admonition >}} ### Example plugin configuration file @@ -324,9 +324,10 @@ apps: ## Dashboards -You can manage dashboards in Grafana by adding one or more YAML config files in the [`provisioning/dashboards`]({{< relref "../../setup-grafana/configure-grafana#dashboards" >}}) directory. Each config file can contain a list of `dashboards providers` that load dashboards into Grafana from the local filesystem. +You can manage dashboards in Grafana by adding one or more YAML configuration files in the [`provisioning/dashboards`]({{< relref "../../setup-grafana/configure-grafana#dashboards" >}}) directory. +Each configuration file can contain a list of `dashboards providers` that load dashboards into Grafana from the local filesystem. -The dashboard provider config file looks somewhat like this: +The dashboard provider configuration file looks somewhat like this: ```yaml apiVersion: 1 @@ -355,40 +356,47 @@ providers: foldersFromFilesStructure: true ``` -When Grafana starts, it will update/insert all dashboards available in the configured path. Then later on poll that path every **updateIntervalSeconds** and look for updated json files and update/insert those into the database. +When Grafana starts, it updates and inserts all dashboards available in the configured path. +Then later on, Grafana polls that path every **updateIntervalSeconds**, looks for updated JSON files, and updates and inserts those into the database. > **Note:** Dashboards are provisioned to the root level if the `folder` option is missing or empty. #### Making changes to a provisioned dashboard -It's possible to make changes to a provisioned dashboard in the Grafana UI. However, it is not possible to automatically save the changes back to the provisioning source. -If `allowUiUpdates` is set to `true` and you make changes to a provisioned dashboard, you can `Save` the dashboard then changes will be persisted to the Grafana database. +While you can change a provisioned dashboard in the Grafana UI, those changes can't be saved back to the provisioning source. +If `allowUiUpdates` is set to `true` and you make changes to a provisioned dashboard, you can `Save` the dashboard, then changes persist to the Grafana database. -> **Note:** -> If a provisioned dashboard is saved from the UI and then later updated from the source, the dashboard stored in the database will always be overwritten. The `version` property in the JSON file will not affect this, even if it is lower than the existing dashboard. -> -> If a provisioned dashboard is saved from the UI and the source is removed, the dashboard stored in the database will be deleted unless the configuration option `disableDeletion` is set to true. +{{< admonition type="note" >}} +If a provisioned dashboard is saved from the UI and then later updated from the source, the dashboard stored in the database will always be overwritten. The `version` property in the JSON file won't affect this, even if it's lower than the version of the existing dashboard. + +If a provisioned dashboard is saved from the UI and the source is removed, the dashboard stored in the database is deleted unless the configuration option `disableDeletion` is set to `true`. +{{< /admonition >}} If `allowUiUpdates` is configured to `false`, you are not able to make changes to a provisioned dashboard. When you click `Save`, Grafana brings up a _Cannot save provisioned dashboard_ dialog. The screenshot below illustrates this behavior. Grafana offers options to export the JSON definition of a dashboard. Either `Copy JSON to Clipboard` or `Save JSON to file` can help you synchronize your dashboard changes back to the provisioning source. -Note: The JSON definition in the input field when using `Copy JSON to Clipboard` or `Save JSON to file` will have the `id` field automatically removed to aid the provisioning workflow. +{{< admonition type="note" >}} +The JSON definition in the input field when using `Copy JSON to Clipboard` or `Save JSON to file` has the `id` field automatically removed to aid the provisioning workflow. +{{< /admonition >}} {{< figure src="/static/img/docs/v51/provisioning_cannot_save_dashboard.png" max-width="500px" class="docs-image--no-shadow" >}} -### Reusable Dashboard URLs +### Reusable dashboard URLs -If the dashboard in the JSON file contains an [UID]({{< relref "../../dashboards/build-dashboards/view-dashboard-json-model" >}}), Grafana forces insert/update on that UID. This allows you to migrate dashboards between Grafana instances and provisioning Grafana from configuration without breaking the URLs given because the new dashboard URL uses the UID as identifier. -When Grafana starts, it updates/inserts all dashboards available in the configured folders. If you modify the file, then the dashboard is also updated. -By default, Grafana deletes dashboards in the database if the file is removed. You can disable this behavior using the `disableDeletion` setting. +If the dashboard in the JSON file contains an [UID]({{< relref "../../dashboards/build-dashboards/view-dashboard-json-model" >}}), Grafana forces insert/update on that UID. +This allows you to migrate dashboards between Grafana instances and provisioning Grafana from configuration without breaking the URLs given because the new dashboard URL uses the UID as identifier. +When Grafana starts, it updates and inserts all dashboards available in the configured folders. +If you modify the file, then the dashboard is also updated. +By default, Grafana deletes dashboards in the database if the file is removed. +You can disable this behavior using the `disableDeletion` setting. -{{% admonition type="note" %}} +{{< admonition type="note" >}} Provisioning allows you to overwrite existing dashboards which leads to problems if you reuse settings that are supposed to be unique. Be careful not to reuse the same `title` multiple times within a folder -or `uid` within the same installation as this will cause weird behaviors. -{{% /admonition %}} +or `uid` within the same installation as this causes weird behaviors. +{{< /admonition >}} ### Provision folders structure from filesystem to Grafana @@ -406,7 +414,7 @@ For example, to replicate these dashboards structure from the filesystem to Graf └── /resources_dashboard.json ``` -you need to specify just this short provision configuration file. +You need to specify just this short provision configuration file. ```yaml apiVersion: 1 @@ -420,32 +428,24 @@ providers: foldersFromFilesStructure: true ``` -`server` and `application` will become new folders in Grafana menu. - -{{% admonition type="note" %}} -`folder` and `folderUid` options should be empty or missing to make `foldersFromFilesStructure` work. -{{% /admonition %}} - -{{% admonition type="note" %}} -To provision dashboards to the root level, store them in the root of your `path`. -{{% /admonition %}} +In this example, `server` and `application` become new folders in the Grafana menu. {{< admonition type="note" >}} -This feature doesn't currently allow you to create nested folder structures, that is, where you have folders within folders. +The `folder` and `folderUid` options should be empty or missing to make `foldersFromFilesStructure` work. + +To provision dashboards to the root level, store them in the root of your `path`. + +You can't create nested folders structures, where you have folders within folders. {{< /admonition >}} ## Alerting For information on provisioning Grafana Alerting, refer to [Provision Grafana Alerting resources]({{< relref "../../alerting/set-up/provision-alerting-resources/" >}}). -### Supported Settings +### Supported settings The following sections detail the supported settings and secure settings for each alert notification type. Secure settings are stored encrypted in the database and you add them to `secure_settings` in the YAML file instead of `settings`. -{{% admonition type="note" %}} -Secure settings is supported since Grafana v7.2. -{{% /admonition %}} - #### Alert notification `pushover` | Name | Secure setting | diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 9ab472e172a..65bd708d356 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -256,7 +256,7 @@ You can configure the alert instance state when its evaluation returns no data: | No Data configuration | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | No Data | The default option. Sets alert instance state to `No data`.
The alert rule also creates a new alert instance `DatasourceNoData` with the name and UID of the alert rule, and UID of the datasource that returned no data as labels. | -| Alerting | Sets alert instance state to `Alerting`. It waits until the [pending period](ref:pending-period) has finished. | +| Alerting | Sets alert instance state to `Alerting`. It transitions from `Pending` to `Alerting` after the [pending period](ref:pending-period) has finished. | | Normal | Sets alert instance state to `Normal`. | | Keep Last State | Maintains the alert instance in its last state. Useful for mitigating temporary issues, refer to [Keep last state](ref:keep-last-state). | @@ -265,7 +265,7 @@ You can also configure the alert instance state when its evaluation returns an e | Error configuration | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Error | The default option. Sets alert instance state to `Error`.
The alert rule also creates a new alert instance `DatasourceError` with the name and UID of the alert rule, and UID of the datasource that returned no data as labels. | -| Alerting | Sets alert instance state to `Alerting`. It waits until the [pending period](ref:pending-period) has finished. | +| Alerting | Sets alert instance state to `Alerting`. It transitions from `Pending` to `Alerting` after the [pending period](ref:pending-period) has finished. | | Normal | Sets alert instance state to `Normal`. | | Keep Last State | Maintains the alert instance in its last state. Useful for mitigating temporary issues, refer to [Keep last state](ref:keep-last-state). | diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md index 5de512dd012..ea83debb9dc 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md @@ -53,6 +53,19 @@ The pending period specifies how long the condition must be met before firing, e You can also set the pending period to zero to skip it and have the alert fire immediately once the condition is met. +## Condition operator + +There are several condition operators available. + +- **and**: Two conditions before and after must be true for the overall condition to be true. +- **or**: If one of conditions before and after are true, the overall condition is true. +- **logic-or**: If the condition before logic-or is true, the overall condition is immediately true, without evaluating subsequent conditions. + +Here are some examples of operators. + +- `TRUE and TRUE or FALSE and FALSE` evaluate to `FALSE`, because last two conditions return `FALSE`. +- `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. + ## Evaluation example Keep in mind: diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md index b40aca49dc4..6b516e80a92 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md @@ -44,13 +44,13 @@ There are three key components that help you understand how your alerts behave d An alert instance can be in either of the following states: -| State | Description | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Normal** | The state of an alert when the condition (threshold) is not met. | -| **Pending** | The state of an alert that has breached the threshold but for less than the [pending period](ref:pending-period). | -| **Alerting** | The state of an alert that has breached the threshold for longer than the [pending period](ref:pending-period). | -| **NoData** | The state of an alert whose query returns no data or all values are null. You can [change the default behavior](/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling). | -| **Error** | The state of an alert when an error or timeout occurred evaluating the alert rule. You can [change the default behavior](/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling). | +| State | Description | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Normal** | The state of an alert when the condition (threshold) is not met. | +| **Pending** | The state of an alert that has breached the threshold but for less than the [pending period](ref:pending-period). | +| **Alerting** | The state of an alert that has breached the threshold for longer than the [pending period](ref:pending-period). | +| **NoData** | The state of an alert whose query returns no data or all values are null. You can [change the default behavior of the no data state](#modify-the-no-data-and-error-state). | +| **Error** | The state of an alert when an error or timeout occurred evaluating the alert rule. You can [change the default behavior of the error state](#modify-the-no-data-and-error-state). | {{< figure src="/media/docs/alerting/alert-instance-states-v3.png" caption="Alert instance state diagram" alt="A diagram of the distinct alert instance states and transitions." max-width="750px" >}} @@ -64,18 +64,37 @@ Alert instances will be routed for [notifications](ref:notifications) when they An alert instance is considered stale if its dimension or series has disappeared from the query results entirely for two evaluation intervals. -Stale alert instances that are in the **Alerting**, **NoData**, or **Error** states transition to the **Normal** state as **Resolved**, and include the `grafana_state_reason` annotation with the value **MissingSeries**. They are routed for notifications like other resolved alert instances. +Stale alert instances that are in the **Alerting**, **NoData**, or **Error** states transition to the **Normal** state as **Resolved**. Once transitioned, these resolved alert instances are routed for notifications like other resolved alerts. -### Keep last state +### Modify the no data and error state -The "Keep Last State" option helps mitigate temporary data source issues, preventing alerts from unintentionally firing, resolving, and re-firing. - -In [Configure no data and error handling,](ref:no-data-and-error-handling) you can decide to keep the last state of the alert instance when a `NoData` and/or `Error` state is encountered. Just like normal evaluation, the alert instance transitions from `Pending` to `Alerting` after the pending period has elapsed. +In [Configure no data and error handling](ref:no-data-and-error-handling), you can change the default behaviour when the evaluation returns no data or an error. You can set the alert instance state to `Alerting`, `Normal`, or keep the last state. {{< figure src="/media/docs/alerting/alert-rule-configure-no-data-and-error.png" alt="A screenshot of the `Configure no data and error handling` option in Grafana Alerting." max-width="500px" >}} +#### Keep last state + +The "Keep Last State" option helps mitigate temporary data source issues, preventing alerts from unintentionally firing, resolving, and re-firing. + However, in situations where strict monitoring is critical, relying solely on the "Keep Last State" option may not be appropriate. Instead, consider using an alternative or implementing additional alert rules to ensure that issues with prolonged data source disruptions are detected. +### `grafana_state_reason` annotation + +Occasionally, an alert instance may be in a state that isn't immediately clear to everyone. For example: + +- Stale alert instances in the `Alerting` state transition to the `Normal` state when the series disappear. +- If "no data" handling is configured to transition to a state other than `NoData`. +- If "error" handling is configured to transition to a state other than `Error`. +- If the alert rule is deleted, paused, or updated in some cases, the alert instance also transitions to the `Normal` state. + +In these situations, the evaluation state may differ from the alert state, and it might be necessary to understand the reason for being in that state when receiving the notification. + +The `grafana_state_reason` annotation is included in these situations, providing the reason in the notifications that explain why the alert instance transitioned to its current state. For example: + +- Stale alert instances in the `Normal` state include the `grafana_state_reason` annotation with the value **MissingSeries**. +- If "no data" or "error" handling transitions to the `Normal` state, the `grafana_state_reason` annotation is included with the value **NoData** or **Error**, respectively. +- If the alert rule is deleted or paused, the `grafana_state_reason` is set to **Paused** or **RuleDeleted**. For some updates, it is set to **Updated**. + ### Special alerts for `NoData` and `Error` When evaluation of an alert rule produces state `NoData` or `Error`, Grafana Alerting generates a new alert instance that have the following additional labels: diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 55efac7be87..9e6ece241e1 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -136,7 +136,7 @@ These functions are available for **Reduce** and **Classic condition** expressio An alert condition is the query or expression that determines whether the alert fires or not depending on the value it yields. There can be only one condition which determines the triggering of the alert. -After you have defined your queries and/or expressions, choose one of them as the alert rule condition. By default, the last expression added is used as the alert condition. +After you have defined your queries and expressions, choose one of them as the alert rule condition. By default, the last expression added is used as the alert condition. When the queried data satisfies the defined condition, Grafana triggers the associated alert, which can be configured to send notifications through various channels like email, Slack, or PagerDuty. diff --git a/docs/sources/dashboards/create-manage-playlists/index.md b/docs/sources/dashboards/create-manage-playlists/index.md index 0b916eeb488..07e51ee44b7 100644 --- a/docs/sources/dashboards/create-manage-playlists/index.md +++ b/docs/sources/dashboards/create-manage-playlists/index.md @@ -26,6 +26,10 @@ Grafana automatically scales dashboards to any resolution, which makes them perf You can access the Playlist feature from Grafana's side menu, in the Dashboards submenu. +{{< admonition type="note" >}} +You must have at least Editor role permissions to create and manage playlists. +{{< /admonition >}} + ## Access, share, and control a playlist Use the information in this section to access playlists. Start and control the display of a playlist using one of the six available modes. diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index 425070ce016..ae06d3becc9 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -335,11 +335,11 @@ You can customize the branding options. Report branding: -- **Company logo:** Company logo displayed in the report PDF. It can be configured by specifying a URL, or by uploading a file. Defaults to the Grafana logo. +- **Company logo:** Company logo displayed in the report PDF. It can be configured by specifying a URL, or by uploading a file. The maximum file size is 16 MB. Defaults to the Grafana logo. Email branding: -- **Company logo:** Company logo displayed in the report email. It can be configured by specifying a URL, or by uploading a file. Defaults to the Grafana logo. +- **Company logo:** Company logo displayed in the report email. It can be configured by specifying a URL, or by uploading a file. The maximum file size is 16 MB. Defaults to the Grafana logo. - **Email footer:** Toggle to enable the report email footer. Select **Sent by** or **None**. - **Footer link text:** Text of the link in the report email footer. Defaults to `Grafana`. - **Footer link URL:** Link of the report email footer. diff --git a/docs/sources/datasources/parca.md b/docs/sources/datasources/parca.md index afc90846aea..69aeda93851 100644 --- a/docs/sources/datasources/parca.md +++ b/docs/sources/datasources/parca.md @@ -37,6 +37,12 @@ refs: Grafana ships with built-in support for Parca, a continuous profiling OSS database for analysis of CPU and memory usage, down to the line number and throughout time. Add it as a data source, and you are ready to query your profiles in [Explore](ref:explore). +## Supported Parca versions + +This data source supports these versions of Parca: + +- v0.19+ + ## Configure the Parca data source To configure basic settings for the data source, complete the following steps: diff --git a/docs/sources/datasources/prometheus/configure-prometheus-data-source.md b/docs/sources/datasources/prometheus/configure-prometheus-data-source.md index 96c4af21075..014487ae277 100644 --- a/docs/sources/datasources/prometheus/configure-prometheus-data-source.md +++ b/docs/sources/datasources/prometheus/configure-prometheus-data-source.md @@ -63,15 +63,19 @@ The first option to configure is the name of your connection: - **Default** - Toggle to select as the default name in dashboard panels. When you go to a dashboard panel this will be the default selected data source. -### HTTP section +### Connection section -- **URL** - The URL of your Prometheus server. If your Prometheus server is local, use ``. If it is on a server within a network, this is the URL with port where you are running Prometheus. Example: ``. +- **Prometheus server URL** - The URL of your Prometheus server. If your Prometheus server is local, use `http://localhost:9090`. If it is on a server within a network, this is the URL with port where you are running Prometheus. Example: `http://prometheus.example.orgname:9090`. -- **Allowed cookies** - Specify cookies by name that should be forwarded to the data source. The Grafana proxy deletes all forwarded cookies by default. +{{< admonition type="note" >}} -- **Timeout** - The HTTP request timeout. This must be in seconds. There is no default, so this setting is up to you. +If you're running Grafana and Prometheus together in different container environments, each localhost refers to its own container - if the server URL is localhost:9090, that means port 9090 inside the Grafana container, not port 9090 on the host machine. -### Auth section +You should use the IP address of the Prometheus container, or the hostname if you are using Docker Compose. Alternatively, you can consider `http://host.docker.internal:9090`. + +{{< /admonition >}} + +### Authentication section There are several authentication methods you can choose in the Authentication section. @@ -99,10 +103,16 @@ Use TLS (Transport Layer Security) for an additional layer of security when work - **Value** - The value of the header. -## Additional settings +## Advanced settings Following are additional configuration options. +### Advanced HTTP settings + +- **Allowed cookies** - Specify cookies by name that should be forwarded to the data source. The Grafana proxy deletes all forwarded cookies by default. + +- **Timeout** - The HTTP request timeout. This must be in seconds. The default is 30 seconds. + ### Alerting - **Manage alerts via Alerting UI** - Toggle to enable `Alertmanager` integration for this data source. @@ -121,12 +131,14 @@ Following are additional configuration options. ### Performance -- **Prometheus type** - The type of your Prometheus server. There are four options: `Prometheus`, `Cortex`, `Thanos`, `Mimir`. +- **Prometheus type** - The type of your Prometheus server. There are four options: `Prometheus`, `Cortex`, `Mimir`, and `Thanos`. -- **Version** Select the version you are using. Once the Prometheus type has been selected, a list of versions auto-populates using the Prometheus [buildinfo](https://semver.org/) API. The `Cortex` Prometheus type does not support this API so you will need to manually add the version. +- **Cache level** - The browser caching level for editor queries. There are four options: `Low`, `Medium`, `High`, or `None`. - **Incremental querying (beta)** - Changes the default behavior of relative queries to always request fresh data from the Prometheus instance. Enable this option to decrease database and network load. +- **Disable recording rules (beta)** - Toggle on to disable the recording rules. Enable this option to improve dashboard performance. + ### Other - **Custom query parameters** - Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. diff --git a/docs/sources/datasources/pyroscope/_index.md b/docs/sources/datasources/pyroscope/_index.md index 072803da3b6..ab4be10a69e 100644 --- a/docs/sources/datasources/pyroscope/_index.md +++ b/docs/sources/datasources/pyroscope/_index.md @@ -16,8 +16,8 @@ labels: - cloud - enterprise - oss -title: Grafana Pyroscope -weight: 1150 +title: Pyroscope +weight: 1350 refs: flame-graph: - pattern: /docs/grafana/ diff --git a/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md index 5d382cca447..8a344f6a7e6 100644 --- a/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md +++ b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md @@ -33,98 +33,93 @@ refs: destination: /docs/grafana//datasources/tempo/configure-tempo-data-source/ - pattern: /docs/grafana-cloud/ destination: docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source/ + provisioning-data-sources: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/provisioning/#data-sources + - pattern: /docs/grafana-cloud/provision + destination: /docs/grafana//administration/provisioning/#data-sources --- # Configure the Grafana Pyroscope data source -To configure basic settings for the data source, complete the following steps: +The Pyroscope data source sets how Grafana connects to your Pyroscope database. -1. Click **Connections** in the left-side menu. -1. Under Your connections, click **Data sources**. -1. Enter `Grafana Pyroscope` in the search bar. -1. Select **Add new data source**. -1. Click **Grafana Pyroscope** to display the **Settings** tab of the data source. -1. Set the data source's basic configuration options. -1. Select **Save & test**. - -## Configuration options - -You can configure several options for the Pyroscope data source, including the name, HTTP, authentication, querying, and private data source connect. +You can configure the data source using either the data source interface in Grafana or using a configuration file. +This page explains how to set up and enable the data source capabilities using Grafana. If you make any changes, select **Save & test** to preserve those changes. -![Configuration options for the Pyroscope data source](/media/docs/grafana/data-sources/screenshot-pyroscope-data-source-config.png) +If you're using your own installation of Grafana, you can provision the Pyroscope data source using a YAML configuration file. +For more information about provisioning and available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources). -### Name and default +## Before you begin -**Name** -: Enter a name to specify the data source in panels, queries, and Explore. +To configure a Pyroscope data source, you need administrator rights to your Grafana instance and a Pyroscope instance configured to send data to Grafana. -**Default** -: The default data source is pre-selected for new panels. +If you're provisioning a Pyroscope data source, then you also need administrative rights on the server hosting your Grafana instance. -### HTTP +## Add or modify a data source -The HTTP section is shown in number 1 in the screenshot. +You can use these procedures to configure a new Pyroscope data source or to edit an existing one. -**URL** -: The URL of the Grafana Pyroscope instance, for example, `https://localhost:4100`. +### Create a new data source -**Allowed cookies** -: The Grafana Proxy deletes forwarded cookies. Use this field to specify cookies by name that should be forwarded to the data source. +To configure basic settings for the data source, complete the following steps: -**Timeout** -: HTTP request timeout in seconds. +1. Select **Connections** in the main menu. +1. Enter `Grafana Pyroscope` in the search bar. +1. Select **Grafana Pyroscope**. +1. Select **Add new data source** in the top-right corner of the page. +1. On the **Settings** tab, complete the **Name**, **Connection**, and **Authentication** sections. -### Auth +- Use the **Name** field to specify the name used for the data source in panels, queries, and Explore. Toggle the **Default** switch for the data source to be pre-selected for new panels. +- Under **Connection**, enter the **URL** of the Pyroscope instance. For example, `https://example.com:4100`. +- Complete the [**Authentication** section](#authentication). -The Auth section is shown in number 2 in the screenshot. +1. Optional: Use **Additional settings** to configure other options. +1. Select **Save & test**. -**Basic auth** -: Enable basic authentication to the data source. When activated, it provides **User** and **Password** fields. +### Update an existing data source -**With Credentials** -: Whether credentials, such as cookies or auth headers, should be sent with cross-site requests. +To modify an existing Pyroscope data source: -**TLS Client Auth** -: Toggle on to use client authentication. When enabled, it adds the **Server name**, **Client cert**, and **Client key** fields. The client provides a certificate that is validated by the server to establish the client's trusted identity. The client key encrypts the data between client and server. These details are encrypted and stored in the Grafana database. +1. Select **Connections** in the main menu. +1. Select **Data sources** to view a list of configured data sources. +1. Select the Pyroscope data source you wish to modify. +1. Optional: Use **Additional settings** to configure or modify other options. +1. After completing your updates, select **Save & test**. -**With CA Cert** -: Activate this option to verify self-signed TLS certificates. +## Authentication -**Skip TLS Verify** -: When activated, it bypasses TLS certificate verification. +Use this section to select an authentication method to access the data source. -**Forward OAuth Identity** -: When activated, the user’s upstream OAuth 2.0 identity is forwarded to the data source along with their access token. +{{< admonition type="note" >}} +Use Transport Layer Security (TLS) for an additional layer of security when working with Pyroscope. +For additional information on setting up TLS encryption with Pyroscope, refer to [Pyroscope configuration](https://grafana.com/docs/pyroscope//configure-server/reference-configuration-parameters/). +{{< /admonition >}} -**Custom HTTP Headers** -: Select Add header to add Header and Value fields. +[//]: # 'Shared content for authentication section procedure in data sources' -**Header** -: Add a custom header. This allows custom headers to be passed based on the needs of your Pyroscope instance. +{{< docs/shared source="grafana" lookup="datasources/datasouce-authentication.md" leveloffset="+2" version="" >}} -**Value** -: The value of the header. +## Additional settings + +Use the down arrow to expand the **Additional settings** section to view these options. + +### Advanced HTTP settings + +The Grafana Proxy deletes forwarded cookies. Use the **Allowed cookies** field to specify cookies that should be forwarded to the data source by name. + +The **Timeout** field sets the HTTP request timeout in seconds. ### Querying -The **Querying** section is shown in number 3 in the screenshot. - **Minimum step** is used for queries returning time-series data. The default value is 15 seconds. Adjusting this option can help prevent gaps when you zoom in to profiling data. ### Private data source connect -The **Private data source connect** section is shown in number 4 in the screenshot. +[//]: # 'Shared content for authentication section procedure in data sources' -This feature is only available in Grafana Cloud. - -This option lets you query data that lives within a secured network without opening the network to inbound traffic from Grafana Cloud. - -Use the drop-down box to select a configured private data sources. - -Select **Manage private data source connect** to configure and manage any private data sources you have configured. - -For more information, refer to [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/). +{{< docs/shared source="grafana" lookup="datasources/datasouce-private-ds-connect.md" leveloffset="+2" version="" >}} diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index b6b2889617a..1a85b2357af 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -47,7 +47,7 @@ refs: provisioning-data-sources: - pattern: /docs/grafana/ destination: /docs/grafana//administration/provisioning/#data-sources - - pattern: /docs/grafana-cloud/ + - pattern: /docs/grafana-cloud/provision destination: /docs/grafana//administration/provisioning/#data-sources explore: - pattern: /docs/grafana/ @@ -56,66 +56,108 @@ refs: destination: /docs/grafana//explore/ --- -# Configure the Tempo data source +# Configure a Tempo data source The Tempo data source sets how Grafana connects to your Tempo database and lets you configure features and integrations with other telemetry signals. -To configure basic settings for the Tempo data source, complete the following steps: +You can configure the data source using either the data source interface in Grafana or using a configuration file. +This page explains how to set up and enable the data source capabilities using Grafana. -1. Click **Connections** in the left-side menu. -1. Under Your connections, click **Data sources**. -1. Enter `Tempo` in the search bar. -1. Select **Tempo**. +If you're using your own installation of Grafana, you can provision the Tempo data source using a YAML configuration file. -1. On the **Settings** tab, set the data source's basic configuration options: +Depending upon your tracing environment, you may have more than one Tempo instance. +Grafana supports multiple Tempo data sources. - | Name | Description | - | -------------- | ------------------------------------------------------------------------ | - | **Name** | Sets the name you use to refer to the data source in panels and queries. | - | **Default** | Sets the data source that's pre-selected for new panels. | - | **URL** | Sets the URL of the Tempo instance, such as `http://tempo`. | - | **Basic Auth** | Enables authentication to the Tempo data source. | - | **User** | Sets the user name for basic authentication. | - | **Password** | Sets the password for basic authentication. | +## Before you begin -You can also configure settings specific to the Tempo data source. +To configure a Tempo data source, you need administrator rights to your Grafana instance and a Tempo instance configured to send tracing data to Grafana. -This video explains how to add data sources, including Loki, Tempo, and Mimir, to Grafana and Grafana Cloud. Tempo data source set up starts at 4:58 in the video. +If you're provisioning a Tempo data source, then you also need administrative rights on the server hosting your Grafana instance. +Refer to [Provision the data source](#provision-the-data-source) for next steps. -{{< youtube id="cqHO0oYW6Ic" start="298" >}} +![Provisioned data source warning](/media/docs/grafana/data-sources/tempo/tempo-data-source-provisioned-error.png) + +## Add or modify a data source + +You can use these procedures to configure a new Tempo data source or to edit an existing one. + +### Add a new data source + +Follow these steps to set up a new Tempo data source: + +1. Select **Connections** in the main menu. +1. Enter `Tempo` in the search bar. +1. Select **Tempo**. +1. Select **Add new data source** in the top-right corner of the page. +1. On the **Settings** tab, complete the **Name**, **Connection**, and **Authentication** sections. + +- Use the **Name** field to specify the name used for the data source in panels, queries, and Explore. Toggle the **Default** switch for the data source to be pre-selected for new panels. +- Under **Connection**, enter the **URL** of the Tempo instance, for example, `https://example.com:4100`. +- Complete the [**Authentication** section](#authentication). + +1. Optional: Configure other sections to add capabilities to your tracing data. Refer to the additional procedures for instructions. +1. Select **Save & test**. + +### Update an existing data source + +To modify an existing Tempo data source: + +1. Select **Connections** in the main menu. +1. Select **Data sources** to view a list of configured data sources. +1. Select the Tempo data source you wish to modify. +1. Configure or update additional sections to add capabilities to your tracing data. Refer to the additional procedures for instructions. +1. After completing your updates, select **Save & test**. + +## Authentication + +Use this section to select an authentication method to access the data source. + +{{< admonition type="note" >}} +Use Transport Layer Security (TLS) for an additional layer of security when working with Tempo. +For additional information on setting up TLS encryption with Tempo, refer to [Configure TLS communication](https://grafana.com/docs/tempo//configuration/network/tls/) and [Tempo configuration](https://grafana.com/docs/tempo//configuration/). +{{< /admonition >}} + +[//]: # 'Shared content for authentication section procedure in data sources' + +{{< docs/shared source="grafana" lookup="datasources/datasouce-authentication.md" leveloffset="+2" version="" >}} ## Streaming -Streaming enables TraceQL query results to be displayed as they become available. Without streaming, no results are displayed until all results have returned. +Streaming enables TraceQL query results to be displayed as they become available. +Without streaming, no results are displayed until all results have returned. {{< docs/public-preview product="TraceQL streaming results" >}} -### Requirements - To use streaming, you need to: -- Be running Tempo version 2.2 or newer, or Grafana Enterprise Traces (GET) version 2.2 or newer, or be using Grafana Cloud Traces. +- Run Tempo version 2.2 or newer, or Grafana Enterprise Traces (GET) version 2.2 or newer, or use Grafana Cloud Traces. - For self-managed Tempo or GET instances: If your Tempo or GET instance is behind a load balancer or proxy that doesn't supporting gRPC or HTTP2, streaming may not work and should be disabled. ### Activate streaming -For streaming to work for a particular Tempo data source, set your Grafana's `traceQLStreaming` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) to true and set **Streaming** to enabled in your Tempo data source configuration. +You can activate streaming by either setting the `traceQLStreaming` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) to true or by activating the **Streaming** toggle in the Tempo data source. ![Streaming section in Tempo data source](/media/docs/grafana/data-sources/tempo-data-source-streaming-v11.2.png) If you are using Grafana Cloud, the `traceQLStreaming` feature toggle is already set to `true` by default. -If the Tempo data source is set to allow streaming but the `traceQLStreaming` feature toggle is set to `false` in Grafana, no streaming will occur. +If the Tempo data source is set to allow streaming but the `traceQLStreaming` feature toggle is set to `false` in Grafana, streaming occurs. -If the data source has streaming disabled and `traceQLStreaming` is set to `true`, no streaming will happen for that data source. +If the data source has streaming disabled and `traceQLStreaming` is set to `true`, streaming happens for that data source. + +When streaming is active, it's shows as **Enabled** in **Explore**. +To check the status, select Explore in the menu, select your Tempo data source, and expand the **Options** section. + +![The Explore screen shows the Tempo data source with streaming active](/media/docs/grafana/data-sources/tempo/tempo-query-stream-active.png) ## Trace to logs The **Trace to logs** setting configures [trace to logs](ref:explore-trace-integration) that's available when you integrate Grafana with Tempo. +Trace to logs can also be used with other tracing data sources, such as Jaeger and Zipkin. -![Trace to logs settings](/media/docs/tempo/tempo-trace-to-logs-9-4.png) +![Trace to logs settings](/media/docs/grafana/data-sources/tempo/tempo-data-source-trace-to-logs.png) There are two ways to configure the trace to logs feature: @@ -171,7 +213,9 @@ There are two ways to configure the trace to metrics feature: - Use a basic configuration with a default query, or - Configure one or more custom queries where you can use a [template language](ref:variable-syntax) to interpolate variables from the trace or span. -Refer to the Trace to metrics configuration options section to learn about the available options. +Refer to the [Trace to metrics configuration options](#trace-tometrics-configuration-options) section to learn about the available options. + +![Trace to metrics settings in the Tempo data source](/media/docs/grafana/data-sources/tempo/tempo-data-source-trace-to-metrics.png) ### Set up a simple configuration @@ -233,7 +277,7 @@ To use custom queries with the configuration, follow these steps: [//]: # 'Shared content for Trace to profiles in the Tempo data source' -{{< docs/shared source="grafana" lookup="datasources/tempo-traces-to-profiles.md" leveloffset="+1" version="" >}} +{{< docs/shared source="grafana" lookup="datasources/tempo-traces-to-profiles.md" leveloffset="+2" version="" >}} ## Custom query variables @@ -252,27 +296,37 @@ For example, `${__span.name}`. | **\_\_trace.duration** | The duration of the trace. | | **\_\_trace.name** | The name of the trace. | -## Service Graph +## Additional settings -The **Service Graph** setting configures the [Service Graph](/docs/tempo/latest/metrics-generator/service_graphs/enable-service-graphs/) feature. +Use the down arrow to expand the **Additional settings** section to view these options. + +### Advanced HTTP settings + +The Grafana Proxy deletes forwarded cookies. Use the **Allowed cookies** field to specify cookies by name that should be forwarded to the data source. + +The **Timeout** field sets the HTTP request timeout in seconds. + +### Service graph + +The **Service graph** setting configures the [Service Graph](/docs/tempo/latest/metrics-generator/service_graphs/enable-service-graphs/) data. Configure the **Data source** setting to define in which Prometheus instance the Service Graph data is stored. To use the Service Graph, refer to the [Service Graph documentation](#use-the-service-graph). -## Node Graph +### Node graph -The **Node Graph** setting enables the [node graph visualization](ref:node-graph), which is disabled by default. +The **Node graph** setting enables the [node graph visualization](ref:node-graph), which isn't activated by default. -Once enabled, Grafana displays the node graph above the trace view. +Once activated, Grafana displays the node graph above the trace view. -## Tempo search +### Tempo search The **Search** setting configures [Tempo search](/docs/tempo/latest/configuration/#search). You can configure the **Hide search** setting to hide the search query option in **Explore** if search is not configured in the Tempo instance. -## TraceID query +### TraceID query The **TraceID query** setting modifies how TraceID queries are run. The time range can be used when there are performance issues or timeouts since it will narrow down the search to the defined range. This setting is disabled by default. @@ -284,7 +338,7 @@ You can configure this setting as follows: | **Time shift start** | Time shift for start of search. Default: `30m`. | | **Time shift end** | Time shift for end of search. Default: `30m`. | -## Span bar +### Span bar The **Span bar** setting helps you display additional information in the span bar row. @@ -296,12 +350,30 @@ You can choose one of three options: | **Duration** | _(Default)_ Displays the span duration on the span bar row. | | **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `component`. | +### Private data source connect + +[//]: # 'Shared content for authentication section procedure in data sources' + +{{< docs/shared source="grafana" lookup="datasources/datasouce-private-ds-connect.md" leveloffset="+2" version="" >}} + ## Provision the data source -You can define and configure the Tempo data source in YAML files as part of Grafana's provisioning system. +You can define and configure the Tempo data source in YAML files as part of the Grafana provisioning system. +Provisioning is primarily used Grafana instances that don't use Grafana Cloud. + +You can use version control, like git, to track and manage file changes. +Changes can be updated or rolled back as needed. + For more information about provisioning and available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources). -Example provision YAML file: +{{< admonition type="note" >}} +You can't modify a provisioned data source using the Tempo data source settings in Grafana. +Grafana displays a message for provisioned data sources. +{{< /admonition >}} + +### Example file + +This example provision YAML file sets up the equivalents of the options available in the Tempo data source user interface. ```yaml apiVersion: 1 diff --git a/docs/sources/explore/_index.md b/docs/sources/explore/_index.md index 9bc40ed1f7c..0deda3011d3 100644 --- a/docs/sources/explore/_index.md +++ b/docs/sources/explore/_index.md @@ -10,159 +10,53 @@ labels: - cloud - enterprise - oss +menuTitle: Explore title: Explore weight: 90 +hero: + title: Explore + level: 1 + width: 110 + height: 110 + description: >- + Use Explore to query, collect, and analyze data for detailed real-time data analysis. +cards: + title_class: pt-0 lh-1 + items: + - title: Get started with Explore + href: ./get-started-with-explore/ + description: Get started using Explore to create queries and do real-time analysis on your data. + height: 24 + - title: Query management + href: ./query-management/ + description: Learn how to manage queries in Explore. + height: 24 + - title: Query inspector in Explore + href: ./explore-inspector/ + description: Learn how to use the Query inspector to troubleshoot issues with your queries. + height: 24 + - title: Logs in Explore + href: ./logs-integration/ + description: Learn about working with logs and log data in Explore. + height: 24 + - title: Traces in Explore + href: ./trace-integration/ + description: Learn about working with traces and tracing data in Explore. + height: 24 + - title: Correlations editor in Explore + href: ./correlations-editor-in-explore/ + description: Learn how to create and use Correlations. + height: 24 --- -# Explore +{{< docs/hero-simple key="hero" >}} -Grafana's dashboard UI is all about building dashboards for visualization. Explore strips away the dashboard and panel options so that you can focus on the query. It helps you iterate until you have a working query and then think about building a dashboard. +--- -> Refer to [Role-based access control]({{< relref "../administration/roles-and-permissions/access-control/" >}}) in Grafana Enterprise to understand how you can control access with role-based permissions. +## Overview -If you just want to explore your data and do not want to create a dashboard, then Explore makes this much easier. If your data source supports graph and table data, then Explore shows the results both as a graph and a table. This allows you to see trends in the data and more details at the same time. See also: +Explore is your starting point for querying, analyzing, and aggregating data in Grafana. You can quickly begin creating queries to start analyzing data without having to create a dashboard or customize a visualization. -- [Query management in Explore]({{< relref "query-management/" >}}) -- [Logs integration in Explore]({{< relref "logs-integration/" >}}) -- [Trace integration in Explore]({{< relref "trace-integration/" >}}) -- [Explore metrics]({{< relref "explore-metrics/" >}}) -- [Correlations Editor in Explore]({{< relref "correlations-editor-in-explore/" >}}) -- [Inspector in Explore]({{< relref "explore-inspector/" >}}) +## Explore -## Start exploring - -{{< youtube id="1q3YzX2DDM4" >}} - -> Refer to [Role-based access Control]({{< relref "../administration/roles-and-permissions/access-control/" >}}) in Grafana Enterprise to understand how you can manage Explore with role-based permissions. - -In order to access Explore, you must have an editor or an administrator role, unless the [viewers_can_edit option]({{< relref "../setup-grafana/configure-grafana/#viewers_can_edit" >}}) is enabled. Refer to [About users and permissions]({{< relref "../administration/roles-and-permissions/" >}}) for more information on what each role has access to. - -{{% admonition type="note" %}} -If you are using Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to enable the `viewers_can_edit` option -{{% /admonition %}} - -To access Explore: - -1. Click on the Explore icon on the menu bar. - - An empty Explore tab opens. - - Alternately to start with an existing query in a panel, choose the Explore option from the Panel menu. This opens an Explore tab with the query from the panel and allows you to tweak or iterate in the query outside of your dashboard. - - {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-panel-menu-10.1.png" class="docs-image--no-shadow" max-width= "650px" caption="Screenshot of the panel menu including the Explore option" >}} - -1. Choose your data source from the drop-down in the top left. - - You can also click **Open advanced data source picker** to see more options, including adding a data source (Admins only). - -1. Write the query using a query editor provided by the selected data source. Please check [data sources documentation]({{< relref "../datasources" >}}) to see how to use various query editors. -1. For general documentation on querying data sources in Grafana, see [Query and transform data]({{< relref "../panels-visualizations/query-transform-data" >}}). -1. Run the query using the button in the top right corner. - -## Split and compare - -The split view provides an easy way to compare visualizations side-by-side or to look at related data together on one page. - -To open the split view: - -1. Click the split button to duplicate the current query and split the page into two side-by-side queries. - -It is possible to select another data source for the new query which for example, allows you to compare the same query for two different servers or to compare the staging environment to the production environment. - -{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-explore-split-10.1.png" max-width= "950px" caption="Screenshot of Explore screen split" >}} - -In split view, timepickers for both panels can be linked (if you change one, the other gets changed as well) by clicking on one of the time-sync buttons attached to the timepickers. Linking of timepickers helps with keeping the start and the end times of the split view queries in sync. It ensures that you’re looking at the same time interval in both split panels. - -To close the newly created query, click on the Close Split button. - -## Content outline - -The content outline is a side navigation bar that keeps track of the queries and visualization panels you created in Explore. It allows you to navigate between them quickly. - -The content outline also works in a split view. When you are in split view, the content outline is generated for each pane. - -To open the content outline: - -1. Click the Outline button in the top left corner of the Explore screen. - -You can then click on any panel icon in the content outline to navigate to that panel. - -### Filter logs in content outline - -When using Explore with logs, you can filter the logs in the content outline. You can filter by log level, which is currently supported for Elasticsearch and Loki data sources. To select multiple filters, press Command-click on a Mac or Ctrl+Click in Windows. - -{{% admonition type="note" %}} -Log levels only show if the datasource supports the log volume histogram and contains multiple levels. Additionally, the query to the data source may have to format the log lines to see the levels. For example, in Loki, the `logfmt` parser commonly will display log levels. -{{% /admonition %}} - -{{< figure src="/media/docs/grafana/explore/screenshot-explore-content-outline-logs-filtering-11.2.png" max-width= "950px" caption="Screenshot of Explore content outline logs filtering" >}} - -### Pin logs to content outline - -When using Explore with logs, you can pin logs to content outline by hovering over a log in the logs panel and clicking on the _Pin to content outline_ icon in the log row menu. - -{{< figure src="/media/docs/grafana/explore/screenshot-explore-content-outline-logs-pinning-11.2.png" max-width= "450px" caption="Screenshot of Explore content outline logs pinning" >}} - -Clicking on a pinned log opens the [log context modal](https://grafana.com/docs/grafana//explore/logs-integration/#log-context), showing the log highlighted in context with other logs. From here, you can also open the log in split mode to preserve the time range in the left pane while having the time range specific to that log in the right pane. - -## Share Explore URLs - -When using Explore, the URL in the browser address bar updates as you make changes to the queries. You can share or bookmark this URL. - -{{% admonition type="note" %}} -Explore may generate relatively long URLs, some tools, like messaging or videoconferencing apps, may truncate messages to a fixed length. In such cases Explore will display a warning message and load a default state. If you encounter issues when sharing Explore links in such apps, you can generate shortened links. See [Share shortened link](#share-shortened-link) for more information. -{{% /admonition %}} - -### Generating Explore URLs from external tools - -Because Explore URLs have a defined structure, you can build a URL from external tools and open it in Grafana. The URL structure is: - -``` -http:///explore?panes=&schemaVersion=&orgId= -``` - -where: - -- `org_id` is the organization ID -- `schema_version` is the schema version (should be set to the latest version which is `1`) -- `panes` is a url-encoded JSON object of panes, where each key is the pane ID and each value is an object matching the following schema: - -``` -{ - datasource: string; // the pane's root datasource UID, or `-- Mixed --` for mixed datasources - queries: { - refId: string; // an alphanumeric identifier for this query, must be unique within the pane, i.e. "A", "B", "C", etc. - datasource: { - uid: string; // the query's datasource UID ie: "AD7864H6422" - type: string; // the query's datasource type-id, i.e: "loki" - } - // ... any other datasource-specific query parameters - }[]; // array of queries for this pane - range: { - from: string; // the start time, in milliseconds since epoch - to: string; // the end time, in milliseconds since epoch - } -} -``` - -{{% admonition type="note" %}} -The `from` and `to` also accept relative ranges defined in [Time units and relative ranges]({{< relref "../dashboards/use-dashboards/#time-units-and-relative-ranges" >}}). -{{% /admonition %}} - -## Share shortened link - -{{% admonition type="note" %}} -Available in Grafana 7.3 and later versions. -{{% /admonition %}} - -The Share shortened link capability allows you to create smaller and simpler URLs of the format /goto/:uid instead of using longer URLs with query parameters. To create a shortened link to the executed query, click the **Share** option in the Explore toolbar. - -A shortened link that is not accessed will automatically get deleted after a [configurable period](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#short_links) (defaulting to seven days). If a link is used at least once, it won't be deleted. - -### Sharing shortened links with absolute time - -{{% admonition type="note" %}} -Available in Grafana 10.3 and later versions. -{{% /admonition %}} - -Short links have two options - keeping relative time (for example, from two hours ago to now) or absolute time (for example, from 8am to 10am). Sharing a shortened link by default will copy the time range selected, relative or absolute. Clicking the dropdown button next to the share shortened link button and selecting one of the options under "Time-Sync URL Links" will allow you to create a short link with the absolute time - meaning anyone receiving the link will see the same data you are seeing, even if they open the link at another time. This will not affect your selected time range. +{{< card-grid key="cards" type="simple" >}} diff --git a/docs/sources/explore/explore-metrics/index.md b/docs/sources/explore/explore-metrics/index.md index 69293a823cb..be3fc570f6c 100644 --- a/docs/sources/explore/explore-metrics/index.md +++ b/docs/sources/explore/explore-metrics/index.md @@ -21,13 +21,15 @@ Explore Metrics is currently in [public preview](/docs/release-life-cycle/). Gra With Explore Metrics, you can: -- easily slice and dice metrics based on their labels, so you can immediately see anomalies and identify issues -- see the right visualization for your metric based on its type (gauge vs. counter, for example) without building it yourself -- surface other metrics relevant to the current metric -- “explore in a drawer” - expand a drawer over a dashboard with more content so you don’t lose your place -- view a history of user steps when navigating through metrics and their filters +- Easily slice and dice metrics based on their labels, so you can immediately see anomalies and identify issues +- See the right visualization for your metric based on its type (gauge vs. counter, for example) without building it yourself +- Surface other metrics relevant to the current metric +- “Explore in a drawer” - expand a drawer over a dashboard with more content so you don’t lose your place +- View a history of user steps when navigating through metrics and their filters +{{< docs/play title="Explore Metrics" url="https://play.grafana.org/explore/metrics/trail?from=now-1h&to=now&var-ds=grafanacloud-demoinfra-prom&var-filters=&refresh=&metricPrefix=all" >}} + You can access Explore Metrics either as a standalone experience or as part of Grafana dashboards. ## Standalone experience diff --git a/docs/sources/explore/get-started-with-explore.md b/docs/sources/explore/get-started-with-explore.md new file mode 100644 index 00000000000..321bc709cbb --- /dev/null +++ b/docs/sources/explore/get-started-with-explore.md @@ -0,0 +1,208 @@ +--- +aliases: + - +keywords: + - explore + - loki + - logs +labels: + products: + - cloud + - enterprise + - oss +title: Get started with Explore +weight: 5 +--- + +# Get started with Explore + +Explore is your gateway for querying, analyzing, and aggregating data in Grafana. It allows you to visually explore and iterate until you develop a working query or set of queries for building visualizations and conducting data analysis. If your data source supports graph and table data, there's no need to create a dashboard, as Explore can display the results in both formats. This facilitates quick, detailed, real-time data analysis. + +With Explore you can: + +- Create visualizations to integrate into your dashboards. +- Create queries using mixed data sources. +- Create multiple queries within a single interface. +- Understand the shape of your data across various data sources. +- Perform real time data exploration and analysis. + +Key features include: + +- Query editor, based on specific data source, to create and iterate queries. +- [Query history](https://grafana.com/docs/grafana//explore/query-management/) to track and maintain your queries. +- [Query inspector](https://grafana.com/docs/grafana//explore/explore-inspector/) to help troubleshoot query performance. + +Watch the following video to get started using Explore: + +{{< youtube id="1q3YzX2DDM4" >}} + +## Before you begin + +In order to access Explore, you must have either the `editor` or `administrator` role, unless the [`viewers_can_edit` option](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#viewers_can_edit) is enabled. Refer to [Role and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions/) for more information on what each role can access. + +Refer to [Role-based access control (RBAC)](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control/) in Grafana Enterprise to understand how you can manage Explore with role-based permissions. + +{{< admonition type="note" >}} +If you are using Grafana Cloud, open a [support ticket in the Cloud Portal](/https://grafana.com/auth/sign-in) to enable the `viewers_can_edit` option. +{{< /admonition >}} + +## Explore elements + +Explore consists of a toolbar, outline, query editor, the ability to add multiple queries, a query history and a query inspector. + +- **Outline** - Keeps track of the queries and visualization panels created in Explore. Refer to [Content outline](#content-outline) for more detail. + +- **Toolbar** - Provides quick access to frequently used tools and settings. + + - **Data source picker** - Select a data source from the dropdown menu, or use absolute time. + - **Split** - Click to compare visualizations side by side. Refer to [Split and compare](#split-and-compare) for additional detail. + - **Add** - Click to add your exploration to a dashboard. You can also use this to declare an incident,create a forecast, detect outliers and to run an investigation. + - **Time picker** - Select a time range form the time picker. You can also enter an absolute time range. Refer to [Time picker](#time-picker) for more information. + - **Run query** - Click to run your query. + +- **Query editor** - Interface where you construct the query for a specific data source. Query editor elements differ based on data source. In order to run queries across multiple data sources you need to select **Mixed** from the data source picker. + +- **+Add query** - Add additional queries. +- **Query history** - Query history contains the list of queries that you created in Explore. Refer to [Query history](/docs/grafana//explore/query-management/#query-history) for detailed information on working with your query history. +- **Query inspector** - Provides detailed statistics regarding your query. Inspector functions as a kind of debugging tool that "inspects" your query. It provides query statistics under **Stats**, request response time under **Query**, data frame details under **{} JSON**, and the shape of your data under **Data**. Refer to [Query inspector in Explore](/docs/grafana/latest/explore/explore-inspector/) for additional information. + +## Access Explore + +To access Explore: + +1. Click on **Explore** in the left side menu. + + To start with an existing query from a dashboard panel, select the Explore option from the Panel menu in the upper right. This opens an Explore page with the panel's query, enabling you to tweak or iterate the query outside your dashboard. + + {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-panel-menu-10.1.png" class="docs-image--no-shadow" caption="Panel menu with Explore option" >}} + +1. Select a data source from the drop-down in the upper left. + +1. Using the query editor provided for the specific data source, begin writing your query. Each query editor differs based on each data source's unique elements. + +Some query editors provide a **Kick start your query** option, which gives you a list of basic pre-written queries. Refer to [Use query editors](https://grafana.com/docs/grafana//datasources/#use-query-editors) to see how to use various query editors. For general information on querying data sources in Grafana, refer to [Query and transform data](https://grafana.com/docs/grafana//panels-visualizations/query-transform-data/). + +Based on specific data source, certain query editors allow you to select the label or labels to add to your query. Labels are fields that consist of key/value pairs representing information in the data. Some data sources allow for selecting fields. + +1. Click **Run query** in the upper right to run your query. + +## Content outline + +The content outline is a side navigation bar that keeps track of the queries and visualizations you created in Explore. It allows you to navigate between them quickly. + +The content outline works in a split view, with a separate outline generated for each pane. + +To open the content outline: + +1. Click the Outline button in the top left corner of the Explore screen. + +You can then click on any panel icon in the content outline to navigate to that panel. + +## Split and compare + +The split view enables easy side-by-side comparison of visualizations or simultaneous viewing of related data on a single page. + +To open the split view: + +1. Click the split button to duplicate the current query and split the page into two side-by-side queries. +1. Run and re-run queries as often as needed. + +You can select a different data source, or different metrics and label filters for the new query, allowing you to compare the same query across two different servers or compare the staging environment with the production environment. + +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-explore-split-10.1.png" max-width= "950px" caption="Screenshot of Explore screen split" >}} + +You can also link the time pickers for both panels by clicking on one of the time-sync buttons attached to the time pickers. When linked, changing the time in one panel automatically updates the other, keeping the start and end times synchronized. This ensures that both split panels display data for the same time interval. + +Click **Close** to quit split view. + +## Time picker + +Use the time picker to select a time range for your query. The default is **last hour**. You can select a different option from the dropdown or use an absolute time range. You can also change the timezone associated with the query, or use a fiscal year. + +1. Click **Change time settings** to change the timezone or apply a fiscal year. + +Refer to [Set dashboard time range](https://grafana.com/docs/grafana//dashboards/use-dashboards/#set-dashboard-time-range) for more information on absolute and relative time ranges. You can also [control the time range using a URL](https://grafana.com/docs/grafana//dashboards/use-dashboards/#control-the-time-range-using-a-url). + +## Mixed data source + +Select **Mixed** from the data source dropdown to run queries across multiple data sources in the same panel. When you select Mixed, you can select a different data source for each new query that you add. + +## Share Explore URLs + +When using Explore, the URL in the browser address bar updates as you make changes to the queries. You can share or bookmark this URL. + +{{% admonition type="note" %}} +Explore may generate long URLs, which some tools, like messaging or videoconferencing applications, might truncate due to fixed message lengths. In such cases, Explore displays a warning and loads a default state. +If you encounter issues when sharing Explore links in these applications, you can generate shortened links. See [Share shortened link](#share-shortened-link) for more information. +{{% /admonition %}} + +### Generate Explore URLs from external tools + +Because Explore URLs have a defined structure, you can build a URL from external tools and open it in Grafana. The URL structure is: + +``` +http:///explore?panes=&schemaVersion=&orgId= +``` + +where: + +- `org_id` is the organization ID +- `schema_version` is the schema version (should be set to the latest version which is `1`) +- `panes` is a URL-encoded JSON object of panes, where each key is the pane ID and each value is an object matching the following schema: + +``` +{ + datasource: string; // the pane's root datasource UID, or `-- Mixed --` for mixed datasources + queries: { + refId: string; // an alphanumeric identifier for this query, must be unique within the pane, i.e. "A", "B", "C", etc. + datasource: { + uid: string; // the query's datasource UID ie: "AD7864H6422" + type: string; // the query's datasource type-id, i.e: "loki" + } + // ... any other datasource-specific query parameters + }[]; // array of queries for this pane + range: { + from: string; // the start time, in milliseconds since epoch + to: string; // the end time, in milliseconds since epoch + } +} +``` + +{{< admonition type="note" >}} +The `from` and `to` also accept relative ranges defined in [Time units and relative ranges](https://grafana.com/docs/grafana//dashboards/use-dashboards/#time-units-and-relative-ranges). +{{< /admonition >}} + +## Share shortened link + +{{< admonition type="note" >}} +Available in Grafana 7.3 and later versions. +{{< /admonition >}} + +The Share shortened link capability allows you to create smaller and simpler URLs of the format `/goto/:uid` instead of using longer URLs with query parameters. To create a shortened link to the executed query, click the **Share** option in the Explore toolbar. + +A shortened link that's not accessed automatically gets deleted after a [configurable period](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#short_links), which defaults to seven days. However, if the link is accessed at least once, it will not be deleted. + +### Share shortened links with absolute time + +{{< admonition type="note" >}} +Available in Grafana 10.3 and later versions. +{{< /admonition >}} + +Shortened links have two options: relative time (e.g., from two hours ago to now) or absolute time (e.g., from 8am to 10am). By default, sharing a shortened link copies the selected time range, whether it's relative or absolute. + +To create a short link with an absolute time: + +1. Click the dropdown button next to the share shortened link button. +1. Select one of the options under **Time-Sync URL Links**. + +This ensures that anyone receiving the link will see the same data you see, regardless of when they open it. Your selected time range will remain unaffected. + +## Next steps + +Now that you are familiar with Explore you can: + +- [Build dashboards](https://grafana.com/docs/grafana//dashboards/build-dashboards/) +- Create a wide variety of [visualizations](https://grafana.com/docs/grafana//panels-visualizations/visualizations/) +- [Work with logs](https://grafana.com/docs/grafana//explore/logs-integration/) +- [Work with traces](https://grafana.com/docs/grafana/) +- [Create and use correlations](https://grafana.com/docs/grafana//explore/correlations-editor-in-explore/) diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index e94c3b32c0f..437e2f1ad2a 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -1428,6 +1428,22 @@ For each generated **Trend** field value, a calculation function can be selected Use this transformation to pivot the data frame, converting rows into columns and columns into rows. This transformation is particularly useful when you want to switch the orientation of your data to better suit your visualization needs. If you have multiple types it will default to string type. +**Before Transformation:** + +| env | January | February | +| ---- | ------- | -------- | +| prod | 1 | 2 | +| dev | 3 | 4 | + +**After applying transpose transformation:** + +| Field | prod | dev | +| -------- | ---- | --- | +| January | 1 | 3 | +| February | 2 | 4 | + +{{< figure src="/media/docs/grafana/transformations/screenshot-grafana-11-2-transpose-transformation.png" class="docs-image--no-shadow" max-width= "1100px" alt="Before and after transpose transformation" >}} + ### Regression analysis Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 7f0257e95ed..f6ad8b08e08 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -952,6 +952,14 @@ This setting is ignored if multiple OAuth providers are configured. Default is ` How many seconds the OAuth state cookie lives before being deleted. Default is `600` (seconds) Administrators can increase this if they experience OAuth login state mismatch errors. +### oauth_refresh_token_server_lock_min_wait_ms + +Minimum wait time in milliseconds for the server lock retry mechanism. Default is `1000` (milliseconds). The server lock retry mechanism is used to prevent multiple Grafana instances from simultaneously refreshing OAuth tokens. This mechanism waits at least this amount of time before retrying to acquire the server lock. + +There are five retries in total, so with the default value, the total wait time (for acquiring the lock) is at least 5 seconds (the wait time between retries is calculated as random(n, n + 500)), which means that the maximum token refresh duration must be less than 5-6 seconds. + +If you experience issues with the OAuth token refresh mechanism, you can increase this value to allow more time for the token refresh to complete. + ### oauth_skip_org_role_update_sync {{% admonition type="note" %}} diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 70a4dcfdae9..d76bb3845ec 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -101,6 +101,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate on-prem dashboards, folders, and data source configurations to your Grafana Cloud stack. | | `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | | `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | +| `accessActionSets` | Introduces action sets for resource permissions. Also ensures that all folder editors and admins can create subfolders without needing any additional permissions. | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | | `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | | `cloudwatchMetricInsightsCrossAccount` | Enables cross account observability for Cloudwatch Metric Insights query builder | @@ -138,6 +139,7 @@ Experimental features might be changed or removed without prior notice. | `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | | `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | | `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | +| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | | `vizAndWidgetSplit` | Split panels between visualizations and widgets | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | @@ -177,7 +179,6 @@ Experimental features might be changed or removed without prior notice. | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | | `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | | `expressionParser` | Enable new expression parser | -| `accessActionSets` | Introduces action sets for resource permissions | | `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | | `queryLibrary` | Enables Query Library feature in Explore | | `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | @@ -191,6 +192,7 @@ Experimental features might be changed or removed without prior notice. | `databaseReadReplica` | Use a read replica for some database queries. | | `alertingApiServer` | Register Alerting APIs with the K8s API server | | `dashboardRestoreUI` | Enables the frontend to be able to restore a recently deleted dashboard | +| `backgroundPluginInstaller` | Enable background plugin installer | | `dataplaneAggregator` | Enable grafana dataplane aggregator | | `adhocFilterOneOf` | Exposes a new 'one of' operator for ad-hoc filters. This operator allows users to filter by multiple values in a single filter. | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md index 2dcfbc23eaf..b3baddc683b 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md @@ -75,19 +75,21 @@ Sign in to Grafana and navigate to **Administration > Authentication > Configure | **Single logout** | The SAML single logout feature enables users to log out from all applications associated with the current IdP session established using SAML SSO. For more information, refer to [SAML single logout documentation]]({{< relref "../saml#single-logout" >}}). | | **Identity provider initiated login** | Enables users to log in to Grafana directly from the SAML IdP. For more information, refer to [IdP initiated login documentation]({{< relref "../saml#idp-initiated-single-sign-on-sso" >}}). | -1. Click **Next: Key and certificate**. +1. Click **Next: Sign requests**. -### 2. Key and Certificate Section +### 2. Sign Requests Section -1. Provide a certificate and a private key that will be used by the service provider (Grafana) and the SAML IdP. +1. In the **Sign requests** field, specify whether you want the outgoing requests to be signed, and, if so, then: - Use the [PKCS #8](https://en.wikipedia.org/wiki/PKCS_8) format to issue the private key. + 1. Provide a certificate and a private key that will be used by the service provider (Grafana) and the SAML IdP. - For more information, refer to an [example on how to generate SAML credentials]({{< relref "../saml#generate-private-key-for-saml-authentication" >}}). + Use the [PKCS #8](https://en.wikipedia.org/wiki/PKCS_8) format to issue the private key. -1. In the **Sign requests** field, specify whether you want the outgoing requests to be signed, and, if so, which signature algorithm should be used. + For more information, refer to an [example on how to generate SAML credentials]({{< relref "../saml#generate-private-key-for-saml-authentication" >}}). - The SAML standard recommends using a digital signature for some types of messages, like authentication or logout requests to avoid [man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). + 1. Choose which signature algorithm should be used. + + The SAML standard recommends using a digital signature for some types of messages, like authentication or logout requests to avoid [man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). 1. Click **Next: Connect Grafana with Identity Provider**. diff --git a/docs/sources/shared/alerts/alerting_provisioning.md b/docs/sources/shared/alerts/alerting_provisioning.md index baa017bb57b..4fef1da7e57 100644 --- a/docs/sources/shared/alerts/alerting_provisioning.md +++ b/docs/sources/shared/alerts/alerting_provisioning.md @@ -1060,7 +1060,7 @@ GET /api/v1/provisioning/templates/:name | Code | Status | Description | Has headers | Schema | | ------------------------------ | --------- | -------------------- | :---------: | ---------------------------------------- | | [200](#route-get-template-200) | OK | NotificationTemplate | | [schema](#route-get-template-200-schema) | -| [404](#route-get-template-404) | Not Found | Not found. | | [schema](#route-get-template-404-schema) | +| [404](#route-get-template-404) | Not Found | GenericPublicError | | [schema](#route-get-template-404-schema) | #### Responses @@ -1074,7 +1074,7 @@ Status: OK ##### 404 - Not found. -Status: Not Found +[GenericPublicError](#generic-public-error) ###### Schema @@ -1086,10 +1086,9 @@ GET /api/v1/provisioning/templates #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------- | --------- | --------------------- | :---------: | ----------------------------------------- | -| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | -| [404](#route-get-templates-404) | Not Found | Not found. | | [schema](#route-get-templates-404-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------- | ------ | --------------------- | :---------: | ----------------------------------------- | +| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | #### Responses @@ -1101,12 +1100,6 @@ Status: OK [NotificationTemplates](#notification-templates) -##### 404 - Not found. - -Status: Not Found - -###### Schema - ### Create a new alert rule. (_RoutePostAlertRule_) ``` @@ -1480,7 +1473,7 @@ PUT /api/v1/provisioning/templates/:name | Code | Status | Description | Has headers | Schema | | ------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------- | | [202](#route-put-template-202) | Accepted | NotificationTemplate | | [schema](#route-put-template-202-schema) | -| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | +| [400](#route-put-template-400) | Bad Request | GenericPublicError | | [schema](#route-put-template-400-schema) | | [409](#route-put-template-409) | Conflict | GenericPublicError | | [schema](#route-put-template-409-schema) | #### Responses @@ -1499,7 +1492,7 @@ Status: Bad Request ###### Schema -[ValidationError](#validation-error) +[GenericPublicError](#generic-public-error) ##### 409 - Conflict diff --git a/docs/sources/shared/datasources/datasouce-authentication.md b/docs/sources/shared/datasources/datasouce-authentication.md new file mode 100644 index 00000000000..4582eb48593 --- /dev/null +++ b/docs/sources/shared/datasources/datasouce-authentication.md @@ -0,0 +1,41 @@ +--- +headless: true +labels: + products: + - enterprise + - oss +--- + +[//]: # 'This file documents the Authentication section for data sources.' +[//]: # 'This shared file is included in these locations:' +[//]: # '/grafana/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md' +[//]: # '/grafana/docs/sources/datasources/tempo/configure-tempo-data-source.md' +[//]: # 'If you make changes to this file, verify that the meaning and content are not changed in any place where the file is included.' +[//]: # 'Any links should be fully qualified and not relative: /docs/grafana/ instead of ../grafana/.' + + + +To set up authentication: + +1. Select an authentication method from the drop-down list: + + - **Basic authentication**: Authenticates your data source using a username and password + - **Forward OAuth identity**: Forwards the OAuth access token and the OIDC ID token, if available, of the user querying to the data source + - **No authentication**: No authentication is required to access the data source + +1. For **Basic authentication** only: Enter the **User** and **Password**. +1. Optional: Complete the **TLS settings** for additional security methods. + + **TLS Client Authentication** + : Toggle on to use client authentication. When enabled, it adds the **Server name**, **Client cert**, and **Client key** fields. The client provides a certificate that is validated by the server to establish the client's trusted identity. The client key encrypts the data between client and server. These details are encrypted and stored in the Grafana database. + + **Add self-signed certificate** + : Activate this option to use a self-signed TLS certificate. You can add your own Certificate Authority (CA) certificate on top of one generated by the certificate authorities for additional security measure. + + **Skip TLS certification validation** + : When activated, it bypasses TLS certificate verification. Not recommended, unless absolutely necessary for testing. + ![Authentication section showing the TLS client certificate options](/media/docs/grafana/data-sources/tempo/tempo-data-source-authentication.png) + +1. Optional: Add **HTTP Headers**. You can pass along additional context and metadata data about the request and response. Select **Add header** to add **Header** and **Value** fields. + +1. Select **Save & test** to preserve your changes. diff --git a/docs/sources/shared/datasources/datasouce-private-ds-connect.md b/docs/sources/shared/datasources/datasouce-private-ds-connect.md new file mode 100644 index 00000000000..4a405e1a23b --- /dev/null +++ b/docs/sources/shared/datasources/datasouce-private-ds-connect.md @@ -0,0 +1,28 @@ +--- +headless: true +labels: + products: + - enterprise + - oss +--- + +[//]: # 'This file documents the Private data source section for data sources.' +[//]: # 'This shared file is included in these locations:' +[//]: # '/grafana/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md' +[//]: # '/grafana/docs/sources/datasources/tempo/configure-tempo-data-source.md' +[//]: # 'If you make changes to this file, verify that the meaning and content are not changed in any place where the file is included.' +[//]: # 'Any links should be fully qualified and not relative: /docs/grafana/ instead of ../grafana/.' + + + +{{< admonition type="note" >}} +This feature is only available in Grafana Cloud. +{{< /admonition >}} + +Use private data source connect (PDC) to connect to and query data within a secure network without opening that network to inbound traffic from Grafana Cloud. + +Refer to [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) for more information on how PDC works and [Configure Grafana private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc) for steps on setting up a PDC connection. + +Use the drop-down list to select a configured private data source. If you make changes, select **Test & save** to preserve your changes. + +Use **Manage private data source connect** to configure and manage any private data sources you have configured. diff --git a/docs/sources/shared/datasources/tempo-traces-to-profiles.md b/docs/sources/shared/datasources/tempo-traces-to-profiles.md index ba09dfffe52..7860fededd7 100644 --- a/docs/sources/shared/datasources/tempo-traces-to-profiles.md +++ b/docs/sources/shared/datasources/tempo-traces-to-profiles.md @@ -19,7 +19,13 @@ labels: Using Trace to profiles, you can use Grafana’s ability to correlate different signals by adding the functionality to link between traces and profiles. **Trace to profiles** lets you link your Grafana Pyroscope data source to tracing data. -When configured, this connection lets you run queries from a trace span into the profile data. +When configured, this connection lets you run queries from a trace span into the profile data using **Explore**. +Each span links to your queries. Clicking a link runs the query in a split panel. +If tags are configured, Grafana dynamically inserts the span attribute values into the query. +The query runs over the time range of the (span start time - 60) to (span end time + 60 seconds). + +Embedded flame graphs are also inserted into each span details section that has a linked profile. +This lets you see resource consumption in a flame graph visualization for each span without having to navigate away from the current view. {{< youtube id="AG8VzfFMLxo" >}} @@ -28,37 +34,13 @@ There are two ways to configure the trace to profiles feature: - Use a basic configuration with default query, or - Configure a custom query where you can use a template language to interpolate variables from the trace or span. -{{< admonition type="note">}} -Traces to profile requires a Tempo data source with Traces to profiles configured and a Pyroscope data source. +![Traces to profiles section in the Tempo data source](/media/docs/grafana/data-sources/tempo/tempo-data-source-trace-to-profiles.png) + +## Before you begin + +Traces to profile requires a Tempo data source with Traces to profiles configured and a [Grafana Pyroscope data source](/docs/grafana//datasources/grafana-pyroscope/). As with traces, your application needs to be instrumented to emit profiling data. For more information, refer to [Linking tracing and profiling with span profiles](/docs/pyroscope//configure-client/trace-span-profiles/). -{{< /admonition >}} - -To use trace to profiles, navigate to **Explore** and query a trace. -Each span links to your queries. Clicking a link runs the query in a split panel. -If tags are configured, Grafana dynamically inserts the span attribute values into the query. -The query runs over the time range of the (span start time - 60) to (span end time + 60 seconds). - -![Selecting a link in the span queries the profile data source](/media/docs/tempo/profiles/tempo-trace-to-profile.png) - -To use trace to profiles, you must have a configured Grafana Pyroscope data source. -For more information, refer to the [Grafana Pyroscope data source](/docs/grafana//datasources/grafana-pyroscope/) documentation. - -**Embedded flame graphs** are also inserted into each span details section that has a linked profile. -This lets you see resource consumption in a flame graph visualization for each span without having to navigate away from the current view. -Hover over a particular block in the flame graph to see more details about the consumed resources. - -## Configuration options - -The following table describes options for configuring your Trace to profiles settings: - -| Setting name | Description | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select a Pyroscope \[profiling\] data source. | -| **Tags** | Defines the tags to use in the profile query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`. You can change the tag name for example to remove dots from the name if they're not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Profile type** | Defines the profile type that used in the query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ## Use a basic configuration @@ -77,7 +59,6 @@ To use a basic configuration, follow these steps: 1. Select one or more profile types to use in the query. Select the drop-down and choose options from the menu. The profile type or app must be selected for the query to be valid. Grafana doesn't show any data if the profile type or app isn’t selected when a query runs. - ![Traces to profile configuration options in the Tempo data source](/media/docs/tempo/profiles/Tempo-data-source-profiles-Settings.png) 1. Select **Save and Test**. @@ -94,9 +75,23 @@ To use a custom query with the configuration, follow these steps: 1. Select a Pyroscope data source in the **Data source** drop-down. 1. Optional: Choose any tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. - These tags can be used in the custom query with `${__tags}` variable. This variable interpolates the mapped tags as list in an appropriate syntax for the data source. Only the tags that were present in the span are included; tags that aren't present are omitted. You can also configure a new name for the tag. This is useful in cases where the tag has dots in the name and the target data source doesn't allow using dots in labels. For example, you can remap `service.name` to `service_name`. If you don’t map any tags here, you can still use any tag in the query, for example: `method="${__span.tags.method}"`. You can learn more about custom query variables [here](/docs/grafana//datasources/tempo/configure-tempo-data-source/#custom-query-variables). + These tags can be used in the custom query with `${__tags}` variable. This variable interpolates the mapped tags as list in an appropriate syntax for the data source. Only tags present in the span are included. Tags that aren't present are omitted. + + You can also configure a name for the tag. Tag names are useful where the tag has dots in the name and the target data source doesn't allow using dots in labels. For example, you can remap `service.name` to `service_name`. If you don’t map any tags here, you can still use any tag in the query, for example: `method="${__span.tags.method}"`. Learn more about [custom query variables](/docs/grafana//datasources/tempo/configure-tempo-data-source/#custom-query-variables). 1. Select one or more profile types to use in the query. Select the drop-down and choose options from the menu. 1. Switch on **Use custom query** to enter a custom query. -1. Specify a custom query to be used to query profile data. You can use various variables to make that query relevant for current span. The link is shown only if all the variables are interpolated with non-empty values to prevent creating an invalid query. You can interpolate the configured tags using the `$__tags` keyword. +1. Specify a custom query to be used to query profile data. You can use various variables to make that query relevant for current span. The link shows only if all the variables are interpolated with non-empty values to prevent creating an invalid query. You can interpolate the configured tags using the `$__tags` keyword. 1. Select **Save and Test**. + +## Configuration options + +The following table describes options for configuring your **Trace to profiles** settings: + +| Setting name | Description | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Data source | Defines the target data source. You can select a Pyroscope \[profiling\] data source. | +| Tags | Defines the tags to use in the profile query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`. You can change the tag name for example to remove dots from the name if they're not allowed in the target data source. For example, map `http.status` to `http_status`. | +| Profile type | Defines the profile type that used in the query. | +| Use custom query | Toggles use of custom query with interpolation. | +| Query | Input to write custom query. Use variable interpolation to customize it with variables from span. | diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index d1a5c91c9fa..1b7ea42a167 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -32,7 +32,21 @@ In this tutorial you will: - Set up an alert rule. - Receive firing and resolved alert notifications in a public webhook. -Check out [Part 2](http://grafana.com/tutorials/alerting-get-started-pt2/) if you want to learn more about alerts and notification routing. + + +{{< admonition type="tip" >}} + +Before you dive in, remember that you can [explore advanced topics like alert instances and notification routing](http://grafana.com/tutorials/alerting-get-started-pt2/) in the second part of this guide. + +{{< /admonition >}} + + + +{{< docs/ignore >}} + +> Before you dive in, remember that you can [explore advanced topics like alert instances and notification routing](http://grafana.com/tutorials/alerting-get-started-pt2/) in the second part of this guide. + +{{< /docs/ignore >}} @@ -265,16 +279,20 @@ By incrementing the threshold, the condition is no longer met, and after the eva ## Learn more -Your learning journey continues in [Part 2](http://grafana.com/tutorials/alerting-get-started-pt2/) where you will learn about alert instances and notification routing. + -## Summary +{{< admonition type="tip" >}} -In this tutorial, you have learned how to set up a contact point, create an alert, and send alert notifications to a public Webhook. By following these steps, you’ve gained a foundational understanding of how to leverage Grafana Alerting capabilities to monitor and respond to events of interest in your data. +Advance your skills by exploring [alert instances and notification routing](http://grafana.com/tutorials/alerting-get-started-pt2/) in Part 2 of your learning journey. -Feel free to experiment with different [contact points](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/) to customize your alert notifications and discover the configuration that best suits your needs. +{{< /admonition >}} -If you run into any problems, you are welcome to post questions in our [Grafana Community forum](https://community.grafana.com/). + -Happy monitoring! +{{< docs/ignore >}} + +Advance your skills by exploring [alert instances and notification routing](http://grafana.com/tutorials/alerting-get-started-pt2/) in Part 2 of your learning journey. + +{{< /docs/ignore >}} diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md index 5c4d710d4fe..f92da6381bb 100644 --- a/docs/sources/tutorials/grafana-fundamentals/index.md +++ b/docs/sources/tutorials/grafana-fundamentals/index.md @@ -389,6 +389,22 @@ Let's see how we can configure this. {{< figure src="/media/tutorials/grafana-alert-on-dashboard.png" alt="A panel in a Grafana dashboard with alerting and annotations configured" caption="Displaying Grafana Alerts on a dashboard" >}} + + +{{< admonition type="tip" >}} + +Check out our [advanced alerting tutorial](http://grafana.com/tutorials/alerting-get-started-pt2/) for more insights and tips. + +{{< /admonition >}} + + + +{{< docs/ignore >}} + +> Check out our [advanced alerting tutorial](http://grafana.com/tutorials/alerting-get-started-pt2/) for more insights and tips. + +{{< /docs/ignore >}} + ## Summary In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command: diff --git a/e2e/custom-plugins/app-with-exposed-components/README.md b/e2e/custom-plugins/app-with-exposed-components/README.md new file mode 100644 index 00000000000..03590601496 --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/README.md @@ -0,0 +1,22 @@ +# App with exposed components + +This directory contains two apps - `myorg-componentconsumer-app` and `myorg-componentexposer-app` which is nested inside `myorg-componentconsumer-app`. + +`myorg-componentconsumer-app` exposes a simple React component using the [`exposeComponent`](https://grafana.com/developers/plugin-tools/reference/ui-extensions#exposecomponent) api. `myorg-componentconsumer-app` in turn, consumes this compoment using the [`https://grafana.com/developers/plugin-tools/reference/ui-extensions#useplugincomponent`](https://grafana.com/developers/plugin-tools/reference/ui-extensions#useplugincomponent) hook. + +To test this app: + +```sh +# start e2e test instance (it will install this plugin) +PORT=3000 ./scripts/grafana-server/start-server +# run Playwright tests using Playwright VSCode extension or with the following script +yarn e2e:playwright +``` + +or + +``` +PORT=3000 ./scripts/grafana-server/start-server +yarn start +yarn e2e +``` diff --git a/e2e/custom-plugins/app-with-exposed-components/img/logo.svg b/e2e/custom-plugins/app-with-exposed-components/img/logo.svg new file mode 100644 index 00000000000..3d284dea3af --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/e2e/custom-plugins/app-with-exposed-components/module.js b/e2e/custom-plugins/app-with-exposed-components/module.js new file mode 100644 index 00000000000..b73fbd617ce --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/module.js @@ -0,0 +1,28 @@ +define(['@grafana/data', '@grafana/runtime', 'react'], function (grafanaData, grafanaRuntime, React) { + var AppPlugin = grafanaData.AppPlugin; + var usePluginComponent = grafanaRuntime.usePluginComponent; + + var MyComponent = function () { + var plugin = usePluginComponent('myorg-componentexposer-app/reusable-component/v1'); + var TestComponent = plugin.component; + var isLoading = plugin.isLoading; + + if (!TestComponent) { + return null; + } + + return React.createElement( + React.Fragment, + null, + React.createElement('div', null, 'Exposed component:'), + isLoading ? 'Loading..' : React.createElement(TestComponent, { name: 'World' }) + ); + }; + + var App = function () { + return React.createElement('div', null, 'Hello Grafana!', React.createElement(MyComponent, null)); + }; + + var plugin = new AppPlugin().setRootPage(App); + return { plugin: plugin }; +}); diff --git a/e2e/custom-plugins/app-with-exposed-components/plugin.json b/e2e/custom-plugins/app-with-exposed-components/plugin.json new file mode 100644 index 00000000000..caf74b2d1a8 --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/plugin.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "Extensions exposed component App", + "id": "myorg-componentconsumer-app", + "preload": true, + "info": { + "keywords": ["app"], + "description": "Example on how to extend grafana ui from a plugin", + "author": { + "name": "Myorg" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "1.0.0", + "updated": "2024-08-09" + }, + "includes": [ + { + "type": "page", + "name": "Default", + "path": "/a/myorg-componentconsumer-app", + "role": "Admin", + "addToNav": true, + "defaultNav": true + } + ], + "dependencies": { + "grafanaDependency": ">=10.3.3", + "plugins": [] + } +} diff --git a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg new file mode 100644 index 00000000000..3d284dea3af --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js new file mode 100644 index 00000000000..4ccaba8ca36 --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js @@ -0,0 +1,14 @@ +define(['@grafana/data', 'module', 'react'], function (grafanaData, amdModule, React) { + const plugin = new grafanaData.AppPlugin().exposeComponent({ + id: 'myorg-componentexposer-app/reusable-component/v1', + title: 'Reusable component', + description: 'A component that can be reused by other app plugins.', + component: function ({ name }) { + return React.createElement('div', { 'data-testid': 'exposed-component' }, 'Hello ', name, '!'); + }, + }); + + return { + plugin: plugin, + }; +}); diff --git a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json new file mode 100644 index 00000000000..3487cd45540 --- /dev/null +++ b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "A App", + "id": "myorg-componentexposer-app", + "preload": true, + "info": { + "keywords": ["app"], + "description": "Will extend root app with ui extensions", + "author": { + "name": "Myorg" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "%VERSION%", + "updated": "%TODAY%" + }, + "includes": [ + { + "type": "page", + "name": "Default", + "path": "/a/myorg-componentexposer-app", + "role": "Admin", + "addToNav": false, + "defaultNav": false + } + ], + "dependencies": { + "grafanaDependency": ">=10.3.3", + "plugins": [] + } +} diff --git a/e2e/custom-plugins/app-with-extension-point/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugin.json index b1d3c396384..3a4ab08faa2 100644 --- a/e2e/custom-plugins/app-with-extension-point/plugin.json +++ b/e2e/custom-plugins/app-with-extension-point/plugin.json @@ -32,7 +32,5 @@ "grafanaDependency": ">=10.3.3", "plugins": [] }, - "generated": { - "extensions": [] - } + "extensions": [] } diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json index ac501bc7f56..7bd55c0e572 100644 --- a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json +++ b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json @@ -32,14 +32,12 @@ "grafanaDependency": ">=10.3.3", "plugins": [] }, - "generated": { - "extensions": [ - { - "extensionPointId": "plugins/myorg-extensionpoint-app/actions", - "title": "Open from B", - "description": "Open a modal from plugin B", - "type": "link" - } - ] - } + "extensions": [ + { + "extensionPointId": "plugins/myorg-extensionpoint-app/actions", + "title": "Open from B", + "description": "Open a modal from plugin B", + "type": "link" + } + ] } diff --git a/e2e/dashboards-suite/general-dashboards.spec.ts b/e2e/dashboards-suite/general-dashboards.spec.ts index 00584030c23..f450f698e98 100644 --- a/e2e/dashboards-suite/general-dashboards.spec.ts +++ b/e2e/dashboards-suite/general-dashboards.spec.ts @@ -12,12 +12,9 @@ describe('Dashboards', () => { e2e.components.Panels.Panel.title('Panel #1').should('be.visible'); // scroll to the bottom - e2e.pages.Dashboard.DashNav.navV2() - .parent() - .parent() // Note, this will probably fail when we change the custom scrollbars - .scrollTo('bottom', { - timeout: 5 * 1000, - }); + cy.get('#page-scrollbar').scrollTo('bottom', { + timeout: 5 * 1000, + }); // The last panel should be visible... e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts rename to e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts rename to e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts new file mode 100644 index 00000000000..9a01139b445 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const pluginId = 'myorg-componentconsumer-app'; +const exposedComponentTestId = 'exposed-component'; + +test('should display component exposed by another app', async ({ page }) => { + await page.goto(`/a/${pluginId}`); + await expect(await page.getByTestId(exposedComponentTestId)).toHaveText('Hello World!'); +}); diff --git a/e2e/scenes/dashboards-suite/general-dashboards.spec.ts b/e2e/scenes/dashboards-suite/general-dashboards.spec.ts index 8270a7b1e5e..1edd34482b6 100644 --- a/e2e/scenes/dashboards-suite/general-dashboards.spec.ts +++ b/e2e/scenes/dashboards-suite/general-dashboards.spec.ts @@ -12,12 +12,9 @@ describe('Dashboards', () => { e2e.components.Panels.Panel.title('Panel #1').should('be.visible'); // scroll to the bottom - e2e.pages.Dashboard.DashNav.scrollContainer() - .children() - .first() - .scrollTo('bottom', { - timeout: 5 * 1000, - }); + cy.get('#page-scrollbar').scrollTo('bottom', { + timeout: 5 * 1000, + }); // The last panel should be visible... e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); @@ -30,6 +27,6 @@ describe('Dashboards', () => { // And the last panel should still be visible! // TODO: investigate scroll to on navigating back // e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); - e2e.components.Panels.Panel.title('Panel #1').should('be.visible'); + // e2e.components.Panels.Panel.title('Panel #1').should('be.visible'); }); }); diff --git a/go.mod b/go.mod index 8bf438d9e44..6fc8040651c 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ replace cuelang.org/go => github.com/grafana/cue v0.0.0-20230926092038-971951014 replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.52.0 require ( - buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 // @grafana/observability-traces-and-profiling - buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.33.0-20240414232344-9ca06271cb73.1 // @grafana/observability-traces-and-profiling + buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.10.0-20240523185345-933eab74d046.1 // @grafana/observability-traces-and-profiling + buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.1-20240523185345-933eab74d046.1 // @grafana/observability-traces-and-profiling cloud.google.com/go/kms v1.15.7 // @grafana/grafana-backend-group cloud.google.com/go/storage v1.38.0 // @grafana/grafana-backend-group cuelang.org/go v0.6.0-0.dev // @grafana/grafana-as-code @@ -74,9 +74,9 @@ require ( github.com/googleapis/gax-go/v2 v2.12.3 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240723124849-f2ab7c7b8f7d // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20240812070441-ccb639ea96d0 // @grafana/identity-access-team - github.com/grafana/authlib/claims v0.0.0-20240809101159-74eaccc31a06 // @grafana/identity-access-team + github.com/grafana/alerting v0.0.0-20240812131556-611a23ff0f7f // @grafana/alerting-backend + github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team + github.com/grafana/authlib/claims v0.0.0-20240814072707-6cffd53bb828 // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -89,15 +89,16 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.2.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.241.0 // @grafana/plugins-platform-backend - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240730185644-783ff7156079 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-plugin-sdk-go v0.243.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20240708134731-e9876749d440 // @grafana/grafana-app-platform-squad // This needs to be here for other projects that import grafana/grafana // For local development grafana/grafana will always use the local files // Check go.work file for details github.com/grafana/grafana/pkg/promlib v0.0.6 // @grafana/observability-metrics github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group - github.com/grafana/pyroscope-go/godeltaprof v0.1.7 // @grafana/observability-traces-and-profiling + github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling github.com/grafana/pyroscope/api v0.3.0 // @grafana/observability-traces-and-profiling github.com/grafana/tempo v1.5.1-0.20240604192202-01f4bc8ac2d1 // @grafana/observability-traces-and-profiling github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // @grafana/plugins-platform-backend @@ -144,7 +145,7 @@ require ( github.com/redis/go-redis/v9 v9.1.0 // @grafana/alerting-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/grafana-backend-group github.com/russellhaering/goxmldsig v1.4.0 // @grafana/grafana-backend-group - github.com/scottlepp/go-duck v0.0.21 // @grafana/grafana-app-platform-squad + github.com/scottlepp/go-duck v0.1.0 // @grafana/grafana-app-platform-squad github.com/spf13/cobra v1.8.1 // @grafana/grafana-app-platform-squad github.com/spf13/pflag v1.0.5 // @grafana-app-platform-squad github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad @@ -161,7 +162,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // @grafana/plugins-platform-backend go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/propagators/jaeger v1.28.0 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/samplers/jaegerremote v0.22.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel v1.28.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // @grafana/grafana-backend-group @@ -175,7 +176,7 @@ require ( golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // @grafana/alerting-backend golang.org/x/mod v0.18.0 // indirect; @grafana/grafana-backend-group golang.org/x/net v0.28.0 // @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.21.0 // @grafana/identity-access-team + golang.org/x/oauth2 v0.22.0 // @grafana/identity-access-team golang.org/x/sync v0.8.0 // @grafana/alerting-backend golang.org/x/text v0.17.0 // @grafana/grafana-backend-group golang.org/x/time v0.5.0 // @grafana/grafana-backend-group @@ -187,14 +188,13 @@ require ( gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend - k8s.io/api v0.31.0-rc.1 // @grafana/grafana-app-platform-squad - k8s.io/apimachinery v0.31.0-rc.1 // @grafana/grafana-app-platform-squad - k8s.io/apiserver v0.31.0-rc.1 // @grafana/grafana-app-platform-squad - k8s.io/client-go v0.31.0-rc.1 // @grafana/grafana-app-platform-squad - k8s.io/code-generator v0.31.0-rc.1 // @grafana/grafana-app-platform-squad - k8s.io/component-base v0.31.0-rc.1 // @grafana/grafana-app-platform-squad + k8s.io/api v0.31.0 // @grafana/grafana-app-platform-squad + k8s.io/apimachinery v0.31.0 // @grafana/grafana-app-platform-squad + k8s.io/apiserver v0.31.0 // @grafana/grafana-app-platform-squad + k8s.io/client-go v0.31.0 // @grafana/grafana-app-platform-squad + k8s.io/component-base v0.31.0 // @grafana/grafana-app-platform-squad k8s.io/klog/v2 v2.130.1 // @grafana/grafana-app-platform-squad - k8s.io/kube-aggregator v0.31.0-rc.1 // @grafana/grafana-app-platform-squad + k8s.io/kube-aggregator v0.31.0 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // @grafana/grafana-app-platform-squad k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // @grafana/partner-datasources sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // @grafana-app-platform-squad @@ -231,7 +231,6 @@ require ( github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/apache/thrift v0.20.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -455,7 +454,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/kms v0.31.0-rc.1 // indirect + k8s.io/kms v0.31.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.41.0 // indirect modernc.org/mathutil v1.6.0 // indirect @@ -481,6 +480,12 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect ) +require ( + github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 // indirect + github.com/hairyhenderson/go-which v0.2.0 // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect +) + // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240523142256-cc370b98af7c diff --git a/go.sum b/go.sum index 4bfd3f335b3..fe17f694855 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 h1:wQ75SnlaD0X30PnrmA+07A/5fnQWrAHy1mzv+CPB5Oo= -buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1/go.mod h1:VYzBTKhjl92cl3sv+xznQcJHCezU7qnI0FhBAUb4n8c= -buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.33.0-20240414232344-9ca06271cb73.1 h1:arEscdzM2EZPZT8x7tSzuBVgyZrnsKuvoftapClxUgw= -buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.33.0-20240414232344-9ca06271cb73.1/go.mod h1:/vvnaG5MGgbuJxYTucpo0QOom9xbSb6Y43za3/as9qk= +buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.10.0-20240523185345-933eab74d046.1 h1:q6MBjQ5fj3ygW/ySxOck7iwLhPQWbzOKJRZYzXRBmBk= +buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.10.0-20240523185345-933eab74d046.1/go.mod h1:DEC5vsD4oLn7c6QeBVfUS7WpW5iwmW5lXfhpsjVxVt0= +buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.1-20240523185345-933eab74d046.1 h1:Osqg+/+sFJKr5iyna6/AvyxXPY0jqaIGr3ltzzSDLRk= +buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.1-20240523185345-933eab74d046.1/go.mod h1:lbDqoSeErWK6pETEKo/LO+JmU2GbZqVE8ILESypLuZU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -1536,8 +1536,6 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= -github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= -github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= @@ -2176,7 +2174,6 @@ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= @@ -2309,12 +2306,12 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240723124849-f2ab7c7b8f7d h1:d2NZeTs+zBPVMd8uOOV5+6lyfs0BCDKxtiNxIMjnPNA= -github.com/grafana/alerting v0.0.0-20240723124849-f2ab7c7b8f7d/go.mod h1:DLj8frbtCaITljC2jc0L85JQViPF3mPfOSiYhm1osso= -github.com/grafana/authlib v0.0.0-20240812070441-ccb639ea96d0 h1:LDLHuN0nwa9fwZUKQrOBflePLxzOz4u4AuNutI78AHk= -github.com/grafana/authlib v0.0.0-20240812070441-ccb639ea96d0/go.mod h1:71+xJm0AE6eNGNExUvnABtyEztQ/Acb53/TAdOgwdmc= -github.com/grafana/authlib/claims v0.0.0-20240809101159-74eaccc31a06 h1:uD1LcKwvEAqzDsgVChBudPqo5BhPxkj9AgylT5QCReo= -github.com/grafana/authlib/claims v0.0.0-20240809101159-74eaccc31a06/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/alerting v0.0.0-20240812131556-611a23ff0f7f h1:c8QAFXkilBiF29xc7oKO2IkbGE3bp9NIKgiNLazdooY= +github.com/grafana/alerting v0.0.0-20240812131556-611a23ff0f7f/go.mod h1:DLj8frbtCaITljC2jc0L85JQViPF3mPfOSiYhm1osso= +github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg= +github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE= +github.com/grafana/authlib/claims v0.0.0-20240814072707-6cffd53bb828 h1:Hk6Oe0o1yIfdm2+2F3yHLjuaktukGVEOjju2txQXu8c= +github.com/grafana/authlib/claims v0.0.0-20240814072707-6cffd53bb828/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= @@ -2344,14 +2341,18 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.241.0 h1:zBcSW9xV9gA9hD8UN+HjJtD7tESMZcaQhA1BI76MTxM= -github.com/grafana/grafana-plugin-sdk-go v0.241.0/go.mod h1:2HjNwzGCfaFAyR2HGoECTwAmq8vSIn2L1/1yOt4XRS4= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240730185644-783ff7156079 h1:JnIzjNpW56Z9Tdmkzfs+kn3h2vtqavdr/CJTdAG1GIM= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240730185644-783ff7156079/go.mod h1:m/Tqd1ow+lmYtCj6/UZpejLdP2sxtN/4r6THdzS48r4= +github.com/grafana/grafana-plugin-sdk-go v0.243.0 h1:Xrkv7rN0aL5AK7b8zVsuD0ryCJX4HlaXUGGKjMuHeL4= +github.com/grafana/grafana-plugin-sdk-go v0.243.0/go.mod h1:JDrwijH50ym2SxBd4zNoQ4K+sdC1VppH4kVS8B1Nh0U= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2/go.mod h1:gBLBniiSUQvyt4LRrpIeysj8Many0DV+hdUKifRE0Ec= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240708134731-e9876749d440 h1:833vWSgndCcOXycwCq2Y98W8+W2ouuuhTL+Gf3BNKg8= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240708134731-e9876749d440/go.mod h1:qfZc7FEYBdKcxHUTtWtEAH+ArbMIkEQnbVPzr8giY3k= github.com/grafana/grafana/pkg/promlib v0.0.6 h1:FuRyHMIgVVXkLuJnCflNfk3gqJflmyiI+/ZuJ9MoAfY= github.com/grafana/grafana/pkg/promlib v0.0.6/go.mod h1:shFkrG1fQ/PPNRGhxAPNMLp0SAeG/jhqaLoG6n2191M= +github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= +github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435/go.mod h1:8cz+z0i57IjN6MYmu/zZQdCg9CQcsnEHbaJBBEf3KQo= github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240624122844-a89deaeb7365 h1:XRHqYGxjN2+/4QHPoOtr7kYTL9p2P5UxTXfnbiaO/NI= github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240624122844-a89deaeb7365/go.mod h1:X4dwV2eQI8z8G2aHXvhZZXu/y/rb3psQXuaZa66WZfA= github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZyeetkAB8dmeti2o= @@ -2360,8 +2361,8 @@ github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= -github.com/grafana/pyroscope-go/godeltaprof v0.1.7 h1:C11j63y7gymiW8VugJ9ZW0pWfxTZugdSJyC48olk5KY= -github.com/grafana/pyroscope-go/godeltaprof v0.1.7/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope/api v0.3.0 h1:WcVKNZ8JlriJnD28wTkZray0wGo8dGkizSJXnbG7Gd8= github.com/grafana/pyroscope/api v0.3.0/go.mod h1:JggA80ToAAUACYGfwL49XoFk5aN5ecHp4pNIZhlk9Uc= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= @@ -2395,6 +2396,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYp github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hairyhenderson/go-which v0.2.0 h1:vxoCKdgYc6+MTBzkJYhWegksHjjxuXPNiqo5G2oBM+4= +github.com/hairyhenderson/go-which v0.2.0/go.mod h1:U1BQQRCjxYHfOkXDyCgst7OZVknbqI7KuGKhGnmyIik= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= @@ -2501,6 +2504,8 @@ github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -2651,13 +2656,11 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCy github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -3023,7 +3026,6 @@ github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZu github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= @@ -3176,8 +3178,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26 h1:F+GIVtGqCFxPxO46ujf8cEOP574MBoRm3gNbPXECbxs= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= -github.com/scottlepp/go-duck v0.0.21 h1:bFg5/8ULOo62vmvIjEOy1EOf7Q86cpzq82BDN5RakVE= -github.com/scottlepp/go-duck v0.0.21/go.mod h1:m6V1VGZ4hdgvCj6+BmNMFo0taqiWhMx3CeL3uKHmP2E= +github.com/scottlepp/go-duck v0.1.0 h1:Lfunl1wd767v0dF0/dr+mBh+KnUFuDmgNycC76NJjeE= +github.com/scottlepp/go-duck v0.1.0/go.mod h1:xGoYUbgph5AbxwsMElWv2i/mgzQl89WIgwE69Ytml7Q= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= @@ -3453,8 +3455,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIX go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/contrib/propagators/jaeger v1.28.0 h1:xQ3ktSVS128JWIaN1DiPGIjcH+GsvkibIAVRWFjS9eM= go.opentelemetry.io/contrib/propagators/jaeger v1.28.0/go.mod h1:O9HIyI2kVBrFoEwQZ0IN6PHXykGoit4mZV2aEjkTRH4= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 h1:ja+d7Aea/9PgGxB63+E0jtRFpma717wubS0KFkZpmYw= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0/go.mod h1:Yc1eg51SJy7xZdOTyg1xyFcwE+ghcWh3/0hKeLo6Wlo= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.22.0 h1:OYxqumWcd1yaV/qvCt1B7Sru9OeUNGjeXq/oldx3AGk= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.22.0/go.mod h1:2tZTRqCbvx7nG57wUwd5NQpNVujOWnR84iPLllIH0Ok= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= @@ -3821,8 +3823,8 @@ golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2 golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -4333,7 +4335,6 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= @@ -4629,6 +4630,7 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -4682,6 +4684,7 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -4693,20 +4696,18 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= -k8s.io/api v0.31.0-rc.1 h1:ph2dq1aCz0s+Qa4wT//TMYgVFpYPdYLf1bOUeBL9mN0= -k8s.io/api v0.31.0-rc.1/go.mod h1:PcQwrOI3pFXW19JtLyLqIwFC95rRJN1fakusa1HD0ZM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= -k8s.io/apimachinery v0.31.0-rc.1 h1:WDq9mGUrmrmgpnbzoSPK1QSrtpp2YE/gvZRjYMZytFY= -k8s.io/apimachinery v0.31.0-rc.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.0-rc.1 h1:haMKeieDUxA5Z8yQ1XMJ8Sm2O18EIDFoC0TLA04KBOg= -k8s.io/apiserver v0.31.0-rc.1/go.mod h1:CdFvqtAIiWDfZl1LMixuXYGpttymfuopCol/F6AbxmI= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY= +k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= -k8s.io/client-go v0.31.0-rc.1 h1:EcCpZsDO3qdxhN6Gi1TD37Z1KOZhCXJIKU+knAHtMBM= -k8s.io/client-go v0.31.0-rc.1/go.mod h1:d9mIuVK07FX6Mc4b+BFLedsdglgk0aoCGkHt4invDN0= -k8s.io/code-generator v0.31.0-rc.1 h1:+kAoEeod3OBbeMY8XmD426ZF+AacWthzjRreudcsyL8= -k8s.io/code-generator v0.31.0-rc.1/go.mod h1:HqqhOJxD46ECI95Va1PQPex7+h8SO8CQnErTm9CDNM4= -k8s.io/component-base v0.31.0-rc.1 h1:MBTLTqo2/P0OHGOvUwaZBICPKMrylOllOV3e0REcD0U= -k8s.io/component-base v0.31.0-rc.1/go.mod h1:YV7bvpvHLgCCzOW6geKYADukl7yZuOMbObetd45kTQE= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= +k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= @@ -4717,10 +4718,10 @@ k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.31.0-rc.1 h1:gdvrXpBNT3VijWgZcxpTNYcjaO1LObkdIEqbtTxhBGg= -k8s.io/kms v0.31.0-rc.1/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94= -k8s.io/kube-aggregator v0.31.0-rc.1 h1:kkt+saxeIL7Cxn9CDxvgnk4u+aNnceXpKwt6uuzo4T0= -k8s.io/kube-aggregator v0.31.0-rc.1/go.mod h1:+DH4QiiBYaufKt7kDFLj4/Aj3fw8rrioUKV78ISUSDY= +k8s.io/kms v0.31.0 h1:KchILPfB1ZE+ka7223mpU5zeFNkmb45jl7RHnlImUaI= +k8s.io/kms v0.31.0/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94= +k8s.io/kube-aggregator v0.31.0 h1:3DqSpmqHF8rey7fY+qYXLJms0tYPhxrgWvjpnKVnS0Y= +k8s.io/kube-aggregator v0.31.0/go.mod h1:Fa+OVSpMQC7zbTTz7/QG7FXe9jZ8usuJQej5sMdCrkM= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= diff --git a/go.work b/go.work index aefa6dda97f..5e9b02fc37a 100644 --- a/go.work +++ b/go.work @@ -5,6 +5,7 @@ go 1.22.4 use ( . // skip:golangci-lint + ./pkg/aggregator ./pkg/apimachinery ./pkg/apiserver ./pkg/build diff --git a/go.work.sum b/go.work.sum index 63adaa4e545..b9ea373c6c3 100644 --- a/go.work.sum +++ b/go.work.sum @@ -209,6 +209,8 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEq github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/DiJbg= @@ -221,6 +223,7 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= +github.com/aws/aws-sdk-go v1.44.321/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3 h1:nUP29LA4GZZPihNSo5ZcF4Rl73u+bN5IBRnrQA0jFK4= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4 h1:EmIEXOjAdXtxa2OGM1VAajZV/i06Q8qd4kBpJd9/p1k= @@ -266,6 +269,8 @@ github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nC github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= @@ -275,8 +280,6 @@ github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHo github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= @@ -340,6 +343,7 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTg github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= @@ -359,7 +363,6 @@ github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs= @@ -384,6 +387,7 @@ github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+Nbn github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= @@ -395,6 +399,7 @@ github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MG github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -407,13 +412,9 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/grafana/alerting v0.0.0-20240712142914-5558735b4462 h1:MWpvVoPcSej4YfxSIuAllr9vg0UgVEG5CQifD5fK+ps= -github.com/grafana/alerting v0.0.0-20240712142914-5558735b4462/go.mod h1:DLj8frbtCaITljC2jc0L85JQViPF3mPfOSiYhm1osso= -github.com/grafana/authlib v0.0.0-20240611075137-331cbe4e840f/go.mod h1:+MjD5sxxgLOIvw0ox18wJmjBzz8tOECo7quiiZAmgJY= -github.com/grafana/authlib/claims v0.0.0-20240809095826-8eb5495c0b2a/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= -github.com/grafana/grafana-plugin-sdk-go v0.235.0/go.mod h1:6n9LbrjGL3xAATntYVNcIi90G9BVHRJjzHKz5FXVfWw= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= @@ -422,11 +423,17 @@ github.com/hamba/avro/v2 v2.17.2 h1:6PKpEWzJfNnvBgn7m2/8WYaDOUASxfDU+Jyb4ojDgFY= github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo= github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= +github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY= +github.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw= github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= +github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= @@ -491,6 +498,7 @@ github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3ro github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= +github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= @@ -526,13 +534,14 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8= github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/mostynb/go-grpc-compression v1.2.2 h1:XaDbnRvt2+1vgr0b/l0qh4mJAfIxE0bKXtz2Znl3GGI= @@ -550,6 +559,7 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= +github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.97.0 h1:8GH8y3Cq54Ey6He9tyhcVYLfG4TEs/7pp3s6934zNKA= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.97.0/go.mod h1:QBHXt+tHds39B4xGyBkbOx2TST+p8JLWBiXbKKAhNss= @@ -612,6 +622,8 @@ github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ul github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= +github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= @@ -622,9 +634,8 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= -github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= +github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.26.0 h1:SQl3M6suC6NWQYEzOvIv+EF6dAMYEqIuZy+o4H9F5Ig= github.com/prometheus/statsd_exporter v0.26.0/go.mod h1:GXFLADOmBTVDrHc7b04nX8ooq3azG61pnECNqT7O5DM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= @@ -653,8 +664,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -662,7 +671,6 @@ github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93wh github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk= github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8= github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RUB4zA= github.com/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU= @@ -683,6 +691,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.28.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= @@ -720,6 +730,7 @@ github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1/go.mod h1:udNPW8eupyH/EZocecFmaSNJ github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= @@ -728,9 +739,8 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= -go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= -go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= -go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.opentelemetry.io/collector v0.97.0 h1:qyOju13byHIKEK/JehmTiGMj4pFLa4kDyrOCtTmjHU0= go.opentelemetry.io/collector v0.97.0/go.mod h1:V6xquYAaO2VHVu4DBK28JYuikRdZajh7DH5Vl/Y8NiA= go.opentelemetry.io/collector/component v0.97.0 h1:vanKhXl5nptN8igRH4PqVYHOILif653vaPIKv6LCZCI= @@ -797,12 +807,9 @@ go.opentelemetry.io/contrib/config v0.4.0 h1:Xb+ncYOqseLroMuBesGNRgVQolXcXOhMj7E go.opentelemetry.io/contrib/config v0.4.0/go.mod h1:drNk2xRqLWW4/amk6Uh1S+sDAJTc7bcEEN1GfJzj418= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= go.opentelemetry.io/contrib/propagators/b3 v1.23.0 h1:aaIGWc5JdfRGpCafLRxMJbD65MfTa206AwSKkvGS0Hg= go.opentelemetry.io/contrib/propagators/b3 v1.23.0/go.mod h1:Gyz7V7XghvwTq+mIhLFlTgcc03UDroOg8vezs4NLhwU= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= go.opentelemetry.io/otel/bridge/opencensus v1.26.0 h1:DZzxj9QjznMVoehskOJnFP2gsTCWtDTFBDvFhPAY7nc= go.opentelemetry.io/otel/bridge/opencensus v1.26.0/go.mod h1:rJiX0KrF5m8Tm1XE8jLczpAv5zUaDcvhKecFG0ZoFG4= go.opentelemetry.io/otel/bridge/opentracing v1.26.0 h1:Q/dHj0DOhfLMAs5u5ucAbC7gy66x9xxsZRLpHCJ4XhI= @@ -811,28 +818,17 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqR go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 h1:q/Nj5/2TZRIt6PderQ9oU0M00fzoe8UZuINGw6ETGTw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1/go.mod h1:DTE9yAu6r08jU3xa68GiSeI7oRcSEQ2RpKbbQGO+dWM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/prometheus v0.37.0 h1:NQc0epfL0xItsmGgSXgfbH2C1fq2VLXkZoDFsfRNHpc= -go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ= go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.23.1 h1:C8r95vDR125t815KD+b1tI0Fbc1pFnwHTBxkbIZ6Szc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.23.1/go.mod h1:Qr0qomr64jentMtOjWMbtYeJMSuMSlsPEjmnRA2sWZ4= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= -go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -840,46 +836,41 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEa go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= -google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240325203815-454cdb8f5daa h1:wBkzraZsSqhj1M4L/nMrljUU6XasJkgHvUsq8oRGwF0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= @@ -897,8 +888,8 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -k8s.io/component-base v0.0.0-20240417101527-62c04b35eff6 h1:WN8Lymy+dCTDHgn4vhUSNIB6U+0sDiv/c9Zdr0UeAnI= -k8s.io/component-base v0.0.0-20240417101527-62c04b35eff6/go.mod h1:l0ukbPS0lwFxOzSq5ZqjutzF+5IL2TLp495PswRPSZk= +k8s.io/code-generator v0.31.0 h1:w607nrMi1KeDKB3/F/J4lIoOgAwc+gV9ZKew4XRfMp8= +k8s.io/code-generator v0.31.0/go.mod h1:84y4w3es8rOJOUUP1rLsIiGlO1JuEaPFXQPA9e/K6U0= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= @@ -919,5 +910,4 @@ rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= diff --git a/hack/externalTools.go b/hack/externalTools.go index 5ccc03e363b..f6bf18b1ff1 100644 --- a/hack/externalTools.go +++ b/hack/externalTools.go @@ -3,5 +3,5 @@ package hack // this ensures that code-generator is available in the go.mod file, // which is a dependency of the ./update-codegen.sh script. import ( - _ "k8s.io/code-generator/pkg/util" + _ "k8s.io/code-generator/cmd/client-gen/generators" ) diff --git a/hack/go.mod b/hack/go.mod new file mode 100644 index 00000000000..e5507f966e0 --- /dev/null +++ b/hack/go.mod @@ -0,0 +1,16 @@ +module github.com/grafana/grafana/hack + +go 1.22.4 + +require k8s.io/code-generator v0.31.0 + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect + k8s.io/klog/v2 v2.130.1 // indirect +) diff --git a/hack/go.sum b/hack/go.sum new file mode 100644 index 00000000000..bb36abd861f --- /dev/null +++ b/hack/go.sum @@ -0,0 +1,18 @@ +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +k8s.io/code-generator v0.31.0 h1:w607nrMi1KeDKB3/F/J4lIoOgAwc+gV9ZKew4XRfMp8= +k8s.io/code-generator v0.31.0/go.mod h1:84y4w3es8rOJOUUP1rLsIiGlO1JuEaPFXQPA9e/K6U0= +k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= +k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= diff --git a/hack/openapi-codegen.sh b/hack/openapi-codegen.sh index 070966375dc..5f9e5e68a30 100644 --- a/hack/openapi-codegen.sh +++ b/hack/openapi-codegen.sh @@ -45,38 +45,38 @@ function grafana::codegen::gen_openapi() { while [ "$#" -gt 0 ]; do case "$1" in - "--input-pkg-single") - in_pkg_single="$2" - shift 2 - ;; - "--include-common-input-dirs") - if [ "$2" == "true" ]; then - COMMON_INPUT_DIRS='--input-dirs "k8s.io/apimachinery/pkg/apis/meta/v1" --input-dirs "k8s.io/apimachinery/pkg/runtime" --input-dirs "k8s.io/apimachinery/pkg/version"' - else - COMMON_INPUT_DIRS="" - fi - shift 2 - ;; - "--output-base") - out_base="$2" - shift 2 - ;; - "--report-filename") - report="$2" - shift 2 - ;; - "--update-report") - update_report="true" - shift - ;; - "--boilerplate") - boilerplate="$2" - shift 2 - ;; - *) - echo "unknown argument: $1" >&2 - return 1 - ;; + "--input-pkg-single") + in_pkg_single="$2" + shift 2 + ;; + "--include-common-input-dirs") + if [ "$2" == "true" ]; then + COMMON_INPUT_DIRS='k8s.io/apimachinery/pkg/apis/meta/v1 k8s.io/apimachinery/pkg/runtime k8s.io/apimachinery/pkg/version' + else + COMMON_INPUT_DIRS="" + fi + shift 2 + ;; + "--output-base") + out_base="$2" + shift 2 + ;; + "--report-filename") + report="$2" + shift 2 + ;; + "--update-report") + update_report="true" + shift + ;; + "--boilerplate") + boilerplate="$2" + shift 2 + ;; + *) + echo "unknown argument: $1" >&2 + return 1 + ;; esac done @@ -99,62 +99,62 @@ function grafana::codegen::gen_openapi() { # To support running this from anywhere, first cd into this directory, # and then install with forced module mode on and fully qualified name. cd "${KUBE_CODEGEN_ROOT}" + GO111MODULE=on go mod download BINS=( openapi-gen ) # shellcheck disable=2046 # printf word-splitting is intentional - GO111MODULE=on go install $(printf "k8s.io/code-generator/cmd/%s " "${BINS[@]}") + GO111MODULE=on go install $(printf "k8s.io/kube-openapi/cmd/%s " "${BINS[@]}") ) # Go installs in $GOBIN if defined, and $GOPATH/bin otherwise gobin="${GOBIN:-$(go env GOPATH)/bin}" # These tools all assume out-dir == in-dir. - root="${out_base}/${in_pkg_single}" + root="${in_pkg_single}" mkdir -p "${root}" - root="$(cd "${root}" && pwd -P)" local input_pkgs=() while read -r dir; do pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" input_pkgs+=("${pkg}") done < <( - ( kube::codegen::internal::git_grep -l --null \ - -e '+k8s:openapi-gen=' \ - ":(glob)${root}"/'**/*.go' \ - || true \ - ) | while read -r -d $'\0' F; do dirname "${F}"; done \ - | LC_ALL=C sort -u + ( + kube::codegen::internal::grep -l --null \ + -e '+k8s:openapi-gen=' \ + -r "${root}" \ + --include '*.go' || + true + ) | while read -r -d $'\0' F; do dirname "${F}"; done | + LC_ALL=C sort -u ) - - local new_report="" - if [ "${#input_pkgs[@]}" != 0 ]; then - echo "Generating openapi code for ${#input_pkgs[@]} targets" - - kube::codegen::internal::git_find -z \ - ":(glob)${root}"/'**/zz_generated.openapi.go' \ - | xargs -0 rm -f - - local inputs=() - for arg in "${input_pkgs[@]}"; do - inputs+=("--input-dirs" "$arg") - done - - new_report="${root}/${report}.tmp" - if [ -n "${update_report}" ]; then - new_report="${root}/${report}" - fi - - "${gobin}/openapi-gen" \ - -v "${v}" \ - -O zz_generated.openapi \ - --go-header-file "${boilerplate}" \ - --output-base "${out_base}" \ - --output-package "${in_pkg_single}" \ - --report-filename "${new_report}" \ - ${COMMON_INPUT_DIRS} \ - "${inputs[@]}" + local new_report="" + if [ "${#input_pkgs[@]}" == 0 ]; then + return 0 fi + echo "Generating openapi code for ${#input_pkgs[@]} targets" + + kube::codegen::internal::findz \ + "${root}" \ + -type f \ + -name zz_generated.openapi.go \ + | xargs -0 rm -f + + local new_report + new_report="$(mktemp -t "$(basename "$0").api_violations.XXXXXX")" + if [ -n "${update_report}" ]; then + new_report="${root}/${report}" + fi + + "${gobin}/openapi-gen" \ + -v "${v}" \ + --output-file zz_generated.openapi.go \ + --go-header-file "${boilerplate}" \ + --output-dir "${root}" \ + --output-pkg "github.com/grafana/grafana/${in_pkg_single}" \ + --report-filename "${new_report}" \ + ${COMMON_INPUT_DIRS} \ + "${input_pkgs[@]}" touch "${root}/${report}" # in case it doesn't exist yet if [[ -z "${new_report}" ]]; then @@ -169,6 +169,6 @@ function grafana::codegen::gen_openapi() { # if all goes well, remove the temporary reports if [ -z "${update_report}" ]; then - rm -f "${new_report}" + rm -f "${new_report}" fi } diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 039a6f2c94a..54e1efcef46 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -10,7 +10,8 @@ set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. -CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.31.0-rc.1)} +pushd "${SCRIPT_ROOT}/hack" && GO111MODULE=on go mod tidy && popd +CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.31.0)} OUTDIR="${HOME}/go/src" OPENAPI_VIOLATION_EXCEPTIONS_FILENAME="zz_generated.openapi_violation_exceptions.list" @@ -33,26 +34,27 @@ grafana::codegen:run() { include_common_input_dirs=$([[ ${api_pkg} == "common" ]] && echo "true" || echo "false") kube::codegen::gen_helpers \ - --input-pkg-root github.com/grafana/grafana/${generate_root}/apis/${api_pkg} \ - --output-base "${OUTDIR}" \ - --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" + --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + ${generate_root}/apis/${api_pkg} - for pkg_version in $(grafana:codegen:lsdirs ./${generate_root}/apis/${api_pkg}); do + for pkg_version in $(grafana:codegen:lsdirs ./${generate_root}/apis/${api_pkg}); do grafana::codegen::gen_openapi \ - --input-pkg-single github.com/grafana/grafana/${generate_root}/apis/${api_pkg}/${pkg_version} \ + --input-pkg-single ${generate_root}/apis/${api_pkg}/${pkg_version} \ --output-base "${OUTDIR}" \ --report-filename "${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" \ --update-report \ --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ --include-common-input-dirs ${include_common_input_dirs} - violations_file="${OUTDIR}/github.com/grafana/grafana/${generate_root}/apis/${api_pkg}/${pkg_version}/${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" + violations_file="${generate_root}/apis/${api_pkg}/${pkg_version}/${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" + if [ ! -f "${violations_file}" ]; then + continue + fi # delete violation exceptions file, if empty if ! grep -q . "${violations_file}"; then echo "Deleting ${violations_file} since it is empty" rm ${violations_file} fi - echo "" done done @@ -67,12 +69,12 @@ grafana::codegen:run() { echo "-------------------------" kube::codegen::gen_client \ - --with-watch \ - --with-applyconfig \ - --input-pkg-root github.com/grafana/grafana/${generate_root}/apis \ - --output-pkg-root github.com/grafana/grafana/${generate_root}/generated \ - --output-base "${OUTDIR}" \ - --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" + --with-watch \ + --with-applyconfig \ + --output-dir ${generate_root}/generated \ + --output-pkg github.com/grafana/grafana/${generate_root}/generated \ + --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + ${generate_root}/apis echo "" } @@ -83,5 +85,6 @@ grafana:codegen:lsdirs() { grafana::codegen:run pkg grafana::codegen:run pkg/apimachinery +grafana::codegen:run pkg/aggregator echo "done." diff --git a/lerna.json b/lerna.json index e0ddde2398a..88526882713 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "npmClient": "yarn", - "version": "11.2.0-pre" + "version": "11.3.0-pre" } diff --git a/package.json b/package.json index a83bd3fbeee..457f1b37e4c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "repository": "github:grafana/grafana", "scripts": { "build": "NODE_ENV=production nx exec --verbose -- webpack --config scripts/webpack/webpack.prod.js", @@ -78,7 +78,7 @@ "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/plugin-e2e": "1.6.1", - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.46.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", @@ -112,6 +112,7 @@ "@types/google.analytics": "^0.0.46", "@types/gtag.js": "^0.0.20", "@types/history": "4.7.11", + "@types/ini": "^4", "@types/jest": "29.5.12", "@types/jquery": "3.5.30", "@types/js-yaml": "^4.0.5", @@ -142,6 +143,7 @@ "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.5", "@types/slate-react": "0.22.9", + "@types/swagger-ui-react": "4.18.3", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "@types/tinycolor2": "1.4.6", @@ -189,6 +191,7 @@ "html-webpack-plugin": "5.6.0", "http-server": "14.1.1", "i18next-parser": "9.0.1", + "ini": "^4.1.3", "jest": "29.7.0", "jest-canvas-mock": "2.5.2", "jest-date-mock": "1.0.10", @@ -263,7 +266,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.7.4", + "@grafana/scenes": "^5.8.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -381,6 +384,7 @@ "react-window": "1.8.10", "react-window-infinite-loader": "1.0.9", "react-zoom-pan-pinch": "^3.3.0", + "reduce-reducers": "^1.0.4", "redux": "5.0.1", "redux-thunk": "3.1.0", "regenerator-runtime": "0.14.1", @@ -391,6 +395,7 @@ "slate": "0.47.9", "slate-plain-serializer": "0.7.13", "slate-react": "0.22.10", + "swagger-ui-react": "5.17.14", "symbol-observable": "4.0.0", "systemjs": "6.15.1", "systemjs-cjs-extra": "0.2.1", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index f20bda69f4f..04ac3d12bbb 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -36,7 +36,7 @@ }, "dependencies": { "@braintree/sanitize-url": "7.0.1", - "@grafana/schema": "11.2.0-pre", + "@grafana/schema": "11.3.0-pre", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", "d3-interpolate": "3.0.1", @@ -61,7 +61,7 @@ "xss": "^1.0.14" }, "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.2.3", "@types/dompurify": "^3.0.0", "@types/history": "4.7.11", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index bf16d4e093a..005e89554da 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -554,6 +554,7 @@ export { type PluginExtensionDataSourceConfigContext, type PluginExtensionCommandPaletteContext, type PluginExtensionOpenModalOptions, + type PluginExposedComponentConfig, } from './types/pluginExtensions'; export { type ScopeDashboardBindingSpec, diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index 5f2fbc12a68..9c16723c737 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -7,6 +7,7 @@ import { type PluginExtensionLinkConfig, PluginExtensionTypes, PluginExtensionComponentConfig, + PluginExposedComponentConfig, PluginExtensionConfig, } from './pluginExtensions'; @@ -56,6 +57,7 @@ export interface AppPluginMeta extends PluginMeta } export class AppPlugin extends GrafanaPlugin> { + private _exposedComponentConfigs: PluginExposedComponentConfig[] = []; private _extensionConfigs: PluginExtensionConfig[] = []; // Content under: /a/${plugin-id}/* @@ -98,6 +100,10 @@ export class AppPlugin extends GrafanaPlugin extends GrafanaPlugin( - componentConfig: { id: string } & Omit, 'type' | 'extensionPointId'> - ) { - const { id, ...extension } = componentConfig; - - this._extensionConfigs.push({ - ...extension, - extensionPointId: `capabilities/${id}`, - type: PluginExtensionTypes.component, - } as PluginExtensionComponentConfig); + exposeComponent(componentConfig: PluginExposedComponentConfig) { + this._exposedComponentConfigs.push(componentConfig as PluginExposedComponentConfig); return this; } @@ -165,7 +163,6 @@ export class AppPlugin extends GrafanaPlugin(extension: Omit, 'type'>) { this.addComponent({ diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b8bd9eff3d6..1c577afa5f0 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -59,6 +59,7 @@ export interface FeatureToggles { influxdbBackendMigration?: boolean; influxqlStreamingParser?: boolean; influxdbRunQueriesInParallel?: boolean; + prometheusRunQueriesInParallel?: boolean; prometheusDataplane?: boolean; lokiMetricDataplane?: boolean; lokiLogsDataplane?: boolean; @@ -76,6 +77,7 @@ export interface FeatureToggles { lokiPredefinedOperations?: boolean; pluginsFrontendSandbox?: boolean; frontendSandboxMonitorOnly?: boolean; + pluginsDetailsRightPanel?: boolean; sqlDatasourceDatabaseSelection?: boolean; recordedQueriesMulti?: boolean; vizAndWidgetSplit?: boolean; @@ -199,6 +201,7 @@ export interface FeatureToggles { bodyScrolling?: boolean; cloudwatchMetricInsightsCrossAccount?: boolean; prometheusAzureOverrideAudience?: boolean; + backgroundPluginInstaller?: boolean; dataplaneAggregator?: boolean; adhocFilterOneOf?: boolean; } diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 00594fad001..0c027f3b1ed 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -95,6 +95,29 @@ export type PluginExtensionComponentConfig = { extensionPointId: string; }; +export type PluginExposedComponentConfig = { + /** + * The unique identifier of the component + * Shoud be in the format of `//`. e.g. `myorg-todo-app/todo-list/v1` + */ + id: string; + + /** + * The title of the component + */ + title: string; + + /** + * A short description of the component + */ + description: string; + + /** + * The React component that will be exposed to other plugins + */ + component: React.ComponentType; +}; + export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig; export type PluginExtensionOpenModalOptions = { diff --git a/packages/grafana-data/src/utils/csv.test.ts b/packages/grafana-data/src/utils/csv.test.ts index 52c36beb651..eb776cc1e6d 100644 --- a/packages/grafana-data/src/utils/csv.test.ts +++ b/packages/grafana-data/src/utils/csv.test.ts @@ -5,6 +5,7 @@ import { MutableDataFrame } from '../dataframe/MutableDataFrame'; import { getDataFrameRow, toDataFrameDTO } from '../dataframe/processDataFrame'; import { getDisplayProcessor } from '../field/displayProcessor'; import { createTheme } from '../themes/createTheme'; +import { FieldType } from '../types/dataFrame'; import { CSVHeaderStyle, readCSV, toCSV } from './csv'; @@ -159,4 +160,23 @@ describe('DataFrame to CSV', () => { 1589455688623,2020-05-14 11:28:08" `); }); + + it('should handle field type frame', () => { + const dataFrame = new MutableDataFrame({ + fields: [ + { name: 'Time', values: [1589455688623] }, + { + name: 'Value', + type: FieldType.frame, + values: [{ value: '1234' }], + }, + ], + }); + + const csv = toCSV([dataFrame]); + expect(csv).toMatchInlineSnapshot(` + ""Time","Value" + 1589455688623,1234" + `); + }); }); diff --git a/packages/grafana-data/src/utils/csv.ts b/packages/grafana-data/src/utils/csv.ts index 766c3680ee8..2b2c48fddd1 100644 --- a/packages/grafana-data/src/utils/csv.ts +++ b/packages/grafana-data/src/utils/csv.ts @@ -309,7 +309,11 @@ export function toCSV(data: DataFrame[], config?: CSVConfig): string { csv = csv + config.delimiter; } - const v = fields[j].values[i]; + let v = fields[j].values[i]; + // For FieldType frame, use value if it exists to prevent exporting [object object] + if (fields[j].type === FieldType.frame && fields[j].values[i].value) { + v = fields[j].values[i].value; + } if (v !== null) { csv = csv + writers[j](v); } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index f289eb45131..71c99e89d75 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", @@ -49,7 +49,7 @@ "rollup-plugin-node-externals": "^5.0.0" }, "dependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "tslib": "2.6.3", "typescript": "5.4.5" } diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index d5936714cb9..d0c9cd1f8f3 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -106,6 +106,7 @@ export const Components = { metricsBrowser: { openButton: 'data-testid open metrics browser', selectMetric: 'data-testid select a metric', + seriesLimit: 'data-testid series limit', metricList: 'data-testid metric list', labelNamesFilter: 'data-testid label names filter', labelValuesFilter: 'data-testid label values filter', diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index cfa7668cbe6..04a8bb48b6a 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "@grafana/eslint-plugin", "description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 6b484918ff7..41b89610d6d 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/flamegraph", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana flamegraph visualization component", "keywords": [ "grafana", @@ -44,8 +44,8 @@ ], "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "@leeoniya/ufuzzy": "1.0.14", "d3": "^7.8.5", "lodash": "4.17.21", @@ -59,7 +59,7 @@ "@babel/core": "7.25.2", "@babel/preset-env": "7.25.3", "@babel/preset-react": "7.24.7", - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.2.3", "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index a7ea505fbb0..a0166934b15 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -1,6 +1,6 @@ { "name": "@grafana/saga-icons", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "private": true, "description": "Icons for Grafana", "author": "Grafana Labs", @@ -35,7 +35,7 @@ }, "devDependencies": { "@babel/core": "7.25.2", - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", "@svgr/babel-plugin-remove-jsx-attribute": "^8.0.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index a9855c31202..db965c0b913 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "name": "@grafana/o11y-ds-frontend", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Library to manage traces in Grafana.", "sideEffects": false, "repository": { @@ -18,19 +18,19 @@ }, "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", - "@grafana/e2e-selectors": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/experimental": "1.7.13", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "react-select": "5.8.0", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.6.3" }, "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "15.0.2", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 0096a18af0a..1373acf6369 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -2,12 +2,12 @@ "name": "@grafana/plugin-configs", "description": "Shared dependencies and files for core plugins", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "tslib": "2.6.3" }, "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@swc/core": "1.4.2", "@types/eslint": "8.56.10", "copy-webpack-plugin": "12.0.2", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 849befcdb9c..2782c01e490 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "AGPL-3.0-only", "name": "@grafana/prometheus", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana Prometheus Library", "keywords": [ "typescript" @@ -38,12 +38,12 @@ "dependencies": { "@emotion/css": "11.11.2", "@floating-ui/react": "0.26.22", - "@grafana/data": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.7.13", "@grafana/faro-web-sdk": "1.9.0", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "@hello-pangea/dnd": "16.6.0", "@leeoniya/ufuzzy": "1.0.14", "@lezer/common": "1.2.1", @@ -76,8 +76,8 @@ }, "devDependencies": { "@emotion/eslint-plugin": "11.11.0", - "@grafana/e2e-selectors": "11.2.0-pre", - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-image": "3.0.3", "@rollup/plugin-node-resolve": "15.2.3", "@swc/core": "1.4.2", diff --git a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx index 924e1067544..963590f3667 100644 --- a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx +++ b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx @@ -45,8 +45,12 @@ interface BrowserState { error: string; validationStatus: string; valueSearchTerm: string; + seriesLimit?: string; } +export const DEFAULT_SERIES_LIMIT = '40000'; +export const REMOVE_SERIES_LIMIT = 'none'; + interface FacettableValue { name: string; selected?: boolean; @@ -214,6 +218,10 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component) => { + this.setState({ seriesLimit: event.target.value.trim() }); + }; + onChangeValueSearch = (event: ChangeEvent) => { this.setState({ valueSearchTerm: event.target.value }); }; @@ -419,7 +427,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component
- +
+ +
+ +
>>([]); + const [truncatedLabelOptions, setTruncatedLabelOptions] = useState>>([]); + const [allLabelOptions, setAllLabelOptions] = useState>>([]); + + /** + * Set the both allLabels and truncatedLabels + * + * @param names + * @param variables + */ + function setLabels(names: SelectableValue[], variables: SelectableValue[]) { + setAllLabelOptions([...variables, ...names]); + const truncatedNames = truncateResult(names); + setTruncatedLabelOptions([...variables, ...truncatedNames]); + } // label filters have been added as a filter for metrics in label values query type const [labelFilters, setLabelFilters] = useState([]); @@ -100,7 +115,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: // get all the labels datasource.getTagKeys({ filters: [] }).then((labelNames: Array<{ text: string }>) => { const names = labelNames.map(({ text }) => ({ label: text, value: text })); - setLabelOptions([...variables, ...names]); + setLabels(names, variables); }); } else { // fetch the labels filtered by the metric @@ -110,7 +125,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: datasource.languageProvider.fetchLabelsWithMatch(expr).then((labelsIndex: Record) => { const labelNames = Object.keys(labelsIndex); const names = labelNames.map((value) => ({ label: value, value: value })); - setLabelOptions([...variables, ...names]); + setLabels(names, variables); }); } }, [datasource, qryType, metric]); @@ -220,6 +235,18 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: return { metric: metric, labels: labelFilters, operations: [] }; }, [metric, labelFilters]); + /** + * Debounce a search through all the labels possible and truncate by . + */ + const labelNamesSearch = debounce((query: string) => { + // we limit the select to show 1000 options, + // but we still search through all the possible options + const results = allLabelOptions.filter((label) => { + return label.value?.includes(query); + }); + return truncateResult(results); + }, 300); + return ( <> @@ -256,14 +283,15 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
} > - { setState({ isLoadingLabelNames: true }); const labelNames = await onGetLabelNames(item); + // store all label names to allow for full label searching by typing in the select option, see loadOptions function labelNamesSearch + setAllLabels(labelNames); setLabelNamesMenuOpen(true); - setState({ labelNames, isLoadingLabelNames: undefined }); + // truncate the results the same amount as the metric select + const truncatedLabelNames = truncateResult(labelNames); + setState({ labelNames: truncatedLabelNames, isLoadingLabelNames: undefined }); }} onCloseMenu={() => { setLabelNamesMenuOpen(false); }} isOpen={labelNamesMenuOpen} isLoading={state.isLoadingLabelNames ?? false} - options={state.labelNames} + loadOptions={labelNamesSearch} + defaultOptions={state.labelNames} onChange={(change) => { if (change.label) { onChange({ diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx index 8647feee695..00bb07331e1 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx @@ -1,14 +1,28 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilters.test.tsx -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ComponentProps } from 'react'; +import { selectors } from '@grafana/e2e-selectors'; + import { selectOptionInTest } from '../../test/helpers/selectOptionInTest'; import { getLabelSelects } from '../testUtils'; import { LabelFilters, MISSING_LABEL_FILTER_ERROR_MESSAGE, LabelFiltersProps } from './LabelFilters'; describe('LabelFilters', () => { + it('truncates list of label names to 1000', async () => { + const manyMockValues = [...Array(1001).keys()].map((idx: number) => { + return { label: 'random_label' + idx }; + }); + + setup({ onGetLabelNames: jest.fn().mockResolvedValue(manyMockValues) }); + + await openLabelNamesSelect(); + + await waitFor(() => expect(screen.getAllByTestId(selectors.components.Select.option)).toHaveLength(1000)); + }); + it('renders empty input without labels', async () => { setup(); expect(screen.getAllByText('Select label')).toHaveLength(1); @@ -162,3 +176,8 @@ function setup(propOverrides?: Partial>) { function getAddButton() { return screen.getByLabelText(/Add/); } + +async function openLabelNamesSelect() { + const select = screen.getByText('Select label').parentElement!; + await userEvent.click(select); +} diff --git a/packages/grafana-prometheus/src/result_transformer.test.ts b/packages/grafana-prometheus/src/result_transformer.test.ts index b530babd4f5..761216b4230 100644 --- a/packages/grafana-prometheus/src/result_transformer.test.ts +++ b/packages/grafana-prometheus/src/result_transformer.test.ts @@ -350,6 +350,62 @@ describe('Prometheus Result Transformer', () => { expect(series.data[1].meta?.preferredVisualisationType).toEqual('rawPrometheus' as PreferredVisualisationType); }); + it('histogram results with table format have le values as strings for table filtering', () => { + const options = { + targets: [ + { + format: 'table', + refId: 'A', + }, + ], + } as unknown as DataQueryRequest; + const response = { + state: 'Done', + data: [ + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'Value', + type: FieldType.number, + values: [10, 10, 0], + labels: { le: '1' }, + }, + ], + }), + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'Value', + type: FieldType.number, + values: [30, 10, 40], + labels: { le: '+Inf' }, + }, + ], + }), + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'Value', + type: FieldType.number, + values: [20, 10, 30], + labels: { le: '2' }, + }, + ], + }), + ], + } as unknown as DataQueryResponse; + + const series = transformV2(response, options, {}); + const leFields = series.data[0].fields.filter((f) => f.name === 'le'); + const leValuesAreStrings = leFields[0].values.every((v) => typeof v === 'string'); + expect(leValuesAreStrings).toBe(true); + }); // Heatmap frames can either have a name of the metric, or if there is no metric, a name of "Value" it('results with heatmap format (no metric name) should be correctly transformed', () => { const options = { diff --git a/packages/grafana-prometheus/src/result_transformer.ts b/packages/grafana-prometheus/src/result_transformer.ts index a3f11c39087..7a901a0f995 100644 --- a/packages/grafana-prometheus/src/result_transformer.ts +++ b/packages/grafana-prometheus/src/result_transformer.ts @@ -202,11 +202,10 @@ export function transformDFToTable(dfs: DataFrame[]): DataFrame[] { .forEach((label) => { // If we don't have label in labelFields, add it if (!labelFields.some((l) => l.name === label)) { - const numberField = label === HISTOGRAM_QUANTILE_LABEL_NAME; labelFields.push({ name: label, config: { filterable: true }, - type: numberField ? FieldType.number : FieldType.string, + type: FieldType.string, values: [], }); } @@ -280,9 +279,6 @@ function getDataLinks(options: ExemplarTraceIdDestination): DataLink[] { function getLabelValue(metric: PromMetric, label: string): string | number { if (metric.hasOwnProperty(label)) { - if (label === HISTOGRAM_QUANTILE_LABEL_NAME) { - return parseSampleValue(metric[label]); - } return metric[label]; } return ''; diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 9b960b4b2c8..4497f035587 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -37,18 +37,18 @@ "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@grafana/data": "11.2.0-pre", - "@grafana/e2e-selectors": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/faro-web-sdk": "^1.3.6", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "history": "4.10.1", "lodash": "4.17.21", "rxjs": "7.8.1", "tslib": "2.6.3" }, "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-terser": "0.4.4", "@testing-library/dom": "10.0.0", diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index b2bc145eacf..ad37ef63f28 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -42,6 +42,11 @@ export type AppPluginConfig = { angular: AngularMeta; }; +export type PreinstalledPlugin = { + id: string; + version: string; +}; + export class GrafanaBootConfig implements GrafanaConfig { publicDashboardAccessToken?: string; publicDashboardsEnabled = true; @@ -124,6 +129,7 @@ export class GrafanaBootConfig implements GrafanaConfig { pluginAdminExternalManageEnabled = false; pluginCatalogHiddenPlugins: string[] = []; pluginCatalogManagedPlugins: string[] = []; + pluginCatalogPreinstalledPlugins: PreinstalledPlugin[] = []; pluginsCDNBaseURL = ''; expressionsEnabled = false; customTheme?: undefined; diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 5b99bf3496d..3bb8624372c 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" @@ -36,7 +36,7 @@ "postpack": "mv package.json.bak package.json" }, "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.2.3", "esbuild": "0.20.2", "glob": "^10.2.7", diff --git a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts index 5ce304ac6ba..fd19d4267ca 100644 --- a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { limit: number; diff --git a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts index 2a47f916ce3..a6b78e671c4 100644 --- a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting { /** diff --git a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts index 04ab4bc08ca..f34fac40264 100644 --- a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends common.SingleStatBaseOptions { displayMode: common.BarGaugeDisplayMode; diff --git a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts index 59e21e28834..0dea20acc4b 100644 --- a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export enum VizDisplayMode { Candles = 'candles', diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 955ada22eb7..b40116598e7 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export enum HorizontalConstraint { Center = 'center', diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index f49b4453da4..f59d93e60d2 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface MetricStat { /** diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index 9686c53d545..d64925b0958 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index a17ddfbbd32..bdc84b74fc5 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { selectedSeries: number; diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index d5ba8eb8008..d7d41860dad 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export type UpdateConfig = { render: boolean, diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index 0fdf5d23f46..d21b25534ac 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index b5cfa9d3c23..b9f809b5ea2 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends common.SingleStatBaseOptions { minVizHeight: number; diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index fa6bf48f9f8..8f3bb011101 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { basemap: ui.MapLayerOptions; diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index 0da60652bd3..b0764d00b3b 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; /** * Controls the color mode of the heatmap diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index 10ad5681b8e..ff2aea48588 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { /** diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index 92e3cb32e29..5af91fec64c 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -10,10 +10,11 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { dedupStrategy: common.LogsDedupStrategy; + displayedFields?: Array; enableLogDetails: boolean; isFilterLabelActive?: unknown; /** @@ -23,6 +24,8 @@ export interface Options { onClickFilterOutLabel?: unknown; onClickFilterOutString?: unknown; onClickFilterString?: unknown; + onClickHideField?: unknown; + onClickShowField?: unknown; prettifyLogMessage: boolean; showCommonLabels: boolean; showLabels: boolean; @@ -31,3 +34,7 @@ export interface Options { sortOrder: common.LogsSortOrder; wrapLogMessage: boolean; } + +export const defaultOptions: Partial = { + displayedFields: [], +}; diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index 518ffe39426..6298dd986e2 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export enum QueryEditorMode { Builder = 'builder', diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index ef2f1c75e6b..e011dddf763 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index 50ce8f07fc0..9405bd13003 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface ArcOption { /** diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index c61d5b90d3e..48aa1661936 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; /** * Select the pie chart display style. diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index 6ec283b76c1..c720c206ee0 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends common.SingleStatBaseOptions { colorMode: common.BigValueColorMode; diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index e5fa116eeb7..c65eb67fd0c 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 93b685c4df0..77e8c9cd68a 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index 5b521629e12..435c36726ee 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index a8e7dba879a..4e3daebded8 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export enum TextMode { Code = 'code', diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 810c2ff93ea..f8b95bf3f7d 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index 3cf487be608..bd84e783a06 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; /** * Identical to timeseries... except it does not have timezone settings diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index 0bd7ca39a90..6ca02d98289 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.2.0-pre"; +export const pluginVersion = "11.3.0-pre"; /** * Auto is "table" in the UI diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index d8fa9e895ec..f6796f0bf92 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "@grafana/sql", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git", @@ -15,11 +15,11 @@ }, "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", - "@grafana/e2e-selectors": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/experimental": "1.7.13", - "@grafana/runtime": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "@react-awesome-query-builder/ui": "6.6.2", "immutable": "4.3.7", "lodash": "4.17.21", @@ -34,7 +34,7 @@ "uuid": "9.0.1" }, "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "15.0.2", diff --git a/packages/grafana-sql/src/components/ConfirmModal.tsx b/packages/grafana-sql/src/components/ConfirmModal.tsx index 52090012b79..a926b5d74eb 100644 --- a/packages/grafana-sql/src/components/ConfirmModal.tsx +++ b/packages/grafana-sql/src/components/ConfirmModal.tsx @@ -25,7 +25,7 @@ export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmMod return ( +
Warning
@@ -57,4 +57,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ titleText: css({ paddingLeft: theme.spacing(2), }), + modalHeaderTitle: css({ + fontSize: theme.typography.size.lg, + float: 'left', + paddingTop: theme.spacing(1), + margin: theme.spacing(0, 2), + }), }); diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 0f46d100074..c937b635087 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "11.2.0-pre", + "version": "11.3.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -50,10 +50,10 @@ "@emotion/css": "11.11.2", "@emotion/react": "11.11.4", "@floating-ui/react": "0.26.22", - "@grafana/data": "11.2.0-pre", - "@grafana/e2e-selectors": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/faro-web-sdk": "^1.3.6", - "@grafana/schema": "11.2.0-pre", + "@grafana/schema": "11.3.0-pre", "@hello-pangea/dnd": "16.6.0", "@leeoniya/ufuzzy": "1.0.14", "@monaco-editor/react": "4.6.0", @@ -116,7 +116,7 @@ "devDependencies": { "@babel/core": "7.25.2", "@faker-js/faker": "^8.4.1", - "@grafana/tsconfig": "^1.3.0-rc1", + "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.2.3", "@storybook/addon-a11y": "^8.1.6", "@storybook/addon-actions": "^8.1.6", diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 6951d0dc98c..c553bcc8ef0 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -48,7 +48,11 @@ function estimateSize() { export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => { const MIN_WIDTH = 400; const [items, setItems] = useState(options); - const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]); + const selectedItemIndex = useMemo( + () => options.findIndex((option) => option.value === value) || null, + [options, value] + ); + const selectedItem = selectedItemIndex ? options[selectedItemIndex] : null; const inputRef = useRef(null); const floatingRef = useRef(null); @@ -66,6 +70,7 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro items, itemToString, selectedItem, + defaultHighlightedIndex: selectedItemIndex ?? undefined, scrollIntoView: () => {}, onInputValueChange: ({ inputValue }) => { setItems(options.filter(itemFilter(inputValue))); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index 3253dc59ba6..c800ccea7c6 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -112,7 +112,7 @@ export const DataLinksInlineEditor = ({ const key = `${link.title}/${idx}`; const linkJSX = ( -
+
{ // but we don't want the backdrop styling to apply over the top bar as it looks weird // instead have a child pseudo element to apply the backdrop styling below the top bar mask: css({ - backgroundColor: 'transparent', + // The !important here is to override the default .rc-drawer-mask styles + backgroundColor: 'transparent !important', position: 'fixed', '&:before': { diff --git a/packages/grafana-ui/src/components/Modal/ModalTabContent.tsx b/packages/grafana-ui/src/components/Modal/ModalTabContent.tsx index 1efe5b489e4..85f5a979287 100644 --- a/packages/grafana-ui/src/components/Modal/ModalTabContent.tsx +++ b/packages/grafana-ui/src/components/Modal/ModalTabContent.tsx @@ -1,5 +1,9 @@ +import { css } from '@emotion/css'; import * as React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; import { IconName } from '../../types'; interface Props { @@ -11,11 +15,23 @@ interface Props { /** @internal */ export const ModalTabContent = ({ children }: React.PropsWithChildren) => { + const styles = useStyles2(getStyles); + return ( -
-
-
{children}
+
+
+
{children}
); }; + +const getStyles = (theme: GrafanaTheme2) => ({ + header: css({ + display: 'flex', + margin: theme.spacing(0, 0, 3, 0), + }), + content: css({ + flexGrow: 1, + }), +}); diff --git a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx index ae959975bbb..00aaf6b4533 100644 --- a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx @@ -129,7 +129,8 @@ class UnthemedCodeEditor extends PureComponent { }; render() { - const { theme, language, width, height, showMiniMap, showLineNumbers, readOnly, monacoOptions } = this.props; + const { theme, language, width, height, showMiniMap, showLineNumbers, readOnly, wordWrap, monacoOptions } = + this.props; const { alwaysConsumeMouseWheel, ...restMonacoOptions } = monacoOptions ?? {}; const value = this.props.value ?? ''; @@ -138,7 +139,7 @@ class UnthemedCodeEditor extends PureComponent { const containerStyles = this.props.containerStyles ?? getStyles(theme).container; const options: MonacoOptions = { - wordWrap: 'off', + wordWrap: wordWrap ? 'on' : 'off', tabSize: 2, codeLens: false, contextmenu: false, diff --git a/packages/grafana-ui/src/components/Monaco/types.ts b/packages/grafana-ui/src/components/Monaco/types.ts index d41fcbe13d3..0d403c0ca24 100644 --- a/packages/grafana-ui/src/components/Monaco/types.ts +++ b/packages/grafana-ui/src/components/Monaco/types.ts @@ -27,6 +27,7 @@ export interface CodeEditorProps { readOnly?: boolean; showMiniMap?: boolean; showLineNumbers?: boolean; + wordWrap?: boolean; monacoOptions?: MonacoOptions; /** diff --git a/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx b/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx index af8720cca61..403a7c4b408 100644 --- a/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx +++ b/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx @@ -1,21 +1,37 @@ import { HTMLAttributes } from 'react'; -import { PluginSignatureStatus } from '@grafana/data'; +import { PluginSignatureStatus, PluginSignatureType } from '@grafana/data'; +import { IconName } from '../../types'; import { Badge, BadgeProps } from '../Badge/Badge'; +const SIGNATURE_ICONS: Record = { + [PluginSignatureType.grafana]: 'grafana', + [PluginSignatureType.commercial]: 'shield', + [PluginSignatureType.community]: 'shield', + DEFAULT: 'shield-exclamation', +}; + /** * @public */ export interface PluginSignatureBadgeProps extends HTMLAttributes { status?: PluginSignatureStatus; + signatureType?: PluginSignatureType; + signatureOrg?: string; } /** * @public */ -export const PluginSignatureBadge = ({ status, color, ...otherProps }: PluginSignatureBadgeProps) => { - const display = getSignatureDisplayModel(status); +export const PluginSignatureBadge = ({ + status, + color, + signatureType, + signatureOrg, + ...otherProps +}: PluginSignatureBadgeProps) => { + const display = getSignatureDisplayModel(status, signatureType, signatureOrg); return ( ); @@ -23,16 +39,27 @@ export const PluginSignatureBadge = ({ status, color, ...otherProps }: PluginSig PluginSignatureBadge.displayName = 'PluginSignatureBadge'; -function getSignatureDisplayModel(signature?: PluginSignatureStatus): BadgeProps { +function getSignatureDisplayModel( + signature?: PluginSignatureStatus, + signatureType?: PluginSignatureType, + signatureOrg?: string +): BadgeProps { if (!signature) { signature = PluginSignatureStatus.invalid; } + const signatureIcon = SIGNATURE_ICONS[signatureType || ''] || SIGNATURE_ICONS.DEFAULT; + switch (signature) { case PluginSignatureStatus.internal: return { text: 'Core', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' }; case PluginSignatureStatus.valid: - return { text: 'Signed', icon: 'lock', color: 'green', tooltip: 'Signed and verified plugin' }; + return { + text: signatureType ? signatureType : 'Signed', + icon: signatureType ? signatureIcon : 'lock', + color: 'green', + tooltip: 'Signed and verified plugin', + }; case PluginSignatureStatus.invalid: return { text: 'Invalid signature', diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index 1ecba02b578..09d58a9ebac 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -207,6 +207,7 @@ export class Sparkline extends PureComponent { fillColor: customConfig.fillColor, lineStyle: customConfig.lineStyle, gradientMode: customConfig.gradientMode, + spanNulls: customConfig.spanNulls, }); } diff --git a/packages/grafana-ui/src/components/Table/CellActions.tsx b/packages/grafana-ui/src/components/Table/CellActions.tsx index e5908664428..222c441d5e2 100644 --- a/packages/grafana-ui/src/components/Table/CellActions.tsx +++ b/packages/grafana-ui/src/components/Table/CellActions.tsx @@ -6,12 +6,12 @@ import { IconButton } from '../IconButton/IconButton'; import { Stack } from '../Layout/Stack/Stack'; import { TooltipPlacement } from '../Tooltip'; -import { TableCellInspector } from './TableCellInspector'; +import { TableCellInspector, TableCellInspectorMode } from './TableCellInspector'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps } from './types'; import { getTextAlign } from './utils'; interface CellActionProps extends TableCellProps { - previewMode: 'text' | 'code'; + previewMode: TableCellInspectorMode; } interface CommonButtonProps { diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 02b03f10ea3..77a30a7c051 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -11,6 +11,7 @@ import { clearLinkButtonStyles } from '../Button'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; +import { TableCellInspectorMode } from './TableCellInspector'; import { TableStyles } from './styles'; import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './types'; import { getCellColors, getCellOptions } from './utils'; @@ -114,7 +115,9 @@ export const DefaultCell = (props: TableCellProps) => { )} - {hover && showActions && } + {hover && showActions && ( + + )}
); }; diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx index ba5691b2c7e..d013d6dc6ad 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx @@ -7,6 +7,7 @@ import { Button, clearLinkButtonStyles } from '../Button'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; +import { TableCellInspectorMode } from './TableCellInspector'; import { TableCellProps } from './types'; export function JSONViewCell(props: TableCellProps): JSX.Element { @@ -51,7 +52,7 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { )}
- {inspectEnabled && } + {inspectEnabled && }
); } diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index b99ad697a87..ef45bd98400 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -43,7 +43,7 @@ export const TableCell = ({ cellProps.style.minWidth = cellProps.style.width; const justifyContent = (cell.column as any).justifyContent; - if (justifyContent === 'flex-end') { + if (justifyContent === 'flex-end' && !field.config.unit) { // justify-content flex-end is not compatible with cellLink overflow; use direction instead cellProps.style.textAlign = 'right'; cellProps.style.direction = 'rtl'; diff --git a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx index 7cc49f706b6..7f318608705 100644 --- a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx @@ -1,57 +1,84 @@ import { isString } from 'lodash'; +import { useState } from 'react'; import { ClipboardButton } from '../ClipboardButton/ClipboardButton'; import { Drawer } from '../Drawer/Drawer'; +import { Stack } from '../Layout/Stack/Stack'; import { CodeEditor } from '../Monaco/CodeEditor'; +import { Tab, TabsBar } from '../Tabs'; + +export enum TableCellInspectorMode { + code = 'code', + text = 'text', +} interface TableCellInspectorProps { value: any; onDismiss: () => void; - mode: 'code' | 'text'; + mode: TableCellInspectorMode; } export function TableCellInspector({ value, onDismiss, mode }: TableCellInspectorProps) { let displayValue = value; + const [currentMode, setMode] = useState(mode); + if (isString(value)) { const trimmedValue = value.trim(); // Exclude numeric strings like '123' from being displayed in code/JSON mode if (trimmedValue[0] === '{' || trimmedValue[0] === '[' || mode === 'code') { try { value = JSON.parse(value); - mode = 'code'; - } catch { - mode = 'text'; - } // ignore errors - } else { - mode = 'text'; + } catch {} } } else { displayValue = JSON.stringify(value, null, ' '); } let text = displayValue; - if (mode === 'code') { - text = JSON.stringify(value, null, ' '); - } + const tabs = [ + { + label: 'Plain text', + value: 'text', + }, + { + label: 'Code editor', + value: 'code', + }, + ]; + + const changeTabs = () => { + setMode(currentMode === TableCellInspectorMode.text ? TableCellInspectorMode.code : TableCellInspectorMode.text); + }; + + const tabBar = ( + + {tabs.map((t, index) => ( + + ))} + + ); return ( - - {mode === 'code' ? ( - 100} - value={text} - readOnly={true} - /> - ) : ( -
{text}
- )} - text}> - Copy to Clipboard - + + + text} style={{ marginLeft: 'auto', width: '200px' }}> + Copy to Clipboard + + {currentMode === 'code' ? ( + 100} + value={text} + readOnly={true} + wordWrap={true} + /> + ) : ( +
{text}
+ )} +
); } diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 940734cbd2f..bbd6c66cc13 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -645,7 +645,7 @@ export function guessTextBoundingBox( lineHeight: number, defaultRowHeight: number ) { - const width = Number(headerGroup.width ?? 300); + const width = Number(headerGroup?.width ?? 300); const LINE_SCALE_FACTOR = 1.17; const LOW_LINE_PAD = 42; diff --git a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx index 59587af744d..6434df3cdb6 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx +++ b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx @@ -7,6 +7,7 @@ import { getAlertingStyles } from './alerting'; import { getAgularPanelStyles } from './angularPanelStyles'; import { getCardStyles } from './card'; import { getCodeStyles } from './code'; +import { getDashboardGridStyles } from './dashboardGrid'; import { getDashDiffStyles } from './dashdiff'; import { getElementStyles } from './elements'; import { getExtraStyles } from './extra'; @@ -35,6 +36,7 @@ export function GlobalStyles() { getAlertingStyles(theme), getCodeStyles(theme), getDashDiffStyles(theme), + getDashboardGridStyles(theme), getElementStyles(theme), getExtraStyles(theme), getFilterTableStyles(theme), diff --git a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts new file mode 100644 index 00000000000..bbd6d6da876 --- /dev/null +++ b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts @@ -0,0 +1,68 @@ +import { css } from '@emotion/react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +export function getDashboardGridStyles(theme: GrafanaTheme2) { + return css({ + '.react-resizable-handle': { + // this needs to use visibility and not display none in order not to cause resize flickering + visibility: 'hidden', + }, + + '.react-grid-item, #grafana-portal-container': { + touchAction: 'initial !important', + + '&:hover': { + '.react-resizable-handle': { + visibility: 'visible', + }, + }, + }, + + [theme.breakpoints.down('md')]: { + '.react-grid-item': { + display: 'block !important', + transitionProperty: 'none !important', + // can't avoid type assertion here due to !important + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + position: 'unset !important' as 'unset', + transform: 'translate(0px, 0px) !important', + marginBottom: theme.spacing(2), + }, + '.panel-repeater-grid-item': { + height: 'auto !important', + }, + }, + + '.react-grid-item.react-grid-placeholder': { + boxShadow: `0 0 4px ${theme.colors.primary.border} !important`, + background: `${theme.colors.primary.transparent} !important`, + zIndex: '-1 !important', + opacity: 'unset !important', + }, + + '.react-grid-item > .react-resizable-handle::after': { + borderRight: `2px solid ${theme.isDark ? theme.v1.palette.gray1 : theme.v1.palette.gray3} !important`, + borderBottom: `2px solid ${theme.isDark ? theme.v1.palette.gray1 : theme.v1.palette.gray3} !important`, + }, + + // Hack for preventing panel menu overlapping. + '.react-grid-item.resizing.panel, .react-grid-item.panel.dropdown-menu-open, .react-grid-item.react-draggable-dragging.panel': + { + zIndex: theme.zIndex.dropdown, + }, + + // Disable animation on initial rendering and enable it when component has been mounted. + '.react-grid-item.cssTransforms': { + transitionProperty: 'none !important', + }, + + [theme.transitions.handleMotion('no-preference')]: { + '.react-grid-layout--enable-move-animations': { + '.react-grid-item.cssTransforms': { + transitionProperty: 'transform !important', + }, + }, + }, + }); +} diff --git a/packages/grafana-ui/src/themes/GlobalStyles/elements.ts b/packages/grafana-ui/src/themes/GlobalStyles/elements.ts index 800a79b2b03..6586dbb6e0d 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/elements.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/elements.ts @@ -337,6 +337,13 @@ export function getElementStyles(theme: GrafanaTheme2) { '.template-variable': { color: theme.colors.primary.text, }, + + '.modal-header-title': { + fontSize: theme.typography.size.lg, + float: 'left', + paddingTop: theme.spacing(1), + margin: theme.spacing(0, 2), + }, }); } diff --git a/packages/grafana-ui/src/themes/GlobalStyles/forms.ts b/packages/grafana-ui/src/themes/GlobalStyles/forms.ts index c3a87d89adf..2cc1b03e94a 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/forms.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/forms.ts @@ -27,5 +27,345 @@ export function getFormElementStyles(theme: GrafanaTheme2) { { width: 'auto', // Override of generic input selector }, + '.gf-form': { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + textAlign: 'left', + position: 'relative', + marginBottom: theme.spacing(0.5), + + '&--offset-1': { + marginLeft: theme.spacing(2), + }, + + '&--grow': { + flexGrow: 1, + }, + + '&--flex-end': { + justifyContent: 'flex-end', + }, + + '&--align-center': { + alignContent: 'center', + }, + + '&--alt': { + flexDirection: 'column', + alignItems: 'flex-start', + + '.gf-form-label': { + padding: '4px 0', + }, + }, + }, + '.gf-form--has-input-icon': { + position: 'relative', + marginRight: theme.spacing(0.5), + + '.gf-form-input-icon': { + position: 'absolute', + top: '8px', + fontSize: theme.typography.size.lg, + left: '10px', + color: theme.colors.text.disabled, + }, + + '> input': { + paddingLeft: '35px', + + '&:focus + .gf-form-input-icon': { + color: theme.colors.text.secondary, + }, + }, + + '.Select--multi .Select-multi-value-wrapper, .Select-placeholder': { + paddingLeft: '30px', + }, + }, + + '.gf-form-disabled': { + color: theme.colors.text.secondary, + + '.gf-form-select-wrapper::after': { + color: theme.colors.text.secondary, + }, + + 'a, .gf-form-input': { + color: theme.colors.text.secondary, + }, + }, + + '.gf-form-group': { + marginBottom: theme.spacing(5), + }, + '.gf-form-inline': { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + alignContent: 'flex-start', + + '&--nowrap': { + flexWrap: 'nowrap', + }, + + '&--xs-view-flex-column': { + flexDirection: 'row', + flexWrap: 'nowrap', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + }, + }, + + '.select-container': { + marginRight: theme.spacing(0.5), + }, + + '.gf-form-spacing': { + marginRight: theme.spacing(0.5), + }, + }, + + '.gf-form-button-row': { + paddingTop: theme.spacing(3), + 'a, button': { + marginRight: theme.spacing(2), + }, + }, + '.gf-form-label': { + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + flexShrink: 0, + fontWeight: theme.typography.fontWeightMedium, + fontSize: theme.typography.size.sm, + backgroundColor: theme.colors.background.secondary, + height: '32px', + lineHeight: '32px', + marginRight: theme.spacing(0.5), + borderRadius: theme.shape.radius.default, + justifyContent: 'space-between', + border: 'none', + + '&--grow': { + flexGrow: 1, + }, + + '&--transparent': { + backgroundColor: 'transparent', + border: 0, + textAlign: 'right', + paddingLeft: 0, + }, + + '&--variable': { + color: theme.colors.primary.text, + background: theme.components.panel.background, + border: `1px solid ${theme.components.panel.borderColor}`, + }, + + '&--btn': { + border: 'none', + borderRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.background.secondary, + color: theme.colors.text.primary, + }, + }, + + '&:disabled': { + color: theme.colors.text.secondary, + }, + }, + '.gf-form-label + .gf-form-label': { + marginRight: theme.spacing(0.5), + }, + '.gf-form-pre': { + display: 'block !important', + flexGrow: 1, + margin: 0, + marginRight: theme.spacing(0.5), + border: `1px solid transparent`, + borderLeft: 'none', + borderRadius: theme.shape.radius.default, + }, + '.gf-form-textarea': { + maxWidth: '650px', + }, + '.gf-form-input': { + display: 'block', + width: '100%', + height: '32px', + padding: theme.spacing(0, 1), + fontSize: theme.typography.size.md, + lineHeight: '18px', + color: theme.components.input.text, + backgroundColor: theme.components.input.background, + backgroundImage: 'none', + backgroundClip: 'padding-box', + border: `1px solid ${theme.components.input.borderColor}`, + borderRadius: theme.shape.radius.default, + marginRight: theme.spacing(0.5), + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + // text areas should be scrollable + '&textarea': { + overflow: 'auto', + whiteSpace: 'pre-wrap', + padding: `6px ${theme.spacing(1)}`, + minHeight: '32px', + height: 'auto', + }, + + // Unstyle the caret on ` { (r) => r.url.includes('/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST' ); - const body = await amConfigUpdateRequest?.clone().json(); + const body: AlertManagerCortexConfig = await amConfigUpdateRequest?.clone().json(); expect(body.alertmanager_config.mute_time_intervals).toHaveLength(0); }); diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index bbd6e3acca0..3f2fb940696 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -1,4 +1,3 @@ -import { produce } from 'immer'; import { useEffect } from 'react'; import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; @@ -6,22 +5,25 @@ import { timeIntervalsApi } from 'app/features/alerting/unified/api/timeInterval import { mergeTimeIntervals } from 'app/features/alerting/unified/components/mute-timings/util'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval, - ReadNamespacedTimeIntervalApiResponse, + IoK8SApimachineryPkgApisMetaV1ObjectMeta, } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; -import { deleteMuteTimingAction, updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions'; import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks'; -import { renameMuteTimings } from 'app/features/alerting/unified/utils/alertmanager'; -import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { getK8sNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; + +import { useAsync } from '../../hooks/useAsync'; +import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; +import { + addMuteTimingAction, + deleteMuteTimingAction, + updateMuteTimingAction, +} from '../../reducers/alertmanager/muteTimings'; const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; const { useLazyListNamespacedTimeIntervalQuery, useCreateNamespacedTimeIntervalMutation, - useLazyReadNamespacedTimeIntervalQuery, useReplaceNamespacedTimeIntervalMutation, useDeleteNamespacedTimeIntervalMutation, } = timeIntervalsApi; @@ -32,7 +34,7 @@ const { * */ export type MuteTiming = MuteTimeInterval & { id: string; - metadata?: ReadNamespacedTimeIntervalApiResponse['metadata']; + metadata?: IoK8SApimachineryPkgApisMetaV1ObjectMeta; }; /** Alias for generated kuberenetes Alerting API Server type */ @@ -113,6 +115,8 @@ export const useMuteTimings = ({ alertmanager }: BaseAlertmanagerArgs) => { return useK8sApi ? intervalsResponse : configApiResponse; }; +type CreateUpdateMuteTimingArgs = { interval: MuteTimeInterval }; + /** * Create a new mute timing. * @@ -124,40 +128,24 @@ export const useMuteTimings = ({ alertmanager }: BaseAlertmanagerArgs) => { export const useCreateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const dispatch = useDispatch(); const [createGrafanaTimeInterval] = useCreateNamespacedTimeIntervalMutation(); - const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery(); + const [updateConfiguration] = useProduceNewAlertmanagerConfiguration(); - const isGrafanaAm = alertmanager === GRAFANA_RULES_SOURCE_NAME; - - if (useK8sApi) { + const addToK8sAPI = useAsync(({ interval }: CreateUpdateMuteTimingArgs) => { const namespace = getK8sNamespace(); - return ({ timeInterval }: { timeInterval: MuteTimeInterval }) => - createGrafanaTimeInterval({ - namespace, - comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: { metadata: {}, spec: timeInterval }, - }).unwrap(); - } - return async ({ timeInterval }: { timeInterval: MuteTimeInterval }) => { - const result = await getAlertmanagerConfig(alertmanager).unwrap(); - const newConfig = produce(result, (draft) => { - const propertyToUpdate = isGrafanaAm ? 'mute_time_intervals' : 'time_intervals'; - draft.alertmanager_config[propertyToUpdate] = draft.alertmanager_config[propertyToUpdate] ?? []; - draft.alertmanager_config[propertyToUpdate] = (draft.alertmanager_config[propertyToUpdate] ?? []).concat( - timeInterval - ); - }); + return createGrafanaTimeInterval({ + namespace, + comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: { metadata: {}, spec: interval }, + }).unwrap(); + }); - return dispatch( - updateAlertManagerConfigAction({ - newConfig, - oldConfig: result, - alertManagerSourceName: alertmanager, - successMessage: 'Mute timing saved', - }) - ).unwrap(); - }; + const addToAlertmanagerConfiguration = useAsync(({ interval }: CreateUpdateMuteTimingArgs) => { + const action = addMuteTimingAction({ interval }); + return updateConfiguration(action); + }); + + return useK8sApi ? addToK8sAPI : addToAlertmanagerConfiguration; }; /** @@ -167,14 +155,18 @@ export const useCreateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertmanagerArgs & { name: string }) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const [getGrafanaTimeInterval, k8sResponse] = useLazyReadNamespacedTimeIntervalQuery({ + const [getGrafanaTimeInterval, k8sResponse] = useLazyListNamespacedTimeIntervalQuery({ selectFromResult: ({ data, ...rest }) => { if (!data) { return { data, ...rest }; } + if (data.items.length === 0) { + return { ...rest, data: undefined, isError: true }; + } + return { - data: parseK8sTimeInterval(data), + data: parseK8sTimeInterval(data.items[0]), ...rest, }; }, @@ -203,7 +195,7 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma useEffect(() => { if (useK8sApi) { const namespace = getK8sNamespace(); - getGrafanaTimeInterval({ namespace, name: nameToFind }, true); + getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${nameToFind}` }, true); } else { getAlertmanagerTimeInterval(alertmanager, true); } @@ -223,84 +215,59 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma export const useUpdateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const dispatch = useDispatch(); const [replaceGrafanaTimeInterval] = useReplaceNamespacedTimeIntervalMutation(); - const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery(); + const [updateConfiguration] = useProduceNewAlertmanagerConfiguration(); - if (useK8sApi) { - return async ({ timeInterval, originalName }: { timeInterval: MuteTimeInterval; originalName: string }) => { + const updateToK8sAPI = useAsync( + async ({ interval, originalName }: CreateUpdateMuteTimingArgs & { originalName: string }) => { const namespace = getK8sNamespace(); + return replaceGrafanaTimeInterval({ name: originalName, namespace, comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: { - spec: timeInterval, + spec: interval, metadata: { name: originalName }, }, }).unwrap(); - }; - } + } + ); - return async ({ timeInterval, originalName }: { timeInterval: MuteTimeInterval; originalName: string }) => { - const nameHasChanged = timeInterval.name !== originalName; - const result = await getAlertmanagerConfig(alertmanager).unwrap(); + const updateToAlertmanagerConfiguration = useAsync( + async ({ interval, originalName }: CreateUpdateMuteTimingArgs & { originalName: string }) => { + const action = updateMuteTimingAction({ interval, originalName }); + return updateConfiguration(action); + } + ); - const newConfig = produce(result, (draft) => { - const existingIntervalIndex = (draft.alertmanager_config?.time_intervals || [])?.findIndex( - ({ name }) => name === originalName - ); - if (existingIntervalIndex !== -1) { - draft.alertmanager_config.time_intervals![existingIntervalIndex] = timeInterval; - } - - const existingMuteIntervalIndex = (draft.alertmanager_config?.mute_time_intervals || [])?.findIndex( - ({ name }) => name === originalName - ); - if (existingMuteIntervalIndex !== -1) { - draft.alertmanager_config.mute_time_intervals![existingMuteIntervalIndex] = timeInterval; - } - - if (nameHasChanged && draft.alertmanager_config.route) { - draft.alertmanager_config.route = renameMuteTimings( - timeInterval.name, - originalName, - draft.alertmanager_config.route - ); - } - }); - - return dispatch( - updateAlertManagerConfigAction({ - newConfig, - oldConfig: result, - alertManagerSourceName: alertmanager, - successMessage: 'Mute timing saved', - }) - ).unwrap(); - }; + return useK8sApi ? updateToK8sAPI : updateToAlertmanagerConfiguration; }; /** * Delete a mute timing interval */ +type DeleteMuteTimingArgs = { name: string }; export const useDeleteMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const dispatch = useDispatch(); + const [updateConfiguration, _updateConfigurationRequestState] = useProduceNewAlertmanagerConfiguration(); const [deleteGrafanaTimeInterval] = useDeleteNamespacedTimeIntervalMutation(); - if (useK8sApi) { - return async ({ name }: { name: string }) => { - const namespace = getK8sNamespace(); - return deleteGrafanaTimeInterval({ - name, - namespace, - ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, - }).unwrap(); - }; - } + const deleteFromAlertmanagerAPI = useAsync(async ({ name }: DeleteMuteTimingArgs) => { + const action = deleteMuteTimingAction({ name }); + return updateConfiguration(action); + }); - return async ({ name }: { name: string }) => dispatch(deleteMuteTimingAction(alertmanager, name)); + const deleteFromK8sAPI = useAsync(async ({ name }: DeleteMuteTimingArgs) => { + const namespace = getK8sNamespace(); + await deleteGrafanaTimeInterval({ + name, + namespace, + ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, + }).unwrap(); + }); + + return useK8sApi ? deleteFromK8sAPI : deleteFromAlertmanagerAPI; }; export const useValidateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { diff --git a/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx index 29d496d1eec..bc0a9a6ec0f 100644 --- a/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx @@ -1,52 +1,115 @@ -import { SelectableValue } from '@grafana/data'; -import { Select, SelectCommonProps, Text, Stack } from '@grafana/ui'; +import { css, cx, keyframes } from '@emotion/css'; +import { useMemo, useState } from 'react'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { Select, SelectCommonProps, Stack, Alert, IconButton, Text, useStyles2 } from '@grafana/ui'; +import { ContactPointReceiverSummary } from 'app/features/alerting/unified/components/contact-points/ContactPoint'; import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; -import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from '../contact-points/constants'; import { useContactPointsWithStatus } from '../contact-points/useContactPoints'; -import { ReceiverConfigWithMetadata } from '../contact-points/utils'; +import { ContactPointWithMetadata } from '../contact-points/utils'; -export const ContactPointSelector = (props: SelectCommonProps) => { +const MAX_CONTACT_POINTS_RENDERED = 500; + +// Mock sleep method, as fetching receivers is very fast and may seem like it hasn't occurred +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const LOADING_SPINNER_DURATION = 1000; + +type ContactPointSelectorProps = { + selectProps: SelectCommonProps; + showRefreshButton?: boolean; + /** Name of a contact point to optionally find and set as the preset value on the dropdown */ + selectedContactPointName?: string | null; +}; + +export const ContactPointSelector = ({ + selectProps, + showRefreshButton, + selectedContactPointName, +}: ContactPointSelectorProps) => { const { selectedAlertmanager } = useAlertmanager(); - const { contactPoints, isLoading, error } = useContactPointsWithStatus({ alertmanager: selectedAlertmanager! }); + const { contactPoints, isLoading, error, refetch } = useContactPointsWithStatus({ + alertmanager: selectedAlertmanager!, + }); + const [loaderSpinning, setLoaderSpinning] = useState(false); + const styles = useStyles2(getStyles); - // TODO error handling - if (error) { - return Failed to load contact points; - } - - const options: Array> = contactPoints.map((contactPoint) => { + const options: Array> = contactPoints.map((contactPoint) => { return { label: contactPoint.name, - value: contactPoint.name, - component: () => , + value: contactPoint, + component: () => ( + + + + ), }; }); - return MAX_CONTACT_POINTS_RENDERED} + options={options} + value={matchedContactPoint} + {...selectProps} + isLoading={isLoading} + /> + {showRefreshButton && ( + + )} ); }; + +const rotation = keyframes({ + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(720deg)', + }, +}); + +const getStyles = (theme: GrafanaTheme2) => ({ + refreshButton: css({ + color: theme.colors.text.secondary, + cursor: 'pointer', + borderRadius: theme.shape.radius.circle, + overflow: 'hidden', + }), + loading: css({ + pointerEvents: 'none', + [theme.transitions.handleMotion('no-preference')]: { + animation: `${rotation} 2s infinite linear`, + }, + [theme.transitions.handleMotion('reduce')]: { + animation: `${rotation} 6s infinite linear`, + }, + }), +}); diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx index 3c16817ebb2..e7ed08151a5 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx @@ -3,12 +3,14 @@ import { render } from 'test/test-utils'; import { byRole } from 'testing-library-selector'; import { Button } from '@grafana/ui'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; +import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; +import { AccessControlAction } from 'app/types'; import { RouteWithID } from '../../../../../plugins/datasource/alertmanager/types'; -import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp'; import { FormAmRoute } from '../../types/amroutes'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; -import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types'; import { AmRootRouteForm } from './EditDefaultPolicyForm'; @@ -20,12 +22,15 @@ const ui = { groupIntervalInput: byRole('textbox', { name: /Group interval/ }), repeatIntervalInput: byRole('textbox', { name: /Repeat interval/ }), }; - -const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker'); -useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined); - +setupMswServer(); // TODO Default and Notification policy form should be unified so we don't need to maintain two almost identical forms describe('EditDefaultPolicyForm', function () { + beforeEach(() => { + grantUserPermissions([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + ]); + }); describe('Timing options', function () { it('should render prometheus duration strings in form inputs', async function () { const { user } = renderRouteForm({ @@ -47,7 +52,6 @@ describe('EditDefaultPolicyForm', function () { id: '0', receiver: 'default', }, - [{ value: 'default', label: 'Default' }], onSubmit ); @@ -78,7 +82,6 @@ describe('EditDefaultPolicyForm', function () { id: '0', receiver: 'default', }, - [{ value: 'default', label: 'Default' }], onSubmit ); @@ -105,7 +108,6 @@ describe('EditDefaultPolicyForm', function () { group_interval: '2d4h30m35s', repeat_interval: '1w2d6h', }, - [{ value: 'default', label: 'Default' }], onSubmit ); @@ -128,18 +130,15 @@ describe('EditDefaultPolicyForm', function () { }); }); -function renderRouteForm( - route: RouteWithID, - receivers: AmRouteReceiver[] = [], - onSubmit: (route: Partial) => void = noop -) { +function renderRouteForm(route: RouteWithID, onSubmit: (route: Partial) => void = noop) { return render( - Update default policy} - onSubmit={onSubmit} - receivers={receivers} - route={route} - /> + + Update default policy} + onSubmit={onSubmit} + route={route} + /> + ); } diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx index 5f75bfcde1c..bfba851da73 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx @@ -1,7 +1,9 @@ import { ReactNode, useState } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { Collapse, Field, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui'; +import { Collapse, Field, Link, MultiSelect, useStyles2 } from '@grafana/ui'; +import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; +import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils'; import { RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../../types/amroutes'; @@ -9,14 +11,12 @@ import { amRouteToFormAmRoute, commonGroupByOptions, mapMultiSelectValueToStrings, - mapSelectValueToString, promDurationValidator, repeatIntervalValidator, stringsToSelectableValues, stringToSelectableValue, } from '../../utils/amroutes'; import { makeAMLink } from '../../utils/misc'; -import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types'; import { PromDurationInput } from './PromDurationInput'; import { getFormStyles } from './formStyles'; @@ -26,17 +26,10 @@ export interface AmRootRouteFormProps { alertManagerSourceName: string; actionButtons: ReactNode; onSubmit: (route: Partial) => void; - receivers: AmRouteReceiver[]; route: RouteWithID; } -export const AmRootRouteForm = ({ - actionButtons, - alertManagerSourceName, - onSubmit, - receivers, - route, -}: AmRootRouteFormProps) => { +export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmit, route }: AmRootRouteFormProps) => { const styles = useStyles2(getFormStyles); const [isTimingOptionsExpanded, setIsTimingOptionsExpanded] = useState(false); const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route.group_by)); @@ -62,13 +55,13 @@ export const AmRootRouteForm = ({ <>
( - onChange(mapSelectValueToString(value))} - options={receiversWithOnCallOnTop} - isClearable + render={({ field: { onChange, ref, value, ...field } }) => ( + handleContactPointSelect(value, onChange), + isClearable: true, + }} + selectedContactPointName={value} /> )} control={control} @@ -298,14 +302,6 @@ export const AmRoutesExpandedForm = ({ ); }; -function onCallFirst(receiver: AmRouteReceiver) { - if (receiver.grafanaAppReceiverType === SupportedPlugin.OnCall) { - return -1; - } else { - return 0; - } -} - const getStyles = (theme: GrafanaTheme2) => { const commonSpacing = theme.spacing(3.5); diff --git a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx index 4989cf15a83..5fb6c57459e 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/css'; import { debounce, isEqual } from 'lodash'; import { useCallback, useEffect, useRef } from 'react'; -import { SelectableValue } from '@grafana/data'; -import { Button, Field, Icon, Input, Label, Select, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; -import { ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; +import { Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; +import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; +import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { useURLSearchParams } from '../../hooks/useURLSearchParams'; import { matcherToObjectMatcher } from '../../utils/alertmanager'; @@ -15,14 +15,12 @@ import { } from '../../utils/matchers'; interface NotificationPoliciesFilterProps { - receivers: Receiver[]; onChangeMatchers: (labels: ObjectMatcher[]) => void; onChangeReceiver: (receiver: string | undefined) => void; matchingCount: number; } const NotificationPoliciesFilter = ({ - receivers, onChangeReceiver, onChangeMatchers, matchingCount, @@ -47,12 +45,9 @@ const NotificationPoliciesFilter = ({ if (searchInputRef.current) { searchInputRef.current.value = ''; } - setSearchParams({ contactPoint: undefined, queryString: undefined }); + setSearchParams({ contactPoint: '', queryString: undefined }); }, [setSearchParams]); - const receiverOptions: Array> = receivers.map(toOption); - const selectedContactPoint = receiverOptions.find((option) => option.value === contactPoint) ?? null; - const hasFilters = queryString || contactPoint; let inputValid = Boolean(queryString && queryString.length > 3); @@ -103,16 +98,17 @@ const NotificationPoliciesFilter = ({ /> - + + + + + + + + ); +}; diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index e210d39707d..5f81ee11bde 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -4,6 +4,7 @@ import { config } from '@grafana/runtime'; import { TextLink } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; +import { ServerDiscoveryField } from './components/ServerDiscoveryField'; import { FieldData, SSOProvider, SSOSettingsField } from './types'; import { isSelectableValue } from './utils/guards'; import { isUrlValid } from './utils/url'; @@ -67,6 +68,7 @@ export const sectionFields: Section = { 'clientSecret', 'authStyle', 'scopes', + 'serverDiscoveryUrl', 'authUrl', 'tokenUrl', 'apiUrl', @@ -620,6 +622,13 @@ export function fieldMap(provider: string): Record { 'If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the Allowed Domains list specified by the user.', type: 'checkbox', }, + serverDiscoveryUrl: { + label: 'OpenID Connect Discovery URL', + description: + 'The .well-known/openid-configuration endpoint for your IdP. The info extracted from this URL will be used to populate the Auth URL, Token URL and API URL fields.', + type: 'custom', + content: (setValue) => , + }, }; } diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts index 19c827fd356..e35622b06c5 100644 --- a/public/app/features/auth-config/types.ts +++ b/public/app/features/auth-config/types.ts @@ -1,5 +1,6 @@ import { ReactElement } from 'react'; import { Validate } from 'react-hook-form'; +import { UseFormSetValue } from 'react-hook-form/dist/types/form'; import { IconName, SelectableValue } from '@grafana/data'; import { Settings } from 'app/types'; @@ -72,6 +73,7 @@ export type SSOProvider = { allowedGroups?: string; scopes?: string; orgMapping?: string; + serverDiscoveryUrl?: string; }; }; @@ -83,6 +85,7 @@ export type SSOProviderDTO = Partial & { allowedGroups?: Array>; scopes?: Array>; orgMapping?: Array>; + serverDiscoveryUrl?: string; }; export interface AuthConfigState { @@ -123,8 +126,13 @@ export type FieldData = { placeholder?: string; defaultValue?: SelectableValue; hidden?: boolean; + content?: (setValue: UseFormSetValue) => ReactElement; }; export type SSOSettingsField = | keyof SSOProvider['settings'] | { name: keyof SSOProvider['settings']; dependsOn: keyof SSOProvider['settings']; hidden?: boolean }; + +export interface ServerDiscoveryFormData { + url: string; +} diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 93c73f97d01..47c7137b065 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -87,7 +87,9 @@ const BrowseDashboardsPage = memo(({ match }: Props) => { const hasSelection = useHasSelection(); - const { canEditFolders, canEditDashboards, canCreateDashboards, canCreateFolders } = getFolderPermissions(folderDTO); + const { data: rootFolder } = useGetFolderQuery('general'); + let folder = folderDTO ? folderDTO : rootFolder; + const { canEditFolders, canEditDashboards, canCreateDashboards, canCreateFolders } = getFolderPermissions(folder); const showEditTitle = canEditFolders && folderUID; const canSelect = canEditFolders || canEditDashboards; diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts index f107500de11..c70c9ee25ab 100644 --- a/public/app/features/browse-dashboards/permissions.ts +++ b/public/app/features/browse-dashboards/permissions.ts @@ -3,22 +3,34 @@ import { contextSrv } from 'app/core/core'; import { AccessControlAction, FolderDTO } from 'app/types'; function checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDTO) { - return folderDTO ? contextSrv.hasPermissionInMetadata(action, folderDTO) : contextSrv.hasPermission(action); + // Only some permissions are assigned in the root folder (aka "general" folder), so we can ignore them in most cases + return folderDTO && folderDTO.uid !== 'general' + ? contextSrv.hasPermissionInMetadata(action, folderDTO) + : contextSrv.hasPermission(action); } function checkCanCreateFolders(folderDTO?: FolderDTO) { + // Can only create a folder if we have permissions and either we're at root or nestedFolders is enabled if (folderDTO && !config.featureToggles.nestedFolders) { return false; } - return config.featureToggles.accessActionSets - ? checkFolderPermission(AccessControlAction.FoldersCreate, folderDTO) - : checkFolderPermission(AccessControlAction.FoldersCreate) && - checkFolderPermission(AccessControlAction.FoldersWrite, folderDTO); + if (!config.featureToggles.accessActionSets) { + if (!folderDTO || folderDTO.uid === 'general') { + return checkFolderPermission(AccessControlAction.FoldersCreate); + } + return ( + checkFolderPermission(AccessControlAction.FoldersCreate) && + checkFolderPermission(AccessControlAction.FoldersWrite, folderDTO) + ); + } + + return folderDTO + ? contextSrv.hasPermissionInMetadata(AccessControlAction.FoldersCreate, folderDTO) + : contextSrv.hasPermission(AccessControlAction.FoldersCreate); } export function getFolderPermissions(folderDTO?: FolderDTO) { - // Can only create a folder if we have permissions and either we're at root or nestedFolders is enabled const canCreateDashboards = checkFolderPermission(AccessControlAction.DashboardsCreate, folderDTO); const canCreateFolders = checkCanCreateFolders(folderDTO); const canDeleteFolders = checkFolderPermission(AccessControlAction.FoldersDelete, folderDTO); diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index f16daaddee1..34083445bd4 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -373,6 +373,8 @@ export class ElementState implements LayerElement { const scene = this.getScene(); const frames = scene?.data?.series; + this.options.links = this.options.links?.filter((link) => link !== null); + if (frames) { const defaultField = { name: 'Default field', diff --git a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx index 11a7859df4f..a10c8eae6b9 100644 --- a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx +++ b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx @@ -42,7 +42,7 @@ interface RendererProps extends EmbeddedDashboardProps { function EmbeddedDashboardRenderer({ model, initialState, onStateChange }: RendererProps) { const [isActive, setIsActive] = useState(false); - const { controls, body, scopes } = model.useState(); + const { controls, body } = model.useState(); const styles = useStyles2(getStyles); useEffect(() => { @@ -64,12 +64,9 @@ function EmbeddedDashboardRenderer({ model, initialState, onStateChange }: Rende } return ( -
- {scopes && } +
{controls && ( -
+
)} @@ -121,13 +118,6 @@ function getStyles(theme: GrafanaTheme2) { "panels"`, gridTemplateRows: 'auto 1fr', }), - canvasWithScopes: css({ - gridTemplateAreas: ` - "scopes controls" - "panels panels"`, - gridTemplateColumns: `${theme.spacing(32)} 1fr`, - gridTemplateRows: 'auto 1fr', - }), body: css({ label: 'body', flexGrow: 1, @@ -143,8 +133,5 @@ function getStyles(theme: GrafanaTheme2) { gridArea: 'controls', padding: theme.spacing(2, 0, 2, 2), }), - controlsWrapperWithScopes: css({ - padding: theme.spacing(2, 0), - }), }; } diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 0aebf3fe3d2..e7524d1ceeb 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -25,6 +25,7 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), setPluginExtensionGetter: jest.fn(), getPluginLinkExtensions: jest.fn(), + useChromeHeaderHeight: jest.fn().mockReturnValue(80), getBackendSrv: () => { return { get: jest.fn().mockResolvedValue({ dashboard: simpleDashboard, meta: { url: '' } }), diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 201773bd2f6..40cd0b07fc1 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -14,9 +14,9 @@ import { removeDashboardToFetchFromLocalStorage, } from 'app/features/dashboard/state/initDashboard'; import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking'; +import { getSelectedScopesNames } from 'app/features/scopes'; import { DashboardDTO, DashboardRoutes } from 'app/types'; -import { getScopesFromUrl } from '../../dashboard/utils/getScopesFromUrl'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel'; @@ -299,15 +299,13 @@ export class DashboardScenePageStateManager extends StateManagerBase) { const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState(); const { sourcePanel } = vizManager.useState(); const libraryPanel = getLibraryPanel(sourcePanel.resolve()); - const { controls, scopes } = dashboard.useState(); + const { controls } = dashboard.useState(); const styles = useStyles2(getStyles); const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = @@ -89,16 +89,9 @@ function VizAndDataPane({ model }: SceneComponentProps) { } return ( -
- {scopes && } +
{controls && ( -
+
)} @@ -163,13 +156,6 @@ function getStyles(theme: GrafanaTheme2) { "panels"`, gridTemplateRows: 'auto 1fr', }), - pageContainerWithScopes: css({ - gridTemplateAreas: ` - "scopes controls" - "panels panels"`, - gridTemplateColumns: `${theme.spacing(32)} 1fr`, - gridTemplateRows: 'auto 1fr', - }), container: css({ gridArea: 'panels', height: '100%', @@ -225,9 +211,6 @@ function getStyles(theme: GrafanaTheme2) { gridArea: 'controls', padding: theme.spacing(2, 0, 2, 2), }), - controlsWrapperWithScopes: css({ - padding: theme.spacing(2, 0), - }), openDataPaneButton: css({ width: theme.spacing(8), justifyContent: 'center', diff --git a/public/app/features/dashboard-scene/saving/shared.tsx b/public/app/features/dashboard-scene/saving/shared.tsx index d06ffb55463..23c7fc22e00 100644 --- a/public/app/features/dashboard-scene/saving/shared.tsx +++ b/public/app/features/dashboard-scene/saving/shared.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { selectors } from '@grafana/e2e-selectors'; -import { isFetchError } from '@grafana/runtime'; +import { config, isFetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Alert, Box, Button, Stack } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; import { Diffs } from '../settings/version-history/utils'; @@ -38,7 +39,21 @@ export interface NameAlreadyExistsErrorProps { } export function NameAlreadyExistsError({ cancelButton, saveButton }: NameAlreadyExistsErrorProps) { - return ( + const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore && config.featureToggles.dashboardRestoreUI; + return isRestoreDashboardsEnabled ? ( + +

+ + A dashboard with the same name in the selected folder already exists, including recently deleted dashboards. + +

+

+ + Please choose a different name or folder. + +

+
+ ) : (

A dashboard with the same name in selected folder already exists. Would you still like to save this dashboard? diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 3e992340079..5d83e8a244e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -1,4 +1,4 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import { GrafanaTheme2, VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -119,7 +119,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps +

{!hideVariableControls && variableControls.map((c) => )} @@ -159,18 +156,12 @@ function getStyles(theme: GrafanaTheme2) { flexDirection: 'row', flexWrap: 'nowrap', position: 'relative', - background: theme.colors.background.canvas, - zIndex: theme.zIndex.activePanel, width: '100%', marginLeft: 'auto', [theme.breakpoints.down('sm')]: { flexDirection: 'column-reverse', alignItems: 'stretch', }, - [theme.breakpoints.up('sm')]: { - position: 'sticky', - top: 0, - }, }), embedded: css({ background: 'unset', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 4f39bb8b104..8a63083655a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -34,6 +34,7 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { deleteDashboard } from 'app/features/manage-dashboards/state/actions'; +import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -74,7 +75,6 @@ import { DashboardSceneRenderer } from './DashboardSceneRenderer'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { LibraryVizPanel } from './LibraryVizPanel'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; -import { ScopesScene } from './Scopes/ScopesScene'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; @@ -123,10 +123,10 @@ export interface DashboardSceneState extends SceneObjectState { overlay?: SceneObject; /** The dashboard doesn't have panels */ isEmpty?: boolean; - /** Scene object that handles the scopes selector */ - scopes?: ScopesScene; /** Kiosk mode */ kioskMode?: KioskMode; + /** Share view */ + shareView?: string; } export class DashboardScene extends SceneObjectBase { @@ -163,6 +163,11 @@ export class DashboardScene extends SceneObjectBase { */ private _fromExplore = false; + /** + * A reference to the scopes facade + */ + private _scopesFacade: ScopesFacade | null; + public constructor(state: Partial) { super({ title: 'Dashboard', @@ -170,10 +175,11 @@ export class DashboardScene extends SceneObjectBase { editable: true, body: state.body ?? new SceneFlexLayout({ children: [] }), links: state.links ?? [], - scopes: state.uid && config.featureToggles.scopeFilters ? new ScopesScene() : undefined, ...state, }); + this._scopesFacade = getClosestScopesFacade(this); + this._changeTracker = new DashboardSceneChangeTracker(this); this.addActivationHandler(() => this._activationHandler()); @@ -229,6 +235,9 @@ export class DashboardScene extends SceneObjectBase { // Propagate change edit mode change to children this.propagateEditModeChange(); + // Propagate edit mode to scopes + this._scopesFacade?.enterReadOnly(); + this._changeTracker.startTrackingChanges(); }; @@ -274,6 +283,7 @@ export class DashboardScene extends SceneObjectBase { if (!this.state.isDirty || skipConfirm) { this.exitEditModeConfirmed(restoreInitialState || this.state.isDirty); + this._scopesFacade?.exitReadOnly(); return; } @@ -283,7 +293,10 @@ export class DashboardScene extends SceneObjectBase { text: `You have unsaved changes to this dashboard. Are you sure you want to discard them?`, icon: 'trash-alt', yesText: 'Discard', - onConfirm: this.exitEditModeConfirmed.bind(this), + onConfirm: () => { + this.exitEditModeConfirmed(); + this._scopesFacade?.exitReadOnly(); + }, }) ); } @@ -301,6 +314,7 @@ export class DashboardScene extends SceneObjectBase { editview: null, inspect: null, inspectTab: null, + shareView: null, }) ); @@ -401,7 +415,7 @@ export class DashboardScene extends SceneObjectBase { uid: this.state.uid, slug: meta.slug, currentQueryParams: location.search, - updateQuery: { viewPanel: null, inspect: null, editview: null, editPanel: null, tab: null }, + updateQuery: { viewPanel: null, inspect: null, editview: null, editPanel: null, tab: null, shareView: null }, isHomeDashboard: !meta.url && !meta.slug && !meta.isNew, }), }; @@ -846,13 +860,13 @@ export class DashboardScene extends SceneObjectBase { dashboardUID: this.state.uid, panelId, panelPluginId: panel?.state.pluginId, - scopes: this.state.scopes?.getSelectedScopes(), + scopes: this._scopesFacade?.value, }; } public enrichFiltersRequest(): Partial { return { - scopes: this.state.scopes?.getSelectedScopes(), + scopes: this._scopesFacade?.value, }; } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx index c8ee835f62b..881883dd441 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx @@ -1,15 +1,15 @@ -import { render, screen } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { screen } from '@testing-library/react'; +import { render } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; -import { locationService } from '@grafana/runtime'; -import { GrafanaContext } from 'app/core/context/GrafanaContext'; -import { configureStore } from 'app/store/configureStore'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), +})); + describe('DashboardSceneRenderer', () => { it('should render Not Found notice when dashboard is not found', async () => { const scene = transformSaveModelToScene({ @@ -46,18 +46,7 @@ describe('DashboardSceneRenderer', () => { }, }); - const store = configureStore({}); - const context = getGrafanaContextMock(); - - render( - - - - - - - - ); + render(); expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument(); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index a06650ad702..215d83f4dcc 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -1,12 +1,11 @@ import { css, cx } from '@emotion/css'; import { useLocation } from 'react-router-dom'; -import { useMedia } from 'react-use'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { config } from '@grafana/runtime'; +import { useChromeHeaderHeight } from '@grafana/runtime'; import { SceneComponentProps } from '@grafana/scenes'; -import { CustomScrollbar, useStyles2, useTheme2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; +import NativeScrollbar from 'app/core/components/NativeScrollbar'; import { Page } from 'app/core/components/Page/Page'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -17,9 +16,9 @@ import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { controls, overlay, editview, editPanel, isEmpty, scopes, meta } = model.useState(); - const { isExpanded: isScopesExpanded } = scopes?.useState() ?? {}; - const styles = useStyles2(getStyles); + const { controls, overlay, editview, editPanel, isEmpty, meta } = model.useState(); + const headerHeight = useChromeHeaderHeight(); + const styles = useStyles2(getStyles, headerHeight); const location = useLocation(); const navIndex = useSelector((state) => state.navIndex); const pageNav = model.getPageNav(location, navIndex); @@ -60,79 +59,43 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps {editPanel && } {!editPanel && ( -
- {scopes && !meta.dashboardNotFound && } - - {controls && ( -
- -
- )} - + +
+ + {controls && ( +
+ +
+ )}
{body}
- -
+
+ )} {overlay && } ); } -function getStyles(theme: GrafanaTheme2) { +function getStyles(theme: GrafanaTheme2, headerHeight: number | undefined) { return { - pageContainer: css( - { - display: 'grid', - gridTemplateAreas: ` + pageContainer: css({ + display: 'grid', + gridTemplateAreas: ` "panels"`, - gridTemplateColumns: `1fr`, - gridTemplateRows: '1fr', - height: '100%', - [theme.breakpoints.down('sm')]: { - display: 'flex', - flexDirection: 'column', - }, + gridTemplateColumns: `1fr`, + gridTemplateRows: '1fr', + flexGrow: 1, + [theme.breakpoints.down('sm')]: { + display: 'flex', + flexDirection: 'column', }, - config.featureToggles.bodyScrolling && { - position: 'absolute', - width: '100%', - } - ), + }), pageContainerWithControls: css({ gridTemplateAreas: ` "controls" "panels"`, gridTemplateRows: 'auto 1fr', }), - pageContainerWithScopes: css({ - gridTemplateAreas: ` - "scopes controls" - "panels panels"`, - gridTemplateColumns: `${theme.spacing(32)} 1fr`, - gridTemplateRows: 'auto 1fr', - }), - pageContainerWithScopesExpanded: css({ - gridTemplateAreas: ` - "scopes controls" - "scopes panels"`, - }), - panelsContainer: css({ - gridArea: 'panels', - }), controlsWrapper: css({ display: 'flex', flexDirection: 'column', @@ -142,9 +105,13 @@ function getStyles(theme: GrafanaTheme2) { ':empty': { display: 'none', }, - }), - controlsWrapperWithScopes: css({ - padding: theme.spacing(2, 2, 2, 0), + // Make controls sticky on larger screens (> mobile) + [theme.breakpoints.up('md')]: { + position: 'sticky', + zIndex: theme.zIndex.activePanel, + background: theme.colors.background.canvas, + top: headerHeight, + }, }), canvasContent: css({ label: 'canvas-content', @@ -152,7 +119,9 @@ function getStyles(theme: GrafanaTheme2) { flexDirection: 'column', padding: theme.spacing(0, 2), flexBasis: '100%', + gridArea: 'panels', flexGrow: 1, + minWidth: 0, }), body: css({ label: 'body', @@ -167,34 +136,3 @@ function getStyles(theme: GrafanaTheme2) { }), }; } - -interface PanelsContainerProps { - id: string; - children: React.ReactNode; - className?: string; - testId?: string; -} -/** - * Removes the scrollbar on mobile and uses a custom scrollbar on desktop - */ -const PanelsContainer = ({ id, children, className, testId }: PanelsContainerProps) => { - const theme = useTheme2(); - const isMobile = useMedia(`(max-width: ${theme.breakpoints.values.sm}px)`); - const styles = useStyles2(() => ({ - nonScrollable: css({ - height: '100%', - display: 'flex', - flexDirection: 'column', - }), - })); - - return isMobile ? ( -
- {children} -
- ) : ( - - {children} - - ); -}; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index fb891974cfe..0f0e291daf9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -16,6 +16,7 @@ import { KioskMode } from 'app/types'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { createDashboardEditViewFor } from '../settings/utils'; +import { ShareModal } from '../sharing/ShareModal'; import { findVizPanelByKey, getDashboardSceneFor, getLibraryPanel, isPanelClone } from '../utils/utils'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; @@ -29,7 +30,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { constructor(private _scene: DashboardScene) {} getKeys(): string[] { - return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'kiosk']; + return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'kiosk', 'shareView']; } getUrlState(): SceneObjectUrlValues { @@ -41,11 +42,12 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { editview: state.editview?.getUrlKey(), editPanel: state.editPanel?.getUrlKey() || undefined, kiosk: state.kioskMode === KioskMode.Full ? '' : state.kioskMode === KioskMode.TV ? 'tv' : undefined, + shareView: state.shareView, }; } updateFromUrl(values: SceneObjectUrlValues): void { - const { inspectPanelKey, viewPanelScene, isEditing, editPanel } = this._scene.state; + const { inspectPanelKey, viewPanelScene, isEditing, editPanel, shareView } = this._scene.state; const update: Partial = {}; if (typeof values.editview === 'string' && this._scene.canEditDashboard()) { @@ -153,6 +155,16 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { update.editPanel = undefined; } + if (typeof values.shareView === 'string') { + update.shareView = values.shareView; + update.overlay = new ShareModal({ + activeTab: values.shareView, + }); + } else if (shareView && values.shareView === null) { + update.overlay = undefined; + update.shareView = undefined; + } + if (this._scene.state.body instanceof SceneGridLayout) { const UNSAFE_fitPanels = typeof values.autofitpanels === 'string'; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 0d44671f1e6..4140343a9cc 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -27,7 +27,6 @@ import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor'; import ExportButton from '../sharing/ExportButton/ExportButton'; import ShareButton from '../sharing/ShareButton/ShareButton'; -import { ShareModal } from '../sharing/ShareModal'; import { DashboardInteractions } from '../utils/interactions'; import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; @@ -314,7 +313,7 @@ export function ToolbarActions({ dashboard }: Props) { fill="outline" onClick={() => { DashboardInteractions.toolbarShareClick(); - dashboard.showModal(new ShareModal({})); + locationService.partial({ shareView: 'link' }); }} data-testid={selectors.components.NavToolbar.shareDashboard} > diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index c3f2ced3c7e..9a736dc2936 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -13,6 +13,7 @@ import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } import { DataQuery, OptionsWithLegend } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { t } from 'app/core/internationalization'; +import { contextSrv } from 'app/core/services/context_srv'; import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { InspectTab } from 'app/features/inspector/types'; @@ -21,8 +22,10 @@ import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { ShareSnapshot } from '../sharing/ShareButton/share-snapshot/ShareSnapshot'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; +import { SharePanelEmbedTab } from '../sharing/SharePanelEmbedTab'; import { SharePanelInternally } from '../sharing/panel-share/SharePanelInternally'; import { DashboardInteractions } from '../utils/interactions'; import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; @@ -94,6 +97,35 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) { dashboard.showModal(drawer); }, }); + subMenu.push({ + text: t('share-panel.menu.share-embed-title', 'Share embed'), + iconClassName: 'arrow', + shortcut: 'p e', + onClick: () => { + const drawer = new ShareDrawer({ + title: t('share-panel.drawer.share-embed-title', 'Share embed'), + body: new SharePanelEmbedTab({ panelRef: panel.getRef() }), + }); + + dashboard.showModal(drawer); + }, + }); + + if (contextSrv.isSignedIn && config.snapshotEnabled && dashboard.canEditDashboard()) { + subMenu.push({ + text: t('share-panel.menu.share-snapshot-title', 'Share snapshot'), + iconClassName: 'camera', + shortcut: 'p s', + onClick: () => { + const drawer = new ShareDrawer({ + title: t('share-panel.drawer.share-snapshot-title', 'Share snapshot'), + body: new ShareSnapshot({ dashboardRef: dashboard.getRef(), panelRef: panel.getRef() }), + }); + + dashboard.showModal(drawer); + }, + }); + } items.push({ type: 'submenu', diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx deleted file mode 100644 index d9bc1cc88d5..00000000000 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { css } from '@emotion/css'; -import { Link } from 'react-router-dom'; - -import { GrafanaTheme2, Scope, urlUtil } from '@grafana/data'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { Button, CustomScrollbar, FilterInput, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; -import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { t, Trans } from 'app/core/internationalization'; - -import { fetchSuggestedDashboards } from './api'; -import { SuggestedDashboard } from './types'; - -export interface ScopesDashboardsSceneState extends SceneObjectState { - dashboards: SuggestedDashboard[]; - filteredDashboards: SuggestedDashboard[]; - isLoading: boolean; - scopesSelected: boolean; - searchQuery: string; -} - -export class ScopesDashboardsScene extends SceneObjectBase { - static Component = ScopesDashboardsSceneRenderer; - - constructor() { - super({ - dashboards: [], - filteredDashboards: [], - isLoading: false, - scopesSelected: false, - searchQuery: '', - }); - } - - public async fetchDashboards(scopes: Scope[]) { - if (scopes.length === 0) { - return this.setState({ dashboards: [], filteredDashboards: [], isLoading: false, scopesSelected: false }); - } - - this.setState({ isLoading: true }); - - const dashboards = await fetchSuggestedDashboards(scopes); - - this.setState({ - dashboards, - filteredDashboards: this.filterDashboards(dashboards, this.state.searchQuery), - isLoading: false, - scopesSelected: scopes.length > 0, - }); - } - - public changeSearchQuery(searchQuery: string) { - this.setState({ - filteredDashboards: searchQuery - ? this.filterDashboards(this.state.dashboards, searchQuery) - : this.state.dashboards, - searchQuery: searchQuery ?? '', - }); - } - - private filterDashboards(dashboards: SuggestedDashboard[], searchQuery: string): SuggestedDashboard[] { - const lowerCasedSearchQuery = searchQuery.toLowerCase(); - - return dashboards.filter(({ dashboardTitle }) => dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery)); - } -} - -export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps) { - const { dashboards, filteredDashboards, isLoading, searchQuery, scopesSelected } = model.useState(); - const styles = useStyles2(getStyles); - - const [queryParams] = useQueryParams(); - - if (!isLoading) { - if (!scopesSelected) { - return ( -

- No scopes selected -

- ); - } else if (dashboards.length === 0) { - return ( -

- - No dashboards found for the selected scopes - -

- ); - } - } - - return ( - <> -
- model.changeSearchQuery(value)} - /> -
- - {isLoading ? ( - - ) : filteredDashboards.length > 0 ? ( - - {filteredDashboards.map(({ dashboard, dashboardTitle }) => ( - - {dashboardTitle} - - ))} - - ) : ( -

- No results found for your query - - -

- )} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => { - return { - noResultsContainer: css({ - alignItems: 'center', - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(1), - justifyContent: 'center', - textAlign: 'center', - }), - searchInputContainer: css({ - flex: '0 1 auto', - }), - loadingIndicator: css({ - alignSelf: 'center', - }), - dashboardItem: css({ - padding: theme.spacing(1, 0), - borderBottom: `1px solid ${theme.colors.border.weak}`, - - '& :is(:first-child)': { - paddingTop: 0, - }, - }), - }; -}; diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx deleted file mode 100644 index e661f703950..00000000000 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { css, cx } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { IconButton, useStyles2 } from '@grafana/ui'; -import { t } from 'app/core/internationalization'; - -import { ScopesDashboardsScene } from './ScopesDashboardsScene'; -import { ScopesFiltersScene } from './ScopesFiltersScene'; - -export interface ScopesSceneState extends SceneObjectState { - dashboards: ScopesDashboardsScene; - filters: ScopesFiltersScene; - isExpanded: boolean; - isViewing: boolean; -} - -export class ScopesScene extends SceneObjectBase { - static Component = ScopesSceneRenderer; - - constructor() { - super({ - dashboards: new ScopesDashboardsScene(), - filters: new ScopesFiltersScene(), - isExpanded: false, - isViewing: false, - }); - - this.addActivationHandler(() => { - this._subs.add( - this.state.filters.subscribeToState((newState, prevState) => { - if (!newState.isLoadingScopes && newState.scopes !== prevState.scopes) { - if (this.state.isExpanded) { - this.state.dashboards.fetchDashboards(this.state.filters.getSelectedScopes()); - } - - sceneGraph.getTimeRange(this.parent!).onRefresh(); - } - }) - ); - - this._subs.add( - this.parent?.subscribeToState((newState) => { - const isEditing = 'isEditing' in newState ? !!newState.isEditing : false; - - if (isEditing !== this.state.isViewing) { - if (isEditing) { - this.enterViewMode(); - } else { - this.exitViewMode(); - } - } - }) - ); - }); - } - - public getSelectedScopes() { - return this.state.filters.getSelectedScopes(); - } - - public toggleIsExpanded() { - const isExpanded = !this.state.isExpanded; - - if (isExpanded) { - this.state.dashboards.fetchDashboards(this.getSelectedScopes()); - } - - this.setState({ isExpanded }); - } - - private enterViewMode() { - this.setState({ isExpanded: false, isViewing: true }); - - this.state.filters.enterViewMode(); - } - - private exitViewMode() { - this.setState({ isViewing: false }); - } -} - -export function ScopesSceneRenderer({ model }: SceneComponentProps) { - const { filters, dashboards, isExpanded, isViewing } = model.useState(); - const styles = useStyles2(getStyles); - - return ( -
-
- {!isViewing && ( - model.toggleIsExpanded()} - /> - )} - -
- - {isExpanded && !isViewing && ( -
- -
- )} -
- ); -} - -const getStyles = (theme: GrafanaTheme2) => { - return { - container: css({ - display: 'flex', - flexDirection: 'column', - gridArea: 'scopes', - }), - containerExpanded: css({ - backgroundColor: theme.colors.background.primary, - height: '100%', - }), - filtersContainer: css({ - display: 'flex', - flex: '0 1 auto', - flexDirection: 'row', - padding: theme.spacing(2, 2, 2, 2), - }), - filtersContainerExpanded: css({ - borderBottom: `1px solid ${theme.colors.border.weak}`, - padding: theme.spacing(2), - }), - iconNotExpanded: css({ - transform: 'scaleX(-1)', - }), - dashboardsContainer: css({ - display: 'flex', - flex: '1 1 auto', - flexDirection: 'column', - gap: theme.spacing(3), - overflow: 'hidden', - padding: theme.spacing(2), - }), - }; -}; diff --git a/public/app/features/dashboard-scene/scene/Scopes/utils.ts b/public/app/features/dashboard-scene/scene/Scopes/utils.ts deleted file mode 100644 index bbe68774966..00000000000 --- a/public/app/features/dashboard-scene/scene/Scopes/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Scope } from '@grafana/data'; - -export function getBasicScope(name: string): Scope { - return { - metadata: { name }, - spec: { - filters: [], - title: name, - type: '', - category: '', - description: '', - }, - }; -} - -export function mergeScopes(scope1: Scope, scope2: Scope): Scope { - return { - ...scope1, - metadata: { - ...scope1.metadata, - ...scope2.metadata, - }, - spec: { - ...scope1.spec, - ...scope2.spec, - }, - }; -} diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index eac3fb92a80..90d2baa7093 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -4,9 +4,12 @@ import { sceneGraph, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { t } from 'app/core/internationalization'; import { KeybindingSet } from 'app/core/services/KeybindingSet'; +import { contextSrv } from 'app/core/services/context_srv'; +import { ShareSnapshot } from '../sharing/ShareButton/share-snapshot/ShareSnapshot'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; +import { SharePanelEmbedTab } from '../sharing/SharePanelEmbedTab'; import { SharePanelInternally } from '../sharing/panel-share/SharePanelInternally'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; @@ -60,6 +63,31 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { scene.showModal(drawer); }), }); + keybindings.addBinding({ + key: 'p e', + onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => { + const drawer = new ShareDrawer({ + title: t('share-panel.drawer.share-embed-title', 'Share embed'), + body: new SharePanelEmbedTab({ panelRef: vizPanel.getRef() }), + }); + + scene.showModal(drawer); + }), + }); + + if (contextSrv.isSignedIn && config.snapshotEnabled && scene.canEditDashboard()) { + keybindings.addBinding({ + key: 'p s', + onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => { + const drawer = new ShareDrawer({ + title: t('share-panel.drawer.share-snapshot-title', 'Share snapshot'), + body: new ShareSnapshot({ dashboardRef: scene.getRef(), panelRef: vizPanel.getRef() }), + }); + + scene.showModal(drawer); + }), + }); + } } else { keybindings.addBinding({ key: 'p s', diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 5d45e4a4aa0..d03b55af774 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -28,8 +28,10 @@ import { UserActionEvent, GroupByVariable, AdHocFiltersVariable, + sceneGraph, } from '@grafana/scenes'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; +import { ScopesFacade } from 'app/features/scopes'; import { DashboardDTO, DashboardDataDTO } from 'app/types'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; @@ -281,6 +283,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), preserveDashboardSceneStateInLocalStorage, + new ScopesFacade({ + handler: (facade) => sceneGraph.getTimeRange(facade).onRefresh(), + }), ], $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), controls: new DashboardControls({ diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/CreateSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/CreateSnapshot.tsx index 73aaf435fde..85cf3b3e0c9 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/CreateSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/CreateSnapshot.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { Alert, Button, @@ -20,7 +21,10 @@ import { Trans } from 'app/core/internationalization'; import { SnapshotSharingOptions } from '../../../../dashboard/services/SnapshotSrv'; import { getExpireOptions } from '../../ShareSnapshotTab'; -const SNAPSHOT_URL = 'https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#publish-a-snapshot'; +const DASHBOARD_SNAPSHOT_URL = + 'https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#publish-a-snapshot'; +const PANEL_SNAPSHOT_URL = + 'https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#publish-a-snapshot-1'; interface Props { isLoading: boolean; @@ -31,6 +35,7 @@ interface Props { onCreateClick: (isExternal?: boolean) => void; onNameChange: (v: string) => void; onExpireChange: (v: number) => void; + panelRef?: SceneObjectRef; } export function CreateSnapshot({ name, @@ -41,6 +46,7 @@ export function CreateSnapshot({ onCancelClick, onCreateClick, isLoading, + panelRef, }: Props) { const styles = useStyles2(getStyles); @@ -49,18 +55,30 @@ export function CreateSnapshot({ - - A Grafana dashboard snapshot publicly shares a dashboard while removing sensitive data such as queries and - panel links, leaving only visible metrics and series names. Anyone with the link can access the snapshot. - + {panelRef ? ( + + A Grafana panel snapshot publicly shares a panel while removing sensitive data such as queries and panel + links, leaving only visible metrics and series names. Anyone with the link can access the snapshot. + + ) : ( + + A Grafana dashboard snapshot publicly shares a dashboard while removing sensitive data such as queries + and panel links, leaving only visible metrics and series names. Anyone with the link can access the + snapshot. + + )} - - - onNameChange(e.target.value)} /> + + onNameChange(e.currentTarget.value)} /> diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx index d225a75b1e3..60ba8c30992 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx @@ -23,7 +23,7 @@ function ShareSnapshotRenderer({ model }: SceneComponentProps) { const [showDeletedAlert, setShowDeletedAlert] = useState(false); const [step, setStep] = useState(1); - const { snapshotName, snapshotSharingOptions, selectedExpireOption, dashboardRef } = model.useState(); + const { snapshotName, snapshotSharingOptions, selectedExpireOption, dashboardRef, panelRef } = model.useState(); const [snapshotResult, createSnapshot] = useAsyncFn(async (external = false) => { const response = await model.onSnapshotCreate(external); @@ -76,6 +76,7 @@ function ShareSnapshotRenderer({ model }: SceneComponentProps) { onExpireChange={model.onExpireChange} onCreateClick={createSnapshot} isLoading={snapshotResult.loading} + panelRef={panelRef} /> )} diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 746caf6b1ce..6ec5e4138bb 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -111,7 +111,7 @@ function ShareExportTabRenderer({ model }: SceneComponentProps) <> {!isViewingJSON && ( <> -

+

Export this dashboard.

diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx index 65461f1d3bf..3a1ae740f1e 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx @@ -156,7 +156,7 @@ function ShareLinkTabRenderer({ model }: SceneComponentProps) { return ( <> -

+

Create a direct link to this dashboard or panel, customized with the options below. diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index 916eddc9dde..8a325d5d0b9 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -1,7 +1,7 @@ import { ComponentProps } from 'react'; -import { config } from '@grafana/runtime'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, SceneObjectRef } from '@grafana/scenes'; +import { config, locationService } from '@grafana/runtime'; +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { t } from 'app/core/internationalization'; @@ -45,10 +45,10 @@ export class ShareModal extends SceneObjectBase implements Moda ...state, }); - this.addActivationHandler(() => this.buildTabs()); + this.addActivationHandler(() => this.buildTabs(state.activeTab)); } - private buildTabs() { + private buildTabs(activeTab?: string) { const { panelRef } = this.state; const modalRef = this.getRef(); @@ -82,12 +82,13 @@ export class ShareModal extends SceneObjectBase implements Moda } } - this.setState({ tabs }); + const at = tabs.find((t) => t.tabId === activeTab); + + this.setState({ activeTab: at?.tabId ?? tabs[0].tabId, tabs }); } onDismiss = () => { - const dashboard = getDashboardSceneFor(this); - dashboard.closeModal(); + locationService.partial({ shareView: null }); }; onChangeTab: ComponentProps['onChangeTab'] = (tab) => { diff --git a/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx b/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx index 584d64868e4..23fe123d2e4 100644 --- a/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx +++ b/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx @@ -48,6 +48,7 @@ function SharePanelEmbedTabRenderer({ model }: SceneComponentProps dash.closeModal()} /> ); } diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index f584a8cfe6a..1e25452f58b 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -185,14 +185,14 @@ function ShareSnapshotTabRenderer({ model }: SceneComponentProps

-

+

A snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive data like queries (metric, template, and annotation) and panel links, leaving only the visible metric data and series names embedded in your dashboard.

-

+

Keep in mind, your snapshot can be viewed by anyone that has the link and can access the URL. Share wisely. diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 477495fda96..661a02379a8 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -10,10 +10,9 @@ import { import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; +import { getSelectedScopesNames } from 'app/features/scopes'; import { DashboardDTO, DashboardDataDTO, SaveDashboardResponseDTO } from 'app/types'; -import { getScopesFromUrl } from '../utils/getScopesFromUrl'; - export interface DashboardAPI { /** Get a dashboard with the access control metadata */ getDashboardDTO(uid: string): Promise; @@ -43,8 +42,7 @@ class LegacyDashboardAPI implements DashboardAPI { } getDashboardDTO(uid: string): Promise { - const scopesSearchParams = getScopesFromUrl(); - const scopes = scopesSearchParams?.getAll('scopes') ?? []; + const scopes = getSelectedScopesNames(); const queryParams = scopes.length > 0 ? { scopes } : undefined; return getBackendSrv().get(`/api/dashboards/uid/${uid}`, queryParams); diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx index de6e5d81606..2a5941c82e9 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx @@ -2,9 +2,10 @@ import { css } from '@emotion/css'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { FetchError } from '@grafana/runtime'; +import { config, FetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Button, ConfirmModal, Modal, useStyles2 } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; import { DashboardModel } from '../../state/DashboardModel'; @@ -30,7 +31,7 @@ export const SaveDashboardErrorProxy = ({ setErrorIsHandled, }: SaveDashboardErrorProxyProps) => { const { onDashboardSave } = useDashboardSave(); - + const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore && config.featureToggles.dashboardRestoreUI; return ( <> {error.data && error.data.status === 'version-mismatch' && ( @@ -51,22 +52,44 @@ export const SaveDashboardErrorProxy = ({ /> )} {error.data && error.data.status === 'name-exists' && ( - - A dashboard with the same name in selected folder already exists.
- Would you still like to save this dashboard? -

- } - confirmText="Save and overwrite" - onConfirm={async () => { - await onDashboardSave(dashboardSaveModel, { overwrite: true }, dashboard); - onDismiss(); - }} - onDismiss={onDismiss} - /> + <> + {isRestoreDashboardsEnabled ? ( + +

+ + A dashboard with the same name in the selected folder already exists, including recently deleted + dashboards. + +

+

+ + Please choose a different name or folder. + +

+
+ ) : ( + + A dashboard with the same name in selected folder already exists.
+ Would you still like to save this dashboard? +
+ } + confirmText="Save and overwrite" + onConfirm={async () => { + await onDashboardSave(dashboardSaveModel, { overwrite: true }, dashboard); + onDismiss(); + }} + onDismiss={onDismiss} + /> + )} + )} {error.data && error.data.status === 'plugin-dashboard' && ( { dashboard: { uid: string; time: RawTimeRange }; range?: TimeRange; buildIframe?: typeof buildIframeHtml; + onCancelClick?: () => void; } -export function ShareEmbed({ panel, dashboard, range, buildIframe = buildIframeHtml }: Props) { +export function ShareEmbed({ panel, dashboard, range, onCancelClick, buildIframe = buildIframeHtml }: Props) { const [useCurrentTimeRange, setUseCurrentTimeRange] = useState(true); const [selectedTheme, setSelectedTheme] = useState('current'); const [iframeHtml, setIframeHtml] = useState(''); @@ -44,21 +45,45 @@ export function ShareEmbed({ panel, dashboard, range, buildIframe = buildIframeH setSelectedTheme(value); }; - const isRelativeTime = dashboard.time.to === 'now'; - const timeRangeDescription = isRelativeTime - ? t( - 'share-modal.embed.time-range-description', - 'Transforms the current relative time range to an absolute time range' - ) - : ''; + const clipboardButton = ( + iframeHtml} + onClipboardCopy={() => { + DashboardInteractions.embedSnippetCopy({ + currentTimeRange: useCurrentTimeRange, + theme: selectedTheme, + shareResource: getTrackingSource(panel), + }); + }} + > + Copy to clipboard + + ); return ( <> -

+

Generate HTML for embedding an iframe with this panel.

- - + + + + + - - iframeHtml} - onClipboardCopy={() => { - DashboardInteractions.embedSnippetCopy({ - currentTimeRange: useCurrentTimeRange, - theme: selectedTheme, - shareResource: getTrackingSource(panel), - }); - }} - > - Copy to clipboard - - + {config.featureToggles.newDashboardSharingComponent ? ( + + {clipboardButton} + + + ) : ( + {clipboardButton} + )} ); } diff --git a/public/app/features/dashboard/components/ShareModal/ShareExport.tsx b/public/app/features/dashboard/components/ShareModal/ShareExport.tsx index aa942ce4877..46341ce195f 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareExport.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareExport.tsx @@ -101,7 +101,7 @@ export class ShareExport extends PureComponent { return ( <> -

+

Export this dashboard.

diff --git a/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx b/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx index 94aca5e30fc..1b4f57c6a0e 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx @@ -22,7 +22,7 @@ export const ShareLibraryPanel = ({ panel, initialFolderUid, onCreateLibraryPane return ( <> -

+

Create library panel.

{ return ( <> -

+

Create a direct link to this dashboard or panel, customized with the options below. diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index e88d1bebf84..662a0b29999 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -264,7 +264,6 @@ export function ConfigPublicDashboard({ publicDashboard, unsupportedDatasources onRevoke={() => { DashboardInteractions.revokePublicDashboardClicked(); showModal(DeletePublicDashboardModal, { - dashboardTitle: dashboard.title, onConfirm: () => onDeletePublicDashboardClick(hideModal), onDismiss: () => { showModal(ShareModal, { diff --git a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx index b6dd25275fb..d300289fe74 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx @@ -240,14 +240,14 @@ export class ShareSnapshot extends PureComponent { return ( <>

-

+

A snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive data like queries (metric, template, and annotation) and panel links, leaving only the visible metric data and series names embedded in your dashboard.

-

+

Keep in mind, your snapshot can be viewed by anyone that has the link and can access the URL. Share wisely. @@ -317,14 +317,12 @@ export class ShareSnapshot extends PureComponent { renderStep3() { return ( -

-

- - The snapshot has been deleted. If you have already accessed it once, then it might take up to an hour before - before it is removed from browser caches or CDN caches. - -

-
+

+ + The snapshot has been deleted. If you have already accessed it once, then it might take up to an hour before + before it is removed from browser caches or CDN caches. + +

); } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 27e356bb9fa..e4f56d47b29 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -1,8 +1,8 @@ -import { cx } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { NavModel, NavModelItem, TimeRange, PageLayoutType, locationUtil } from '@grafana/data'; +import { NavModel, NavModelItem, TimeRange, PageLayoutType, locationUtil, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; import { Themeable2, withTheme2 } from '@grafana/ui'; @@ -44,6 +44,9 @@ import { initDashboard } from '../state/initDashboard'; import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './types'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; + export const mapStateToProps = (state: StoreState) => ({ initPhase: state.dashboard.initPhase, initError: state.dashboard.initError, @@ -79,6 +82,40 @@ export interface State { sectionNav?: NavModel; } +const getStyles = (theme: GrafanaTheme2) => ({ + fullScreenPanel: css({ + '.react-grid-layout': { + height: 'auto !important', + transitionProperty: 'none', + }, + '.react-grid-item': { + display: 'none !important', + transitionProperty: 'none !important', + + '&--fullscreen': { + display: 'block !important', + // can't avoid type assertion here due to !important + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + position: 'unset !important' as 'unset', + transform: 'translate(0px, 0px) !important', + }, + }, + + // Disable grid interaction indicators in fullscreen panels + '.panel-header:hover': { + backgroundColor: 'inherit', + }, + + '.panel-title-container': { + cursor: 'pointer', + }, + + '.react-resizable-handle': { + display: 'none', + }, + }), +}); + export class UnthemedDashboardPage extends PureComponent { declare context: GrafanaContextType; static contextType = GrafanaContext; @@ -328,9 +365,10 @@ export class UnthemedDashboardPage extends PureComponent { }; render() { - const { dashboard, initError, queryParams } = this.props; + const { dashboard, initError, queryParams, theme } = this.props; const { editPanel, viewPanel, updateScrollTop, pageNav, sectionNav } = this.state; const kioskMode = getKioskMode(this.props.queryParams); + const styles = getStyles(theme); if (!dashboard || !pageNav || !sectionNav) { return ; @@ -342,7 +380,7 @@ export class UnthemedDashboardPage extends PureComponent { const showToolbar = kioskMode !== KioskMode.Full && !queryParams.editview; const pageClassName = cx({ - 'panel-in-fullscreen': Boolean(viewPanel), + [styles.fullScreenPanel]: Boolean(viewPanel), 'page-hidden': Boolean(queryParams.editview || editPanel), }); @@ -444,7 +482,9 @@ export class UnthemedDashboardPage extends PureComponent { /> {inspectPanel && } - {queryParams.shareView && } + {queryParams.shareView && ( + + )} {editPanel && ( ['scopes', String(scope)])); -} diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 2372df87670..a62a291e162 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -21,7 +21,7 @@ import { ExploreActions } from './ExploreActions'; import { ExploreDrawer } from './ExploreDrawer'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { QueriesDrawerContextProvider, useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; -import { AddToLibraryForm } from './QueryLibrary/AddToLibraryForm'; +import { QueryTemplateForm } from './QueryLibrary/QueryTemplateForm'; import RichHistoryContainer from './RichHistory/RichHistoryContainer'; import { useExplorePageTitle } from './hooks/useExplorePageTitle'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; @@ -132,11 +132,11 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa )} setQueryToAdd(undefined)} > - { setQueryToAdd(undefined); }} @@ -145,7 +145,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa setQueryToAdd(undefined); } }} - query={queryToAdd!} + queryToAdd={queryToAdd!} />
diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 5143293d345..ceeb7772aec 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -938,6 +938,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { rows={logRows} scrollElement={logsContainerRef.current} sortOrder={logsSortOrder} + app={CoreApp.Explore} > { return { navContainer: css` max-height: ${navContainerHeight}; + ${oldestLogsFirst ? 'width: 58px;' : ''} display: flex; flex-direction: column; ${config.featureToggles.logsInfiniteScrolling diff --git a/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx b/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx deleted file mode 100644 index 551f63edde2..00000000000 --- a/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useMemo } from 'react'; -import { useForm } from 'react-hook-form'; - -import { AppEvents, dateTime } from '@grafana/data'; -import { DataSourcePicker, getAppEvents } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema'; -import { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui'; -import { Field } from '@grafana/ui/'; -import { Input } from '@grafana/ui/src/components/Input/Input'; -import { Trans, t } from 'app/core/internationalization'; -import { getQueryDisplayText } from 'app/core/utils/richHistory'; -import { useAddQueryTemplateMutation } from 'app/features/query-library'; -import { AddQueryTemplateCommand } from 'app/features/query-library/types'; - -import { useDatasource } from '../QueryLibrary/utils/useDatasource'; - -type Props = { - onCancel: () => void; - onSave: (isSuccess: boolean) => void; - query: DataQuery; -}; - -export type QueryDetails = { - description: string; -}; - -const VisibilityOptions = [ - { value: 'Public', label: t('explore.query-library.public', 'Public') }, - { value: 'Private', label: t('explore.query-library.private', 'Private') }, -]; - -const info = t( - 'explore.add-to-library-modal.info', - `You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.` -); - -export const AddToLibraryForm = ({ onCancel, onSave, query }: Props) => { - const { register, handleSubmit } = useForm(); - - const [addQueryTemplate] = useAddQueryTemplateMutation(); - - const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => { - return addQueryTemplate(addQueryTemplateCommand) - .unwrap() - .then(() => { - getAppEvents().publish({ - type: AppEvents.alertSuccess.name, - payload: [ - t('explore.query-library.query-template-added', 'Query template successfully added to the library'), - ], - }); - return true; - }) - .catch(() => { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t('explore.query-library.query-template-error', 'Error attempting to add this query to the library'), - ], - }); - return false; - }); - }; - - const datasource = useDatasource(query.datasource); - - const displayText = useMemo(() => { - return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query); - }, [datasource, query]); - - const onSubmit = async (data: QueryDetails) => { - const timestamp = dateTime().toISOString(); - const temporaryDefaultTitle = - data.description || t('explore.query-library.default-description', 'Public', { timestamp: timestamp }); - handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [query] }).then((isSuccess) => { - onSave(isSuccess); - }); - }; - - return ( -
-

{info}

- - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx new file mode 100644 index 00000000000..ed654fa378d --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx @@ -0,0 +1,172 @@ +import { useForm } from 'react-hook-form'; +import { useAsync } from 'react-use'; + +import { AppEvents, dateTime } from '@grafana/data'; +import { DataSourcePicker, getAppEvents, getDataSourceSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; +import { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui'; +import { Field } from '@grafana/ui/'; +import { Input } from '@grafana/ui/src/components/Input/Input'; +import { Trans, t } from 'app/core/internationalization'; +import { getQueryDisplayText } from 'app/core/utils/richHistory'; +import { useAddQueryTemplateMutation, useEditQueryTemplateMutation } from 'app/features/query-library'; +import { AddQueryTemplateCommand, EditQueryTemplateCommand } from 'app/features/query-library/types'; + +import { useDatasource } from '../QueryLibrary/utils/useDatasource'; + +import { QueryTemplateRow } from './QueryTemplatesTable/types'; + +type Props = { + onCancel: () => void; + onSave: (isSuccess: boolean) => void; + queryToAdd?: DataQuery; + templateData?: QueryTemplateRow; +}; + +export type QueryDetails = { + description: string; +}; + +const VisibilityOptions = [ + { value: 'Public', label: t('explore.query-library.public', 'Public') }, + { value: 'Private', label: t('explore.query-library.private', 'Private') }, +]; + +const getInstuctions = (isAdd: boolean) => { + return isAdd + ? t( + 'explore.query-template-modal.add-info', + `You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.` + ) + : t( + 'explore.query-template-modal.edit-info', + `You're about to edit this query. Once saved, you can easily access it in the Query Library tab for future use and reference.` + ); +}; + +export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData }: Props) => { + const { register, handleSubmit } = useForm({ + defaultValues: { + description: templateData?.description, + }, + }); + + const [addQueryTemplate] = useAddQueryTemplateMutation(); + const [editQueryTemplate] = useEditQueryTemplateMutation(); + + const datasource = useDatasource(queryToAdd?.datasource); + + // this is an array to support multi query templates sometime in the future + const queries = + queryToAdd !== undefined ? [queryToAdd] : templateData?.query !== undefined ? [templateData?.query] : []; + + const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => { + return addQueryTemplate(addQueryTemplateCommand) + .unwrap() + .then(() => { + getAppEvents().publish({ + type: AppEvents.alertSuccess.name, + payload: [ + t('explore.query-library.query-template-added', 'Query template successfully added to the library'), + ], + }); + return true; + }) + .catch(() => { + getAppEvents().publish({ + type: AppEvents.alertError.name, + payload: [ + t('explore.query-library.query-template-add-error', 'Error attempting to add this query to the library'), + ], + }); + return false; + }); + }; + + const handleEditQueryTemplate = async (editQueryTemplateCommand: EditQueryTemplateCommand) => { + return editQueryTemplate(editQueryTemplateCommand) + .unwrap() + .then(() => { + getAppEvents().publish({ + type: AppEvents.alertSuccess.name, + payload: [t('explore.query-library.query-template-edited', 'Query template successfully edited')], + }); + return true; + }) + .catch(() => { + getAppEvents().publish({ + type: AppEvents.alertError.name, + payload: [t('explore.query-library.query-template-edit-error', 'Error attempting to edit this query')], + }); + return false; + }); + }; + + const onSubmit = async (data: QueryDetails) => { + const timestamp = dateTime().toISOString(); + const temporaryDefaultTitle = + data.description || t('explore.query-library.default-description', 'Public', { timestamp: timestamp }); + + if (templateData?.uid) { + handleEditQueryTemplate({ uid: templateData.uid, partialSpec: { title: data.description } }).then((isSuccess) => { + onSave(isSuccess); + }); + } else if (queryToAdd) { + handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [queryToAdd] }).then((isSuccess) => { + onSave(isSuccess); + }); + } + }; + + const { value: queryText } = useAsync(async () => { + const promises = queries.map(async (query, i) => { + const datasource = await getDataSourceSrv().get(query.datasource); + return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query); + }); + return Promise.all(promises); + }); + + return ( +
+

{getInstuctions(templateData === undefined)}

+ {queryText && + queryText.map((queryString, i) => ( + + + + ))} + {queryToAdd && ( + <> + + + + + + + + )} + + + + + + + + + + + + + ); +}; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx index 8e78f64c295..e3ae8d14ce6 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx @@ -1,6 +1,7 @@ +import { useState } from 'react'; + import { reportInteraction, getAppEvents } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema'; -import { IconButton } from '@grafana/ui'; +import { IconButton, Modal } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { t } from 'app/core/internationalization'; @@ -10,17 +11,20 @@ import { ShowConfirmModalEvent } from 'app/types/events'; import ExploreRunQueryButton from '../../ExploreRunQueryButton'; import { useQueriesDrawerContext } from '../../QueriesDrawer/QueriesDrawerContext'; +import { QueryTemplateForm } from '../QueryTemplateForm'; import { useQueryLibraryListStyles } from './styles'; +import { QueryTemplateRow } from './types'; interface ActionsCellProps { queryUid?: string; - query?: DataQuery; + queryTemplate: QueryTemplateRow; rootDatasourceUid?: string; } -function ActionsCell({ query, rootDatasourceUid, queryUid }: ActionsCellProps) { +function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCellProps) { const [deleteQueryTemplate] = useDeleteQueryTemplateMutation(); + const [editFormOpen, setEditFormOpen] = useState(false); const { setDrawerOpened } = useQueriesDrawerContext(); const styles = useQueryLibraryListStyles(); @@ -59,11 +63,34 @@ function ActionsCell({ query, rootDatasourceUid, queryUid }: ActionsCellProps) { } }} /> + { + setEditFormOpen(true); + }} + /> setDrawerOpened(false)} /> + setEditFormOpen(false)} + > + setEditFormOpen(false)} + templateData={queryTemplate} + onSave={() => { + setEditFormOpen(false); + }} + /> +
); } diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx index 376e7457dd8..c95398b0956 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx @@ -25,7 +25,7 @@ const columns: Array> = [ id: 'actions', header: '', cell: ({ row: { original } }) => ( - + ), }, ]; diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index 8bfcde6e6e8..611558c0b19 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -5,7 +5,7 @@ import { DataQuery } from '@grafana/schema'; import { Button, Modal } from '@grafana/ui'; import { isQueryLibraryEnabled } from 'app/features/query-library'; -import { AddToLibraryForm } from '../QueryLibrary/AddToLibraryForm'; +import { QueryTemplateForm } from '../QueryLibrary/QueryTemplateForm'; type Props = { query: DataQuery; @@ -23,13 +23,13 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => { {buttonLabel} setIsOpen(false)} > - setIsOpen(() => false)} - query={query} + queryToAdd={query} onSave={(isSuccess) => { if (isSuccess) { setIsOpen(false); diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx index 44845220f38..942a13a476b 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx @@ -109,17 +109,15 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => { // match against every tag filter return tags.every((tag: Tag) => { if (tag.key && tag.value) { - if (tag.operator === '=' && checkKeyValConditionForMatch(tag, span)) { - return getReturnValue(tag.operator, true); - } else if (tag.operator === '=~' && checkKeyValConditionForRegex(tag, span)) { - return getReturnValue(tag.operator, false); - } else if (tag.operator === '!=' && !checkKeyValConditionForMatch(tag, span)) { - return getReturnValue(tag.operator, false); - } else if (tag.operator === '!~' && !checkKeyValConditionForRegex(tag, span)) { - return getReturnValue(tag.operator, false); - } else { - return false; + if ( + (tag.operator === '=' && checkKeyValConditionForMatch(tag, span)) || + (tag.operator === '=~' && checkKeyValConditionForRegex(tag, span)) || + (tag.operator === '!=' && !checkKeyValConditionForMatch(tag, span)) || + (tag.operator === '!~' && !checkKeyValConditionForRegex(tag, span)) + ) { + return true; } + return false; } else if (tag.key) { if ( span.tags.some((kv) => checkKeyForMatch(tag.key!, kv.key)) || @@ -133,10 +131,11 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => { (span.traceState && tag.key === TRACE_STATE) || tag.key === ID ) { - return getReturnValue(tag.operator, true); + return tag.operator === '=' || tag.operator === '=~' ? true : false; } + return tag.operator === '=' || tag.operator === '=~' ? false : true; } - return getReturnValue(tag.operator, false); + return false; }); }); } @@ -184,30 +183,15 @@ const checkKeyValConditionForMatch = (tag: Tag, span: TraceSpan) => { }; const checkKeyForMatch = (tagKey: string, key: string) => { - return tagKey === key.toString() ? true : false; + return tagKey === key.toString(); }; const checkKeyAndValueForMatch = (tag: Tag, kv: TraceKeyValuePair) => { - return tag.key === kv.key.toString() && tag.value === kv.value.toString() ? true : false; + return tag.key === kv.key.toString() && tag.value === kv.value.toString(); }; const checkKeyAndValueForRegex = (tag: Tag, kv: TraceKeyValuePair) => { - return kv.key.toString().includes(tag.key || '') && kv.value.toString().includes(tag.value || '') ? true : false; -}; - -const getReturnValue = (operator: string, found: boolean) => { - switch (operator) { - case '=': - return found; - case '!=': - return !found; - case '~=': - return !found; - case '!~': - return !found; - default: - return !found; - } + return kv.key.toString().includes(tag.key || '') && kv.value.toString().includes(tag.value || ''); }; const getServiceNameMatches = (spans: TraceSpan[], searchProps: SearchProps) => { diff --git a/public/app/features/expressions/components/Condition.tsx b/public/app/features/expressions/components/Condition.tsx index d5118e1b768..0b5383921de 100644 --- a/public/app/features/expressions/components/Condition.tsx +++ b/public/app/features/expressions/components/Condition.tsx @@ -65,7 +65,7 @@ export const Condition = ({ condition, index, onChange, onRemoveCondition, refId }; const buttonWidth = css` - width: 60px; + width: 75px; `; const isRange = diff --git a/public/app/features/logs/components/InfiniteScroll.test.tsx b/public/app/features/logs/components/InfiniteScroll.test.tsx index e90e93eb45b..e5e7854dd72 100644 --- a/public/app/features/logs/components/InfiniteScroll.test.tsx +++ b/public/app/features/logs/components/InfiniteScroll.test.tsx @@ -1,7 +1,8 @@ import { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useEffect, useRef, useState } from 'react'; -import { LogRowModel, dateTimeForTimeZone } from '@grafana/data'; +import { CoreApp, LogRowModel, dateTimeForTimeZone } from '@grafana/data'; import { convertRawToRange } from '@grafana/data/src/datetime/rangeutil'; import { config } from '@grafana/runtime'; import { LogsSortOrder } from '@grafana/schema'; @@ -51,7 +52,13 @@ function ScrollWithWrapper({ children, ...props }: Props) { ); } -function setup(loadMoreMock: () => void, startPosition: number, rows: LogRowModel[], order: LogsSortOrder) { +function setup( + loadMoreMock: () => void, + startPosition: number, + rows: LogRowModel[], + order: LogsSortOrder, + app?: CoreApp +) { const { element, events } = getMockElement(startPosition); function scrollTo(position: number) { @@ -84,6 +91,7 @@ function setup(loadMoreMock: () => void, startPosition: number, rows: LogRowMode scrollElement={element as unknown as HTMLDivElement} loadMoreLogs={loadMoreMock} topScrollEnabled + app={app} >
@@ -267,6 +275,28 @@ describe('InfiniteScroll', () => { }); } ); + + describe('In Explore', () => { + test('Requests older logs from the oldest timestamp', async () => { + const loadMoreMock = jest.fn(); + const rows = createLogRows( + absoluteRange.from + 2 * SCROLLING_THRESHOLD, + absoluteRange.to - 2 * SCROLLING_THRESHOLD + ); + setup(loadMoreMock, 0, rows, LogsSortOrder.Ascending, CoreApp.Explore); + + expect(await screen.findByTestId('contents')).toBeInTheDocument(); + + await screen.findByText('Older logs'); + + await userEvent.click(screen.getByText('Older logs')); + + expect(loadMoreMock).toHaveBeenCalledWith({ + from: absoluteRange.from, + to: rows[0].timeEpochMs, + }); + }); + }); }); function createLogRows(from: number, to: number) { @@ -292,6 +322,7 @@ function getMockElement(scrollTop: number) { clientHeight: 40, scrollTop, scrollTo: jest.fn(), + scroll: jest.fn(), }; return { element, events }; diff --git a/public/app/features/logs/components/InfiniteScroll.tsx b/public/app/features/logs/components/InfiniteScroll.tsx index 65037b4a8b5..9572f0ca283 100644 --- a/public/app/features/logs/components/InfiniteScroll.tsx +++ b/public/app/features/logs/components/InfiniteScroll.tsx @@ -1,14 +1,17 @@ import { css } from '@emotion/css'; -import { ReactNode, useEffect, useRef, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; -import { AbsoluteTimeRange, LogRowModel, TimeRange } from '@grafana/data'; +import { AbsoluteTimeRange, CoreApp, LogRowModel, TimeRange } from '@grafana/data'; import { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/src/datetime/rangeutil'; import { config, reportInteraction } from '@grafana/runtime'; import { LogsSortOrder, TimeZone } from '@grafana/schema'; +import { Button, Icon } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { LoadingIndicator } from './LoadingIndicator'; export type Props = { + app?: CoreApp; children: ReactNode; loading: boolean; loadMoreLogs?: (range: AbsoluteTimeRange) => void; @@ -21,6 +24,7 @@ export type Props = { }; export const InfiniteScroll = ({ + app, children, loading, loadMoreLogs, @@ -135,10 +139,37 @@ export const InfiniteScroll = ({ const hideTopMessage = sortOrder === LogsSortOrder.Descending && isRelativeTime(range.raw.to); const hideBottomMessage = sortOrder === LogsSortOrder.Ascending && isRelativeTime(range.raw.to); + const loadOlderLogs = useCallback(() => { + //If we are not on the last page, use next page's range + reportInteraction('grafana_explore_logs_infinite_pagination_clicked', { + pageType: 'olderLogsButton', + }); + const newRange = canScrollTop(getVisibleRange(rows), range, timeZone, sortOrder); + if (!newRange) { + setUpperOutOfRange(true); + return; + } + setUpperOutOfRange(false); + loadMoreLogs?.(newRange); + setUpperLoading(true); + scrollElement?.scroll({ + behavior: 'auto', + top: 0, + }); + }, [loadMoreLogs, range, rows, scrollElement, sortOrder, timeZone]); + return ( <> {upperLoading && } {!hideTopMessage && upperOutOfRange && outOfRangeMessage} + {sortOrder === LogsSortOrder.Ascending && app === CoreApp.Explore && ( + + )} {children} {!hideBottomMessage && lowerOutOfRange && outOfRangeMessage} {lowerLoading && } @@ -151,6 +182,26 @@ const styles = { textAlign: 'center', padding: 0.25, }), + navButton: css({ + width: '58px', + height: '68px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + lineHeight: '1', + position: 'absolute', + top: 0, + right: -3, + zIndex: 1, + }), + navButtonContent: css({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + whiteSpace: 'normal', + }), }; const outOfRangeMessage = ( diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx index b91f8e212aa..4156c57ebab 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { config } from '@grafana/runtime'; import { Button, ModalsController, ButtonProps } from '@grafana/ui/src'; import { t } from 'app/core/internationalization'; import { useDeletePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; @@ -41,17 +42,15 @@ export const DeletePublicDashboardButton = ({ return ( {({ showModal, hideModal }) => { - const translatedRevocationButtonText = t( - 'public-dashboard-list.button.revoke-button-text', - 'Revoke public URL' - ); + const translatedRevocationButtonText = config.featureToggles.newDashboardSharingComponent + ? t('shared-dashboard-list.button.revoke-button-text', 'Revoke access') + : t('public-dashboard-list.button.revoke-button-text', 'Revoke public URL'); return ( @@ -152,12 +157,12 @@ export function InstallControlsButton({ return ( - {!plugin.isManaged && ( + {!plugin.isManaged && !plugin.isPreinstalled.withVersion && ( )} - diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx index 9d10b8a6728..0cbf52ca151 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, PluginType } from '@grafana/data'; import { config, featureEnabled } from '@grafana/runtime'; -import { Icon, LinkButton, Stack, useStyles2 } from '@grafana/ui'; +import { HorizontalGroup, LinkButton, useStyles2, Alert } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; @@ -24,67 +24,90 @@ export const InstallControlsWarning = ({ plugin, pluginStatus, latestCompatibleV const isCompatible = Boolean(latestCompatibleVersion); if (plugin.type === PluginType.renderer) { - return
Renderer plugins cannot be managed by the Plugin Catalog.
; + return ( + + ); } if (plugin.type === PluginType.secretsmanager) { - return
Secrets manager plugins cannot be managed by the Plugin Catalog.
; + return ( + + ); } if (plugin.isEnterprise && !featureEnabled('enterprise.plugins')) { return ( - - No valid Grafana Enterprise license detected. - - Learn more - - + + + No valid Grafana Enterprise license detected. + + Learn more + + + ); } if (plugin.isDev) { return ( -
This is a development build of the plugin and can't be uninstalled.
+ ); } if (!hasPermission && !isExternallyManaged) { - return
{statusToMessage(pluginStatus)}
; + return ; } if (!plugin.isPublished) { return ( -
- This plugin is not published to{' '} - - grafana.com/plugins - {' '} - and can't be managed via the catalog. -
+ +
+ This plugin is not published to{' '} + + grafana.com/plugins + {' '} + and can't be managed via the catalog. +
+
); } if (!isCompatible) { return ( -
- -  This plugin doesn't support your version of Grafana. -
+ ); } if (!isRemotePluginsAvailable) { return ( -
- The install controls have been disabled because the Grafana server cannot access grafana.com. -
+ ); } @@ -93,9 +116,9 @@ export const InstallControlsWarning = ({ plugin, pluginStatus, latestCompatibleV export const getStyles = (theme: GrafanaTheme2) => { return { - message: css` - color: ${theme.colors.text.secondary}; - `, + alert: css({ + marginTop: `${theme.spacing(2)}`, + }), }; }; diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index 1c31ffb7b15..a905dbe30ee 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -5,6 +5,7 @@ import { AppPlugin, GrafanaTheme2, PluginContextProvider, UrlQueryMap } from '@g import { config } from '@grafana/runtime'; import { CellProps, Column, InteractiveTable, Stack, useStyles2 } from '@grafana/ui'; +import { Changelog } from '../components/Changelog'; import { VersionList } from '../components/VersionList'; import { usePluginConfig } from '../hooks/usePluginConfig'; import { CatalogPlugin, Permission, PluginTabIds } from '../types'; @@ -60,6 +61,10 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E ); } + if (pageId === PluginTabIds.CHANGELOG && plugin?.details?.changelog) { + return ; + } + if (pageId === PluginTabIds.CONFIG && pluginConfig?.angularConfigCtrl) { return (
diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx index 375bb27171a..5314def26a6 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx @@ -1,13 +1,11 @@ import { css } from '@emotion/css'; import * as React from 'react'; -import { GrafanaTheme2, PluginSignatureStatus } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { PluginSignatureBadge, useStyles2 } from '@grafana/ui'; import { CatalogPlugin } from '../types'; -import { PluginSignatureDetailsBadge } from './PluginSignatureDetailsBadge'; - type Props = { plugin: CatalogPlugin; }; @@ -15,7 +13,6 @@ type Props = { // Designed to show plugin signature information in the header on the plugin's details page export function PluginDetailsHeaderSignature({ plugin }: Props): React.ReactElement { const styles = useStyles2(getStyles); - const isSignatureValid = plugin.signature === PluginSignatureStatus.valid; return (
@@ -25,12 +22,12 @@ export function PluginDetailsHeaderSignature({ plugin }: Props): React.ReactElem rel="noreferrer" className={styles.link} > - + - - {isSignatureValid && ( - - )}
); } diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index c2dbbea44f3..47d03e5c5fd 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -12,6 +12,7 @@ import { AngularDeprecationPluginNotice } from '../../angularDeprecation/Angular import { Loader } from '../components/Loader'; import { PluginDetailsBody } from '../components/PluginDetailsBody'; import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError'; +import { PluginDetailsRightPanel } from '../components/PluginDetailsRightPanel'; import { PluginDetailsSignature } from '../components/PluginDetailsSignature'; import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs'; import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions'; @@ -72,26 +73,31 @@ export function PluginDetailsPage({ ); } + const conditionalProps = !config.featureToggles.pluginsDetailsRightPanel ? { info: info } : {}; + return ( - - - - {plugin.angularDetected && ( - - )} - - - - - - + + + + + {plugin.angularDetected && ( + + )} + + + + + + + {config.featureToggles.pluginsDetailsRightPanel && } + ); } diff --git a/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx b/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx new file mode 100644 index 00000000000..61303e61e33 --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx @@ -0,0 +1,55 @@ +import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage'; +import { TextLink, Stack, Text } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; +import { formatDate } from 'app/core/internationalization/dates'; + +import { CatalogPlugin } from '../types'; + +type Props = { + info: PageInfoItem[]; + plugin: CatalogPlugin; +}; + +export function PluginDetailsRightPanel(props: Props): React.ReactElement | null { + const { info, plugin } = props; + + return ( + + {info.map((infoItem, index) => { + return ( + + {infoItem.label + ':'} +
{infoItem.value}
+
+ ); + })} + + {plugin.updatedAt && ( +
+ + Last updated: + {' '} + {formatDate(new Date(plugin.updatedAt))} +
+ )} + + {plugin?.details?.links && plugin.details?.links?.length > 0 && ( + + {plugin.details.links.map((link, index) => ( +
+ + {link.name} + +
+ ))} +
+ )} + + {!plugin?.isCore && ( + + Report Abuse + + )} +
+ ); +} diff --git a/public/app/features/plugins/admin/components/PluginList.test.tsx b/public/app/features/plugins/admin/components/PluginList.test.tsx deleted file mode 100644 index e3fd9c61eed..00000000000 --- a/public/app/features/plugins/admin/components/PluginList.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { render } from '@testing-library/react'; -import { useLocation } from 'react-router-dom'; - -import { PluginSignatureStatus } from '@grafana/data'; -import { config } from '@grafana/runtime'; - -import { CatalogPlugin, PluginListDisplayMode } from '../types'; - -import { PluginList } from './PluginList'; - -jest.mock('react-router-dom', () => ({ - useLocation: jest.fn(), -})); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - config: { - appSubUrl: '', - }, -})); - -const useLocationMock = useLocation as jest.Mock; - -const getMockPlugin = (id: string): CatalogPlugin => { - return { - description: 'The test plugin', - downloads: 5, - id, - info: { - logos: { - small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small', - large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large', - }, - keywords: ['test', 'plugin'], - }, - name: 'Testing Plugin', - orgName: 'Test', - popularity: 0, - signature: PluginSignatureStatus.valid, - publishedAt: '2020-09-01', - updatedAt: '2021-06-28', - hasUpdate: false, - isInstalled: false, - isCore: false, - isDev: false, - isEnterprise: false, - isDisabled: false, - isDeprecated: false, - isPublished: true, - isManaged: false, - }; -}; - -const plugins = [getMockPlugin('test1'), getMockPlugin('test2'), getMockPlugin('test3')]; -describe('PluginList', () => { - beforeAll(() => { - useLocationMock.mockImplementation(() => ({ - pathname: '/plugins', - })); - }); - - it('renders a plugin list', () => { - const result = render(); - expect(result.getByTestId('plugin-list')).toBeTruthy(); - const links = result.getAllByRole('link'); - for (const link of links) { - expect(link).toHaveAttribute('href', expect.stringMatching(/^\/plugins\/test\d/)); - } - }); - it('renders a plugin list with a subAppUrl', () => { - config.appSubUrl = 'test-sub-url'; - const result = render(); - expect(result.getByTestId('plugin-list')).toBeTruthy(); - const links = result.getAllByRole('link'); - for (const link of links) { - expect(link).toHaveAttribute('href', expect.stringMatching(/^test-sub-url\/plugins\/test\d/)); - } - }); -}); diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index a026047fe84..2650e3abee2 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -4,18 +4,16 @@ import { config } from '@grafana/runtime'; import { EmptyState, Grid } from '@grafana/ui'; import { t } from 'app/core/internationalization'; -import { CatalogPlugin, PluginListDisplayMode } from '../types'; +import { CatalogPlugin } from '../types'; import { PluginListItem } from './PluginListItem'; interface Props { plugins: CatalogPlugin[]; - displayMode: PluginListDisplayMode; isLoading?: boolean; } -export const PluginList = ({ plugins, displayMode, isLoading }: Props) => { - const isList = displayMode === PluginListDisplayMode.List; +export const PluginList = ({ plugins, isLoading }: Props) => { const { pathname } = useLocation(); const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname); @@ -24,12 +22,10 @@ export const PluginList = ({ plugins, displayMode, isLoading }: Props) => { } return ( - + {isLoading - ? new Array(50).fill(null).map((_, index) => ) - : plugins.map((plugin) => ( - - ))} + ? new Array(50).fill(null).map((_, index) => ) + : plugins.map((plugin) => )} ); }; diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx index 9a609f4be84..1f7a5bf3bb8 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/data'; -import { CatalogPlugin, PluginListDisplayMode } from '../types'; +import { CatalogPlugin } from '../types'; import { PluginListItem } from './PluginListItem'; @@ -58,6 +58,7 @@ describe('PluginListItem', () => { isDeprecated: false, isPublished: true, isManaged: false, + isPreinstalled: { found: false, withVersion: false }, }; /** As Grid */ @@ -102,47 +103,4 @@ describe('PluginListItem', () => { expect(screen.getByText(/disabled/i)).toBeVisible(); }); - - /** As List */ - it('renders a row with link, image, name, orgName and badges', () => { - render(); - - expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin'); - - const logo = screen.getByRole('presentation'); - expect(logo).toHaveAttribute('src', plugin.info.logos.small); - - expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible(); - expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible(); - expect(screen.getByText(/signed/i)).toBeVisible(); - expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument(); - }); - - it('renders a datasource plugin with correct icon', () => { - const datasourcePlugin = { ...plugin, type: PluginType.datasource }; - render(); - - expect(screen.getByTitle(/datasource plugin/i)).toBeInTheDocument(); - }); - - it('renders a panel plugin with correct icon', () => { - const panelPlugin = { ...plugin, type: PluginType.panel }; - render(); - - expect(screen.getByTitle(/panel plugin/i)).toBeInTheDocument(); - }); - - it('renders an app plugin with correct icon', () => { - const appPlugin = { ...plugin, type: PluginType.app }; - render(); - - expect(screen.getByTitle(/app plugin/i)).toBeInTheDocument(); - }); - - it('renders a disabled plugin with a badge to indicate its error', () => { - const pluginWithError = { ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature }; - render(); - - expect(screen.getByText(/disabled/i)).toBeVisible(); - }); }); diff --git a/public/app/features/plugins/admin/components/PluginListItem.tsx b/public/app/features/plugins/admin/components/PluginListItem.tsx index d54ea5aea3b..252ab24be13 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.tsx @@ -6,7 +6,7 @@ import { locationService, reportInteraction } from '@grafana/runtime'; import { Badge, Icon, Stack, useStyles2 } from '@grafana/ui'; import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable'; -import { CatalogPlugin, PluginIconName, PluginListDisplayMode } from '../types'; +import { CatalogPlugin, PluginIconName } from '../types'; import { PluginListItemBadges } from './PluginListItemBadges'; import { PluginLogo } from './PluginLogo'; @@ -16,12 +16,10 @@ export const LOGO_SIZE = '48px'; type Props = { plugin: CatalogPlugin; pathName: string; - displayMode?: PluginListDisplayMode; }; -function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDisplayMode.Grid }: Props) { +function PluginListItemComponent({ plugin, pathName }: Props) { const styles = useStyles2(getStyles); - const isList = displayMode === PluginListDisplayMode.List; const reportUserClickInteraction = () => { if (locationService.getSearchObject()?.q) { @@ -29,11 +27,7 @@ function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDis } }; return ( - +

{plugin.name}

@@ -47,15 +41,11 @@ function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDis ); } -const PluginListItemSkeleton: SkeletonComponent> = ({ - displayMode = PluginListDisplayMode.Grid, - rootProps, -}) => { +const PluginListItemSkeleton: SkeletonComponent = ({ rootProps }) => { const styles = useStyles2(getStyles); - const isList = displayMode === PluginListDisplayMode.List; return ( -
+
{ background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), }, }), - list: css({ - rowGap: 0, - - '> img': { - alignSelf: 'start', - }, - - '> .plugin-content': { - minHeight: 0, - gridArea: '2 / 2 / 4 / 3', - - '> p': { - margin: theme.spacing(0, 0, 0.5, 0), - }, - }, - - '> .plugin-name': { - alignSelf: 'center', - gridArea: '1 / 2 / 2 / 3', - }, - }), pluginType: css({ gridArea: '1 / 3 / 2 / 4', color: theme.colors.text.secondary, diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx index 251ab8df195..b6afda55066 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx @@ -34,6 +34,7 @@ describe('PluginListItemBadges', () => { isDeprecated: false, isPublished: true, isManaged: false, + isPreinstalled: { found: false, withVersion: false }, }; afterEach(() => { @@ -84,6 +85,20 @@ describe('PluginListItemBadges', () => { expect(screen.queryByText(/update available/i)).toBeNull(); }); + it('does not render an upgrade badge (when plugin is preinstalled with a version)', () => { + render( + + ); + expect(screen.queryByText(/update available/i)).toBeNull(); + }); + it('renders an angular badge (when plugin is angular)', () => { render(); expect(screen.getByText(/angular/i)).toBeVisible(); diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx index bc533a791a2..909f380db92 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx @@ -24,7 +24,9 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) { {plugin.isDisabled && } - {hasUpdate && !plugin.isManaged && } + {hasUpdate && !plugin.isManaged && !plugin.isPreinstalled.withVersion && ( + + )} {plugin.angularDetected && } ); @@ -36,7 +38,9 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) { {plugin.isDisabled && } {plugin.isDeprecated && } {plugin.isInstalled && } - {hasUpdate && !plugin.isManaged && } + {hasUpdate && !plugin.isManaged && !plugin.isPreinstalled.withVersion && ( + + )} {plugin.angularDetected && } ); diff --git a/public/app/features/plugins/admin/components/PluginSubtitle.tsx b/public/app/features/plugins/admin/components/PluginSubtitle.tsx index ee1f42c18e8..53e604bd110 100644 --- a/public/app/features/plugins/admin/components/PluginSubtitle.tsx +++ b/public/app/features/plugins/admin/components/PluginSubtitle.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { Fragment } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Alert, useStyles2 } from '@grafana/ui'; import { InstallControlsWarning } from '../components/InstallControls'; @@ -35,7 +36,7 @@ export const PluginSubtitle = ({ plugin }: Props) => { )} {plugin?.description &&
{plugin?.description}
} - {plugin?.details?.links && plugin.details.links.length > 0 && ( + {!config.featureToggles.pluginsDetailsRightPanel && !!plugin?.details?.links?.length && ( {plugin.details.links.map((link, index) => ( diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index dfadd646071..b2a80ec3ee0 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -204,6 +204,7 @@ describe('Plugins/Helpers', () => { isDeprecated: false, isPublished: true, isManaged: false, + isPreinstalled: { found: false, withVersion: false }, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2111, @@ -282,6 +283,7 @@ describe('Plugins/Helpers', () => { isPublished: false, isDeprecated: false, isManaged: false, + isPreinstalled: { found: false, withVersion: false }, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0, @@ -335,6 +337,7 @@ describe('Plugins/Helpers', () => { isPublished: true, isDeprecated: false, isManaged: false, + isPreinstalled: { found: false, withVersion: false }, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2111, diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 88416895c87..86d441763d6 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -143,6 +143,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C isInstalled: isDisabled, isDisabled: isDisabled, isManaged: isManagedPlugin(id), + isPreinstalled: isPreinstalledPlugin(id), isDeprecated: status === RemotePluginStatus.Deprecated, isCore: plugin.internal, isDev: false, @@ -193,6 +194,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat isDev: Boolean(dev), isEnterprise: false, isManaged: isManagedPlugin(id), + isPreinstalled: isPreinstalledPlugin(id), type, error: error?.errorCode, accessControl: accessControl, @@ -241,6 +243,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e isDeprecated: remote?.status === RemotePluginStatus.Deprecated, isPublished: true, isManaged: isManagedPlugin(id), + isPreinstalled: isPreinstalledPlugin(id), // TODO name: remote?.name || local?.name || '', // TODO @@ -382,6 +385,13 @@ export function isManagedPlugin(id: string) { return pluginCatalogManagedPlugins?.includes(id); } +export function isPreinstalledPlugin(id: string): { found: boolean; withVersion: boolean } { + const { pluginCatalogPreinstalledPlugins } = config; + + const plugin = pluginCatalogPreinstalledPlugins?.find((p) => p.id === id); + return { found: !!plugin?.id, withVersion: !!plugin?.version }; +} + function isDisabledSecretsPlugin(type?: PluginType): boolean { return type === PluginType.secretsmanager && !config.secretsManagerPluginEnabled; } diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 5ff258b5916..3173e5df198 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -35,6 +35,15 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, pageId?: PluginTabI active: PluginTabIds.VERSIONS === currentPageId, }); } + if (isPublished && plugin?.details?.changelog) { + navModelChildren.push({ + text: PluginTabLabels.CHANGELOG, + id: PluginTabIds.CHANGELOG, + icon: 'rocket', + url: `${pathname}?page=${PluginTabIds.CHANGELOG}`, + active: PluginTabIds.CHANGELOG === currentPageId, + }); + } // Not extending the tabs with the config pages if the plugin is not installed if (!pluginConfig) { diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx index e50b393e022..0b4fca761a5 100644 --- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, PluginSignatureType } from '@grafana/data'; +import { t } from 'app/core/internationalization'; import { PageInfoItem } from '../../../../core/components/Page/types'; import { PluginDisabledBadge } from '../components/Badges'; @@ -24,23 +25,23 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { version = latestCompatibleVersion?.version; } - if (Boolean(version)) { + if (version) { if (plugin.isManaged) { info.push({ - label: 'Version', + label: t('plugins.details.labels.version', 'Version'), value: 'Managed by Grafana', }); } else { info.push({ - label: 'Version', - value: version, + label: t('plugins.details.labels.version', 'Version'), + value: `${version}${plugin.isPreinstalled.withVersion ? ' (preinstalled)' : ''}`, }); } } if (Boolean(plugin.orgName)) { info.push({ - label: 'From', + label: t('plugins.details.labels.from', 'From'), value: plugin.orgName, }); } @@ -51,7 +52,7 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { plugin.signatureType === PluginSignatureType.commercial; if (showDownloads && Boolean(plugin.downloads > 0)) { info.push({ - label: 'Downloads', + label: t('plugins.details.labels.downloads', 'Downloads'), value: new Intl.NumberFormat().format(plugin.downloads), }); } @@ -65,20 +66,20 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { if (!hasNoDependencyInfo) { info.push({ - label: 'Dependencies', + label: t('plugins.details.labels.dependencies', 'Dependencies'), value: , }); } if (plugin.isDisabled) { info.push({ - label: 'Status', + label: t('plugins.details.labels.status', 'Status'), value: , }); } info.push({ - label: 'Signature', + label: t('plugins.details.labels.signature', 'Signature'), value: , }); diff --git a/public/app/features/plugins/admin/pages/Browse.test.tsx b/public/app/features/plugins/admin/pages/Browse.test.tsx index 3cb936fb7c4..48ac0897243 100644 --- a/public/app/features/plugins/admin/pages/Browse.test.tsx +++ b/public/app/features/plugins/admin/pages/Browse.test.tsx @@ -1,5 +1,4 @@ import { render, RenderResult, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { PluginType, escapeStringForRegex } from '@grafana/data'; @@ -406,39 +405,4 @@ describe('Browse list of plugins', () => { await waitFor(() => expect(getByRole('radio', { name: 'Installed' })).toBeDisabled()); }); }); - - it('should be possible to switch between display modes', async () => { - const { findByTestId, getByRole, getByTitle, queryByText } = renderBrowse('/plugins?filterBy=all', [ - getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1' }), - getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2' }), - getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3' }), - ]); - - await findByTestId('plugin-list'); - - const listOptionTitle = 'Display plugins in list'; - const gridOptionTitle = 'Display plugins in a grid layout'; - const listOption = getByRole('radio', { name: listOptionTitle }); - const listOptionLabel = getByTitle(listOptionTitle); - const gridOption = getByRole('radio', { name: gridOptionTitle }); - const gridOptionLabel = getByTitle(gridOptionTitle); - - // All options should be visible - expect(listOptionLabel).toBeVisible(); - expect(gridOptionLabel).toBeVisible(); - - // The default display mode should be "grid" - expect(gridOption).toBeChecked(); - expect(listOption).not.toBeChecked(); - - // Switch to "list" view - await userEvent.click(listOption); - expect(gridOption).not.toBeChecked(); - expect(listOption).toBeChecked(); - - // All plugins are still visible - expect(queryByText('Plugin 1')).toBeInTheDocument(); - expect(queryByText('Plugin 2')).toBeInTheDocument(); - expect(queryByText('Plugin 3')).toBeInTheDocument(); - }); }); diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 0e60bb60003..c72d702ab73 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -17,14 +17,12 @@ import { RoadmapLinks } from '../components/RoadmapLinks'; import { SearchField } from '../components/SearchField'; import { Sorters } from '../helpers'; import { useHistory } from '../hooks/useHistory'; -import { useGetAll, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks'; -import { PluginListDisplayMode } from '../types'; +import { useGetAll, useIsRemotePluginsAvailable } from '../state/hooks'; export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null { const location = useLocation(); const locationSearch = locationSearchToObject(location.search); const navModel = useSelector((state) => getNavModel(state.navIndex, 'plugins')); - const { displayMode, setDisplayMode } = useDisplayMode(); const styles = useStyles2(getStyles); const history = useHistory(); const remotePluginsAvailable = useIsRemotePluginsAvailable(); @@ -143,27 +141,10 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem ]} /> - - {/* Display mode */} - - - className={styles.displayAs} - value={displayMode} - onChange={setDisplayMode} - options={[ - { - value: PluginListDisplayMode.Grid, - icon: 'table', - description: 'Display plugins in a grid layout', - }, - { value: PluginListDisplayMode.List, icon: 'list-ul', description: 'Display plugins in list' }, - ]} - /> -
- +
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index e13a855336b..41303424649 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -215,7 +215,7 @@ describe('Plugin details page', () => { it('should display a "Signed" badge if the plugin signature is verified', async () => { const { queryByText } = renderPluginDetails({ id, signature: PluginSignatureStatus.valid }); - expect(await queryByText('Signed')).toBeInTheDocument(); + expect(await queryByText('community')).toBeInTheDocument(); }); it('should display a "Missing signature" badge if the plugin signature is missing', async () => { @@ -326,6 +326,24 @@ describe('Plugin details page', () => { expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); }); + it('should not display an update button for a plugin that is pre installed', async () => { + const { queryByRole, getByText } = renderPluginDetails({ + id, + isInstalled: true, + hasUpdate: true, + isPreinstalled: { found: true, withVersion: true }, + }); + + // Does not display an "update" button + expect(await queryByRole('button', { name: /update/i })).not.toBeInTheDocument(); + + // Does not display "install" button + expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); + + // Display an uninstall button but disabled + expect(getByText(/Uninstall/i).closest('button')).toBeDisabled(); + }); + it('should display an install button for enterprise plugins if license is valid', async () => { config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true }; @@ -880,4 +898,42 @@ describe('Plugin details page', () => { expect(queryByText('Add new data source')).toBeNull(); }); }); + + describe('Display plugin details right panel', () => { + beforeAll(() => { + mockUserPermissions({ + isAdmin: true, + isDataSourceEditor: false, + isOrgAdmin: true, + }); + config.featureToggles.pluginsDetailsRightPanel = true; + }); + + afterAll(() => { + config.featureToggles.pluginsDetailsRightPanel = false; + }); + + it('should display Last updated and report abuse information', async () => { + const id = 'right-panel-test-plugin'; + const updatedAt = '2023-10-26T16:54:55.000Z'; + const { queryByText } = renderPluginDetails({ id, updatedAt }); + expect(queryByText('Last updated:')).toBeVisible(); + expect(queryByText('10/26/2023')).toBeVisible(); + expect(queryByText('Report Abuse')).toBeVisible(); + }); + + it('should not display Last updated if there is no updated At data', async () => { + const id = 'right-panel-test-plugin'; + const updatedAt = undefined; + const { queryByText } = renderPluginDetails({ id, updatedAt }); + expect(queryByText('Last updated:')).toBeNull(); + }); + + it('should not display Report Abuse if the plugin is Core', async () => { + const id = 'right-panel-test-plugin'; + const isCore = true; + const { queryByText } = renderPluginDetails({ id, isCore }); + expect(queryByText('Report Abuse')).toBeNull(); + }); + }); }); diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index c50d9548348..a1cd84b3e16 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -4,17 +4,15 @@ import { PluginError, PluginType } from '@grafana/data'; import { useDispatch, useSelector } from 'app/types'; import { sortPlugins, Sorters } from '../helpers'; -import { CatalogPlugin, PluginListDisplayMode } from '../types'; +import { CatalogPlugin } from '../types'; import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions'; -import { setDisplayMode } from './reducer'; import { selectPlugins, selectById, selectIsRequestPending, selectRequestError, selectIsRequestNotFetched, - selectDisplayMode, selectPluginErrors, type PluginFilters, } from './selectors'; @@ -150,13 +148,3 @@ export const useFetchDetailsLazy = () => { return (id: string) => dispatch(fetchDetails(id)); }; - -export const useDisplayMode = () => { - const dispatch = useDispatch(); - const displayMode = useSelector(selectDisplayMode); - - return { - displayMode, - setDisplayMode: (v: PluginListDisplayMode) => dispatch(setDisplayMode(v)), - }; -}; diff --git a/public/app/features/plugins/admin/state/reducer.ts b/public/app/features/plugins/admin/state/reducer.ts index 2891856ea4a..f2414a31405 100644 --- a/public/app/features/plugins/admin/state/reducer.ts +++ b/public/app/features/plugins/admin/state/reducer.ts @@ -3,7 +3,7 @@ import { createSlice, createEntityAdapter, Reducer, AnyAction, PayloadAction } f import { PanelPlugin } from '@grafana/data'; import { STATE_PREFIX } from '../constants'; -import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types'; +import { CatalogPlugin, ReducerState, RequestStatus } from '../types'; import { fetchDetails, @@ -33,9 +33,7 @@ const getOriginalActionType = (type: string) => { export const initialState: ReducerState = { items: pluginsAdapter.getInitialState(), requests: {}, - settings: { - displayMode: PluginListDisplayMode.Grid, - }, + // Backwards compatibility // (we need to have the following fields in the store as well to be backwards compatible with other parts of Grafana) // TODO @@ -51,11 +49,7 @@ export const initialState: ReducerState = { const slice = createSlice({ name: 'plugins', initialState, - reducers: { - setDisplayMode(state, action: PayloadAction) { - state.settings.displayMode = action.payload; - }, - }, + reducers: {}, extraReducers: (builder) => builder .addCase(addPlugins, (state, action: PayloadAction) => { @@ -113,5 +107,4 @@ const slice = createSlice({ }), }); -export const { setDisplayMode } = slice.actions; export const reducer: Reducer = slice.reducer; diff --git a/public/app/features/plugins/admin/state/selectors.ts b/public/app/features/plugins/admin/state/selectors.ts index db0de51035c..8ffb65ce52c 100644 --- a/public/app/features/plugins/admin/state/selectors.ts +++ b/public/app/features/plugins/admin/state/selectors.ts @@ -12,8 +12,6 @@ export const selectRoot = (state: PluginCatalogStoreState) => state.plugins; export const selectItems = createSelector(selectRoot, ({ items }) => items); -export const selectDisplayMode = createSelector(selectRoot, ({ settings }) => settings.displayMode); - export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems); export type PluginFilters = { diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index aa6b41c7737..93517af5261 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -13,11 +13,6 @@ import { StoreState, PluginsState } from 'app/types'; export type PluginTypeCode = 'app' | 'panel' | 'datasource'; -export enum PluginListDisplayMode { - Grid = 'grid', - List = 'list', -} - export enum PluginAdminRoutes { Home = 'plugins-home', Browse = 'plugins-browse', @@ -45,6 +40,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata { isDisabled: boolean; isDeprecated: boolean; isManaged: boolean; // Indicates that the plugin version is managed by Grafana + isPreinstalled: { found: boolean; withVersion: boolean }; // Indicates that the plugin is pre-installed // `isPublished` is TRUE if the plugin is published to grafana.com isPublished: boolean; name: string; @@ -80,6 +76,7 @@ export interface CatalogPluginDetails { pluginDependencies?: PluginDependencies['plugins']; statusContext?: string; iam?: IdentityAccessManagement; + changelog?: string; } export interface CatalogPluginInfo { @@ -91,6 +88,7 @@ export interface CatalogPluginInfo { } export type RemotePlugin = { + changelog: string; createdAt: string; description: string; downloads: number; @@ -252,6 +250,7 @@ export enum PluginTabLabels { DASHBOARDS = 'Dashboards', USAGE = 'Usage', IAM = 'IAM', + CHANGELOG = 'Changelog', } export enum PluginTabIds { @@ -261,6 +260,7 @@ export enum PluginTabIds { DASHBOARDS = 'dashboards', USAGE = 'usage', IAM = 'iam', + CHANGELOG = 'changelog', } export enum RequestStatus { @@ -292,9 +292,6 @@ export type PluginDetailsTab = { export type ReducerState = PluginsState & { items: EntityState; requests: Record; - settings: { - displayMode: PluginListDisplayMode; - }; }; // TODO diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx index 51d3317496b..3038c8d7544 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx @@ -22,6 +22,7 @@ function createPluginExtensionRegistry(preloadResults: Array<{ pluginId: string; registry.register({ pluginId, extensionConfigs, + exposedComponentConfigs: [], }); } diff --git a/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts index b958016764a..b4f90414e0d 100644 --- a/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts +++ b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts @@ -39,6 +39,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry = await reactiveRegistry.getRegistry(); @@ -64,6 +65,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry1 = await reactiveRegistry.getRegistry(); @@ -83,6 +85,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry2 = await reactiveRegistry.getRegistry(); @@ -116,6 +119,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockImplementation((context) => ({ title: context?.title })), }, ], + exposedComponentConfigs: [], }); const registry = await reactiveRegistry.getRegistry(); @@ -168,6 +172,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry1 = await reactiveRegistry.getRegistry(); @@ -201,6 +206,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry2 = await reactiveRegistry.getRegistry(); @@ -251,6 +257,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry1 = await reactiveRegistry.getRegistry(); @@ -284,6 +291,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry2 = await reactiveRegistry.getRegistry(); @@ -335,6 +343,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); // Register extensions to a different extension point @@ -350,6 +359,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry2 = await reactiveRegistry.getRegistry(); @@ -399,6 +409,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); // Register extensions to a different extension point @@ -414,6 +425,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); const registry2 = await reactiveRegistry.getRegistry(); @@ -469,6 +481,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); expect(subscribeCallback).toHaveBeenCalledTimes(2); @@ -486,6 +499,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); expect(subscribeCallback).toHaveBeenCalledTimes(3); @@ -538,6 +552,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); observable.subscribe(subscribeCallback); @@ -581,6 +596,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); expect(consoleWarn).toHaveBeenCalled(); @@ -640,6 +656,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); expect(consoleWarn).toHaveBeenCalled(); @@ -669,6 +686,7 @@ describe('createPluginExtensionsRegistry', () => { configure: jest.fn().mockReturnValue({}), }, ], + exposedComponentConfigs: [], }); expect(consoleWarn).toHaveBeenCalled(); diff --git a/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts index 2ef23fe4b2e..d5d29cb22e1 100644 --- a/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts +++ b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { PluginPreloadResult } from '../pluginPreloader'; import { PluginExtensionRegistry, PluginExtensionRegistryItem } from './types'; -import { deepFreeze, isPluginCapability, logWarning } from './utils'; +import { deepFreeze, logWarning } from './utils'; import { isPluginExtensionConfigValid } from './validators'; export class ReactivePluginExtensionsRegistry { @@ -54,21 +54,6 @@ function resultsToRegistry(registry: PluginExtensionRegistry, result: PluginPrel for (const extensionConfig of extensionConfigs) { const { extensionPointId } = extensionConfig; - // Change the extension point id for capabilities - if (isPluginCapability(extensionConfig)) { - const regex = /capabilities\/([a-zA-Z0-9_.\-\/]+)$/; - const match = regex.exec(extensionPointId); - - if (!match) { - logWarning( - `"${pluginId}" plugin has an invalid capability ID: ${extensionPointId.replace('capabilities/', '')} (It must be a string)` - ); - continue; - } - - extensionConfig.extensionPointId = `capabilities/${match[1]}`; - } - // Check if the config is valid if (!extensionConfig || !isPluginExtensionConfigValid(pluginId, extensionConfig)) { return registry; @@ -81,12 +66,7 @@ function resultsToRegistry(registry: PluginExtensionRegistry, result: PluginPrel pluginId, }; - // Capability (only a single value per identifier, can be overriden) - if (isPluginCapability(extensionConfig)) { - registry.extensions[extensionPointId] = [registryItem]; - } - // Extension (multiple extensions per extension point identifier) - else if (!Array.isArray(registry.extensions[extensionPointId])) { + if (!Array.isArray(registry.extensions[extensionPointId])) { registry.extensions[extensionPointId] = [registryItem]; } else { registry.extensions[extensionPointId].push(registryItem); diff --git a/public/app/features/plugins/extensions/registry/ExportedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/ExportedComponentsRegistry.test.ts new file mode 100644 index 00000000000..64e18fe74ab --- /dev/null +++ b/public/app/features/plugins/extensions/registry/ExportedComponentsRegistry.test.ts @@ -0,0 +1,348 @@ +import React from 'react'; +import { firstValueFrom } from 'rxjs'; + +import { ExposedComponentsRegistry } from './ExposedComponentsRegistry'; + +describe('ExposedComponentsRegistry', () => { + const consoleWarn = jest.fn(); + + beforeEach(() => { + global.console.warn = consoleWarn; + consoleWarn.mockReset(); + }); + + it('should return empty registry when no exposed components have been registered', async () => { + const reactiveRegistry = new ExposedComponentsRegistry(); + const observable = reactiveRegistry.asObservable(); + const registry = await firstValueFrom(observable); + expect(registry).toEqual({}); + }); + + it('should be possible to register exposed components in the registry', async () => { + const pluginId = 'grafana-basic-app'; + const id = `${pluginId}/hello-world/v1`; + const reactiveRegistry = new ExposedComponentsRegistry(); + + reactiveRegistry.register({ + pluginId, + configs: [ + { + id, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World'), + }, + ], + }); + + const registry = await reactiveRegistry.getState(); + + expect(Object.keys(registry)).toHaveLength(1); + expect(registry[id]).toMatchObject({ + pluginId, + config: { + id, + title: 'not important', + description: 'not important', + }, + }); + }); + + it('should be possible to register multiple exposed components at one time', async () => { + const pluginId = 'grafana-basic-app'; + const id1 = `${pluginId}/hello-world1/v1`; + const id2 = `${pluginId}/hello-world2/v1`; + const id3 = `${pluginId}/hello-world3/v1`; + const reactiveRegistry = new ExposedComponentsRegistry(); + + reactiveRegistry.register({ + pluginId, + configs: [ + { + id: id1, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + { + id: id2, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World2'), + }, + { + id: id3, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World3'), + }, + ], + }); + + const registry = await reactiveRegistry.getState(); + + expect(Object.keys(registry)).toHaveLength(3); + expect(registry[id1]).toMatchObject({ config: { id: id1 }, pluginId }); + expect(registry[id2]).toMatchObject({ config: { id: id2 }, pluginId }); + expect(registry[id3]).toMatchObject({ config: { id: id3 }, pluginId }); + }); + + it('should be possible to register multiple exposed components from multiple plugins', async () => { + const pluginId1 = 'grafana-basic-app1'; + const pluginId2 = 'grafana-basic-app2'; + const id1 = `${pluginId1}/hello-world1/v1`; + const id2 = `${pluginId1}/hello-world2/v1`; + const id3 = `${pluginId2}/hello-world1/v1`; + const id4 = `${pluginId2}/hello-world2/v1`; + const reactiveRegistry = new ExposedComponentsRegistry(); + + reactiveRegistry.register({ + pluginId: pluginId1, + configs: [ + { + id: id1, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + { + id: id2, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World2'), + }, + ], + }); + + reactiveRegistry.register({ + pluginId: pluginId2, + configs: [ + { + id: id3, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World3'), + }, + { + id: id4, + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World4'), + }, + ], + }); + + const registry = await reactiveRegistry.getState(); + + expect(Object.keys(registry)).toHaveLength(4); + expect(registry[id1]).toMatchObject({ config: { id: id1 }, pluginId: pluginId1 }); + expect(registry[id2]).toMatchObject({ config: { id: id2 }, pluginId: pluginId1 }); + expect(registry[id3]).toMatchObject({ config: { id: id3 }, pluginId: pluginId2 }); + expect(registry[id4]).toMatchObject({ config: { id: id4 }, pluginId: pluginId2 }); + }); + + it('should notify subscribers when the registry changes', async () => { + const registry = new ExposedComponentsRegistry(); + const observable = registry.asObservable(); + const subscribeCallback = jest.fn(); + + observable.subscribe(subscribeCallback); + + // Register extensions for the first plugin + registry.register({ + pluginId: 'grafana-basic-app1', + configs: [ + { + id: 'grafana-basic-app1/hello-world/v1', + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(subscribeCallback).toHaveBeenCalledTimes(2); + + // Register exposed components for the second plugin + registry.register({ + pluginId: 'grafana-basic-app2', + configs: [ + { + id: 'grafana-basic-app2/hello-world/v1', + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(subscribeCallback).toHaveBeenCalledTimes(3); + + const mock = subscribeCallback.mock.calls[2][0]; + expect(mock).toHaveProperty('grafana-basic-app1/hello-world/v1'); + expect(mock).toHaveProperty('grafana-basic-app2/hello-world/v1'); + }); + + it('should give the last version of the registry for new subscribers', async () => { + const registry = new ExposedComponentsRegistry(); + const observable = registry.asObservable(); + const subscribeCallback = jest.fn(); + + // Register extensions for the first plugin + registry.register({ + pluginId: 'grafana-basic-app', + configs: [ + { + id: 'grafana-basic-app/hello-world/v1', + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const mock = subscribeCallback.mock.calls[0][0]; + + expect(mock['grafana-basic-app/hello-world/v1']).toMatchObject({ + pluginId: 'grafana-basic-app', + config: { + id: 'grafana-basic-app/hello-world/v1', + title: 'not important', + description: 'not important', + }, + }); + }); + + it('should log a warning if another component with the same id already exists in the registry', async () => { + const registry = new ExposedComponentsRegistry(); + registry.register({ + pluginId: 'grafana-basic-app1', + configs: [ + { + id: 'grafana-basic-app1/hello-world/v1', + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + const currentState1 = await registry.getState(); + expect(Object.keys(currentState1)).toHaveLength(1); + expect(currentState1['grafana-basic-app1/hello-world/v1']).toMatchObject({ + pluginId: 'grafana-basic-app1', + config: { + id: 'grafana-basic-app1/hello-world/v1', + }, + }); + + registry.register({ + pluginId: 'grafana-basic-app2', + configs: [ + { + id: 'grafana-basic-app1/hello-world/v1', // incorrectly scoped + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalledWith( + "[Plugin Extensions] Could not register exposed component with id 'grafana-basic-app1/hello-world/v1'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." + ); + const currentState2 = await registry.getState(); + expect(Object.keys(currentState2)).toHaveLength(1); + }); + + it('should skip registering component and log a warning when id is not prefixed with plugin id', async () => { + const registry = new ExposedComponentsRegistry(); + registry.register({ + pluginId: 'grafana-basic-app1', + configs: [ + { + id: 'hello-world/v1', + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalledWith( + "[Plugin Extensions] Could not register exposed component with id 'hello-world/v1'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." + ); + const currentState = await registry.getState(); + expect(Object.keys(currentState)).toHaveLength(0); + }); + + it('should log a warning when exposed component id is not suffixed with component version', async () => { + const registry = new ExposedComponentsRegistry(); + registry.register({ + pluginId: 'grafana-basic-app1', + configs: [ + { + id: 'grafana-basic-app1/hello-world', + title: 'not important', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalledWith( + "[Plugin Extensions] Exposed component with id 'grafana-basic-app1/hello-world' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'." + ); + const currentState = await registry.getState(); + expect(Object.keys(currentState)).toHaveLength(1); + }); + + it('should not register component when description is missing', async () => { + const registry = new ExposedComponentsRegistry(); + + registry.register({ + pluginId: 'grafana-basic-app', + configs: [ + { + id: 'grafana-basic-app/hello-world/v1', + title: 'not important', + description: '', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalledWith( + "[Plugin Extensions] Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Description is missing." + ); + + const currentState = await registry.getState(); + expect(Object.keys(currentState)).toHaveLength(0); + }); + + it('should not register component when title is missing', async () => { + const registry = new ExposedComponentsRegistry(); + + registry.register({ + pluginId: 'grafana-basic-app', + configs: [ + { + id: 'grafana-basic-app/hello-world/v1', + title: '', + description: 'not important', + component: () => React.createElement('div', null, 'Hello World1'), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalledWith( + "[Plugin Extensions] Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Title is missing." + ); + + const currentState = await registry.getState(); + expect(Object.keys(currentState)).toHaveLength(0); + }); +}); diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts new file mode 100644 index 00000000000..f0036742fab --- /dev/null +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts @@ -0,0 +1,60 @@ +import { PluginExposedComponentConfig } from '@grafana/data'; + +import { logWarning } from '../utils'; + +import { Registry, RegistryType, PluginExtensionConfigs } from './Registry'; + +export class ExposedComponentsRegistry extends Registry { + constructor(initialState: RegistryType = {}) { + super({ + initialState, + }); + } + + mapToRegistry( + registry: RegistryType, + { pluginId, configs }: PluginExtensionConfigs + ): RegistryType { + if (!configs) { + return registry; + } + + for (const config of configs) { + const { id, description, title } = config; + + if (!id.startsWith(pluginId)) { + logWarning( + `Could not register exposed component with id '${id}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'.` + ); + continue; + } + + if (!id.match(/.*\/v\d+$/)) { + logWarning( + `Exposed component with id '${id}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.` + ); + } + + if (registry[id]) { + logWarning( + `Could not register exposed component with id '${id}'. Reason: An exposed component with the same id already exists.` + ); + continue; + } + + if (!title) { + logWarning(`Could not register exposed component with id '${id}'. Reason: Title is missing.`); + continue; + } + + if (!description) { + logWarning(`Could not register exposed component with id '${id}'. Reason: Description is missing.`); + continue; + } + + registry[id] = { config, pluginId }; + } + + return registry; + } +} diff --git a/public/app/features/plugins/extensions/registry/Registry.ts b/public/app/features/plugins/extensions/registry/Registry.ts new file mode 100644 index 00000000000..fc3a8721be3 --- /dev/null +++ b/public/app/features/plugins/extensions/registry/Registry.ts @@ -0,0 +1,57 @@ +import { Observable, ReplaySubject, Subject, firstValueFrom, map, scan, startWith } from 'rxjs'; + +import { deepFreeze } from '../utils'; + +export type PluginExtensionConfigs = { + pluginId: string; + configs: T[]; +}; + +export type RegistryItem = { + pluginId: string; + config: T; +}; + +export type RegistryType = Record>; + +type ConstructorOptions = { + initialState: RegistryType; +}; + +// This is the base-class used by the separate specific registries. +export abstract class Registry { + private resultSubject: Subject>; + private registrySubject: ReplaySubject>; + + constructor(options: ConstructorOptions) { + const { initialState } = options; + this.resultSubject = new Subject>(); + // This is the subject that we expose. + // (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.) + this.registrySubject = new ReplaySubject>(1); + + this.resultSubject + .pipe( + scan(this.mapToRegistry, initialState), + // Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values) + startWith(initialState), + map((registry) => deepFreeze(registry)) + ) + // Emitting the new registry to `this.registrySubject` + .subscribe(this.registrySubject); + } + + abstract mapToRegistry(registry: RegistryType, item: PluginExtensionConfigs): RegistryType; + + register(result: PluginExtensionConfigs): void { + this.resultSubject.next(result); + } + + asObservable(): Observable> { + return this.registrySubject.asObservable(); + } + + getState(): Promise> { + return firstValueFrom(this.asObservable()); + } +} diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index cc1772ef3c3..46b81ad3325 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -1,9 +1,7 @@ import { act, render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; -import { PluginExtensionTypes } from '@grafana/data'; - -import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; +import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { createUsePluginComponent } from './usePluginComponent'; jest.mock('app/features/plugins/pluginSettings', () => ({ @@ -18,14 +16,14 @@ jest.mock('app/features/plugins/pluginSettings', () => ({ })); describe('usePluginComponent()', () => { - let reactiveRegistry: ReactivePluginExtensionsRegistry; + let registry: ExposedComponentsRegistry; beforeEach(() => { - reactiveRegistry = new ReactivePluginExtensionsRegistry(); + registry = new ExposedComponentsRegistry(); }); it('should return null if there are no component exposed for the id', () => { - const usePluginComponent = createUsePluginComponent(reactiveRegistry); + const usePluginComponent = createUsePluginComponent(registry); const { result } = renderHook(() => usePluginComponent('foo/bar')); expect(result.current.component).toEqual(null); @@ -33,23 +31,15 @@ describe('usePluginComponent()', () => { }); it('should return component, that can be rendered, from the registry', async () => { - const id = 'my-app-plugin/foo/bar'; + const id = 'my-app-plugin/foo/bar/v1'; const pluginId = 'my-app-plugin'; - reactiveRegistry.register({ + registry.register({ pluginId, - extensionConfigs: [ - { - extensionPointId: `capabilities/${id}`, - type: PluginExtensionTypes.component, - title: 'not important', - description: 'not important', - component: () =>
Hello World
, - }, - ], + configs: [{ id, title: 'not important', description: 'not important', component: () =>
Hello World
}], }); - const usePluginComponent = createUsePluginComponent(reactiveRegistry); + const usePluginComponent = createUsePluginComponent(registry); const { result } = renderHook(() => usePluginComponent(id)); const Component = result.current.component; @@ -63,9 +53,9 @@ describe('usePluginComponent()', () => { }); it('should dynamically update when component is registered to the registry', async () => { - const id = 'my-app-plugin/foo/bar'; + const id = 'my-app-plugin/foo/bar/v1'; const pluginId = 'my-app-plugin'; - const usePluginComponent = createUsePluginComponent(reactiveRegistry); + const usePluginComponent = createUsePluginComponent(registry); const { result, rerender } = renderHook(() => usePluginComponent(id)); // No extensions yet @@ -74,12 +64,11 @@ describe('usePluginComponent()', () => { // Add extensions to the registry act(() => { - reactiveRegistry.register({ + registry.register({ pluginId, - extensionConfigs: [ + configs: [ { - extensionPointId: `capabilities/${id}`, - type: PluginExtensionTypes.component, + id, title: 'not important', description: 'not important', component: () =>
Hello World
, @@ -103,9 +92,9 @@ describe('usePluginComponent()', () => { }); it('should only render the hook once', () => { - const spy = jest.spyOn(reactiveRegistry, 'asObservable'); + const spy = jest.spyOn(registry, 'asObservable'); const id = 'my-app-plugin/foo/bar'; - const usePluginComponent = createUsePluginComponent(reactiveRegistry); + const usePluginComponent = createUsePluginComponent(registry); renderHook(() => usePluginComponent(id)); expect(spy).toHaveBeenCalledTimes(1); diff --git a/public/app/features/plugins/extensions/usePluginComponent.tsx b/public/app/features/plugins/extensions/usePluginComponent.tsx index 99a63c8c921..df2689a66ce 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.tsx @@ -3,39 +3,30 @@ import { useObservable } from 'react-use'; import { UsePluginComponentResult } from '@grafana/runtime'; -import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; -import { isPluginExtensionComponentConfig, wrapWithPluginContext } from './utils'; +import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; +import { wrapWithPluginContext } from './utils'; // Returns a component exposed by a plugin. // (Exposed components can be defined in plugins by calling .exposeComponent() on the AppPlugin instance.) -export function createUsePluginComponent(extensionsRegistry: ReactivePluginExtensionsRegistry) { - const observableRegistry = extensionsRegistry.asObservable(); +export function createUsePluginComponent(registry: ExposedComponentsRegistry) { + const observableRegistry = registry.asObservable(); return function usePluginComponent(id: string): UsePluginComponentResult { const registry = useObservable(observableRegistry); return useMemo(() => { - if (!registry) { + if (!registry || !registry[id]) { return { isLoading: false, component: null, }; } - const registryId = `capabilities/${id}`; - const registryItems = registry.extensions[registryId]; - const registryItem = Array.isArray(registryItems) ? registryItems[0] : null; - - if (registryItem && isPluginExtensionComponentConfig(registryItem.config)) { - return { - isLoading: false, - component: wrapWithPluginContext(registryItem.pluginId, registryItem.config.component), - }; - } + const registryItem = registry[id]; return { isLoading: false, - component: null, + component: wrapWithPluginContext(registryItem.pluginId, registryItem.config.component), }; }, [id, registry]); }; diff --git a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx index 92c3d4c7f1c..f6259d2bfeb 100644 --- a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx +++ b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx @@ -46,6 +46,7 @@ describe('usePluginExtensions()', () => { path: `/a/${pluginId}/2`, }, ], + exposedComponentConfigs: [], }); const usePluginExtensions = createUsePluginExtensions(reactiveRegistry); @@ -85,6 +86,7 @@ describe('usePluginExtensions()', () => { path: `/a/${pluginId}/2`, }, ], + exposedComponentConfigs: [], }); }); @@ -130,6 +132,7 @@ describe('usePluginExtensions()', () => { path: `/a/${pluginId}/2`, }, ], + exposedComponentConfigs: [], }); }); @@ -165,6 +168,7 @@ describe('usePluginExtensions()', () => { path: `/a/${pluginId}/2`, }, ], + exposedComponentConfigs: [], }); }); @@ -193,6 +197,7 @@ describe('usePluginExtensions()', () => { path: `/a/${pluginId}/2`, }, ], + exposedComponentConfigs: [], }); }); @@ -213,6 +218,7 @@ describe('usePluginExtensions()', () => { path: `/a/${pluginId}/2`, }, ], + exposedComponentConfigs: [], }); }); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 54092186677..48d690f0a02 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -37,17 +37,6 @@ export function isPluginExtensionComponentConfig( return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.component; } -export function isPluginCapability( - extension: PluginExtensionConfig | undefined -): extension is PluginExtensionComponentConfig { - return ( - typeof extension === 'object' && - 'type' in extension && - extension['type'] === PluginExtensionTypes.component && - extension.extensionPointId.startsWith('capabilities/') - ); -} - export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') { return (...args: unknown[]) => { try { diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index 1a00a69716d..2fd705bdd4e 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -1,20 +1,23 @@ -import type { PluginExtensionConfig } from '@grafana/data'; +import type { PluginExposedComponentConfig, PluginExtensionConfig } from '@grafana/data'; import type { AppPluginConfig } from '@grafana/runtime'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { ReactivePluginExtensionsRegistry } from './extensions/reactivePluginExtensionRegistry'; +import { ExposedComponentsRegistry } from './extensions/registry/ExposedComponentsRegistry'; import * as pluginLoader from './plugin_loader'; export type PluginPreloadResult = { pluginId: string; error?: unknown; extensionConfigs: PluginExtensionConfig[]; + exposedComponentConfigs: PluginExposedComponentConfig[]; }; export async function preloadPlugins( apps: AppPluginConfig[] = [], registry: ReactivePluginExtensionsRegistry, + exposedComponentsRegistry: ExposedComponentsRegistry, eventName = 'frontend_plugins_preload' ) { startMeasure(eventName); @@ -22,7 +25,17 @@ export async function preloadPlugins( const preloadedPlugins = await Promise.all(promises); for (const preloadedPlugin of preloadedPlugins) { + if (preloadedPlugin.error) { + console.error(`[Plugins] Skip loading extensions for "${preloadedPlugin.pluginId}" due to an error.`); + continue; + } + registry.register(preloadedPlugin); + + exposedComponentsRegistry.register({ + pluginId: preloadedPlugin.pluginId, + configs: preloadedPlugin.exposedComponentConfigs, + }); } stopMeasure(eventName); @@ -38,16 +51,16 @@ async function preload(config: AppPluginConfig): Promise { isAngular: config.angular.detected, pluginId, }); - const { extensionConfigs = [] } = plugin; + const { extensionConfigs = [], exposedComponentConfigs = [] } = plugin; // Fetching meta-information for the preloaded app plugin and caching it for later. // (The function below returns a promise, but it's not awaited for a reason: we don't want to block the preload process, we would only like to cache the result for later.) getPluginSettings(pluginId); - return { pluginId, extensionConfigs }; + return { pluginId, extensionConfigs, exposedComponentConfigs }; } catch (error) { console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error); - return { pluginId, extensionConfigs: [], error }; + return { pluginId, extensionConfigs: [], error, exposedComponentConfigs: [] }; } finally { stopMeasure(`frontend_plugin_preload_${pluginId}`); } diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index 07e081df46e..6ffb106a0ec 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -60,6 +60,7 @@ class UserSessions extends PureComponent { +

+ )} +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + container: css({ + backgroundColor: theme.colors.background.primary, + borderRight: `1px solid ${theme.colors.border.weak}`, + display: 'flex', + flexDirection: 'column', + height: '100%', + gap: theme.spacing(1), + padding: theme.spacing(2), + width: theme.spacing(37.5), + }), + noResultsContainer: css({ + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + height: '100%', + justifyContent: 'center', + margin: 0, + textAlign: 'center', + }), + searchInputContainer: css({ + flex: '0 1 auto', + }), + loadingIndicator: css({ + alignSelf: 'center', + }), + dashboardItem: css({ + padding: theme.spacing(1, 0), + borderBottom: `1px solid ${theme.colors.border.weak}`, + + '& :is(:first-child)': { + paddingTop: 0, + }, + }), + }; +}; diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesInput.tsx b/public/app/features/scopes/internal/ScopesInput.tsx similarity index 80% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesInput.tsx rename to public/app/features/scopes/internal/ScopesInput.tsx index 9e4e3d0e9bc..e440d38741f 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesInput.tsx +++ b/public/app/features/scopes/internal/ScopesInput.tsx @@ -40,20 +40,21 @@ export function ScopesInput({ let titles: string[]; if (path.length > 0) { - titles = path - .map((nodeName) => { - const cl = currentLevel[nodeName]; - if (!cl) { - return null; - } + titles = path.reduce((acc, nodeName) => { + const cl = currentLevel[nodeName]; - const { title, nodes } = cl; + if (!cl) { + return acc; + } - currentLevel = nodes; + const { title, nodes } = cl; - return title; - }) - .filter((title) => title !== null) as string[]; + currentLevel = nodes; + + acc.push(title); + + return acc; + }, []); if (titles[0] === '') { titles.splice(0, 1); @@ -90,15 +91,16 @@ export function ScopesInput({ () => ( 0 && !isDisabled ? ( onRemoveAllClick()} /> @@ -127,8 +129,8 @@ const getStyles = (theme: GrafanaTheme2) => { return { scopePath: css({ color: theme.colors.text.primary, - fontSize: theme.typography.pxToRem(14), - margin: theme.spacing(1, 0), + fontSize: theme.typography.pxToRem(12), + margin: theme.spacing(0, 0), }), }; }; diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx b/public/app/features/scopes/internal/ScopesSelectorScene.tsx similarity index 63% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx rename to public/app/features/scopes/internal/ScopesSelectorScene.tsx index d4dffcfa466..e9e5f0166c8 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx +++ b/public/app/features/scopes/internal/ScopesSelectorScene.tsx @@ -2,66 +2,72 @@ import { css } from '@emotion/css'; import { isEqual } from 'lodash'; import { finalize, from, Subscription } from 'rxjs'; -import { GrafanaTheme2, Scope } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, - sceneGraph, SceneObjectBase, + SceneObjectRef, SceneObjectState, SceneObjectUrlSyncConfig, SceneObjectUrlValues, SceneObjectWithUrlSync, } from '@grafana/scenes'; -import { Button, Drawer, Spinner, useStyles2 } from '@grafana/ui'; +import { Button, Drawer, IconButton, Spinner, useStyles2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; +import { ScopesDashboardsScene } from './ScopesDashboardsScene'; import { ScopesInput } from './ScopesInput'; -import { ScopesScene } from './ScopesScene'; import { ScopesTree } from './ScopesTree'; import { fetchNodes, fetchScope, fetchSelectedScopes } from './api'; import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types'; -import { getBasicScope } from './utils'; +import { getBasicScope, getScopeNamesFromSelectedScopes, getTreeScopesFromSelectedScopes } from './utils'; -export interface ScopesFiltersSceneState extends SceneObjectState { +export interface ScopesSelectorSceneState extends SceneObjectState { + dashboards: SceneObjectRef | null; nodes: NodesMap; loadingNodeName: string | undefined; scopes: SelectedScope[]; treeScopes: TreeScope[]; + isReadOnly: boolean; isLoadingScopes: boolean; - isOpened: boolean; + isPickerOpened: boolean; + isEnabled: boolean; } -export class ScopesFiltersScene extends SceneObjectBase implements SceneObjectWithUrlSync { - static Component = ScopesFiltersSceneRenderer; +export const initialSelectorState: Omit = { + nodes: { + '': { + name: '', + reason: NodeReason.Result, + nodeType: 'container', + title: '', + isExpandable: true, + isSelectable: false, + isExpanded: true, + query: '', + nodes: {}, + }, + }, + loadingNodeName: undefined, + scopes: [], + treeScopes: [], + isReadOnly: false, + isLoadingScopes: false, + isPickerOpened: false, + isEnabled: false, +}; + +export class ScopesSelectorScene extends SceneObjectBase implements SceneObjectWithUrlSync { + static Component = ScopesSelectorSceneRenderer; protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] }); private nodesFetchingSub: Subscription | undefined; - get scopesParent(): ScopesScene { - return sceneGraph.getAncestor(this, ScopesScene); - } - constructor() { super({ - nodes: { - '': { - name: '', - reason: NodeReason.Result, - nodeType: 'container', - title: '', - isExpandable: true, - isSelectable: false, - isExpanded: true, - query: '', - nodes: {}, - }, - }, - loadingNodeName: undefined, - scopes: [], - treeScopes: [], - isLoadingScopes: false, - isOpened: false, + dashboards: null, + ...initialSelectorState, }); this.addActivationHandler(() => { @@ -75,7 +81,7 @@ export class ScopesFiltersScene extends SceneObjectBase public getUrlState() { return { - scopes: this.state.scopes.map(({ scope }) => scope.metadata.name), + scopes: this.state.isEnabled ? getScopeNamesFromSelectedScopes(this.state.scopes) : [], }; } @@ -177,8 +183,8 @@ export class ScopesFiltersScene extends SceneObjectBase } } - public open() { - if (!this.scopesParent.state.isViewing) { + public openPicker() { + if (!this.state.isReadOnly) { let nodes = { ...this.state.nodes }; // First close all nodes @@ -191,20 +197,16 @@ export class ScopesFiltersScene extends SceneObjectBase // Expand the nodes to the selected scope nodes = this.expandNodes(nodes, path); - this.setState({ isOpened: true, nodes }); + this.setState({ isPickerOpened: true, nodes }); } } - public close() { - this.setState({ isOpened: false }); - } - - public getSelectedScopes(): Scope[] { - return this.state.scopes.map(({ scope }) => scope); + public closePicker() { + this.setState({ isPickerOpened: false }); } public async updateScopes(treeScopes = this.state.treeScopes) { - if (isEqual(treeScopes, this.getTreeScopes())) { + if (isEqual(treeScopes, getTreeScopesFromSelectedScopes(this.state.scopes))) { return; } @@ -221,15 +223,27 @@ export class ScopesFiltersScene extends SceneObjectBase } public resetDirtyScopeNames() { - this.setState({ treeScopes: this.getTreeScopes() }); + this.setState({ treeScopes: getTreeScopesFromSelectedScopes(this.state.scopes) }); } public removeAllScopes() { this.setState({ scopes: [], treeScopes: [], isLoadingScopes: false }); } - public enterViewMode() { - this.setState({ isOpened: false }); + public enterReadOnly() { + this.setState({ isReadOnly: true, isPickerOpened: false }); + } + + public exitReadOnly() { + this.setState({ isReadOnly: false }); + } + + public enable() { + this.setState({ isEnabled: true }); + } + + public disable() { + this.setState({ isEnabled: false }); } private closeNodes(nodes: NodesMap): NodesMap { @@ -260,42 +274,68 @@ export class ScopesFiltersScene extends SceneObjectBase return nodes; } - - private getTreeScopes(): TreeScope[] { - return this.state.scopes.map(({ scope, path }) => ({ - scopeName: scope.metadata.name, - path, - })); - } } -export function ScopesFiltersSceneRenderer({ model }: SceneComponentProps) { +export function ScopesSelectorSceneRenderer({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); - const { nodes, loadingNodeName, treeScopes, isLoadingScopes, isOpened, scopes } = model.useState(); - const { isViewing } = model.scopesParent.useState(); + const { + dashboards: dashboardsRef, + nodes, + loadingNodeName, + scopes, + treeScopes, + isReadOnly, + isLoadingScopes, + isPickerOpened, + isEnabled, + } = model.useState(); + + const dashboards = dashboardsRef?.resolve(); + + const { isPanelOpened: isDashboardsPanelOpened } = dashboards?.useState() ?? {}; + + if (!isEnabled) { + return null; + } + + const dashboardsIconLabel = isReadOnly + ? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode') + : isDashboardsPanelOpened + ? t('scopes.dashboards.toggle.collapse', 'Collapse suggested dashboards list') + : t('scopes.dashboards.toggle..expand', 'Expand suggested dashboards list'); return ( - <> +
+ dashboards?.togglePanel()} + /> + model.open()} + onInputClick={() => model.openPicker()} onRemoveAllClick={() => model.removeAllScopes()} /> - {isOpened && ( + {isPickerOpened && ( { - model.close(); + model.closePicker(); model.resetDirtyScopeNames(); }} > {isLoadingScopes ? ( - + ) : (
)} - +
); } const getStyles = (theme: GrafanaTheme2) => { return { + container: css({ + borderLeft: `1px solid ${theme.colors.border.weak}`, + display: 'flex', + flexDirection: 'row', + paddingLeft: theme.spacing(2), + }), + dashboards: css({ + color: theme.colors.text.secondary, + marginRight: theme.spacing(2), + + '&:hover': css({ + color: theme.colors.text.primary, + }), + }), buttonGroup: css({ display: 'flex', gap: theme.spacing(1), diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx b/public/app/features/scopes/internal/ScopesTree.tsx similarity index 100% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx rename to public/app/features/scopes/internal/ScopesTree.tsx diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx b/public/app/features/scopes/internal/ScopesTreeHeadline.tsx similarity index 100% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx rename to public/app/features/scopes/internal/ScopesTreeHeadline.tsx diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx b/public/app/features/scopes/internal/ScopesTreeItem.tsx similarity index 100% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx rename to public/app/features/scopes/internal/ScopesTreeItem.tsx diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx b/public/app/features/scopes/internal/ScopesTreeLoading.tsx similarity index 100% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx rename to public/app/features/scopes/internal/ScopesTreeLoading.tsx diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx b/public/app/features/scopes/internal/ScopesTreeSearch.tsx similarity index 100% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx rename to public/app/features/scopes/internal/ScopesTreeSearch.tsx diff --git a/public/app/features/dashboard-scene/scene/Scopes/api.ts b/public/app/features/scopes/internal/api.ts similarity index 91% rename from public/app/features/dashboard-scene/scene/Scopes/api.ts rename to public/app/features/scopes/internal/api.ts index d1deda21d73..634de927253 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/api.ts +++ b/public/app/features/scopes/internal/api.ts @@ -87,10 +87,10 @@ export async function fetchSelectedScopes(treeScopes: TreeScope[]): Promise { +export async function fetchDashboards(scopeNames: string[]): Promise { try { const response = await getBackendSrv().get<{ items: ScopeDashboardBinding[] }>(dashboardsEndpoint, { - scope: scopes.map(({ metadata: { name } }) => name), + scope: scopeNames, }); return response?.items ?? []; @@ -99,8 +99,8 @@ export async function fetchDashboards(scopes: Scope[]): Promise { - const items = await fetchDashboards(scopes); +export async function fetchSuggestedDashboards(scopeNames: string[]): Promise { + const items = await fetchDashboards(scopeNames); return Object.values( items.reduce>((acc, item) => { diff --git a/public/app/features/scopes/internal/const.ts b/public/app/features/scopes/internal/const.ts new file mode 100644 index 00000000000..643a106960d --- /dev/null +++ b/public/app/features/scopes/internal/const.ts @@ -0,0 +1 @@ +export const DASHBOARDS_OPENED_KEY = 'grafana.scopes.dashboards.opened'; diff --git a/public/app/features/dashboard-scene/scene/Scopes/types.ts b/public/app/features/scopes/internal/types.ts similarity index 100% rename from public/app/features/dashboard-scene/scene/Scopes/types.ts rename to public/app/features/scopes/internal/types.ts diff --git a/public/app/features/scopes/internal/utils.ts b/public/app/features/scopes/internal/utils.ts new file mode 100644 index 00000000000..5052469ac48 --- /dev/null +++ b/public/app/features/scopes/internal/utils.ts @@ -0,0 +1,45 @@ +import { Scope } from '@grafana/data'; + +import { SelectedScope, TreeScope } from './types'; + +export function getBasicScope(name: string): Scope { + return { + metadata: { name }, + spec: { + filters: [], + title: name, + type: '', + category: '', + description: '', + }, + }; +} + +export function mergeScopes(scope1: Scope, scope2: Scope): Scope { + return { + ...scope1, + metadata: { + ...scope1.metadata, + ...scope2.metadata, + }, + spec: { + ...scope1.spec, + ...scope2.spec, + }, + }; +} + +export function getTreeScopesFromSelectedScopes(scopes: SelectedScope[]): TreeScope[] { + return scopes.map(({ scope, path }) => ({ + scopeName: scope.metadata.name, + path, + })); +} + +export function getScopesFromSelectedScopes(scopes: SelectedScope[]): Scope[] { + return scopes.map(({ scope }) => scope); +} + +export function getScopeNamesFromSelectedScopes(scopes: SelectedScope[]): string[] { + return scopes.map(({ scope }) => scope.metadata.name); +} diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx b/public/app/features/scopes/scopes.test.tsx similarity index 75% rename from public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx rename to public/app/features/scopes/scopes.test.tsx index 0f4fb4d188e..50e014c53a6 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx +++ b/public/app/features/scopes/scopes.test.tsx @@ -1,13 +1,13 @@ import { act, cleanup, waitFor } from '@testing-library/react'; import userEvents from '@testing-library/user-event'; -import { config, locationService } from '@grafana/runtime'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { config, locationService, setPluginImportUtils } from '@grafana/runtime'; import { sceneGraph } from '@grafana/scenes'; import { getDashboardAPI, setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; -import { ScopesFiltersScene } from './ScopesFiltersScene'; -import { ScopesScene } from './ScopesScene'; +import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from './instance'; import { buildTestScene, fetchNodesSpy, @@ -15,12 +15,8 @@ import { fetchSelectedScopesSpy, fetchSuggestedDashboardsSpy, getDashboard, - getDashboardsContainer, getDashboardsExpand, getDashboardsSearch, - getFiltersApply, - getFiltersCancel, - getFiltersInput, getMock, getNotFoundForFilter, getNotFoundForFilterClear, @@ -43,51 +39,71 @@ import { getResultClustersSlothClusterEastRadio, getResultClustersSlothClusterNorthRadio, getResultClustersSlothClusterSouthRadio, + getSelectorApply, + getSelectorCancel, + getSelectorInput, getTreeHeadline, getTreeSearch, mocksScopes, queryAllDashboard, queryDashboard, queryDashboardsContainer, - queryDashboardsExpand, queryDashboardsSearch, - queryFiltersApply, queryPersistedApplicationsSlothPictureFactoryTitle, queryPersistedApplicationsSlothVoteTrackerTitle, queryResultApplicationsClustersTitle, queryResultApplicationsSlothPictureFactoryTitle, queryResultApplicationsSlothVoteTrackerTitle, + querySelectorApply, renderDashboard, + resetScenes, } from './testUtils'; +import { getClosestScopesFacade } from './utils'; jest.mock('@grafana/runtime', () => ({ __esModule: true, ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), getBackendSrv: () => ({ get: getMock, }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), })); -describe('ScopesScene', () => { +const panelPlugin = getPanelPlugin({ + id: 'table', + skipDataQuery: true, +}); + +config.panels['table'] = panelPlugin.meta; + +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(panelPlugin), + getPanelPluginFromCache: (id: string) => undefined, +}); + +describe('Scopes', () => { describe('Feature flag off', () => { beforeAll(() => { config.featureToggles.scopeFilters = false; + config.featureToggles.groupByVariable = true; + + initializeScopes(); }); it('Does not initialize', () => { const dashboardScene = buildTestScene(); dashboardScene.activate(); - expect(dashboardScene.state.scopes).toBeUndefined(); + expect(scopesSelectorScene).toBeNull(); }); }); describe('Feature flag on', () => { let dashboardScene: DashboardScene; - let scopesScene: ScopesScene; - let filtersScene: ScopesFiltersScene; beforeAll(() => { config.featureToggles.scopeFilters = true; + config.featureToggles.groupByVariable = true; }); beforeEach(() => { @@ -99,27 +115,28 @@ describe('ScopesScene', () => { fetchSuggestedDashboardsSpy.mockClear(); getMock.mockClear(); + initializeScopes(); + dashboardScene = buildTestScene(); - scopesScene = dashboardScene.state.scopes!; - filtersScene = scopesScene.state.filters; renderDashboard(dashboardScene); }); afterEach(() => { + resetScenes(); cleanup(); }); describe('Tree', () => { it('Navigates through scopes nodes', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsClustersExpand()); await userEvents.click(getResultApplicationsExpand()); }); it('Fetches scope details on select', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1)); @@ -127,49 +144,49 @@ describe('ScopesScene', () => { it('Selects the proper scopes', async () => { await act(async () => - filtersScene.updateScopes([ + scopesSelectorScene?.updateScopes([ { scopeName: 'slothPictureFactory', path: [] }, { scopeName: 'slothVoteTracker', path: [] }, ]) ); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); expect(getResultApplicationsSlothVoteTrackerSelect()).toBeChecked(); expect(getResultApplicationsSlothPictureFactorySelect()).toBeChecked(); }); it('Can select scopes from same level', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.click(getResultApplicationsClustersSelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper'); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper'); }); it('Can select a node from an inner level', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); await userEvents.click(getResultApplicationsClustersExpand()); await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toBe('slothClusterNorth'); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toBe('slothClusterNorth'); }); it('Can select a node from an upper level', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultClustersSelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toBe('Cluster Index Helper'); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toBe('Cluster Index Helper'); }); it('Respects only one select per container', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultClustersExpand()); await userEvents.click(getResultClustersSlothClusterNorthRadio()); expect(getResultClustersSlothClusterNorthRadio().checked).toBe(true); @@ -180,7 +197,7 @@ describe('ScopesScene', () => { }); it('Search works', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.type(getTreeSearch(), 'Clusters'); await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); @@ -196,18 +213,18 @@ describe('ScopesScene', () => { }); it('Opens to a selected scope', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultClustersExpand()); - await userEvents.click(getFiltersApply()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorApply()); + await userEvents.click(getSelectorInput()); expect(queryResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument(); }); it('Persists a scope', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.type(getTreeSearch(), 'slothVoteTracker'); @@ -219,7 +236,7 @@ describe('ScopesScene', () => { }); it('Does not persist a retrieved scope', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.type(getTreeSearch(), 'slothPictureFactory'); @@ -229,7 +246,7 @@ describe('ScopesScene', () => { }); it('Removes persisted nodes', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.type(getTreeSearch(), 'slothVoteTracker'); @@ -243,7 +260,7 @@ describe('ScopesScene', () => { }); it('Persists nodes from search', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.type(getTreeSearch(), 'sloth'); await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); @@ -260,33 +277,33 @@ describe('ScopesScene', () => { }); it('Selects a persisted scope', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.type(getTreeSearch(), 'slothVoteTracker'); await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toBe('slothPictureFactory, slothVoteTracker'); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toBe('slothPictureFactory, slothVoteTracker'); }); it('Deselects a persisted scope', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); await userEvents.type(getTreeSearch(), 'slothVoteTracker'); await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toBe('slothPictureFactory, slothVoteTracker'); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toBe('slothPictureFactory, slothVoteTracker'); + await userEvents.click(getSelectorInput()); await userEvents.click(getPersistedApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toBe('slothVoteTracker'); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toBe('slothVoteTracker'); }); it('Shows the proper headline', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); expect(getTreeHeadline()).toHaveTextContent('Recommended'); await userEvents.type(getTreeSearch(), 'Applications'); await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(2)); @@ -297,90 +314,90 @@ describe('ScopesScene', () => { }); }); - describe('Filters', () => { + describe('Selector', () => { it('Opens', async () => { - await userEvents.click(getFiltersInput()); - expect(getFiltersApply()).toBeInTheDocument(); + await userEvents.click(getSelectorInput()); + expect(getSelectorApply()).toBeInTheDocument(); }); it('Fetches scope details on save', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultClustersSelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => expect(fetchSelectedScopesSpy).toHaveBeenCalled()); - expect(filtersScene.getSelectedScopes()).toEqual( + expect(getClosestScopesFacade(dashboardScene)?.value).toEqual( mocksScopes.filter(({ metadata: { name } }) => name === 'indexHelperCluster') ); }); it("Doesn't save the scopes on close", async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultClustersSelect()); - await userEvents.click(getFiltersCancel()); + await userEvents.click(getSelectorCancel()); await waitFor(() => expect(fetchSelectedScopesSpy).not.toHaveBeenCalled()); - expect(filtersScene.getSelectedScopes()).toEqual([]); + expect(getClosestScopesFacade(dashboardScene)?.value).toEqual([]); }); it('Shows selected scopes', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultClustersSelect()); - await userEvents.click(getFiltersApply()); - expect(getFiltersInput().value).toEqual('Cluster Index Helper'); + await userEvents.click(getSelectorApply()); + expect(getSelectorInput().value).toEqual('Cluster Index Helper'); }); }); describe('Dashboards list', () => { it('Toggles expanded state', async () => { await userEvents.click(getDashboardsExpand()); - expect(getDashboardsContainer()).toBeInTheDocument(); + expect(getNotFoundNoScopes()).toBeInTheDocument(); }); it('Does not fetch dashboards list when the list is not expanded', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => expect(fetchSuggestedDashboardsSpy).not.toHaveBeenCalled()); }); it('Fetches dashboards list when the list is expanded', async () => { await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled()); }); it('Fetches dashboards list when the list is expanded after scope selection', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await userEvents.click(getDashboardsExpand()); await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled()); }); it('Shows dashboards for multiple scopes', async () => { await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); expect(getDashboard('1')).toBeInTheDocument(); expect(getDashboard('2')).toBeInTheDocument(); expect(queryDashboard('3')).not.toBeInTheDocument(); expect(queryDashboard('4')).not.toBeInTheDocument(); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); expect(getDashboard('1')).toBeInTheDocument(); expect(getDashboard('2')).toBeInTheDocument(); expect(getDashboard('3')).toBeInTheDocument(); expect(getDashboard('4')).toBeInTheDocument(); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); expect(queryDashboard('1')).not.toBeInTheDocument(); expect(queryDashboard('2')).not.toBeInTheDocument(); expect(getDashboard('3')).toBeInTheDocument(); @@ -389,10 +406,10 @@ describe('ScopesScene', () => { it('Filters the dashboards list', async () => { await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); expect(getDashboard('1')).toBeInTheDocument(); expect(getDashboard('2')).toBeInTheDocument(); await userEvents.type(getDashboardsSearch(), '1'); @@ -401,12 +418,12 @@ describe('ScopesScene', () => { it('Deduplicates the dashboards list', async () => { await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsClustersExpand()); await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect()); await userEvents.click(getResultApplicationsClustersSlothClusterSouthSelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); expect(queryAllDashboard('5')).toHaveLength(1); expect(queryAllDashboard('6')).toHaveLength(1); expect(queryAllDashboard('7')).toHaveLength(1); @@ -421,20 +438,20 @@ describe('ScopesScene', () => { it('Does not show the input when there are no dashboards found for scope', async () => { await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultClustersExpand()); await userEvents.click(getResultClustersSlothClusterEastRadio()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); expect(getNotFoundForScope()).toBeInTheDocument(); expect(queryDashboardsSearch()).not.toBeInTheDocument(); }); it('Does show the input and a message when there are no dashboards found for filter', async () => { await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await userEvents.type(getDashboardsSearch(), 'unknown'); expect(queryDashboardsSearch()).toBeInTheDocument(); expect(getNotFoundForFilter()).toBeInTheDocument(); @@ -446,14 +463,14 @@ describe('ScopesScene', () => { describe('View mode', () => { it('Enters view mode', async () => { await act(async () => dashboardScene.onEnterEditMode()); - expect(scopesScene.state.isViewing).toEqual(true); - expect(scopesScene.state.isExpanded).toEqual(false); + expect(scopesSelectorScene?.state?.isReadOnly).toEqual(true); + expect(scopesDashboardsScene?.state?.isPanelOpened).toEqual(false); }); - it('Closes filters on enter', async () => { - await userEvents.click(getFiltersInput()); + it('Closes selector on enter', async () => { + await userEvents.click(getSelectorInput()); await act(async () => dashboardScene.onEnterEditMode()); - expect(queryFiltersApply()).not.toBeInTheDocument(); + expect(querySelectorApply()).not.toBeInTheDocument(); }); it('Closes dashboards list on enter', async () => { @@ -462,24 +479,24 @@ describe('ScopesScene', () => { expect(queryDashboardsContainer()).not.toBeInTheDocument(); }); - it('Does not open filters when view mode is active', async () => { + it('Does not open selector when view mode is active', async () => { await act(async () => dashboardScene.onEnterEditMode()); - await userEvents.click(getFiltersInput()); - expect(queryFiltersApply()).not.toBeInTheDocument(); + await userEvents.click(getSelectorInput()); + expect(querySelectorApply()).not.toBeInTheDocument(); }); - it('Hides the expand button when view mode is active', async () => { + it('Disables the expand button when view mode is active', async () => { await act(async () => dashboardScene.onEnterEditMode()); - expect(queryDashboardsExpand()).not.toBeInTheDocument(); + expect(getDashboardsExpand()).toBeDisabled(); }); }); describe('Enrichers', () => { it('Data requests', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => { const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!; expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual( @@ -487,9 +504,9 @@ describe('ScopesScene', () => { ); }); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => { const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!; expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual( @@ -499,9 +516,9 @@ describe('ScopesScene', () => { ); }); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => { const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!; expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual( @@ -511,19 +528,19 @@ describe('ScopesScene', () => { }); it('Filters requests', async () => { - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsExpand()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => { expect(dashboardScene.enrichFiltersRequest().scopes).toEqual( mocksScopes.filter(({ metadata: { name } }) => name === 'slothPictureFactory') ); }); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsSlothVoteTrackerSelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => { expect(dashboardScene.enrichFiltersRequest().scopes).toEqual( mocksScopes.filter( @@ -532,9 +549,9 @@ describe('ScopesScene', () => { ); }); - await userEvents.click(getFiltersInput()); + await userEvents.click(getSelectorInput()); await userEvents.click(getResultApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); + await userEvents.click(getSelectorApply()); await waitFor(() => { expect(dashboardScene.enrichFiltersRequest().scopes).toEqual( mocksScopes.filter(({ metadata: { name } }) => name === 'slothVoteTracker') @@ -556,17 +573,24 @@ describe('ScopesScene', () => { locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3'); }); - it('Legacy API should not pass the scopes', () => { - config.featureToggles.kubernetesDashboards = false; - getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', undefined); + afterEach(() => { + resetScenes(); + cleanup(); }); - it('K8s API should not pass the scopes', () => { + it('Legacy API should not pass the scopes', async () => { + config.featureToggles.kubernetesDashboards = false; + getDashboardAPI().getDashboardDTO('1'); + await waitFor(() => expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', undefined)); + }); + + it('K8s API should not pass the scopes', async () => { config.featureToggles.kubernetesDashboards = true; getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith( - '/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto' + await waitFor(() => + expect(getMock).toHaveBeenCalledWith( + '/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto' + ) ); }); }); @@ -580,19 +604,29 @@ describe('ScopesScene', () => { beforeEach(() => { setDashboardAPI(undefined); locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3'); + initializeScopes(); }); - it('Legacy API should pass the scopes', () => { + afterEach(() => { + resetScenes(); + cleanup(); + }); + + it('Legacy API should pass the scopes', async () => { config.featureToggles.kubernetesDashboards = false; getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', { scopes: ['scope1', 'scope2', 'scope3'] }); + await waitFor(() => + expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', { scopes: ['scope1', 'scope2', 'scope3'] }) + ); }); - it('K8s API should not pass the scopes', () => { + it('K8s API should not pass the scopes', async () => { config.featureToggles.kubernetesDashboards = true; getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith( - '/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto' + await waitFor(() => + expect(getMock).toHaveBeenCalledWith( + '/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto' + ) ); }); }); diff --git a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx b/public/app/features/scopes/testUtils.tsx similarity index 88% rename from public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx rename to public/app/features/scopes/testUtils.tsx index f83bf074dfb..1ab33d8e7ef 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx +++ b/public/app/features/scopes/testUtils.tsx @@ -1,11 +1,13 @@ import { screen } from '@testing-library/react'; -import { render } from 'test/test-utils'; +import { KBarProvider } from 'kbar'; +import { getWrapper, render } from 'test/test-utils'; import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data'; import { AdHocFiltersVariable, behaviors, GroupByVariable, + sceneGraph, SceneGridItem, SceneGridLayout, SceneQueryRunner, @@ -13,10 +15,18 @@ import { SceneVariableSet, VizPanel, } from '@grafana/scenes'; +import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; +import { AppChromeService } from 'app/core/components/AppChrome/AppChromeService'; import { DashboardControls } from 'app/features/dashboard-scene/scene//DashboardControls'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; +import { configureStore } from 'app/store/configureStore'; -import * as api from './api'; +import { ScopesFacade } from './ScopesFacadeScene'; +import { scopesDashboardsScene, scopesSelectorScene } from './instance'; +import { getInitialDashboardsState } from './internal/ScopesDashboardsScene'; +import { initialSelectorState } from './internal/ScopesSelectorScene'; +import * as api from './internal/api'; +import { DASHBOARDS_OPENED_KEY } from './internal/const'; export const mocksScopes: Scope[] = [ { @@ -321,12 +331,12 @@ const selectors = { expand: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-expand`, title: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-title`, }, - filters: { - input: 'scopes-filters-input', - container: 'scopes-filters-container', - loading: 'scopes-filters-loading', - apply: 'scopes-filters-apply', - cancel: 'scopes-filters-cancel', + selector: { + input: 'scopes-selector-input', + container: 'scopes-selector-container', + loading: 'scopes-selector-loading', + apply: 'scopes-selector-apply', + cancel: 'scopes-selector-cancel', }, dashboards: { expand: 'scopes-dashboards-expand', @@ -341,15 +351,13 @@ const selectors = { }, }; -export const getFiltersInput = () => screen.getByTestId(selectors.filters.input); -export const queryFiltersApply = () => screen.queryByTestId(selectors.filters.apply); -export const getFiltersApply = () => screen.getByTestId(selectors.filters.apply); -export const getFiltersCancel = () => screen.getByTestId(selectors.filters.cancel); +export const getSelectorInput = () => screen.getByTestId(selectors.selector.input); +export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply); +export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply); +export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel); -export const queryDashboardsExpand = () => screen.queryByTestId(selectors.dashboards.expand); export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand); export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); -export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search); export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search); export const queryAllDashboard = (uid: string) => screen.queryAllByTestId(selectors.dashboards.dashboard(uid)); @@ -416,7 +424,12 @@ export function buildTestScene(overrides: Partial = {}) { timeZone: 'browser', }), controls: new DashboardControls({}), - $behaviors: [new behaviors.CursorSync({})], + $behaviors: [ + new behaviors.CursorSync({}), + new ScopesFacade({ + handler: (facade) => sceneGraph.getTimeRange(facade).onRefresh(), + }), + ], $variables: new SceneVariableSet({ variables: [ new AdHocFiltersVariable({ @@ -451,5 +464,31 @@ export function buildTestScene(overrides: Partial = {}) { } export function renderDashboard(dashboardScene: DashboardScene) { - return render(); + const store = configureStore(); + const chrome = new AppChromeService(); + chrome.update({ chromeless: false }); + const Wrapper = getWrapper({ store, renderWithRouter: true, grafanaContext: { chrome } }); + + return render( + + + + + + + , + { + historyOptions: { + initialEntries: ['/'], + }, + } + ); +} + +export function resetScenes() { + scopesSelectorScene?.setState(initialSelectorState); + + localStorage.removeItem(DASHBOARDS_OPENED_KEY); + + scopesDashboardsScene?.setState(getInitialDashboardsState()); } diff --git a/public/app/features/scopes/useScopesDashboardsState.ts b/public/app/features/scopes/useScopesDashboardsState.ts new file mode 100644 index 00000000000..7fc2e7d385a --- /dev/null +++ b/public/app/features/scopes/useScopesDashboardsState.ts @@ -0,0 +1,5 @@ +import { scopesDashboardsScene } from './instance'; + +export const useScopesDashboardsState = () => { + return scopesDashboardsScene?.useState(); +}; diff --git a/public/app/features/scopes/utils.ts b/public/app/features/scopes/utils.ts new file mode 100644 index 00000000000..85d01626fa8 --- /dev/null +++ b/public/app/features/scopes/utils.ts @@ -0,0 +1,39 @@ +import { Scope } from '@grafana/data'; +import { sceneGraph, SceneObject } from '@grafana/scenes'; + +import { ScopesFacade } from './ScopesFacadeScene'; +import { scopesDashboardsScene, scopesSelectorScene } from './instance'; +import { getScopesFromSelectedScopes } from './internal/utils'; + +export function getSelectedScopes(): Scope[] { + return getScopesFromSelectedScopes(scopesSelectorScene?.state.scopes ?? []); +} + +export function getSelectedScopesNames(): string[] { + return getSelectedScopes().map((scope) => scope.metadata.name); +} + +export function enableScopes() { + scopesSelectorScene?.enable(); + scopesDashboardsScene?.enable(); +} + +export function disableScopes() { + scopesSelectorScene?.disable(); + scopesDashboardsScene?.disable(); +} + +export function exitScopesReadOnly() { + scopesSelectorScene?.exitReadOnly(); + scopesDashboardsScene?.enable(); +} + +export function enterScopesReadOnly() { + scopesSelectorScene?.enterReadOnly(); + scopesDashboardsScene?.disable(); +} + +export function getClosestScopesFacade(scene: SceneObject): ScopesFacade | null { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return sceneGraph.findObject(scene, (obj) => obj instanceof ScopesFacade) as ScopesFacade | null; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts index 822c388c925..94d2287e126 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts +++ b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts @@ -1,4 +1,4 @@ -import { getQueryGeneratorFor } from './query-generators'; +import { getQueryGeneratorFor } from './query-generators/getQueryGeneratorFor'; import { AutoQueryInfo } from './types'; export function getAutoQueriesForMetric(metric: string): AutoQueryInfo { diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts new file mode 100644 index 00000000000..d018b288a3a --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts @@ -0,0 +1,8 @@ +import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR } from 'app/features/trails/shared'; + +const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; +const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`; + +export function getGeneralBaseQuery(rate: boolean) { + return rate ? GENERAL_RATE_BASE_QUERY : GENERAL_BASE_QUERY; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts deleted file mode 100644 index 4257e212ec7..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { generateQueries } from './queries'; -import { getGeneratorParameters } from './rules'; - -function generator(metricParts: string[]) { - const params = getGeneratorParameters(metricParts); - return generateQueries(params); -} - -export default { generator }; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts deleted file mode 100644 index eb43d8537ce..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; -import { AutoQueryInfo } from '../../types'; -import { generateCommonAutoQueryInfo } from '../common/generator'; - -import { AutoQueryParameters } from './types'; - -const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; -const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`; - -export function getGeneralBaseQuery(rate: boolean) { - return rate ? GENERAL_RATE_BASE_QUERY : GENERAL_BASE_QUERY; -} - -const aggLabels: Record = { - avg: 'average', - sum: 'overall', -}; - -function getAggLabel(agg: string) { - return aggLabels[agg] || agg; -} - -export function generateQueries({ agg, rate, unit }: AutoQueryParameters): AutoQueryInfo { - const baseQuery = getGeneralBaseQuery(rate); - - const aggregationDescription = rate ? `${getAggLabel(agg)} per-second rate` : `${getAggLabel(agg)}`; - - const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; - - const mainQueryExpr = `${agg}(${baseQuery})`; - const breakdownQueryExpr = `${agg}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; - - return generateCommonAutoQueryInfo({ - description, - mainQueryExpr, - breakdownQueryExpr, - unit, - }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts deleted file mode 100644 index 96e0302089d..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getUnit, getPerSecondRateUnit } from '../../units'; - -import { AutoQueryParameters } from './types'; - -/** These suffixes will set rate to true */ -const RATE_SUFFIXES = new Set(['count', 'total']); - -const UNSUPPORTED_SUFFIXES = new Set(['sum', 'bucket']); - -/** Non-default aggregattion keyed by suffix */ -const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { - count: 'sum', - total: 'sum', -}; - -function checkPreviousForUnit(suffix: string) { - return suffix === 'total'; -} - -export function getGeneratorParameters(metricParts: string[]): AutoQueryParameters { - const suffix = metricParts.at(-1); - - if (suffix == null || UNSUPPORTED_SUFFIXES.has(suffix)) { - throw new Error(`This function does not support a metric suffix of "${suffix}"`); - } - - const rate = RATE_SUFFIXES.has(suffix); - const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; - - const unitSuffix = checkPreviousForUnit(suffix) ? metricParts.at(-2) : suffix; - - const unit = rate ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); - - return { - agg, - unit, - rate, - }; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts deleted file mode 100644 index cf49ce8e526..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type AutoQueryParameters = { - agg: string; - unit: string; - rate: boolean; -}; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts similarity index 93% rename from public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts rename to public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts index bb8650338ed..cddc48925ec 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts @@ -1,7 +1,8 @@ -import { VAR_GROUP_BY_EXP } from '../../../shared'; -import { AutoQueryDef, AutoQueryInfo } from '../../types'; +import { VAR_GROUP_BY_EXP } from '../../shared'; +import { AutoQueryDef, AutoQueryInfo } from '../types'; -import { generateQueries, getGeneralBaseQuery } from './queries'; +import { getGeneralBaseQuery } from './common/baseQuery'; +import { generateQueries } from './default'; describe('generateQueries', () => { const agg = 'sum'; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts new file mode 100644 index 00000000000..b81feb30d62 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts @@ -0,0 +1,82 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from 'app/features/trails/shared'; + +import { AutoQueryInfo } from '../types'; +import { getPerSecondRateUnit, getUnit } from '../units'; + +import { getGeneralBaseQuery } from './common/baseQuery'; +import { generateCommonAutoQueryInfo } from './common/generator'; + +/** These suffixes will set rate to true */ +const RATE_SUFFIXES = new Set(['count', 'total']); + +const UNSUPPORTED_SUFFIXES = new Set(['sum', 'bucket']); + +/** Non-default aggregation keyed by suffix */ +const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { + count: 'sum', + total: 'sum', +}; + +function shouldCheckPreviousSuffixForUnit(suffix: string) { + return suffix === 'total'; +} + +const aggLabels: Record = { + avg: 'average', + sum: 'overall', +}; + +function getAggLabel(agg: string) { + return aggLabels[agg] || agg; +} + +export type AutoQueryParameters = { + agg: string; + unit: string; + rate: boolean; +}; + +export function generateQueries({ agg, rate, unit }: AutoQueryParameters): AutoQueryInfo { + const baseQuery = getGeneralBaseQuery(rate); + + const aggregationDescription = rate ? `${getAggLabel(agg)} per-second rate` : `${getAggLabel(agg)}`; + + const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; + + const mainQueryExpr = `${agg}(${baseQuery})`; + const breakdownQueryExpr = `${agg}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; + + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); +} + +export function createDefaultMetricQueryDefs(metricParts: string[]) { + // Get the last part of the metric name + const suffix = metricParts.at(-1); + + // If the suffix is null or is in the set of unsupported suffixes, throw an error because the metric should be delegated to a different generator (summary or histogram) + if (suffix == null || UNSUPPORTED_SUFFIXES.has(suffix)) { + throw new Error(`This function does not support a metric suffix of "${suffix}"`); + } + + // Check if generating rate query and/or aggregation query + const rate = RATE_SUFFIXES.has(suffix); + const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; + + // Try to find the unit in the Prometheus metric name + const unitSuffix = shouldCheckPreviousSuffixForUnit(suffix) ? metricParts.at(-2) : suffix; + + // Get the Grafana unit or Grafana rate unit + const unit = rate ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); + + const params = { + agg, + unit, + rate, + }; + return generateQueries(params); +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts new file mode 100644 index 00000000000..e44bfc31e60 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts @@ -0,0 +1,20 @@ +import { AutoQueryInfo } from '../types'; + +import { createDefaultMetricQueryDefs } from './default'; +import { createHistogramMetricQueryDefs } from './histogram'; +import { createSummaryMetricQueryDefs } from './summary'; + +// TODO: when we have a known unit parameter, use that rather than having the generator functions infer from suffix +export type MetricQueriesGenerator = (metricParts: string[]) => AutoQueryInfo; + +export function getQueryGeneratorFor(suffix?: string): MetricQueriesGenerator { + if (suffix === 'sum') { + return createSummaryMetricQueryDefs; + } + + if (suffix === 'bucket') { + return createHistogramMetricQueryDefs; + } + + return createDefaultMetricQueryDefs; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts index e6e4644bb15..2a12b285ad7 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts @@ -7,7 +7,7 @@ import { simpleGraphBuilder } from '../graph-builders/simple'; import { AutoQueryDef } from '../types'; import { getUnit } from '../units'; -export function createHistogramQueryDefs(metricParts: string[]) { +export function createHistogramMetricQueryDefs(metricParts: string[]) { const title = `${VAR_METRIC_EXPR}`; const unitSuffix = metricParts.at(-2); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts deleted file mode 100644 index 569f4e8e0bf..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import general from './common'; -import { createHistogramQueryDefs } from './histogram'; -import { createSummaryQueryDefs } from './summary'; -import { MetricQueriesGenerator } from './types'; - -const SUFFIX_TO_ALTERNATIVE_GENERATOR: Record = { - sum: createSummaryQueryDefs, - bucket: createHistogramQueryDefs, -}; - -export function getQueryGeneratorFor(suffix?: string) { - return (suffix && SUFFIX_TO_ALTERNATIVE_GENERATOR[suffix]) || general.generator; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts index b6a625b0898..cd73d93db25 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts @@ -2,13 +2,13 @@ import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; import { AutoQueryInfo } from '../types'; import { getUnit } from '../units'; +import { getGeneralBaseQuery } from './common/baseQuery'; import { generateCommonAutoQueryInfo } from './common/generator'; -import { getGeneralBaseQuery } from './common/queries'; -export function createSummaryQueryDefs(metricParts: string[]): AutoQueryInfo { +export function createSummaryMetricQueryDefs(metricParts: string[]): AutoQueryInfo { const suffix = metricParts.at(-1); if (suffix !== 'sum') { - throw new Error('createSummaryQueryDefs is only to be used for metrics that end in "_sum"'); + throw new Error('createSummaryMetricQueryDefs is only to be used for metrics that end in "_sum"'); } const unitSuffix = metricParts.at(-2); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts deleted file mode 100644 index ddf30ccc27c..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AutoQueryInfo } from '../types'; - -export type MetricQueriesGenerator = (metricParts: string[]) => AutoQueryInfo; diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index 490df92529f..d03f6aa4efc 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -1522,10 +1522,30 @@ ${buildImageContent( }, transpose: { name: 'Transpose', - getHelperDocs: function () { + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` Use this transformation to pivot the data frame, converting rows into columns and columns into rows. This transformation is particularly useful when you want to switch the orientation of your data to better suit your visualization needs. If you have multiple types it will default to string type. + +**Before Transformation:** + +| env | January | February | +| ---- | --------- | -------- | +| prod | 1 | 2 | +| dev | 3 | 4 | + +**After applying transpose transformation:** + +| Field | prod | dev | +| ---- | --------- | -------- | +| January | 1 | 3 | +| February | 2 | 4 | + +${buildImageContent( + '/media/docs/grafana/transformations/screenshot-grafana-11-2-transpose-transformation.png', + imageRenderType, + 'Before and after transpose transformation' +)} `; }, }, diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index c5bcacd9457..c27d4e53122 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -119,7 +119,10 @@ export type Route = { group_interval?: string; repeat_interval?: string; routes?: Route[]; + /** Times when the route should be muted. */ mute_time_intervals?: string[]; + /** Times when the route should be active. This is the opposite of `mute_time_intervals` */ + active_time_intervals?: string[]; /** only the root policy might have a provenance field defined */ provenance?: string; }; diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 0f0d7f44be5..c1e83fcddd4 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -2,14 +2,14 @@ "name": "@grafana-plugins/grafana-azure-monitor-datasource", "description": "Grafana data source for Azure Monitor", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.7.13", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "@kusto/monaco-kusto": "^10.0.0", "fast-deep-equal": "^3.1.3", "i18next": "^23.0.0", @@ -25,8 +25,8 @@ "tslib": "2.6.3" }, "devDependencies": { - "@grafana/e2e-selectors": "11.2.0-pre", - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 9bc016c8007..3cd909c0991 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -2,15 +2,15 @@ "name": "@grafana-plugins/stackdriver", "description": "Grafana data source for Google Cloud Monitoring", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.7.13", "@grafana/google-sdk": "0.1.2", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "debounce-promise": "3.1.2", "fast-deep-equal": "^3.1.3", "i18next": "^23.0.0", @@ -26,8 +26,8 @@ "tslib": "2.6.3" }, "devDependencies": { - "@grafana/e2e-selectors": "11.2.0-pre", - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index ef6a0bc2e52..dfb43583ab6 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -2,22 +2,22 @@ "name": "@grafana-plugins/grafana-postgresql-datasource", "description": "PostgreSQL data source plugin", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.7.13", - "@grafana/runtime": "11.2.0-pre", - "@grafana/sql": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/sql": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "lodash": "4.17.21", "react": "18.2.0", "rxjs": "7.8.1", "tslib": "2.6.3" }, "devDependencies": { - "@grafana/e2e-selectors": "11.2.0-pre", - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 51cb54c324b..c3a27ed1751 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -2,13 +2,13 @@ "name": "@grafana-plugins/grafana-pyroscope-datasource", "description": "Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "fast-deep-equal": "^3.1.3", "lodash": "4.17.21", "monaco-editor": "0.34.1", @@ -20,7 +20,7 @@ "tslib": "2.6.3" }, "devDependencies": { - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "15.0.2", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 621ad86bc72..e52ba2d21e9 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -2,14 +2,14 @@ "name": "@grafana-plugins/grafana-testdata-datasource", "description": "Generates test data in different forms", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.7.13", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "d3-random": "^3.0.1", "lodash": "4.17.21", "micro-memoize": "^4.1.2", @@ -22,8 +22,8 @@ "uuid": "9.0.1" }, "devDependencies": { - "@grafana/e2e-selectors": "11.2.0-pre", - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index f5ed35d34db..d177f63fdd1 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -2,7 +2,7 @@ "name": "@grafana-plugins/jaeger", "description": "Jaeger plugin for Grafana", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index b323bfb4c34..29df5d1d6ab 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -64,8 +64,8 @@ const inputFrame: DataFrame = { length: 5, }; -describe('loki backendResultTransformer', () => { - it('processes a logs-dataframe correctly', () => { +describe('backendResultTransformer', () => { + it('processes a logs dataframe correctly', () => { const response: DataQueryResponse = { data: [cloneDeep(inputFrame)] }; const expectedFrame = cloneDeep(inputFrame); @@ -232,4 +232,24 @@ describe('loki backendResultTransformer', () => { ); expect(result.error?.message).toBe('parse error at line 1, col 2: invalid char escape'); }); + + it('resolves search words from queries with template variables', () => { + const dataFrame = cloneDeep(inputFrame); + dataFrame.meta = { + executedQueryString: 'Expr: {service_name="tns-app"} |~ "(?i)template" |= "variable"', + }; + const result = transformBackendResult( + { data: [dataFrame] }, + [ + { + refId: 'A', + expr: `{service_name="tns-app"} |~ "(?i)$search"`, + }, + ], + [] + ); + + expect(result.data[0].meta.searchWords).toContain('(?i)template'); + expect(result.data[0].meta.searchWords).toContain('variable'); + }); }); diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.ts b/public/app/plugins/datasource/loki/backendResultTransformer.ts index 2a720cf0d7b..1cc91c4c621 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.ts @@ -2,7 +2,7 @@ import { DataQueryResponse, DataFrame, isDataFrame, FieldType, QueryResultMeta, import { getDerivedFields } from './getDerivedFields'; import { makeTableFrames } from './makeTableFrames'; -import { getHighlighterExpressionsFromQuery } from './queryUtils'; +import { getExpressionFromExecutedQuery, getHighlighterExpressionsFromQuery } from './queryUtils'; import { dataFrameHasLokiError } from './responseUtils'; import { DerivedFieldConfig, LokiQuery, LokiQueryType } from './types'; @@ -39,7 +39,7 @@ function processStreamFrame( const meta: QueryResultMeta = { preferredVisualisationType: 'logs', limit: query?.maxLines, - searchWords: query !== undefined ? getHighlighterExpressionsFromQuery(query.expr) : undefined, + searchWords: query ? getHighlighterExpressionsFromQuery(query.expr) : undefined, custom, }; @@ -145,7 +145,17 @@ export function transformBackendResult( return d; }); - const queryMap = new Map(queries.map((query) => [query.refId, query])); + const queryMap = new Map( + queries.map((query) => { + const executedExpr = response.data.find((data) => data.refId === query.refId)?.meta.executedQueryString; + const executedQuery = { + ...query, + expr: executedExpr ? getExpressionFromExecutedQuery(executedExpr) : query.expr, + }; + + return [query.refId, executedQuery]; + }) + ); const { streamsFrames, metricInstantFrames, metricRangeFrames } = groupFrames(dataFrames, queryMap); diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index 45bb0870104..368f1819620 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -31,10 +31,10 @@ import { LokiQuery, LokiQueryType } from './types'; /** * Returns search terms from a LogQL query. - * E.g., `{} |= foo |=bar != baz` returns `['foo', 'bar']`. + * E.g., `{} |= "foo" |= "bar" != "baz"` returns `['foo', 'bar']`. */ export function getHighlighterExpressionsFromQuery(input = ''): string[] { - const results = []; + const results: string[] = []; const filters = getNodesFromQuery(input, [LineFilter]); @@ -76,6 +76,10 @@ export function getHighlighterExpressionsFromQuery(input = ''): string[] { return results; } +export function getExpressionFromExecutedQuery(executedQueryString: string) { + return executedQueryString.replace('Expr: ', ''); +} + export function getStringsFromLineFilter(filter: SyntaxNode): SyntaxNode[] { const nodes: SyntaxNode[] = []; let node: SyntaxNode | null = filter; diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index bda91563233..8019a660388 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -2,22 +2,22 @@ "name": "@grafana-plugins/mysql", "description": "MySQL data source plugin", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.7.13", - "@grafana/runtime": "11.2.0-pre", - "@grafana/sql": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/sql": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "lodash": "4.17.21", "react": "18.2.0", "rxjs": "7.8.1", "tslib": "2.6.3" }, "devDependencies": { - "@grafana/e2e-selectors": "11.2.0-pre", - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 1f0dfcb047d..4c961caa2e9 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -2,13 +2,13 @@ "name": "@grafana-plugins/parca", "description": "Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", - "@grafana/data": "11.2.0-pre", - "@grafana/runtime": "11.2.0-pre", - "@grafana/schema": "11.2.0-pre", - "@grafana/ui": "11.2.0-pre", + "@grafana/data": "11.3.0-pre", + "@grafana/runtime": "11.3.0-pre", + "@grafana/schema": "11.3.0-pre", + "@grafana/ui": "11.3.0-pre", "lodash": "4.17.21", "monaco-editor": "0.34.1", "react": "18.2.0", @@ -18,7 +18,7 @@ "tslib": "2.6.3" }, "devDependencies": { - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.test.tsx index 02e8b7190fc..b3a8dae79ae 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.test.tsx @@ -5,7 +5,7 @@ import { useState } from 'react'; import { TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; -import { initTemplateSrv } from '../test_utils'; +import { initTemplateSrv } from '../test/test_utils'; import { TempoQuery } from '../types'; import { GroupByField } from './GroupByField'; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx index dc1c3565d4f..220e2d810a4 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx @@ -1,17 +1,17 @@ import { css } from '@emotion/css'; -import { useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { AccessoryButton } from '@grafana/experimental'; -import { HorizontalGroup, Select, useStyles2 } from '@grafana/ui'; +import { HorizontalGroup, InputActionMeta, Select, useStyles2 } from '@grafana/ui'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import { TempoQuery } from '../types'; import InlineSearchField from './InlineSearchField'; -import { withTemplateVariableOptions } from './SearchField'; +import { maxOptions, withTemplateVariableOptions } from './SearchField'; import { replaceAt } from './utils'; interface Props { @@ -26,6 +26,7 @@ export const GroupByField = (props: Props) => { const { datasource, onChange, query, isTagsLoading, addVariablesToOptions } = props; const styles = useStyles2(getStyles); const generateId = () => uuidv4().slice(0, 8); + const [tagQuery, setTagQuery] = useState(''); useEffect(() => { if (!query.groupBy || query.groupBy.length === 0) { @@ -41,9 +42,18 @@ export const GroupByField = (props: Props) => { } }, [onChange, query]); - const getTags = (f: TraceqlFilter) => { - return datasource!.languageProvider.getMetricsSummaryTags(f.scope); - }; + const tagOptions = useMemo( + () => (f: TraceqlFilter) => { + const tags = datasource!.languageProvider.getMetricsSummaryTags(f.scope); + if (tagQuery.length === 0) { + return tags.slice(0, maxOptions); + } + + const queryLowerCase = tagQuery.toLowerCase(); + return tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, maxOptions); + }, + [datasource, tagQuery] + ); const addFilter = () => { updateFilter({ @@ -74,8 +84,8 @@ export const GroupByField = (props: Props) => { <> {query.groupBy?.map((f, i) => { - const tags = getTags(f) - ?.concat(f.tag !== undefined && !getTags(f)?.includes(f.tag) ? [f.tag] : []) + const tags = tagOptions(f) + ?.concat(f.tag !== undefined && !tagOptions(f)?.includes(f.tag) ? [f.tag] : []) .map((t) => ({ label: t, value: t, @@ -102,6 +112,12 @@ export const GroupByField = (props: Props) => { updateFilter({ ...f, tag: v?.value }); }} options={addVariablesToOptions ? withTemplateVariableOptions(tags) : tags} + onInputChange={(value: string, { action }: InputActionMeta) => { + if (action === 'input-change') { + setTagQuery(value); + } + }} + onCloseMenu={() => setTagQuery('')} placeholder="Select tag" value={f.tag || ''} /> diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx index 55207d53415..7330956c63d 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx @@ -6,7 +6,7 @@ import { LanguageProvider } from '@grafana/data'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; -import { initTemplateSrv } from '../test_utils'; +import { initTemplateSrv } from '../test/test_utils'; import { keywordOperators, numberOperators, operators, stringOperators } from '../traceql/traceql'; import SearchField from './SearchField'; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx index 612c671d506..bb56da5cdb8 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx @@ -6,7 +6,7 @@ import useAsync from 'react-use/lib/useAsync'; import { SelectableValue } from '@grafana/data'; import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime'; -import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui'; +import { Select, HorizontalGroup, useStyles2, InputActionMeta } from '@grafana/ui'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; @@ -59,6 +59,8 @@ const SearchField = ({ // there's only one value selected, so we store the previous operator and value const [prevOperator, setPrevOperator] = useState(filter.operator); const [prevValue, setPrevValue] = useState(filter.value); + const [tagQuery, setTagQuery] = useState(''); + const [tagValuesQuery, setTagValuesQuery] = useState(''); const updateOptions = async () => { try { @@ -127,14 +129,42 @@ const SearchField = ({ case 'float': operatorList = numberOperators; } - - const tagOptions = (filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({ - label: t, - value: t, - })); - const operatorOptions = operatorList.map(operatorSelectableValue); + const formatTagOptions = (tags: string[], filterTag: string | undefined) => { + return (filterTag !== undefined ? uniq([filterTag, ...tags]) : tags).map((t) => ({ label: t, value: t })); + }; + + const tagOptions = useMemo(() => { + if (tagQuery.length === 0) { + return formatTagOptions(tags.slice(0, maxOptions), filter.tag); + } + + const queryLowerCase = tagQuery.toLowerCase(); + const filterdOptions = tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, maxOptions); + return formatTagOptions(filterdOptions, filter.tag); + }, [filter.tag, tagQuery, tags]); + + const tagValueOptions = useMemo(() => { + if (!options) { + return; + } + + if (tagValuesQuery.length === 0) { + return options.slice(0, maxOptions); + } + + const queryLowerCase = tagValuesQuery.toLowerCase(); + return options + .filter((tag) => { + if (tag.value && tag.value.length > 0) { + return tag.value.toLowerCase().includes(queryLowerCase); + } + return false; + }) + .slice(0, maxOptions); + }, [tagValuesQuery, options]); + return ( <> @@ -144,9 +174,7 @@ const SearchField = ({ inputId={`${filter.id}-scope`} options={addVariablesToOptions ? withTemplateVariableOptions(scopeOptions) : scopeOptions} value={filter.scope} - onChange={(v) => { - updateFilter({ ...filter, scope: v?.value }); - }} + onChange={(v) => updateFilter({ ...filter, scope: v?.value })} placeholder="Select scope" aria-label={`select ${filter.id} scope`} /> @@ -158,10 +186,14 @@ const SearchField = ({ isLoading={isTagsLoading} // Add the current tag to the list if it doesn't exist in the tags prop, otherwise the field will be empty even though the state has a value options={addVariablesToOptions ? withTemplateVariableOptions(tagOptions) : tagOptions} - value={filter.tag} - onChange={(v) => { - updateFilter({ ...filter, tag: v?.value, value: [] }); + onInputChange={(value: string, { action }: InputActionMeta) => { + if (action === 'input-change') { + setTagQuery(value); + } }} + onCloseMenu={() => setTagQuery('')} + onChange={(v) => updateFilter({ ...filter, tag: v?.value, value: [] })} + value={filter.tag} placeholder="Select tag" isClearable aria-label={`select ${filter.id} tag`} @@ -174,9 +206,7 @@ const SearchField = ({ inputId={`${filter.id}-operator`} options={addVariablesToOptions ? withTemplateVariableOptions(operatorOptions) : operatorOptions} value={filter.operator} - onChange={(v) => { - updateFilter({ ...filter, operator: v?.value }); - }} + onChange={(v) => updateFilter({ ...filter, operator: v?.value })} isClearable={false} aria-label={`select ${filter.id} operator`} allowCustomValue={true} @@ -193,8 +223,14 @@ const SearchField = ({ className={styles.dropdown} inputId={`${filter.id}-value`} isLoading={isLoadingValues} - options={addVariablesToOptions ? withTemplateVariableOptions(options) : options} + options={addVariablesToOptions ? withTemplateVariableOptions(tagValueOptions) : tagValueOptions} value={filter.value} + onInputChange={(value: string, { action }: InputActionMeta) => { + if (action === 'input-change') { + setTagValuesQuery(value); + } + }} + onCloseMenu={() => setTagValuesQuery('')} onChange={(val) => { if (Array.isArray(val)) { updateFilter({ @@ -231,4 +267,7 @@ export const withTemplateVariableOptions = (options: SelectableValue[] | undefin return [...(options || []), ...templateVariables.map((v) => ({ label: `$${v.name}`, value: `$${v.name}` }))]; }; +// Limit maximum options in select dropdowns for performance reasons +export const maxOptions = 1000; + export default SearchField; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx index 6133badd652..b7834c57774 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; -import { initTemplateSrv } from '../test_utils'; +import { initTemplateSrv } from '../test/test_utils'; import { Scope } from '../types'; import TagsInput from './TagsInput'; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx index 5bf430ba53e..ae2e26ed67f 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx @@ -7,7 +7,7 @@ import { config } from '@grafana/runtime'; import { TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; -import { initTemplateSrv } from '../test_utils'; +import { initTemplateSrv } from '../test/test_utils'; import { TempoQuery } from '../types'; import TraceQLSearch from './TraceQLSearch'; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts index 842ca2745ac..4a580f0adff 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts @@ -151,9 +151,9 @@ describe('generateQueryFromAdHocFilters generates the correct query for', () => expect( generateQueryFromAdHocFilters([ { key: 'footag', operator: '=', value: 'foovalue' }, - { key: 'bartag', operator: '=', value: 'barvalue' }, + { key: 'bartag', operator: '=', value: '0' }, ]) - ).toBe('{footag="foovalue" && bartag="barvalue"}'); + ).toBe('{footag="foovalue" && bartag=0}'); }); it('a filter with intrinsic values', () => { diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts index 795577ad1bc..82078699814 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts @@ -81,9 +81,16 @@ const adHocValueHelper = (f: AdHocVariableFilter) => { if (intrinsics.find((t) => t === f.key)) { return f.value; } + if (parseInt(f.value, 10).toString() === f.value) { + return f.value; + } return `"${f.value}"`; }; +export const getTagWithoutScope = (tag: string) => { + return tag.replace(/^(event|link|resource|span)\./, ''); +}; + export const filterScopedTag = (f: TraceqlFilter) => { return scopeHelper(f) + f.tag; }; diff --git a/public/app/plugins/datasource/tempo/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/tempo/VariableQueryEditor.test.tsx index 700c2defc36..de0e63fb112 100644 --- a/public/app/plugins/datasource/tempo/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/tempo/VariableQueryEditor.test.tsx @@ -10,7 +10,7 @@ import { TempoVariableQueryType, } from './VariableQueryEditor'; import { selectOptionInTest } from './_importedDependencies/test/helpers/selectOptionInTest'; -import { createTempoDatasource } from './mocks'; +import { createTempoDatasource } from './test/mocks'; const refId = 'TempoDatasourceVariableQueryEditor-VariableQuery'; diff --git a/public/app/plugins/datasource/tempo/VariableQueryEditor.tsx b/public/app/plugins/datasource/tempo/VariableQueryEditor.tsx index 5681134baed..e2a2a28373e 100644 --- a/public/app/plugins/datasource/tempo/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/tempo/VariableQueryEditor.tsx @@ -1,8 +1,9 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { DataQuery, SelectableValue } from '@grafana/data'; -import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; +import { InlineField, InlineFieldRow, InputActionMeta, Select } from '@grafana/ui'; +import { maxOptions } from './SearchTraceQLEditor/SearchField'; import { TempoDatasource } from './datasource'; export enum TempoVariableQueryType { @@ -33,6 +34,7 @@ export const TempoVariableQueryEditor = ({ onChange, query, datasource }: TempoV const [label, setLabel] = useState(query.label || ''); const [type, setType] = useState(query.type); const [labelOptions, setLabelOptions] = useState>>([]); + const [labelQuery, setLabelQuery] = useState(''); useEffect(() => { if (type === TempoVariableQueryType.LabelValues) { @@ -42,6 +44,22 @@ export const TempoVariableQueryEditor = ({ onChange, query, datasource }: TempoV } }, [datasource, query, type]); + const options = useMemo(() => { + if (labelQuery.length === 0) { + return labelOptions.slice(0, maxOptions); + } + + const queryLowerCase = labelQuery.toLowerCase(); + return labelOptions + .filter((tag) => { + if (tag.value && tag.value.length > 0) { + return tag.value.toLowerCase().includes(queryLowerCase); + } + return false; + }) + .slice(0, maxOptions); + }, [labelQuery, labelOptions]); + const onQueryTypeChange = (newType: SelectableValue) => { setType(newType.value); if (newType.value !== undefined) { @@ -93,10 +111,17 @@ export const TempoVariableQueryEditor = ({ onChange, query, datasource }: TempoV aria-label="Label" onChange={onLabelChange} onBlur={handleBlur} + onInputChange={(value: string, { action }: InputActionMeta) => { + if (action === 'input-change') { + setLabelQuery(value); + } + }} + onCloseMenu={() => setLabelQuery('')} value={{ label, value: label }} - options={labelOptions} + options={options} width={32} allowCustomValue + virtualized /> diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 43dbb94b2a7..4289a20e1cc 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -41,10 +41,10 @@ import { getFieldConfig, getEscapedSpanNames, } from './datasource'; -import mockJson from './mockJsonResponse.json'; -import mockServiceGraph from './mockServiceGraph.json'; -import { createMetadataRequest, createTempoDatasource } from './mocks'; -import { initTemplateSrv } from './test_utils'; +import mockJson from './test/mockJsonResponse.json'; +import mockServiceGraph from './test/mockServiceGraph.json'; +import { createMetadataRequest, createTempoDatasource } from './test/mocks'; +import { initTemplateSrv } from './test/test_utils'; import { TempoJsonData, TempoQuery } from './types'; let mockObservable: () => Observable; @@ -68,7 +68,7 @@ describe('Tempo data source', () => { describe('runs correctly', () => { config.featureToggles.traceQLStreaming = true; jest.spyOn(TempoDatasource.prototype, 'isFeatureAvailable').mockImplementation(() => true); - const handleStreamingSearch = jest.spyOn(TempoDatasource.prototype, 'handleStreamingSearch'); + const handleStreamingQuery = jest.spyOn(TempoDatasource.prototype, 'handleStreamingQuery'); const request = jest.spyOn(TempoDatasource.prototype, '_request'); const templateSrv: TemplateSrv = { replace: (s: string) => s } as unknown as TemplateSrv; @@ -104,7 +104,7 @@ describe('Tempo data source', () => { config.liveEnabled = true; const ds = new TempoDatasource(defaultSettings, templateSrv); await lastValueFrom(ds.query(traceqlQuery as DataQueryRequest)); - expect(handleStreamingSearch).toHaveBeenCalledTimes(1); + expect(handleStreamingQuery).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledTimes(0); }); @@ -112,7 +112,7 @@ describe('Tempo data source', () => { config.liveEnabled = true; const ds = new TempoDatasource(defaultSettings, templateSrv); await lastValueFrom(ds.query(traceqlSearchQuery as DataQueryRequest)); - expect(handleStreamingSearch).toHaveBeenCalledTimes(1); + expect(handleStreamingQuery).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledTimes(0); }); @@ -120,7 +120,7 @@ describe('Tempo data source', () => { config.liveEnabled = false; const ds = new TempoDatasource(defaultSettings, templateSrv); await lastValueFrom(ds.query(traceqlQuery as DataQueryRequest)); - expect(handleStreamingSearch).toHaveBeenCalledTimes(1); + expect(handleStreamingQuery).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledTimes(1); }); @@ -128,7 +128,7 @@ describe('Tempo data source', () => { config.liveEnabled = false; const ds = new TempoDatasource(defaultSettings, templateSrv); await lastValueFrom(ds.query(traceqlSearchQuery as DataQueryRequest)); - expect(handleStreamingSearch).toHaveBeenCalledTimes(1); + expect(handleStreamingQuery).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledTimes(1); }); }); @@ -365,14 +365,14 @@ describe('Tempo data source', () => { describe('test the testDatasource function', () => { it('should return a success msg if response.ok is true', async () => { mockObservable = () => of({ ok: true }); - const handleStreamingSearch = jest - .spyOn(TempoDatasource.prototype, 'handleStreamingSearch') + const handleStreamingQuery = jest + .spyOn(TempoDatasource.prototype, 'handleStreamingQuery') .mockImplementation(() => of({ data: [] })); const ds = new TempoDatasource(defaultSettings); const response = await ds.testDatasource(); expect(response.status).toBe('success'); - expect(handleStreamingSearch).toHaveBeenCalled(); + expect(handleStreamingQuery).toHaveBeenCalled(); }); }); @@ -397,7 +397,7 @@ describe('Tempo data source', () => { raw: { from: 'now-15m', to: 'now' }, }; - const request = ds.traceIdQueryRequest( + const request = ds.makeTraceIdRequest( { requestId: 'test', interval: '', @@ -426,7 +426,7 @@ describe('Tempo data source', () => { jsonData: { traceQuery: { timeShiftEnabled: false, spanStartTimeShift: '2m', spanEndTimeShift: '4m' } }, }); - const request = ds.traceIdQueryRequest( + const request = ds.makeTraceIdRequest( { requestId: 'test', interval: '', @@ -1192,10 +1192,7 @@ describe('should provide functionality for ad-hoc filters', () => { }, }; const response = await datasource.getTagValues(options); - expect(response).toEqual([ - { text: { type: 'value1', value: 'value1', label: 'value1' } }, - { text: { type: 'value2', value: 'value2', label: 'value2' } }, - ]); + expect(response).toEqual([{ text: 'value1' }, { text: 'value2' }]); }); }); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index da625c8b8bc..d85c944aef9 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -37,6 +37,7 @@ import { BarGaugeDisplayMode, TableCellDisplayMode, VariableFormatID } from '@gr import { generateQueryFromAdHocFilters, generateQueryFromFilters, + getTagWithoutScope, interpolateFilters, } from './SearchTraceQLEditor/utils'; import { TempoVariableQuery, TempoVariableQueryType } from './VariableQueryEditor'; @@ -180,7 +181,7 @@ export class TempoDatasource extends DataSourceWithBackend tag !== undefined).map((tag) => ({ text: tag })); } - async labelValuesQuery(labelName?: string, query?: string): Promise> { + async labelValuesQuery(labelName?: string): Promise> { if (!labelName) { return []; } @@ -202,7 +203,7 @@ export class TempoDatasource extends DataSourceWithBackend): Promise> { const query = generateQueryFromAdHocFilters(options.filters); - return this.labelValuesQuery(options.key.replace(/^(resource|span)\./, ''), query); + return this.tagValuesQuery(options.key, query); + } + + async tagValuesQuery(tag: string, query: string): Promise> { + let options; + try { + // For V2, we need to send scope and tag name, e.g. `span.http.status_code`, + // unless the tag has intrinsic scope + options = await this.languageProvider.getOptionsV2(tag, query); + } catch { + // For V1, the tag name (e.g. `http.status_code`) is enough + options = await this.languageProvider.getOptionsV1(getTagWithoutScope(tag)); + } + + return options.flatMap((option: SelectableValue) => + option.value !== undefined ? [{ text: option.value }] : [] + ); } init = async () => { @@ -283,6 +300,19 @@ export class TempoDatasource extends DataSourceWithBackend): Observable { const subQueries: Array> = []; const filteredTargets = options.targets.filter((target) => !target.hide); @@ -358,7 +388,7 @@ export class TempoDatasource extends DataSourceWithBackend) => { + formatGroupBy = (groupBy: TraceqlFilter[]) => { + return groupBy + ?.filter((f) => f.tag) + .map((f) => { + if (f.scope === TraceqlSearchScope.Unscoped) { + return `.${f.tag}`; + } + return f.scope !== TraceqlSearchScope.Intrinsic ? `${f.scope}.${f.tag}` : f.tag; + }) + .join(', '); + }; + + hasGroupBy = (query: TempoQuery) => { + return query.groupBy?.find((gb) => gb.tag); + }; + + /** + * Handles the simplest of the queries where we have just a trace id and return trace data for it. + * @param options + * @param targets + * @private + */ + handleTraceIdQuery(options: DataQueryRequest, targets: TempoQuery[]): Observable { + const validTargets = targets + .filter((t) => t.query) + .map((t): TempoQuery => ({ ...t, query: t.query?.trim(), queryType: 'traceId' })); + if (!validTargets.length) { + return EMPTY; + } + + const request = this.makeTraceIdRequest(options, validTargets); + return super.query(request).pipe( + map((response) => { + if (response.error) { + return response; + } + return transformTrace(response, this.instanceSettings, this.nodeGraph?.enabled); + }) + ); + } + + handleTraceQlQuery = ( + options: DataQueryRequest, + targets: { + [type: string]: TempoQuery[]; + }, + queryValue: string + ): Observable => { + if (this.isStreamingSearchEnabled()) { + return this.handleStreamingQuery(options, targets.traceql, queryValue); + } else { + return this._request('/api/search', { + q: queryValue, + limit: options.targets[0].limit ?? DEFAULT_LIMIT, + spss: options.targets[0].spss ?? DEFAULT_SPSS, + start: options.range.from.unix(), + end: options.range.to.unix(), + }).pipe( + map((response) => { + return { + data: formatTraceQLResponse(response.data.traces, this.instanceSettings, targets.traceql[0].tableType), + }; + }), + catchError((err) => { + return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); + }) + ); + } + }; + + handleTraceQlMetricsQuery = ( + options: DataQueryRequest, + queryValue: string + ): Observable => { + const requestData = { + query: queryValue, + start: options.range.from.unix(), + end: options.range.to.unix(), + step: options.targets[0].step, + }; + + if (!requestData.step) { + delete requestData.step; + } + + return this._request('/api/metrics/query_range', requestData).pipe( + map((response) => { + return { + data: formatTraceQLMetrics(queryValue, response.data), + }; + }), + catchError((err) => { + return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); + }) + ); + }; + + handleMetricsSummaryQuery = (target: TempoQuery, query: string, options: DataQueryRequest) => { reportInteraction('grafana_traces_metrics_summary_queried', { datasourceType: 'tempo', app: options.app ?? '', @@ -574,105 +688,30 @@ export class TempoDatasource extends DataSourceWithBackend { - return groupBy - ?.filter((f) => f.tag) - .map((f) => { - if (f.scope === TraceqlSearchScope.Unscoped) { - return `.${f.tag}`; - } - return f.scope !== TraceqlSearchScope.Intrinsic ? `${f.scope}.${f.tag}` : f.tag; - }) - .join(', '); - }; - - hasGroupBy = (query: TempoQuery) => { - return query.groupBy?.find((gb) => gb.tag); - }; - - /** - * Handles the simplest of the queries where we have just a trace id and return trace data for it. - * @param options - * @param targets - * @private - */ - handleTraceIdQuery(options: DataQueryRequest, targets: TempoQuery[]): Observable { - const validTargets = targets - .filter((t) => t.query) - .map((t): TempoQuery => ({ ...t, query: t.query?.trim(), queryType: 'traceId' })); - if (!validTargets.length) { + // This function can probably be simplified by avoiding passing both `targets` and `query`, + // since `query` is built from `targets`, if you look at how this function is currently called + handleStreamingQuery( + options: DataQueryRequest, + targets: TempoQuery[], + query: string + ): Observable { + if (query === '') { return EMPTY; } - const traceRequest = this.traceIdQueryRequest(options, validTargets); - - return super.query(traceRequest).pipe( - map((response) => { - if (response.error) { - return response; - } - return transformTrace(response, this.instanceSettings, this.nodeGraph?.enabled); - }) + return merge( + ...targets.map((target) => + doTempoChannelStream( + { ...target, query }, + this, // the datasource + options, + this.instanceSettings + ) + ) ); } - handleTraceQlQuery = ( - options: DataQueryRequest, - targets: { - [type: string]: TempoQuery[]; - }, - queryValue: string - ): Observable => { - if (this.isStreamingSearchEnabled()) { - return this.handleStreamingSearch(options, targets.traceql, queryValue); - } else { - return this._request('/api/search', { - q: queryValue, - limit: options.targets[0].limit ?? DEFAULT_LIMIT, - spss: options.targets[0].spss ?? DEFAULT_SPSS, - start: options.range.from.unix(), - end: options.range.to.unix(), - }).pipe( - map((response) => { - return { - data: formatTraceQLResponse(response.data.traces, this.instanceSettings, targets.traceql[0].tableType), - }; - }), - catchError((err) => { - return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); - }) - ); - } - }; - - handleTraceQlMetricsQuery = ( - options: DataQueryRequest, - queryValue: string - ): Observable => { - const requestData = { - query: queryValue, - start: options.range.from.unix(), - end: options.range.to.unix(), - step: options.targets[0].step, - }; - - if (!requestData.step) { - delete requestData.step; - } - - return this._request('/api/metrics/query_range', requestData).pipe( - map((response) => { - return { - data: formatTraceQLMetrics(queryValue, response.data), - }; - }), - catchError((err) => { - return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); - }) - ); - }; - - traceIdQueryRequest(options: DataQueryRequest, targets: TempoQuery[]): DataQueryRequest { + makeTraceIdRequest(options: DataQueryRequest, targets: TempoQuery[]): DataQueryRequest { const request = { ...options, targets, @@ -697,29 +736,6 @@ export class TempoDatasource extends DataSourceWithBackend, - targets: TempoQuery[], - query: string - ): Observable { - if (query === '') { - return EMPTY; - } - - return merge( - ...targets.map((target) => - doTempoChannelStream( - { ...target, query }, - this, // the datasource - options, - this.instanceSettings - ) - ) - ); - } - async metadataRequest(url: string, params = {}) { return await lastValueFrom(this._request(url, params, { method: 'GET', hideFromInspector: true })); } @@ -728,7 +744,6 @@ export class TempoDatasource extends DataSourceWithBackend): DataQ }; } -function getServiceGraphView( +function getServiceGraphViewDataFrames( request: DataQueryRequest, rateResponse: ServiceMapQueryResponseWithRates, secondResponse: DataQueryResponse, diff --git a/public/app/plugins/datasource/tempo/graphTransform.test.ts b/public/app/plugins/datasource/tempo/graphTransform.test.ts index efebee60cd6..7a5ab7ccac8 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.test.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.test.ts @@ -1,7 +1,7 @@ import { DataFrameView, dateTime, createDataFrame, FieldType } from '@grafana/data'; import { createGraphFrames, mapPromMetricsToServiceMap } from './graphTransform'; -import { bigResponse } from './testResponse'; +import { bigResponse } from './test/testResponse'; describe('createGraphFrames', () => { it('transforms basic response into nodes and edges frame', async () => { diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 645ad01f028..da5624e717c 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -2,7 +2,7 @@ "name": "@grafana-plugins/tempo", "description": "Grafana plugin for the Tempo data source.", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", @@ -39,7 +39,7 @@ "uuid": "9.0.1" }, "devDependencies": { - "@grafana/plugin-configs": "11.2.0-pre", + "@grafana/plugin-configs": "11.3.0-pre", "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "15.0.2", diff --git a/public/app/plugins/datasource/tempo/resultTransformer.test.ts b/public/app/plugins/datasource/tempo/resultTransformer.test.ts index 0c9c4750755..fc47f88dab2 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.test.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.test.ts @@ -14,7 +14,7 @@ import { otlpDataFrameFromResponse, otlpResponse, traceQlResponse, -} from './testResponse'; +} from './test/testResponse'; import { TraceSearchMetadata } from './types'; const defaultSettings: DataSourceInstanceSettings = { diff --git a/public/app/plugins/datasource/tempo/mockJsonResponse.json b/public/app/plugins/datasource/tempo/test/mockJsonResponse.json similarity index 100% rename from public/app/plugins/datasource/tempo/mockJsonResponse.json rename to public/app/plugins/datasource/tempo/test/mockJsonResponse.json diff --git a/public/app/plugins/datasource/tempo/mockServiceGraph.json b/public/app/plugins/datasource/tempo/test/mockServiceGraph.json similarity index 100% rename from public/app/plugins/datasource/tempo/mockServiceGraph.json rename to public/app/plugins/datasource/tempo/test/mockServiceGraph.json diff --git a/public/app/plugins/datasource/tempo/mocks.ts b/public/app/plugins/datasource/tempo/test/mocks.ts similarity index 94% rename from public/app/plugins/datasource/tempo/mocks.ts rename to public/app/plugins/datasource/tempo/test/mocks.ts index 8a9c568ac81..28fe7c66f91 100644 --- a/public/app/plugins/datasource/tempo/mocks.ts +++ b/public/app/plugins/datasource/tempo/test/mocks.ts @@ -1,8 +1,8 @@ import { DataSourceInstanceSettings, PluginType, toUtc } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; -import { TempoDatasource } from './datasource'; -import { TempoJsonData } from './types'; +import { TempoDatasource } from '../datasource'; +import { TempoJsonData } from '../types'; const rawRange = { from: toUtc('2018-04-25 10:00'), diff --git a/public/app/plugins/datasource/tempo/testResponse.ts b/public/app/plugins/datasource/tempo/test/testResponse.ts similarity index 100% rename from public/app/plugins/datasource/tempo/testResponse.ts rename to public/app/plugins/datasource/tempo/test/testResponse.ts diff --git a/public/app/plugins/datasource/tempo/test_utils.ts b/public/app/plugins/datasource/tempo/test/test_utils.ts similarity index 100% rename from public/app/plugins/datasource/tempo/test_utils.ts rename to public/app/plugins/datasource/tempo/test/test_utils.ts diff --git a/public/app/plugins/datasource/tempo/variables.test.ts b/public/app/plugins/datasource/tempo/variables.test.ts index 1b5f9fce94c..4a2215c4aae 100644 --- a/public/app/plugins/datasource/tempo/variables.test.ts +++ b/public/app/plugins/datasource/tempo/variables.test.ts @@ -3,7 +3,7 @@ import { lastValueFrom } from 'rxjs'; import { DataQueryRequest, TimeRange } from '@grafana/data'; import { TempoVariableQuery } from './VariableQueryEditor'; -import { createMetadataRequest, createTempoDatasource } from './mocks'; +import { createMetadataRequest, createTempoDatasource } from './test/mocks'; import { TempoVariableSupport } from './variables'; describe('TempoVariableSupport', () => { diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 559f4e94970..34f8d08f539 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -2,7 +2,7 @@ "name": "@grafana-plugins/zipkin", "description": "Zipkin plugin for Grafana", "private": true, - "version": "11.2.0-pre", + "version": "11.3.0-pre", "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 392b1c0722a..3574b6d5045 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -399,7 +399,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { // Calculate final co-ordinates for text position const x = u.bbox.left + (isXHorizontal ? lft + wid / 2 : value < 0 ? lft - labelOffset : lft + wid + labelOffset); - const y = + let y = u.bbox.top + (isXHorizontal ? (value < 0 ? top + hgt + labelOffset : top - labelOffset) : top + hgt / 2 - middleShift); @@ -436,6 +436,11 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { xAdjust = value < 0 ? textMetrics.width * scaleFactor : 0; } + // Force label bounding box y position to not be negative + if (y - yAdjust < 0) { + y = yAdjust; + } + // Construct final bounding box for the label text labels[dataIdx][seriesIdx].x = x; labels[dataIdx][seriesIdx].y = y; diff --git a/public/app/plugins/panel/canvas/migrations.ts b/public/app/plugins/panel/canvas/migrations.ts index 527782bd142..c4fe614aab3 100644 --- a/public/app/plugins/panel/canvas/migrations.ts +++ b/public/app/plugins/panel/canvas/migrations.ts @@ -54,7 +54,7 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial => { // append override links to elements with dimensions mapped to same field name for (const prop of override.properties) { if (prop.id === 'links') { - addLinks(panel.options.root.elements, prop.value, override.matcher.options); + addLinks(panel.options.root.elements, prop.value ?? [], override.matcher.options); } else { props.push(prop); } diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx index 88e875ae0ed..7b7a034f35e 100644 --- a/public/app/plugins/panel/logs/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -426,6 +426,122 @@ describe('LogsPanel', () => { }); }); }); + + describe('Show/hide fields', () => { + const series = [ + createDataFrame({ + refId: 'A', + fields: [ + { + name: 'time', + type: FieldType.time, + values: ['2019-04-26T09:28:11.352440161Z'], + }, + { + name: 'message', + type: FieldType.string, + values: ['logline text'], + labels: { + app: 'common_app', + }, + }, + ], + }), + ]; + + it('displays the provided fields instead of the log line', async () => { + setup({ + data: { + series, + }, + options: { + showLabels: false, + showTime: false, + wrapLogMessage: false, + showCommonLabels: false, + prettifyLogMessage: false, + sortOrder: LogsSortOrder.Descending, + dedupStrategy: LogsDedupStrategy.none, + enableLogDetails: true, + displayedFields: ['app'], + onClickHideField: undefined, + onClickShowField: undefined, + }, + }); + + expect(await screen.findByRole('row')).toBeInTheDocument(); + expect(screen.queryByText('logline text')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByText('app=common_app')); + + expect(screen.getByLabelText('Hide this field')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Hide this field')); + + expect(screen.getByText('logline text')).toBeInTheDocument(); + }); + + it('enables the behavior with a default implementation', async () => { + setup({ + data: { + series, + }, + options: { + showLabels: false, + showTime: false, + wrapLogMessage: false, + showCommonLabels: false, + prettifyLogMessage: false, + sortOrder: LogsSortOrder.Descending, + dedupStrategy: LogsDedupStrategy.none, + enableLogDetails: true, + displayedFields: [], + onClickHideField: undefined, + onClickShowField: undefined, + }, + }); + + expect(await screen.findByRole('row')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('logline text')); + await userEvent.click(screen.getByLabelText('Show this field instead of the message')); + + expect(screen.getByText('app=common_app')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Hide this field')); + + expect(screen.getByText('logline text')).toBeInTheDocument(); + }); + + it('overrides the default implementation when the callbacks are provided', async () => { + const onClickShowFieldMock = jest.fn(); + + setup({ + data: { + series, + }, + options: { + showLabels: false, + showTime: false, + wrapLogMessage: false, + showCommonLabels: false, + prettifyLogMessage: false, + sortOrder: LogsSortOrder.Descending, + dedupStrategy: LogsDedupStrategy.none, + enableLogDetails: true, + onClickHideField: jest.fn(), + onClickShowField: onClickShowFieldMock, + }, + }); + + expect(await screen.findByRole('row')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('logline text')); + await userEvent.click(screen.getByLabelText('Show this field instead of the message')); + + expect(onClickShowFieldMock).toHaveBeenCalledTimes(1); + }); + }); }); const setup = (propsOverrides?: {}) => { diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index a03c2719210..f9a78596387 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -36,6 +36,8 @@ import { isOnClickFilterOutLabel, isOnClickFilterOutString, isOnClickFilterString, + isOnClickHideField, + isOnClickShowField, Options, } from './types'; import { useDatasourcesFromTargets } from './useDatasourcesFromTargets'; @@ -56,6 +58,15 @@ interface LogsPanelProps extends PanelProps { * * Determines if a given key => value filter is active in a given query. Used by Log details. * isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; + * + * Array of field names to display instead of the log line. Pass a list of fields or an empty array to enable hide/show fields in Log Details. + * displayedFields?: string[] + * + * Called from the "eye" icon in Log Details to request showing the displayed field. If ommited, a default implementation is used. + * onClickShowField?: (key: string) => void; + * + * Called from the "eye" icon in Log Details to request hiding the displayed field. If ommited, a default implementation is used. + * onClickHideField?: (key: string) => void; */ } interface LogsPermalinkUrlState { @@ -85,6 +96,7 @@ export const LogsPanel = ({ onClickFilterOutString, onClickFilterString, isFilterLabelActive, + ...options }, id, }: LogsPanelProps) => { @@ -96,6 +108,7 @@ export const LogsPanel = ({ const timeRange = data.timeRange; const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets); const [scrollElement, setScrollElement] = useState(null); + const [displayedFields, setDisplayedFields] = useState(options.displayedFields ?? []); let closeCallback = useRef<() => void>(); const { eventBus, onAddAdHocFilter } = usePanelContext(); @@ -272,6 +285,26 @@ export const LogsPanel = ({ [onAddAdHocFilter] ); + const showField = useCallback( + (key: string) => { + const index = displayedFields?.indexOf(key); + if (index === -1) { + setDisplayedFields(displayedFields?.concat(key)); + } + }, + [displayedFields] + ); + + const hideField = useCallback( + (key: string) => { + const index = displayedFields?.indexOf(key); + if (index !== undefined && index > -1) { + setDisplayedFields(displayedFields?.filter((k) => key !== k)); + } + }, + [displayedFields] + ); + if (!data || logRows.length === 0) { return ; } @@ -290,6 +323,9 @@ export const LogsPanel = ({ const defaultOnClickFilterLabel = onAddAdHocFilter ? handleOnClickFilterLabel : undefined; const defaultOnClickFilterOutLabel = onAddAdHocFilter ? handleOnClickFilterOutLabel : undefined; + const onClickShowField = isOnClickShowField(options.onClickShowField) ? options.onClickShowField : showField; + const onClickHideField = isOnClickHideField(options.onClickHideField) ? options.onClickHideField : hideField; + return ( <> {contextRow && ( @@ -342,6 +378,9 @@ export const LogsPanel = ({ isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined } isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined} + displayedFields={displayedFields} + onClickShowField={displayedFields !== undefined ? onClickShowField : undefined} + onClickHideField={displayedFields !== undefined ? onClickHideField : undefined} /> {showCommonLabels && isAscending && renderCommonLabels()}
diff --git a/public/app/plugins/panel/logs/panelcfg.cue b/public/app/plugins/panel/logs/panelcfg.cue index 239de796ceb..d66b2d7c07b 100644 --- a/public/app/plugins/panel/logs/panelcfg.cue +++ b/public/app/plugins/panel/logs/panelcfg.cue @@ -41,6 +41,9 @@ composableKinds: PanelCfg: { isFilterLabelActive?: _ onClickFilterString?: _ onClickFilterOutString?: _ + onClickShowField?: _ + onClickHideField?: _ + displayedFields?: [...string] } @cuetsy(kind="interface") } }] diff --git a/public/app/plugins/panel/logs/panelcfg.gen.ts b/public/app/plugins/panel/logs/panelcfg.gen.ts index c41589a2ce0..b8139c88826 100644 --- a/public/app/plugins/panel/logs/panelcfg.gen.ts +++ b/public/app/plugins/panel/logs/panelcfg.gen.ts @@ -12,6 +12,7 @@ import * as common from '@grafana/schema'; export interface Options { dedupStrategy: common.LogsDedupStrategy; + displayedFields?: Array; enableLogDetails: boolean; isFilterLabelActive?: unknown; /** @@ -21,6 +22,8 @@ export interface Options { onClickFilterOutLabel?: unknown; onClickFilterOutString?: unknown; onClickFilterString?: unknown; + onClickHideField?: unknown; + onClickShowField?: unknown; prettifyLogMessage: boolean; showCommonLabels: boolean; showLabels: boolean; @@ -29,3 +32,7 @@ export interface Options { sortOrder: common.LogsSortOrder; wrapLogMessage: boolean; } + +export const defaultOptions: Partial = { + displayedFields: [], +}; diff --git a/public/app/plugins/panel/logs/types.ts b/public/app/plugins/panel/logs/types.ts index 711bd17c54f..6fc10e9970e 100644 --- a/public/app/plugins/panel/logs/types.ts +++ b/public/app/plugins/panel/logs/types.ts @@ -7,6 +7,8 @@ type onClickFilterOutLabelType = (key: string, value: string, frame?: DataFrame) type onClickFilterValueType = (value: string, refId?: string) => void; type onClickFilterOutStringType = (value: string, refId?: string) => void; type isFilterLabelActiveType = (key: string, value: string, refId?: string) => Promise; +type isOnClickShowFieldType = (value: string) => void; +type isOnClickHideFieldType = (value: string) => void; export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType { return typeof callback === 'function'; @@ -27,3 +29,11 @@ export function isOnClickFilterOutString(callback: unknown): callback is onClick export function isIsFilterLabelActive(callback: unknown): callback is isFilterLabelActiveType { return typeof callback === 'function'; } + +export function isOnClickShowField(callback: unknown): callback is isOnClickShowFieldType { + return typeof callback === 'function'; +} + +export function isOnClickHideField(callback: unknown): callback is isOnClickHideFieldType { + return typeof callback === 'function'; +} diff --git a/public/app/plugins/panel/text/TextPanel.test.tsx b/public/app/plugins/panel/text/TextPanel.test.tsx index 08f034dc3df..04c86415e67 100644 --- a/public/app/plugins/panel/text/TextPanel.test.tsx +++ b/public/app/plugins/panel/text/TextPanel.test.tsx @@ -60,6 +60,15 @@ describe('TextPanel', () => { expect(() => setup()).not.toThrow(); }); + it('should not throw an error when interpolating variables results in empty content', () => { + const contentTest = '${__all_variables}'; + const props = Object.assign({}, defaultProps, { + options: { content: contentTest, mode: TextMode.HTML }, + }); + + expect(() => setup(props)).not.toThrow(); + }); + it('sanitizes content in html mode', () => { const contentTest = '

Form tags are sanitized.

\n'; replaceVariablesMock.mockReturnValueOnce(contentTest); diff --git a/public/app/plugins/panel/text/TextPanel.tsx b/public/app/plugins/panel/text/TextPanel.tsx index 98762f4a106..4f0c15ffcec 100644 --- a/public/app/plugins/panel/text/TextPanel.tsx +++ b/public/app/plugins/panel/text/TextPanel.tsx @@ -64,14 +64,15 @@ export function TextPanel(props: Props) { function processContent(options: Options, interpolate: InterpolateFunction, disableSanitizeHtml: boolean): string { let { mode, content } = options; - if (!content) { - return ' '; - } // Variables must be interpolated before content is converted to markdown so using variables // in URLs work properly content = interpolate(content, {}, options.code?.language === 'json' ? 'json' : 'html'); + if (!content) { + return ' '; + } + switch (mode) { case TextMode.Code: break; // nothing diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 552a3431479..a52aafa5eeb 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -20,6 +20,7 @@ import { getAppPluginRoutes } from 'app/features/plugins/routes'; import { getProfileRoutes } from 'app/features/profile/routes'; import { AccessControlAction, DashboardRoutes } from 'app/types'; +import { BookmarksPage } from '../core/components/Bookmarks/BookmarksPage'; import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport'; import { RouteDescriptor } from '../core/navigation/types'; import { getPublicDashboardRoutes } from '../features/dashboard/routes'; @@ -513,7 +514,7 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/bookmarks', - component: () => , + component: () => , }, ...getPluginCatalogRoutes(), ...getSupportBundleRoutes(), diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index ba90501e3ac..c1a4563946a 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -106,6 +106,7 @@ "export-all": "", "view": "" }, + "contact-point": "", "contact-points": { "delivery-duration": "", "last-delivery-attempt": "", @@ -136,7 +137,21 @@ "saving": "" }, "policies": { + "default-policy": { + "description": "", + "title": "" + }, + "generated-policies": "", "metadata": { + "active-time": "", + "delivered-to": "", + "grouped-by": "", + "grouping": { + "none": "", + "single-group": "" + }, + "inherited": "", + "mute-time": "", "timingOptions": { "groupInterval": { "description": "", @@ -150,8 +165,15 @@ "description": "", "label": "" } - } - } + }, + "n-instances_one": "", + "n-instances_other": "" + }, + "new-child": "", + "new-policy": "", + "no-matchers": "", + "n-more-policies_one": "", + "n-more-policies_other": "" }, "provisioning": { "badge-tooltip-provenance": "", @@ -1911,16 +1933,7 @@ "dismissable-button": "Schließen" }, "scopes": { - "filters": { - "apply": "", - "cancel": "", - "input": { - "placeholder": "", - "removeAll": "" - }, - "title": "" - }, - "suggestedDashboards": { + "dashboards": { "loading": "", "noResultsForFilter": "", "noResultsForFilterClear": "", @@ -1929,9 +1942,19 @@ "search": "", "toggle": { "collapse": "", + "disabled": "", "expand": "" } }, + "selector": { + "apply": "", + "cancel": "", + "input": { + "placeholder": "", + "removeAll": "" + }, + "title": "" + }, "tree": { "collapse": "", "expand": "", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 84a34e98efb..eab5ecd0bb6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -106,6 +106,7 @@ "export-all": "Export all", "view": "View" }, + "contact-point": "Contact Point", "contact-points": { "delivery-duration": "Last delivery took <1>", "last-delivery-attempt": "Last delivery attempt", @@ -119,6 +120,9 @@ "used-by_one": "Used by {{ count }} notification policy", "used-by_other": "Used by {{ count }} notification policies" }, + "contactPointFilter": { + "label": "Contact point" + }, "grafana-rules": { "export-rules": "Export rules", "loading": "Loading...", @@ -136,7 +140,23 @@ "saving": "Saving mute timing" }, "policies": { + "default-policy": { + "description": "All alert instances will be handled by the default policy if no other matching policies are found.", + "title": "Default policy" + }, + "generated-policies": "Auto-generated policies", "metadata": { + "active-time": "Active when", + "delivered-to": "Delivered to", + "grouped-by": "Grouped by", + "grouping": { + "none": "Not grouping", + "single-group": "Single group" + }, + "inherited": "Inherited", + "mute-time": "Muted when", + "n-instances_one": "instance", + "n-instances_other": "instances", "timingOptions": { "groupInterval": { "description": "How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent.", @@ -151,7 +171,12 @@ "label": "Repeated every <1>" } } - } + }, + "n-more-policies_one": "{{count}} additional policy", + "n-more-policies_other": "{{count}} additional policies", + "new-child": "New child policy", + "new-policy": "Add new policy", + "no-matchers": "No matchers" }, "provisioning": { "badge-tooltip-provenance": "This resource has been provisioned via {{provenance}} and cannot be edited through the UI", @@ -669,18 +694,14 @@ "message": "No data sources found" } }, + "embed": { + "share": { + "time-range-description": "Change the current relative time range to an absolute time range", + "time-range-label": "Time range" + } + }, "explore": { "add-to-dashboard": "Add to dashboard", - "add-to-library-modal": { - "auto-star": "Auto-star this query to add it to your starred list in the Query Library.", - "data-source-name": "Data source name", - "data-source-type": "Data source type", - "description": "Description", - "info": "You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.", - "query": "Query", - "title": "Add query to Query Library", - "visibility": "Visibility" - }, "logs": { "maximum-pinned-logs": "Maximum of {{PINNED_LOGS_LIMIT}} pinned logs reached. Unpin a log to add another.", "no-logs-found": "No logs found.", @@ -688,6 +709,7 @@ "stop-scan": "Stop scan" }, "query-library": { + "add-edit-description": "Add/edit description", "cancel": "Cancel", "default-description": "Public", "delete-query": "Delete query", @@ -696,10 +718,26 @@ "private": "Private", "public": "Public", "query-deleted": "Query deleted", + "query-template-add-error": "Error attempting to add this query to the library", "query-template-added": "Query template successfully added to the library", - "query-template-error": "Error attempting to add this query to the library", + "query-template-edit-error": "Error attempting to edit this query", + "query-template-edited": "Query template successfully edited", "save": "Save" }, + "query-template-modal": { + "add-info": "You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.", + "add-title": "Add query to Query Library", + "auto-star": "Auto-star this query to add it to your starred list in the Query Library.", + "data-source-name": "Data source name", + "description": "Description", + "edit-info": "You're about to edit this query. Once saved, you can easily access it in the Query Library tab for future use and reference.", + "edit-title": "Edit query", + "query": "Query", + "visibility": "Visibility" + }, + "query-template-modall": { + "data-source-type": "Data source type" + }, "rich-history": { "close-tooltip": "Close query history", "datasource-a-z": "Data source A-Z", @@ -1018,6 +1056,11 @@ "new-to-question": "New to Grafana?" } }, + "logs": { + "infinite-scroll": { + "older-logs": "Older logs" + } + }, "migrate-to-cloud": { "build-snapshot": { "description": "This tool can migrate some resources from this installation to your cloud stack. To get started, you'll need to create a snapshot of this installation. Creating a snapshot typically takes less than two minutes. The snapshot is stored alongside this Grafana installation.", @@ -1481,6 +1524,10 @@ "subtitle": "Use service accounts to run automated workloads in Grafana", "title": "Service accounts" }, + "shared-dashboard": { + "subtitle": "Manage your organization's externally shared dashboards", + "title": "Shared dashboards" + }, "sign-out": { "title": "Sign out" }, @@ -1562,6 +1609,14 @@ "starred-dashboard": "Dashboard starred", "unstarred-dashboard": "Dashboard unstarred" }, + "oauth": { + "form": { + "server-discovery-action-button": "Enter OpenID Connect Discovery URL", + "server-discovery-modal-close": "Close", + "server-discovery-modal-loading": "Loading...", + "server-discovery-modal-submit": "Submit" + } + }, "panel": { "header-menu": { "copy": "Copy", @@ -1634,6 +1689,18 @@ } }, "plugins": { + "details": { + "labels": { + "dependencies": "Dependencies", + "downloads": "Downloads", + "from": "From", + "reportAbuse": "Report Abuse", + "signature": "Signature", + "status": "Status", + "updatedAt": "Last updated: ", + "version": "Version" + } + }, "empty-state": { "message": "No plugins found" } @@ -1694,8 +1761,7 @@ "welcome-title": "Welcome to public dashboards!" }, "delete-modal": { - "revoke-nonorphaned-body-text": "Are you sure you want to revoke this URL? The dashboard will no longer be public.", - "revoke-orphaned-body-text": "Orphaned public dashboard will no longer be public.", + "revoke-body-text": "Are you sure you want to revoke this URL? The dashboard will no longer be public.", "revoke-title": "Revoke public URL" }, "email-sharing": { @@ -1794,10 +1860,6 @@ "revoke-button-tooltip": "Revoke public dashboard URL", "view-button-tooltip": "View public dashboard" }, - "dashboard-title": { - "orphaned-title": "<0>Orphaned public dashboard", - "orphaned-tooltip": "The linked dashboard has already been deleted" - }, "empty-state": { "message": "You haven't created any public dashboards yet", "more-info": "Create a public dashboard from any existing dashboard through the <1>Share modal. <4>Learn more" @@ -1808,10 +1870,12 @@ }, "public-dashboard-users-access-list": { "dashboard-modal": { + "external-link": "External link", "loading-text": "Loading...", "open-dashboard-list-text": "Open dashboards list", "public-dashboard-link": "Public dashboard URL", - "public-dashboard-setting": "Public dashboard settings" + "public-dashboard-setting": "Public dashboard settings", + "sharing-setting": "Sharing settings" }, "delete-user-modal": { "delete-user-button-text": "Delete user", @@ -1821,8 +1885,12 @@ "revoke-user-access-modal-desc-line1": "Are you sure you want to revoke access for {{email}}?", "revoke-user-access-modal-desc-line2": "This action will immediately revoke {{email}}'s access to all public dashboards." }, + "delete-user-shared-dashboards-modal": { + "revoke-user-access-modal-desc-line2": "This action will immediately revoke {{email}}'s access to all shared dashboards." + }, "modal": { - "dashboard-modal-title": "Public dashboards" + "dashboard-modal-title": "Public dashboards", + "shared-dashboard-modal-title": "Shared dashboards" }, "table-header": { "activated-label": "Activated", @@ -1910,17 +1978,15 @@ }, "dismissable-button": "Close" }, + "save-dashboards": { + "name-exists": { + "message-info": "A dashboard with the same name in the selected folder already exists, including recently deleted dashboards.", + "message-suggestion": "Please choose a different name or folder.", + "title": "Dashboard name already exists" + } + }, "scopes": { - "filters": { - "apply": "Apply", - "cancel": "Cancel", - "input": { - "placeholder": "Select scopes...", - "removeAll": "Remove all scopes" - }, - "title": "Select scopes" - }, - "suggestedDashboards": { + "dashboards": { "loading": "Loading dashboards", "noResultsForFilter": "No results found for your query", "noResultsForFilterClear": "Clear search", @@ -1928,10 +1994,20 @@ "noResultsNoScopes": "No scopes selected", "search": "Search", "toggle": { - "collapse": "Collapse scope filters", - "expand": "Expand scope filters" + "collapse": "Collapse suggested dashboards list", + "disabled": "Suggested dashboards list is disabled due to read only mode", + "expand": "Expand suggested dashboards list" } }, + "selector": { + "apply": "Apply", + "cancel": "Cancel", + "input": { + "placeholder": "Select scopes...", + "removeAll": "Remove all scopes" + }, + "title": "Select scopes" + }, "tree": { "collapse": "Collapse", "expand": "Expand", @@ -2008,8 +2084,7 @@ "html": "Embed HTML", "html-description": "The HTML code below can be pasted and included in another web page. Unless anonymous access is enabled, the user viewing that page need to be signed into Grafana for the graph to load.", "info": "Generate HTML for embedding an iframe with this panel.", - "time-range": "Current time range", - "time-range-description": "Transforms the current relative time range to an absolute time range" + "time-range": "Time range" }, "export": { "back-button": "Back to export config", @@ -2079,10 +2154,14 @@ }, "share-panel": { "drawer": { - "share-link-title": "Link settings" + "share-embed-title": "Share embed", + "share-link-title": "Link settings", + "share-snapshot-title": "Share snapshot" }, "menu": { - "share-link-title": "Share link" + "share-embed-title": "Share embed", + "share-link-title": "Share link", + "share-snapshot-title": "Share snapshot" } }, "share-playlist": { @@ -2106,10 +2185,25 @@ } }, "shared-dashboard": { + "delete-modal": { + "revoke-body-text": "Are you sure you want to revoke this access? The dashboard can no longer be shared.", + "revoke-title": "Revoke access" + }, "fields": { "timezone-label": "Timezone" } }, + "shared-dashboard-list": { + "button": { + "config-button-tooltip": "Configure shared dashboard", + "revoke-button-text": "Revoke access", + "revoke-button-tooltip": "Revoke access", + "view-button-tooltip": "View shared dashboard" + }, + "toggle": { + "pause-sharing-toggle-text": "Pause access" + } + }, "shared-preferences": { "fields": { "home-dashboard-label": "Home Dashboard", @@ -2163,12 +2257,15 @@ "info-alert": "A Grafana dashboard snapshot publicly shares a dashboard while removing sensitive data such as queries and panel links, leaving only visible metrics and series names. Anyone with the link can access the snapshot.", "learn-more-button": "Learn more", "local-button": "Publish snapshot", - "name-label": "Snapshot name*", + "name-label": "Snapshot name", "new-snapshot-button": "New snapshot", "success-creation": "Your snapshot has been created", "success-delete": "Your snapshot has been deleted", "view-all-button": "View all snapshots" }, + "share-panel": { + "info-alert": "A Grafana panel snapshot publicly shares a panel while removing sensitive data such as queries and panel links, leaving only visible metrics and series names. Anyone with the link can access the snapshot." + }, "url-column-header": "Snapshot url", "view-button": "View" }, @@ -2297,7 +2394,8 @@ }, "users-access-list": { "tabs": { - "public-dashboard-users-tab-title": "Public dashboard users" + "public-dashboard-users-tab-title": "Public dashboard users", + "shared-dashboard-users-tab-title": "Shared dashboard users" } }, "variable": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index fa2c5b4fd11..56de05e7067 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -106,6 +106,7 @@ "export-all": "", "view": "" }, + "contact-point": "", "contact-points": { "delivery-duration": "", "last-delivery-attempt": "", @@ -136,7 +137,21 @@ "saving": "" }, "policies": { + "default-policy": { + "description": "", + "title": "" + }, + "generated-policies": "", "metadata": { + "active-time": "", + "delivered-to": "", + "grouped-by": "", + "grouping": { + "none": "", + "single-group": "" + }, + "inherited": "", + "mute-time": "", "timingOptions": { "groupInterval": { "description": "", @@ -150,8 +165,15 @@ "description": "", "label": "" } - } - } + }, + "n-instances_one": "", + "n-instances_other": "" + }, + "new-child": "", + "new-policy": "", + "no-matchers": "", + "n-more-policies_one": "", + "n-more-policies_other": "" }, "provisioning": { "badge-tooltip-provenance": "", @@ -1911,16 +1933,7 @@ "dismissable-button": "Cerrar" }, "scopes": { - "filters": { - "apply": "", - "cancel": "", - "input": { - "placeholder": "", - "removeAll": "" - }, - "title": "" - }, - "suggestedDashboards": { + "dashboards": { "loading": "", "noResultsForFilter": "", "noResultsForFilterClear": "", @@ -1929,9 +1942,19 @@ "search": "", "toggle": { "collapse": "", + "disabled": "", "expand": "" } }, + "selector": { + "apply": "", + "cancel": "", + "input": { + "placeholder": "", + "removeAll": "" + }, + "title": "" + }, "tree": { "collapse": "", "expand": "", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 19c0e8da1e1..b8614545a5b 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -106,6 +106,7 @@ "export-all": "", "view": "" }, + "contact-point": "", "contact-points": { "delivery-duration": "", "last-delivery-attempt": "", @@ -136,7 +137,21 @@ "saving": "" }, "policies": { + "default-policy": { + "description": "", + "title": "" + }, + "generated-policies": "", "metadata": { + "active-time": "", + "delivered-to": "", + "grouped-by": "", + "grouping": { + "none": "", + "single-group": "" + }, + "inherited": "", + "mute-time": "", "timingOptions": { "groupInterval": { "description": "", @@ -150,8 +165,15 @@ "description": "", "label": "" } - } - } + }, + "n-instances_one": "", + "n-instances_other": "" + }, + "new-child": "", + "new-policy": "", + "no-matchers": "", + "n-more-policies_one": "", + "n-more-policies_other": "" }, "provisioning": { "badge-tooltip-provenance": "", @@ -1911,16 +1933,7 @@ "dismissable-button": "Fermer" }, "scopes": { - "filters": { - "apply": "", - "cancel": "", - "input": { - "placeholder": "", - "removeAll": "" - }, - "title": "" - }, - "suggestedDashboards": { + "dashboards": { "loading": "", "noResultsForFilter": "", "noResultsForFilterClear": "", @@ -1929,9 +1942,19 @@ "search": "", "toggle": { "collapse": "", + "disabled": "", "expand": "" } }, + "selector": { + "apply": "", + "cancel": "", + "input": { + "placeholder": "", + "removeAll": "" + }, + "title": "" + }, "tree": { "collapse": "", "expand": "", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 067128ad03d..c0b22d9ccbf 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -106,6 +106,7 @@ "export-all": "Ēχpőřŧ äľľ", "view": "Vįęŵ" }, + "contact-point": "Cőʼnŧäčŧ Pőįʼnŧ", "contact-points": { "delivery-duration": "Ŀäşŧ đęľįvęřy ŧőőĸ <1>", "last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ", @@ -119,6 +120,9 @@ "used-by_one": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčy", "used-by_other": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčįęş" }, + "contactPointFilter": { + "label": "Cőʼnŧäčŧ pőįʼnŧ" + }, "grafana-rules": { "export-rules": "Ēχpőřŧ řūľęş", "loading": "Ŀőäđįʼnģ...", @@ -136,7 +140,23 @@ "saving": "Ŝävįʼnģ mūŧę ŧįmįʼnģ" }, "policies": { + "default-policy": { + "description": "Åľľ äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę ĥäʼnđľęđ þy ŧĥę đęƒäūľŧ pőľįčy įƒ ʼnő őŧĥęř mäŧčĥįʼnģ pőľįčįęş äřę ƒőūʼnđ.", + "title": "Đęƒäūľŧ pőľįčy" + }, + "generated-policies": "Åūŧő-ģęʼnęřäŧęđ pőľįčįęş", "metadata": { + "active-time": "Åčŧįvę ŵĥęʼn", + "delivered-to": "Đęľįvęřęđ ŧő", + "grouped-by": "Ğřőūpęđ þy", + "grouping": { + "none": "Ńőŧ ģřőūpįʼnģ", + "single-group": "Ŝįʼnģľę ģřőūp" + }, + "inherited": "Ĩʼnĥęřįŧęđ", + "mute-time": "Mūŧęđ ŵĥęʼn", + "n-instances_one": "įʼnşŧäʼnčę", + "n-instances_other": "įʼnşŧäʼnčęş", "timingOptions": { "groupInterval": { "description": "Ħőŵ ľőʼnģ ŧő ŵäįŧ þęƒőřę şęʼnđįʼnģ ä ʼnőŧįƒįčäŧįőʼn äþőūŧ ʼnęŵ äľęřŧş ŧĥäŧ äřę äđđęđ ŧő ä ģřőūp őƒ äľęřŧş ƒőř ŵĥįčĥ äʼn įʼnįŧįäľ ʼnőŧįƒįčäŧįőʼn ĥäş äľřęäđy þęęʼn şęʼnŧ.", @@ -151,7 +171,12 @@ "label": "Ŗępęäŧęđ ęvęřy <1>" } } - } + }, + "n-more-policies_one": "{{count}} äđđįŧįőʼnäľ pőľįčy", + "n-more-policies_other": "{{count}} äđđįŧįőʼnäľ pőľįčįęş", + "new-child": "Ńęŵ čĥįľđ pőľįčy", + "new-policy": "Åđđ ʼnęŵ pőľįčy", + "no-matchers": "Ńő mäŧčĥęřş" }, "provisioning": { "badge-tooltip-provenance": "Ŧĥįş řęşőūřčę ĥäş þęęʼn přővįşįőʼnęđ vįä {{provenance}} äʼnđ čäʼnʼnőŧ þę ęđįŧęđ ŧĥřőūģĥ ŧĥę ŮĨ", @@ -669,18 +694,14 @@ "message": "Ńő đäŧä şőūřčęş ƒőūʼnđ" } }, + "embed": { + "share": { + "time-range-description": "Cĥäʼnģę ŧĥę čūřřęʼnŧ řęľäŧįvę ŧįmę řäʼnģę ŧő äʼn äþşőľūŧę ŧįmę řäʼnģę", + "time-range-label": "Ŧįmę řäʼnģę" + } + }, "explore": { "add-to-dashboard": "Åđđ ŧő đäşĥþőäřđ", - "add-to-library-modal": { - "auto-star": "Åūŧő-şŧäř ŧĥįş qūęřy ŧő äđđ įŧ ŧő yőūř şŧäřřęđ ľįşŧ įʼn ŧĥę Qūęřy Ŀįþřäřy.", - "data-source-name": "Đäŧä şőūřčę ʼnämę", - "data-source-type": "Đäŧä şőūřčę ŧypę", - "description": "Đęşčřįpŧįőʼn", - "info": "Ÿőū'řę äþőūŧ ŧő şävę ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.", - "query": "Qūęřy", - "title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy", - "visibility": "Vįşįþįľįŧy" - }, "logs": { "maximum-pinned-logs": "Mäχįmūm őƒ {{PINNED_LOGS_LIMIT}} pįʼnʼnęđ ľőģş řęäčĥęđ. Ůʼnpįʼn ä ľőģ ŧő äđđ äʼnőŧĥęř.", "no-logs-found": "Ńő ľőģş ƒőūʼnđ.", @@ -688,6 +709,7 @@ "stop-scan": "Ŝŧőp şčäʼn" }, "query-library": { + "add-edit-description": "Åđđ/ęđįŧ đęşčřįpŧįőʼn", "cancel": "Cäʼnčęľ", "default-description": "Pūþľįč", "delete-query": "Đęľęŧę qūęřy", @@ -696,10 +718,26 @@ "private": "Přįväŧę", "public": "Pūþľįč", "query-deleted": "Qūęřy đęľęŧęđ", + "query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy", "query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy", - "query-template-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy", + "query-template-edit-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ęđįŧ ŧĥįş qūęřy", + "query-template-edited": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy ęđįŧęđ", "save": "Ŝävę" }, + "query-template-modal": { + "add-info": "Ÿőū'řę äþőūŧ ŧő şävę ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.", + "add-title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy", + "auto-star": "Åūŧő-şŧäř ŧĥįş qūęřy ŧő äđđ įŧ ŧő yőūř şŧäřřęđ ľįşŧ įʼn ŧĥę Qūęřy Ŀįþřäřy.", + "data-source-name": "Đäŧä şőūřčę ʼnämę", + "description": "Đęşčřįpŧįőʼn", + "edit-info": "Ÿőū'řę äþőūŧ ŧő ęđįŧ ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.", + "edit-title": "Ēđįŧ qūęřy", + "query": "Qūęřy", + "visibility": "Vįşįþįľįŧy" + }, + "query-template-modall": { + "data-source-type": "Đäŧä şőūřčę ŧypę" + }, "rich-history": { "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "datasource-a-z": "Đäŧä şőūřčę Å-Ż", @@ -1018,6 +1056,11 @@ "new-to-question": "Ńęŵ ŧő Ğřäƒäʼnä?" } }, + "logs": { + "infinite-scroll": { + "older-logs": "Øľđęř ľőģş" + } + }, "migrate-to-cloud": { "build-snapshot": { "description": "Ŧĥįş ŧőőľ čäʼn mįģřäŧę şőmę řęşőūřčęş ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn ŧő yőūř čľőūđ şŧäčĸ. Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ŧő čřęäŧę ä şʼnäpşĥőŧ őƒ ŧĥįş įʼnşŧäľľäŧįőʼn. Cřęäŧįʼnģ ä şʼnäpşĥőŧ ŧypįčäľľy ŧäĸęş ľęşş ŧĥäʼn ŧŵő mįʼnūŧęş. Ŧĥę şʼnäpşĥőŧ įş şŧőřęđ äľőʼnģşįđę ŧĥįş Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn.", @@ -1481,6 +1524,10 @@ "subtitle": "Ůşę şęřvįčę äččőūʼnŧş ŧő řūʼn äūŧőmäŧęđ ŵőřĸľőäđş įʼn Ğřäƒäʼnä", "title": "Ŝęřvįčę äččőūʼnŧş" }, + "shared-dashboard": { + "subtitle": "Mäʼnäģę yőūř őřģäʼnįžäŧįőʼn'ş ęχŧęřʼnäľľy şĥäřęđ đäşĥþőäřđş", + "title": "Ŝĥäřęđ đäşĥþőäřđş" + }, "sign-out": { "title": "Ŝįģʼn őūŧ" }, @@ -1562,6 +1609,14 @@ "starred-dashboard": "Đäşĥþőäřđ şŧäřřęđ", "unstarred-dashboard": "Đäşĥþőäřđ ūʼnşŧäřřęđ" }, + "oauth": { + "form": { + "server-discovery-action-button": "Ēʼnŧęř ØpęʼnĨĐ Cőʼnʼnęčŧ Đįşčővęřy ŮŖĿ", + "server-discovery-modal-close": "Cľőşę", + "server-discovery-modal-loading": "Ŀőäđįʼnģ...", + "server-discovery-modal-submit": "Ŝūþmįŧ" + } + }, "panel": { "header-menu": { "copy": "Cőpy", @@ -1634,6 +1689,18 @@ } }, "plugins": { + "details": { + "labels": { + "dependencies": "Đępęʼnđęʼnčįęş", + "downloads": "Đőŵʼnľőäđş", + "from": "Fřőm", + "reportAbuse": "Ŗępőřŧ Åþūşę", + "signature": "Ŝįģʼnäŧūřę", + "status": "Ŝŧäŧūş", + "updatedAt": "Ŀäşŧ ūpđäŧęđ: ", + "version": "Vęřşįőʼn" + } + }, "empty-state": { "message": "Ńő pľūģįʼnş ƒőūʼnđ" } @@ -1694,8 +1761,7 @@ "welcome-title": "Ŵęľčőmę ŧő pūþľįč đäşĥþőäřđş!" }, "delete-modal": { - "revoke-nonorphaned-body-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő řęvőĸę ŧĥįş ŮŖĿ? Ŧĥę đäşĥþőäřđ ŵįľľ ʼnő ľőʼnģęř þę pūþľįč.", - "revoke-orphaned-body-text": "Øřpĥäʼnęđ pūþľįč đäşĥþőäřđ ŵįľľ ʼnő ľőʼnģęř þę pūþľįč.", + "revoke-body-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő řęvőĸę ŧĥįş ŮŖĿ? Ŧĥę đäşĥþőäřđ ŵįľľ ʼnő ľőʼnģęř þę pūþľįč.", "revoke-title": "Ŗęvőĸę pūþľįč ŮŖĿ" }, "email-sharing": { @@ -1794,10 +1860,6 @@ "revoke-button-tooltip": "Ŗęvőĸę pūþľįč đäşĥþőäřđ ŮŖĿ", "view-button-tooltip": "Vįęŵ pūþľįč đäşĥþőäřđ" }, - "dashboard-title": { - "orphaned-title": "<0>Øřpĥäʼnęđ pūþľįč đäşĥþőäřđ", - "orphaned-tooltip": "Ŧĥę ľįʼnĸęđ đäşĥþőäřđ ĥäş äľřęäđy þęęʼn đęľęŧęđ" - }, "empty-state": { "message": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny pūþľįč đäşĥþőäřđş yęŧ", "more-info": "Cřęäŧę ä pūþľįč đäşĥþőäřđ ƒřőm äʼny ęχįşŧįʼnģ đäşĥþőäřđ ŧĥřőūģĥ ŧĥę <1>Ŝĥäřę mőđäľ. <4>Ŀęäřʼn mőřę" @@ -1808,10 +1870,12 @@ }, "public-dashboard-users-access-list": { "dashboard-modal": { + "external-link": "Ēχŧęřʼnäľ ľįʼnĸ", "loading-text": "Ŀőäđįʼnģ...", "open-dashboard-list-text": "Øpęʼn đäşĥþőäřđş ľįşŧ", "public-dashboard-link": "Pūþľįč đäşĥþőäřđ ŮŖĿ", - "public-dashboard-setting": "Pūþľįč đäşĥþőäřđ şęŧŧįʼnģş" + "public-dashboard-setting": "Pūþľįč đäşĥþőäřđ şęŧŧįʼnģş", + "sharing-setting": "Ŝĥäřįʼnģ şęŧŧįʼnģş" }, "delete-user-modal": { "delete-user-button-text": "Đęľęŧę ūşęř", @@ -1821,8 +1885,12 @@ "revoke-user-access-modal-desc-line1": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő řęvőĸę äččęşş ƒőř {{email}}?", "revoke-user-access-modal-desc-line2": "Ŧĥįş äčŧįőʼn ŵįľľ įmmęđįäŧęľy řęvőĸę {{email}}&äpőş;ş äččęşş ŧő äľľ pūþľįč đäşĥþőäřđş." }, + "delete-user-shared-dashboards-modal": { + "revoke-user-access-modal-desc-line2": "Ŧĥįş äčŧįőʼn ŵįľľ įmmęđįäŧęľy řęvőĸę {{email}}&äpőş;ş äččęşş ŧő äľľ şĥäřęđ đäşĥþőäřđş." + }, "modal": { - "dashboard-modal-title": "Pūþľįč đäşĥþőäřđş" + "dashboard-modal-title": "Pūþľįč đäşĥþőäřđş", + "shared-dashboard-modal-title": "Ŝĥäřęđ đäşĥþőäřđş" }, "table-header": { "activated-label": "Åčŧįväŧęđ", @@ -1910,17 +1978,15 @@ }, "dismissable-button": "Cľőşę" }, + "save-dashboards": { + "name-exists": { + "message-info": "Å đäşĥþőäřđ ŵįŧĥ ŧĥę şämę ʼnämę įʼn ŧĥę şęľęčŧęđ ƒőľđęř äľřęäđy ęχįşŧş, įʼnčľūđįʼnģ řęčęʼnŧľy đęľęŧęđ đäşĥþőäřđş.", + "message-suggestion": "Pľęäşę čĥőőşę ä đįƒƒęřęʼnŧ ʼnämę őř ƒőľđęř.", + "title": "Đäşĥþőäřđ ʼnämę äľřęäđy ęχįşŧş" + } + }, "scopes": { - "filters": { - "apply": "Åppľy", - "cancel": "Cäʼnčęľ", - "input": { - "placeholder": "Ŝęľęčŧ şčőpęş...", - "removeAll": "Ŗęmővę äľľ şčőpęş" - }, - "title": "Ŝęľęčŧ şčőpęş" - }, - "suggestedDashboards": { + "dashboards": { "loading": "Ŀőäđįʼnģ đäşĥþőäřđş", "noResultsForFilter": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy", "noResultsForFilterClear": "Cľęäř şęäřčĥ", @@ -1928,10 +1994,20 @@ "noResultsNoScopes": "Ńő şčőpęş şęľęčŧęđ", "search": "Ŝęäřčĥ", "toggle": { - "collapse": "Cőľľäpşę şčőpę ƒįľŧęřş", - "expand": "Ēχpäʼnđ şčőpę ƒįľŧęřş" + "collapse": "Cőľľäpşę şūģģęşŧęđ đäşĥþőäřđş ľįşŧ", + "disabled": "Ŝūģģęşŧęđ đäşĥþőäřđş ľįşŧ įş đįşäþľęđ đūę ŧő řęäđ őʼnľy mőđę", + "expand": "Ēχpäʼnđ şūģģęşŧęđ đäşĥþőäřđş ľįşŧ" } }, + "selector": { + "apply": "Åppľy", + "cancel": "Cäʼnčęľ", + "input": { + "placeholder": "Ŝęľęčŧ şčőpęş...", + "removeAll": "Ŗęmővę äľľ şčőpęş" + }, + "title": "Ŝęľęčŧ şčőpęş" + }, "tree": { "collapse": "Cőľľäpşę", "expand": "Ēχpäʼnđ", @@ -2008,8 +2084,7 @@ "html": "Ēmþęđ ĦŦMĿ", "html-description": "Ŧĥę ĦŦMĿ čőđę þęľőŵ čäʼn þę päşŧęđ äʼnđ įʼnčľūđęđ įʼn äʼnőŧĥęř ŵęþ päģę. Ůʼnľęşş äʼnőʼnymőūş äččęşş įş ęʼnäþľęđ, ŧĥę ūşęř vįęŵįʼnģ ŧĥäŧ päģę ʼnęęđ ŧő þę şįģʼnęđ įʼnŧő Ğřäƒäʼnä ƒőř ŧĥę ģřäpĥ ŧő ľőäđ.", "info": "Ğęʼnęřäŧę ĦŦMĿ ƒőř ęmþęđđįʼnģ äʼn įƒřämę ŵįŧĥ ŧĥįş päʼnęľ.", - "time-range": "Cūřřęʼnŧ ŧįmę řäʼnģę", - "time-range-description": "Ŧřäʼnşƒőřmş ŧĥę čūřřęʼnŧ řęľäŧįvę ŧįmę řäʼnģę ŧő äʼn äþşőľūŧę ŧįmę řäʼnģę" + "time-range": "Ŧįmę řäʼnģę" }, "export": { "back-button": "ßäčĸ ŧő ęχpőřŧ čőʼnƒįģ", @@ -2079,10 +2154,14 @@ }, "share-panel": { "drawer": { - "share-link-title": "Ŀįʼnĸ şęŧŧįʼnģş" + "share-embed-title": "Ŝĥäřę ęmþęđ", + "share-link-title": "Ŀįʼnĸ şęŧŧįʼnģş", + "share-snapshot-title": "Ŝĥäřę şʼnäpşĥőŧ" }, "menu": { - "share-link-title": "Ŝĥäřę ľįʼnĸ" + "share-embed-title": "Ŝĥäřę ęmþęđ", + "share-link-title": "Ŝĥäřę ľįʼnĸ", + "share-snapshot-title": "Ŝĥäřę şʼnäpşĥőŧ" } }, "share-playlist": { @@ -2106,10 +2185,25 @@ } }, "shared-dashboard": { + "delete-modal": { + "revoke-body-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő řęvőĸę ŧĥįş äččęşş? Ŧĥę đäşĥþőäřđ čäʼn ʼnő ľőʼnģęř þę şĥäřęđ.", + "revoke-title": "Ŗęvőĸę äččęşş" + }, "fields": { "timezone-label": "Ŧįmęžőʼnę" } }, + "shared-dashboard-list": { + "button": { + "config-button-tooltip": "Cőʼnƒįģūřę şĥäřęđ đäşĥþőäřđ", + "revoke-button-text": "Ŗęvőĸę äččęşş", + "revoke-button-tooltip": "Ŗęvőĸę äččęşş", + "view-button-tooltip": "Vįęŵ şĥäřęđ đäşĥþőäřđ" + }, + "toggle": { + "pause-sharing-toggle-text": "Päūşę äččęşş" + } + }, "shared-preferences": { "fields": { "home-dashboard-label": "Ħőmę Đäşĥþőäřđ", @@ -2163,12 +2257,15 @@ "info-alert": "Å Ğřäƒäʼnä đäşĥþőäřđ şʼnäpşĥőŧ pūþľįčľy şĥäřęş ä đäşĥþőäřđ ŵĥįľę řęmővįʼnģ şęʼnşįŧįvę đäŧä şūčĥ äş qūęřįęş äʼnđ päʼnęľ ľįʼnĸş, ľęävįʼnģ őʼnľy vįşįþľę męŧřįčş äʼnđ şęřįęş ʼnämęş. Åʼnyőʼnę ŵįŧĥ ŧĥę ľįʼnĸ čäʼn äččęşş ŧĥę şʼnäpşĥőŧ.", "learn-more-button": "Ŀęäřʼn mőřę", "local-button": "Pūþľįşĥ şʼnäpşĥőŧ", - "name-label": "Ŝʼnäpşĥőŧ ʼnämę*", + "name-label": "Ŝʼnäpşĥőŧ ʼnämę", "new-snapshot-button": "Ńęŵ şʼnäpşĥőŧ", "success-creation": "Ÿőūř şʼnäpşĥőŧ ĥäş þęęʼn čřęäŧęđ", "success-delete": "Ÿőūř şʼnäpşĥőŧ ĥäş þęęʼn đęľęŧęđ", "view-all-button": "Vįęŵ äľľ şʼnäpşĥőŧş" }, + "share-panel": { + "info-alert": "Å Ğřäƒäʼnä päʼnęľ şʼnäpşĥőŧ pūþľįčľy şĥäřęş ä päʼnęľ ŵĥįľę řęmővįʼnģ şęʼnşįŧįvę đäŧä şūčĥ äş qūęřįęş äʼnđ päʼnęľ ľįʼnĸş, ľęävįʼnģ őʼnľy vįşįþľę męŧřįčş äʼnđ şęřįęş ʼnämęş. Åʼnyőʼnę ŵįŧĥ ŧĥę ľįʼnĸ čäʼn äččęşş ŧĥę şʼnäpşĥőŧ." + }, "url-column-header": "Ŝʼnäpşĥőŧ ūřľ", "view-button": "Vįęŵ" }, @@ -2297,7 +2394,8 @@ }, "users-access-list": { "tabs": { - "public-dashboard-users-tab-title": "Pūþľįč đäşĥþőäřđ ūşęřş" + "public-dashboard-users-tab-title": "Pūþľįč đäşĥþőäřđ ūşęřş", + "shared-dashboard-users-tab-title": "Ŝĥäřęđ đäşĥþőäřđ ūşęřş" } }, "variable": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 3ec327ece69..9322802313d 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -106,6 +106,7 @@ "export-all": "", "view": "" }, + "contact-point": "", "contact-points": { "delivery-duration": "", "last-delivery-attempt": "", @@ -136,7 +137,21 @@ "saving": "" }, "policies": { + "default-policy": { + "description": "", + "title": "" + }, + "generated-policies": "", "metadata": { + "active-time": "", + "delivered-to": "", + "grouped-by": "", + "grouping": { + "none": "", + "single-group": "" + }, + "inherited": "", + "mute-time": "", "timingOptions": { "groupInterval": { "description": "", @@ -150,8 +165,15 @@ "description": "", "label": "" } - } - } + }, + "n-instances_one": "", + "n-instances_other": "" + }, + "new-child": "", + "new-policy": "", + "no-matchers": "", + "n-more-policies_one": "", + "n-more-policies_other": "" }, "provisioning": { "badge-tooltip-provenance": "", @@ -1911,16 +1933,7 @@ "dismissable-button": "Fechar" }, "scopes": { - "filters": { - "apply": "", - "cancel": "", - "input": { - "placeholder": "", - "removeAll": "" - }, - "title": "" - }, - "suggestedDashboards": { + "dashboards": { "loading": "", "noResultsForFilter": "", "noResultsForFilterClear": "", @@ -1929,9 +1942,19 @@ "search": "", "toggle": { "collapse": "", + "disabled": "", "expand": "" } }, + "selector": { + "apply": "", + "cancel": "", + "input": { + "placeholder": "", + "removeAll": "" + }, + "title": "" + }, "tree": { "collapse": "", "expand": "", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 7a840e4061f..dad8bf901ce 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -106,6 +106,7 @@ "export-all": "", "view": "" }, + "contact-point": "", "contact-points": { "delivery-duration": "", "last-delivery-attempt": "", @@ -135,7 +136,21 @@ "saving": "" }, "policies": { + "default-policy": { + "description": "", + "title": "" + }, + "generated-policies": "", "metadata": { + "active-time": "", + "delivered-to": "", + "grouped-by": "", + "grouping": { + "none": "", + "single-group": "" + }, + "inherited": "", + "mute-time": "", "timingOptions": { "groupInterval": { "description": "", @@ -149,8 +164,13 @@ "description": "", "label": "" } - } - } + }, + "n-instances_other": "" + }, + "new-child": "", + "new-policy": "", + "no-matchers": "", + "n-more-policies_other": "" }, "provisioning": { "badge-tooltip-provenance": "", @@ -1902,16 +1922,7 @@ "dismissable-button": "关闭" }, "scopes": { - "filters": { - "apply": "", - "cancel": "", - "input": { - "placeholder": "", - "removeAll": "" - }, - "title": "" - }, - "suggestedDashboards": { + "dashboards": { "loading": "", "noResultsForFilter": "", "noResultsForFilterClear": "", @@ -1920,9 +1931,19 @@ "search": "", "toggle": { "collapse": "", + "disabled": "", "expand": "" } }, + "selector": { + "apply": "", + "cancel": "", + "input": { + "placeholder": "", + "removeAll": "" + }, + "title": "" + }, "tree": { "collapse": "", "expand": "", diff --git a/public/openapi3.json b/public/openapi3.json index 72c4b53bcd3..f703e5e0bbb 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6304,7 +6304,7 @@ "type": "array" } }, - "title": "Headers represents the configuration for HTTP headers.", + "title": "Header represents the configuration for a single HTTP header.", "type": "object" }, "Headers": { @@ -9559,6 +9559,12 @@ "Route": { "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { + "active_time_intervals": { + "items": { + "type": "string" + }, + "type": "array" + }, "continue": { "type": "boolean" }, @@ -12222,6 +12228,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/components/schemas/alertGroup" }, @@ -25833,9 +25840,6 @@ } }, "description": "NotificationTemplates" - }, - "404": { - "description": " Not found." } }, "summary": "Get all notification templates.", @@ -25911,7 +25915,14 @@ "description": "NotificationTemplate" }, "404": { - "description": " Not found." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericPublicError" + } + } + }, + "description": "GenericPublicError" } }, "summary": "Get a notification template.", @@ -25964,11 +25975,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationError" + "$ref": "#/components/schemas/GenericPublicError" } } }, - "description": "ValidationError" + "description": "GenericPublicError" }, "409": { "content": { diff --git a/public/sass/_angular.scss b/public/sass/_angular.scss index 2b3a07c87cc..d6e75e4d31b 100644 --- a/public/sass/_angular.scss +++ b/public/sass/_angular.scss @@ -3,6 +3,20 @@ @use 'sass:map'; @use 'sass:color'; +@use 'sass:list'; + +// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider. +@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) { + $min: if(map.get($breakpoints, $name) != 0, map.get($breakpoints, $name), null); + @if $min { + @media (min-width: $min) { + @content; + } + } @else { + @content; + } +} // Gradients @mixin gradient-vertical($startColor: #555, $endColor: #333) { @@ -2171,3 +2185,130 @@ $easing: cubic-bezier(0, 0, 0.265, 1); div.flot-text { color: $text-color !important; } + +.modal-header { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-header-close { + float: right; + padding: 9px $spacer; +} + +// Body (where all modal content resides) +.modal-body { + position: relative; +} + +.modal-content { + padding: $spacer * 2; + + &--has-scroll { + max-height: calc(100vh - 400px); + position: relative; + } +} + +// Footer (for actions) +.modal-footer { + padding: 14px 15px 15px; + border-top: 1px solid $panel-bg; + background-color: $panel-bg; + text-align: right; // right align buttons + // clear it in case folks use .pull-* classes on buttons + &::after { + content: ''; + display: table; + clear: both; + } +} + +.confirm-modal { + max-width: 500px; + + .confirm-modal-icon { + padding-top: 41px; + font-size: 280%; + color: $green-base; + padding-bottom: 20px; + } + + .confirm-modal-text { + font-size: $font-size-h4; + color: $link-color; + margin-bottom: $spacer * 2; + padding-top: $spacer; + } + + .confirm-modal-text2 { + font-size: $font-size-base; + padding-top: $spacer; + } + + .confirm-modal-buttons { + margin-bottom: $spacer; + button { + margin-right: calc($spacer/2); + } + } + + .modal-content-confirm-text { + margin-bottom: $space-xl; + span { + text-align: center; + } + } +} + +.gf-form-dropdown-typeahead { + position: relative; + + &::after { + position: absolute; + top: 35%; + right: $space-sm; + background-color: transparent; + color: $input-color; + font: normal normal normal list.slash($font-size-sm, 1) FontAwesome; + content: '\f0d7'; + pointer-events: none; + font-size: 11px; + } +} + +.gf-form-help-icon { + flex-grow: 0; + color: $text-color-weak; + + &--bold { + color: $text-color-emphasis; + padding-left: 0; + } + + &--right-absolute { + position: absolute; + right: $spacer; + top: 6px; + } + + &--right-normal { + float: right; + } + + &--header { + margin-bottom: $space-xxs; + } + + &--small-padding { + padding-left: 4px; + } + + &:hover { + color: $text-color; + } +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 272f946c31a..127d6ead988 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,6 +1,3 @@ -// MIXINS -@import 'mixins/breakpoints'; - // BASE @import 'base/reboot'; @import 'base/forms'; @@ -13,10 +10,7 @@ // COMPONENTS @import 'components/buttons'; -@import 'components/gf-form'; -@import 'components/modals'; @import 'components/dropdown'; -@import 'components/dashboard_grid'; // ANGULAR @import 'angular'; diff --git a/public/sass/base/_grid.scss b/public/sass/base/_grid.scss index 481d04f799a..75deba50990 100644 --- a/public/sass/base/_grid.scss +++ b/public/sass/base/_grid.scss @@ -1,4 +1,18 @@ @use 'sass:math'; +@use 'sass:map'; + +// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider. +@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) { + $min: if(map.get($breakpoints, $name) != 0, map.get($breakpoints, $name), null); + @if $min { + @media (min-width: $min) { + @content; + } + } @else { + @content; + } +} /// Grid system // diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss deleted file mode 100644 index 7185a2a9be2..00000000000 --- a/public/sass/components/_dashboard_grid.scss +++ /dev/null @@ -1,106 +0,0 @@ -@import 'react-grid-layout/css/styles'; -@import 'react-resizable/css/styles'; - -.react-resizable-handle { - // this needs to use visibility and not display none in order not to cause resize flickering - visibility: hidden; -} - -.react-grid-item, -#grafana-portal-container { - touch-action: initial !important; - - &:hover { - .react-resizable-handle { - visibility: visible; - } - } -} - -.panel-in-fullscreen { - .react-grid-layout { - height: auto !important; - } - .react-grid-item { - display: none !important; - transition-property: none !important; - - &--fullscreen { - display: block !important; - position: unset !important; - transform: translate(0px, 0px) !important; - } - } - - // Disable grid interaction indicators in fullscreen panels - .panel-header:hover { - background-color: inherit; - } - - .panel-title-container { - cursor: pointer; - } - - .react-resizable-handle { - display: none; - } - - // the react-grid has a height transition - .react-grid-layout { - transition-property: none; - } -} - -@include media-breakpoint-down(sm) { - .react-grid-item { - display: block !important; - transition-property: none !important; - position: unset !important; - transform: translate(0px, 0px) !important; - margin-bottom: $space-md; - } - .panel-repeater-grid-item { - height: auto !important; - } -} - -.react-grid-item.react-grid-placeholder { - box-shadow: $panel-grid-placeholder-shadow; - background: $panel-grid-placeholder-bg; - z-index: -1; - opacity: unset; -} - -.theme-dark { - .react-grid-item > .react-resizable-handle::after { - border-right: 2px solid $gray-1; - border-bottom: 2px solid $gray-1; - } -} - -.theme-light { - .react-grid-item > .react-resizable-handle::after { - border-right: 2px solid $gray-3; - border-bottom: 2px solid $gray-3; - } -} - -// Hack for preventing panel menu overlapping. -.react-grid-item.resizing.panel, -.react-grid-item.panel.dropdown-menu-open, -.react-grid-item.react-draggable-dragging.panel { - z-index: $zindex-dropdown; -} - -// Disable animation on initial rendering and enable it when component has been mounted. -.react-grid-item.cssTransforms { - transition-property: none; -} - -@media (prefers-reduced-motion: no-preference) { - .react-grid-layout--enable-move-animations { - .react-grid-item.cssTransforms { - transition-property: transform; - } - } -} diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss deleted file mode 100644 index 04376ed5c8d..00000000000 --- a/public/sass/components/_gf-form.scss +++ /dev/null @@ -1,484 +0,0 @@ -@use 'sass:list'; -$input-border: 1px solid $input-border-color; - -@mixin form-control-focus() { - &:focus { - border-color: $input-border-focus; - outline: none; - } -} - -.gf-form { - display: flex; - flex-direction: row; - align-items: flex-start; - text-align: left; - position: relative; - margin-bottom: $space-xs; - - &--offset-1 { - margin-left: $spacer; - } - - &--grow { - flex-grow: 1; - } - - &--flex-end { - justify-content: flex-end; - } - - &--align-center { - align-content: center; - } - - &--alt { - flex-direction: column; - align-items: flex-start; - - .gf-form-label { - padding: 4px 0; - } - } -} - -.gf-form--has-input-icon { - position: relative; - margin-right: $space-xs; - - .gf-form-input-icon { - position: absolute; - top: 8px; - font-size: $font-size-lg; - left: 10px; - color: $input-color-placeholder; - } - - > input { - padding-left: 35px; - - &:focus + .gf-form-input-icon { - color: $text-muted; - } - } - - .Select--multi .Select-multi-value-wrapper, - .Select-placeholder { - padding-left: 30px; - } -} - -.gf-form-disabled { - color: $text-color-weak; - - .gf-form-select-wrapper::after { - color: $text-color-weak; - } - - a, - .gf-form-input { - color: $text-color-weak; - } -} - -.gf-form-group { - margin-bottom: $spacer * 2.5; -} - -.gf-form-inline { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-content: flex-start; - - &--nowrap { - flex-wrap: nowrap; - } - - &--xs-view-flex-column { - flex-direction: row; - flex-wrap: nowrap; - @include media-breakpoint-down(xs) { - flex-direction: column; - } - } - - .select-container { - margin-right: $space-xs; - } - - .gf-form-spacing { - margin-right: $space-xs; - } -} - -.gf-form-button-row { - padding-top: $spacer * 1.5; - a, - button { - margin-right: $spacer; - } -} - -.gf-form-label { - display: flex; - align-items: center; - padding: $input-padding; - flex-shrink: 0; - font-weight: $font-weight-semi-bold; - font-size: $font-size-sm; - background-color: $input-label-bg; - height: $input-height; - line-height: $input-height; - margin-right: $space-xs; - border-radius: $input-border-radius; - justify-content: space-between; - border: none; - - &--grow { - flex-grow: 1; - } - - &--error { - color: $critical; - } - - &--transparent { - background-color: transparent; - border: 0; - text-align: right; - padding-left: 0px; - } - - &--variable { - color: $variable; - background: $panel-bg; - border: $panel-border; - } - - &--dashlink { - background: $panel-bg; - border: $panel-border; - } - - &--justify-left { - justify-content: left; - } - - &--btn { - border: none; - border-radius: $border-radius; - - &:hover { - background: $list-item-hover-bg; - color: $link-color; - } - } - - &:disabled { - color: $text-color-weak; - } -} - -.gf-form-label + .gf-form-label { - margin-right: $space-xs; -} - -.gf-form-pre { - display: block !important; - flex-grow: 1; - margin: 0; - margin-right: $space-xs; - border: $border-width solid transparent; - border-left: none; - border-radius: $input-border-radius; -} - -.gf-form-textarea { - max-width: 650px; -} - -.gf-form-input { - display: block; - width: 100%; - height: $input-height; - padding: $input-padding; - font-size: $font-size-md; - line-height: $input-line-height; - color: $input-color; - background-color: $input-bg; - background-image: none; - background-clip: padding-box; - border: $input-border; - border-radius: $input-border-radius; - margin-right: $space-xs; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - // text areas should be scrollable - @at-root textarea#{&} { - overflow: auto; - white-space: pre-wrap; - padding: 6px $space-sm; - min-height: $input-height; - height: auto; - } - - // Unstyle the caret on ` `Use: ${v}`} + onFocus={() => { + // Delay loading until we click on the name + setFocus(true); + }} + options={options} + isLoading={loading} + isClearable={true} + defaultOptions + value={value} + onChange={(v: SelectableValue) => { + props.onChange(v?.value ?? ''); + }} + onCreateOption={(v) => { + props.onChange(v); + }} + /> + ); + } + return ; + }} + + ); + }} + + ); +} diff --git a/public/swagger/SwaggerPage.tsx b/public/swagger/SwaggerPage.tsx new file mode 100644 index 00000000000..f32033cc206 --- /dev/null +++ b/public/swagger/SwaggerPage.tsx @@ -0,0 +1,105 @@ +import getDefaultMonacoLanguages from 'lib/monaco-languages'; +import { useState } from 'react'; +import { useAsync } from 'react-use'; +import SwaggerUI from 'swagger-ui-react'; + +import { createTheme, monacoLanguageRegistry, SelectableValue } from '@grafana/data'; +import { Stack, Select } from '@grafana/ui'; +import { setMonacoEnv } from 'app/core/monacoEnv'; +import { ThemeProvider } from 'app/core/utils/ConfigProvider'; + +import { NamespaceContext, WrappedPlugins } from './plugins'; + +export const Page = () => { + const theme = createTheme({ colors: { mode: 'light' } }); + const [url, setURL] = useState>(); + const urls = useAsync(async () => { + const v2 = { label: 'Grafana API (OpenAPI v2)', key: 'openapi2', value: 'public/api-merged.json' }; + const v3 = { label: 'Grafana API (OpenAPI v3)', key: 'openapi3', value: 'public/openapi3.json' }; + const urls: Array> = [v2, v3]; + + const rsp = await fetch('openapi/v3'); + const apis = await rsp.json(); + for (const [key, val] of Object.entries(apis.paths)) { + const parts = key.split('/'); + if (parts.length === 3) { + urls.push({ + key: `${parts[1]}-${parts[2]}`, + label: `${parts[1]}/${parts[2]}`, + value: val.serverRelativeURL.substring(1), // remove initial slash + }); + } + } + + let idx = 0; + const urlParams = new URLSearchParams(window.location.search); + const api = urlParams.get('api'); + if (api) { + urls.forEach((url, i) => { + if (url.key === api) { + idx = i; + } + }); + } + + monacoLanguageRegistry.setInit(getDefaultMonacoLanguages); + setMonacoEnv(); + + setURL(urls[idx]); // Remove to start at the generic landing page + return urls; + }); + + const namespace = useAsync(async () => { + const response = await fetch('api/frontend/settings'); + if (!response.ok) { + console.warn('No settings found'); + return ''; + } + const val = await response.json(); + return val.namespace; + }); + + return ( +
+ + +
+ + Grafana +