Merge branch 'main' into ash/32369
This commit is contained in:
+6
-19
@@ -8,14 +8,14 @@ exports[`no enzyme tests`] = {
|
||||
"packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:2976628669": [
|
||||
[0, 26, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js:1676554632": [
|
||||
[14, 19, 13, "RegExp match", "2409514259"]
|
||||
"packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx:793800575": [
|
||||
[14, 35, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js:186764954": [
|
||||
[14, 19, 13, "RegExp match", "2409514259"]
|
||||
"packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx:596989456": [
|
||||
[14, 35, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:1734982398": [
|
||||
[14, 26, 13, "RegExp match", "2409514259"]
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx:3266788928": [
|
||||
[14, 56, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js:989353473": [
|
||||
[15, 17, 13, "RegExp match", "2409514259"]
|
||||
@@ -2907,9 +2907,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/features/alerting/unified/RuleEditor.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/RuleList.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@@ -6309,22 +6306,12 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/prometheus/components/PromExploreExtraField.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromLink.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromLink.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
||||
+66
-38
@@ -2,6 +2,9 @@ import { regexp } from '@betterer/regexp';
|
||||
import { BettererFileTest } from '@betterer/betterer';
|
||||
import { ESLint, Linter } from 'eslint';
|
||||
import { existsSync } from 'fs';
|
||||
import { exec } from 'child_process';
|
||||
import path from 'path';
|
||||
import glob from 'glob';
|
||||
|
||||
export default {
|
||||
'no enzyme tests': () => regexp(/from 'enzyme'/g).include('**/*.test.*'),
|
||||
@@ -22,54 +25,79 @@ function countUndocumentedStories() {
|
||||
});
|
||||
}
|
||||
|
||||
async function findEslintConfigFiles(): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
glob('**/.eslintrc', (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function countEslintErrors() {
|
||||
return new BettererFileTest(async (filePaths, fileTestResult, resolver) => {
|
||||
const { baseDirectory } = resolver;
|
||||
const cli = new ESLint({ cwd: baseDirectory });
|
||||
|
||||
await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
const linterOptions = (await cli.calculateConfigForFile(filePath)) as Linter.Config;
|
||||
const eslintConfigFiles = await findEslintConfigFiles();
|
||||
const eslintConfigMainPaths = eslintConfigFiles.map((file) => path.resolve(path.dirname(file)));
|
||||
|
||||
const rules: Partial<Linter.RulesRecord> = {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
};
|
||||
const baseRules: Partial<Linter.RulesRecord> = {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
};
|
||||
|
||||
const isTestFile =
|
||||
filePath.endsWith('.test.tsx') ||
|
||||
filePath.endsWith('.test.ts') ||
|
||||
filePath.includes('__mocks__') ||
|
||||
filePath.includes('public/test/');
|
||||
const nonTestFilesRules: Partial<Linter.RulesRecord> = {
|
||||
...baseRules,
|
||||
'@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }],
|
||||
};
|
||||
|
||||
if (!isTestFile) {
|
||||
rules['@typescript-eslint/consistent-type-assertions'] = [
|
||||
'error',
|
||||
{
|
||||
assertionStyle: 'never',
|
||||
},
|
||||
];
|
||||
}
|
||||
// group files by eslint config file
|
||||
// this will create two file groups for each eslint config file
|
||||
// one for test files and one for non-test files
|
||||
const fileGroups: Record<string, string[]> = {};
|
||||
|
||||
const runner = new ESLint({
|
||||
baseConfig: {
|
||||
...linterOptions,
|
||||
rules,
|
||||
},
|
||||
useEslintrc: false,
|
||||
cwd: baseDirectory,
|
||||
});
|
||||
for (const filePath of filePaths) {
|
||||
let configPath = eslintConfigMainPaths.find((configPath) => filePath.startsWith(configPath)) ?? '';
|
||||
const isTestFile =
|
||||
filePath.endsWith('.test.tsx') ||
|
||||
filePath.endsWith('.test.ts') ||
|
||||
filePath.includes('__mocks__') ||
|
||||
filePath.includes('public/test/');
|
||||
|
||||
const lintResults = await runner.lintFiles([filePath]);
|
||||
lintResults
|
||||
.filter((lintResult) => lintResult.source)
|
||||
.forEach((lintResult) => {
|
||||
const { messages } = lintResult;
|
||||
const file = fileTestResult.addFile(filePath, '');
|
||||
messages.forEach((message, index) => {
|
||||
file.addIssue(0, 0, message.message, `${index}`);
|
||||
});
|
||||
if (isTestFile) {
|
||||
configPath += '-test';
|
||||
}
|
||||
if (!fileGroups[configPath]) {
|
||||
fileGroups[configPath] = [];
|
||||
}
|
||||
fileGroups[configPath].push(filePath);
|
||||
}
|
||||
|
||||
for (const configPath of Object.keys(fileGroups)) {
|
||||
const rules = configPath.endsWith('-test') ? baseRules : nonTestFilesRules;
|
||||
// this is by far the slowest part of this code. It takes eslint about 2 seconds just to find the config
|
||||
const linterOptions = (await cli.calculateConfigForFile(fileGroups[configPath][0])) as Linter.Config;
|
||||
const runner = new ESLint({
|
||||
baseConfig: {
|
||||
...linterOptions,
|
||||
rules: rules,
|
||||
},
|
||||
useEslintrc: false,
|
||||
cwd: baseDirectory,
|
||||
});
|
||||
const lintResults = await runner.lintFiles(fileGroups[configPath]);
|
||||
lintResults
|
||||
.filter((lintResult) => lintResult.source)
|
||||
.forEach((lintResult) => {
|
||||
const { messages } = lintResult;
|
||||
const filePath = lintResult.filePath;
|
||||
const file = fileTestResult.addFile(filePath, '');
|
||||
messages.forEach((message, index) => {
|
||||
file.addIssue(0, 0, message.message, `${index}`);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+37
-9
@@ -7,17 +7,45 @@
|
||||
load('scripts/drone/events/pr.star', 'pr_pipelines')
|
||||
load('scripts/drone/events/main.star', 'main_pipelines')
|
||||
load('scripts/drone/pipelines/docs.star', 'docs_pipelines')
|
||||
load('scripts/drone/events/release.star', 'oss_pipelines', 'enterprise_pipelines', 'enterprise2_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline', 'artifacts_page_pipeline')
|
||||
load('scripts/drone/pipelines/publish_images.star', 'publish_image_pipelines_public', 'publish_image_pipelines_security')
|
||||
load(
|
||||
'scripts/drone/events/release.star',
|
||||
'oss_pipelines',
|
||||
'enterprise_pipelines',
|
||||
'enterprise2_pipelines',
|
||||
'publish_artifacts_pipelines',
|
||||
'publish_npm_pipelines',
|
||||
'publish_packages_pipeline',
|
||||
'artifacts_page_pipeline',
|
||||
)
|
||||
load(
|
||||
'scripts/drone/pipelines/publish_images.star',
|
||||
'publish_image_pipelines_public',
|
||||
'publish_image_pipelines_security',
|
||||
)
|
||||
load('scripts/drone/version.star', 'version_branch_pipelines')
|
||||
load('scripts/drone/events/cron.star', 'cronjobs')
|
||||
load('scripts/drone/vault.star', 'secrets')
|
||||
|
||||
|
||||
def main(ctx):
|
||||
edition = 'oss'
|
||||
return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + oss_pipelines() + enterprise_pipelines() + enterprise2_pipelines() + \
|
||||
enterprise2_pipelines(prefix='custom-', trigger = {'event': ['custom']},) + \
|
||||
publish_image_pipelines_public() + publish_image_pipelines_security() + \
|
||||
publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \
|
||||
publish_npm_pipelines('public') + publish_packages_pipeline() + artifacts_page_pipeline() + \
|
||||
version_branch_pipelines() + cronjobs(edition=edition) + secrets()
|
||||
return (
|
||||
pr_pipelines()
|
||||
+ main_pipelines()
|
||||
+ oss_pipelines()
|
||||
+ enterprise_pipelines()
|
||||
+ enterprise2_pipelines()
|
||||
+ enterprise2_pipelines(
|
||||
prefix='custom-',
|
||||
trigger={'event': ['custom']},
|
||||
)
|
||||
+ publish_image_pipelines_public()
|
||||
+ publish_image_pipelines_security()
|
||||
+ publish_artifacts_pipelines('security')
|
||||
+ publish_artifacts_pipelines('public')
|
||||
+ publish_npm_pipelines()
|
||||
+ publish_packages_pipeline()
|
||||
+ artifacts_page_pipeline()
|
||||
+ version_branch_pipelines()
|
||||
+ cronjobs()
|
||||
+ secrets()
|
||||
)
|
||||
|
||||
+17
-5
@@ -575,7 +575,7 @@ steps:
|
||||
- failure
|
||||
- commands:
|
||||
- yarn storybook:build
|
||||
- ./bin/grabpl verify-storybook
|
||||
- ./bin/build verify-storybook
|
||||
depends_on:
|
||||
- build-frontend
|
||||
- build-frontend-packages
|
||||
@@ -1430,7 +1430,7 @@ steps:
|
||||
- failure
|
||||
- commands:
|
||||
- yarn storybook:build
|
||||
- ./bin/grabpl verify-storybook
|
||||
- ./bin/build verify-storybook
|
||||
depends_on:
|
||||
- build-frontend
|
||||
- build-frontend-packages
|
||||
@@ -2155,7 +2155,7 @@ steps:
|
||||
- failure
|
||||
- commands:
|
||||
- yarn storybook:build
|
||||
- ./bin/grabpl verify-storybook
|
||||
- ./bin/build verify-storybook
|
||||
depends_on:
|
||||
- build-frontend
|
||||
- build-frontend-packages
|
||||
@@ -3857,6 +3857,10 @@ platform:
|
||||
os: linux
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
image: alpine:3.15.6
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.17/grabpl
|
||||
@@ -3949,6 +3953,10 @@ platform:
|
||||
os: linux
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
image: alpine:3.15.6
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.17/grabpl
|
||||
@@ -4024,6 +4032,10 @@ platform:
|
||||
os: linux
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
image: alpine:3.15.6
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.17/grabpl
|
||||
@@ -4687,7 +4699,7 @@ steps:
|
||||
- failure
|
||||
- commands:
|
||||
- yarn storybook:build
|
||||
- ./bin/grabpl verify-storybook
|
||||
- ./bin/build verify-storybook
|
||||
depends_on:
|
||||
- build-frontend
|
||||
- build-frontend-packages
|
||||
@@ -6306,6 +6318,6 @@ kind: secret
|
||||
name: packages_secret_access_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: e7746a4b35fba9e1a7cb3096b947a874786b082f41e4252448ac6acde7ee3ccf
|
||||
hmac: dcf24226fae30872050cdc031430374d811e6bbe13158ce0fbf234c90c1d83f9
|
||||
|
||||
...
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"react-dom",
|
||||
"react-test-renderer"
|
||||
],
|
||||
"includePaths": ["package.json", "packages/*"],
|
||||
"ignorePaths": ["packages/grafana-toolkit/package.json", "emails/**", "plugins-bundled/**", "**/mocks/**"],
|
||||
"labels": ["area/frontend", "dependencies", "no-backport", "no-changelog"],
|
||||
"packageRules": [
|
||||
@@ -79,6 +80,7 @@
|
||||
"enabled": false
|
||||
},
|
||||
"prConcurrentLimit": 10,
|
||||
"rebaseWhen": "conflicted",
|
||||
"reviewers": ["team:grafana/frontend-ops"],
|
||||
"separateMajorMinor": false,
|
||||
"vulnerabilityAlerts": {
|
||||
|
||||
@@ -233,5 +233,8 @@ drone: $(DRONE)
|
||||
$(DRONE) lint .drone.yml --trusted
|
||||
$(DRONE) --server https://drone.grafana.net sign --save grafana/grafana
|
||||
|
||||
format-drone:
|
||||
black --include '\.star$$' -S scripts/drone/ .drone.star
|
||||
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
@@ -162,6 +162,12 @@ type = database
|
||||
# memcache: 127.0.0.1:11211
|
||||
connstr =
|
||||
|
||||
# prefix prepended to all the keys in the remote cache
|
||||
prefix =
|
||||
|
||||
# This enables encryption of values stored in the remote cache
|
||||
encryption =
|
||||
|
||||
#################################### Data proxy ###########################
|
||||
[dataproxy]
|
||||
|
||||
|
||||
@@ -169,6 +169,12 @@
|
||||
# memcache: 127.0.0.1:11211
|
||||
;connstr =
|
||||
|
||||
# prefix prepended to all the keys in the remote cache
|
||||
; prefix =
|
||||
|
||||
# This enables encryption of values stored in the remote cache
|
||||
;encryption =
|
||||
|
||||
#################################### Data proxy ###########################
|
||||
[dataproxy]
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ To view available data source plugins, go to the [plugin catalog](/grafana/plugi
|
||||
For details about the plugin catalog, refer to [Plugin management]({{< relref "../../administration/plugin-management/" >}}).
|
||||
|
||||
You can further filter the plugin catalog's results for data sources provided by the Grafana community, Grafana Labs, and partners.
|
||||
If you use [Grafana Enterprise]{{< relref "../../enterprise/" >}}, you can also filter by Enterprise-supported plugins.
|
||||
If you use [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}), you can also filter by Enterprise-supported plugins.
|
||||
|
||||
For more documentation on a specific data source plugin's features, including its query language and editor, refer to its plugin catalog page.
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ The following tables list permissions associated with basic and fixed roles.
|
||||
| Basic role | Associated fixed roles | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixes:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixed:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
|
||||
|
||||
+9
-5
@@ -26,13 +26,17 @@ Grafana performs provisioning during startup. After you make a change to the con
|
||||
|
||||
1. Sign in to the Grafana server.
|
||||
|
||||
2. Locate the Grafana provisioning folder.
|
||||
1. Locate the Grafana provisioning folder.
|
||||
|
||||
3. Create a new YAML in the following folder: **provisioning/access-control**. For example, `provisioning/access-control/custom-roles.yml`
|
||||
1. Create a new YAML in the following folder: **provisioning/access-control**. For example, `provisioning/access-control/custom-roles.yml`
|
||||
|
||||
4. Add RBAC provisioning details to the configuration file. See [manage RBAC roles]({{< relref "./manage-rbac-roles/" >}}) and [assign RBAC roles]({{< relref "./assign-rbac-roles/" >}}) for instructions, and see this [example role provisioning file]({{< relref "./rbac-provisioning/#example" >}}) for a complete example of a provisioning file.
|
||||
1. Add RBAC provisioning details to the configuration file.
|
||||
|
||||
5. Reload the provisioning configuration file.
|
||||
Refer to [Manage RBAC roles]({{< relref "./manage-rbac-roles/" >}}) and [Assign RBAC roles]({{< relref "./assign-rbac-roles/" >}}) for instructions.
|
||||
|
||||
Refer to [example role provisioning file]({{< relref "#example-role-configuration-file-using-grafana-provisioning" >}}) for a complete example of a provisioning file.
|
||||
|
||||
1. Reload the provisioning configuration file.
|
||||
|
||||
For more information about reloading the provisioning configuration at runtime, refer to [Reload provisioning configurations]({{< relref "../../../../developers/http_api/admin/#reload-provisioning-configurations" >}}).
|
||||
|
||||
@@ -46,7 +50,7 @@ The following example shows a complete YAML configuration file that:
|
||||
- Assign roles to teams
|
||||
- Revoke assignments of roles to teams
|
||||
|
||||
## Example
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
||||
@@ -42,10 +42,10 @@ We support the latest two minor versions of both Prometheus and Alertmanager. We
|
||||
|
||||
As an example, if the current Prometheus version is `2.31.1`, we support >= `2.29.0`.
|
||||
|
||||
## Grafana is not an alert receiver
|
||||
## The Grafana Alertmanager can only receive Grafana managed alerts
|
||||
|
||||
Grafana is not an alert receiver; it is an alert generator. This means that Grafana cannot receive alerts from anything other than its internal alert generator.
|
||||
Grafana cannot be used to receive external alerts. You can only send alerts to the Grafana Alertmanager using Grafana managed alerts.
|
||||
|
||||
Receiving alerts from Prometheus (or anything else) is not supported at the time.
|
||||
You have the option to send Grafana managed alerts to an external Alertmanager, you can find this option in the admin tab on the Alerting page.
|
||||
|
||||
For more information, refer to [this GitHub discussion](https://github.com/grafana/grafana/discussions/45773).
|
||||
For more information, refer to [this GitHub discussion](https://github.com/grafana/grafana/discussions/45773). To learn more about the different Alertmanagers, read [this documentation]({{< relref "../alerting/manage-notifications/alertmanager/" >}})
|
||||
|
||||
@@ -17,17 +17,22 @@ weight: 150
|
||||
|
||||
# Alertmanager data source
|
||||
|
||||
Grafana includes built-in support for Prometheus Alertmanager. Once you add it as a data source, you can use the [Grafana Alerting UI](/docs/grafana/latest/alerting/) to manage silences, contact points as well as notification policies. A drop-down option in these pages allows you to switch between Grafana and any configured Alertmanager data sources.
|
||||
Grafana includes built-in support for Alertmanager implementations in Prometheus and Mimir.
|
||||
Once you add it as a data source, you can use the [Grafana Alerting UI](/docs/grafana/latest/alerting/) to manage silences, contact points, and notification policies.
|
||||
To switch between Grafana and any configured Alertmanager data sources, you can select your preference from a drop-down option in those databases' data source settings pages.
|
||||
|
||||
## Alertmanager implementations
|
||||
|
||||
[Prometheus](https://prometheus.io/) and [Grafana Mimir](/docs/mimir/latest/) (default) implementations of Alertmanager are supported. You can specify implementation in the data source settings page. In case of Prometheus contact points and notification policies are read-only in the Grafana Alerting UI, as it does not support updating configuration via HTTP API.
|
||||
The data source supports [Prometheus](https://prometheus.io/) and [Grafana Mimir](https://grafana.com/docs/mimir/latest/) (default) implementations of Alertmanager.
|
||||
You can specify the implementation in the data source's Settings page.
|
||||
When using Prometheus, contact points and notification policies are read-only in the Grafana Alerting UI, because it doesn't support updates to the configuration using HTTP API.
|
||||
|
||||
## Provision the data source
|
||||
## Provision the Alertmanager data source
|
||||
|
||||
Configure the Alertmanager data sources by updating Grafana's configuration files. For more information on how it works and the settings available, refer to the [provisioning docs page]({{< relref "../../administration/provisioning#data-sources" >}}).
|
||||
You can provision Alertmanager data sources by updating Grafana's configuration files.
|
||||
For more information on provisioning, and common settings available, refer to the [provisioning docs page]({{< relref "../administration/provisioning/#datasources" >}}).
|
||||
|
||||
For example, this YAML provisions an Alertmanager data source running on port 9093, with proxy access and basic authentication:
|
||||
Here is an example for provisioning the Alertmanager data source:
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
@@ -38,6 +43,8 @@ datasources:
|
||||
url: http://localhost:9093
|
||||
access: proxy
|
||||
jsonData:
|
||||
# Options for implementation include prometheus and mimir
|
||||
implementation: prometheus
|
||||
# optionally
|
||||
basicAuth: true
|
||||
basicAuthUser: my_user
|
||||
|
||||
@@ -38,7 +38,7 @@ Once you've added the Google Cloud Monitoring data source, you can [configure it
|
||||
|
||||
1. Hover the cursor over the **Configuration** (gear) icon.
|
||||
1. Select **Data Sources**.
|
||||
1. Select the AWS CloudWatch data source.
|
||||
1. Select the **Google Cloud Monitoring** data source.
|
||||
|
||||
Set the data source's basic configuration options carefully:
|
||||
|
||||
|
||||
@@ -43,21 +43,28 @@ For more information on how to query other Prometheus-compatible projects from G
|
||||
|
||||
Set the data source's basic configuration options carefully:
|
||||
|
||||
| Name | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Url** | Sets the URL of your Prometheus server, such as `http://prometheus.example.org:9090`. |
|
||||
| **Access** | Only Server access mode is functional. If Server mode is already selected, this option is hidden. Otherwise, change this to Server mode to prevent errors. |
|
||||
| **Basic Auth** | Enables basic authentication to the Prometheus data source. |
|
||||
| **User** | Sets the user name for basic authentication. |
|
||||
| **Password** | Sets the password for basic authentication. |
|
||||
| **Scrape interval** | Sets the scrape and evaluation interval. We recommend the same value as the typical configured in Prometheus. Defaults to 15s. |
|
||||
| **Type** | Defines the type of your Prometheus server. Valid values are `Prometheus`, `Cortex`, `Thanos`, `Mimir`. When selected, the Prometheus version field attempts to detect the version automatically using the Prometheus [buildinfo](https://semver.org/) API. Some Prometheus types, such as Cortex, don't support this API, and you must provide their version. |
|
||||
| **Version** | Defines the version of your Prometheus server. This field is visible only after the **Type** field is defined. |
|
||||
| **HTTP method** | Sets the HTTP method used to query your data source. We recommend POST, which is pre-selected, because it allows for larger queries. Use GET if the Prometheus version is older than 2.1, or if POST requests are restricted in your network. |
|
||||
| **Disable metrics lookup** | Disables the metrics chooser and metric/label support in the query field's autocompletion. This can prevent performance issues with larger Prometheus instances. |
|
||||
| **Custom query parameters** | Adds custom parameters to the Prometheus query URL, such as `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Concatenate multiple parameters with '&'. |
|
||||
| Name | Description |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
|
||||
| `Default` | Default data source that is pre-selected for new panels. |
|
||||
| `URL` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
|
||||
| `Access` | Only Server access mode is functional. If Server mode is already selected this option is hidden. Otherwise change to Server mode to prevent errors. |
|
||||
| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
|
||||
| `User` | User name for basic authentication. |
|
||||
| `Password` | Password for basic authentication. |
|
||||
| `Manage alerts via Alerting UI` | Toggle whether to enable Alertmanager integration for this data source. |
|
||||
| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
|
||||
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
|
||||
| `Type` | The type of your Prometheus server; `Prometheus`, `Cortex`, `Thanos`, `Mimir`. When selected, the **Version** field attempts to populate automatically using the Prometheus [buildinfo](https://semver.org/) API. Some Prometheus types, such as Cortex, don't support this API and must be manually populated. |
|
||||
| `Version` | The version of your Prometheus server, note that this field is not visible until the Prometheus type is selected. |
|
||||
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
|
||||
| `Custom query parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
|
||||
| **Exemplars configuration** | |
|
||||
| `Internal link` | Enable this option is you have an internal link. When you enable this option, you will see a data source selector. Select the backend tracing data store for your exemplar data. |
|
||||
| `Data source` | You will see this option only if you enable `Internal link` option. Select the backend tracing data store for your exemplar data. |
|
||||
| `URL` | You will see this option only if the `Internal link` option is disabled. Enter the full URL of the external link. You can interpolate the value from the field with `${__value.raw }` macro. |
|
||||
| `URL Label` | (Optional) add a custom display label to override the value of the `Label name` field. |
|
||||
| `Label name` | Add a name for the exemplar traceID property. |
|
||||
|
||||
**Exemplars configuration:**
|
||||
|
||||
@@ -87,6 +94,9 @@ datasources:
|
||||
url: http://localhost:9090
|
||||
jsonData:
|
||||
httpMethod: POST
|
||||
manageAlerts: true
|
||||
prometheusType: Prometheus
|
||||
prometheusVersion: 2.37.0
|
||||
exemplarTraceIdDestinations:
|
||||
# Field with internal link pointing to data source in Grafana.
|
||||
# datasourceUid value can be anything, but it should be unique across all defined data source uids.
|
||||
|
||||
@@ -53,6 +53,7 @@ Alpha features might be changed or removed without prior notice.
|
||||
|
||||
| Feature toggle name | Description |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `returnUnameHeader` | Return user login as header for authenticated requests |
|
||||
| `alertingBigTransactions` | Use big transactions for alerting database writes |
|
||||
| `dashboardPreviews` | Create and show thumbnails for dashboard search results |
|
||||
| `live-config` | Save Grafana Live configuration in SQL tables |
|
||||
@@ -89,8 +90,10 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
||||
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
||||
| `elasticsearchBackendMigration` | Use Elasticsearch as backend data source |
|
||||
| `datasourceOnboarding` | Enable data source onboarding page |
|
||||
| `secureSocksDatasourceProxy` | Enable secure socks tunneling for supported core datasources |
|
||||
| `authnService` | Use new auth service to perform authentication |
|
||||
| `sessionRemoteCache` | Enable using remote cache for user sessions |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ Logs of usage insights contain the following fields, where the fields followed b
|
||||
| `panelName` | string | Name of the panel of the query. |
|
||||
| `error` | string | Error returned by the query. |
|
||||
| `duration` | number | Duration of the query. |
|
||||
| `source` | string | Source of the query. For example, `dashboard` or `explore`. |
|
||||
| `orgId`\* | number | ID of the user’s organization. |
|
||||
| `orgName`\* | string | Name of the user’s organization. |
|
||||
| `timestamp`\* | string | The date and time that the request was made, in Coordinated Universal Time (UTC) in [RFC3339](https://tools.ietf.org/html/rfc3339#section-5.6) format. |
|
||||
|
||||
@@ -59,7 +59,7 @@ require (
|
||||
github.com/grafana/cuetsy v0.1.1
|
||||
github.com/grafana/grafana-aws-sdk v0.11.0
|
||||
github.com/grafana/grafana-azure-sdk-go v1.3.1
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.142.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.145.0
|
||||
github.com/grafana/thema v0.0.0-20221113112305-b441ed85a1fd
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/hashicorp/go-hclog v1.0.0
|
||||
@@ -121,7 +121,7 @@ require (
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
xorm.io/builder v0.3.6
|
||||
xorm.io/core v0.7.3
|
||||
@@ -304,7 +304,11 @@ require (
|
||||
github.com/segmentio/asm v1.1.4 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect
|
||||
github.com/unknwon/com v1.0.1 // indirect
|
||||
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect
|
||||
go.starlark.net v0.0.0-20221020143700-22309ac47eac // indirect
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -1342,6 +1342,8 @@ github.com/gophercloud/gophercloud v0.18.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075
|
||||
github.com/gophercloud/gophercloud v0.20.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4=
|
||||
github.com/gophercloud/gophercloud v0.24.0 h1:jDsIMGJ1KZpAjYfQgGI2coNQj5Q83oPzuiGJRFWgMzw=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de h1:F7WD09S8QB4LrkEpka0dFPLSotH11HRpCsLIbIcJ7sU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
@@ -1376,8 +1378,8 @@ github.com/grafana/grafana-azure-sdk-go v1.3.1/go.mod h1:rgrnK9m6CgKlgx4rH3FFP/6
|
||||
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE=
|
||||
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.142.0 h1:fDgA0EmWWy5+/7nX7fdHBfADR6pWuR1TZA5QL36VX7U=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.142.0/go.mod h1:srvRQ+de4C5h7FqA5lSFUkFCs5pJolWT+PGV2AyBOFk=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.145.0 h1:ZlRxxV3C6RA+wNWeGr+rLVD70pgsZwiLI9etzE0zu+Q=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.145.0/go.mod h1:dFof/7GenWBFTmrfcPRCpLau7tgIED0ykzupWAlB0o0=
|
||||
github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293 h1:dJIdfHqu+XjKz+w9zXLqXKPdp6Jjx/UPSOwdeSfWdeQ=
|
||||
github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293/go.mod h1:HVHqK+BVPa/tmL8EMhLCCrPt2a1GdJpEyxr5hgur2UI=
|
||||
github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4=
|
||||
@@ -1665,6 +1667,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o=
|
||||
github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
@@ -2309,8 +2313,12 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/snowflakedb/gosnowflake v1.3.4/go.mod h1:NsRq2QeiMUuoNUJhp5Q6xGC4uBrsS9g6LwZVEkTWgsE=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
@@ -2435,6 +2443,12 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI=
|
||||
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4=
|
||||
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
|
||||
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg=
|
||||
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
@@ -2979,6 +2993,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
+14
-14
@@ -30,7 +30,7 @@ func main() {
|
||||
// Core kinds composite code generator. Produces all generated code in
|
||||
// grafana/grafana that derives from raw and structured core kinds.
|
||||
coreKindsGen := codejen.JennyListWithNamer(func(decl *codegen.DeclForGen) string {
|
||||
return decl.Meta.Common().MachineName
|
||||
return decl.Properties.Common().MachineName
|
||||
})
|
||||
|
||||
// All the jennies that comprise the core kinds generator pipeline
|
||||
@@ -63,12 +63,12 @@ func main() {
|
||||
continue
|
||||
}
|
||||
rel := filepath.Join(kindsys.CoreStructuredDeclParentPath, ent.Name())
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rel, rt.Context(), nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rel, rt.Context(), nil)
|
||||
if err != nil {
|
||||
die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil)))
|
||||
}
|
||||
if decl.Meta.MachineName != ent.Name() {
|
||||
die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name()))
|
||||
if decl.Properties.MachineName != ent.Name() {
|
||||
die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Properties.Name, ent.Name()))
|
||||
}
|
||||
|
||||
all = append(all, elsedie(codegen.ForGen(rt, decl.Some()))(rel))
|
||||
@@ -82,19 +82,19 @@ func main() {
|
||||
continue
|
||||
}
|
||||
rel := filepath.Join(kindsys.RawDeclParentPath, ent.Name())
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.RawMeta](rel, rt.Context(), nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.RawProperties](rel, rt.Context(), nil)
|
||||
if err != nil {
|
||||
die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil)))
|
||||
}
|
||||
if decl.Meta.MachineName != ent.Name() {
|
||||
die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name()))
|
||||
if decl.Properties.MachineName != ent.Name() {
|
||||
die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Properties.Name, ent.Name()))
|
||||
}
|
||||
dfg, _ := codegen.ForGen(nil, decl.Some())
|
||||
all = append(all, dfg)
|
||||
}
|
||||
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return nameFor(all[i].Meta) < nameFor(all[j].Meta)
|
||||
return nameFor(all[i].Properties) < nameFor(all[j].Properties)
|
||||
})
|
||||
|
||||
jfs, err := coreKindsGen.GenerateFS(all...)
|
||||
@@ -111,18 +111,18 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func nameFor(m kindsys.SomeKindMeta) string {
|
||||
func nameFor(m kindsys.SomeKindProperties) string {
|
||||
switch x := m.(type) {
|
||||
case kindsys.RawMeta:
|
||||
case kindsys.RawProperties:
|
||||
return x.Name
|
||||
case kindsys.CoreStructuredMeta:
|
||||
case kindsys.CoreStructuredProperties:
|
||||
return x.Name
|
||||
case kindsys.CustomStructuredMeta:
|
||||
case kindsys.CustomStructuredProperties:
|
||||
return x.Name
|
||||
case kindsys.ComposableMeta:
|
||||
case kindsys.ComposableProperties:
|
||||
return x.Name
|
||||
default:
|
||||
// unreachable so long as all the possibilities in KindMetas have switch branches
|
||||
// unreachable so long as all the possibilities in KindProperties have switch branches
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -111,7 +111,6 @@
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/angular": "1.8.4",
|
||||
"@types/angular-route": "1.7.2",
|
||||
"@types/classnames": "2.3.0",
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/d3": "7.4.0",
|
||||
"@types/d3-force": "^2.1.0",
|
||||
@@ -121,6 +120,7 @@
|
||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||
"@types/eslint": "8.4.9",
|
||||
"@types/file-saver": "2.0.5",
|
||||
"@types/glob": "^8.0.0",
|
||||
"@types/google.analytics": "^0.0.42",
|
||||
"@types/gtag.js": "^0.0.12",
|
||||
"@types/history": "4.7.11",
|
||||
@@ -137,7 +137,6 @@
|
||||
"@types/papaparse": "5.3.5",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/prismjs": "1.26.0",
|
||||
"@types/rc-time-picker": "3.4.1",
|
||||
"@types/react": "17.0.42",
|
||||
"@types/react-beautiful-dnd": "13.1.2",
|
||||
"@types/react-dom": "17.0.14",
|
||||
@@ -152,13 +151,11 @@
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1",
|
||||
"@types/redux-mock-store": "1.0.3",
|
||||
"@types/reselect": "2.2.0",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/slate": "0.47.11",
|
||||
"@types/slate-plain-serializer": "0.7.2",
|
||||
"@types/slate-react": "0.22.9",
|
||||
"@types/testing-library__jest-dom": "5.14.5",
|
||||
"@types/testing-library__react-hooks": "^3.2.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.0",
|
||||
@@ -279,11 +276,10 @@
|
||||
"@react-stately/collections": "3.4.1",
|
||||
"@react-stately/menu": "3.4.1",
|
||||
"@react-stately/tree": "3.3.1",
|
||||
"@reduxjs/toolkit": "1.8.6",
|
||||
"@reduxjs/toolkit": "1.9.1",
|
||||
"@sentry/browser": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"@types/rc-tree": "^3.0.0",
|
||||
"@types/react-resizable": "3.0.3",
|
||||
"@types/webpack-env": "1.18.0",
|
||||
"@visx/event": "2.6.0",
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
"@types/react-dom": "17.0.14",
|
||||
"@types/sinon": "10.0.13",
|
||||
"@types/testing-library__jest-dom": "5.14.5",
|
||||
"@types/testing-library__react-hooks": "^3.2.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"esbuild": "0.15.12",
|
||||
"react": "17.0.2",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export interface FeatureToggles {
|
||||
[name: string]: boolean | undefined; // support any string value
|
||||
|
||||
returnUnameHeader?: boolean;
|
||||
alertingBigTransactions?: boolean;
|
||||
promQueryBuilder?: boolean;
|
||||
trimDefaults?: boolean;
|
||||
@@ -81,6 +82,8 @@ export interface FeatureToggles {
|
||||
nestedFolders?: boolean;
|
||||
accessTokenExpirationCheck?: boolean;
|
||||
elasticsearchBackendMigration?: boolean;
|
||||
datasourceOnboarding?: boolean;
|
||||
secureSocksDatasourceProxy?: boolean;
|
||||
authnService?: boolean;
|
||||
sessionRemoteCache?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"projectId": "zb7k1c",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videoCompression": 20
|
||||
"videoCompression": 20,
|
||||
"viewportWidth": 1920,
|
||||
"viewportHeight": 1080
|
||||
}
|
||||
|
||||
@@ -236,9 +236,9 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.singleValue().should('have.text', 'Query').click();
|
||||
e2e.components.Select.singleValue().should('have.text', 'Query').parent().click();
|
||||
});
|
||||
e2e.components.Select.option().should('be.visible').contains(type).click();
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().find('input').type(`${type}{enter}`);
|
||||
}
|
||||
|
||||
if (label) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CoreApp } from '@grafana/data';
|
||||
|
||||
import { EchoEvent, EchoEventType } from '../services/EchoSrv';
|
||||
|
||||
/**
|
||||
@@ -20,6 +22,7 @@ export interface DashboardInfo {
|
||||
* @public
|
||||
*/
|
||||
export interface DataRequestInfo extends Partial<DashboardInfo> {
|
||||
source?: CoreApp | string;
|
||||
datasourceName: string;
|
||||
datasourceId: number;
|
||||
datasourceUid: string;
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags?page=1&name=alpine
|
||||
##
|
||||
|
||||
DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.0-alpine"
|
||||
DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1-alpine"
|
||||
|
||||
@@ -18,8 +18,8 @@ apk add --no-cache curl npm yarn build-base openssh git-lfs perl-utils coreutils
|
||||
# apk add --no-cache xvfb glib nss nspr gdk-pixbuf "gtk+3.0" pango atk cairo dbus-libs libxcomposite libxrender libxi libxtst libxrandr libxscrnsaver alsa-lib at-spi2-atk at-spi2-core cups-libs gcompat libc6-compat
|
||||
|
||||
# Install Go
|
||||
filename="go1.19.3.linux-amd64.tar.gz"
|
||||
get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "74b9640724fd4e6bb0ed2a1bc44ae813a03f1e72a4c76253e2d5c015494430ba"
|
||||
filename="go1.19.4.linux-amd64.tar.gz"
|
||||
get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8"
|
||||
untar_file "/tmp/$filename"
|
||||
|
||||
# Install golangci-lint
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
##
|
||||
|
||||
DOCKER_IMAGE_BASE_NAME="grafana/grafana-plugin-ci-e2e"
|
||||
DOCKER_IMAGE_VERSION="1.6.0"
|
||||
DOCKER_IMAGE_VERSION="1.6.1"
|
||||
DOCKER_IMAGE_NAME="${DOCKER_IMAGE_BASE_NAME}:${DOCKER_IMAGE_VERSION}"
|
||||
|
||||
@@ -22,8 +22,8 @@ source "/etc/profile"
|
||||
npm i -g yarn
|
||||
|
||||
# Install Go
|
||||
filename="go1.19.3.linux-amd64.tar.gz"
|
||||
get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "74b9640724fd4e6bb0ed2a1bc44ae813a03f1e72a4c76253e2d5c015494430ba"
|
||||
filename="go1.19.4.linux-amd64.tar.gz"
|
||||
get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8"
|
||||
untar_file "/tmp/$filename"
|
||||
|
||||
# Install golangci-lint
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags
|
||||
##
|
||||
|
||||
DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.0"
|
||||
DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
source "./deploy-common.sh"
|
||||
|
||||
# Install Go
|
||||
filename="go1.19.3.linux-amd64.tar.gz"
|
||||
get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "74b9640724fd4e6bb0ed2a1bc44ae813a03f1e72a4c76253e2d5c015494430ba"
|
||||
filename="go1.19.4.linux-amd64.tar.gz"
|
||||
get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8"
|
||||
untar_file "/tmp/$filename"
|
||||
|
||||
# Install golangci-lint
|
||||
|
||||
@@ -140,7 +140,6 @@
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/classnames": "2.3.0",
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/d3": "7.4.0",
|
||||
"@types/enzyme": "3.10.12",
|
||||
@@ -168,7 +167,6 @@
|
||||
"@types/slate-plain-serializer": "0.7.2",
|
||||
"@types/slate-react": "0.22.9",
|
||||
"@types/testing-library__jest-dom": "5.14.5",
|
||||
"@types/testing-library__react-hooks": "^3.2.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.7.0",
|
||||
|
||||
+30
-20
@@ -7,7 +7,6 @@ import { stylesFactory, useTheme2 } from '../../../themes';
|
||||
import { isCompactUrl } from '../../../utils/dataLinks';
|
||||
import { FieldValidationMessage } from '../../Forms/FieldValidationMessage';
|
||||
import { IconButton } from '../../IconButton/IconButton';
|
||||
import { HorizontalGroup, VerticalGroup } from '../../Layout/Layout';
|
||||
|
||||
export interface DataLinksListItemProps {
|
||||
index: number;
|
||||
@@ -31,26 +30,24 @@ export const DataLinksListItem: FC<DataLinksListItemProps> = ({ link, onEdit, on
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<HorizontalGroup justify="space-between" align="flex-start" width="100%">
|
||||
<div className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}>
|
||||
{hasTitle ? title : 'Data link title not provided'}
|
||||
</div>
|
||||
<HorizontalGroup>
|
||||
<IconButton name="pen" onClick={onEdit} />
|
||||
<IconButton name="times" onClick={onRemove} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<div
|
||||
className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}
|
||||
title={url}
|
||||
>
|
||||
{hasUrl ? url : 'Data link url not provided'}
|
||||
<div className={styles.titleWrapper}>
|
||||
<div className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}>
|
||||
{hasTitle ? title : 'Data link title not provided'}
|
||||
</div>
|
||||
{isCompactExploreUrl && (
|
||||
<FieldValidationMessage>Explore data link may not work in the future. Please edit.</FieldValidationMessage>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
<div className={styles.actionButtons}>
|
||||
<IconButton name="pen" onClick={onEdit} />
|
||||
<IconButton name="times" onClick={onRemove} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}
|
||||
title={url}
|
||||
>
|
||||
{hasUrl ? url : 'Data link url not provided'}
|
||||
</div>
|
||||
{isCompactExploreUrl && (
|
||||
<FieldValidationMessage>Explore data link may not work in the future. Please edit.</FieldValidationMessage>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -63,6 +60,19 @@ const getDataLinkListItemStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
titleWrapper: css`
|
||||
label: data-links-list-item-title;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
`,
|
||||
actionButtons: css`
|
||||
margin-left: ${theme.spacing(1)};
|
||||
display: flex;
|
||||
`,
|
||||
errored: css`
|
||||
color: ${theme.colors.error.text};
|
||||
|
||||
@@ -127,8 +127,8 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
|
||||
{isOpen && (
|
||||
<>
|
||||
<div role="presentation" className={cx(modalBackdrop, styles.backdrop)} {...underlayProps} />
|
||||
<FocusScope contain autoFocus>
|
||||
<section className={styles.content} ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<section ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<FocusScope contain autoFocus>
|
||||
<TimePickerContent
|
||||
timeZone={timeZone}
|
||||
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||
@@ -142,8 +142,8 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
|
||||
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||
hideQuickRanges={hideQuickRanges}
|
||||
/>
|
||||
</section>
|
||||
</FocusScope>
|
||||
</FocusScope>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
+2
-2
@@ -160,7 +160,6 @@ const NarrowScreenForm = (props: FormProps) => {
|
||||
<div className={styles.form}>
|
||||
<TimeRangeContent value={value} onApply={onChange} timeZone={timeZone} isFullscreen={false} />
|
||||
</div>
|
||||
<p></p>
|
||||
{showHistory && (
|
||||
<TimeRangeList
|
||||
title={t('time-picker.absolute.recent-title', 'Recently used absolute ranges')}
|
||||
@@ -246,7 +245,8 @@ function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOpt
|
||||
if (!Array.isArray(ranges) || ranges.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return ranges.slice(ranges.length - 4).map((range) => mapRangeToTimeOption(range, timeZone));
|
||||
|
||||
return ranges.map((range) => mapRangeToTimeOption(range, timeZone));
|
||||
}
|
||||
|
||||
EmptyRecentList.displayName = 'EmptyRecentList';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { ButtonSelect } from './ButtonSelect';
|
||||
|
||||
const OPTIONS: SelectableValue[] = [
|
||||
{
|
||||
label: 'Hello',
|
||||
value: 'a',
|
||||
},
|
||||
{
|
||||
label: 'World',
|
||||
value: 'b',
|
||||
},
|
||||
];
|
||||
|
||||
describe('ButtonSelect', () => {
|
||||
it('initially renders the selected value with the menu closed', () => {
|
||||
const selected = OPTIONS[0];
|
||||
render(<ButtonSelect value={selected} options={OPTIONS} onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
expect(screen.queryAllByRole('menuitemradio')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('opens the menu when clicking the button', async () => {
|
||||
const selected = OPTIONS[0];
|
||||
render(<ButtonSelect value={selected} options={OPTIONS} onChange={() => {}} />);
|
||||
|
||||
const button = screen.getByText('Hello');
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(screen.queryAllByRole('menuitemradio')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('closes the menu when clicking an option', async () => {
|
||||
const selected = OPTIONS[0];
|
||||
const onChange = jest.fn();
|
||||
render(<ButtonSelect value={selected} options={OPTIONS} onChange={onChange} />);
|
||||
|
||||
const button = screen.getByText('Hello');
|
||||
await userEvent.click(button);
|
||||
|
||||
const option = screen.getByText('World');
|
||||
await userEvent.click(option);
|
||||
|
||||
expect(screen.queryAllByRole('menuitemradio')).toHaveLength(0);
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
label: 'World',
|
||||
value: 'b',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { Monaco, monacoTypes } from './types';
|
||||
@@ -6,13 +8,24 @@ function getColors(theme?: GrafanaTheme2): monacoTypes.editor.IColors {
|
||||
if (theme === undefined) {
|
||||
return {};
|
||||
} else {
|
||||
return {
|
||||
const colors: Record<string, string> = {
|
||||
'editor.background': theme.components.input.background,
|
||||
'minimap.background': theme.colors.background.secondary,
|
||||
};
|
||||
|
||||
Object.keys(colors).forEach((resultKey) => {
|
||||
colors[resultKey] = normalizeColorForMonaco(colors[resultKey]);
|
||||
});
|
||||
return colors;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeColorForMonaco(color?: string): string {
|
||||
// monaco needs 6char hex colors
|
||||
// see https://github.com/grafana/grafana/issues/43158
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
// we support calling this without a theme, it will make sure the themes
|
||||
// are registered in monaco, even if the colors are not perfect.
|
||||
export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) {
|
||||
@@ -24,9 +37,9 @@ export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) {
|
||||
colors: colors,
|
||||
// fallback syntax highlighting for languages that microsoft doesn't handle (ex cloudwatch's metric math)
|
||||
rules: [
|
||||
{ token: 'predefined', foreground: theme?.visualization.getColorByName('purple') },
|
||||
{ token: 'operator', foreground: theme?.visualization.getColorByName('orange') },
|
||||
{ token: 'tag', foreground: theme?.visualization.getColorByName('green') },
|
||||
{ token: 'predefined', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('purple')) },
|
||||
{ token: 'operator', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('orange')) },
|
||||
{ token: 'tag', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('green')) },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -36,9 +49,9 @@ export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) {
|
||||
colors: colors,
|
||||
// fallback syntax highlighting for languages that microsoft doesn't handle (ex cloudwatch's metric math)
|
||||
rules: [
|
||||
{ token: 'predefined', foreground: theme?.visualization.getColorByName('purple') },
|
||||
{ token: 'operator', foreground: theme?.visualization.getColorByName('orange') },
|
||||
{ token: 'tag', foreground: theme?.visualization.getColorByName('green') },
|
||||
{ token: 'predefined', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('purple')) },
|
||||
{ token: 'operator', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('orange')) },
|
||||
{ token: 'tag', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('green')) },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
series: UPlotSeriesBuilder[] = [];
|
||||
private axes: Record<string, UPlotAxisBuilder> = {};
|
||||
private scales: UPlotScaleBuilder[] = [];
|
||||
private bands: Band[] = [];
|
||||
|
||||
@@ -13,15 +13,14 @@
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/deep-freeze": "^0.1.1",
|
||||
"@types/enzyme": "3.10.12",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/jest": "29.2.3",
|
||||
"@types/lodash": "4.14.187",
|
||||
"@types/prop-types": "15.7.5",
|
||||
"@types/react": "17.0.42",
|
||||
"@types/react-icons": "2.2.7",
|
||||
"@types/reselect": "2.2.0",
|
||||
"@types/slate-react": "0.22.9",
|
||||
"@types/testing-library__jest-dom": "5.14.5",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
|
||||
+54
-53
@@ -14,27 +14,30 @@
|
||||
|
||||
jest.mock('./scroll-page');
|
||||
|
||||
import ScrollManager from './ScrollManager';
|
||||
import traceGenerator from '../src/demo/trace-generators';
|
||||
|
||||
import ScrollManager, { Accessors } from './ScrollManager';
|
||||
import { scrollBy, scrollTo } from './scroll-page';
|
||||
import { Trace, TraceSpanData, TraceSpanReference } from './types/trace';
|
||||
|
||||
const SPAN_HEIGHT = 2;
|
||||
|
||||
function getTrace() {
|
||||
const spans = [];
|
||||
const trace = {
|
||||
spans,
|
||||
duration: 2000,
|
||||
startTime: 1000,
|
||||
};
|
||||
for (let i = 0; i < 10; i++) {
|
||||
spans.push({ duration: 1, startTime: 1000, spanID: i + 1 });
|
||||
}
|
||||
return trace;
|
||||
function getTrace(): Trace {
|
||||
const generatedTrace = traceGenerator.trace({ numberOfSpans: 10 });
|
||||
generatedTrace.duration = 2000;
|
||||
generatedTrace.startTime = 1000;
|
||||
|
||||
generatedTrace.spans.forEach((span: TraceSpanData, index: number) => {
|
||||
span.duration = 1;
|
||||
span.startTime = 1000;
|
||||
span.spanID = (index + 1).toString();
|
||||
});
|
||||
return generatedTrace;
|
||||
}
|
||||
|
||||
function getAccessors() {
|
||||
return {
|
||||
getViewRange: jest.fn(() => [0, 1]),
|
||||
getViewRange: jest.fn(() => [0, 1] as [number, number]),
|
||||
getSearchedSpanIDs: jest.fn(),
|
||||
getCollapsedChildren: jest.fn(),
|
||||
getViewHeight: jest.fn(() => SPAN_HEIGHT * 2),
|
||||
@@ -47,13 +50,13 @@ function getAccessors() {
|
||||
}
|
||||
|
||||
describe('ScrollManager', () => {
|
||||
let trace;
|
||||
let accessors;
|
||||
let manager;
|
||||
let trace: Trace;
|
||||
let accessors: Accessors;
|
||||
let manager: ScrollManager;
|
||||
|
||||
beforeEach(() => {
|
||||
scrollBy.mockReset();
|
||||
scrollTo.mockReset();
|
||||
jest.mocked(scrollBy).mockReset();
|
||||
jest.mocked(scrollTo).mockReset();
|
||||
trace = getTrace();
|
||||
accessors = getAccessors();
|
||||
manager = new ScrollManager(trace, { scrollBy, scrollTo });
|
||||
@@ -61,14 +64,13 @@ describe('ScrollManager', () => {
|
||||
});
|
||||
|
||||
it('saves the accessors', () => {
|
||||
const n = Math.random();
|
||||
manager.setAccessors(n);
|
||||
expect(manager._accessors).toBe(n);
|
||||
accessors = getAccessors();
|
||||
manager.setAccessors(accessors);
|
||||
expect(manager._accessors).toBe(accessors);
|
||||
});
|
||||
|
||||
describe('_scrollPast()', () => {
|
||||
it('throws if accessors is not set', () => {
|
||||
manager.setAccessors(null);
|
||||
expect(manager._scrollPast).toThrow();
|
||||
});
|
||||
|
||||
@@ -77,10 +79,10 @@ describe('ScrollManager', () => {
|
||||
const oldWarn = console.warn;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn = () => {};
|
||||
manager._scrollPast(null, null);
|
||||
expect(accessors.getRowPosition.mock.calls.length).toBe(1);
|
||||
expect(accessors.getViewHeight.mock.calls.length).toBe(0);
|
||||
expect(scrollTo.mock.calls.length).toBe(0);
|
||||
manager._scrollPast(-2, 1);
|
||||
expect(jest.mocked(accessors.getRowPosition).mock.calls.length).toBe(1);
|
||||
expect(jest.mocked(accessors.getViewHeight).mock.calls.length).toBe(0);
|
||||
expect(jest.mocked(scrollTo).mock.calls.length).toBe(0);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn = oldWarn;
|
||||
});
|
||||
@@ -88,44 +90,43 @@ describe('ScrollManager', () => {
|
||||
it('scrolls up with direction is `-1`', () => {
|
||||
const y = 10;
|
||||
const expectTo = y - 0.5 * accessors.getViewHeight();
|
||||
accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT });
|
||||
jest.mocked(accessors.getRowPosition).mockReturnValue({ y, height: SPAN_HEIGHT });
|
||||
manager._scrollPast(NaN, -1);
|
||||
expect(scrollTo.mock.calls).toEqual([[expectTo]]);
|
||||
expect(jest.mocked(scrollTo).mock.calls).toEqual([[expectTo]]);
|
||||
});
|
||||
|
||||
it('scrolls down with direction `1`', () => {
|
||||
const y = 10;
|
||||
const vh = accessors.getViewHeight();
|
||||
const expectTo = y + SPAN_HEIGHT - 0.5 * vh;
|
||||
accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT });
|
||||
jest.mocked(accessors.getRowPosition).mockReturnValue({ y, height: SPAN_HEIGHT });
|
||||
manager._scrollPast(NaN, 1);
|
||||
expect(scrollTo.mock.calls).toEqual([[expectTo]]);
|
||||
expect(jest.mocked(scrollTo).mock.calls).toEqual([[expectTo]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_scrollToVisibleSpan()', () => {
|
||||
function getRefs(spanID) {
|
||||
return [{ refType: 'CHILD_OF', spanID }];
|
||||
function getRefs(spanID: string | undefined) {
|
||||
return [{ refType: 'CHILD_OF', spanID }] as TraceSpanReference[];
|
||||
}
|
||||
let scrollPastMock;
|
||||
let scrollPastMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
scrollPastMock = jest.fn();
|
||||
manager._scrollPast = scrollPastMock;
|
||||
});
|
||||
it('throws if accessors is not set', () => {
|
||||
manager.setAccessors(null);
|
||||
expect(manager._scrollToVisibleSpan).toThrow();
|
||||
});
|
||||
it('exits if the trace is not set', () => {
|
||||
manager.setTrace(null);
|
||||
manager._scrollToVisibleSpan();
|
||||
manager._scrollToVisibleSpan(1);
|
||||
expect(scrollPastMock.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does nothing if already at the boundary', () => {
|
||||
accessors.getTopRowIndexVisible.mockReturnValue(0);
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(0);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(trace.spans.length - 1);
|
||||
manager._scrollToVisibleSpan(-1);
|
||||
expect(scrollPastMock.mock.calls.length).toBe(0);
|
||||
manager._scrollToVisibleSpan(1);
|
||||
@@ -133,8 +134,8 @@ describe('ScrollManager', () => {
|
||||
});
|
||||
|
||||
it('centers the current top or bottom span', () => {
|
||||
accessors.getTopRowIndexVisible.mockReturnValue(5);
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(5);
|
||||
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(5);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(5);
|
||||
manager._scrollToVisibleSpan(-1);
|
||||
expect(scrollPastMock).lastCalledWith(5, -1);
|
||||
manager._scrollToVisibleSpan(1);
|
||||
@@ -144,8 +145,8 @@ describe('ScrollManager', () => {
|
||||
it('skips spans that are out of view', () => {
|
||||
trace.spans[4].startTime = trace.startTime + trace.duration * 0.5;
|
||||
accessors.getViewRange = () => [0.4, 0.6];
|
||||
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
|
||||
manager._scrollToVisibleSpan(1);
|
||||
expect(scrollPastMock).lastCalledWith(4, 1);
|
||||
manager._scrollToVisibleSpan(-1);
|
||||
@@ -153,8 +154,8 @@ describe('ScrollManager', () => {
|
||||
});
|
||||
|
||||
it('skips spans that do not match the text search', () => {
|
||||
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
|
||||
accessors.getSearchedSpanIDs = () => new Set([trace.spans[4].spanID]);
|
||||
manager._scrollToVisibleSpan(1);
|
||||
expect(scrollPastMock).lastCalledWith(4, 1);
|
||||
@@ -164,8 +165,8 @@ describe('ScrollManager', () => {
|
||||
|
||||
it('scrolls to boundary when scrolling away from closest spanID in findMatches', () => {
|
||||
const closetFindMatchesSpanID = 4;
|
||||
accessors.getTopRowIndexVisible.mockReturnValue(closetFindMatchesSpanID - 1);
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(closetFindMatchesSpanID + 1);
|
||||
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(closetFindMatchesSpanID - 1);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(closetFindMatchesSpanID + 1);
|
||||
accessors.getSearchedSpanIDs = () => new Set([trace.spans[closetFindMatchesSpanID].spanID]);
|
||||
|
||||
manager._scrollToVisibleSpan(1);
|
||||
@@ -177,7 +178,7 @@ describe('ScrollManager', () => {
|
||||
|
||||
it('scrolls to last visible row when boundary is hidden', () => {
|
||||
const parentOfLastRowWithHiddenChildrenIndex = trace.spans.length - 2;
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
|
||||
accessors.getCollapsedChildren = () => new Set([trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID]);
|
||||
accessors.getSearchedSpanIDs = () => new Set([trace.spans[0].spanID]);
|
||||
trace.spans[trace.spans.length - 1].references = getRefs(
|
||||
@@ -204,9 +205,9 @@ describe('ScrollManager', () => {
|
||||
}
|
||||
}
|
||||
// set which spans are "in-view" and which have collapsed children
|
||||
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||
accessors.getCollapsedChildren.mockReturnValue(new Set([spans[0].spanID, spans[4].spanID]));
|
||||
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1);
|
||||
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
|
||||
jest.mocked(accessors.getCollapsedChildren).mockReturnValue(new Set([spans[0].spanID, spans[4].spanID]));
|
||||
});
|
||||
|
||||
it('skips spans that are hidden because their parent is collapsed', () => {
|
||||
@@ -219,7 +220,7 @@ describe('ScrollManager', () => {
|
||||
it('ignores references with unknown types', () => {
|
||||
// modify spans[2] so that it has an unknown refType
|
||||
const spans = trace.spans;
|
||||
spans[2].references = [{ refType: 'OTHER' }];
|
||||
spans[2].references = [{ refType: 'OTHER' }] as unknown as TraceSpanReference[];
|
||||
manager.scrollToNextVisibleSpan();
|
||||
expect(scrollPastMock).lastCalledWith(2, 1);
|
||||
manager.scrollToPrevVisibleSpan();
|
||||
@@ -239,7 +240,7 @@ describe('ScrollManager', () => {
|
||||
|
||||
describe('scrollToFirstVisibleSpan', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(manager, '_scrollToVisibleSpan').mockImplementationOnce();
|
||||
jest.spyOn(manager, '_scrollToVisibleSpan');
|
||||
});
|
||||
|
||||
it('calls _scrollToVisibleSpan searching downwards from first span', () => {
|
||||
@@ -261,12 +262,12 @@ describe('ScrollManager', () => {
|
||||
manager._accessors = null;
|
||||
manager.scrollPageDown();
|
||||
manager.scrollPageUp();
|
||||
expect(scrollBy.mock.calls.length).toBe(0);
|
||||
expect(jest.mocked(scrollBy).mock.calls.length).toBe(0);
|
||||
manager._accessors = accessors;
|
||||
manager._scroller = null;
|
||||
manager.scrollPageDown();
|
||||
manager.scrollPageUp();
|
||||
expect(scrollBy.mock.calls.length).toBe(0);
|
||||
expect(jest.mocked(scrollBy).mock.calls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ function isSpanHidden(span: TraceSpan, childrenAreHidden: Set<string>, spansMap:
|
||||
*/
|
||||
export default class ScrollManager {
|
||||
_trace: Trace | TNil;
|
||||
_scroller: Scroller;
|
||||
_scroller: Scroller | TNil;
|
||||
_accessors: Accessors | TNil;
|
||||
|
||||
constructor(trace: Trace | TNil, scroller: Scroller) {
|
||||
@@ -117,7 +117,7 @@ export default class ScrollManager {
|
||||
y -= vh;
|
||||
}
|
||||
y += direction * 0.5 * vh;
|
||||
this._scroller.scrollTo(y);
|
||||
this._scroller?.scrollTo(y);
|
||||
}
|
||||
|
||||
_scrollToVisibleSpan(direction: 1 | -1, startRow?: number) {
|
||||
|
||||
+3
-3
@@ -12,12 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import GraphTicks from './GraphTicks';
|
||||
import GraphTicks, { GraphTicksProps } from './GraphTicks';
|
||||
|
||||
const setup = (propOverrides) => {
|
||||
const setup = (propOverrides?: GraphTicksProps) => {
|
||||
const defaultProps = {
|
||||
items: [
|
||||
{ valueWidth: 100, valueOffset: 25, serviceName: 'a' },
|
||||
@@ -27,7 +27,7 @@ const getStyles = () => {
|
||||
};
|
||||
};
|
||||
|
||||
type GraphTicksProps = {
|
||||
export type GraphTicksProps = {
|
||||
numTicks: number;
|
||||
};
|
||||
|
||||
|
||||
+4
-4
@@ -15,19 +15,19 @@
|
||||
import { render, screen, fireEvent, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import Scrubber from './Scrubber';
|
||||
import Scrubber, { ScrubberProps } from './Scrubber';
|
||||
|
||||
describe('<Scrubber>', () => {
|
||||
const defaultProps = {
|
||||
position: 0,
|
||||
};
|
||||
|
||||
let rerender;
|
||||
let rerender: (arg0: JSX.Element) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
({ rerender } = render(
|
||||
<svg>
|
||||
<Scrubber {...defaultProps} />
|
||||
<Scrubber {...(defaultProps as ScrubberProps)} />
|
||||
</svg>
|
||||
));
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe('<Scrubber>', () => {
|
||||
it('calculates the correct x% for a timestamp', () => {
|
||||
rerender(
|
||||
<svg>
|
||||
<Scrubber {...defaultProps} position={0.5} />
|
||||
<Scrubber {...(defaultProps as ScrubberProps)} position={0.5} />
|
||||
</svg>
|
||||
);
|
||||
const line = screen.getByTestId('scrubber-component-line');
|
||||
@@ -72,7 +72,7 @@ export const getStyles = () => {
|
||||
};
|
||||
};
|
||||
|
||||
type ScrubberProps = {
|
||||
export type ScrubberProps = {
|
||||
isDragging: boolean;
|
||||
position: number;
|
||||
onMouseDown: (evt: React.MouseEvent<any>) => void;
|
||||
|
||||
+45
-42
@@ -12,22 +12,23 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { createTheme } from '@grafana/data';
|
||||
|
||||
import { EUpdateTypes } from '../../utils/DraggableManager';
|
||||
import { ViewRangeTime } from '../../TraceTimelineViewer/types';
|
||||
import DraggableManager, { DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager';
|
||||
import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame';
|
||||
|
||||
import GraphTicks from './GraphTicks';
|
||||
import Scrubber from './Scrubber';
|
||||
import ViewingLayer, { dragTypes, getStyles } from './ViewingLayer';
|
||||
import Scrubber, { ScrubberProps } from './Scrubber';
|
||||
import ViewingLayer, { dragTypes, getStyles, ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer';
|
||||
|
||||
function getViewRange(viewStart, viewEnd) {
|
||||
function getViewRange(viewStart: number, viewEnd: number) {
|
||||
return {
|
||||
time: {
|
||||
current: [viewStart, viewEnd],
|
||||
current: [viewStart, viewEnd] as [number, number],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -35,8 +36,8 @@ function getViewRange(viewStart, viewEnd) {
|
||||
describe('<SpanGraph>', () => {
|
||||
polyfillAnimationFrame(window);
|
||||
|
||||
let props;
|
||||
let wrapper;
|
||||
let props: ViewingLayerProps;
|
||||
let wrapper: ShallowWrapper<ViewingLayerProps, {}, UnthemedViewingLayer>;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
@@ -45,7 +46,8 @@ describe('<SpanGraph>', () => {
|
||||
updateNextViewRangeTime: jest.fn(),
|
||||
updateViewRangeTime: jest.fn(),
|
||||
viewRange: getViewRange(0, 1),
|
||||
};
|
||||
} as unknown as ViewingLayerProps;
|
||||
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.dive()
|
||||
.dive();
|
||||
@@ -57,11 +59,12 @@ describe('<SpanGraph>', () => {
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.dive()
|
||||
.dive();
|
||||
|
||||
wrapper.instance()._setRoot({
|
||||
getBoundingClientRect() {
|
||||
return { left: 10, width: 100 };
|
||||
},
|
||||
});
|
||||
} as SVGElement);
|
||||
});
|
||||
|
||||
it('throws if _root is not set', () => {
|
||||
@@ -105,81 +108,81 @@ describe('<SpanGraph>', () => {
|
||||
describe('reframe', () => {
|
||||
it('handles mousemove', () => {
|
||||
const value = 0.5;
|
||||
wrapper.instance()._handleReframeMouseMove({ value });
|
||||
const calls = props.updateNextViewRangeTime.mock.calls;
|
||||
wrapper.instance()._handleReframeMouseMove({ value } as DraggingUpdate);
|
||||
const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls;
|
||||
expect(calls).toEqual([[{ cursor: value }]]);
|
||||
});
|
||||
|
||||
it('handles mouseleave', () => {
|
||||
wrapper.instance()._handleReframeMouseLeave();
|
||||
const calls = props.updateNextViewRangeTime.mock.calls;
|
||||
const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls;
|
||||
expect(calls).toEqual([[{ cursor: null }]]);
|
||||
});
|
||||
|
||||
describe('drag update', () => {
|
||||
it('handles sans anchor', () => {
|
||||
const value = 0.5;
|
||||
wrapper.instance()._handleReframeDragUpdate({ value });
|
||||
const calls = props.updateNextViewRangeTime.mock.calls;
|
||||
wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate);
|
||||
const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls;
|
||||
expect(calls).toEqual([[{ reframe: { anchor: value, shift: value } }]]);
|
||||
});
|
||||
|
||||
it('handles the existing anchor', () => {
|
||||
const value = 0.5;
|
||||
const anchor = 0.1;
|
||||
const time = { ...props.viewRange.time, reframe: { anchor } };
|
||||
const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime;
|
||||
props = { ...props, viewRange: { time } };
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.dive()
|
||||
.dive();
|
||||
wrapper.instance()._handleReframeDragUpdate({ value });
|
||||
const calls = props.updateNextViewRangeTime.mock.calls;
|
||||
wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate);
|
||||
const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls;
|
||||
expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag end', () => {
|
||||
let manager;
|
||||
let manager: DraggableManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = { resetBounds: jest.fn() };
|
||||
manager = { resetBounds: jest.fn() } as unknown as DraggableManager;
|
||||
});
|
||||
|
||||
it('handles sans anchor', () => {
|
||||
const value = 0.5;
|
||||
wrapper.instance()._handleReframeDragEnd({ manager, value });
|
||||
expect(manager.resetBounds.mock.calls).toEqual([[]]);
|
||||
const calls = props.updateViewRangeTime.mock.calls;
|
||||
wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate);
|
||||
expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]);
|
||||
const calls = (props.updateViewRangeTime as jest.Mock).mock.calls;
|
||||
expect(calls).toEqual([[value, value, 'minimap']]);
|
||||
});
|
||||
|
||||
it('handles dragged left (anchor is greater)', () => {
|
||||
const value = 0.5;
|
||||
const anchor = 0.6;
|
||||
const time = { ...props.viewRange.time, reframe: { anchor } };
|
||||
const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime;
|
||||
props = { ...props, viewRange: { time } };
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.dive()
|
||||
.dive();
|
||||
wrapper.instance()._handleReframeDragEnd({ manager, value });
|
||||
wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate);
|
||||
|
||||
expect(manager.resetBounds.mock.calls).toEqual([[]]);
|
||||
const calls = props.updateViewRangeTime.mock.calls;
|
||||
expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]);
|
||||
const calls = (props.updateViewRangeTime as jest.Mock).mock.calls;
|
||||
expect(calls).toEqual([[value, anchor, 'minimap']]);
|
||||
});
|
||||
|
||||
it('handles dragged right (anchor is less)', () => {
|
||||
const value = 0.5;
|
||||
const anchor = 0.4;
|
||||
const time = { ...props.viewRange.time, reframe: { anchor } };
|
||||
const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime;
|
||||
props = { ...props, viewRange: { time } };
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.dive()
|
||||
.dive();
|
||||
wrapper.instance()._handleReframeDragEnd({ manager, value });
|
||||
wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate);
|
||||
|
||||
expect(manager.resetBounds.mock.calls).toEqual([[]]);
|
||||
const calls = props.updateViewRangeTime.mock.calls;
|
||||
expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]);
|
||||
const calls = (props.updateViewRangeTime as jest.Mock).mock.calls;
|
||||
expect(calls).toEqual([[anchor, value, 'minimap']]);
|
||||
});
|
||||
});
|
||||
@@ -187,12 +190,12 @@ describe('<SpanGraph>', () => {
|
||||
|
||||
describe('scrubber', () => {
|
||||
it('prevents the cursor from being drawn on scrubber mouseover', () => {
|
||||
wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter });
|
||||
wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter } as DraggingUpdate);
|
||||
expect(wrapper.state('preventCursorLine')).toBe(true);
|
||||
});
|
||||
|
||||
it('prevents the cursor from being drawn on scrubber mouseleave', () => {
|
||||
wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave });
|
||||
wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave } as DraggingUpdate);
|
||||
expect(wrapper.state('preventCursorLine')).toBe(false);
|
||||
});
|
||||
|
||||
@@ -203,7 +206,7 @@ describe('<SpanGraph>', () => {
|
||||
event: { stopPropagation },
|
||||
type: EUpdateTypes.DragStart,
|
||||
};
|
||||
wrapper.instance()._handleScrubberDragUpdate(update);
|
||||
wrapper.instance()._handleScrubberDragUpdate(update as unknown as DraggingUpdate);
|
||||
expect(stopPropagation.mock.calls).toEqual([[]]);
|
||||
});
|
||||
|
||||
@@ -229,7 +232,7 @@ describe('<SpanGraph>', () => {
|
||||
},
|
||||
];
|
||||
cases.forEach((_case) => {
|
||||
instance._handleScrubberDragUpdate(_case.dragUpdate);
|
||||
instance._handleScrubberDragUpdate(_case.dragUpdate as DraggingUpdate);
|
||||
expect(props.updateNextViewRangeTime).lastCalledWith(_case.viewRangeUpdate);
|
||||
});
|
||||
});
|
||||
@@ -261,9 +264,9 @@ describe('<SpanGraph>', () => {
|
||||
const { manager } = _case.dragUpdate;
|
||||
wrapper.setState({ preventCursorLine: true });
|
||||
expect(wrapper.state('preventCursorLine')).toBe(true);
|
||||
instance._handleScrubberDragEnd(_case.dragUpdate);
|
||||
instance._handleScrubberDragEnd(_case.dragUpdate as unknown as DraggingUpdate);
|
||||
expect(wrapper.state('preventCursorLine')).toBe(false);
|
||||
expect(manager.resetBounds.mock.calls).toEqual([[]]);
|
||||
expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]);
|
||||
expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap');
|
||||
});
|
||||
});
|
||||
@@ -315,7 +318,7 @@ describe('<SpanGraph>', () => {
|
||||
|
||||
const leftBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`);
|
||||
expect(leftBox.length).toBe(1);
|
||||
const width = Number(leftBox.prop('width').slice(0, -1));
|
||||
const width = Number(leftBox.prop('width')?.toString().slice(0, -1));
|
||||
const x = leftBox.prop('x');
|
||||
expect(Math.round(width)).toBe(20);
|
||||
expect(x).toBe(0);
|
||||
@@ -329,17 +332,17 @@ describe('<SpanGraph>', () => {
|
||||
|
||||
const rightBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`);
|
||||
expect(rightBox.length).toBe(1);
|
||||
const width = Number(rightBox.prop('width').slice(0, -1));
|
||||
const x = Number(rightBox.prop('x').slice(0, -1));
|
||||
const width = Number(rightBox.prop('width')?.toString().slice(0, -1));
|
||||
const x = Number(rightBox.prop('x')?.toString().slice(0, -1));
|
||||
expect(Math.round(width)).toBe(20);
|
||||
expect(x).toBe(80);
|
||||
});
|
||||
|
||||
it('renders handles for the timeRangeFilter', () => {
|
||||
const [viewStart, viewEnd] = props.viewRange.time.current;
|
||||
let scrubber = <Scrubber position={viewStart} />;
|
||||
let scrubber = <Scrubber {...({ position: viewStart } as ScrubberProps)} />;
|
||||
expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy();
|
||||
scrubber = <Scrubber position={viewEnd} />;
|
||||
scrubber = <Scrubber {...({ position: viewEnd } as ScrubberProps)} />;
|
||||
expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -89,7 +89,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
};
|
||||
});
|
||||
|
||||
type ViewingLayerProps = {
|
||||
export type ViewingLayerProps = {
|
||||
height: number;
|
||||
numTicks: number;
|
||||
updateViewRangeTime: TUpdateViewRangeTimeFunction;
|
||||
|
||||
+7
-7
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import traceGenerator from '../../demo/trace-generators';
|
||||
@@ -21,9 +21,9 @@ import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnim
|
||||
|
||||
import CanvasSpanGraph from './CanvasSpanGraph';
|
||||
import TickLabels from './TickLabels';
|
||||
import ViewingLayer from './ViewingLayer';
|
||||
import ViewingLayer, { ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer';
|
||||
|
||||
import SpanGraph from './index';
|
||||
import SpanGraph, { SpanGraphProps } from './index';
|
||||
|
||||
describe('<SpanGraph>', () => {
|
||||
polyfillAnimationFrame(window);
|
||||
@@ -39,10 +39,10 @@ describe('<SpanGraph>', () => {
|
||||
},
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
let wrapper: ShallowWrapper<ViewingLayerProps, {}, UnthemedViewingLayer>;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SpanGraph {...props} />);
|
||||
wrapper = shallow(<SpanGraph {...(props as unknown as SpanGraphProps)} />);
|
||||
});
|
||||
|
||||
it('renders a <CanvasSpanGraph />', () => {
|
||||
@@ -54,7 +54,7 @@ describe('<SpanGraph>', () => {
|
||||
});
|
||||
|
||||
it('returns a <div> if a trace is not provided', () => {
|
||||
wrapper = shallow(<SpanGraph {...props} trace={null} />);
|
||||
wrapper = shallow(<SpanGraph {...({ ...props, trace: null } as unknown as SpanGraphProps)} />);
|
||||
expect(wrapper.matchesElement(<div />)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('<SpanGraph>', () => {
|
||||
|
||||
it('passes items to CanvasSpanGraph', () => {
|
||||
const canvasGraph = wrapper.find(CanvasSpanGraph).first();
|
||||
const items = trace.spans.map((span) => ({
|
||||
const items = trace?.spans.map((span) => ({
|
||||
valueOffset: span.relativeStartTime,
|
||||
valueWidth: span.duration,
|
||||
serviceName: span.process.serviceName,
|
||||
@@ -27,7 +27,7 @@ import ViewingLayer from './ViewingLayer';
|
||||
const DEFAULT_HEIGHT = 60;
|
||||
const TIMELINE_TICK_INTERVAL = 4;
|
||||
|
||||
type SpanGraphProps = {
|
||||
export type SpanGraphProps = {
|
||||
height?: number;
|
||||
trace: Trace;
|
||||
viewRange: ViewRange;
|
||||
|
||||
+25
-16
@@ -15,7 +15,6 @@
|
||||
import { range as _range } from 'lodash';
|
||||
|
||||
import renderIntoCanvas, {
|
||||
BG_COLOR,
|
||||
ITEM_ALPHA,
|
||||
MIN_ITEM_HEIGHT,
|
||||
MAX_TOTAL_HEIGHT,
|
||||
@@ -24,8 +23,10 @@ import renderIntoCanvas, {
|
||||
MAX_ITEM_HEIGHT,
|
||||
} from './render-into-canvas';
|
||||
|
||||
const BG_COLOR = '#FFFFFF';
|
||||
|
||||
const getCanvasWidth = () => window.innerWidth * 2;
|
||||
const getBgFillRect = (items) => ({
|
||||
const getBgFillRect = (items?: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>) => ({
|
||||
fillStyle: BG_COLOR,
|
||||
height: !items || items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(MAX_TOTAL_HEIGHT, items.length),
|
||||
width: getCanvasWidth(),
|
||||
@@ -37,12 +38,15 @@ describe('renderIntoCanvas()', () => {
|
||||
const basicItem = { valueWidth: 100, valueOffset: 50, serviceName: 'some-name' };
|
||||
|
||||
class CanvasContext {
|
||||
fillStyle: undefined;
|
||||
fillRectAccumulator: Array<{ fillStyle: undefined; height: number; width: number; x: number; y: number }> = [];
|
||||
|
||||
constructor() {
|
||||
this.fillStyle = undefined;
|
||||
this.fillRectAccumulator = [];
|
||||
}
|
||||
|
||||
fillRect(x, y, width, height) {
|
||||
fillRect(x: number, y: number, width: number, height: number) {
|
||||
const fillStyle = this.fillStyle;
|
||||
this.fillRectAccumulator.push({
|
||||
fillStyle,
|
||||
@@ -55,6 +59,11 @@ describe('renderIntoCanvas()', () => {
|
||||
}
|
||||
|
||||
class Canvas {
|
||||
height: number;
|
||||
width: number;
|
||||
contexts: CanvasContext[];
|
||||
getContext: jest.Mock;
|
||||
|
||||
constructor() {
|
||||
this.contexts = [];
|
||||
this.height = NaN;
|
||||
@@ -71,13 +80,13 @@ describe('renderIntoCanvas()', () => {
|
||||
|
||||
function getColorFactory() {
|
||||
let i = 0;
|
||||
const inputOutput = [];
|
||||
function getFakeColor(str) {
|
||||
const rv = [i, i, i];
|
||||
const inputOutput: Array<{ input: string; output: [number, number, number] }> = [];
|
||||
function getFakeColor(str: string) {
|
||||
const rv: [number, number, number] = [i, i, i];
|
||||
i++;
|
||||
inputOutput.push({
|
||||
input: str,
|
||||
output: rv.slice(),
|
||||
output: rv.slice() as [number, number, number],
|
||||
});
|
||||
return rv;
|
||||
}
|
||||
@@ -88,7 +97,7 @@ describe('renderIntoCanvas()', () => {
|
||||
it('sets the width', () => {
|
||||
const canvas = new Canvas();
|
||||
expect(canvas.width !== canvas.width).toBe(true);
|
||||
renderIntoCanvas(canvas, [basicItem], 150, getColorFactory());
|
||||
renderIntoCanvas(canvas as unknown as HTMLCanvasElement, [basicItem], 150, getColorFactory(), BG_COLOR);
|
||||
expect(canvas.width).toBe(getCanvasWidth());
|
||||
});
|
||||
|
||||
@@ -96,18 +105,18 @@ describe('renderIntoCanvas()', () => {
|
||||
it('sets the height', () => {
|
||||
const canvas = new Canvas();
|
||||
expect(canvas.height !== canvas.height).toBe(true);
|
||||
renderIntoCanvas(canvas, [basicItem], 150, getColorFactory());
|
||||
renderIntoCanvas(canvas as unknown as HTMLCanvasElement, [basicItem], 150, getColorFactory(), BG_COLOR);
|
||||
expect(canvas.height).toBe(MIN_TOTAL_HEIGHT);
|
||||
});
|
||||
|
||||
it('draws the background', () => {
|
||||
const expectedDrawing = [getBgFillRect()];
|
||||
const canvas = new Canvas();
|
||||
const items = [];
|
||||
const items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }> = [];
|
||||
const totalValueWidth = 4000;
|
||||
const getFillColor = getColorFactory();
|
||||
renderIntoCanvas(canvas, items, totalValueWidth, getFillColor);
|
||||
expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]);
|
||||
renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, totalValueWidth, getFillColor, BG_COLOR);
|
||||
expect((canvas.getContext as jest.Mock).mock.calls).toEqual([['2d', { alpha: false }]]);
|
||||
expect(canvas.contexts.length).toBe(1);
|
||||
expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawing);
|
||||
});
|
||||
@@ -141,7 +150,7 @@ describe('renderIntoCanvas()', () => {
|
||||
];
|
||||
const canvas = new Canvas();
|
||||
const getFillColor = getColorFactory();
|
||||
renderIntoCanvas(canvas, items, totalValueWidth, getFillColor);
|
||||
renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, totalValueWidth, getFillColor, BG_COLOR);
|
||||
expect(getFillColor.inputOutput).toEqual(expectedColors);
|
||||
expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]);
|
||||
expect(canvas.contexts.length).toBe(1);
|
||||
@@ -157,7 +166,7 @@ describe('renderIntoCanvas()', () => {
|
||||
items.push(basicItem);
|
||||
}
|
||||
expect(canvas.height !== canvas.height).toBe(true);
|
||||
renderIntoCanvas(canvas, items, 150, getColorFactory());
|
||||
renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, 150, getColorFactory(), BG_COLOR);
|
||||
expect(canvas.height).toBe(items.length);
|
||||
});
|
||||
|
||||
@@ -187,9 +196,9 @@ describe('renderIntoCanvas()', () => {
|
||||
];
|
||||
const canvas = new Canvas();
|
||||
const getFillColor = getColorFactory();
|
||||
renderIntoCanvas(canvas, items, totalValueWidth, getFillColor);
|
||||
renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, totalValueWidth, getFillColor, BG_COLOR);
|
||||
expect(getFillColor.inputOutput).toEqual(expectedColors);
|
||||
expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]);
|
||||
expect((canvas.getContext as jest.Mock).mock.calls).toEqual([['2d', { alpha: false }]]);
|
||||
expect(canvas.contexts.length).toBe(1);
|
||||
expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawings);
|
||||
});
|
||||
+8
-9
@@ -16,9 +16,8 @@ import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { createTheme } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import TracePageSearchBar, { getStyles } from './TracePageSearchBar';
|
||||
import TracePageSearchBar, { getStyles, TracePageSearchBarProps } from './TracePageSearchBar';
|
||||
|
||||
const defaultProps = {
|
||||
forwardedRef: React.createRef(),
|
||||
@@ -30,8 +29,8 @@ const defaultProps = {
|
||||
describe('<TracePageSearchBar>', () => {
|
||||
describe('truthy textFilter', () => {
|
||||
it('renders UiFindInput with correct props', () => {
|
||||
render(<TracePageSearchBar {...defaultProps} />);
|
||||
expect(screen.getByPlaceholderText('Find...')['value']).toEqual('value');
|
||||
render(<TracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />);
|
||||
expect((screen.getByPlaceholderText('Find...') as HTMLInputElement)['value']).toEqual('value');
|
||||
const suffix = screen.getByLabelText('Search bar suffix');
|
||||
const theme = createTheme();
|
||||
expect(suffix['className']).toBe(getStyles(theme).TracePageSearchBarSuffix);
|
||||
@@ -39,13 +38,13 @@ describe('<TracePageSearchBar>', () => {
|
||||
});
|
||||
|
||||
it('renders buttons', () => {
|
||||
render(<TracePageSearchBar {...defaultProps} />);
|
||||
render(<TracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />);
|
||||
const nextResButton = screen.queryByRole('button', { name: 'Next results button' });
|
||||
const prevResButton = screen.queryByRole('button', { name: 'Prev results button' });
|
||||
expect(nextResButton).toBeInTheDocument();
|
||||
expect(prevResButton).toBeInTheDocument();
|
||||
expect(nextResButton['disabled']).toBe(false);
|
||||
expect(prevResButton['disabled']).toBe(false);
|
||||
expect((nextResButton as HTMLButtonElement)['disabled']).toBe(false);
|
||||
expect((prevResButton as HTMLButtonElement)['disabled']).toBe(false);
|
||||
});
|
||||
|
||||
it('only shows navigable buttons when navigable is true', () => {
|
||||
@@ -53,7 +52,7 @@ describe('<TracePageSearchBar>', () => {
|
||||
...defaultProps,
|
||||
navigable: false,
|
||||
};
|
||||
render(<TracePageSearchBar {...props} />);
|
||||
render(<TracePageSearchBar {...(props as unknown as TracePageSearchBarProps)} />);
|
||||
expect(screen.queryByRole('button', { name: 'Next results button' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Prev results button' })).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -65,7 +64,7 @@ describe('<TracePageSearchBar>', () => {
|
||||
...defaultProps,
|
||||
searchValue: '',
|
||||
};
|
||||
render(<TracePageSearchBar {...props} />);
|
||||
render(<TracePageSearchBar {...(props as unknown as TracePageSearchBarProps)} />);
|
||||
});
|
||||
|
||||
it('does not render suffix', () => {
|
||||
@@ -69,7 +69,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
};
|
||||
};
|
||||
|
||||
type TracePageSearchBarProps = {
|
||||
export type TracePageSearchBarProps = {
|
||||
navigable: boolean;
|
||||
searchValue: string;
|
||||
setSearch: (value: string) => void;
|
||||
|
||||
+3
-2
@@ -16,8 +16,9 @@ import Positions from './Positions';
|
||||
|
||||
describe('Positions', () => {
|
||||
const bufferLen = 1;
|
||||
const getHeight = (i) => i * 2 + 2;
|
||||
let ps;
|
||||
const getHeight = (i: number) => i * 2 + 2;
|
||||
|
||||
let ps: Positions;
|
||||
|
||||
beforeEach(() => {
|
||||
ps = new Positions(bufferLen);
|
||||
+44
-17
@@ -12,18 +12,30 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { TNil } from '../../types';
|
||||
import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame';
|
||||
|
||||
import ListView from './index';
|
||||
import ListView, { TListViewProps } from './index';
|
||||
|
||||
// Util to get list of all callbacks added to an event emitter by event type.
|
||||
// jest adds "error" event listeners to window, this util makes it easier to
|
||||
// ignore those calls.
|
||||
function getListenersByType(mockFn) {
|
||||
const rv = {};
|
||||
function getListenersByType(
|
||||
mockFn: jest.MockContext<
|
||||
void,
|
||||
[
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions | undefined
|
||||
]
|
||||
>
|
||||
) {
|
||||
const rv: {
|
||||
[eventType: string]: EventListenerOrEventListenerObject[];
|
||||
} = {};
|
||||
mockFn.calls.forEach(([eventType, callback]) => {
|
||||
if (!rv[eventType]) {
|
||||
rv[eventType] = [callback];
|
||||
@@ -40,25 +52,24 @@ describe('<ListView>', () => {
|
||||
|
||||
const DATA_LENGTH = 40;
|
||||
|
||||
function getHeight(index) {
|
||||
function getHeight(index: number) {
|
||||
return index * 2 + 2;
|
||||
}
|
||||
|
||||
function Item(props) {
|
||||
function Item(props: React.HTMLProps<HTMLDivElement>) {
|
||||
const { children, ...rest } = props;
|
||||
return <div {...rest}>{children}</div>;
|
||||
}
|
||||
|
||||
function renderItem(itemKey, styles, itemIndex, attrs) {
|
||||
const renderItem: TListViewProps['itemRenderer'] = (itemKey, styles, itemIndex, attrs) => {
|
||||
return (
|
||||
<Item key={itemKey} style={styles} {...attrs}>
|
||||
{itemIndex}
|
||||
</Item>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
let instance;
|
||||
let instance: ListView;
|
||||
|
||||
const props = {
|
||||
dataLength: DATA_LENGTH,
|
||||
@@ -74,6 +85,7 @@ describe('<ListView>', () => {
|
||||
};
|
||||
|
||||
describe('shallow tests', () => {
|
||||
let wrapper: ShallowWrapper<TListViewProps, {}, ListView>;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ListView {...props} />);
|
||||
});
|
||||
@@ -92,10 +104,10 @@ describe('<ListView>', () => {
|
||||
|
||||
it('sets the height of the items according to the height func', () => {
|
||||
const items = wrapper.find(Item);
|
||||
const expectedHeights = [];
|
||||
const expectedHeights: number[] = [];
|
||||
const heights = items.map((node, i) => {
|
||||
expectedHeights.push(getHeight(i));
|
||||
return node.prop('style').height;
|
||||
return node.prop('style')?.height;
|
||||
});
|
||||
expect(heights.length).toBe(props.initialDraw);
|
||||
expect(heights).toEqual(expectedHeights);
|
||||
@@ -109,12 +121,13 @@ describe('<ListView>', () => {
|
||||
});
|
||||
|
||||
describe('mount tests', () => {
|
||||
let wrapper: ReactWrapper<TListViewProps, {}, ListView>;
|
||||
describe('accessor functions', () => {
|
||||
const clientHeight = 2;
|
||||
const scrollTop = 3;
|
||||
|
||||
let oldRender;
|
||||
let oldInitWrapper;
|
||||
let oldRender: () => JSX.Element;
|
||||
let oldInitWrapper: (elm: HTMLElement | TNil) => void;
|
||||
const initWrapperMock = jest.fn((elm) => {
|
||||
if (elm != null) {
|
||||
// jsDom requires `defineProperties` instead of just setting the props
|
||||
@@ -173,14 +186,28 @@ describe('<ListView>', () => {
|
||||
});
|
||||
|
||||
describe('windowScroller', () => {
|
||||
let windowAddListenerSpy;
|
||||
let windowRmListenerSpy;
|
||||
let windowAddListenerSpy: jest.SpyInstance<
|
||||
void,
|
||||
[
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions | undefined
|
||||
]
|
||||
>;
|
||||
let windowRmListenerSpy: jest.SpyInstance<
|
||||
void,
|
||||
[
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions | undefined
|
||||
]
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
windowAddListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
windowRmListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
const wsProps = { ...props, windowScroller: true };
|
||||
wrapper = mount(<ListView {...wsProps} />);
|
||||
wrapper = mount(<ListView {...(wsProps as unknown as TListViewProps)} />);
|
||||
instance = wrapper.instance();
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ type TWrapperProps = {
|
||||
/**
|
||||
* @typedef
|
||||
*/
|
||||
type TListViewProps = {
|
||||
export type TListViewProps = {
|
||||
/**
|
||||
* Number of elements in the list.
|
||||
*/
|
||||
|
||||
@@ -5,18 +5,8 @@
|
||||
# the system-wide Grafana configuration that was bundled with the package as we
|
||||
# use the binary.
|
||||
|
||||
DEFAULT=/etc/default/grafana
|
||||
|
||||
GRAFANA_HOME="${GRAFANA_HOME:-/usr/share/grafana}"
|
||||
|
||||
CONF_DIR=/etc/grafana
|
||||
DATA_DIR=/var/lib/grafana
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
LOG_DIR=/var/log/grafana
|
||||
|
||||
CONF_FILE=$CONF_DIR/grafana.ini
|
||||
PROVISIONING_CFG_DIR=$CONF_DIR/provisioning
|
||||
|
||||
EXECUTABLE="$GRAFANA_HOME/bin/grafana"
|
||||
|
||||
if [ ! -x $EXECUTABLE ]; then
|
||||
@@ -24,18 +14,6 @@ if [ ! -x $EXECUTABLE ]; then
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# overwrite settings from default file
|
||||
if [ -f "$DEFAULT" ]; then
|
||||
. "$DEFAULT"
|
||||
fi
|
||||
|
||||
OPTS="--homepath=${GRAFANA_HOME} \
|
||||
--config=${CONF_FILE} \
|
||||
--configOverrides='cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}'"
|
||||
|
||||
CMD=server
|
||||
|
||||
eval $EXECUTABLE "$CMD" "$OPTS" "$@"
|
||||
eval $EXECUTABLE "$CMD" "$@"
|
||||
|
||||
+4
-1
@@ -114,8 +114,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/admin/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
|
||||
r.Get("/admin/orgs/edit/:id", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
|
||||
r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
|
||||
r.Get("/admin/storage/*", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index)
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagStorage) {
|
||||
r.Get("/admin/storage", reqSignedIn, hs.Index)
|
||||
r.Get("/admin/storage/*", reqSignedIn, hs.Index)
|
||||
}
|
||||
r.Get("/styleguide", reqSignedIn, hs.Index)
|
||||
|
||||
r.Get("/live", reqGrafanaAdmin, hs.Index)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
@@ -37,11 +38,17 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
||||
sqlStore.Cfg.AutoAssignOrg = true
|
||||
sqlStore.Cfg.AutoAssignOrgId = int(testOrgID)
|
||||
|
||||
_, err := sqlStore.CreateUser(context.Background(), user.CreateUserCommand{Email: "testUser@grafana.com", Login: testUserLogin})
|
||||
quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg)
|
||||
orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
|
||||
require.NoError(t, err)
|
||||
_, err = sqlStore.CreateUser(context.Background(), user.CreateUserCommand{Email: "user1@grafana.com", Login: "user1"})
|
||||
usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService)
|
||||
require.NoError(t, err)
|
||||
_, err = sqlStore.CreateUser(context.Background(), user.CreateUserCommand{Email: "user2@grafana.com", Login: "user2"})
|
||||
|
||||
_, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: "testUser@grafana.com", Login: testUserLogin})
|
||||
require.NoError(t, err)
|
||||
_, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: "user1@grafana.com", Login: "user1"})
|
||||
require.NoError(t, err)
|
||||
_, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: "user2@grafana.com", Login: "user2"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -331,13 +338,15 @@ var (
|
||||
func setupOrgUsersDBForAccessControlTests(t *testing.T, db *sqlstore.SQLStore, orgService org.Service) {
|
||||
t.Helper()
|
||||
|
||||
var err error
|
||||
quotaService := quotaimpl.ProvideService(db, db.Cfg)
|
||||
usrSvc, err := userimpl.ProvideService(db, orgService, db.Cfg, nil, nil, quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.CreateUser(context.Background(), user.CreateUserCommand{Email: testServerAdminViewer.Email, SkipOrgSetup: true, Login: testServerAdminViewer.Login})
|
||||
_, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: testServerAdminViewer.Email, SkipOrgSetup: true, Login: testServerAdminViewer.Login})
|
||||
require.NoError(t, err)
|
||||
_, err = db.CreateUser(context.Background(), user.CreateUserCommand{Email: testAdminOrg2.Email, SkipOrgSetup: true, Login: testAdminOrg2.Login})
|
||||
_, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: testAdminOrg2.Email, SkipOrgSetup: true, Login: testAdminOrg2.Login})
|
||||
require.NoError(t, err)
|
||||
_, err = db.CreateUser(context.Background(), user.CreateUserCommand{Email: testEditorOrg1.Email, SkipOrgSetup: true, Login: testEditorOrg1.Login})
|
||||
_, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: testEditorOrg1.Email, SkipOrgSetup: true, Login: testEditorOrg1.Login})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create both orgs with server admin
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/teamguardian/database"
|
||||
"github.com/grafana/grafana/pkg/services/teamguardian/manager"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -46,6 +48,12 @@ func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
||||
teamSvc := teamimpl.ProvideService(sqlStore, setting.NewCfg())
|
||||
team, err := teamSvc.CreateTeam("group1 name", "test1@test.com", testOrgID)
|
||||
require.NoError(t, err)
|
||||
quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg)
|
||||
orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
|
||||
require.NoError(t, err)
|
||||
usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
userCmd = user.CreateUserCommand{
|
||||
Email: fmt.Sprint("user", i, "@test.com"),
|
||||
@@ -53,7 +61,7 @@ func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
||||
Login: fmt.Sprint("loginuser", i),
|
||||
}
|
||||
// user
|
||||
user, err := sqlStore.CreateUser(context.Background(), userCmd)
|
||||
user, err := usrSvc.CreateUserForTests(context.Background(), &userCmd)
|
||||
require.NoError(t, err)
|
||||
err = teamSvc.AddTeamMember(user.ID, testOrgID, team.Id, false, 1)
|
||||
require.NoError(t, err)
|
||||
@@ -115,7 +123,13 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
}
|
||||
|
||||
func createUser(db sqlstore.Store, orgId int64, t *testing.T) int64 {
|
||||
user, err := db.CreateUser(context.Background(), user.CreateUserCommand{
|
||||
quotaService := quotaimpl.ProvideService(db, setting.NewCfg())
|
||||
orgService, err := orgimpl.ProvideService(db, setting.NewCfg(), quotaService)
|
||||
require.NoError(t, err)
|
||||
usrSvc, err := userimpl.ProvideService(db, orgService, setting.NewCfg(), nil, nil, quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{
|
||||
Login: fmt.Sprintf("TestUser%d", rand.Int()),
|
||||
OrgID: orgId,
|
||||
Password: "password",
|
||||
@@ -127,7 +141,11 @@ func createUser(db sqlstore.Store, orgId int64, t *testing.T) int64 {
|
||||
|
||||
func setupTeamTestScenario(userCount int, db *sqlstore.SQLStore, orgService org.Service, t *testing.T) int64 {
|
||||
teamService := teamimpl.ProvideService(db, setting.NewCfg()) // FIXME
|
||||
user, err := db.CreateUser(context.Background(), user.CreateUserCommand{SkipOrgSetup: true, Login: testUserLogin})
|
||||
quotaService := quotaimpl.ProvideService(db, db.Cfg)
|
||||
usrSvc, err := userimpl.ProvideService(db, orgService, db.Cfg, teamService, nil, quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{SkipOrgSetup: true, Login: testUserLogin})
|
||||
require.NoError(t, err)
|
||||
cmd := &org.CreateOrgCommand{Name: "TestOrg", UserID: user.ID}
|
||||
testOrg, err := orgService.CreateWithMember(context.Background(), cmd)
|
||||
|
||||
+12
-4
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
||||
authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||
"github.com/grafana/grafana/pkg/services/login/logintest"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
||||
@@ -63,6 +64,11 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
&usagestats.UsageStatsMock{},
|
||||
)
|
||||
hs.authInfoService = srv
|
||||
orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil))
|
||||
require.NoError(t, err)
|
||||
userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sc.cfg, nil, nil, quotatest.New(false, nil))
|
||||
require.NoError(t, err)
|
||||
hs.userService = userSvc
|
||||
|
||||
createUserCmd := user.CreateUserCommand{
|
||||
Email: fmt.Sprint("user", "@test.com"),
|
||||
@@ -70,9 +76,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
Login: "loginuser",
|
||||
IsAdmin: true,
|
||||
}
|
||||
user, err := sqlStore.CreateUser(context.Background(), createUserCmd)
|
||||
require.Nil(t, err)
|
||||
hs.userService, err = userimpl.ProvideService(sqlStore, nil, sc.cfg, nil, nil, quotatest.New(false, nil))
|
||||
user, err := userSvc.CreateUserForTests(context.Background(), &createUserCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.handlerFunc = hs.GetUserByID
|
||||
@@ -128,7 +132,11 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
Login: "admin",
|
||||
IsAdmin: true,
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), createUserCmd)
|
||||
orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil))
|
||||
require.NoError(t, err)
|
||||
userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sc.cfg, nil, nil, quotatest.New(false, nil))
|
||||
require.NoError(t, err)
|
||||
_, err = userSvc.Create(context.Background(), &createUserCmd)
|
||||
require.Nil(t, err)
|
||||
|
||||
sc.handlerFunc = hs.GetUserByLoginOrEmail
|
||||
|
||||
@@ -36,7 +36,7 @@ func GenerateMetadata(c *cli.Context) (config.Metadata, error) {
|
||||
releaseMode = config.ReleaseMode{Mode: mode}
|
||||
case config.Custom:
|
||||
if edition, _ := os.LookupEnv("EDITION"); edition == string(config.EditionEnterprise2) {
|
||||
releaseMode = config.ReleaseMode{Mode: config.TagMode}
|
||||
releaseMode = config.ReleaseMode{Mode: config.Enterprise2Mode}
|
||||
if tag != "" {
|
||||
version = strings.TrimPrefix(tag, "v")
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func GenerateMetadata(c *cli.Context) (config.Metadata, error) {
|
||||
}
|
||||
// if there is a custom event targeting the main branch, that's an enterprise downstream build
|
||||
if mode == config.MainBranch {
|
||||
releaseMode = config.ReleaseMode{Mode: config.CustomMode}
|
||||
releaseMode = config.ReleaseMode{Mode: config.DownstreamMode}
|
||||
} else {
|
||||
releaseMode = config.ReleaseMode{Mode: mode}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
DroneTag = "DRONE_TAG"
|
||||
DroneSemverPrerelease = "DRONE_SEMVER_PRERELEASE"
|
||||
DroneBuildNumber = "DRONE_BUILD_NUMBER"
|
||||
Edition = "EDITION"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,7 +34,8 @@ func TestGetMetadata(t *testing.T) {
|
||||
{map[string]string{DroneBuildEvent: config.Push, DroneTargetBranch: versionedBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.ReleaseBranchMode}},
|
||||
{map[string]string{DroneBuildEvent: config.Push, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.MainMode}},
|
||||
{map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: versionedBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.ReleaseBranchMode}},
|
||||
{map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.Custom}},
|
||||
{map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.DownstreamMode}},
|
||||
{map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345", Edition: "enterprise2"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.Enterprise2Mode}},
|
||||
{map[string]string{DroneBuildEvent: config.Tag, DroneTargetBranch: "", DroneTag: "v9.2.0", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, "9.2.0", config.ReleaseMode{Mode: config.TagMode, IsBeta: false, IsTest: false}},
|
||||
{map[string]string{DroneBuildEvent: config.Tag, DroneTargetBranch: "", DroneTag: "v9.2.0-beta", DroneSemverPrerelease: "beta", DroneBuildNumber: "12345"}, "9.2.0-beta", config.ReleaseMode{Mode: config.TagMode, IsBeta: true, IsTest: false}},
|
||||
{map[string]string{DroneBuildEvent: config.Tag, DroneTargetBranch: "", DroneTag: "v9.2.0-test", DroneSemverPrerelease: "test", DroneBuildNumber: "12345"}, "9.2.0-test", config.ReleaseMode{Mode: config.TagMode, IsBeta: false, IsTest: true}},
|
||||
|
||||
@@ -146,7 +146,7 @@ func publishPackages(cfg packaging.PublishConfig) error {
|
||||
}
|
||||
|
||||
switch cfg.ReleaseMode.Mode {
|
||||
case config.MainMode, config.CustomMode, config.CronjobMode:
|
||||
case config.MainMode, config.DownstreamMode, config.CronjobMode:
|
||||
pth = path.Join(pth, packaging.MainFolder)
|
||||
default:
|
||||
pth = path.Join(pth, packaging.ReleaseFolder)
|
||||
|
||||
+10
-1
@@ -152,7 +152,7 @@ func main() {
|
||||
},
|
||||
{
|
||||
Name: "store-storybook",
|
||||
Usage: "Integrity check for storybook build",
|
||||
Usage: "Stores storybook to GCS buckets",
|
||||
Action: StoreStorybook,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
@@ -161,6 +161,11 @@ func main() {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "verify-storybook",
|
||||
Usage: "Integrity check for storybook build",
|
||||
Action: VerifyStorybook,
|
||||
},
|
||||
{
|
||||
Name: "upload-packages",
|
||||
Usage: "Upload Grafana packages",
|
||||
@@ -168,6 +173,10 @@ func main() {
|
||||
Flags: []cli.Flag{
|
||||
&jobsFlag,
|
||||
&editionFlag,
|
||||
&cli.BoolFlag{
|
||||
Name: "enterprise2",
|
||||
Usage: "Declare if the edition is enterprise2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,9 +69,17 @@ func UploadPackages(c *cli.Context) error {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
|
||||
edition, ok := os.LookupEnv("EDITION")
|
||||
if !ok {
|
||||
return fmt.Errorf("EDITION envvar is missing, exitting")
|
||||
var edition config.Edition
|
||||
if e, ok := os.LookupEnv("EDITION"); ok {
|
||||
edition = config.Edition(e)
|
||||
}
|
||||
|
||||
if c.Bool("enterprise2") {
|
||||
edition = config.EditionEnterprise2
|
||||
}
|
||||
|
||||
if edition == "" {
|
||||
return fmt.Errorf("both EDITION envvar and '--enterprise2' flag are missing. At least one of those is required")
|
||||
}
|
||||
|
||||
// TODO: Verify config values
|
||||
@@ -80,7 +88,7 @@ func UploadPackages(c *cli.Context) error {
|
||||
Version: version,
|
||||
Bucket: releaseModeConfig.Buckets.Artifacts,
|
||||
},
|
||||
edition: config.Edition(edition),
|
||||
edition: edition,
|
||||
versionMode: releaseMode.Mode,
|
||||
gcpKey: gcpKey,
|
||||
distDir: distDir,
|
||||
@@ -88,7 +96,7 @@ func UploadPackages(c *cli.Context) error {
|
||||
|
||||
if cfg.edition == config.EditionEnterprise2 {
|
||||
if releaseModeConfig.Buckets.ArtifactsEnterprise2 != "" {
|
||||
cfg.Config.Bucket = releaseModeConfig.Buckets.ArtifactsEnterprise2
|
||||
cfg.Bucket = releaseModeConfig.Buckets.ArtifactsEnterprise2
|
||||
} else {
|
||||
return fmt.Errorf("enterprise2 bucket var doesn't exist")
|
||||
}
|
||||
@@ -142,7 +150,7 @@ func uploadPackages(cfg uploadConfig) error {
|
||||
switch cfg.versionMode {
|
||||
case config.TagMode:
|
||||
versionFolder = releaseFolder
|
||||
case config.MainMode, config.CustomMode:
|
||||
case config.MainMode, config.DownstreamMode:
|
||||
versionFolder = mainFolder
|
||||
case config.ReleaseBranchMode:
|
||||
versionFolder = releaseBranchFolder
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Package verifystorybook contains the sub-command "verify-storybook".
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// VerifyStorybook Action implements the sub-command "verify-storybook".
|
||||
func VerifyStorybook(c *cli.Context) error {
|
||||
const grafanaDir = "."
|
||||
|
||||
paths := []string{
|
||||
"packages/grafana-ui/dist/storybook/index.html",
|
||||
"packages/grafana-ui/dist/storybook/iframe.html"}
|
||||
for _, p := range paths {
|
||||
exists, err := fs.Exists(filepath.Join(grafanaDir, p))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("failed to verify Storybook build: %s", err), 1)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("failed to verify Storybook build, missing %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Successfully verified Storybook integrity")
|
||||
return nil
|
||||
}
|
||||
@@ -8,7 +8,8 @@ const (
|
||||
TagMode VersionMode = "release"
|
||||
ReleaseBranchMode VersionMode = "branch"
|
||||
PullRequestMode VersionMode = "pull_request"
|
||||
CustomMode VersionMode = "custom"
|
||||
DownstreamMode VersionMode = "downstream"
|
||||
Enterprise2Mode VersionMode = "enterprise2"
|
||||
CronjobMode VersionMode = "cron"
|
||||
)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ var Versions = VersionMap{
|
||||
Storybook: "grafana-storybook",
|
||||
},
|
||||
},
|
||||
CustomMode: {
|
||||
DownstreamMode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
@@ -165,4 +165,42 @@ var Versions = VersionMap{
|
||||
StorybookSrcDir: "artifacts/storybook",
|
||||
},
|
||||
},
|
||||
Enterprise2Mode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: true,
|
||||
AdminSign: true,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: true,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
ArchARM64,
|
||||
ArchARMv7,
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
Ubuntu,
|
||||
},
|
||||
PrereleaseBucket: "grafana-prerelease/artifacts/docker",
|
||||
},
|
||||
Buckets: Buckets{
|
||||
Artifacts: "grafana-prerelease/artifacts/downloads",
|
||||
ArtifactsEnterprise2: "grafana-prerelease/artifacts/downloads-enterprise2",
|
||||
CDNAssets: "grafana-prerelease",
|
||||
CDNAssetsDir: "artifacts/static-assets",
|
||||
Storybook: "grafana-prerelease",
|
||||
StorybookSrcDir: "artifacts/storybook",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -340,18 +340,6 @@ func createPackage(srcDir string, options linuxPackageOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove unneeded binaries, these are exposed via wrappers that provide the needed configuration
|
||||
for _, fileName := range []string{
|
||||
cliBinary,
|
||||
cliBinary + ".md5",
|
||||
serverBinary,
|
||||
serverBinary + ".md5",
|
||||
} {
|
||||
if err := os.Remove(filepath.Join(packageRoot, options.homeBinDir, fileName)); err != nil {
|
||||
return fmt.Errorf("failed to remove %q: %w", filepath.Join(options.homeBinDir, fileName), err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := executeFPM(options, packageRoot, srcDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package packaging_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/packaging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPackageRegexp(t *testing.T) {
|
||||
t.Run("It should match enterprise2 packages", func(t *testing.T) {
|
||||
rgx := packaging.PackageRegexp(config.EditionEnterprise2)
|
||||
matches := []string{
|
||||
"grafana-enterprise2-1.2.3-4567pre.linux-amd64.tar.gz",
|
||||
"grafana-enterprise2-1.2.3-4567pre.linux-amd64.tar.gz.sha256",
|
||||
}
|
||||
for _, v := range matches {
|
||||
assert.Truef(t, rgx.MatchString(v), "'%s' should match regex '%s'", v, rgx.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -12,9 +12,13 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -102,7 +106,7 @@ func TestBuildConflictBlock(t *testing.T) {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Restore after destructive operation
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
for _, u := range tc.users {
|
||||
cmd := user.CreateUserCommand{
|
||||
@@ -111,7 +115,7 @@ func TestBuildConflictBlock(t *testing.T) {
|
||||
Login: u.Login,
|
||||
OrgID: int64(testOrgID),
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), cmd)
|
||||
_, err := usrSvc.CreateUserForTests(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
||||
@@ -207,7 +211,7 @@ conflict: test2
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Restore after destructive operation
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
for _, u := range tc.users {
|
||||
cmd := user.CreateUserCommand{
|
||||
@@ -216,7 +220,7 @@ conflict: test2
|
||||
Login: u.Login,
|
||||
OrgID: int64(testOrgID),
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), cmd)
|
||||
_, err := usrSvc.CreateUserForTests(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -385,6 +389,7 @@ func TestGetConflictingUsers(t *testing.T) {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Restore after destructive operation
|
||||
sqlStore := db.InitTestDB(t)
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
for _, u := range tc.users {
|
||||
cmd := user.CreateUserCommand{
|
||||
@@ -394,7 +399,7 @@ func TestGetConflictingUsers(t *testing.T) {
|
||||
OrgID: int64(testOrgID),
|
||||
IsServiceAccount: u.IsServiceAccount,
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), cmd)
|
||||
_, err := usrSvc.CreateUserForTests(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
||||
@@ -493,6 +498,7 @@ func TestGenerateConflictingUsersFile(t *testing.T) {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Restore after destructive operation
|
||||
sqlStore := db.InitTestDB(t)
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
for _, u := range tc.users {
|
||||
cmd := user.CreateUserCommand{
|
||||
@@ -501,7 +507,7 @@ func TestGenerateConflictingUsersFile(t *testing.T) {
|
||||
Login: u.Login,
|
||||
OrgID: int64(testOrgID),
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), cmd)
|
||||
_, err := usrSvc.CreateUserForTests(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
||||
@@ -543,6 +549,8 @@ func TestRunValidateConflictUserFile(t *testing.T) {
|
||||
t.Run("should validate file thats gets created", func(t *testing.T) {
|
||||
// Restore after destructive operation
|
||||
sqlStore := db.InitTestDB(t)
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
|
||||
const testOrgID int64 = 1
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
// add additional user with conflicting login where DOMAIN is upper case
|
||||
@@ -551,14 +559,14 @@ func TestRunValidateConflictUserFile(t *testing.T) {
|
||||
Login: "user_duplicate_test_1_login",
|
||||
OrgID: testOrgID,
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), dupUserLogincmd)
|
||||
_, err := usrSvc.Create(context.Background(), &dupUserLogincmd)
|
||||
require.NoError(t, err)
|
||||
dupUserEmailcmd := user.CreateUserCommand{
|
||||
Email: "USERDUPLICATETEST1@TEST.COM",
|
||||
Login: "USER_DUPLICATE_TEST_1_LOGIN",
|
||||
OrgID: testOrgID,
|
||||
}
|
||||
_, err = sqlStore.CreateUser(context.Background(), dupUserEmailcmd)
|
||||
_, err = usrSvc.Create(context.Background(), &dupUserEmailcmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get users
|
||||
@@ -589,6 +597,7 @@ func TestIntegrationMergeUser(t *testing.T) {
|
||||
teamSvc := teamimpl.ProvideService(sqlStore, setting.NewCfg())
|
||||
team1, err := teamSvc.CreateTeam("team1 name", "", 1)
|
||||
require.Nil(t, err)
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
const testOrgID int64 = 1
|
||||
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
@@ -601,7 +610,7 @@ func TestIntegrationMergeUser(t *testing.T) {
|
||||
Login: "user_duplicate_test_1_login",
|
||||
OrgID: testOrgID,
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), dupUserLogincmd)
|
||||
_, err := usrSvc.Create(context.Background(), &dupUserLogincmd)
|
||||
require.NoError(t, err)
|
||||
dupUserEmailcmd := user.CreateUserCommand{
|
||||
Email: "USERDUPLICATETEST1@TEST.COM",
|
||||
@@ -609,7 +618,7 @@ func TestIntegrationMergeUser(t *testing.T) {
|
||||
Login: "USER_DUPLICATE_TEST_1_LOGIN",
|
||||
OrgID: testOrgID,
|
||||
}
|
||||
userWithUpperCase, err := sqlStore.CreateUser(context.Background(), dupUserEmailcmd)
|
||||
userWithUpperCase, err := usrSvc.Create(context.Background(), &dupUserEmailcmd)
|
||||
require.NoError(t, err)
|
||||
// this is the user we want to update to another team
|
||||
err = teamSvc.AddTeamMember(userWithUpperCase.ID, testOrgID, team1.Id, false, 0)
|
||||
@@ -746,6 +755,7 @@ conflict: test2
|
||||
for _, tc := range testCases {
|
||||
// Restore after destructive operation
|
||||
sqlStore := db.InitTestDB(t)
|
||||
usrSvc := setupTestUserService(t, sqlStore)
|
||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
||||
for _, u := range tc.users {
|
||||
cmd := user.CreateUserCommand{
|
||||
@@ -754,7 +764,7 @@ conflict: test2
|
||||
Login: u.Login,
|
||||
OrgID: int64(testOrgID),
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), cmd)
|
||||
_, err := usrSvc.CreateUserForTests(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// add additional user with conflicting login where DOMAIN is upper case
|
||||
@@ -840,3 +850,13 @@ func TestMarshalConflictUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestUserService(t *testing.T, sqlStore *sqlstore.SQLStore) user.Service {
|
||||
t.Helper()
|
||||
orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, "atest.FakeQuotaService{})
|
||||
require.NoError(t, err)
|
||||
usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, "atest.FakeQuotaService{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return usrSvc
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (decl *DeclForGen) Lineage() thema.Lineage {
|
||||
// ForLatestSchema returns a [SchemaForGen] for the latest schema in this
|
||||
// DeclForGen's lineage.
|
||||
func (decl *DeclForGen) ForLatestSchema() SchemaForGen {
|
||||
comm := decl.Meta.Common()
|
||||
comm := decl.Properties.Common()
|
||||
return SchemaForGen{
|
||||
Name: comm.Name,
|
||||
Schema: decl.Lineage().Latest(),
|
||||
|
||||
@@ -21,7 +21,7 @@ func CoreStructuredKindJenny(gokindsdir string, cfg *CoreStructuredKindGenerator
|
||||
}
|
||||
if cfg.GenDirName == nil {
|
||||
cfg.GenDirName = func(decl *DeclForGen) string {
|
||||
return decl.Meta.Common().MachineName
|
||||
return decl.Properties.Common().MachineName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (gen *genCoreStructuredKind) Generate(decl *DeclForGen) (*codejen.File, err
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go")
|
||||
path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Properties.Common().MachineName+"_kind_gen.go")
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpls.Lookup("kind_corestructured.tmpl").Execute(buf, decl); err != nil {
|
||||
return nil, fmt.Errorf("failed executing kind_corestructured template for %s: %w", path, err)
|
||||
|
||||
@@ -33,7 +33,7 @@ func (j *lmox) Generate(decl *DeclForGen) (codejen.Files, error) {
|
||||
if decl.IsRaw() {
|
||||
return nil, nil
|
||||
}
|
||||
comm := decl.Meta.Common()
|
||||
comm := decl.Properties.Common()
|
||||
sfg := SchemaForGen{
|
||||
Name: comm.Name,
|
||||
IsGroup: comm.LineageIsGroup,
|
||||
@@ -42,7 +42,7 @@ func (j *lmox) Generate(decl *DeclForGen) (codejen.Files, error) {
|
||||
do := func(sfg SchemaForGen, infix string) (codejen.Files, error) {
|
||||
f, err := j.inner.Generate(sfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Meta.Common().Name, err)
|
||||
return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Properties.Common().Name, err)
|
||||
}
|
||||
if f == nil || !f.Exists() {
|
||||
return nil, nil
|
||||
|
||||
@@ -21,7 +21,7 @@ func RawKindJenny(gokindsdir string, cfg *RawKindGeneratorConfig) OneToOne {
|
||||
}
|
||||
if cfg.GenDirName == nil {
|
||||
cfg.GenDirName = func(decl *DeclForGen) string {
|
||||
return decl.Meta.Common().MachineName
|
||||
return decl.Properties.Common().MachineName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (gen *genRawKind) Generate(decl *DeclForGen) (*codejen.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go")
|
||||
path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Properties.Common().MachineName+"_kind_gen.go")
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpls.Lookup("kind_raw.tmpl").Execute(buf, decl); err != nil {
|
||||
return nil, fmt.Errorf("failed executing kind_raw template for %s: %w", path, err)
|
||||
|
||||
@@ -48,15 +48,15 @@ func (gen *genTSVeneerIndex) Generate(decls ...*DeclForGen) (*codejen.File, erro
|
||||
|
||||
sch := decl.Lineage().Latest()
|
||||
f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{
|
||||
RootName: decl.Meta.Common().Name,
|
||||
Group: decl.Meta.Common().LineageIsGroup,
|
||||
RootName: decl.Properties.Common().Name,
|
||||
Group: decl.Properties.Common().LineageIsGroup,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err)
|
||||
return nil, fmt.Errorf("%s: %w", decl.Properties.Common().Name, err)
|
||||
}
|
||||
elems, err := gen.extractTSIndexVeneerElements(decl, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err)
|
||||
return nil, fmt.Errorf("%s: %w", decl.Properties.Common().Name, err)
|
||||
}
|
||||
tsf.Nodes = append(tsf.Nodes, elems...)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (gen *genTSVeneerIndex) Generate(decls ...*DeclForGen) (*codejen.File, erro
|
||||
|
||||
func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(decl *DeclForGen, tf *ast.File) ([]ast.Decl, error) {
|
||||
lin := decl.Lineage()
|
||||
comm := decl.Meta.Common()
|
||||
comm := decl.Properties.Common()
|
||||
|
||||
// Check the root, then walk the tree
|
||||
rootv := lin.Latest().Underlying()
|
||||
@@ -131,7 +131,7 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(decl *DeclForGen, tf *
|
||||
}
|
||||
|
||||
vpath := fmt.Sprintf("v%v", thema.LatestVersion(lin)[0])
|
||||
if decl.Meta.Common().Maturity.Less(kindsys.MaturityStable) {
|
||||
if decl.Properties.Common().Maturity.Less(kindsys.MaturityStable) {
|
||||
vpath = "x"
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func (j *latestj) Generate(decl *DeclForGen) (*codejen.File, error) {
|
||||
if decl.IsRaw() {
|
||||
return nil, nil
|
||||
}
|
||||
comm := decl.Meta.Common()
|
||||
comm := decl.Properties.Common()
|
||||
sfg := SchemaForGen{
|
||||
Name: comm.Name,
|
||||
Schema: decl.Lineage().Latest(),
|
||||
@@ -44,7 +44,7 @@ func (j *latestj) Generate(decl *DeclForGen) (*codejen.File, error) {
|
||||
|
||||
f, err := j.inner.Generate(sfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Meta.Common().Name, err)
|
||||
return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Properties.Common().Name, err)
|
||||
}
|
||||
if f == nil || !f.Exists() {
|
||||
return nil, nil
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package {{ .Meta.MachineName }}
|
||||
package {{ .Properties.MachineName }}
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
// directory containing the .cue files in which this kind is declared. Necessary
|
||||
// for runtime errors related to the declaration and/or lineage to provide
|
||||
// a real path to the correct .cue file.
|
||||
const rootrel string = "kinds/structured/{{ .Meta.MachineName }}"
|
||||
const rootrel string = "kinds/structured/{{ .Properties.MachineName }}"
|
||||
|
||||
// TODO standard generated docs
|
||||
type Kind struct {
|
||||
lin thema.ConvergentLineage[*{{ .Meta.Name }}]
|
||||
lin thema.ConvergentLineage[*{{ .Properties.Name }}]
|
||||
jcodec vmux.Codec
|
||||
valmux vmux.ValueMux[*{{ .Meta.Name }}]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredMeta]
|
||||
valmux vmux.ValueMux[*{{ .Properties.Name }}]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredProperties]
|
||||
}
|
||||
|
||||
// type guard
|
||||
@@ -25,7 +25,7 @@ var _ kindsys.Structured = &Kind{}
|
||||
|
||||
// TODO standard generated docs
|
||||
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,14 +40,14 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
|
||||
// Get the thema.Schema that the meta says is in the current version (which
|
||||
// codegen ensures is always the latest)
|
||||
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
|
||||
tsch, err := thema.BindType[*{{ .Meta.Name }}](cursch, &{{ .Meta.Name }}{})
|
||||
cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion)
|
||||
tsch, err := thema.BindType[*{{ .Properties.Name }}](cursch, &{{ .Properties.Name }}{})
|
||||
if err != nil {
|
||||
// Should be unreachable, modulo bugs in the Thema->Go code generator
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k.jcodec = vmux.NewJSONCodec("{{ .Meta.MachineName }}.json")
|
||||
k.jcodec = vmux.NewJSONCodec("{{ .Properties.MachineName }}.json")
|
||||
k.lin = tsch.ConvergentLineage()
|
||||
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec)
|
||||
return k, nil
|
||||
@@ -55,12 +55,12 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Name() string {
|
||||
return "{{ .Meta.MachineName }}"
|
||||
return "{{ .Properties.MachineName }}"
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) MachineName() string {
|
||||
return "{{ .Meta.MachineName }}"
|
||||
return "{{ .Properties.MachineName }}"
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
@@ -69,28 +69,37 @@ func (k *Kind) Lineage() thema.Lineage {
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Meta.Name }}] {
|
||||
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Properties.Name }}] {
|
||||
return k.lin
|
||||
}
|
||||
|
||||
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
|
||||
// at any schematized dashboard version to an instance of {{ .Meta.Name }}.
|
||||
// at any schematized dashboard version to an instance of {{ .Properties.Name }}.
|
||||
//
|
||||
// Validation and translation errors emitted from this func will identify the
|
||||
// input bytes as "dashboard.json".
|
||||
//
|
||||
// This is a thin wrapper around Thema's [vmux.ValueMux].
|
||||
func (k *Kind) JSONValueMux(b []byte) (*{{ .Meta.Name }}, thema.TranslationLacunas, error) {
|
||||
func (k *Kind) JSONValueMux(b []byte) (*{{ .Properties.Name }}, thema.TranslationLacunas, error) {
|
||||
return k.valmux(b)
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Maturity() kindsys.Maturity {
|
||||
return k.decl.Meta.Maturity
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] {
|
||||
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
|
||||
// {{ .Properties.MachineName }} declaration in .cue files.
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] {
|
||||
d := k.decl
|
||||
return &d
|
||||
}
|
||||
|
||||
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties],
|
||||
// representing the static properties declared in the {{ .Properties.MachineName }} kind.
|
||||
//
|
||||
// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface].
|
||||
func (k *Kind) Props() kindsys.SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package {{ .Meta.MachineName }}
|
||||
package {{ .Properties.MachineName }}
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// TODO standard generated docs
|
||||
type Kind struct {
|
||||
decl kindsys.Decl[kindsys.RawMeta]
|
||||
decl kindsys.Decl[kindsys.RawProperties]
|
||||
}
|
||||
|
||||
// type guard
|
||||
@@ -16,7 +16,7 @@ var _ kindsys.Raw = &Kind{}
|
||||
|
||||
// TODO standard generated docs
|
||||
func NewKind() (*Kind, error) {
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/{{ .Meta.MachineName }}", nil, nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.RawProperties]("kinds/raw/{{ .Properties.MachineName }}", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -28,21 +28,30 @@ func NewKind() (*Kind, error) {
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Name() string {
|
||||
return "{{ .Meta.Name }}"
|
||||
return "{{ .Properties.Name }}"
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) MachineName() string {
|
||||
return "{{ .Meta.MachineName }}"
|
||||
return "{{ .Properties.MachineName }}"
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Maturity() kindsys.Maturity {
|
||||
return k.decl.Meta.Maturity
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.RawMeta] {
|
||||
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
|
||||
// {{ .Properties.MachineName }} declaration in .cue files.
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.RawProperties] {
|
||||
d := k.decl
|
||||
return &d
|
||||
}
|
||||
|
||||
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.RawProperties],
|
||||
// representing the static properties declared in the {{ .Properties.MachineName }} kind.
|
||||
//
|
||||
// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface].
|
||||
func (k *Kind) Props() kindsys.SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
{{range .Kinds }}
|
||||
"{{ $.KindPackagePrefix }}/{{ .Meta.MachineName }}"{{end}}
|
||||
"{{ $.KindPackagePrefix }}/{{ .Properties.MachineName }}"{{end}}
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
"github.com/grafana/thema"
|
||||
@@ -25,19 +25,19 @@ type Base struct {
|
||||
all []kindsys.Interface
|
||||
numRaw, numStructured int
|
||||
{{- range .Kinds }}
|
||||
{{ .Meta.MachineName }} *{{ .Meta.MachineName }}.Kind{{end}}
|
||||
{{ .Properties.MachineName }} *{{ .Properties.MachineName }}.Kind{{end}}
|
||||
}
|
||||
|
||||
// type guards
|
||||
var (
|
||||
{{- range .Kinds }}
|
||||
_ kindsys.{{ if .IsRaw }}Raw{{ else }}Structured{{ end }} = &{{ .Meta.MachineName }}.Kind{}{{end}}
|
||||
_ kindsys.{{ if .IsRaw }}Raw{{ else }}Structured{{ end }} = &{{ .Properties.MachineName }}.Kind{}{{end}}
|
||||
)
|
||||
|
||||
{{range .Kinds }}
|
||||
// {{ .Meta.Name }} returns the [kindsys.Interface] implementation for the {{ .Meta.MachineName }} kind.
|
||||
func (b *Base) {{ .Meta.Name }}() *{{ .Meta.MachineName }}.Kind {
|
||||
return b.{{ .Meta.MachineName }}
|
||||
// {{ .Properties.Name }} returns the [kindsys.Interface] implementation for the {{ .Properties.MachineName }} kind.
|
||||
func (b *Base) {{ .Properties.Name }}() *{{ .Properties.MachineName }}.Kind {
|
||||
return b.{{ .Properties.MachineName }}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
@@ -49,11 +49,11 @@ func doNewBase(rt *thema.Runtime) *Base {
|
||||
}
|
||||
|
||||
{{range .Kinds }}
|
||||
reg.{{ .Meta.MachineName }}, err = {{ .Meta.MachineName }}.NewKind({{ if .IsCoreStructured }}rt{{ end }})
|
||||
reg.{{ .Properties.MachineName }}, err = {{ .Properties.MachineName }}.NewKind({{ if .IsCoreStructured }}rt{{ end }})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error while initializing the {{ .Meta.MachineName }} Kind: %s", err))
|
||||
panic(fmt.Sprintf("error while initializing the {{ .Properties.MachineName }} Kind: %s", err))
|
||||
}
|
||||
reg.all = append(reg.all, reg.{{ .Meta.MachineName }})
|
||||
reg.all = append(reg.all, reg.{{ .Properties.MachineName }})
|
||||
{{end}}
|
||||
|
||||
return reg
|
||||
|
||||
@@ -31,6 +31,14 @@ var InitTestDBwithCfg = sqlstore.InitTestDBWithCfg
|
||||
var ProvideService = sqlstore.ProvideService
|
||||
var NewSqlBuilder = sqlstore.NewSqlBuilder
|
||||
|
||||
func IsTestDbSQLite() bool {
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); !present || db == "sqlite" {
|
||||
return true
|
||||
}
|
||||
|
||||
return !IsTestDbMySQL() && !IsTestDbPostgres()
|
||||
}
|
||||
|
||||
func IsTestDbMySQL() bool {
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
return db == migrator.MySQL
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type requestStartTimeContextKey struct{}
|
||||
|
||||
var requestStartTime = requestStartTimeContextKey{}
|
||||
|
||||
// InitCounter creates a pointer on the context that can be incremented later
|
||||
func InitstartTime(ctx context.Context, now time.Time) context.Context {
|
||||
return context.WithValue(ctx, requestStartTime, now)
|
||||
}
|
||||
|
||||
// TimeSinceStart returns time spend since the request started in grafana
|
||||
func TimeSinceStart(ctx context.Context, now time.Time) time.Duration {
|
||||
val := ctx.Value(requestStartTime)
|
||||
if val != nil {
|
||||
startTime, ok := val.(time.Time)
|
||||
if ok {
|
||||
return now.Sub(startTime)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -14,12 +14,14 @@ const databaseCacheType = "database"
|
||||
|
||||
type databaseCache struct {
|
||||
SQLStore db.DB
|
||||
codec codec
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func newDatabaseCache(sqlstore db.DB) *databaseCache {
|
||||
func newDatabaseCache(sqlstore db.DB, codec codec) *databaseCache {
|
||||
dc := &databaseCache{
|
||||
SQLStore: sqlstore,
|
||||
codec: codec,
|
||||
log: log.New("remotecache.database"),
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, erro
|
||||
}
|
||||
}
|
||||
|
||||
if err = decodeGob(cacheHit.Data, item); err != nil {
|
||||
if err = dc.codec.Decode(ctx, cacheHit.Data, item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, erro
|
||||
|
||||
func (dc *databaseCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error {
|
||||
item := &cachedItem{Val: value}
|
||||
data, err := encodeGob(item)
|
||||
data, err := dc.codec.Encode(ctx, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) {
|
||||
|
||||
db := &databaseCache{
|
||||
SQLStore: sqlstore,
|
||||
codec: &gobCodec{},
|
||||
log: log.New("remotecache.database"),
|
||||
}
|
||||
|
||||
@@ -64,6 +65,7 @@ func TestSecondSet(t *testing.T) {
|
||||
|
||||
db := &databaseCache{
|
||||
SQLStore: sqlstore,
|
||||
codec: &gobCodec{},
|
||||
log: log.New("remotecache.database"),
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ import (
|
||||
const memcachedCacheType = "memcached"
|
||||
|
||||
type memcachedStorage struct {
|
||||
c *memcache.Client
|
||||
c *memcache.Client
|
||||
codec codec
|
||||
}
|
||||
|
||||
func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage {
|
||||
func newMemcachedStorage(opts *setting.RemoteCacheOptions, codec codec) *memcachedStorage {
|
||||
return &memcachedStorage{
|
||||
c: memcache.New(opts.ConnStr),
|
||||
c: memcache.New(opts.ConnStr),
|
||||
codec: codec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +33,7 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item {
|
||||
// Set sets value to given key in the cache.
|
||||
func (s *memcachedStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error {
|
||||
item := &cachedItem{Val: val}
|
||||
bytes, err := encodeGob(item)
|
||||
bytes, err := s.codec.Encode(ctx, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -58,7 +60,7 @@ func (s *memcachedStorage) Get(ctx context.Context, key string) (interface{}, er
|
||||
|
||||
item := &cachedItem{}
|
||||
|
||||
err = decodeGob(memcachedItem.Value, item)
|
||||
err = s.codec.Decode(ctx, memcachedItem.Value, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
const redisCacheType = "redis"
|
||||
|
||||
type redisStorage struct {
|
||||
c *redis.Client
|
||||
c *redis.Client
|
||||
codec codec
|
||||
}
|
||||
|
||||
// parseRedisConnStr parses k=v pairs in csv and builds a redis Options object
|
||||
@@ -76,18 +77,18 @@ func parseRedisConnStr(connStr string) (*redis.Options, error) {
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) {
|
||||
func newRedisStorage(opts *setting.RemoteCacheOptions, codec codec) (*redisStorage, error) {
|
||||
opt, err := parseRedisConnStr(opts.ConnStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &redisStorage{c: redis.NewClient(opt)}, nil
|
||||
return &redisStorage{c: redis.NewClient(opt), codec: codec}, nil
|
||||
}
|
||||
|
||||
// Set sets value to given key in session.
|
||||
func (s *redisStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error {
|
||||
item := &cachedItem{Val: val}
|
||||
value, err := encodeGob(item)
|
||||
value, err := s.codec.Encode(ctx, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -100,7 +101,7 @@ func (s *redisStorage) Get(ctx context.Context, key string) (interface{}, error)
|
||||
v := s.c.Get(ctx, key)
|
||||
|
||||
item := &cachedItem{}
|
||||
err := decodeGob([]byte(v.Val()), item)
|
||||
err := s.codec.Decode(ctx, []byte(v.Val()), item)
|
||||
|
||||
if err == nil {
|
||||
return item.Val, nil
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
glog "github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -29,8 +30,14 @@ const (
|
||||
ServiceName = "RemoteCache"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore db.DB) (*RemoteCache, error) {
|
||||
client, err := createClient(cfg.RemoteCacheOptions, sqlStore)
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, secretsService secrets.Service) (*RemoteCache, error) {
|
||||
var codec codec
|
||||
if cfg.RemoteCacheOptions.Encryption {
|
||||
codec = &encryptionCodec{secretsService}
|
||||
} else {
|
||||
codec = &gobCodec{}
|
||||
}
|
||||
client, err := createClient(cfg.RemoteCacheOptions, sqlStore, codec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -97,20 +104,24 @@ func (ds *RemoteCache) Run(ctx context.Context) error {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (CacheStorage, error) {
|
||||
if opts.Name == redisCacheType {
|
||||
return newRedisStorage(opts)
|
||||
func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) (cache CacheStorage, err error) {
|
||||
switch opts.Name {
|
||||
case redisCacheType:
|
||||
cache, err = newRedisStorage(opts, codec)
|
||||
case memcachedCacheType:
|
||||
cache = newMemcachedStorage(opts, codec)
|
||||
case databaseCacheType:
|
||||
cache = newDatabaseCache(sqlstore, codec)
|
||||
default:
|
||||
return nil, ErrInvalidCacheType
|
||||
}
|
||||
|
||||
if opts.Name == memcachedCacheType {
|
||||
return newMemcachedStorage(opts), nil
|
||||
if err != nil {
|
||||
return cache, err
|
||||
}
|
||||
|
||||
if opts.Name == databaseCacheType {
|
||||
return newDatabaseCache(sqlstore), nil
|
||||
if opts.Prefix != "" {
|
||||
cache = &prefixCacheStorage{cache: cache, prefix: opts.Prefix}
|
||||
}
|
||||
|
||||
return nil, ErrInvalidCacheType
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
// Register records a type, identified by a value for that type, under its
|
||||
@@ -127,13 +138,57 @@ type cachedItem struct {
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func encodeGob(item *cachedItem) ([]byte, error) {
|
||||
type codec interface {
|
||||
Encode(context.Context, *cachedItem) ([]byte, error)
|
||||
Decode(context.Context, []byte, *cachedItem) error
|
||||
}
|
||||
|
||||
type gobCodec struct{}
|
||||
|
||||
func (c *gobCodec) Encode(_ context.Context, item *cachedItem) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := gob.NewEncoder(buf).Encode(item)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func decodeGob(data []byte, out *cachedItem) error {
|
||||
func (c *gobCodec) Decode(_ context.Context, data []byte, out *cachedItem) error {
|
||||
buf := bytes.NewBuffer(data)
|
||||
return gob.NewDecoder(buf).Decode(&out)
|
||||
}
|
||||
|
||||
type encryptionCodec struct {
|
||||
secretsService secrets.Service
|
||||
}
|
||||
|
||||
func (c *encryptionCodec) Encode(ctx context.Context, item *cachedItem) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := gob.NewEncoder(buf).Encode(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.secretsService.Encrypt(ctx, buf.Bytes(), secrets.WithoutScope())
|
||||
}
|
||||
|
||||
func (c *encryptionCodec) Decode(ctx context.Context, data []byte, out *cachedItem) error {
|
||||
decrypted, err := c.secretsService.Decrypt(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBuffer(decrypted)
|
||||
return gob.NewDecoder(buf).Decode(&out)
|
||||
}
|
||||
|
||||
type prefixCacheStorage struct {
|
||||
cache CacheStorage
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (pcs *prefixCacheStorage) Get(ctx context.Context, key string) (interface{}, error) {
|
||||
return pcs.cache.Get(ctx, pcs.prefix+key)
|
||||
}
|
||||
func (pcs *prefixCacheStorage) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error {
|
||||
return pcs.cache.Set(ctx, pcs.prefix+key, value, expire)
|
||||
}
|
||||
func (pcs *prefixCacheStorage) Delete(ctx context.Context, key string) error {
|
||||
return pcs.cache.Delete(ctx, pcs.prefix+key)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -27,7 +29,7 @@ func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore d
|
||||
cfg := &setting.Cfg{
|
||||
RemoteCacheOptions: opts,
|
||||
}
|
||||
dc, err := ProvideService(cfg, sqlstore)
|
||||
dc, err := ProvideService(cfg, sqlstore, fakes.NewFakeSecretsService())
|
||||
require.Nil(t, err, "Failed to init client for test")
|
||||
|
||||
return dc
|
||||
@@ -45,7 +47,7 @@ func TestCachedBasedOnConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidCacheTypeReturnsError(t *testing.T) {
|
||||
_, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil)
|
||||
_, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil, &gobCodec{})
|
||||
assert.Equal(t, err, ErrInvalidCacheType)
|
||||
}
|
||||
|
||||
@@ -88,3 +90,28 @@ func canNotFetchExpiredItems(t *testing.T, client CacheStorage) {
|
||||
_, err = client.Get(context.Background(), "key1")
|
||||
assert.Equal(t, err, ErrCacheItemNotFound)
|
||||
}
|
||||
|
||||
func TestCachePrefix(t *testing.T) {
|
||||
db := db.InitTestDB(t)
|
||||
cache := &databaseCache{
|
||||
SQLStore: db,
|
||||
log: log.New("remotecache.database"),
|
||||
codec: &gobCodec{},
|
||||
}
|
||||
prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"}
|
||||
|
||||
// Set a value (with a prefix)
|
||||
err := prefixCache.Set(context.Background(), "foo", "bar", time.Hour)
|
||||
require.NoError(t, err)
|
||||
// Get a value (with a prefix)
|
||||
v, err := prefixCache.Get(context.Background(), "foo")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "bar", v)
|
||||
// Get a value directly from the underlying cache, ensure the prefix is in the key
|
||||
v, err = cache.Get(context.Background(), "test/foo")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "bar", v)
|
||||
// Get a value directly from the underlying cache without a prefix, should not be there
|
||||
_, err = cache.Get(context.Background(), "foo")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func NewFakeStore(t *testing.T) *RemoteCache {
|
||||
|
||||
dc, err := ProvideService(&setting.Cfg{
|
||||
RemoteCacheOptions: opts,
|
||||
}, sqlStore)
|
||||
}, sqlStore, fakes.NewFakeSecretsService())
|
||||
require.NoError(t, err, "Failed to init remote cache for test")
|
||||
|
||||
return dc
|
||||
|
||||
@@ -26,7 +26,7 @@ type Kind struct {
|
||||
lin thema.ConvergentLineage[*Dashboard]
|
||||
jcodec vmux.Codec
|
||||
valmux vmux.ValueMux[*Dashboard]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredMeta]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredProperties]
|
||||
}
|
||||
|
||||
// type guard
|
||||
@@ -34,7 +34,7 @@ var _ kindsys.Structured = &Kind{}
|
||||
|
||||
// TODO standard generated docs
|
||||
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
|
||||
// Get the thema.Schema that the meta says is in the current version (which
|
||||
// codegen ensures is always the latest)
|
||||
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
|
||||
cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion)
|
||||
tsch, err := thema.BindType[*Dashboard](cursch, &Dashboard{})
|
||||
if err != nil {
|
||||
// Should be unreachable, modulo bugs in the Thema->Go code generator
|
||||
@@ -95,11 +95,20 @@ func (k *Kind) JSONValueMux(b []byte) (*Dashboard, thema.TranslationLacunas, err
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Maturity() kindsys.Maturity {
|
||||
return k.decl.Meta.Maturity
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] {
|
||||
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
|
||||
// dashboard declaration in .cue files.
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] {
|
||||
d := k.decl
|
||||
return &d
|
||||
}
|
||||
|
||||
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties],
|
||||
// representing the static properties declared in the dashboard kind.
|
||||
//
|
||||
// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface].
|
||||
func (k *Kind) Props() kindsys.SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Kind struct {
|
||||
lin thema.ConvergentLineage[*Playlist]
|
||||
jcodec vmux.Codec
|
||||
valmux vmux.ValueMux[*Playlist]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredMeta]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredProperties]
|
||||
}
|
||||
|
||||
// type guard
|
||||
@@ -34,7 +34,7 @@ var _ kindsys.Structured = &Kind{}
|
||||
|
||||
// TODO standard generated docs
|
||||
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
|
||||
// Get the thema.Schema that the meta says is in the current version (which
|
||||
// codegen ensures is always the latest)
|
||||
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
|
||||
cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion)
|
||||
tsch, err := thema.BindType[*Playlist](cursch, &Playlist{})
|
||||
if err != nil {
|
||||
// Should be unreachable, modulo bugs in the Thema->Go code generator
|
||||
@@ -95,11 +95,20 @@ func (k *Kind) JSONValueMux(b []byte) (*Playlist, thema.TranslationLacunas, erro
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Maturity() kindsys.Maturity {
|
||||
return k.decl.Meta.Maturity
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] {
|
||||
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
|
||||
// playlist declaration in .cue files.
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] {
|
||||
d := k.decl
|
||||
return &d
|
||||
}
|
||||
|
||||
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties],
|
||||
// representing the static properties declared in the playlist kind.
|
||||
//
|
||||
// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface].
|
||||
func (k *Kind) Props() kindsys.SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// TODO standard generated docs
|
||||
type Kind struct {
|
||||
decl kindsys.Decl[kindsys.RawMeta]
|
||||
decl kindsys.Decl[kindsys.RawProperties]
|
||||
}
|
||||
|
||||
// type guard
|
||||
@@ -23,7 +23,7 @@ var _ kindsys.Raw = &Kind{}
|
||||
|
||||
// TODO standard generated docs
|
||||
func NewKind() (*Kind, error) {
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/svg", nil, nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.RawProperties]("kinds/raw/svg", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -45,11 +45,20 @@ func (k *Kind) MachineName() string {
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Maturity() kindsys.Maturity {
|
||||
return k.decl.Meta.Maturity
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.RawMeta] {
|
||||
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
|
||||
// svg declaration in .cue files.
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.RawProperties] {
|
||||
d := k.decl
|
||||
return &d
|
||||
}
|
||||
|
||||
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.RawProperties],
|
||||
// representing the static properties declared in the svg kind.
|
||||
//
|
||||
// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface].
|
||||
func (k *Kind) Props() kindsys.SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Kind struct {
|
||||
lin thema.ConvergentLineage[*Team]
|
||||
jcodec vmux.Codec
|
||||
valmux vmux.ValueMux[*Team]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredMeta]
|
||||
decl kindsys.Decl[kindsys.CoreStructuredProperties]
|
||||
}
|
||||
|
||||
// type guard
|
||||
@@ -34,7 +34,7 @@ var _ kindsys.Structured = &Kind{}
|
||||
|
||||
// TODO standard generated docs
|
||||
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
|
||||
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
|
||||
// Get the thema.Schema that the meta says is in the current version (which
|
||||
// codegen ensures is always the latest)
|
||||
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
|
||||
cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion)
|
||||
tsch, err := thema.BindType[*Team](cursch, &Team{})
|
||||
if err != nil {
|
||||
// Should be unreachable, modulo bugs in the Thema->Go code generator
|
||||
@@ -95,11 +95,20 @@ func (k *Kind) JSONValueMux(b []byte) (*Team, thema.TranslationLacunas, error) {
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Maturity() kindsys.Maturity {
|
||||
return k.decl.Meta.Maturity
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
// TODO standard generated docs
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] {
|
||||
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
|
||||
// team declaration in .cue files.
|
||||
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] {
|
||||
d := k.decl
|
||||
return &d
|
||||
}
|
||||
|
||||
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties],
|
||||
// representing the static properties declared in the team kind.
|
||||
//
|
||||
// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface].
|
||||
func (k *Kind) Props() kindsys.SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
+17
-7
@@ -41,15 +41,25 @@ func (m Maturity) Less(om Maturity) bool {
|
||||
return maturityIdx(m) < maturityIdx(om)
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
// Interface describes a Grafana kind object: a Go representation of the definition of
|
||||
// one of Grafana's categories of kinds.
|
||||
type Interface interface {
|
||||
// TODO docs
|
||||
// Props returns a [kindsys.SomeKindProps], representing the properties
|
||||
// of the kind as declared in the .cue source. The underlying type is
|
||||
// determined by the category of kind.
|
||||
//
|
||||
// This method is largely for convenience, as all actual kind categories are
|
||||
// expected to implement one of the other interfaces, each of which contain
|
||||
// a Decl() method through which these same properties are accessible.
|
||||
Props() SomeKindProperties
|
||||
|
||||
// TODO remove, unnecessary with Props()
|
||||
Name() string
|
||||
|
||||
// TODO docs
|
||||
// TODO remove, unnecessary with Props()
|
||||
MachineName() string
|
||||
|
||||
// TODO docs
|
||||
// TODO remove, unnecessary with Props()
|
||||
Maturity() Maturity // TODO unclear if we want maturity for raw kinds
|
||||
}
|
||||
|
||||
@@ -58,7 +68,7 @@ type Raw interface {
|
||||
Interface
|
||||
|
||||
// TODO docs
|
||||
Decl() *Decl[RawMeta]
|
||||
Decl() *Decl[RawProperties]
|
||||
}
|
||||
|
||||
type Structured interface {
|
||||
@@ -68,7 +78,7 @@ type Structured interface {
|
||||
Lineage() thema.Lineage
|
||||
|
||||
// TODO docs
|
||||
Decl() *Decl[CoreStructuredMeta] // TODO figure out how to reconcile this interface with CustomStructuredMeta
|
||||
Decl() *Decl[CoreStructuredProperties] // TODO figure out how to reconcile this interface with CustomStructuredProperties
|
||||
}
|
||||
|
||||
// type Composable interface {
|
||||
@@ -78,5 +88,5 @@ type Structured interface {
|
||||
// Lineage() thema.Lineage
|
||||
//
|
||||
// // TODO docs
|
||||
// Meta() CoreStructuredMeta // TODO figure out how to reconcile this interface with CustomStructuredMeta
|
||||
// Properties() CoreStructuredProperties // TODO figure out how to reconcile this interface with CustomStructuredProperties
|
||||
// }
|
||||
|
||||
+50
-35
@@ -2,8 +2,8 @@ package kindsys
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
// CommonMeta contains the metadata common to all categories of kinds.
|
||||
type CommonMeta struct {
|
||||
// CommonProperties contains the metadata common to all categories of kinds.
|
||||
type CommonProperties struct {
|
||||
Name string `json:"name"`
|
||||
PluralName string `json:"pluralName"`
|
||||
MachineName string `json:"machineName"`
|
||||
@@ -12,63 +12,78 @@ type CommonMeta struct {
|
||||
Maturity Maturity `json:"maturity"`
|
||||
}
|
||||
|
||||
// TODO generate from type.cue
|
||||
type RawMeta struct {
|
||||
CommonMeta
|
||||
// RawProperties represents the static properties in a #Raw kind declaration that are
|
||||
// trivially representable with basic Go types.
|
||||
//
|
||||
// When a .cue #Raw declaration is loaded through the standard [LoadCoreKind],
|
||||
// func, it is fully validated and populated according to all rules specified
|
||||
// in CUE for #Raw kinds.
|
||||
type RawProperties struct {
|
||||
CommonProperties
|
||||
Extensions []string `json:"extensions"`
|
||||
}
|
||||
|
||||
func (m RawMeta) _private() {}
|
||||
func (m RawMeta) Common() CommonMeta {
|
||||
return m.CommonMeta
|
||||
func (m RawProperties) _private() {}
|
||||
func (m RawProperties) Common() CommonProperties {
|
||||
return m.CommonProperties
|
||||
}
|
||||
|
||||
// TODO
|
||||
type CoreStructuredMeta struct {
|
||||
CommonMeta
|
||||
// CoreStructuredProperties represents the static properties in the declaration of a
|
||||
// #CoreStructured kind that are representable with basic Go types. This
|
||||
// excludes Thema schemas.
|
||||
//
|
||||
// When a .cue #CoreStructured declaration is loaded through the standard [LoadCoreKind],
|
||||
// func, it is fully validated and populated according to all rules specified
|
||||
// in CUE for #CoreStructured kinds.
|
||||
type CoreStructuredProperties struct {
|
||||
CommonProperties
|
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
|
||||
}
|
||||
|
||||
func (m CoreStructuredMeta) _private() {}
|
||||
func (m CoreStructuredMeta) Common() CommonMeta {
|
||||
return m.CommonMeta
|
||||
func (m CoreStructuredProperties) _private() {}
|
||||
func (m CoreStructuredProperties) Common() CommonProperties {
|
||||
return m.CommonProperties
|
||||
}
|
||||
|
||||
// TODO
|
||||
type CustomStructuredMeta struct {
|
||||
CommonMeta
|
||||
// CustomStructuredProperties represents the static properties in the declaration of a
|
||||
// #CustomStructured kind that are representable with basic Go types. This
|
||||
// excludes Thema schemas.
|
||||
type CustomStructuredProperties struct {
|
||||
CommonProperties
|
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
|
||||
}
|
||||
|
||||
func (m CustomStructuredMeta) _private() {}
|
||||
func (m CustomStructuredMeta) Common() CommonMeta {
|
||||
return m.CommonMeta
|
||||
func (m CustomStructuredProperties) _private() {}
|
||||
func (m CustomStructuredProperties) Common() CommonProperties {
|
||||
return m.CommonProperties
|
||||
}
|
||||
|
||||
// TODO
|
||||
type ComposableMeta struct {
|
||||
CommonMeta
|
||||
// ComposableProperties represents the static properties in the declaration of a
|
||||
// #Composable kind that are representable with basic Go types. This
|
||||
// excludes Thema schemas.
|
||||
type ComposableProperties struct {
|
||||
CommonProperties
|
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
|
||||
}
|
||||
|
||||
func (m ComposableMeta) _private() {}
|
||||
func (m ComposableMeta) Common() CommonMeta {
|
||||
return m.CommonMeta
|
||||
func (m ComposableProperties) _private() {}
|
||||
func (m ComposableProperties) Common() CommonProperties {
|
||||
return m.CommonProperties
|
||||
}
|
||||
|
||||
// SomeKindMeta is an interface type to abstract over the different kind
|
||||
// metadata struct types: [RawMeta], [CoreStructuredMeta],
|
||||
// [CustomStructuredMeta].
|
||||
// SomeKindProperties is an interface type to abstract over the different kind
|
||||
// property struct types: [RawProperties], [CoreStructuredProperties],
|
||||
// [CustomStructuredProperties], [ComposableProperties].
|
||||
//
|
||||
// It is the traditional interface counterpart to the generic type constraint
|
||||
// KindMetas.
|
||||
type SomeKindMeta interface {
|
||||
// KindProperties.
|
||||
type SomeKindProperties interface {
|
||||
_private()
|
||||
Common() CommonMeta
|
||||
Common() CommonProperties
|
||||
}
|
||||
|
||||
// KindMetas is a type parameter that comprises the base possible set of
|
||||
// KindProperties is a type parameter that comprises the base possible set of
|
||||
// kind metadata configurations.
|
||||
type KindMetas interface {
|
||||
RawMeta | CoreStructuredMeta | CustomStructuredMeta | ComposableMeta
|
||||
type KindProperties interface {
|
||||
RawProperties | CoreStructuredProperties | CustomStructuredProperties | ComposableProperties
|
||||
}
|
||||
|
||||
+30
-30
@@ -83,52 +83,52 @@ func CUEFramework(ctx *cue.Context) cue.Value {
|
||||
|
||||
// ToKindMeta takes a cue.Value expected to represent a kind of the category
|
||||
// specified by the type parameter and populates the Go type from the cue.Value.
|
||||
func ToKindMeta[T KindMetas](v cue.Value) (T, error) {
|
||||
meta := new(T)
|
||||
func ToKindMeta[T KindProperties](v cue.Value) (T, error) {
|
||||
props := new(T)
|
||||
if !v.Exists() {
|
||||
return *meta, ErrValueNotExist
|
||||
return *props, ErrValueNotExist
|
||||
}
|
||||
|
||||
fw := CUEFramework(v.Context())
|
||||
var kdef cue.Value
|
||||
|
||||
anymeta := any(*meta).(SomeKindMeta)
|
||||
switch anymeta.(type) {
|
||||
case RawMeta:
|
||||
anyprops := any(*props).(SomeKindProperties)
|
||||
switch anyprops.(type) {
|
||||
case RawProperties:
|
||||
kdef = fw.LookupPath(cue.MakePath(cue.Def("Raw")))
|
||||
case CoreStructuredMeta:
|
||||
case CoreStructuredProperties:
|
||||
kdef = fw.LookupPath(cue.MakePath(cue.Def("CoreStructured")))
|
||||
case CustomStructuredMeta:
|
||||
case CustomStructuredProperties:
|
||||
kdef = fw.LookupPath(cue.MakePath(cue.Def("CustomStructured")))
|
||||
case ComposableMeta:
|
||||
case ComposableProperties:
|
||||
kdef = fw.LookupPath(cue.MakePath(cue.Def("Composable")))
|
||||
default:
|
||||
// unreachable so long as all the possibilities in KindMetas have switch branches
|
||||
// unreachable so long as all the possibilities in KindProperties have switch branches
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
item := v.Unify(kdef)
|
||||
if err := item.Validate(cue.Concrete(false), cue.All()); err != nil {
|
||||
return *meta, ewrap(item.Err(), ErrValueNotAKind)
|
||||
return *props, ewrap(item.Err(), ErrValueNotAKind)
|
||||
}
|
||||
if err := item.Decode(meta); err != nil {
|
||||
if err := item.Decode(props); err != nil {
|
||||
// Should only be reachable if CUE and Go framework types have diverged
|
||||
panic(errors.Details(err, nil))
|
||||
}
|
||||
|
||||
return *meta, nil
|
||||
return *props, nil
|
||||
}
|
||||
|
||||
// SomeDecl represents a single kind declaration, having been loaded
|
||||
// and validated by a func such as [LoadCoreKind].
|
||||
//
|
||||
// The underlying type of the Meta field indicates the category of
|
||||
// The underlying type of the Properties field indicates the category of
|
||||
// kind.
|
||||
type SomeDecl struct {
|
||||
// V is the cue.Value containing the entire Kind declaration.
|
||||
V cue.Value
|
||||
// Meta contains the kind's metadata settings.
|
||||
Meta SomeKindMeta
|
||||
// Properties contains the kind's declared properties.
|
||||
Properties SomeKindProperties
|
||||
}
|
||||
|
||||
// BindKindLineage binds the lineage for the kind declaration. nil, nil is returned
|
||||
@@ -140,10 +140,10 @@ func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOptio
|
||||
if rt == nil {
|
||||
rt = cuectx.GrafanaThemaRuntime()
|
||||
}
|
||||
switch decl.Meta.(type) {
|
||||
case RawMeta:
|
||||
switch decl.Properties.(type) {
|
||||
case RawProperties:
|
||||
return nil, nil
|
||||
case CoreStructuredMeta, CustomStructuredMeta, ComposableMeta:
|
||||
case CoreStructuredProperties, CustomStructuredProperties, ComposableProperties:
|
||||
return thema.BindLineage(decl.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...)
|
||||
default:
|
||||
panic("unreachable")
|
||||
@@ -152,25 +152,25 @@ func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOptio
|
||||
|
||||
// IsRaw indicates whether the represented kind is a raw kind.
|
||||
func (decl *SomeDecl) IsRaw() bool {
|
||||
_, is := decl.Meta.(RawMeta)
|
||||
_, is := decl.Properties.(RawProperties)
|
||||
return is
|
||||
}
|
||||
|
||||
// IsCoreStructured indicates whether the represented kind is a core structured kind.
|
||||
func (decl *SomeDecl) IsCoreStructured() bool {
|
||||
_, is := decl.Meta.(CoreStructuredMeta)
|
||||
_, is := decl.Properties.(CoreStructuredProperties)
|
||||
return is
|
||||
}
|
||||
|
||||
// IsCustomStructured indicates whether the represented kind is a custom structured kind.
|
||||
func (decl *SomeDecl) IsCustomStructured() bool {
|
||||
_, is := decl.Meta.(CustomStructuredMeta)
|
||||
_, is := decl.Properties.(CustomStructuredProperties)
|
||||
return is
|
||||
}
|
||||
|
||||
// IsComposable indicates whether the represented kind is a composable kind.
|
||||
func (decl *SomeDecl) IsComposable() bool {
|
||||
_, is := decl.Meta.(ComposableMeta)
|
||||
_, is := decl.Properties.(ComposableProperties)
|
||||
return is
|
||||
}
|
||||
|
||||
@@ -178,18 +178,18 @@ func (decl *SomeDecl) IsComposable() bool {
|
||||
// and validated by a func such as [LoadCoreKind].
|
||||
//
|
||||
// Its type parameter indicates the category of kind.
|
||||
type Decl[T KindMetas] struct {
|
||||
type Decl[T KindProperties] struct {
|
||||
// V is the cue.Value containing the entire Kind declaration.
|
||||
V cue.Value
|
||||
// Meta contains the kind's metadata settings.
|
||||
Meta T
|
||||
// Properties contains the kind's declared properties.
|
||||
Properties T
|
||||
}
|
||||
|
||||
// Some converts the typed Decl to the equivalent typeless SomeDecl.
|
||||
func (decl *Decl[T]) Some() *SomeDecl {
|
||||
return &SomeDecl{
|
||||
V: decl.V,
|
||||
Meta: any(decl.Meta).(SomeKindMeta),
|
||||
V: decl.V,
|
||||
Properties: any(decl.Properties).(SomeKindProperties),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func (decl *Decl[T]) Some() *SomeDecl {
|
||||
// This is a low-level function, primarily intended for use in code generation.
|
||||
// For representations of core kinds that are useful in Go programs at runtime,
|
||||
// see ["github.com/grafana/grafana/pkg/registry/corekind"].
|
||||
func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) {
|
||||
func LoadCoreKind[T RawProperties | CoreStructuredProperties](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) {
|
||||
vk, err := cuectx.BuildGrafanaInstance(ctx, declpath, "kind", overlay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -218,7 +218,7 @@ func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Cont
|
||||
decl := &Decl[T]{
|
||||
V: vk,
|
||||
}
|
||||
decl.Meta, err = ToKindMeta[T](vk)
|
||||
decl.Properties, err = ToKindMeta[T](vk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ func Logger(cfg *setting.Cfg) web.Middleware {
|
||||
|
||||
// we have to init the context with the counter here to update the request
|
||||
r = r.WithContext(log.InitCounter(r.Context()))
|
||||
// put the start time on context so we can measure it later.
|
||||
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
|
||||
|
||||
rw := web.Rw(w, r)
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user