Compare commits

...

13 Commits

Author SHA1 Message Date
Torkel Ödegaard
d666c4e2a7 Update 2025-12-18 17:11:01 +01:00
Torkel Ödegaard
5012b4079e Dashboards: Do not show alert rules button for new dashboads 2025-12-18 16:19:33 +01:00
Andreas Christou
f1b19dd9fa ElasticSearch: Update annotation time-range properties (#115500)
Update time-range properties
2025-12-18 14:38:15 +00:00
Marc M.
4fbcebac2c Deps: Upgrade Scenes to v6.51.0 (#115547)
Scenes: Upgrade to v6.51.0
2025-12-18 15:01:04 +01:00
Alexander Akhmetov
5c7cdabaa3 Alerting: Improve performance of rule list view with limit_alerts=0 (#115548)
Alerting: Improve performance of rule list view
2025-12-18 14:58:39 +01:00
Kevin Minehart
39fa6559ee CI: Remove the default alpine & ubuntu versions so that the ones in Dockerfile (#115544)
* Remove the default alpine & ubuntu versions so that the ones in Dockerfile are used

* set default to just 'alpine' or 'ubuntu'

* use defaults instead
2025-12-18 14:46:24 +01:00
Mariell Hoversholm
14ef6ca4eb docs: remove SECURITY.md (#115549) 2025-12-18 14:23:07 +01:00
Rafael Bortolon Paulovic
90af2c3c3b fix(dashboard): panic on nil logger on dashboard accessor (#115545)
fix(dashboard): fix panic on log
2025-12-18 14:11:47 +01:00
Andre Pereira
241fd69e02 Trace View: Correctly handle span and service name in span filters (#115215)
* Correctly handle span name and service name in trace view span filters

* Consistency and fix test

* i18n extract
2025-12-18 12:38:50 +00:00
Roberto Jiménez Sánchez
e29bb47e95 Provisioning: Add Git Sync limitations warning and migrate resources checkbox (#115532)
* Provisioning: Add Git Sync limitations warning and migrate resources checkbox

- Update SynchronizeStep alert to use warning severity with comprehensive Git Sync limitations
- Add conditional warnings for instance sync (permissions loss, alerts/library panels loss)
- Add conditional warnings for folder sync (folder structure changes, manual cleanup needed)
- Add "Migrate existing resources" checkbox for folder sync mode
- Update useCreateSyncJob hook to handle migrateResources option for folder sync
- Extract i18n translations for new strings

* Simplify createSyncJob: calculate requiresMigration in caller

- Remove syncTarget and migrateResources parameters from useCreateSyncJob hook
- Calculate requiresMigration in SynchronizeStep based on sync target and checkbox value
- Pass requiresMigration as parameter to createSyncJob function

* Revert: Pass requiresMigration as hook parameter

- Calculate requiresMigration in SynchronizeStep using useMemo
- Pass requiresMigration to useCreateSyncJob hook
- Remove parameter from createSyncJob function call

* Revert "Revert: Pass requiresMigration as hook parameter"

This reverts commit 97e3b7107d.

* Fix TypeScript errors in ProvisioningWizard

- Remove requiresMigration from useCreateSyncJob call
- Pass requiresMigration parameter to createSyncJob call
- Remove unused Target import from SynchronizeStep

* Show migrate resources checkbox for instance sync (checked and disabled)

- Display checkbox for both instance and folder sync
- For instance sync: checkbox is checked and disabled with explanation
- For instance sync: automatically set migrateResources to true via useEffect
- Update description to explain instance sync requires all resources to be managed

* Extract i18n translations for instance-migrate-resources-description

* Rename 'Synchronization options' to 'Options'

* Update i18n translations: rename synchronization-options to options

* Remove unnecessary conditional check for sync target

* Add bodySmall variant to announcement banner TextLink

* Move requiresMigration calculation logic into useResourceStats hook

- Add migrateResources parameter to useResourceStats hook
- Calculate final requiresMigration in hook based on sync target and checkbox value
- Use watch instead of getValues to reactively get migrateResources value
- Simplify startSynchronization to use requiresMigration from hook
2025-12-18 12:16:57 +00:00
Jack Westbrook
5bedcc7bd7 Frontend: use custom conditions for development and build (#111685)
* build(frontend): enable custom condition for resolving source files during dev and build

* feat(packages): apply conditional name to export properties

* chore(packages): add standard exports to flamegraph and prometheus

* chore(packages): resolve main, module, types to built files

* build(packages): clean up prepare-npm-package for custom condition changes

* refactor(packages): reduce repetition in conditional exports

* build(storybook): add @grafana-app/source to conditionNames

* test(frontend): add grafana-app/source customCondition for jest tests

* refactor(frontend): remove nested package import paths

* chore(jest): use customExportConditions for source files and browser

* chore(i18n): use src for ./eslint-plugin export

* chore(packages): set packages tsconfigs to moduleResolution bundler

* chore(packages): fix rollup builds

* build(packages): build cjs as multiple files

* chore(sql): reference MonitoringLogger for moduleresolution bundler to pass typecheck

* chore(ui): add type refs for moduleresolution bundler to pass typecheck

* feat(schema): add exports for cleaner import paths

* refactor(frontend): clean up schema paths to point to exports instead of nested file paths

* build(storybook): hack the builder-manager for custom conditions to resolve

* build(decoupled-plugins): fix broken builds due to missing conditionNames

* chore(e2e): pass condition to playwright to resolve local packages

* build(frontend): fix failing build

* chore(select): fix typings

* style(frontend): clean up eslint suppressions

* chore(packages): fix type errors due to incorrect tsconfig settings

* build(generate-apis): use swc with ts-node and moduleResolution bundler

* chore(cypress): add conditionNames to resolve monorepo packages

* build(npm): update prepare to work with latest exports changes

* build(packages): fix prepare-npm-package script

* fix(e2e-selectors): update debugoverlay for data-testid change

* build(packages): stop editing package.json at pack n publish time

* rerun ci

* chore(api-clients): use moduleResolution: bundler for customConditions support

* chore(api-clients): fix generation

* build(packages): remove aliasing exports, remove exports with only customConditions

* Revert "refactor(frontend): clean up schema paths to point to exports instead of nested file paths"

This reverts commit 7949b6ea0e60e51989d2a8149b7a24647cd68916.

* revert(schema): remove exports from package so builds work

* build(api-clients): fix up api-clients exports and rollup config

* build(api-clients): Update generated package exports for api clients

* build(schema): add overrides to cjsOutput and esmOutput so built directory structure is correct

* fix(packages): use rootDirs to prevent types/src directories in built d.ts file paths

* build(packages): prevent empty exports added to package.json during pack

* docs(packages): update readme with custom conditions information

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
2025-12-18 11:47:38 +01:00
Gonzalo Trigueros Manzanas
2123099e88 Provisioning: escape URLs in PR comments to avoid malformed markdown. (#115486)
provisioning: escape URLs in webhook changes to allow for proper markdown.
2025-12-18 10:28:10 +00:00
Torkel Ödegaard
f75b5654c9 Modal: Fix modal button row (#115483)
* Modal: Fix modal button row

* update

* update
2025-12-18 11:20:48 +01:00
107 changed files with 745 additions and 1354 deletions

1
.github/CODEOWNERS vendored
View File

@@ -24,7 +24,6 @@
/NOTICE.md @torkelo
/README.md @grafana/docs-grafana
/ROADMAP.md @torkelo
/SECURITY.md @grafana/security-team
/SUPPORT.md @torkelo
/WORKFLOW.md @torkelo
/contribute/ @grafana/grafana-community-support

View File

@@ -82,14 +82,6 @@ inputs:
description: Docker registry of produced images
default: docker.io
required: false
ubuntu-base:
type: string
default: 'ubuntu:22.04'
required: false
alpine-base:
type: string
default: 'alpine:3.22'
required: false
outputs:
dist-dir:
description: Directory where artifacts are placed
@@ -134,13 +126,11 @@ runs:
UBUNTU_TAG_FORMAT: ${{ inputs.docker-tag-format-ubuntu }}
CHECKSUM: ${{ inputs.checksum }}
VERIFY: ${{ inputs.verify }}
ALPINE_BASE: ${{ inputs.alpine-base }}
UBUNTU_BASE: ${{ inputs.ubuntu-base }}
with:
verb: run
dagger-flags: --verbose=0
version: 0.18.8
args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --alpine-base=${ALPINE_BASE} --ubuntu-base=${UBUNTU_BASE} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE
args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE
- id: output
shell: bash
env:

View File

@@ -0,0 +1,13 @@
diff --git a/dist/builder-manager/index.js b/dist/builder-manager/index.js
index 3d7f9b213dae1801bda62b31db31b9113e382ccd..212501c63d20146c29db63fb0f6300c6779eecb5 100644
--- a/dist/builder-manager/index.js
+++ b/dist/builder-manager/index.js
@@ -1970,7 +1970,7 @@ var pa = /^\/($|\?)/, G, C, xt = /* @__PURE__ */ o(async (e) => {
bundle: !0,
minify: !0,
sourcemap: !1,
- conditions: ["browser", "module", "default"],
+ conditions: ["@grafana-app/source", "browser", "module", "default"],
jsxFactory: "React.createElement",
jsxFragment: "React.Fragment",
jsx: "transform",

View File

@@ -1,29 +0,0 @@
# Reporting security issues
If you think you have found a security vulnerability, we have two routes for reporting security issues.
Important: Whichever route you choose, we ask you to not disclose the vulnerability before it has been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so.
[Full guidance on reporting a security issue can be found here](https://grafana.com/legal/report-a-security-issue/).
This product is in scope for our Bug Bounty Program. To submit a vulnerability report, please visit [Grafana Labs Bug Bounty page](https://app.intigriti.com/programs/grafanalabs/grafanaossbbp/detail) and follow the instructions provided. Our security team will review your submission and get back to you as soon as possible.
---
For products and services outside the scope of our bug bounty program, or if you do not wish to receive a bounty, you can report issues directly to us via email at security@grafana.com. This address can be used for all of Grafana Labs open source and commercial products (including but not limited to Grafana, Grafana Cloud, Grafana Enterprise, and grafana.com).
Please encrypt your message to us; please use our PGP key. The key fingerprint is:
225E 6A9B BB15 A37E 95EB 6312 C66A 51CC B44C 27E0
The key is available from [keyserver.ubuntu.com](https://keyserver.ubuntu.com/pks/lookup?search=0x225E6A9BBB15A37E95EB6312C66A51CCB44C27E0&fingerprint=on&op=index).
Grafana Labs will send you a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
**Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so.
## Security announcements
We will post a summary, remediation, and mitigation details for any patch containing security fixes on the Grafana blog. The security announcement blog posts will be tagged with the [security tag](https://grafana.com/tags/security/).
You can also track security announcements via the [RSS feed](https://grafana.com/tags/security/index.xml).

View File

@@ -18,6 +18,7 @@ const webpackOptions = {
},
resolve: {
extensions: ['.ts', '.js'],
conditionNames: ['@grafana-app/source', '...'],
},
};

View File

@@ -4246,9 +4246,6 @@
}
},
"public/app/plugins/panel/geomap/components/DebugOverlay.tsx": {
"@grafana/no-aria-label-selectors": {
"count": 1
},
"react-prefer-function-component/react-prefer-function-component": {
"count": 1
}

View File

@@ -40,6 +40,9 @@ const esModules = [
module.exports = {
verbose: false,
testEnvironment: 'jsdom',
testEnvironmentOptions: {
customExportConditions: ['@grafana-app/source', 'browser'],
},
transform: {
'^.+\\.(ts|tsx|js|jsx)$': [require.resolve('ts-jest')],
},

View File

@@ -26,10 +26,10 @@
"e2e:enterprise": "./e2e/start-and-run-suite enterprise",
"e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev",
"e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug",
"e2e:playwright": "yarn playwright test --grep-invert @cloud-plugins",
"e2e:playwright:cloud-plugins": "yarn playwright test --grep @cloud-plugins",
"e2e:playwright:storybook": "yarn playwright test -c playwright.storybook.config.ts",
"e2e:acceptance": "yarn playwright test --grep @acceptance",
"e2e:playwright": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep-invert @cloud-plugins",
"e2e:playwright:cloud-plugins": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep @cloud-plugins",
"e2e:playwright:storybook": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test -c playwright.storybook.config.ts",
"e2e:acceptance": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep @acceptance",
"e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true",
"e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'",
"e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100",
@@ -63,7 +63,7 @@
"storybook": "yarn workspace @grafana/ui storybook --ci",
"storybook:build": "yarn workspace @grafana/ui storybook:build",
"themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json",
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
"themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'",
"typecheck": "tsc --noEmit && yarn run packages:typecheck",
"plugins:build-bundled": "echo 'bundled plugins are no longer supported'",
@@ -295,8 +295,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "6.50.0",
"@grafana/scenes-react": "6.50.0",
"@grafana/scenes": "^6.51.0",
"@grafana/scenes-react": "^6.51.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -460,7 +460,8 @@
"tmp@npm:^0.0.33": "~0.2.1",
"js-yaml@npm:4.1.0": "^4.1.0",
"js-yaml@npm:=4.1.0": "^4.1.0",
"nodemailer": "7.0.7"
"nodemailer": "7.0.7",
"@storybook/core@npm:8.6.2": "patch:@storybook/core@npm%3A8.6.2#~/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch"
},
"workspaces": {
"packages": [

View File

@@ -2,13 +2,32 @@
## Exporting code conventions
`@grafana/ui`, `@grafana/data` and `@grafana/runtime` makes use of `exports` in package.json to define three entrypoints that Grafana core and Grafana plugins can access. Before exposing anything in these packages please consider the table below to better understand the use case of each export.
All the `@grafana` packages in this repo (except `@grafana/schema`) make use of `exports` in package.json to define entrypoints that Grafana core and Grafana plugins can access. Exports can also be used to restrict access to internal files in packages.
| Export Name | Import Path | Description | Available to Grafana | Available to plugins |
| ------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------- |
| `./` | `@grafana/ui` | The public API entrypoint. If the code is stable and you want to share it everywhere, this is the place to export it. | ✅ | ✅ |
| `./unstable` | `@grafana/ui/unstable` | The public API entrypoint for all experimental code. If you want to iterate and test code from Grafana and plugins, this is the place to export it. | ✅ | ✅ |
| `./internal` | `@grafana/ui/internal` | The private API entrypoint for internal code shared with Grafana. If you need to import code in Grafana but don't want to expose it to plugins, this is the place to export it. | ✅ | ❌ |
Package authors are free to create as many exports as they like but should consider the following points:
1. Resolution of source code within this repo is handled by the [customCondition](https://www.typescriptlang.org/tsconfig/#customConditions) `@grafana-app/source`. This allows the frontend tooling in this repo to resolve to the source code preventing the need to build all the packages up front. When adding exports it is important to add an entry for the custom condition as the first item. All other entries should point to the built, bundled files. For example:
```json
"exports": {
".": {
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
}
```
2. If you add exports to your package you must export the `package.json` file.
3. Before exposing anything in these packages please consider the table below to better understand the conventions we have put in place for most of the packages in this repository.
| Export Name | Import Path | Description | Available to Grafana | Available to plugins |
| ------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------- |
| `./` | `@grafana/ui` | The public API entrypoint. If the code is stable and you want to share it everywhere, this is the place to export it. | ✅ | ✅ |
| `./unstable` | `@grafana/ui/unstable` | The public API entrypoint for all experimental code. If you want to iterate and test code from Grafana and plugins, this is the place to export it. | ✅ | ✅ |
| `./internal` | `@grafana/ui/internal` | The private API entrypoint for internal code shared with Grafana. If you want to co-locate code in a package with it's public API but only want the Grafana application to access it, this is the place to export it. | ✅ | ❌ |
## Versioning

View File

@@ -17,32 +17,34 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-alerting"
},
"main": "src/index.ts",
"types": "src/index.ts",
"module": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
},
"./internal": {
"import": "./src/internal.ts",
"require": "./src/internal.ts"
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
},
"./unstable": {
"import": "./src/unstable.ts",
"require": "./src/unstable.ts"
"@grafana-app/source": "./src/unstable.ts",
"types": "./dist/types/unstable.d.ts",
"import": "./dist/esm/unstable.mjs",
"require": "./dist/cjs/unstable.cjs"
},
"./internal": {
"@grafana-app/source": "./src/internal.ts"
},
"./testing": {
"import": "./src/testing.ts",
"require": "./src/testing.ts"
"@grafana-app/source": "./src/testing.ts",
"types": "./dist/types/testing.d.ts",
"import": "./dist/esm/testing.mjs",
"require": "./dist/cjs/testing.cjs"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
@@ -57,8 +59,8 @@
"clean": "rimraf ./dist ./compiled ./unstable ./testing ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts",
"prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=testing,unstable node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json && rimraf ./unstable ./testing",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json",
"i18n-extract": "i18next-cli extract --sync-primary"
},
"devDependencies": {

View File

@@ -9,19 +9,19 @@ export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')],
output: [cjsOutput(pkg, 'grafana-alerting'), esmOutput(pkg, 'grafana-alerting')],
treeshake: false,
},
{
input: 'src/unstable.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')],
output: [cjsOutput(pkg, 'grafana-alerting'), esmOutput(pkg, 'grafana-alerting')],
treeshake: false,
},
{
input: 'src/testing.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')],
output: [cjsOutput(pkg, 'grafana-alerting'), esmOutput(pkg, 'grafana-alerting')],
treeshake: false,
},
];

View File

@@ -15,88 +15,121 @@
"url": "https://github.com/grafana/grafana.git",
"directory": "packages/grafana-api-clients"
},
"main": "src/index.ts",
"module": "src/index.ts",
"types": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
},
"./rtkq": {
"import": "./src/clients/rtkq/index.ts",
"require": "./src/clients/rtkq/index.ts"
"@grafana-app/source": "./src/clients/rtkq/index.ts",
"types": "./dist/types/clients/rtkq/index.d.ts",
"import": "./dist/esm/clients/rtkq/index.mjs",
"require": "./dist/cjs/clients/rtkq/index.cjs"
},
"./rtkq/advisor/v0alpha1": {
"import": "./src/clients/rtkq/advisor/v0alpha1/index.ts",
"require": "./src/clients/rtkq/advisor/v0alpha1/index.ts"
},
"./rtkq/correlations/v0alpha1": {
"import": "./src/clients/rtkq/correlations/v0alpha1/index.ts",
"require": "./src/clients/rtkq/correlations/v0alpha1/index.ts"
},
"./rtkq/dashboard/v0alpha1": {
"import": "./src/clients/rtkq/dashboard/v0alpha1/index.ts",
"require": "./src/clients/rtkq/dashboard/v0alpha1/index.ts"
},
"./rtkq/folder/v1beta1": {
"import": "./src/clients/rtkq/folder/v1beta1/index.ts",
"require": "./src/clients/rtkq/folder/v1beta1/index.ts"
},
"./rtkq/iam/v0alpha1": {
"import": "./src/clients/rtkq/iam/v0alpha1/index.ts",
"require": "./src/clients/rtkq/iam/v0alpha1/index.ts"
},
"./rtkq/legacy": {
"import": "./src/clients/rtkq/legacy/index.ts",
"require": "./src/clients/rtkq/legacy/index.ts"
},
"./rtkq/legacy/migrate-to-cloud": {
"import": "./src/clients/rtkq/migrate-to-cloud/index.ts",
"require": "./src/clients/rtkq/migrate-to-cloud/index.ts"
},
"./rtkq/legacy/preferences": {
"import": "./src/clients/rtkq/preferences/user/index.ts",
"require": "./src/clients/rtkq/preferences/user/index.ts"
},
"./rtkq/legacy/user": {
"import": "./src/clients/rtkq/user/index.ts",
"require": "./src/clients/rtkq/user/index.ts"
},
"./rtkq/playlist/v0alpha1": {
"import": "./src/clients/rtkq/playlist/v0alpha1/index.ts",
"require": "./src/clients/rtkq/playlist/v0alpha1/index.ts"
},
"./rtkq/preferences/v1alpha1": {
"import": "./src/clients/rtkq/preferences/v1alpha1/index.ts",
"require": "./src/clients/rtkq/preferences/v1alpha1/index.ts"
"@grafana-app/source": "./src/clients/rtkq/advisor/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/advisor/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/advisor/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/advisor/v0alpha1/index.cjs"
},
"./rtkq/collections/v1alpha1": {
"import": "./src/clients/rtkq/collections/v1alpha1/index.ts",
"require": "./src/clients/rtkq/collections/v1alpha1/index.ts"
"@grafana-app/source": "./src/clients/rtkq/collections/v1alpha1/index.ts",
"types": "./dist/types/clients/rtkq/collections/v1alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/collections/v1alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/collections/v1alpha1/index.cjs"
},
"./rtkq/correlations/v0alpha1": {
"@grafana-app/source": "./src/clients/rtkq/correlations/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/correlations/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/correlations/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/correlations/v0alpha1/index.cjs"
},
"./rtkq/dashboard/v0alpha1": {
"@grafana-app/source": "./src/clients/rtkq/dashboard/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/dashboard/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/dashboard/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/dashboard/v0alpha1/index.cjs"
},
"./rtkq/folder/v1beta1": {
"@grafana-app/source": "./src/clients/rtkq/folder/v1beta1/index.ts",
"types": "./dist/types/clients/rtkq/folder/v1beta1/index.d.ts",
"import": "./dist/esm/clients/rtkq/folder/v1beta1/index.mjs",
"require": "./dist/cjs/clients/rtkq/folder/v1beta1/index.cjs"
},
"./rtkq/iam/v0alpha1": {
"@grafana-app/source": "./src/clients/rtkq/iam/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/iam/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/iam/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/iam/v0alpha1/index.cjs"
},
"./rtkq/legacy": {
"@grafana-app/source": "./src/clients/rtkq/legacy/index.ts",
"types": "./dist/types/clients/rtkq/legacy/index.d.ts",
"import": "./dist/esm/clients/rtkq/legacy/index.mjs",
"require": "./dist/cjs/clients/rtkq/legacy/index.cjs"
},
"./rtkq/legacy/migrate-to-cloud": {
"@grafana-app/source": "./src/clients/rtkq/migrate-to-cloud/index.ts",
"types": "./dist/types/clients/rtkq/migrate-to-cloud/index.d.ts",
"import": "./dist/esm/clients/rtkq/migrate-to-cloud/index.mjs",
"require": "./dist/cjs/clients/rtkq/migrate-to-cloud/index.cjs"
},
"./rtkq/legacy/preferences": {
"@grafana-app/source": "./src/clients/rtkq/preferences/user/index.ts",
"types": "./dist/types/clients/rtkq/preferences/user/index.d.ts",
"import": "./dist/esm/clients/rtkq/preferences/user/index.mjs",
"require": "./dist/cjs/clients/rtkq/preferences/user/index.cjs"
},
"./rtkq/legacy/user": {
"@grafana-app/source": "./src/clients/rtkq/user/index.ts",
"types": "./dist/types/clients/rtkq/user/index.d.ts",
"import": "./dist/esm/clients/rtkq/user/index.mjs",
"require": "./dist/cjs/clients/rtkq/user/index.cjs"
},
"./rtkq/playlist/v0alpha1": {
"@grafana-app/source": "./src/clients/rtkq/playlist/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/playlist/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/playlist/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/playlist/v0alpha1/index.cjs"
},
"./rtkq/preferences/v1alpha1": {
"@grafana-app/source": "./src/clients/rtkq/preferences/v1alpha1/index.ts",
"types": "./dist/types/clients/rtkq/preferences/v1alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/preferences/v1alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/preferences/v1alpha1/index.cjs"
},
"./rtkq/provisioning/v0alpha1": {
"import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts",
"require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts"
"@grafana-app/source": "./src/clients/rtkq/provisioning/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/provisioning/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/provisioning/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/provisioning/v0alpha1/index.cjs"
},
"./rtkq/shorturl/v1beta1": {
"import": "./src/clients/rtkq/shorturl/v1beta1/index.ts",
"require": "./src/clients/rtkq/shorturl/v1beta1/index.ts"
"@grafana-app/source": "./src/clients/rtkq/shorturl/v1beta1/index.ts",
"types": "./dist/types/clients/rtkq/shorturl/v1beta1/index.d.ts",
"import": "./dist/esm/clients/rtkq/shorturl/v1beta1/index.mjs",
"require": "./dist/cjs/clients/rtkq/shorturl/v1beta1/index.cjs"
},
"./rtkq/historian.alerting/v0alpha1": {
"import": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts",
"require": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts"
"@grafana-app/source": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts",
"types": "./dist/types/clients/rtkq/historian.alerting/v0alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/historian.alerting/v0alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/historian.alerting/v0alpha1/index.cjs"
},
"./rtkq/logsdrilldown/v1alpha1": {
"import": "./src/clients/rtkq/logsdrilldown/v1alpha1/index.ts",
"require": "./src/clients/rtkq/logsdrilldown/v1alpha1/index.ts"
"@grafana-app/source": "./src/clients/rtkq/logsdrilldown/v1alpha1/index.ts",
"types": "./dist/types/clients/rtkq/logsdrilldown/v1alpha1/index.d.ts",
"import": "./dist/esm/clients/rtkq/logsdrilldown/v1alpha1/index.mjs",
"require": "./dist/cjs/clients/rtkq/logsdrilldown/v1alpha1/index.cjs"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [

View File

@@ -5,35 +5,17 @@ import { cjsOutput, entryPoint, esmOutput, plugins } from '../rollup.config.part
const rq = createRequire(import.meta.url);
const pkg = rq('./package.json');
const apiClients = Object.entries<{ import: string; require: string }>(pkg.exports).filter(([key]) =>
key.startsWith('./rtkq/')
);
const apiClientConfigs = apiClients.map(([name, { import: importPath }]) => {
const baseCjsOutput = cjsOutput(pkg);
const entryFileNames = name.replace('./', '') + '.cjs';
const cjsOutputConfig = { ...baseCjsOutput, entryFileNames };
return {
input: importPath.replace('./', ''),
plugins,
output: [cjsOutputConfig, esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
};
});
export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
output: [cjsOutput(pkg, 'grafana-api-clients'), esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
},
{
input: 'src/clients/rtkq/index.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
output: [cjsOutput(pkg, 'grafana-api-clients'), esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
},
...apiClientConfigs,
];

View File

@@ -143,8 +143,10 @@ export const updatePackageJsonExports =
// Create the new export entry
const newExportKey = `./rtkq/${groupName}/${version}`;
const newExportValue = {
import: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
require: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
'@grafana-app/source': `./src/clients/rtkq/${groupName}/${version}/index.ts`,
types: `./dist/types/clients/rtkq/${groupName}/${version}/index.d.ts`,
import: `./dist/esm/clients/rtkq/${groupName}/${version}/index.mjs`,
require: `./dist/cjs/clients/rtkq/${groupName}/${version}/index.cjs`,
};
// Check if export already exists

View File

@@ -8,7 +8,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."],
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": [
@@ -17,5 +18,12 @@
"../grafana-ui/src/types/*.d.ts",
"../grafana-i18n/src/types/*.d.ts",
"src/**/*.ts*"
]
],
"ts-node": {
"swc": true,
"compilerOptions": {
"module": "es2020",
"moduleResolution": "Bundler"
}
}
}

View File

@@ -13,32 +13,31 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-data"
},
"main": "src/index.ts",
"types": "src/index.ts",
"module": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
},
"./internal": {
"import": "./src/internal/index.ts",
"require": "./src/internal/index.ts"
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
},
"./unstable": {
"import": "./src/unstable.ts",
"require": "./src/unstable.ts"
"@grafana-app/source": "./src/unstable.ts",
"types": "./dist/types/unstable.d.ts",
"import": "./dist/esm/unstable.mjs",
"require": "./dist/cjs/unstable.cjs"
},
"./internal": {
"@grafana-app/source": "./src/internal/index.ts"
},
"./test": {
"import": "./test/index.ts",
"require": "./test/index.ts"
"@grafana-app/source": "./test/index.ts"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
@@ -51,8 +50,8 @@
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json && rimraf ./unstable"
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json"
},
"dependencies": {
"@braintree/sanitize-url": "7.0.1",

View File

@@ -9,13 +9,13 @@ export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
treeshake: false,
},
{
input: 'src/unstable.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
treeshake: false,
},
];

View File

@@ -3,10 +3,12 @@
"compilerOptions": {
"declaration": true,
"jsx": "react-jsx",
"baseUrl": "./",
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": [

View File

@@ -16,12 +16,19 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-e2e-selectors"
},
"main": "src/index.ts",
"types": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [

View File

@@ -9,7 +9,7 @@ export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-e2e-selectors')],
output: [cjsOutput(pkg, 'grafana-e2e-selectors'), esmOutput(pkg, 'grafana-e2e-selectors')],
treeshake: false,
},
];

View File

@@ -1332,6 +1332,7 @@ export const versionedComponents = {
},
DebugOverlay: {
wrapper: {
'12.3.0': 'data-testid debug-overlay-wrapper',
'9.2.0': 'debug-overlay',
},
},

View File

@@ -5,7 +5,8 @@
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": ["src/**/*.ts"]

View File

@@ -16,12 +16,19 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-flamegraph"
},
"main": "src/index.ts",
"types": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [

View File

@@ -9,7 +9,7 @@ export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-flamegraph')],
output: [cjsOutput(pkg, 'grafana-flamegraph'), esmOutput(pkg, 'grafana-flamegraph')],
treeshake: false,
},
];

View File

@@ -7,7 +7,8 @@
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"]

View File

@@ -14,33 +14,32 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-i18n"
},
"main": "src/index.ts",
"types": "src/index.ts",
"module": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
},
"./internal": {
"import": "./src/internal/index.ts",
"require": "./src/internal/index.ts"
"@grafana-app/source": "./src/internal/index.ts"
},
"./eslint-plugin": {
"@grafana-app/source": "./src/eslint/index.cjs",
"types": "./src/eslint/index.d.ts",
"import": "./src/eslint/index.cjs",
"require": "./src/eslint/index.cjs"
"default": "./src/eslint/index.cjs"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
"dist",
"src/eslint/**/*",
"./README.md",
"./CHANGELOG.md",
"LICENSE_APACHE2"

View File

@@ -1,5 +1,4 @@
import { createRequire } from 'node:module';
import copy from 'rollup-plugin-copy';
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
@@ -9,13 +8,8 @@ const pkg = rq('./package.json');
export default [
{
input: entryPoint,
plugins: [
...plugins,
copy({
targets: [{ src: 'src/eslint', dest: 'dist' }],
}),
],
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-i18n')],
plugins,
output: [cjsOutput(pkg, 'grafana-i18n'), esmOutput(pkg, 'grafana-i18n')],
treeshake: false,
},
];

View File

@@ -6,7 +6,8 @@
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": ["src/**/*.ts*"]

View File

@@ -8,7 +8,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"allowJs": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": [

View File

@@ -17,6 +17,9 @@ export default {
setupFiles: ['jest-canvas-mock'],
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
testEnvironment: 'jsdom',
testEnvironmentOptions: {
customExportConditions: ['@grafana-app/source', 'browser'],
},
testMatch: ['<rootDir>/**/__tests__/**/*.{js,jsx,ts,tsx}', '<rootDir>/**/*.{spec,test,jest}.{js,jsx,ts,tsx}'],
transform: {
'^.+\\.(t|j)sx?$': [

View File

@@ -1,12 +1,13 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"alwaysStrict": true,
"customConditions": ["@grafana-app/source"],
"declaration": false,
"resolveJsonModule": true,
"jsx": "react-jsx",
"moduleResolution": "bundler",
"noEmit": true,
"allowImportingTsExtensions": true
"resolveJsonModule": true
},
"extends": "@grafana/tsconfig",
"exclude": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],

View File

@@ -312,6 +312,7 @@ const config = async (env: Env): Promise<Configuration> => {
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
conditionNames: ['@grafana-app/source', '...'],
unsafeCache: true,
},

View File

@@ -15,8 +15,18 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-prometheus"
},
"main": "src/index.ts",
"types": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"files": [
"./dist",
"./README.md",
@@ -24,9 +34,6 @@
"./LICENSE_AGPL"
],
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"scripts": {

View File

@@ -12,7 +12,7 @@ export default [
{
input: entryPoint,
plugins: [...plugins, image(), json(), dynamicImportVars()],
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-prometheus')],
output: [cjsOutput(pkg, 'grafana-prometheus'), esmOutput(pkg, 'grafana-prometheus')],
treeshake: false,
},
];

View File

@@ -14,28 +14,28 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-runtime"
},
"main": "src/index.ts",
"types": "src/index.ts",
"module": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
},
"./internal": {
"import": "./src/internal/index.ts",
"require": "./src/internal/index.ts"
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
},
"./unstable": {
"import": "./src/unstable.ts",
"require": "./src/unstable.ts"
"@grafana-app/source": "./src/unstable.ts",
"types": "./dist/types/unstable.d.ts",
"import": "./dist/esm/unstable.mjs",
"require": "./dist/cjs/unstable.cjs"
},
"./internal": {
"@grafana-app/source": "./src/internal/index.ts"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
@@ -49,8 +49,8 @@
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json && rimraf ./unstable"
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json"
},
"dependencies": {
"@grafana/data": "12.4.0-pre",

View File

@@ -9,13 +9,13 @@ export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')],
output: [cjsOutput(pkg, 'grafana-runtime'), esmOutput(pkg, 'grafana-runtime')],
treeshake: false,
},
{
input: 'src/unstable.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')],
output: [cjsOutput(pkg, 'grafana-runtime'), esmOutput(pkg, 'grafana-runtime')],
treeshake: false,
},
];

View File

@@ -9,7 +9,15 @@ export * from './analytics/types';
export { loadPluginCss, type PluginCssOptions, setPluginImportUtils, getPluginImportUtils } from './utils/plugin';
export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils';
export { featureEnabled } from './utils/licensing';
export { logInfo, logDebug, logWarning, logError, createMonitoringLogger, logMeasurement } from './utils/logging';
export {
logInfo,
logDebug,
logWarning,
logError,
createMonitoringLogger,
logMeasurement,
type MonitoringLogger,
} from './utils/logging';
export {
DataSourceWithBackend,
HealthCheckError,

View File

@@ -8,7 +8,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"allowJs": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": [

View File

@@ -13,13 +13,14 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-schema"
},
"main": "src/index.ts",
"types": "src/index.ts",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"publishConfig": {
"access": "public",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
"types": "./dist/types/index.d.ts"
},
"files": [
"dist",

View File

@@ -15,7 +15,12 @@ export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-schema')],
output: [
// Schema still uses publishConfig to define output directory.
// TODO: Migrate this package to use exports.
cjsOutput(pkg, 'grafana-schema', { dir: path.dirname(pkg.publishConfig.main) }),
esmOutput(pkg, 'grafana-schema', { dir: path.dirname(pkg.publishConfig.module) }),
],
treeshake: false,
},
{

View File

@@ -3,10 +3,12 @@
"compilerOptions": {
"declaration": true,
"jsx": "react-jsx",
"baseUrl": "./",
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": ["src/**/*.ts*"]

View File

@@ -1,3 +1,3 @@
import { createMonitoringLogger } from '@grafana/runtime';
import { createMonitoringLogger, MonitoringLogger } from '@grafana/runtime';
export const sqlPluginLogger = createMonitoringLogger('features.plugins.sql');
export const sqlPluginLogger: MonitoringLogger = createMonitoringLogger('features.plugins.sql');

View File

@@ -8,7 +8,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"strict": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"]

View File

@@ -95,6 +95,16 @@ const mainConfig: StorybookConfig = {
},
});
// Tell storybook to resolve imports with the @grafana-app/source condition for
// the packages in this repo.
if (config && config.resolve) {
if (Array.isArray(config.resolve.conditionNames)) {
config.resolve.conditionNames.unshift('@grafana-app/source');
} else {
config.resolve.conditionNames = ['@grafana-app/source', '...'];
}
}
return config;
},
};

View File

@@ -1,8 +1,7 @@
{
"compilerOptions": {
"declarationDir": "dist",
"noUnusedLocals": false,
"outDir": "compiled"
"noUnusedLocals": false
},
"extends": "../tsconfig.json",
"include": ["../src/**/*.ts*", "../../../public/app/types/svg.d.ts"]

View File

@@ -16,28 +16,28 @@
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-ui"
},
"main": "src/index.ts",
"types": "src/index.ts",
"module": "src/index.ts",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
},
"./internal": {
"import": "./src/internal/index.ts",
"require": "./src/internal/index.ts"
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
},
"./unstable": {
"import": "./src/unstable.ts",
"require": "./src/unstable.ts"
"@grafana-app/source": "./src/unstable.ts",
"types": "./dist/types/unstable.d.ts",
"import": "./dist/esm/unstable.mjs",
"require": "./dist/cjs/unstable.cjs"
},
"./internal": {
"@grafana-app/source": "./src/internal/index.ts"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
@@ -55,8 +55,8 @@
"storybook:build": "storybook build -o ./dist/storybook -c .storybook",
"storybook:test": "test-storybook --url http://localhost:9001",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json && rimraf ./unstable"
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json"
},
"browserslist": [
"defaults",

View File

@@ -24,7 +24,7 @@ export default [
flatten: false,
}),
],
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')],
output: [cjsOutput(pkg, 'grafana-ui'), esmOutput(pkg, 'grafana-ui')],
treeshake: false,
},
{
@@ -37,7 +37,7 @@ export default [
flatten: false,
}),
],
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')],
output: [cjsOutput(pkg, 'grafana-ui'), esmOutput(pkg, 'grafana-ui')],
treeshake: false,
},
];

View File

@@ -94,4 +94,5 @@ class UnthemedSelectOptionGroup extends PureComponent<ExtendedGroupProps, State>
}
}
export const SelectOptionGroup = withTheme2(UnthemedSelectOptionGroup);
// TODO: type this properly
export const SelectOptionGroup: React.FC<ExtendedGroupProps> = withTheme2(UnthemedSelectOptionGroup);

View File

@@ -79,7 +79,7 @@ export const getModalStyles = (theme: GrafanaTheme2) => {
modalContent: css({
overflow: 'auto',
padding: theme.spacing(3, 3, 0, 3),
marginBottom: theme.spacing(3),
marginBottom: theme.spacing(2.5),
scrollbarWidth: 'thin',
width: '100%',

View File

@@ -76,4 +76,5 @@ class UnthemedValueContainer<Option, isMulti extends boolean, Group extends Grou
}
}
export const ValueContainer = withTheme2(UnthemedValueContainer);
export const ValueContainer: React.FC<ValueContainerProps<unknown, boolean, GroupBase<unknown>>> =
withTheme2(UnthemedValueContainer);

View File

@@ -8,7 +8,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"allowJs": true,
"rootDirs": ["."]
"rootDirs": ["."],
"moduleResolution": "bundler"
},
"exclude": ["dist/**/*"],
"include": ["../../public/test/setupTests.ts", "../../public/app/types/*.d.ts", "src/**/*.ts*"],

View File

@@ -23,25 +23,29 @@ export const plugins = [
];
// Generates a rollup configuration for commonjs output.
export function cjsOutput(pkg) {
export function cjsOutput(pkg, pkgName, overrides = {}) {
return {
format: 'cjs',
sourcemap: true,
dir: dirname(pkg.publishConfig.main),
dir: dirname(pkg.main),
entryFileNames: '[name].cjs',
preserveModules: true,
preserveModulesRoot: resolve(projectCwd, `packages/${pkgName}/src`),
esModule: true,
interop: 'compat',
...overrides,
};
}
// Generate a rollup configuration for es module output.
export function esmOutput(pkg, pkgName) {
export function esmOutput(pkg, pkgName, overrides = {}) {
return {
format: 'esm',
sourcemap: true,
dir: dirname(pkg.publishConfig.module),
dir: dirname(pkg.module),
entryFileNames: '[name].mjs',
preserveModules: true,
preserveModulesRoot: resolve(projectCwd, `packages/${pkgName}/src`),
...overrides,
};
}

View File

@@ -113,6 +113,7 @@ func ProvideMigratorDashboardAccessor(
dashboardPermissionSvc: nil, // not needed for migration
libraryPanelSvc: nil, // not needed for migration
accessControl: accessControl,
log: log.New("legacy.dashboard.migrator.accessor"),
}
}
@@ -136,6 +137,7 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider,
dashboardPermissionSvc: dashboardPermissionSvc,
libraryPanelSvc: libraryPanelSvc,
accessControl: accessControl,
log: log.New("legacy.dashboard.accessor"),
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/url"
"path"
"strings"
"time"
@@ -141,14 +140,20 @@ func (e *evaluator) evaluateFile(ctx context.Context, repo repository.Reader, ba
if info.Parsed.GVK.Kind == dashboardKind {
// FIXME: extract the logic out of a dashboard URL builder/injector or similar
// for testability and decoupling
urlBuilder, err := url.Parse(baseURL)
if err != nil {
info.Error = err.Error()
return info
}
if info.Parsed.Existing != nil {
info.GrafanaURL = fmt.Sprintf("%sd/%s/%s", baseURL, obj.GetName(),
slugify.Slugify(info.Title))
grafanaURL := urlBuilder.JoinPath("d", obj.GetName(), slugify.Slugify(info.Title))
info.GrafanaURL = grafanaURL.String()
}
// Load this file directly
info.PreviewURL = baseURL + path.Join("admin/provisioning",
info.Parsed.Repo.Name, "dashboard/preview", info.Parsed.Info.Path)
previewURL := urlBuilder.JoinPath("admin/provisioning", info.Parsed.Repo.Name, "dashboard/preview", info.Parsed.Info.Path)
info.PreviewURL = previewURL.String()
query := url.Values{}
query.Set("ref", info.Parsed.Info.Ref)

View File

@@ -737,8 +737,78 @@ func TestCalculateChanges(t *testing.T) {
Path: "path/to/file.json",
Ref: "ref",
},
GrafanaURL: "ht tp://bad url/d/the-uid/hello-world", // Malformed URL
PreviewURL: "ht tp://bad url/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
Error: "parse \"ht tp://bad url/\": first path segment in URL cannot contain colon",
}},
},
},
{
name: "path with spaces",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file with spaces.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
progress.On("SetMessage", mock.Anything, "process path/to/file with spaces.json").Return()
reader.On("Read", mock.Anything, "path/to/file with spaces.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file with spaces.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file with spaces.json",
Ref: "ref",
},
GrafanaURL: "http://host/d/the-uid/hello-world",
PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file%20with%20spaces.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
GrafanaScreenshotURL: "",
PreviewScreenshotURL: "",
}},
},
},

View File

@@ -357,7 +357,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
type RuleStatusMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule)
// mutator function used to attach alert states to the rule and returns the totals and filtered totals
type RuleAlertStateMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (total map[string]int64, filteredTotal map[string]int64)
type RuleAlertStateMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, limitAlerts int64) (total map[string]int64, filteredTotal map[string]int64)
func RuleStatusMutatorGenerator(statusReader StatusReader) RuleStatusMutator {
return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule) {
@@ -377,32 +377,18 @@ func RuleStatusMutatorGenerator(statusReader StatusReader) RuleStatusMutator {
}
func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAlertStateMutator {
return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (map[string]int64, map[string]int64) {
return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, limitAlerts int64) (map[string]int64, map[string]int64) {
states := manager.GetStatesForRuleUID(source.OrgID, source.UID)
totals := make(map[string]int64)
totalsFiltered := make(map[string]int64)
for _, alertState := range states {
activeAt := alertState.StartsAt
valString := ""
if alertState.State == eval.Alerting || alertState.State == eval.Pending || alertState.State == eval.Recovering {
valString = FormatValues(alertState)
}
stateKey := strings.ToLower(alertState.State.String())
totals[stateKey] += 1
// Do not add error twice when execution error state is Error
if alertState.Error != nil && source.ExecErrState != ngmodels.ErrorErrState {
totals["error"] += 1
}
alert := apimodels.Alert{
Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)),
Annotations: apimodels.LabelsFromMap(alertState.Annotations),
// TODO: or should we make this two fields? Using one field lets the
// frontend use the same logic for parsing text on annotations and this.
State: state.FormatStateAndReason(alertState.State, alertState.StateReason),
ActiveAt: &activeAt,
Value: valString,
}
// Set the state of the rule based on the state of its alerts.
// Only update the rule state with 'pending' or 'recovering' if the current state is 'inactive'.
@@ -442,7 +428,23 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler
totalsFiltered["error"] += 1
}
toMutate.Alerts = append(toMutate.Alerts, alert)
if limitAlerts != 0 {
valString := ""
if alertState.State == eval.Alerting || alertState.State == eval.Pending || alertState.State == eval.Recovering {
valString = FormatValues(alertState)
}
toMutate.Alerts = append(toMutate.Alerts, apimodels.Alert{
Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)),
Annotations: apimodels.LabelsFromMap(alertState.Annotations),
// TODO: or should we make this two fields? Using one field lets the
// frontend use the same logic for parsing text on annotations and this.
State: state.FormatStateAndReason(alertState.State, alertState.StateReason),
ActiveAt: &activeAt,
Value: valString,
})
}
}
return totals, totalsFiltered
}
@@ -1227,7 +1229,7 @@ func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFull
}
// mutate rule for alert states
totals, totalsFiltered := ruleAlertStateMutator(rule, &alertingRule, stateFilterSet, matchers, labelOptions)
totals, totalsFiltered := ruleAlertStateMutator(rule, &alertingRule, stateFilterSet, matchers, labelOptions, limitAlerts)
if alertingRule.State != "" {
rulesTotals[alertingRule.State] += 1

View File

@@ -39,7 +39,8 @@
"inputs": [
"{workspaceRoot}/scripts/cli/generateSassVariableFiles.ts",
"{workspaceRoot}/packages/grafana-data/src/themes/**",
"{workspaceRoot}/packages/grafana-ui/src/themes/**"
"{workspaceRoot}/packages/grafana-ui/src/themes/**",
"{workspaceRoot}/package.json"
],
"outputs": [
"{workspaceRoot}/public/sass/_variables.generated.scss",

View File

@@ -3,7 +3,7 @@ import { ComponentType, useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { LinkButton, RadioButtonGroup, useStyles2, FilterInput, EmptyState } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';

View File

@@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { GrafanaBootConfig } from '@grafana/runtime';
import config from 'app/core/config';

View File

@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Button, LoadingPlaceholder, Modal, ModalsController, useStyles2 } from '@grafana/ui';
import {

View File

@@ -1,4 +1,4 @@
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Icon, Stack, Tag, Tooltip } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';

View File

@@ -22,7 +22,7 @@ export function initAlerting() {
component: ({ dashboard }) =>
alertingEnabled ? (
<Suspense fallback={null} key="alert-rules-button">
{dashboard && <AlertRulesToolbarButton dashboardUid={dashboard.uid} />}
{dashboard && dashboard.uid && <AlertRulesToolbarButton dashboardUid={dashboard.uid} />}
</Suspense>
) : null,
index: -2,

View File

@@ -76,12 +76,6 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
data-testid={selectors.pages.Dashboard.Sidebar.optionsButton}
active={selectedObject === dashboard ? true : false}
/>
{/* <Sidebar.Button
tooltip={t('dashboard.sidebar.edit-schema.tooltip', 'Edit as code')}
title={t('dashboard.sidebar.edit-schema.title', 'Code')}
icon="brackets-curly"
onClick={() => dashboard.openV2SchemaEditor()}
/> */}
<Sidebar.Divider />
</>
)}

View File

@@ -16,7 +16,7 @@ import {
sceneUtils,
VizPanel,
} from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema/';
import { LibraryPanel } from '@grafana/schema';
import { Alert, Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui';
import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';

View File

@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import { memo, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data/';
import { GrafanaTheme2 } from '@grafana/data';
import { LazyLoader, SceneComponentProps, VizPanel } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';

View File

@@ -1,4 +1,4 @@
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
import { locationService } from '@grafana/runtime';
import { Button } from '@grafana/ui';

View File

@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { useForm } from 'react-hook-form';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Button, ClipboardButton, Field, Input, Stack, Label, ModalsController, Switch, useStyles2 } from '@grafana/ui';
import {

View File

@@ -1,7 +1,7 @@
import { UseFormRegister } from 'react-hook-form';
import { TimeRange } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { FieldSet, Label, Switch, TimeRangeInput, Stack } from '@grafana/ui';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { UseFormRegister } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Checkbox, FieldSet, LinkButton, useStyles2, Stack } from '@grafana/ui';

View File

@@ -1,4 +1,4 @@
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Alert } from '@grafana/ui';

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import cx from 'classnames';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Alert, useStyles2 } from '@grafana/ui';

View File

@@ -1,4 +1,4 @@
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Alert } from '@grafana/ui';

View File

@@ -4,7 +4,7 @@ import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { BootData, DataQuery } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { reportInteraction, setEchoSrv } from '@grafana/runtime';
import { Panel } from '@grafana/schema';
import config from 'app/core/config';

View File

@@ -5,7 +5,7 @@ import { useEffectOnce } from 'react-use';
import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer';
import { render } from 'test/test-utils';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Dashboard, DashboardCursorSync, FieldConfigSource, Panel, ThresholdsMode } from '@grafana/schema/src';
import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps';
import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types/dashboard';

View File

@@ -4,7 +4,7 @@ import { useLocation, useParams } from 'react-router-dom-v5-compat';
import { usePrevious } from 'react-use';
import { GrafanaTheme2, PageLayoutType, TimeZone } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { PageToolbar, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useGrafana } from 'app/core/context/GrafanaContext';

View File

@@ -3,7 +3,7 @@ import { useCopyToClipboard } from 'react-use';
import { Field, GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { isValidLegacyName, utf8Support } from '@grafana/prometheus/src/utf8_support';
import { isValidLegacyName, utf8Support } from '@grafana/prometheus';
import { reportInteraction } from '@grafana/runtime';
import { IconButton, useStyles2 } from '@grafana/ui';

View File

@@ -1,262 +0,0 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants';
import { Trace } from '../../types/trace';
import { SpanFilters } from './SpanFilters';
const trace: Trace = {
traceID: '1ed38015486087ca',
spans: [
{
traceID: '1ed38015486087ca',
spanID: '1ed38015486087ca',
operationName: 'Span0',
tags: [{ key: 'TagKey0', type: 'string', value: 'TagValue0' }],
kind: 'server',
statusCode: 2,
statusMessage: 'message',
instrumentationLibraryName: 'name',
instrumentationLibraryVersion: 'version',
traceState: 'state',
process: {
serviceName: 'Service0',
tags: [{ key: 'ProcessKey0', type: 'string', value: 'ProcessValue0' }],
},
logs: [{ fields: [{ key: 'LogKey0', type: 'string', value: 'LogValue0' }] }],
},
{
traceID: '1ed38015486087ca',
spanID: '2ed38015486087ca',
operationName: 'Span1',
tags: [{ key: 'TagKey1', type: 'string', value: 'TagValue1' }],
process: {
serviceName: 'Service1',
tags: [{ key: 'ProcessKey1', type: 'string', value: 'ProcessValue1' }],
},
logs: [{ fields: [{ key: 'LogKey1', type: 'string', value: 'LogValue1' }] }],
},
],
processes: {
'1ed38015486087ca': {
serviceName: 'Service0',
tags: [],
},
},
} as unknown as Trace;
describe('SpanFilters', () => {
let user: ReturnType<typeof userEvent.setup>;
const SpanFiltersWithProps = ({ showFilters = true, matches }: { showFilters?: boolean; matches?: Set<string> }) => {
const [search, setSearch] = useState(DEFAULT_SPAN_FILTERS);
const props = {
trace: trace,
showSpanFilters: showFilters,
setShowSpanFilters: jest.fn(),
search,
setSearch,
spanFilterMatches: matches,
setFocusedSpanIdForSearch: jest.fn(),
datasourceType: 'tempo',
};
return <SpanFilters {...props} />;
};
beforeEach(() => {
jest.useFakeTimers();
// Need to use delay: null here to work with fakeTimers
// see https://github.com/testing-library/user-event/issues/833
user = userEvent.setup({ delay: null });
});
afterEach(() => {
jest.useRealTimers();
});
it('should render', () => {
expect(() => render(<SpanFiltersWithProps />)).not.toThrow();
});
it('should render filters', async () => {
render(<SpanFiltersWithProps />);
const serviceOperator = screen.getByLabelText('Select service name operator');
const serviceValue = screen.getByLabelText('Select service name');
const spanOperator = screen.getByLabelText('Select span name operator');
const spanValue = screen.getByLabelText('Select span name');
const fromOperator = screen.getByLabelText('Select min span operator');
const fromValue = screen.getByLabelText('Select min span duration');
const toOperator = screen.getByLabelText('Select max span operator');
const toValue = screen.getByLabelText('Select max span duration');
const tagKey = screen.getByLabelText('Select tag key');
const tagOperator = screen.getByLabelText('Select tag operator');
const tagSelectValue = screen.getByLabelText('Select tag value');
expect(serviceOperator).toBeInTheDocument();
expect(getElemText(serviceOperator)).toBe('=');
expect(serviceValue).toBeInTheDocument();
expect(spanOperator).toBeInTheDocument();
expect(getElemText(spanOperator)).toBe('=');
expect(spanValue).toBeInTheDocument();
expect(fromOperator).toBeInTheDocument();
expect(getElemText(fromOperator)).toBe('>');
expect(fromValue).toBeInTheDocument();
expect(toOperator).toBeInTheDocument();
expect(getElemText(toOperator)).toBe('<');
expect(toValue).toBeInTheDocument();
expect(tagKey).toBeInTheDocument();
expect(tagOperator).toBeInTheDocument();
expect(getElemText(tagOperator)).toBe('=');
expect(tagSelectValue).toBeInTheDocument();
await user.click(serviceValue);
jest.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText('Service0')).toBeInTheDocument();
expect(screen.getByText('Service1')).toBeInTheDocument();
});
await user.click(spanValue);
jest.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText('Span0')).toBeInTheDocument();
expect(screen.getByText('Span1')).toBeInTheDocument();
});
await user.click(tagOperator);
jest.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText('!~')).toBeInTheDocument();
expect(screen.getByText('=~')).toBeInTheDocument();
expect(screen.getByText('!~')).toBeInTheDocument();
});
await user.click(tagKey);
jest.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText('TagKey0')).toBeInTheDocument();
expect(screen.getByText('TagKey1')).toBeInTheDocument();
expect(screen.getByText('kind')).toBeInTheDocument();
expect(screen.getByText('ProcessKey0')).toBeInTheDocument();
expect(screen.getByText('ProcessKey1')).toBeInTheDocument();
expect(screen.getByText('LogKey0')).toBeInTheDocument();
expect(screen.getByText('LogKey1')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Find...')).toBeInTheDocument();
});
});
it('should update filters', async () => {
render(<SpanFiltersWithProps />);
const serviceValue = screen.getByLabelText('Select service name');
const spanValue = screen.getByLabelText('Select span name');
const tagKey = screen.getByLabelText('Select tag key');
const tagOperator = screen.getByLabelText('Select tag operator');
const tagValue = screen.getByLabelText('Select tag value');
expect(getElemText(serviceValue)).toBe('All service names');
await selectAndCheckValue(user, serviceValue, 'Service0');
expect(getElemText(spanValue)).toBe('All span names');
await selectAndCheckValue(user, spanValue, 'Span0');
await user.click(tagValue);
jest.advanceTimersByTime(1000);
await waitFor(() => expect(screen.getByText('No options found')).toBeInTheDocument());
expect(getElemText(tagKey)).toBe('Select tag');
await selectAndCheckValue(user, tagKey, 'TagKey0');
expect(getElemText(tagValue)).toBe('Select value');
await selectAndCheckValue(user, tagValue, 'TagValue0');
expect(screen.queryByLabelText('Input tag value')).toBeNull();
await selectAndCheckValue(user, tagOperator, '=~');
expect(screen.getByLabelText('Input tag value')).toBeInTheDocument();
});
it('should order tag filters', async () => {
render(<SpanFiltersWithProps />);
const tagKey = screen.getByLabelText('Select tag key');
await user.click(tagKey);
jest.advanceTimersByTime(1000);
await waitFor(() => {
const container = screen.getByText('TagKey0').parentElement?.parentElement?.parentElement;
expect(container?.childNodes[1].textContent).toBe('ProcessKey0');
expect(container?.childNodes[2].textContent).toBe('ProcessKey1');
expect(container?.childNodes[3].textContent).toBe('TagKey0');
expect(container?.childNodes[4].textContent).toBe('TagKey1');
expect(container?.childNodes[5].textContent).toBe('id');
expect(container?.childNodes[6].textContent).toBe('kind');
expect(container?.childNodes[7].textContent).toBe('library.name');
expect(container?.childNodes[8].textContent).toBe('library.version');
expect(container?.childNodes[9].textContent).toBe('status');
expect(container?.childNodes[10].textContent).toBe('status.message');
expect(container?.childNodes[11].textContent).toBe('trace.state');
expect(container?.childNodes[12].textContent).toBe('LogKey0');
expect(container?.childNodes[13].textContent).toBe('LogKey1');
});
});
it('should only show add/remove tag when necessary', async () => {
render(<SpanFiltersWithProps />);
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
expect(screen.queryAllByLabelText('Remove tag').length).toBe(0); // mot filled in the default tag, so no values to remove
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
await selectAndCheckValue(user, screen.getByLabelText('Select tag key'), 'TagKey0');
expect(screen.getAllByLabelText('Add tag').length).toBe(1);
expect(screen.getAllByLabelText('Remove tag').length).toBe(1);
await user.click(screen.getByLabelText('Add tag'));
jest.advanceTimersByTime(1000);
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the new tag, so no need to add another one
expect(screen.getAllByLabelText('Remove tag').length).toBe(2); // one for each tag
expect(screen.getAllByLabelText('Select tag key').length).toBe(2);
await user.click(screen.getAllByLabelText('Remove tag')[1]);
jest.advanceTimersByTime(1000);
expect(screen.queryAllByLabelText('Add tag').length).toBe(1); // filled in the default tag, so can add another one
expect(screen.queryAllByLabelText('Remove tag').length).toBe(1); // filled in the default tag, so can remove values
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
await user.click(screen.getAllByLabelText('Remove tag')[0]);
jest.advanceTimersByTime(1000);
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
expect(screen.queryAllByLabelText('Remove tag').length).toBe(0); // mot filled in the default tag, so no values to remove
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
});
it('should allow adding/removing tags', async () => {
render(<SpanFiltersWithProps />);
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
const tagKey = screen.getByLabelText('Select tag key');
await selectAndCheckValue(user, tagKey, 'TagKey0');
await user.click(screen.getByLabelText('Add tag'));
jest.advanceTimersByTime(1000);
expect(screen.getAllByLabelText('Select tag key').length).toBe(2);
await user.click(screen.getAllByLabelText('Remove tag')[0]);
jest.advanceTimersByTime(1000);
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
});
it('renders buttons when span filters is collapsed', async () => {
render(<SpanFiltersWithProps showFilters={false} />);
expect(screen.queryByRole('button', { name: 'Next result button' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Prev result button' })).toBeInTheDocument();
});
});
const selectAndCheckValue = async (user: ReturnType<typeof userEvent.setup>, elem: HTMLElement, text: string) => {
await user.click(elem);
jest.advanceTimersByTime(1000);
await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument());
await user.click(screen.getByText(text));
jest.advanceTimersByTime(1000);
expect(screen.getByText(text)).toBeInTheDocument();
};
const getElemText = (elem: HTMLElement) => {
return elem.parentElement?.previousSibling?.textContent;
};

View File

@@ -1,319 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { css } from '@emotion/css';
import React, { useState, useEffect, memo, useCallback, useRef } from 'react';
import { GrafanaTheme2, TraceSearchProps, SelectableValue, toOption } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { IntervalInput } from '@grafana/o11y-ds-frontend';
import { Collapse, Icon, InlineField, InlineFieldRow, Select, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { DEFAULT_SPAN_FILTERS } from '../../../../state/constants';
import { getTraceServiceNames, getTraceSpanNames } from '../../../utils/tags';
import SearchBarInput from '../../common/SearchBarInput';
import { Trace } from '../../types/trace';
import NextPrevResult from '../SearchBar/NextPrevResult';
import TracePageSearchBar from '../SearchBar/TracePageSearchBar';
import { SpanFiltersTags } from './SpanFiltersTags';
export type SpanFilterProps = {
trace: Trace;
search: TraceSearchProps;
setSearch: (newSearch: TraceSearchProps) => void;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
spanFilterMatches: Set<string> | undefined;
datasourceType: string;
};
export const SpanFilters = memo((props: SpanFilterProps) => {
const {
trace,
search,
setSearch,
showSpanFilters,
setShowSpanFilters,
setFocusedSpanIdForSearch,
spanFilterMatches,
datasourceType,
} = props;
const styles = { ...useStyles2(getStyles) };
const [serviceNames, setServiceNames] = useState<Array<SelectableValue<string>>>();
const [spanNames, setSpanNames] = useState<Array<SelectableValue<string>>>();
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
const [tagKeys, setTagKeys] = useState<Array<SelectableValue<string>>>();
const [tagValues, setTagValues] = useState<{ [key: string]: Array<SelectableValue<string>> }>({});
const prevTraceIdRef = useRef<string>();
const durationRegex = /^\d+(?:\.\d)?\d*(?:ns|us|µs|ms|s|m|h)$/;
const clear = useCallback(() => {
setServiceNames(undefined);
setSpanNames(undefined);
setTagKeys(undefined);
setTagValues({});
setSearch(DEFAULT_SPAN_FILTERS);
}, [setSearch]);
useEffect(() => {
// Only clear filters when trace ID actually changes (not on initial mount)
const currentTraceId = trace?.traceID;
const traceHasChanged = prevTraceIdRef.current && prevTraceIdRef.current !== currentTraceId;
if (traceHasChanged) {
clear();
}
prevTraceIdRef.current = currentTraceId;
}, [clear, trace]);
const setShowSpanFilterMatchesOnly = useCallback(
(showMatchesOnly: boolean) => {
setSearch({ ...search, matchesOnly: showMatchesOnly });
},
[search, setSearch]
);
if (!trace) {
return null;
}
const setSpanFiltersSearch = (spanSearch: TraceSearchProps) => {
setFocusedSpanIndexForSearch(-1);
setFocusedSpanIdForSearch('');
setSearch(spanSearch);
};
const getServiceNames = () => {
if (!serviceNames) {
setServiceNames(getTraceServiceNames(trace).map(toOption));
}
};
const getSpanNames = () => {
if (!spanNames) {
setSpanNames(getTraceSpanNames(trace).map(toOption));
}
};
const collapseLabel = (
<>
<Tooltip
content={t(
'explore.span-filters.tooltip-collapse',
'Filter your spans below. You can continue to apply filters until you have narrowed down your resulting spans to the select few you are most interested in.'
)}
placement="right"
>
<span className={styles.collapseLabel}>
<Trans i18nKey="explore.span-filters.label-collapse">Span Filters</Trans>
<Icon size="md" name="info-circle" />
</span>
</Tooltip>
{!showSpanFilters && (
<div className={styles.nextPrevResult}>
<NextPrevResult
trace={trace}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
datasourceType={datasourceType}
showSpanFilters={showSpanFilters}
/>
</div>
)}
</>
);
return (
<div className={styles.container}>
<Collapse label={collapseLabel} isOpen={showSpanFilters} onToggle={setShowSpanFilters}>
<InlineFieldRow className={styles.flexContainer}>
<InlineField label={t('explore.span-filters.label-service-name', 'Service name')} labelWidth={16}>
<Stack gap={0.5}>
<Select
aria-label={t(
'explore.span-filters.aria-label-select-service-name-operator',
'Select service name operator'
)}
onChange={(v) => setSpanFiltersSearch({ ...search, serviceNameOperator: v.value! })}
options={[toOption('='), toOption('!=')]}
value={search.serviceNameOperator}
/>
<Select
aria-label={t('explore.span-filters.aria-label-select-service-name', 'Select service name')}
isClearable
onChange={(v) => setSpanFiltersSearch({ ...search, serviceName: v?.value || '' })}
onOpenMenu={getServiceNames}
options={serviceNames || (search.serviceName ? [search.serviceName].map(toOption) : [])}
placeholder={t('explore.span-filters.placeholder-all-service-names', 'All service names')}
value={search.serviceName || null}
defaultValue={search.serviceName || null}
/>
</Stack>
</InlineField>
<SearchBarInput
onChange={(v) => {
setSpanFiltersSearch({ ...search, query: v, matchesOnly: v !== '' });
}}
value={search.query || ''}
/>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label={t('explore.span-filters.label-span-name', 'Span name')} labelWidth={16}>
<Stack gap={0.5}>
<Select
aria-label={t('explore.span-filters.aria-label-select-span-name-operator', 'Select span name operator')}
onChange={(v) => setSpanFiltersSearch({ ...search, spanNameOperator: v.value! })}
options={[toOption('='), toOption('!=')]}
value={search.spanNameOperator}
/>
<Select
aria-label={t('explore.span-filters.aria-label-select-span-name', 'Select span name')}
isClearable
onChange={(v) => setSpanFiltersSearch({ ...search, spanName: v?.value || '' })}
onOpenMenu={getSpanNames}
options={spanNames || (search.spanName ? [search.spanName].map(toOption) : [])}
placeholder={t('explore.span-filters.placeholder-all-span-names', 'All span names')}
value={search.spanName || null}
/>
</Stack>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField
label={t('explore.span-filters.label-duration', 'Duration')}
labelWidth={16}
tooltip={t('explore.span-filters.tooltip-duration', 'Filter by duration. Accepted units are {{units}}', {
units: 'ns, us, ms, s, m, h',
})}
>
<Stack alignItems="flex-start" gap={0.5}>
<Select
aria-label={t('explore.span-filters.aria-label-select-min-span-operator', 'Select min span operator')}
onChange={(v) => setSpanFiltersSearch({ ...search, fromOperator: v.value! })}
options={[toOption('>'), toOption('>=')]}
value={search.fromOperator}
/>
<div className={styles.intervalInput}>
<IntervalInput
ariaLabel={t('explore.span-filters.ariaLabel-select-min-span-duration', 'Select min span duration')}
onChange={(val) => setSpanFiltersSearch({ ...search, from: val })}
isInvalidError="Invalid duration"
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="e.g. 100ms, 1.2s"
width={18}
value={search.from || ''}
validationRegex={durationRegex}
/>
</div>
<Select
aria-label={t('explore.span-filters.aria-label-select-max-span-operator', 'Select max span operator')}
onChange={(v) => setSpanFiltersSearch({ ...search, toOperator: v.value! })}
options={[toOption('<'), toOption('<=')]}
value={search.toOperator}
/>
<IntervalInput
ariaLabel={t('explore.span-filters.ariaLabel-select-max-span-duration', 'Select max span duration')}
onChange={(val) => setSpanFiltersSearch({ ...search, to: val })}
isInvalidError="Invalid duration"
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="e.g. 100ms, 1.2s"
width={18}
value={search.to || ''}
validationRegex={durationRegex}
/>
</Stack>
</InlineField>
</InlineFieldRow>
<InlineFieldRow className={styles.tagsRow}>
<InlineField
label={t('explore.span-filters.label-tags', 'Tags')}
labelWidth={16}
tooltip={t(
'explore.span-filters.tooltip-tags',
'Filter by tags, process tags or log fields in your spans.'
)}
>
<SpanFiltersTags
search={search}
setSearch={setSpanFiltersSearch}
trace={trace}
tagKeys={tagKeys}
setTagKeys={setTagKeys}
tagValues={tagValues}
setTagValues={setTagValues}
/>
</InlineField>
</InlineFieldRow>
<TracePageSearchBar
trace={trace}
search={search}
spanFilterMatches={spanFilterMatches}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
datasourceType={datasourceType}
showSpanFilters={showSpanFilters}
/>
</Collapse>
</div>
);
});
SpanFilters.displayName = 'SpanFilters';
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
label: 'SpanFilters',
margin: `0.5em 0 -${theme.spacing(1)} 0`,
zIndex: 5,
'& > div': {
borderLeft: 'none',
borderRight: 'none',
},
}),
collapseLabel: css({
svg: {
color: '#aaa',
margin: '-2px 0 0 10px',
},
}),
flexContainer: css({
display: 'flex',
justifyContent: 'space-between',
}),
intervalInput: css({
margin: '0 -4px 0 0',
}),
tagsRow: css({
margin: '-4px 0 0 0',
}),
nextPrevResult: css({
flex: 1,
alignItems: 'center',
display: 'flex',
justifyContent: 'flex-end',
marginRight: theme.spacing(1),
}),
});

View File

@@ -1,202 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { useMount } from 'react-use';
import { GrafanaTheme2, SelectableValue, toOption, TraceSearchProps, TraceSearchTag } from '@grafana/data';
import { t } from '@grafana/i18n';
import { AccessoryButton } from '@grafana/plugin-ui';
import { Input, Select, Stack, useStyles2 } from '@grafana/ui';
import { randomId } from '../../../../state/constants';
import { getTraceTagKeys, getTraceTagValues } from '../../../utils/tags';
import { Trace } from '../../types/trace';
interface Props {
search: TraceSearchProps;
setSearch: (search: TraceSearchProps) => void;
trace: Trace;
tagKeys?: Array<SelectableValue<string>>;
setTagKeys: React.Dispatch<React.SetStateAction<Array<SelectableValue<string>> | undefined>>;
tagValues: Record<string, Array<SelectableValue<string>>>;
setTagValues: React.Dispatch<React.SetStateAction<{ [key: string]: Array<SelectableValue<string>> }>>;
}
export const SpanFiltersTags = ({ search, trace, setSearch, tagKeys, setTagKeys, tagValues, setTagValues }: Props) => {
const styles = { ...useStyles2(getStyles) };
const getTagKeys = () => {
if (!tagKeys) {
setTagKeys(getTraceTagKeys(trace).map(toOption));
}
};
const getTagValues = (key: string) => {
return getTraceTagValues(trace, key).map(toOption);
};
useMount(() => {
if (search.tags) {
search.tags.forEach((tag) => {
if (tag.key) {
setTagValues({
...tagValues,
[tag.id]: getTagValues(tag.key),
});
}
});
}
});
const onTagChange = (tag: TraceSearchTag, v: SelectableValue<string>) => {
setSearch({
...search,
tags: search.tags?.map((x) => {
return x.id === tag.id ? { ...x, key: v?.value || '', value: undefined } : x;
}),
});
const loadTagValues = async () => {
if (v?.value) {
setTagValues({
...tagValues,
[tag.id]: getTagValues(v.value),
});
} else {
// removed value
const updatedValues = { ...tagValues };
if (updatedValues[tag.id]) {
delete updatedValues[tag.id];
}
setTagValues(updatedValues);
}
};
loadTagValues();
};
const addTag = () => {
const tag = {
id: randomId(),
operator: '=',
};
setSearch({ ...search, tags: [...search.tags, tag] });
};
const removeTag = (id: string) => {
let tags = search.tags.filter((tag) => {
return tag.id !== id;
});
if (tags.length === 0) {
tags = [
{
id: randomId(),
operator: '=',
},
];
}
setSearch({ ...search, tags: tags });
};
return (
<div>
{search.tags?.map((tag, i) => (
<div key={tag.id}>
<Stack gap={0} width={'auto'} justifyContent={'flex-start'} alignItems={'center'}>
<div>
<Select
aria-label={t('explore.span-filters-tags.aria-label-select-tag-key', 'Select tag key')}
isClearable
key={tag.key}
onChange={(v) => onTagChange(tag, v)}
onOpenMenu={getTagKeys}
options={tagKeys || (tag.key ? [tag.key].map(toOption) : [])}
placeholder={t('explore.span-filters-tags.placeholder-select-tag', 'Select tag')}
value={tag.key || null}
/>
</div>
<div>
<Select
aria-label={t('explore.span-filters-tags.aria-label-select-tag-operator', 'Select tag operator')}
onChange={(v) => {
setSearch({
...search,
tags: search.tags?.map((x) => {
return x.id === tag.id ? { ...x, operator: v.value! } : x;
}),
});
}}
options={[toOption('='), toOption('!='), toOption('=~'), toOption('!~')]}
value={tag.operator}
/>
</div>
<span className={styles.tagValues}>
{(tag.operator === '=' || tag.operator === '!=') && (
<Select
aria-label={t('explore.span-filters-tags.aria-label-select-tag-value', 'Select tag value')}
isClearable
key={tag.value}
onChange={(v) => {
setSearch({
...search,
tags: search.tags?.map((x) => {
return x.id === tag.id ? { ...x, value: v?.value || '' } : x;
}),
});
}}
options={tagValues[tag.id] ? tagValues[tag.id] : tag.value ? [tag.value].map(toOption) : []}
placeholder={t('explore.span-filters-tags.placeholder-select-value', 'Select value')}
value={tag.value}
/>
)}
{(tag.operator === '=~' || tag.operator === '!~') && (
<Input
aria-label={t('explore.span-filters-tags.aria-label-input-tag-value', 'Input tag value')}
onChange={(v) => {
setSearch({
...search,
tags: search.tags?.map((x) => {
return x.id === tag.id ? { ...x, value: v?.currentTarget?.value || '' } : x;
}),
});
}}
placeholder={t('explore.span-filters-tags.placeholder-tag-value', 'Tag value')}
width={18}
value={tag.value || ''}
/>
)}
</span>
{(tag.key || tag.value || search.tags.length > 1) && (
<AccessoryButton
aria-label={t('explore.span-filters-tags.aria-label-remove-tag', 'Remove tag')}
variant="secondary"
icon="times"
onClick={() => removeTag(tag.id)}
tooltip={t('explore.span-filters-tags.tooltip-remove-tag', 'Remove tag')}
/>
)}
{(tag.key || tag.value) && i === search.tags.length - 1 && (
<span className={styles.addTag}>
<AccessoryButton
aria-label={t('explore.span-filters-tags.aria-label-add-tag', 'Add tag')}
variant="secondary"
icon="plus"
onClick={addTag}
tooltip={t('explore.span-filters-tags.tooltip-add-tag', 'Add tag')}
/>
</span>
)}
</Stack>
</div>
))}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
addTag: css({
marginLeft: theme.spacing(1),
}),
tagValues: css({
maxWidth: '200px',
}),
});

View File

@@ -1,18 +1,3 @@
// Copyright (c) 2025 Grafana Labs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useMemo, useState } from 'react';
import { TraceSearchProps } from '@grafana/data';

View File

@@ -5,3 +5,5 @@ export const LIBRARY_NAME = 'library.name';
export const LIBRARY_VERSION = 'library.version';
export const TRACE_STATE = 'trace.state';
export const ID = 'id';
export const SPAN_NAME = 'span.name';
export const SERVICE_NAME = 'service.name';

View File

@@ -16,7 +16,17 @@ import { SpanStatusCode } from '@opentelemetry/api';
import { SelectableValue, TraceKeyValuePair, TraceSearchProps, TraceSearchTag } from '@grafana/data';
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../constants/span';
import {
KIND,
LIBRARY_NAME,
LIBRARY_VERSION,
STATUS,
STATUS_MESSAGE,
TRACE_STATE,
ID,
SPAN_NAME,
SERVICE_NAME,
} from '../constants/span';
import TNil from '../types/TNil';
import { TraceSpan, CriticalPathSection } from '../types/trace';
@@ -46,13 +56,13 @@ const getAdhocFilterMatches = (spans: TraceSpan[], adhocFilters: Array<Selectabl
return matchTextSearch(value, span);
}
// Special handling for serviceName
if (key === 'serviceName') {
// Special handling for service.name
if (key === SERVICE_NAME) {
return matchField(span.process.serviceName, operator, value);
}
// Special handling for spanName (operationName)
if (key === 'spanName') {
// Special handling for span.name
if (key === SPAN_NAME) {
return matchField(span.operationName, operator, value);
}

View File

@@ -99,7 +99,7 @@ describe('useSearch', () => {
// Check that adhoc filter was created
expect(result.current.search.adhocFilters).toHaveLength(1);
expect(result.current.search.adhocFilters?.[0]).toMatchObject({
key: 'serviceName',
key: 'service.name',
operator: '=',
value: 'my-service',
});
@@ -120,7 +120,7 @@ describe('useSearch', () => {
// Check that adhoc filter was created
expect(result.current.search.adhocFilters).toHaveLength(1);
expect(result.current.search.adhocFilters?.[0]).toMatchObject({
key: 'spanName',
key: 'span.name',
operator: '!=',
value: 'my-operation',
});
@@ -195,13 +195,13 @@ describe('useSearch', () => {
// Verify each filter
const filters = result.current.search.adhocFilters || [];
expect(filters.find((f) => f.key === 'serviceName')).toMatchObject({
key: 'serviceName',
expect(filters.find((f) => f.key === 'service.name')).toMatchObject({
key: 'service.name',
operator: '=',
value: 'my-service',
});
expect(filters.find((f) => f.key === 'spanName')).toMatchObject({
key: 'spanName',
expect(filters.find((f) => f.key === 'span.name')).toMatchObject({
key: 'span.name',
operator: '!=',
value: 'my-operation',
});
@@ -306,7 +306,7 @@ describe('useSearch', () => {
expect(result.current.search.adhocFilters).toHaveLength(5);
const filters = result.current.search.adhocFilters || [];
expect(filters.find((f) => f.key === 'serviceName')?.operator).toBe('!=');
expect(filters.find((f) => f.key === 'service.name')?.operator).toBe('!=');
expect(filters.find((f) => f.key === 'tag1')?.operator).toBe('=');
expect(filters.find((f) => f.key === 'tag2')?.operator).toBe('!=');
expect(filters.find((f) => f.key === 'tag3')?.operator).toBe('=~');

View File

@@ -7,6 +7,7 @@ import { useDispatch, useSelector } from 'app/types/store';
import { DEFAULT_SPAN_FILTERS, randomId } from '../state/constants';
import { changePanelState } from '../state/explorePane';
import { SPAN_NAME, SERVICE_NAME } from './components/constants/span';
import { TraceSpan, CriticalPathSection } from './components/types/trace';
import { filterSpans } from './components/utils/filter-spans';
@@ -25,7 +26,7 @@ export function migrateToAdhocFilters(search: TraceSearchProps): TraceSearchProp
// Migrate serviceName
if (search.serviceName && search.serviceName.trim() !== '') {
adhocFilters.push({
key: 'serviceName',
key: SERVICE_NAME,
operator: search.serviceNameOperator || '=',
value: search.serviceName,
});
@@ -34,7 +35,7 @@ export function migrateToAdhocFilters(search: TraceSearchProps): TraceSearchProp
// Migrate spanName
if (search.spanName && search.spanName.trim() !== '') {
adhocFilters.push({
key: 'spanName',
key: SPAN_NAME,
operator: search.spanNameOperator || '=',
value: search.spanName,
});

View File

@@ -9,6 +9,8 @@ import {
STATUS,
STATUS_MESSAGE,
TRACE_STATE,
SPAN_NAME,
SERVICE_NAME,
} from '../components/constants/span';
import { Trace } from '../components/types/trace';
@@ -37,6 +39,11 @@ export const getTraceTagKeys = (trace: Trace) => {
span.process.tags.forEach((tag) => {
keys.push(tag.key);
});
if (span.process.serviceName) {
keys.push(SERVICE_NAME);
}
if (span.logs !== null) {
span.logs.forEach((log) => {
log.fields.forEach((field) => {
@@ -63,6 +70,9 @@ export const getTraceTagKeys = (trace: Trace) => {
if (span.traceState) {
keys.push(TRACE_STATE);
}
if (span.operationName) {
keys.push(SPAN_NAME);
}
keys.push(ID);
});
keys = uniq(keys).sort();
@@ -93,6 +103,11 @@ export const getTraceTagValues = (trace: Trace, key: string) => {
}
switch (key) {
case SPAN_NAME:
if (span.operationName) {
values.push(span.operationName);
}
break;
case KIND:
if (span.kind) {
values.push(span.kind);

View File

@@ -5,13 +5,13 @@ import { MouseEvent, useCallback, useMemo } from 'react';
import {
CoreApp,
EventBus,
GrafanaTheme2,
LogLevel,
LogsDedupDescription,
LogsDedupStrategy,
LogsSortOrder,
store,
} from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data/';
import { t } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { Dropdown, Menu, useStyles2 } from '@grafana/ui';

View File

@@ -3,7 +3,7 @@ import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { render } from 'test/test-utils';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';

View File

@@ -3,7 +3,7 @@ import { useMemo, useState } from 'react';
import { useMedia } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import {

View File

@@ -117,14 +117,9 @@ export const ProvisioningWizard = memo(function ProvisioningWizard({
const [repoName = '', repoType, syncTarget] = watch(['repositoryName', 'repository.type', 'repository.sync.target']);
const [submitData] = useCreateOrUpdateRepository(repoName);
const [deleteRepository] = useDeleteRepositoryMutation();
const {
shouldSkipSync,
requiresMigration,
isLoading: isResourceStatsLoading,
} = useResourceStats(repoName, syncTarget);
const { shouldSkipSync, isLoading: isResourceStatsLoading } = useResourceStats(repoName, syncTarget);
const { createSyncJob, isLoading: isCreatingSkipJob } = useCreateSyncJob({
repoName: repoName,
requiresMigration,
setStepStatusInfo,
});
@@ -274,8 +269,8 @@ export const ProvisioningWizard = memo(function ProvisioningWizard({
if (activeStep === 'bootstrap' && canSkipSync) {
nextStepIndex = currentStepIndex + 2; // Skip to finish step
// Create a pull job to initialize the repository
const job = await createSyncJob();
// No migration needed when skipping sync
const job = await createSyncJob(false);
if (!job) {
return; // Don't proceed if job creation fails
}

View File

@@ -3,7 +3,7 @@ import { memo, useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
import { Alert, Button, Field, Spinner, Stack, Text, TextLink } from '@grafana/ui';
import { Alert, Button, Checkbox, Field, Spinner, Stack, Text, TextLink } from '@grafana/ui';
import { Job, useGetRepositoryStatusQuery } from 'app/api/clients/provisioning/v0alpha1';
import { JobStatus } from '../Job/JobStatus';
@@ -20,13 +20,17 @@ export interface SynchronizeStepProps {
}
export const SynchronizeStep = memo(function SynchronizeStep({ onCancel, isCancelling }: SynchronizeStepProps) {
const { watch } = useFormContext<WizardFormData>();
const { watch, register } = useFormContext<WizardFormData>();
const { setStepStatusInfo } = useStepStatus();
const [repoName = '', syncTarget] = watch(['repositoryName', 'repository.sync.target']);
const { requiresMigration } = useResourceStats(repoName, syncTarget);
const [repoName = '', syncTarget, migrateResources] = watch([
'repositoryName',
'repository.sync.target',
'migrate.migrateResources',
]);
const { requiresMigration } = useResourceStats(repoName, syncTarget, migrateResources);
const { createSyncJob } = useCreateSyncJob({
repoName,
requiresMigration,
setStepStatusInfo,
});
const [job, setJob] = useState<Job>();
@@ -63,7 +67,7 @@ export const SynchronizeStep = memo(function SynchronizeStep({ onCancel, isCance
const isButtonDisabled = hasError || (checked !== undefined && isRepositoryHealthy === false) || healthStatusNotReady;
const startSynchronization = async () => {
const response = await createSyncJob();
const response = await createSyncJob(requiresMigration);
if (response) {
setJob(response);
}
@@ -108,41 +112,108 @@ export const SynchronizeStep = memo(function SynchronizeStep({ onCancel, isCance
)}
{isRepositoryHealthy && (
<Alert
title={t(
'provisioning.wizard.alert-title',
'Important: No data or configuration will be lost. Dashboards remain accessible during migration, but changes made during this process may not be exported.'
)}
severity={'info'}
title={t('provisioning.wizard.alert-title', 'Important: Review Git Sync limitations before proceeding')}
severity={'warning'}
>
<ul style={{ marginLeft: '16px' }}>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-1">
Resources can still be created, edited, or deleted during this process, but changes may not be exported.
<Stack direction="column" gap={2}>
<Text>
<Trans i18nKey="provisioning.wizard.alert-intro">
Please be aware of the following limitations. For more details, see the{' '}
<TextLink
external
href="https://grafana.com/docs/grafana/latest/as-code/observability-as-code/provision-resources/intro-git-sync/"
>
Git Sync documentation
</TextLink>
.
</Trans>
</li>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-2">
Once provisioning is complete, resources will be marked as managed through external storage.
</Trans>
</li>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-3">
The duration of this process depends on the number of resources involved.
</Trans>
</li>
<li>
</Text>
<ul style={{ marginLeft: '16px', marginTop: 0, marginBottom: 0 }}>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-1">
Resources can still be created, edited, or deleted during this process, but changes may not be
exported.
</Trans>
</li>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-unsupported">
Alerts and library panels are not supported in provisioned folders.
</Trans>
</li>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-permissions">
Fine-grained permissions are not supported. Default permissions apply: Admin, Editor, and Viewer roles
are preserved with their standard access levels.
</Trans>
</li>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-3">
The duration of this process depends on the number of resources involved.
</Trans>
</li>
{syncTarget === 'instance' && (
<li>
<Trans i18nKey="provisioning.wizard.alert-point-instance-alerts">
Existing alerts and library panels will be lost and will not be usable after migration.
</Trans>
</li>
)}
{syncTarget === 'folder' && (
<>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-folder-structure">
When migrating existing dashboards, the folder structure will be replicated in the repository.
Original folders will be emptied of dashboards but may still contain alerts or library panels.
</Trans>
</li>
<li>
<Trans i18nKey="provisioning.wizard.alert-point-folder-cleanup">
You may need to manually remove or manage original folders after migration.
</Trans>
</li>
</>
)}
</ul>
<Text color="secondary" variant="bodySmall">
<Trans i18nKey="provisioning.wizard.alert-point-4">
Enterprise instance administrators can display an announcement banner to notify users that migration is
in progress. See{' '}
<TextLink external href="https://grafana.com/docs/grafana/latest/administration/announcement-banner/">
<TextLink
external
variant="bodySmall"
href="https://grafana.com/docs/grafana/latest/administration/announcement-banner/"
>
this guide
</TextLink>{' '}
for step-by-step instructions.
</Trans>
</li>
</ul>
</Text>
</Stack>
</Alert>
)}
<Text element="h3">
<Trans i18nKey="provisioning.synchronize-step.options">Options</Trans>
</Text>
<Field noMargin>
<Checkbox
{...register('migrate.migrateResources')}
id="migrate-resources"
label={t('provisioning.wizard.sync-option-migrate-resources', 'Migrate existing resources')}
checked={syncTarget === 'instance' ? true : undefined}
disabled={syncTarget === 'instance'}
description={
syncTarget === 'instance' ? (
<Trans i18nKey="provisioning.synchronize-step.instance-migrate-resources-description">
Instance sync requires all resources to be managed. Existing resources will be migrated automatically.
</Trans>
) : (
<Trans i18nKey="provisioning.synchronize-step.migrate-resources-description">
Import existing dashboards from all folders into the new provisioned folder
</Trans>
)
}
/>
</Field>
{healthStatusNotReady ? (
<>
<Stack>

View File

@@ -5,14 +5,13 @@ import { StepStatusInfo } from '../types';
export interface UseCreateSyncJobParams {
repoName: string;
requiresMigration: boolean;
setStepStatusInfo?: (info: StepStatusInfo) => void;
}
export function useCreateSyncJob({ repoName, requiresMigration, setStepStatusInfo }: UseCreateSyncJobParams) {
export function useCreateSyncJob({ repoName, setStepStatusInfo }: UseCreateSyncJobParams) {
const [createJob, { isLoading }] = useCreateRepositoryJobsMutation();
const createSyncJob = async () => {
const createSyncJob = async (requiresMigration: boolean) => {
if (!repoName) {
setStepStatusInfo?.({
status: 'error',

View File

@@ -100,7 +100,7 @@ function getResourceStats(files?: GetRepositoryFilesApiResponse, stats?: GetReso
/**
* Hook that provides resource statistics and sync logic
*/
export function useResourceStats(repoName?: string, syncTarget?: RepositoryView['target']) {
export function useResourceStats(repoName?: string, syncTarget?: RepositoryView['target'], migrateResources?: boolean) {
const resourceStatsQuery = useGetResourceStatsQuery(repoName ? undefined : skipToken);
const filesQuery = useGetRepositoryFilesQuery(repoName ? { name: repoName } : skipToken);
@@ -121,7 +121,22 @@ export function useResourceStats(repoName?: string, syncTarget?: RepositoryView[
};
}, [resourceStatsQuery.data]);
const requiresMigration = resourceCount > 0 && syncTarget === 'instance';
// Calculate base requiresMigration: true if there are resources to migrate
const baseRequiresMigration = resourceCount > 0;
// Calculate final requiresMigration based on sync target and user selection
// For instance sync: always use baseRequiresMigration (checkbox is disabled and always true)
// For folder sync: only migrate if user explicitly opts in via checkbox
const requiresMigration = useMemo(() => {
if (syncTarget === 'instance') {
return baseRequiresMigration;
}
if (syncTarget === 'folder') {
return migrateResources ?? false;
}
return baseRequiresMigration;
}, [syncTarget, baseRequiresMigration, migrateResources]);
const shouldSkipSync = (resourceCount === 0 || syncTarget === 'folder') && fileCount === 0;
// Format display strings

View File

@@ -9,6 +9,7 @@ export type RepoType = RepositorySpec['type'];
export interface MigrateFormData {
history: boolean;
identifier: boolean;
migrateResources?: boolean;
}
export interface WizardFormData {

View File

@@ -1000,7 +1000,7 @@ describe('ElasticDatasource', () => {
});
expect(postResourceRequestMock).toHaveBeenCalledWith(
'_msearch',
'{"search_type":"query_then_fetch","ignore_unavailable":true,"index":"[test-]YYYY.MM.DD"}\n{"query":{"bool":{"filter":[{"bool":{"should":[{"range":{"@test_time":{"from":1683291160012,"to":1683291460012,"format":"epoch_millis"}}},{"range":{"@time_end_field":{"from":1683291160012,"to":1683291460012,"format":"epoch_millis"}}}],"minimum_should_match":1}},{"query_string":{"query":"abc"}}]}},"size":10000}\n'
'{"search_type":"query_then_fetch","ignore_unavailable":true,"index":"[test-]YYYY.MM.DD"}\n{"query":{"bool":{"filter":[{"bool":{"should":[{"range":{"@test_time":{"gte":1683291160012,"lte":1683291460012,"format":"epoch_millis"}}},{"range":{"@time_end_field":{"gte":1683291160012,"lte":1683291460012,"format":"epoch_millis"}}}],"minimum_should_match":1}},{"query_string":{"query":"abc"}}]}},"size":10000}\n'
);
});
@@ -1030,7 +1030,7 @@ describe('ElasticDatasource', () => {
});
expect(postResourceRequestMock).toHaveBeenCalledWith(
'_msearch',
'{"search_type":"query_then_fetch","ignore_unavailable":true,"index":"[test-]YYYY.MM.DD"}\n{"query":{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"from":1683291160012,"to":1683291460012,"format":"epoch_millis"}}}],"minimum_should_match":1}}]}},"size":10000}\n'
'{"search_type":"query_then_fetch","ignore_unavailable":true,"index":"[test-]YYYY.MM.DD"}\n{"query":{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1683291160012,"lte":1683291460012,"format":"epoch_millis"}}}],"minimum_should_match":1}}]}},"size":10000}\n'
);
});
@@ -1087,7 +1087,7 @@ describe('ElasticDatasource', () => {
});
expect(postResourceRequestMock).toHaveBeenCalledWith(
'_msearch',
'{"search_type":"query_then_fetch","ignore_unavailable":true,"index":"[test-]YYYY.MM.DD"}\n{"query":{"bool":{"filter":[{"bool":{"should":[{"range":{"@test_time":{"from":1683291160012,"to":1683291460012,"format":"epoch_millis"}}},{"range":{"@time_end_field":{"from":1683291160012,"to":1683291460012,"format":"epoch_millis"}}}],"minimum_should_match":1}},{"query_string":{"query":"abc AND abc_key:\\"abc_value\\""}}]}},"size":10000}\n'
'{"search_type":"query_then_fetch","ignore_unavailable":true,"index":"[test-]YYYY.MM.DD"}\n{"query":{"bool":{"filter":[{"bool":{"should":[{"range":{"@test_time":{"gte":1683291160012,"lte":1683291460012,"format":"epoch_millis"}}},{"range":{"@time_end_field":{"gte":1683291160012,"lte":1683291460012,"format":"epoch_millis"}}}],"minimum_should_match":1}},{"query_string":{"query":"abc AND abc_key:\\"abc_value\\""}}]}},"size":10000}\n'
);
});
});

View File

@@ -294,8 +294,8 @@ export class ElasticDatasource
const dateRanges = [];
const rangeStart: RangeMap = {};
rangeStart[timeField] = {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
gte: options.range.from.valueOf(),
lte: options.range.to.valueOf(),
format: 'epoch_millis',
};
dateRanges.push({ range: rangeStart });
@@ -303,8 +303,8 @@ export class ElasticDatasource
if (timeEndField) {
const rangeEnd: RangeMap = {};
rangeEnd[timeEndField] = {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
gte: options.range.from.valueOf(),
lte: options.range.to.valueOf(),
format: 'epoch_millis',
};
dateRanges.push({ range: rangeEnd });

View File

@@ -137,7 +137,7 @@ export interface ElasticsearchAnnotationQuery {
index?: string;
}
export type RangeMap = Record<string, { from: number; to: number; format: string }>;
export type RangeMap = Record<string, { gte: number; lte: number; format: string }>;
export type ElasticsearchResponse = ElasticsearchResponseWithHits | ElasticsearchResponseWithAggregations;

View File

@@ -6,7 +6,7 @@ import { PureComponent } from 'react';
import tinycolor from 'tinycolor2';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors/src';
import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
import { config } from 'app/core/config';
@@ -44,7 +44,7 @@ export class DebugOverlay extends PureComponent<Props, State> {
const { zoom, center } = this.state;
return (
<div className={this.style.infoWrap} aria-label={selectors.components.DebugOverlay.wrapper}>
<div className={this.style.infoWrap} data-testid={selectors.components.DebugOverlay.wrapper}>
<table>
<tbody>
<tr>

View File

@@ -5392,10 +5392,6 @@
"title": "Options",
"tooltip": "Dashboard options"
},
"edit-schema": {
"title": "Code",
"tooltip": "Edit as code"
},
"export": {
"title": "Export",
"unsaved-modal": {
@@ -7641,39 +7637,6 @@
},
"share-span": "Share"
},
"span-filters": {
"aria-label-select-max-span-operator": "Select max span operator",
"aria-label-select-min-span-operator": "Select min span operator",
"aria-label-select-service-name": "Select service name",
"aria-label-select-service-name-operator": "Select service name operator",
"aria-label-select-span-name": "Select span name",
"aria-label-select-span-name-operator": "Select span name operator",
"ariaLabel-select-max-span-duration": "Select max span duration",
"ariaLabel-select-min-span-duration": "Select min span duration",
"label-collapse": "Span Filters",
"label-duration": "Duration",
"label-service-name": "Service name",
"label-span-name": "Span name",
"label-tags": "Tags",
"placeholder-all-service-names": "All service names",
"placeholder-all-span-names": "All span names",
"tooltip-collapse": "Filter your spans below. You can continue to apply filters until you have narrowed down your resulting spans to the select few you are most interested in.",
"tooltip-duration": "Filter by duration. Accepted units are {{units}}",
"tooltip-tags": "Filter by tags, process tags or log fields in your spans."
},
"span-filters-tags": {
"aria-label-add-tag": "Add tag",
"aria-label-input-tag-value": "Input tag value",
"aria-label-remove-tag": "Remove tag",
"aria-label-select-tag-key": "Select tag key",
"aria-label-select-tag-operator": "Select tag operator",
"aria-label-select-tag-value": "Select tag value",
"placeholder-select-tag": "Select tag",
"placeholder-select-value": "Select value",
"placeholder-tag-value": "Tag value",
"tooltip-add-tag": "Add tag",
"tooltip-remove-tag": "Remove tag"
},
"span-flame-graph": {
"flame-graph": "Flame graph"
},
@@ -12182,6 +12145,9 @@
"tooltip-unhealthy-repository": "Unable to pull an unhealthy repository"
},
"synchronize-step": {
"instance-migrate-resources-description": "Instance sync requires all resources to be managed. Existing resources will be migrated automatically.",
"migrate-resources-description": "Import existing dashboards from all folders into the new provisioned folder",
"options": "Options",
"repository-error": "Repository error",
"repository-error-message": "Unable to check repository status. Please verify the repository configuration and try again.",
"repository-unhealthy": "The repository cannot be synchronized. Cancel provisioning and try again once the issue has been resolved. See details below."
@@ -12201,11 +12167,16 @@
},
"warning-title-default": "Warning",
"wizard": {
"alert-intro": "Please be aware of the following limitations. For more details, see the <2>Git Sync documentation</2>.",
"alert-point-1": "Resources can still be created, edited, or deleted during this process, but changes may not be exported.",
"alert-point-2": "Once provisioning is complete, resources will be marked as managed through external storage.",
"alert-point-3": "The duration of this process depends on the number of resources involved.",
"alert-point-4": "Enterprise instance administrators can display an announcement banner to notify users that migration is in progress. See <2>this guide</2> for step-by-step instructions.",
"alert-title": "Important: No data or configuration will be lost. Dashboards remain accessible during migration, but changes made during this process may not be exported.",
"alert-point-folder-cleanup": "You may need to manually remove or manage original folders after migration.",
"alert-point-folder-structure": "When migrating existing dashboards, the folder structure will be replicated in the repository. Original folders will be emptied of dashboards but may still contain alerts or library panels.",
"alert-point-instance-alerts": "Existing alerts and library panels will be lost and will not be usable after migration.",
"alert-point-permissions": "Fine-grained permissions are not supported. Default permissions apply: Admin, Editor, and Viewer roles are preserved with their standard access levels.",
"alert-point-unsupported": "Alerts and library panels are not supported in provisioned folders.",
"alert-title": "Important: Review Git Sync limitations before proceeding",
"button-cancel": "Cancel",
"button-cancelling": "Cancelling...",
"button-next": "Finish",
@@ -12223,6 +12194,7 @@
"step-finish": "Choose additional settings",
"step-synchronize": "Synchronize with external storage",
"sync-description": "Sync resources with external storage. After this one-time step, all future updates will be automatically saved to the repository and provisioned back into the instance.",
"sync-option-migrate-resources": "Migrate existing resources",
"title-bootstrap": "Choose what to synchronize",
"title-connect": "Connect to external storage",
"title-finish": "Choose additional settings",

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