Merge branch 'main' into ash/32369

This commit is contained in:
Ashley Harrison
2022-12-09 12:03:26 +00:00
388 changed files with 9038 additions and 5131 deletions
+6 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
...
+2
View File
@@ -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": {
+3
View File
@@ -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)
+6
View File
@@ -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]
+6
View File
@@ -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.
@@ -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. |
@@ -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:
+25 -15
View File
@@ -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 '&amp;'. |
| 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 '&amp;'. |
| **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. |
+6 -2
View File
@@ -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 (
+17 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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",
-1
View File
@@ -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;
}
+3 -1
View File
@@ -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
-2
View File
@@ -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",
@@ -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>
</>
)}
@@ -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[] = [];
+1 -2
View File
@@ -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",
@@ -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) {
@@ -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;
};
@@ -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;
@@ -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;
@@ -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;
@@ -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);
});
@@ -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;
@@ -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);
@@ -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.
*/
+1 -23
View File
@@ -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
View File
@@ -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)
+16 -7
View File
@@ -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
+21 -3
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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}
}
+3 -1
View File
@@ -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}},
+1 -1
View File
@@ -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
View File
@@ -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",
},
},
},
{
+14 -6
View File
@@ -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
+32
View File
@@ -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
}
+2 -1
View File
@@ -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"
)
+39 -1
View File
@@ -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",
},
},
}
-12
View File
@@ -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
}
+22
View File
@@ -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, &quotatest.FakeQuotaService{})
require.NoError(t, err)
usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, &quotatest.FakeQuotaService{})
require.NoError(t, err)
return usrSvc
}
+1 -1
View File
@@ -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(),
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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)
+6 -6
View File
@@ -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"
}
+2 -2
View File
@@ -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
+26 -17
View File
@@ -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
}
+17 -8
View File
@@ -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
}
+9 -9
View File
@@ -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
+8
View File
@@ -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
+28
View File
@@ -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
}
+5 -3
View File
@@ -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"),
}
+7 -5
View File
@@ -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
}
+6 -5
View File
@@ -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
+70 -15
View File
@@ -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)
}
+29 -2
View File
@@ -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)
}
+2 -1
View File
@@ -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
+15 -6
View File
@@ -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
}
+15 -6
View File
@@ -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
}
+14 -5
View File
@@ -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
}
+15 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+2
View File
@@ -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