Merge remote-tracking branch 'origin/main' into alerting/notification-policies-preview-in-alert-creation

This commit is contained in:
Sonia Aguilar 2023-06-12 11:59:56 +02:00
commit 8c539da81b
79 changed files with 1747 additions and 652 deletions

View File

@ -4144,9 +4144,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/influxdb/components/editor/query/influxql/InfluxCheatSheet.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/influxdb/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -4,6 +4,14 @@
### Features and enhancements
- **Alerting:** Migrate unknown NoData\Error settings to the default. [#69010](https://github.com/grafana/grafana/issues/69010), [@grafanabot](https://github.com/grafanabot)
- **Drawer:** Position under nav & minor redesign . [#68396](https://github.com/grafana/grafana/issues/68396), [@grafanabot](https://github.com/grafanabot)
- **Navigation:** Add keyboard shortcut to navigate directly to Dashboards. [#68374](https://github.com/grafana/grafana/issues/68374), [@grafanabot](https://github.com/grafanabot)
- **Explore:** Promote exploreMixedDatasource to Stable & enable by default. [#68353](https://github.com/grafana/grafana/issues/68353), [@Elfo404](https://github.com/Elfo404)
- **Tempo:** Escape regex-sensitive characters in span name before building promql query. [#68313](https://github.com/grafana/grafana/issues/68313), [@grafanabot](https://github.com/grafanabot)
- **Drawer:** Introduce a size property that set's width percentage and minWidth . [#68128](https://github.com/grafana/grafana/issues/68128), [@grafanabot](https://github.com/grafanabot)
- **AngularDeprecation:** Show warnings in panel edit for angular panels. [#68083](https://github.com/grafana/grafana/issues/68083), [@grafanabot](https://github.com/grafanabot)
- **Dashboard:** Change add panel button to fill to remove outline border. [#68017](https://github.com/grafana/grafana/issues/68017), [@grafanabot](https://github.com/grafanabot)
- **Query History:** Remove migration. [#67470](https://github.com/grafana/grafana/issues/67470), [@Elfo404](https://github.com/Elfo404)
- **Alerting:** Implement template testing endpoint. [#67450](https://github.com/grafana/grafana/issues/67450), [@JacobsonMT](https://github.com/JacobsonMT)
- **Trace View:** Export trace button . [#67368](https://github.com/grafana/grafana/issues/67368), [@adrapereira](https://github.com/adrapereira)
@ -100,6 +108,37 @@
### Bug fixes
- **ResourcePicker:** Fix missing border bug on cancel button. [#69113](https://github.com/grafana/grafana/issues/69113), [@nmarrs](https://github.com/nmarrs)
- **TimeSeries:** Fix centeredZero y axis ranging when all values are 0. [#69112](https://github.com/grafana/grafana/issues/69112), [@grafanabot](https://github.com/grafanabot)
- **StatusHistory:** Fix rendering of value-mapped null. [#69108](https://github.com/grafana/grafana/issues/69108), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Fix provenance guard checks for Alertmanager configuration to not cause panic when compared nested objects. [#69094](https://github.com/grafana/grafana/issues/69094), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Add support for Alert State History Loki primary. [#69077](https://github.com/grafana/grafana/issues/69077), [@grafanabot](https://github.com/grafanabot)
- **Dashboards:** Fix undefined aria labels in Annotation Checkboxes for Programmatic Access. [#68873](https://github.com/grafana/grafana/issues/68873), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Fix stale query preview error. [#68836](https://github.com/grafana/grafana/issues/68836), [@grafanabot](https://github.com/grafanabot)
- **AnonymousAuth:** Fix concurrent read-write crash. [#68803](https://github.com/grafana/grafana/issues/68803), [@grafanabot](https://github.com/grafanabot)
- **AzureMonitor:** Ensure legacy properties containing template variables are correctly migrated. [#68792](https://github.com/grafana/grafana/issues/68792), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Add additional contact points for external AM. [#68778](https://github.com/grafana/grafana/issues/68778), [@grafanabot](https://github.com/grafanabot)
- **RBAC:** Remove legacy AC editor and admin role on new dashboard route. [#68777](https://github.com/grafana/grafana/issues/68777), [@grafanabot](https://github.com/grafanabot)
- **Azure Monitor:** Fix bug with top value so more than 10 resources can be shown . [#68725](https://github.com/grafana/grafana/issues/68725), [@grafanabot](https://github.com/grafanabot)
- **NodeGraph:** Fix overlaps preventing opening an edge context menu when nodes were too close. [#68628](https://github.com/grafana/grafana/issues/68628), [@grafanabot](https://github.com/grafanabot)
- **Plugins:** Correct the usage of mutex for gRPC plugin implementation. [#68609](https://github.com/grafana/grafana/issues/68609), [@grafanabot](https://github.com/grafanabot)
- **Azure Monitor:** Fix bug that did not show alert rule preview. [#68581](https://github.com/grafana/grafana/issues/68581), [@grafanabot](https://github.com/grafanabot)
- **FlameGraph:** Fix table sort being reset when search changes. [#68454](https://github.com/grafana/grafana/issues/68454), [@grafanabot](https://github.com/grafanabot)
- **Command Palette:** Prevent stale search results from overwriting newer results. [#68392](https://github.com/grafana/grafana/issues/68392), [@grafanabot](https://github.com/grafanabot)
- **Search:** Fix Search returning results out of order. [#68387](https://github.com/grafana/grafana/issues/68387), [@joshhunt](https://github.com/joshhunt)
- **Explore:** Remove data source onboarding page. [#68381](https://github.com/grafana/grafana/issues/68381), [@grafanabot](https://github.com/grafanabot)
- **Flamegraph:** Fix tooltip positioning. [#68312](https://github.com/grafana/grafana/issues/68312), [@grafanabot](https://github.com/grafanabot)
- **Pyroscope:** Add authentication when calling backendType resource API. [#68311](https://github.com/grafana/grafana/issues/68311), [@grafanabot](https://github.com/grafanabot)
- **Histogram:** Respect min/max panel settings for x-axis. [#68245](https://github.com/grafana/grafana/issues/68245), [@grafanabot](https://github.com/grafanabot)
- **QueryRow:** Make toggle actions screen-readers accessible. [#68210](https://github.com/grafana/grafana/issues/68210), [@grafanabot](https://github.com/grafanabot)
- **Heatmap:** Fix color rendering for value ranges < 1. [#68164](https://github.com/grafana/grafana/issues/68164), [@grafanabot](https://github.com/grafanabot)
- **Heatmap:** Handle unsorted timestamps in calculate mode. [#68151](https://github.com/grafana/grafana/issues/68151), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Fixes Alert list panel "ungrouped" regression. [#68090](https://github.com/grafana/grafana/issues/68090), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Show export button for org admins. [#67995](https://github.com/grafana/grafana/issues/67995), [@grafanabot](https://github.com/grafanabot)
- **Navigation:** Fix 'Page not found' when sending or going back from 'Invitate user' page. [#67972](https://github.com/grafana/grafana/issues/67972), [@grafanabot](https://github.com/grafanabot)
- **InspectDrawer:** Fixes issue with double scrollbars. [#67888](https://github.com/grafana/grafana/issues/67888), [@grafanabot](https://github.com/grafanabot)
- **Connections:** Show core datasource plugins as well. [#67886](https://github.com/grafana/grafana/issues/67886), [@grafanabot](https://github.com/grafanabot)
- **Gauge:** Set min and max for percent unit. [#67719](https://github.com/grafana/grafana/issues/67719), [@grafanabot](https://github.com/grafanabot)
- **TimeSeries:** Fix leading null-fill for missing intervals. [#67570](https://github.com/grafana/grafana/issues/67570), [@leeoniya](https://github.com/leeoniya)
- **Pyroscope:** Fix autodetection in case of using Phlare backend. [#67536](https://github.com/grafana/grafana/issues/67536), [@aocenas](https://github.com/aocenas)
- **Dashboard:** Revert fixed header shown on mobile devices in the new panel header. [#67510](https://github.com/grafana/grafana/issues/67510), [@axelavargas](https://github.com/axelavargas)
@ -181,13 +220,15 @@ The deprecated `plugin:test` and `plugin:dev` commands in the Grafana Toolkit ha
The type signature of the `testDatasource()` method on the `DataSourceWithBackend` class [has changed](https://github.com/grafana/grafana/pull/67014/files/a5608dc4f27ab4459e725b22ff60b8fc05390c08#diff-c58fc1a09e9b9b17e5f45efbfb646273e69145f7687facb134440da4edafc745R263), the returned Promise is now typed stricter, which is probably going to cause type-errors while building plugins against the latest Grafana versions.
````typescript
```typescript
// Before
abstract testDatasource(): Promise<any>;
// After
abstract testDatasource(): Promise<TestDataSourceResponse>;
``` Issue [#67014](https://github.com/grafana/grafana/issues/67014)
```
Issue [#67014](https://github.com/grafana/grafana/issues/67014)
Grafana requires an Elasticsearch version of 7.16 or newer. If you use an older Elasticsearch version, you will get warnings in the query editor and on the datasource configuration page. Issue [#66928](https://github.com/grafana/grafana/issues/66928)
@ -200,19 +241,19 @@ We've removed some now unused properties from the `NavModel` interface. Issue [#
We removed previously deprecated components from `@grafana/data` : `getLogLevel`, `getLogLevelFromKey`, `addLogLevelToSeries`, `LogsParser`, `LogsParsers`, `calculateFieldStats`, `calculateLogsLabelStats`, `calculateStats`, `getParser`, `sortInAscendingOrder`, `sortInDescendingOrder`, `sortLogsResult`, `sortLogRows`, `checkLogsError`, `escapeUnescapedString`. Issue [#66271](https://github.com/grafana/grafana/issues/66271)
We removed previously deprecated components from `@grafana/ui` : `LogLabels`, `LogMessageAnsi`, `LogRows`, `getLogRowStyles`.
Issue [#66268](https://github.com/grafana/grafana/issues/66268)
Issue [#66268](https://github.com/grafana/grafana/issues/66268)
We removed previously deprecated `DataSourceWithLogsVolumeSupport` that was replaced with `DataSourceWithSupplementaryQueriesSupport`. Both APIs are for internal use only. Issue [#66266](https://github.com/grafana/grafana/issues/66266)
We removed previously deprecated `DataSourceWithLogsVolumeSupport` that was replaced with `DataSourceWithSupplementaryQueriesSupport`. Both APIs are for internal use only. Issue [#66266](https://github.com/grafana/grafana/issues/66266)
Additional functions (map/filter/forEach/iterator) have been added to the root Vector interface. Any code using vectors will continue to work unchanged, but in the rare case that you have implemented Vector directly, it be missing these functions. The easiest fix is to extend [FunctionalVector](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/vector/FunctionalVector.ts).
Additional functions (map/filter/forEach/iterator) have been added to the root Vector interface. Any code using vectors will continue to work unchanged, but in the rare case that you have implemented Vector directly, it be missing these functions. The easiest fix is to extend [FunctionalVector](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/vector/FunctionalVector.ts).
The `ArrayVector` class now extends the native JavaScript `Array` and gains all of its prototype/instance methods as a result. Issue [#66187](https://github.com/grafana/grafana/issues/66187)
We've removed the ability for functions to be passed as children to the `Dropdown` component. Previously, this was used to access the `isOpen` state of the dropdown. This can be now be achieved with the `onVisibleChange` prop.
Before:
````
```
return (
<Dropdown overlay={MenuActions} placement="bottom-end">
{(isOpen) =>
@ -220,12 +261,11 @@ return (
}
</Dropdown>
);
```
After:
```
````
const [isOpen, setIsOpen] = useState(false);
...
@ -235,8 +275,7 @@ return (
<ToolbarButton iconOnly icon="plus" isOpen={isOpen} aria-label="New" />
</Dropdown>
);
````Issue [#65467](https://github.com/grafana/grafana/issues/65467)
``` Issue [#65467](https://github.com/grafana/grafana/issues/65467)
(relevant for plugin developers) The deprecated internal `dashboardId` is now removed from the request context. For usage tracking use the `dashboardUid`
@ -3317,8 +3356,8 @@ The change in behavior is that negative-valued series are now stacked downwards
The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
The Tooltip component provided by `@grafana/ui` is no longer automatically interactive (that is you can hover onto it and click a link or select text). It will from now on by default close automatically when you mouse out from the trigger element. To make tooltips behave like before set the new `interactive` property to true.
Issue [#45053](https://github.com/grafana/grafana/issues/45053)
The Tooltip component provided by `@grafana/ui` is no longer automatically interactive (that is you can hover onto it and click a link or select text). It will from now on by default close automatically when you mouse out from the trigger element. To make tooltips behave like before set the new `interactive` property to true.
Issue [#45053](https://github.com/grafana/grafana/issues/45053)
### Deprecations

View File

@ -0,0 +1,225 @@
# Testing Guidelines
The goal of this document is to address the most frequently asked "How to" questions related to unit testing.
## Best practices
- Default to the `*ByRole` queries when testing components as it encourages testing with accessibility concerns in mind. It's also possible to use `*ByLabelText` queries. However, the `*ByRole` queries are [more robust](https://testing-library.com/docs/queries/bylabeltext/#name) and are generally recommended over the former.
## Testing User Interactions
We use the [user-event](https://testing-library.com/docs/user-event/intro) library for simulating user interactions during testing. This library is preferred over the built-in `fireEvent` method, as it more accurately mirrors real user interactions with elements.
There are two important considerations when working with `userEvent`:
1. All methods in `userEvent` are asynchronous, and thus require the use of `await` when called.
2. Directly calling methods from `userEvent` may not be supported in future versions. As such, it's necessary to first call `userEvent.setup()` prior to the tests. This method returns a `userEvent` instance, complete with all its methods. This setup process can be simplified using a utility function:
```tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function setup(jsx: JSX.Element) {
return {
user: userEvent.setup(),
...render(jsx),
};
}
it('should render', async () => {
const { user } = setup(<Button />);
await user.click(screen.getByRole('button'));
});
```
## Debugging Tests
There are a few utilities that can be useful for debugging tests:
- [screen.debug()](https://testing-library.com/docs/queries/about/#screendebug) - This function prints a human-readable representation of the document's DOM tree when called without arguments, or the DOM tree of specific node(s) when provided with arguments. It is internally using `console.log` to log the output to terminal.
- [Testing Playground](https://testing-playground.com/) - An interactive sandbox that allows testing which queries work with specific HTML elements.
- [logRoles](https://testing-library.com/docs/dom-testing-library/api-debugging/#prettydom) - A utility function that prints out all the implicit ARIA roles for a given DOM tree.
## Testing Select Components
Here, the [OrgRolePicker](https://github.com/grafana/grafana/blob/38863844e7ac72c7756038a1097f89632f9985ff/public/app/features/admin/OrgRolePicker.tsx) component is used as an example. This component essentially serves as a wrapper for the `Select` component, complete with its own set of options.
```tsx
import { OrgRole } from '@grafana/data';
import { Select } from '@grafana/ui';
interface Props {
value: OrgRole;
disabled?: boolean;
'aria-label'?: string;
inputId?: string;
onChange: (role: OrgRole) => void;
autoFocus?: boolean;
width?: number | 'auto';
}
const options = Object.keys(OrgRole).map((key) => ({ label: key, value: key }));
export function OrgRolePicker({ value, onChange, 'aria-label': ariaLabel, inputId, autoFocus, ...restProps }: Props) {
return (
<Select
inputId={inputId}
value={value}
options={options}
onChange={(val) => onChange(val.value as OrgRole)}
placeholder="Choose role..."
aria-label={ariaLabel}
autoFocus={autoFocus}
{...restProps}
/>
);
}
```
### Querying the Select Component
The recommended way to query `Select` components is by using a label. Add a `label` element and provide the `htmlFor` prop with a matching `inputId`. Alternatively, `aria-label` can be specified on the `Select`.
```tsx
describe('OrgRolePicker', () => {
it('should render the picker', () => {
setup(
<>
<label htmlFor={'role-picker'}>Role picker</label>
<OrgRolePicker value={OrgRole.Admin} inputId={'role-picker'} onChange={() => {}} />
</>
);
expect(screen.getByRole('combobox', { name: 'Role picker' })).toBeInTheDocument();
});
});
```
### Testing the Display of Correct Options
At times, it might be necessary to verify that the `Select` component is displaying the correct options. In such instances, the best solution is to click the `Select` component and match the desired option using the `*ByText` query.
```tsx
it('should have an "Editor" option', async () => {
const { user } = setup(
<>
<label htmlFor={'role-picker'}>Role picker</label>
<OrgRolePicker value={OrgRole.Admin} inputId={'role-picker'} onChange={() => {}} />
</>
);
await user.click(screen.getByRole('combobox', { name: 'Role picker' }));
expect(screen.getByText('Editor')).toBeInTheDocument();
});
```
### Selecting an option
To simplify the process of selecting an option from a `Select` component, there is a `selectOptionInTest` utility function. This function is a wrapper over the [react-select-event](https://testing-library.com/docs/ecosystem-react-select-event/) package.
```tsx
it('should select an option', async () => {
const mockOnChange = jest.fn();
setup(
<>
<label htmlFor={'role-picker'}>Role picker</label>
<OrgRolePicker value={OrgRole.Admin} inputId={'role-picker'} onChange={mockOnChange} />
</>
);
await selectOptionInTest(screen.getByRole('combobox', { name: 'Role picker' }), 'Viewer');
expect(mockOnChange).toHaveBeenCalledWith('Viewer');
});
```
## Mocking Objects and Functions
### Mocking the `window` Object and Its Methods
The recommended approach for mocking the `window` object is to use Jest spies. Jest's spy functions provide a built-in mechanism for restoring mocks. This feature eliminates the need to manually save a reference to the `window` object.
```tsx
let windowSpy: jest.SpyInstance;
beforeAll(() => {
windowSpy = jest.spyOn(window, 'location', 'get');
});
afterAll(() => {
windowSpy.mockRestore();
});
it('should test with window', function () {
windowSpy.mockImplementation(() => ({
href: 'www.example.com',
}));
expect(window.location.href).toBe('www.example.com');
});
```
### Mocking getBackendSrv()
The `getBackendSrv()` function is used to make HTTP requests to the Grafana backend. It is possible to mock this function using the `jest.mock` method.
```tsx
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
post: postMock,
}),
}));
```
#### Mocking getBackendSrv for AsyncSelect
The `AsyncSelect` component is used to asynchronously load options. As such, it often relies on the `getBackendSrv` for loading the options.
Here's how the test would look like for this [OrgPicker](https://github.com/grafana/grafana/blob/38863844e7ac72c7756038a1097f89632f9985ff/public/app/core/components/Select/OrgPicker.tsx) component, which uses `AsyncSelect` under the hood.
```tsx
import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { OrgPicker } from './OrgPicker';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
get: () =>
Promise.resolve([
{ name: 'Org 1', id: 0 },
{ name: 'Org 2', id: 1 },
]),
}),
}));
function setup(jsx: JSX.Element) {
return {
user: userEvent.setup(),
...render(jsx),
};
}
describe('OrgPicker', () => {
it('should render', async () => {
render(
<>
<label htmlFor={'picker'}>Org picker</label>
<OrgPicker onSelected={() => {}} inputId={'picker'} />
</>
);
expect(await screen.findByRole('combobox', { name: 'Org picker' })).toBeInTheDocument();
});
it('should have the options', async () => {
const { user } = setup(
<>
<label htmlFor={'picker'}>Org picker</label>
<OrgPicker onSelected={() => {}} inputId={'picker'} />
</>
);
await user.click(await screen.findByRole('combobox', { name: 'Org picker' }));
expect(screen.getByText('Org 1')).toBeInTheDocument();
expect(screen.getByText('Org 2')).toBeInTheDocument();
});
});
```

View File

@ -63,6 +63,7 @@ datasources:
prometheusVersion: 2.40.0
- name: gdev-slow-prometheus
uid: gdev-slow-prometheus-uid
type: prometheus
access: proxy
url: http://localhost:3011

View File

@ -55,6 +55,38 @@
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
},
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 2,
"options": {
"mode": "markdown",
"content": "VariableUnderTestText: ${VariableUnderTest:text}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
},
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 18
},
"id": 2,
"options": {
"mode": "markdown",
"content": "VariableUnderTestRaw: ${VariableUnderTest:raw}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
}
],
"schemaVersion": 35,

View File

@ -18,6 +18,14 @@ weight: 400
You can create and manage recording rules for an external Grafana Mimir or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh.
**Note:**
Recording rules are run as instant rules, which means that they run every 10s. To overwrite this configuration, update the min_interval in your custom configuration file.
[min_interval]({{< relref "../../setup-grafana/configure-grafana" >}}) sets the minimum interval to enforce between rule evaluations. The default value is 10s which equals the scheduler interval. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as fewer evaluations are scheduled over time.
This setting has precedence over each individual rule frequency. If a rule frequency is lower than this value, then this value is enforced.
## Before you begin
- Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alerting rules.
@ -44,25 +52,7 @@ To create a Grafana Mimir or Loki managed recording rule
1. In Step 2, select **Mimir or Loki recording rule** option.
- Select your Loki or Prometheus data source.
- Enter a PromQL or LogQL query.
1. In Step 3, add the namespace and the group.
- From the **Namespace** dropdown, select an existing rule namespace. Otherwise, click Add new and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces]({{< relref "./edit-mimir-loki-namespace-group" >}}).
- From the **Group** dropdown, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one.
1. In Step 4, add the custom labels.
- Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value.
1. Click **Save** to save the recording rule or **Save and exit** to save the recording rule and go back to the Alerting page.
<!-- delete next steps? -->
1. In the left-side menu, click **Alerts & IRM** and then **Alerting**.
1. Click **Alert rules**.
1. Click **+ Create alert rule**.
1. In Step 1, add the rule name.
- In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule.
1. In Step 2, add the type, and storage location.
- From the **Rule type** dropdown, select **Mimir / Loki managed alert**.
- From the **Select data source** dropdown, select an external Prometheus, an external Loki, or a Grafana Cloud data source.
- Enter a PromQL or LogQL expression. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series.
1. In Step 3, add evaluation behavior.
1. In Step 3, add alert evaluation behavior.
- Enter a valid **For** duration. The expression has to be true for this long for the alert to be fired.
1. In Step 4, add additional metadata associated with the rule.
- From the **Namespace** dropdown, select an existing rule namespace. Otherwise, click Add new and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces]({{< relref "./edit-mimir-loki-namespace-group" >}}).

View File

@ -61,3 +61,7 @@ To remove a silence, complete the following steps.
1. Select the silence you want to end, then click **Unsilence**.
> **Note:** You cannot remove a silence manually. Silences that have ended are retained and listed for five days.
## Useful links
[Aggregation operators](/docs/prometheus/latest/querying/operators/#aggregation-operators)

View File

@ -179,7 +179,13 @@ Template the title of a Slack message to contain the number of firing and resolv
## Template the content of a Slack message
Template the content of a Slack message to contain a description of all firing and resolved alerts, including their labels, annotations, Silence URL and Dashboard URL:
Template the content of a Slack message to contain a description of all firing and resolved alerts, including their labels, annotations, Silence URL and Dashboard URL.
**Note:**
This template is for Grafana-managed alerts only.
To use the template for Grafana Mimir/Loki-managed alerts, delete the references to DashboardURL and SilenceURL.
For more information, see the [Prometheus documentation on notifications](https://prometheus.io/docs/alerting/latest/notifications/).
```
1 firing alert(s):

View File

@ -41,10 +41,30 @@ Guidance on migrating a plugin to React can be found in our [migration guide]({{
## Apps
### [BelugaCDN](https://grafana.com/grafana/plugins/belugacdn-app)
Latest Version: 1.2.1 | Signature: Commercial | Last Updated: 2023
> [Migration issue](https://github.com/belugacdn/grafana-belugacdn-app/issues/7) has been raised.
> **Warning:** Lack of recent activity in the [project repository](https://github.com/belugacdn/grafana-belugacdn-app) in the past 7 years suggests project _may_ not be actively maintained.
### [Bosun](https://grafana.com/grafana/plugins/bosun-app)
Latest Version: 0.0.29 | Signature: Community | Last Updated: 2023
> [Migration issue](https://github.com/bosun-monitor/bosun-grafana-app/issues/63) has been raised.
### [Cloudflare Grafana App](https://grafana.com/grafana/plugins/cloudflare-app/)
Latest Version: 0.2.4 | Signature: Commercial | Last Updated: 2022
### [GLPI](https://grafana.com/grafana/plugins/ddurieux-glpi-app)
Latest Version: 1.3.1 | Signature: Community | Last Updated: 2021
> [Migration issue](https://github.com/ddurieux/glpi_app_grafana/issues/96) has been raised.
### [DevOpsProdigy KubeGraf](https://grafana.com/grafana/plugins/devopsprodigy-kubegraf-app/)
Latest Version: 1.5.2 | Signature: Community | Last Updated: 2021
@ -53,10 +73,46 @@ Latest Version: 1.5.2 | Signature: Community | Last Updated: 2021
> **Migration available - potential alternative:** Grafana Cloud includes a [Kubernetes integration](https://grafana.com/solutions/kubernetes/).
### [AWS IoT TwinMaker App](https://grafana.com/grafana/plugins/grafana-iot-twinmaker-app)
Latest Version: 1.6.2 | Signature: Commercial | Last Updated: 2023
> **Note:** Plugin should continue to work even if Angular is disabled, and a full removal of Angular related code is planned.
### [Kentik Connect Pro](https://grafana.com/grafana/plugins/kentik-connect-app/)
Latest Version: 1.6.2 | Signature: Commercial | Last Updated: 2023
### [Moogsoft AIOps](https://grafana.com/grafana/plugins/moogsoft-aiops-app)
Latest Version: 8.0.2 | Signature: Commercial | Last Updated: 2022
### [OpenNMS Helm](https://grafana.com/grafana/plugins/opennms-helm-app)
Latest Version: 8.0.4 | Signature: Community | Last Updated: 2023
> **Migration available - plugin superseded:** The plugin has effectively been replaced with a [new plugin](https://grafana.com/grafana/plugins/opennms-opennms-app/) based on React.
### [Percona](https://grafana.com/grafana/plugins/percona-percona-app/)
Latest Version: 1.0.1 | Signature: Community | Last Updated: 2021
> **Warning:** [Project repository](https://github.com/percona/grafana-app) was archived on June 12, 2020.
### [Stagemonitor Elasticsearch](https://grafana.com/grafana/plugins/stagemonitor-elasticsearch-app)
Latest Version: 0.83.3 | Signature: Community | Last Updated: 2021
> [Migration issue](https://github.com/stagemonitor/stagemonitor-grafana-elasticsearch/issues/1) has been raised.
> **Warning:** Lack of recent activity in the [project repository](https://github.com/stagemonitor/stagemonitor-grafana-elasticsearch) in the past 4 years suggests project _may_ not be actively maintained.
### [Voxter VoIP Platform Metrics](https://grafana.com/grafana/plugins/voxter-app)
Latest Version: 0.0.2 | Signature: Community | Last Updated: 2021
> **Warning:** Lack of recent activity in the [project repository](https://github.com/raintank/voxter-app) in the past 3 years suggests project _may_ not be actively maintained.
## Datasources
### [Druid](https://grafana.com/grafana/plugins/abhisant-druid-datasource/)
@ -103,10 +159,6 @@ Latest Version: 2.2.3 | Signature: Community | Last Updated: 2022
> **Warning:** Lack of recent activity in the [project repository](https://github.com/chaos-mesh/datasource) in the past year suggests project _may_ not be actively maintained.
### [Cognite Data Fusion](https://grafana.com/grafana/plugins/cognitedata-datasource/)
Latest Version: 3.0.0 | Signature: Commercial | Last Updated: 2023
### [DeviceHive](https://grafana.com/grafana/plugins/devicehive-devicehive-datasource/)
Latest Version: 2.0.2 | Signature: Community | Last Updated: 2021
@ -185,12 +237,6 @@ Latest Version: 1.4.2 | Signature: Grafana | Last Updated: 2021
> **Note:** If you're looking for an example of a data source plugin to start from, refer to [grafana-starter-datasource-backend](https://github.com/grafana/grafana-starter-datasource-backend).
### [Splunk](https://grafana.com/grafana/plugins/grafana-splunk-datasource/)
Latest Version: 4.1.6 | Signature: Grafana | Last Updated: 2023
> **Note:** Removal of any angular dependency is on the near term roadmap.
### [Strava](https://grafana.com/grafana/plugins/grafana-strava-datasource/)
Latest Version: 1.5.1 | Signature: Grafana | Last Updated: 2022
@ -203,12 +249,6 @@ Latest Version: 1.0.3 | Signature: Community | Last Updated: 2021
> **Warning:** Lack of recent activity in the [project repository](https://github.com/GridProtectionAlliance/openHistorian-grafana/) in the past 2 years suggests project _may_ not be actively maintained.
### [OSIsoft-PI](https://grafana.com/grafana/plugins/gridprotectionalliance-osisoftpi-datasource/)
Latest Version: 3.1.0 | Signature: Community | Last Updated: 2023
> **Note:** Fixed in 4.0.0 which should be published soon - [source](https://github.com/GridProtectionAlliance/osisoftpi-grafana/issues/119#issuecomment-1493566212).
### [Hawkular](https://grafana.com/grafana/plugins/hawkular-datasource/)
Latest Version: 1.1.2 | Signature: Community | Last Updated: 2021
@ -283,7 +323,7 @@ Latest Version: 3.0.0 | Signature: Commercial | Last Updated: 2023
### [Oracle Cloud Infrastructure Metrics](https://grafana.com/grafana/plugins/oci-metrics-datasource/)
Latest Version: 4.0.0 | Signature: Commercial | Last Updated: 2023
Latest Version: 4.0.1 | Signature: Commercial | Last Updated: 2023
### [Warp 10](https://grafana.com/grafana/plugins/ovh-warp10-datasource/)

View File

@ -52,6 +52,10 @@ title: 'Alerting Provisioning HTTP API '
### Contact points
**Note:**
Contact point provisioning is for Grafana-managed alerts only.
| Method | URI | Name | Summary |
| ------ | ----------------------------------------- | --------------------------------------------------------- | --------------------------------- |
| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. |

View File

@ -38,6 +38,7 @@ describe('Variables - Datasource', () => {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('gdev-slow-prometheus').click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus');
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid');
e2e().get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus');
});
});

View File

@ -88,7 +88,7 @@
"@babel/plugin-transform-typescript": "7.22.3",
"@babel/preset-env": "7.22.4",
"@babel/preset-react": "7.22.3",
"@babel/preset-typescript": "7.21.5",
"@babel/preset-typescript": "7.22.5",
"@babel/runtime": "7.22.3",
"@betterer/betterer": "5.4.0",
"@betterer/cli": "5.4.0",
@ -255,8 +255,8 @@
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "1.4.2",
"@grafana/faro-core": "1.0.2",
"@grafana/faro-web-sdk": "1.0.2",
"@grafana/faro-core": "1.1.0",
"@grafana/faro-web-sdk": "1.1.0",
"@grafana/google-sdk": "0.1.1",
"@grafana/lezer-logql": "0.1.5",
"@grafana/monaco-logql": "^0.0.7",

View File

@ -39,7 +39,7 @@
"dependencies": {
"@grafana/data": "10.1.0-pre",
"@grafana/e2e-selectors": "10.1.0-pre",
"@grafana/faro-web-sdk": "1.0.2",
"@grafana/faro-web-sdk": "1.1.0",
"@grafana/ui": "10.1.0-pre",
"history": "4.10.1",
"lodash": "4.17.21",

View File

@ -1,16 +1,14 @@
import { faro, LogLevel } from '@grafana/faro-web-sdk';
import { faro, LogLevel, LogContext } from '@grafana/faro-web-sdk';
import { config } from '../config';
export { LogLevel };
type Contexts = Record<string, Record<string, number | string | Record<string, string | number>>>;
/**
* Log a message at INFO level
* @public
*/
export function logInfo(message: string, contexts?: Contexts) {
export function logInfo(message: string, contexts?: LogContext) {
if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], {
level: LogLevel.INFO,
@ -24,7 +22,7 @@ export function logInfo(message: string, contexts?: Contexts) {
*
* @public
*/
export function logWarning(message: string, contexts?: Contexts) {
export function logWarning(message: string, contexts?: LogContext) {
if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], {
level: LogLevel.WARN,
@ -38,7 +36,7 @@ export function logWarning(message: string, contexts?: Contexts) {
*
* @public
*/
export function logDebug(message: string, contexts?: Contexts) {
export function logDebug(message: string, contexts?: LogContext) {
if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], {
level: LogLevel.DEBUG,
@ -52,7 +50,7 @@ export function logDebug(message: string, contexts?: Contexts) {
*
* @public
*/
export function logError(err: Error, contexts?: Contexts) {
export function logError(err: Error, contexts?: LogContext) {
if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushError(err);
}

View File

@ -51,7 +51,7 @@
"@emotion/react": "11.10.6",
"@grafana/data": "10.1.0-pre",
"@grafana/e2e-selectors": "10.1.0-pre",
"@grafana/faro-web-sdk": "1.0.2",
"@grafana/faro-web-sdk": "1.1.0",
"@grafana/schema": "10.1.0-pre",
"@leeoniya/ufuzzy": "1.0.6",
"@monaco-editor/react": "4.5.1",

View File

@ -75,6 +75,7 @@ export function RadioButtonGroup<T>({
{options.map((opt, i) => {
const isItemDisabled = disabledOptions && opt.value && disabledOptions.includes(opt.value);
const icon = opt.icon ? toIconName(opt.icon) : undefined;
const hasNonIconPart = Boolean(opt.imgUrl || opt.label || opt.component);
return (
<RadioButton
@ -91,7 +92,7 @@ export function RadioButtonGroup<T>({
fullWidth={fullWidth}
ref={value === opt.value ? activeButtonRef : undefined}
>
{icon && <Icon name={icon} className={styles.icon} />}
{icon && <Icon name={icon} className={cx(hasNonIconPart && styles.icon)} />}
{opt.imgUrl && <img src={opt.imgUrl} alt={opt.label} className={styles.img} />}
{opt.label} {opt.component ? <opt.component /> : null}
</RadioButton>

View File

@ -10,11 +10,12 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
)
func TestProcessManager_Start(t *testing.T) {
t.Run("Plugin not found in registry", func(t *testing.T) {
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{}))
m := NewManager(fakes.NewFakePluginRegistry())
err := m.Start(context.Background(), "non-existing-datasource")
require.ErrorIs(t, err, backendplugin.ErrPluginNotRegistered)
})
@ -63,9 +64,11 @@ func TestProcessManager_Start(t *testing.T) {
plugin.SignatureError = tc.signatureError
})
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{
p.ID: p,
}))
m := NewManager(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p.ID: p,
}},
)
err := m.Start(context.Background(), p.ID)
require.NoError(t, err)
@ -83,7 +86,7 @@ func TestProcessManager_Start(t *testing.T) {
func TestProcessManager_Stop(t *testing.T) {
t.Run("Plugin not found in registry", func(t *testing.T) {
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{}))
m := NewManager(fakes.NewFakePluginRegistry())
err := m.Stop(context.Background(), "non-existing-datasource")
require.ErrorIs(t, err, backendplugin.ErrPluginNotRegistered)
})
@ -97,9 +100,11 @@ func TestProcessManager_Stop(t *testing.T) {
plugin.Backend = true
})
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{
pluginID: p,
}))
m := NewManager(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
pluginID: p,
}},
)
err := m.Stop(context.Background(), pluginID)
require.NoError(t, err)
@ -116,9 +121,11 @@ func TestProcessManager_ManagedBackendPluginLifecycle(t *testing.T) {
plugin.Backend = true
})
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{
p.ID: p,
}))
m := NewManager(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p.ID: p,
}},
)
err := m.Start(context.Background(), p.ID)
require.NoError(t, err)
@ -162,40 +169,6 @@ func TestProcessManager_ManagedBackendPluginLifecycle(t *testing.T) {
})
}
type fakePluginRegistry struct {
store map[string]*plugins.Plugin
}
func newFakePluginRegistry(m map[string]*plugins.Plugin) *fakePluginRegistry {
return &fakePluginRegistry{
store: m,
}
}
func (f *fakePluginRegistry) Plugin(_ context.Context, id string) (*plugins.Plugin, bool) {
p, exists := f.store[id]
return p, exists
}
func (f *fakePluginRegistry) Plugins(_ context.Context) []*plugins.Plugin {
var res []*plugins.Plugin
for _, p := range f.store {
res = append(res, p)
}
return res
}
func (f *fakePluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
f.store[p.ID] = p
return nil
}
func (f *fakePluginRegistry) Remove(_ context.Context, id string) error {
delete(f.store, id)
return nil
}
type fakeBackendPlugin struct {
managed bool

View File

@ -54,10 +54,12 @@ func TestStore_Plugin(t *testing.T) {
p1.RegisterClient(&DecommissionedPlugin{})
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel"}}
ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
}))
ps := New(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
},
})
p, exists := ps.Plugin(context.Background(), p1.ID)
require.False(t, exists)
@ -78,13 +80,15 @@ func TestStore_Plugins(t *testing.T) {
p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-panel", Type: plugins.TypePanel}}
p5.RegisterClient(&DecommissionedPlugin{})
ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
p5.ID: p5,
}))
ps := New(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
p5.ID: p5,
},
})
pss := ps.Plugins(context.Background())
require.Equal(t, pss, []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO(), p3.ToDTO(), p4.ToDTO()})
@ -113,14 +117,16 @@ func TestStore_Routes(t *testing.T) {
p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.TypeApp}}
p6.RegisterClient(&DecommissionedPlugin{})
ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
p5.ID: p5,
p6.ID: p6,
}))
ps := New(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
p5.ID: p5,
p6.ID: p6,
},
})
sr := func(p *plugins.Plugin) *plugins.StaticRoute {
return &plugins.StaticRoute{PluginID: p.ID, Directory: p.FS.Base()}
@ -137,11 +143,13 @@ func TestStore_Renderer(t *testing.T) {
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.TypePanel}}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app", Type: plugins.TypeApp}}
ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
}))
ps := New(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
},
})
r := ps.Renderer(context.Background())
require.Equal(t, p1, r)
@ -155,12 +163,14 @@ func TestStore_SecretsManager(t *testing.T) {
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-secrets", Type: plugins.TypeSecretsManager}}
p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}}
ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
}))
ps := New(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
},
})
r := ps.SecretsManager(context.Background())
require.Equal(t, p3, r)
@ -173,12 +183,12 @@ func TestStore_availablePlugins(t *testing.T) {
p1.RegisterClient(&DecommissionedPlugin{})
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app"}}
ps := New(
newFakePluginRegistry(map[string]*plugins.Plugin{
ps := New(&fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
}),
)
},
})
aps := ps.availablePlugins(context.Background())
require.Len(t, aps, 1)
@ -197,36 +207,3 @@ func (p *DecommissionedPlugin) Decommission() error {
func (p *DecommissionedPlugin) IsDecommissioned() bool {
return true
}
type fakePluginRegistry struct {
store map[string]*plugins.Plugin
}
func newFakePluginRegistry(m map[string]*plugins.Plugin) *fakePluginRegistry {
return &fakePluginRegistry{
store: m,
}
}
func (f *fakePluginRegistry) Plugin(_ context.Context, id string) (*plugins.Plugin, bool) {
p, exists := f.store[id]
return p, exists
}
func (f *fakePluginRegistry) Plugins(_ context.Context) []*plugins.Plugin {
var res []*plugins.Plugin
for _, p := range f.store {
res = append(res, p)
}
return res
}
func (f *fakePluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
f.store[p.ID] = p
return nil
}
func (f *fakePluginRegistry) Remove(_ context.Context, id string) error {
delete(f.store, id)
return nil
}

View File

@ -49,7 +49,10 @@ func StateToPostableAlert(alertState *State, appURL *url.URL) *models.PostableAl
}
if alertState.Image != nil {
nA[alertingModels.ImageTokenAnnotation] = generateImageURI(alertState.Image)
imageURI := generateImageURI(alertState.Image)
if imageURI != "" {
nA[alertingModels.ImageTokenAnnotation] = imageURI
}
}
if alertState.StateReason != "" {
@ -174,5 +177,9 @@ func generateImageURI(image *ngModels.Image) string {
if image.URL != "" {
return image.URL
}
return "token://" + image.Token
if image.Token != "" {
return "token://" + image.Token
}
return ""
}

View File

@ -133,6 +133,21 @@ func Test_StateToPostableAlert(t *testing.T) {
require.Equal(t, expected, result.Annotations)
})
t.Run("don't add __alertImageToken__ if there's no image token", func(t *testing.T) {
alertState := randomState(tc.state)
alertState.Annotations = randomMapOfStrings()
alertState.Image = &ngModels.Image{}
result := StateToPostableAlert(alertState, appURL)
expected := make(models.LabelSet, len(alertState.Annotations)+1)
for k, v := range alertState.Annotations {
expected[k] = v
}
require.Equal(t, expected, result.Annotations)
})
})
t.Run("should add state reason annotation if not empty", func(t *testing.T) {

View File

@ -89,7 +89,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
resources := []string{}
var resourceOrWorkspace string
var queryString string
var resultFormat string
var resultFormat dataquery.ResultFormat
traceExploreQuery := ""
traceParentExploreQuery := ""
traceLogsExploreQuery := ""
@ -103,7 +103,9 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
azureLogAnalyticsTarget := queryJSONModel.AzureLogAnalytics
logger.Debug("AzureLogAnalytics", "target", azureLogAnalyticsTarget)
resultFormat = azureLogAnalyticsTarget.ResultFormat
if azureLogAnalyticsTarget.ResultFormat != nil {
resultFormat = *azureLogAnalyticsTarget.ResultFormat
}
if resultFormat == "" {
resultFormat = types.TimeSeries
}
@ -115,14 +117,16 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
if len(azureLogAnalyticsTarget.Resources) > 0 {
resources = azureLogAnalyticsTarget.Resources
resourceOrWorkspace = azureLogAnalyticsTarget.Resources[0]
} else if azureLogAnalyticsTarget.Resource != "" {
resources = []string{azureLogAnalyticsTarget.Resource}
resourceOrWorkspace = azureLogAnalyticsTarget.Resource
} else {
resourceOrWorkspace = azureLogAnalyticsTarget.Workspace
} else if azureLogAnalyticsTarget.Resource != nil && *azureLogAnalyticsTarget.Resource != "" {
resources = []string{*azureLogAnalyticsTarget.Resource}
resourceOrWorkspace = *azureLogAnalyticsTarget.Resource
} else if azureLogAnalyticsTarget.Workspace != nil {
resourceOrWorkspace = *azureLogAnalyticsTarget.Workspace
}
queryString = azureLogAnalyticsTarget.Query
if azureLogAnalyticsTarget.Query != nil {
queryString = *azureLogAnalyticsTarget.Query
}
}
if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) {
@ -205,7 +209,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, &AzureLogAnalyticsQuery{
RefID: query.RefID,
ResultFormat: resultFormat,
ResultFormat: string(resultFormat),
URL: apiURL,
JSON: query.JSON,
TimeRange: query.TimeRange,
@ -701,7 +705,7 @@ func encodeQuery(rawQuery string) (string, error) {
return base64.StdEncoding.EncodeToString(b.Bytes()), nil
}
func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []string, filters []types.TracesFilters, resultFormat *string, resources []string) string {
func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []string, filters []dataquery.AzureTracesFilter, resultFormat *dataquery.ResultFormat, resources []string) string {
types := traceTypes
if len(types) == 0 {
types = Tables
@ -709,7 +713,7 @@ func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []str
filteredTypes := make([]string, 0)
// If the result format is set to trace then we filter out all events that are of the type traces as they don't make sense when visualised as a span
if resultFormat != nil && dataquery.ResultFormat(*resultFormat) == dataquery.ResultFormatTrace {
if resultFormat != nil && *resultFormat == dataquery.ResultFormatTrace {
filteredTypes = slices.Filter(filteredTypes, types, func(s string) bool { return s != "traces" })
} else {
filteredTypes = types

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics"
azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
@ -69,7 +70,7 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
for _, query := range queries {
var target string
queryJSONModel := types.AzureMonitorJSONQuery{}
queryJSONModel := dataquery.AzureMonitorQuery{}
err := json.Unmarshal(query.JSON, &queryJSONModel)
if err != nil {
return nil, fmt.Errorf("failed to decode the Azure Monitor query object from JSON: %w", err)
@ -77,49 +78,29 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
azJSONModel := queryJSONModel.AzureMonitor
// Legacy: If only MetricDefinition is set, use it as namespace
if azJSONModel.MetricDefinition != "" && azJSONModel.MetricNamespace == "" {
if azJSONModel.MetricDefinition != nil && *azJSONModel.MetricDefinition != "" &&
azJSONModel.MetricNamespace != nil && *azJSONModel.MetricNamespace == "" {
azJSONModel.MetricNamespace = azJSONModel.MetricDefinition
}
azJSONModel.DimensionFilters = MigrateDimensionFilters(azJSONModel.DimensionFilters)
alias := azJSONModel.Alias
timeGrain := azJSONModel.TimeGrain
timeGrains := azJSONModel.AllowedTimeGrainsMs
if timeGrain == "auto" {
timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains)
if err != nil {
return nil, err
}
alias := ""
if azJSONModel.Alias != nil {
alias = *azJSONModel.Alias
}
params := url.Values{}
params.Add("api-version", AzureMonitorAPIVersion)
params.Add("timespan", fmt.Sprintf("%v/%v", query.TimeRange.From.UTC().Format(time.RFC3339), query.TimeRange.To.UTC().Format(time.RFC3339)))
params.Add("interval", timeGrain)
params.Add("aggregation", azJSONModel.Aggregation)
params.Add("metricnames", azJSONModel.MetricName)
if azJSONModel.CustomNamespace != "" {
params.Add("metricnamespace", azJSONModel.CustomNamespace)
} else {
params.Add("metricnamespace", azJSONModel.MetricNamespace)
azureURL := ""
if queryJSONModel.Subscription != nil {
azureURL = BuildSubscriptionMetricsURL(*queryJSONModel.Subscription)
}
azureURL := BuildSubscriptionMetricsURL(queryJSONModel.Subscription)
filterInBody := true
if azJSONModel.Region != "" {
params.Add("region", azJSONModel.Region)
}
resourceIDs := []string{}
resourceMap := map[string]types.AzureMonitorResource{}
resourceMap := map[string]dataquery.AzureMonitorResource{}
if hasOne, resourceGroup, resourceName := hasOneResource(queryJSONModel); hasOne {
ub := urlBuilder{
ResourceURI: azJSONModel.ResourceURI,
ResourceURI: azJSONModel.ResourceUri,
// Alternative, used to reconstruct resource URI if it's not present
DefaultSubscription: dsInfo.Settings.SubscriptionId,
DefaultSubscription: &dsInfo.Settings.SubscriptionId,
Subscription: queryJSONModel.Subscription,
ResourceGroup: resourceGroup,
MetricNamespace: azJSONModel.MetricNamespace,
@ -128,25 +109,36 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
azureURL = ub.BuildMetricsURL()
// POST requests are only supported at the subscription level
filterInBody = false
resourceMap[ub.buildResourceURI()] = types.AzureMonitorResource{ResourceGroup: resourceGroup, ResourceName: resourceName}
resourceUri := ub.buildResourceURI()
if resourceUri != nil {
resourceMap[*resourceUri] = dataquery.AzureMonitorResource{ResourceGroup: resourceGroup, ResourceName: resourceName}
}
} else {
for _, r := range azJSONModel.Resources {
ub := urlBuilder{
DefaultSubscription: dsInfo.Settings.SubscriptionId,
DefaultSubscription: &dsInfo.Settings.SubscriptionId,
Subscription: queryJSONModel.Subscription,
ResourceGroup: r.ResourceGroup,
MetricNamespace: azJSONModel.MetricNamespace,
ResourceName: r.ResourceName,
}
resourceUri := ub.buildResourceURI()
resourceMap[resourceUri] = r
resourceIDs = append(resourceIDs, fmt.Sprintf("Microsoft.ResourceId eq '%s'", resourceUri))
if resourceUri != nil {
resourceMap[*resourceUri] = r
}
resourceIDs = append(resourceIDs, fmt.Sprintf("Microsoft.ResourceId eq '%s'", *resourceUri))
}
}
// old model
dimension := strings.TrimSpace(azJSONModel.Dimension)
dimensionFilter := strings.TrimSpace(azJSONModel.DimensionFilter)
dimension := ""
if azJSONModel.Dimension != nil {
dimension = strings.TrimSpace(*azJSONModel.Dimension)
}
dimensionFilter := ""
if azJSONModel.DimensionFilter != nil {
dimensionFilter = strings.TrimSpace(*azJSONModel.DimensionFilter)
}
dimSB := strings.Builder{}
@ -155,9 +147,9 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
} else {
for i, filter := range azJSONModel.DimensionFilters {
if len(filter.Filters) == 0 {
dimSB.WriteString(fmt.Sprintf("%s eq '*'", filter.Dimension))
dimSB.WriteString(fmt.Sprintf("%s eq '*'", *filter.Dimension))
} else {
dimSB.WriteString(filter.ConstructFiltersString())
dimSB.WriteString(types.ConstructFiltersString(filter))
}
if i != len(azJSONModel.DimensionFilters)-1 {
dimSB.WriteString(" and ")
@ -175,16 +167,21 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
}
}
if azJSONModel.Top != "" {
params.Add("top", azJSONModel.Top)
params, err := getParams(azJSONModel, query)
if err != nil {
return nil, err
}
target = params.Encode()
if setting.Env == setting.Dev {
logger.Debug("Azuremonitor request", "params", params)
}
sub := ""
if queryJSONModel.Subscription != nil {
sub = *queryJSONModel.Subscription
}
query := &types.AzureMonitorQuery{
URL: azureURL,
Target: target,
@ -194,7 +191,7 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
TimeRange: query.TimeRange,
Dimensions: azJSONModel.DimensionFilters,
Resources: resourceMap,
Subscription: queryJSONModel.Subscription,
Subscription: sub,
}
if filterString != "" {
if filterInBody {
@ -209,6 +206,45 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
return azureMonitorQueries, nil
}
func getParams(azJSONModel *dataquery.AzureMetricQuery, query backend.DataQuery) (url.Values, error) {
params := url.Values{}
timeGrain := azJSONModel.TimeGrain
timeGrains := azJSONModel.AllowedTimeGrainsMs
if timeGrain != nil && *timeGrain == "auto" {
var err error
timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains)
if err != nil {
return nil, err
}
}
params.Add("api-version", AzureMonitorAPIVersion)
params.Add("timespan", fmt.Sprintf("%v/%v", query.TimeRange.From.UTC().Format(time.RFC3339), query.TimeRange.To.UTC().Format(time.RFC3339)))
if timeGrain != nil {
params.Add("interval", *timeGrain)
}
if azJSONModel.Aggregation != nil {
params.Add("aggregation", *azJSONModel.Aggregation)
}
if azJSONModel.MetricName != nil {
params.Add("metricnames", *azJSONModel.MetricName)
}
if azJSONModel.CustomNamespace != nil && *azJSONModel.CustomNamespace != "" {
params.Add("metricnamespace", *azJSONModel.CustomNamespace)
} else if azJSONModel.MetricNamespace != nil {
params.Add("metricnamespace", *azJSONModel.MetricNamespace)
}
if azJSONModel.Region != nil && *azJSONModel.Region != "" {
params.Add("region", *azJSONModel.Region)
}
if azJSONModel.Top != nil && *azJSONModel.Top != "" {
params.Add("top", *azJSONModel.Top)
}
return params, nil
}
func retrieveSubscriptionDetails(e *AzureMonitorDatasource, cli *http.Client, ctx context.Context, logger log.Logger, tracer tracing.Tracer, subscriptionId string, baseUrl string, dsId int64, orgId int64) {
req, err := e.createRequest(ctx, logger, fmt.Sprintf("%s/subscriptions/%s", baseUrl, subscriptionId))
if err != nil {
@ -489,7 +525,20 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, res
continue
}
switch dimension.Operator {
if dimension.Dimension == nil {
continue
}
if dimension.Operator == nil {
filter := types.AzureMonitorDimensionFilterBackend{
Key: *dimension.Dimension,
Operator: 0,
Values: dimensionFilters,
}
filters = append(filters, filter)
continue
}
switch *dimension.Operator {
case "eq":
dimensionInt = 0
case "ne":
@ -499,7 +548,7 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, res
}
filter := types.AzureMonitorDimensionFilterBackend{
Key: dimension.Dimension,
Key: *dimension.Dimension,
Operator: dimensionInt,
Values: dimensionFilters,
}
@ -552,7 +601,7 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, res
// formatAzureMonitorLegendKey builds the legend key or timeseries name
// Alias patterns like {{resourcename}} are replaced with the appropriate data values.
func formatAzureMonitorLegendKey(alias string, subscriptionId string, resource types.AzureMonitorResource, metricName string, metadataName string,
func formatAzureMonitorLegendKey(alias string, subscriptionId string, resource dataquery.AzureMonitorResource, metricName string, metadataName string,
metadataValue string, namespace string, seriesID string, labels data.Labels) string {
// Could be a collision problem if there were two keys that varied only in case, but I don't think that would happen in azure.
lowerLabels := data.Labels{}
@ -582,16 +631,16 @@ func formatAzureMonitorLegendKey(alias string, subscriptionId string, resource t
}
}
if metaPartName == "resourcegroup" {
return []byte(resource.ResourceGroup)
if metaPartName == "resourcegroup" && resource.ResourceGroup != nil {
return []byte(*resource.ResourceGroup)
}
if metaPartName == "namespace" {
return []byte(namespace)
}
if metaPartName == "resourcename" {
return []byte(resource.ResourceName)
if metaPartName == "resourcename" && resource.ResourceName != nil {
return []byte(*resource.ResourceName)
}
if metaPartName == "metric" {
@ -674,17 +723,17 @@ func extractResourceIDFromMetricsURL(url string) string {
return strings.Split(url, "/providers/microsoft.insights/metrics")[0]
}
func hasOneResource(query types.AzureMonitorJSONQuery) (bool, string, string) {
func hasOneResource(query dataquery.AzureMonitorQuery) (bool, *string, *string) {
azJSONModel := query.AzureMonitor
if len(azJSONModel.Resources) > 1 {
return false, "", ""
return false, nil, nil
}
if len(azJSONModel.Resources) == 1 {
return true, azJSONModel.Resources[0].ResourceGroup, azJSONModel.Resources[0].ResourceName
}
if azJSONModel.ResourceGroup != "" || azJSONModel.ResourceName != "" {
if (azJSONModel.ResourceGroup != nil && *azJSONModel.ResourceGroup != "") || (azJSONModel.ResourceName != nil && *azJSONModel.ResourceName != "") {
// Deprecated, Resources should be used instead
return true, azJSONModel.ResourceGroup, azJSONModel.ResourceName
}
return false, "", ""
return false, nil, nil
}

View File

@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/testdata"
azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
@ -48,7 +49,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
expectedBodyFilter string
expectedParamFilter string
expectedPortalURL *string
resources map[string]types.AzureMonitorResource
resources map[string]dataquery.AzureMonitorResource
}{
{
name: "Parse queries from frontend and build AzureMonitor API queries",
@ -110,7 +111,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "legacy query without resourceURI and has dimensionFilter*s* property with one dimension",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("eq"), Filter: &wildcardFilter}},
"top": "30",
},
queryInterval: duration,
@ -123,7 +124,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "eq", Filter: &wildcardFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("eq"), Filter: &wildcardFilter}, {Dimension: strPtr("tier"), Operator: strPtr("eq"), Filter: &wildcardFilter}},
"top": "30",
},
queryInterval: duration,
@ -148,7 +149,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "has dimensionFilter*s* property with not equals operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test"}}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("ne"), Filter: &wildcardFilter, Filters: []string{"test"}}},
"top": "30",
},
queryInterval: duration,
@ -161,7 +162,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "has dimensionFilter*s* property with startsWith operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &testFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("sw"), Filter: &testFilter}},
"top": "30",
},
queryInterval: duration,
@ -174,7 +175,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "correctly sets dimension operator to eq (irrespective of operator) when filter value is '*'",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "ne", Filter: &wildcardFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("sw"), Filter: &wildcardFilter}, {Dimension: strPtr("tier"), Operator: strPtr("ne"), Filter: &wildcardFilter}},
"top": "30",
},
queryInterval: duration,
@ -187,7 +188,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "correctly constructs target when multiple filter values are provided for the 'eq' operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("eq"), Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"top": "30",
},
queryInterval: duration,
@ -200,7 +201,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "correctly constructs target when multiple filter values are provided for ne 'eq' operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("ne"), Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"top": "30",
},
queryInterval: duration,
@ -226,7 +227,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
"timeGrain": "PT1M",
"top": "10",
"region": "westus",
"resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}, {ResourceGroup: "rg2", ResourceName: "vm2"}},
"resources": []dataquery.AzureMonitorResource{{ResourceGroup: strPtr("rg"), ResourceName: strPtr("vm")}, {ResourceGroup: strPtr("rg2"), ResourceName: strPtr("vm2")}},
},
expectedInterval: "PT1M",
azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines&region=westus&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=10",
@ -237,7 +238,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "includes a single resource as a parameter filter",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}},
"resources": []dataquery.AzureMonitorResource{{ResourceGroup: strPtr("rg"), ResourceName: strPtr("vm")}},
},
queryInterval: duration,
expectedInterval: "PT1M",
@ -248,8 +249,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "includes a resource and a dimesion as filters",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}, {ResourceGroup: "rg2", ResourceName: "vm2"}},
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"resources": []dataquery.AzureMonitorResource{{ResourceGroup: strPtr("rg"), ResourceName: strPtr("vm")}, {ResourceGroup: strPtr("rg2"), ResourceName: strPtr("vm2")}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("ne"), Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"top": "30",
},
queryInterval: duration,
@ -296,14 +297,14 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
queries, err := datasource.buildQueries(log.New("test"), tsdbQuery, dsInfo)
require.NoError(t, err)
resources := map[string]types.AzureMonitorResource{}
resources := map[string]dataquery.AzureMonitorResource{}
if tt.azureMonitorVariedProperties["resources"] != nil {
resourceSlice := tt.azureMonitorVariedProperties["resources"].([]types.AzureMonitorResource)
resourceSlice := tt.azureMonitorVariedProperties["resources"].([]dataquery.AzureMonitorResource)
for _, resource := range resourceSlice {
resources[fmt.Sprintf("/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", resource.ResourceGroup, resource.ResourceName)] = resource
resources[fmt.Sprintf("/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", *resource.ResourceGroup, *resource.ResourceName)] = resource
}
} else {
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] = types.AzureMonitorResource{ResourceGroup: "grafanastaging", ResourceName: "grafana"}
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] = dataquery.AzureMonitorResource{ResourceGroup: strPtr("grafanastaging"), ResourceName: strPtr("grafana")}
}
azureMonitorQuery := &types.AzureMonitorQuery{
@ -366,8 +367,9 @@ func TestCustomNamespace(t *testing.T) {
}
func TestAzureMonitorParseResponse(t *testing.T) {
resources := map[string]types.AzureMonitorResource{}
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] = types.AzureMonitorResource{ResourceGroup: "grafanastaging", ResourceName: "grafana"}
resources := map[string]dataquery.AzureMonitorResource{}
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] =
dataquery.AzureMonitorResource{ResourceGroup: strPtr("grafanastaging"), ResourceName: strPtr("grafana")}
subscription := "12345678-aaaa-bbbb-cccc-123456789abc"
tests := []struct {
@ -484,7 +486,7 @@ func TestAzureMonitorParseResponse(t *testing.T) {
Params: url.Values{
"aggregation": {"Average"},
},
Resources: map[string]types.AzureMonitorResource{"/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanatest/providers/Microsoft.Storage/storageAccounts/testblobaccount/blobServices/default/providers/Microsoft.Insights/metrics": {ResourceGroup: "grafanatest", ResourceName: "testblobaccount"}},
Resources: map[string]dataquery.AzureMonitorResource{"/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanatest/providers/Microsoft.Storage/storageAccounts/testblobaccount/blobServices/default/providers/Microsoft.Insights/metrics": {ResourceGroup: strPtr("grafanatest"), ResourceName: strPtr("testblobaccount")}},
Subscription: subscription,
},
},
@ -681,3 +683,7 @@ func TestExtractResourceNameFromMetricsURL(t *testing.T) {
require.Equal(t, expected, extractResourceNameFromMetricsURL((url)))
})
}
func strPtr(s string) *string {
return &s
}

View File

@ -1,11 +1,9 @@
package metrics
import (
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
)
import "github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
func MigrateDimensionFilters(filters []types.AzureMonitorDimensionFilter) []types.AzureMonitorDimensionFilter {
var newFilters []types.AzureMonitorDimensionFilter
func MigrateDimensionFilters(filters []dataquery.AzureMetricDimension) []dataquery.AzureMetricDimension {
var newFilters []dataquery.AzureMetricDimension
for _, filter := range filters {
newFilter := filter
// Ignore the deprecation check as this is a migration

View File

@ -5,7 +5,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
)
func TestDimensionFiltersMigration(t *testing.T) {
@ -14,38 +14,38 @@ func TestDimensionFiltersMigration(t *testing.T) {
additionalTestFilter := "testFilter2"
tests := []struct {
name string
dimensionFilters []types.AzureMonitorDimensionFilter
expectedDimensionFilters []types.AzureMonitorDimensionFilter
dimensionFilters []dataquery.AzureMetricDimension
expectedDimensionFilters []dataquery.AzureMetricDimension
}{
{
name: "will return new format unchanged",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{"testFilter"}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{"testFilter"}}},
},
{
name: "correctly updates old format with wildcard",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq"}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &wildcard}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq")}},
},
{
name: "correctly updates old format with a value",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &testFilter}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter}}},
},
{
name: "correctly ignores wildcard if filters has a value",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard, Filters: []string{testFilter}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &wildcard, Filters: []string{testFilter}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter}}},
},
{
name: "correctly merges values if filters has a value (ignores duplicates)",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &testFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter}}},
},
{
name: "correctly merges values if filters has a value",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &additionalTestFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter, additionalTestFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &additionalTestFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter, additionalTestFilter}}},
},
}

View File

@ -7,42 +7,54 @@ import (
// urlBuilder builds the URL for calling the Azure Monitor API
type urlBuilder struct {
ResourceURI string
ResourceURI *string
// Following fields will be used to generate a ResourceURI
DefaultSubscription string
Subscription string
ResourceGroup string
MetricNamespace string
ResourceName string
DefaultSubscription *string
Subscription *string
ResourceGroup *string
MetricNamespace *string
ResourceName *string
}
func (params *urlBuilder) buildResourceURI() string {
if params.ResourceURI != "" {
func (params *urlBuilder) buildResourceURI() *string {
if params.ResourceURI != nil && *params.ResourceURI != "" {
return params.ResourceURI
}
subscription := params.Subscription
if params.Subscription == "" {
if params.Subscription == nil || *params.Subscription == "" {
subscription = params.DefaultSubscription
}
metricNamespaceArray := strings.Split(params.MetricNamespace, "/")
resourceNameArray := strings.Split(params.ResourceName, "/")
if params.MetricNamespace == nil || *params.MetricNamespace == "" {
return nil
}
metricNamespaceArray := strings.Split(*params.MetricNamespace, "/")
var resourceNameArray []string
if params.ResourceName != nil && *params.ResourceName != "" {
resourceNameArray = strings.Split(*params.ResourceName, "/")
}
provider := metricNamespaceArray[0]
metricNamespaceArray = metricNamespaceArray[1:]
if strings.HasPrefix(strings.ToLower(params.MetricNamespace), "microsoft.storage/storageaccounts/") &&
!strings.HasSuffix(params.ResourceName, "default") {
if strings.HasPrefix(strings.ToLower(*params.MetricNamespace), "microsoft.storage/storageaccounts/") &&
params.ResourceName != nil &&
!strings.HasSuffix(*params.ResourceName, "default") {
resourceNameArray = append(resourceNameArray, "default")
}
resGroup := ""
if params.ResourceGroup != nil {
resGroup = *params.ResourceGroup
}
urlArray := []string{
"/subscriptions",
subscription,
*subscription,
"resourceGroups",
params.ResourceGroup,
resGroup,
"providers",
provider,
}
@ -52,7 +64,7 @@ func (params *urlBuilder) buildResourceURI() string {
}
resourceURI := strings.Join(urlArray, "/")
return resourceURI
return &resourceURI
}
// BuildMetricsURL checks the metric properties to see which form of the url
@ -61,11 +73,11 @@ func (params *urlBuilder) BuildMetricsURL() string {
resourceURI := params.ResourceURI
// Prior to Grafana 9, we had a legacy query object rather than a resourceURI, so we manually create the resource URI
if resourceURI == "" {
if resourceURI == nil || *resourceURI == "" {
resourceURI = params.buildResourceURI()
}
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI)
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", *resourceURI)
}
// BuildSubscriptionMetricsURL returns a URL for querying metrics for all resources in a subscription

View File

@ -10,7 +10,7 @@ func TestURLBuilder(t *testing.T) {
t.Run("AzureMonitor URL Builder", func(t *testing.T) {
t.Run("when only resource uri is provided it returns resource/uri/providers/microsoft.insights/metrics", func(t *testing.T) {
ub := &urlBuilder{
ResourceURI: "/subscriptions/sub/resource/uri",
ResourceURI: strPtr("/subscriptions/sub/resource/uri"),
}
url := ub.BuildMetricsURL()
@ -19,11 +19,11 @@ func TestURLBuilder(t *testing.T) {
t.Run("when resource uri and legacy fields are provided the legacy fields are ignored", func(t *testing.T) {
ub := &urlBuilder{
ResourceURI: "/subscriptions/sub/resource/uri",
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
ResourceName: "rn1/rn2/rn3",
ResourceURI: strPtr("/subscriptions/sub/resource/uri"),
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.NetApp/netAppAccounts/capacityPools/volumes"),
ResourceName: strPtr("rn1/rn2/rn3"),
}
url := ub.BuildMetricsURL()
@ -33,10 +33,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("Legacy URL Builder params", func(t *testing.T) {
t.Run("when metric definition is in the short form", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Compute/virtualMachines"),
ResourceName: strPtr("rn"),
}
url := ub.BuildMetricsURL()
@ -45,11 +45,11 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is in the short form and a subscription is defined", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
Subscription: "specified-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
DefaultSubscription: strPtr("default-sub"),
Subscription: strPtr("specified-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Compute/virtualMachines"),
ResourceName: strPtr("rn"),
}
url := ub.BuildMetricsURL()
@ -58,10 +58,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Storage/storageAccounts/blobServices",
ResourceName: "rn1/default",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Storage/storageAccounts/blobServices"),
ResourceName: strPtr("rn1/default"),
}
url := ub.BuildMetricsURL()
@ -70,10 +70,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Storage/storageAccounts/fileServices",
ResourceName: "rn1/default",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Storage/storageAccounts/fileServices"),
ResourceName: strPtr("rn1/default"),
}
url := ub.BuildMetricsURL()
@ -82,10 +82,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
ResourceName: "rn1/rn2/rn3",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.NetApp/netAppAccounts/capacityPools/volumes"),
ResourceName: strPtr("rn1/rn2/rn3"),
}
url := ub.BuildMetricsURL()
@ -94,13 +94,13 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Storage/storageAccounts/blobServices",
ResourceName: "rn1",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Storage/storageAccounts/blobServices"),
ResourceName: strPtr("rn1"),
}
url := ub.buildResourceURI()
url := *ub.buildResourceURI()
assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default", url)
})
})

View File

@ -8,14 +8,14 @@ var (
// SetAutoTimeGrain tries to find the closest interval to the query's intervalMs value
// if the metric has a limited set of possible intervals/time grains then use those
// instead of the default list of intervals
func SetAutoTimeGrain(intervalMs int64, timeGrains []int64) (string, error) {
func SetAutoTimeGrain(intervalMs int64, timeGrains []int64) (*string, error) {
autoInterval := FindClosestAllowedIntervalMS(intervalMs, timeGrains)
autoTimeGrain, err := CreateISO8601DurationFromIntervalMS(autoInterval)
if err != nil {
return "", err
return nil, err
}
return autoTimeGrain, nil
return &autoTimeGrain, nil
}
// FindClosestAllowedIntervalMS is used for the auto time grain setting.

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
)
const (
@ -83,8 +84,8 @@ type AzureMonitorQuery struct {
Alias string
TimeRange backend.TimeRange
BodyFilter string
Dimensions []AzureMonitorDimensionFilter
Resources map[string]AzureMonitorResource
Dimensions []dataquery.AzureMetricDimension
Resources map[string]dataquery.AzureMonitorResource
Subscription string
}
@ -138,110 +139,38 @@ type AzureMonitorResource struct {
ResourceName string `json:"resourceName"`
}
// AzureMonitorJSONQuery is the frontend JSON query model for an Azure Monitor query.
type AzureMonitorJSONQuery struct {
AzureMonitor struct {
ResourceURI string `json:"resourceUri"`
// These are used to reconstruct a resource URI
MetricNamespace string `json:"metricNamespace"`
CustomNamespace string `json:"customNamespace"`
MetricName string `json:"metricName"`
Region string `json:"region"`
Resources []AzureMonitorResource `json:"resources"`
Aggregation string `json:"aggregation"`
Alias string `json:"alias"`
DimensionFilters []AzureMonitorDimensionFilter `json:"dimensionFilters"` // new model
TimeGrain string `json:"timeGrain"`
Top string `json:"top"`
AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"`
Dimension string `json:"dimension"` // old model
DimensionFilter string `json:"dimensionFilter"` // old model
Format string `json:"format"`
// Deprecated, MetricNamespace should be used instead
MetricDefinition string `json:"metricDefinition"`
// Deprecated: Use Resources with a single element instead
AzureMonitorResource
} `json:"azureMonitor"`
Subscription string `json:"subscription"`
}
// AzureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric
// queries like "BlobType", "eq", "*"
type AzureMonitorDimensionFilter struct {
Dimension string `json:"dimension"`
Operator string `json:"operator"`
Filters []string `json:"filters,omitempty"`
// Deprecated: To support multiselection, filters are passed in a slice now. Also migrated in frontend.
Filter *string `json:"filter,omitempty"`
}
type AzureMonitorDimensionFilterBackend struct {
Key string `json:"key"`
Operator int `json:"operator"`
Values []string `json:"values"`
}
func (a AzureMonitorDimensionFilter) ConstructFiltersString() string {
func ConstructFiltersString(a dataquery.AzureMetricDimension) string {
var filterStrings []string
for _, filter := range a.Filters {
filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter))
dimension := ""
operator := ""
if a.Dimension != nil {
dimension = *a.Dimension
}
if a.Operator != nil {
operator = *a.Operator
}
filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", dimension, operator, filter))
}
if a.Operator == "eq" {
if a.Operator != nil && *a.Operator == "eq" {
return strings.Join(filterStrings, " or ")
} else {
return strings.Join(filterStrings, " and ")
}
return strings.Join(filterStrings, " and ")
}
// LogJSONQuery is the frontend JSON query model for an Azure Log Analytics query.
type LogJSONQuery struct {
AzureLogAnalytics struct {
Query string `json:"query"`
ResultFormat string `json:"resultFormat"`
Resources []string `json:"resources"`
OperationId string `json:"operationId"`
// Deprecated: Queries should be migrated to use Resource instead
Workspace string `json:"workspace"`
// Deprecated: Use Resources instead
Resource string `json:"resource"`
} `json:"azureLogAnalytics"`
AzureLogAnalytics dataquery.AzureLogsQuery `json:"azureLogAnalytics"`
}
type TracesJSONQuery struct {
AzureTraces struct {
// Filters for property values.
Filters []TracesFilters `json:"filters"`
// Operation ID. Used only for Traces queries.
OperationId *string `json:"operationId"`
// KQL query to be executed.
Query *string `json:"query"`
// Array of resource URIs to be queried.
Resources []string `json:"resources"`
// Specifies the format results should be returned as.
ResultFormat *string `json:"resultFormat"`
// Types of events to filter by.
TraceTypes []string `json:"traceTypes"`
} `json:"azureTraces"`
}
type TracesFilters struct {
// Values to filter by.
Filters []string `json:"filters"`
// Comparison operator to use. Either equals or not equals.
Operation string `json:"operation"`
// Property name, auto-populated based on available traces.
Property string `json:"property"`
AzureTraces dataquery.AzureTracesQuery `json:"azureTraces"`
}
// MetricChartDefinition is the JSON model for a metrics chart definition

View File

@ -0,0 +1,48 @@
import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { OrgPicker } from './OrgPicker';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
get: () =>
Promise.resolve([
{ name: 'Org 1', id: 0 },
{ name: 'Org 2', id: 1 },
]),
}),
}));
function setup(jsx: JSX.Element) {
return {
user: userEvent.setup(),
...render(jsx),
};
}
describe('OrgPicker', () => {
it('should render', async () => {
render(
<>
<label htmlFor={'picker'}>Org picker</label>
<OrgPicker onSelected={() => {}} inputId={'picker'} />
</>
);
expect(await screen.findByRole('combobox', { name: 'Org picker' })).toBeInTheDocument();
});
it('should have the options', async () => {
const { user } = setup(
<>
<label htmlFor={'picker'}>Org picker</label>
<OrgPicker onSelected={() => {}} inputId={'picker'} />
</>
);
await user.click(await screen.findByRole('combobox', { name: 'Org picker' }));
expect(screen.getByText('Org 1')).toBeInTheDocument();
expect(screen.getByText('Org 2')).toBeInTheDocument();
});
});

View File

@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { AsyncSelect } from '@grafana/ui';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { Organization, UserOrg } from 'app/types';
export type OrgSelectItem = SelectableValue<Organization>;

View File

@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { StoreState } from 'app/types';
@ -63,7 +64,13 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet
documentation.
</a>
);
const subTitle = <span>Manage your auth settings and configure single sign-on. Find out more in our {docsLink}</span>;
const onCTAClick = () => {
reportInteraction('authentication_ui_created', { provider: firstAvailableProvider?.type });
};
return (
<Page navId="authentication" subTitle={subTitle}>
<Page.Contents isLoading={isLoading}>
@ -92,6 +99,7 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet
description={`Important: if you have ${firstAvailableProvider.type} configuration enabled via the .ini file Grafana is using it.
Configuring ${firstAvailableProvider.type} via UI will take precedence over any configuration in the .ini file.
No changes will be written into .ini file.`}
onClick={onCTAClick}
/>
)}
{!!configuresProviders?.length && (

View File

@ -12,6 +12,7 @@ export interface Props {
buttonTitle: string;
buttonDisabled?: boolean;
description?: string;
onClick?: () => void;
}
const ConfigureAuthCTA: React.FunctionComponent<Props> = ({
@ -21,6 +22,7 @@ const ConfigureAuthCTA: React.FunctionComponent<Props> = ({
buttonTitle,
buttonDisabled,
description,
onClick,
}) => {
const styles = useStyles2(getStyles);
const footer = description ? <span key="proTipFooter">{description}</span> : '';
@ -34,6 +36,7 @@ const ConfigureAuthCTA: React.FunctionComponent<Props> = ({
className={ctaElementClassName}
data-testid={selectors.components.CallToActionCard.buttonV2(buttonTitle)}
disabled={buttonDisabled}
onClick={() => onClick && onClick()}
>
{buttonTitle}
</LinkButton>

View File

@ -16,7 +16,7 @@ import { skipToken, useGetFolderQuery, useSaveFolderMutation } from './api/brows
import { BrowseActions } from './components/BrowseActions/BrowseActions';
import { BrowseFilters } from './components/BrowseFilters';
import { BrowseView } from './components/BrowseView';
import { CreateNewButton } from './components/CreateNewButton';
import CreateNewButton from './components/CreateNewButton';
import { FolderActionsButton } from './components/FolderActionsButton';
import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions';
@ -104,7 +104,8 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
{folderDTO && <FolderActionsButton folder={folderDTO} />}
{(canCreateDashboards || canCreateFolder) && (
<CreateNewButton
inFolder={folderUID}
parentFolderTitle={folderDTO?.title}
parentFolderUid={folderUID}
canCreateDashboard={canCreateDashboards}
canCreateFolder={canCreateFolder}
/>

View File

@ -1,11 +1,16 @@
import { render, screen } from '@testing-library/react';
import { render as rtlRender, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { CreateNewButton } from './CreateNewButton';
import CreateNewButton from './CreateNewButton';
function render(...[ui, options]: Parameters<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
async function renderAndOpen(folderUID?: string) {
render(<CreateNewButton canCreateDashboard canCreateFolder inFolder={folderUID} />);
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolderUid={folderUID} />);
const newButton = screen.getByText('New');
await userEvent.click(newButton);
}
@ -14,27 +19,39 @@ describe('NewActionsButton', () => {
it('should display the correct urls with a given folderUID', async () => {
await renderAndOpen('123');
expect(screen.getByText('New Dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=123');
expect(screen.getByText('New Folder')).toHaveAttribute('href', '/dashboards/folder/new?folderUid=123');
expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=123');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import?folderUid=123');
});
it('should display urls without params when there is no folderUID', async () => {
await renderAndOpen();
expect(screen.getByText('New Dashboard')).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByText('New Folder')).toHaveAttribute('href', '/dashboards/folder/new');
expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import');
});
it('clicking the "New folder" button opens the drawer', async () => {
const mockParentFolderTitle = 'mockParentFolderTitle';
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolderTitle={mockParentFolderTitle} />);
const newButton = screen.getByText('New');
await userEvent.click(newButton);
await userEvent.click(screen.getByText('New folder'));
const drawer = screen.getByRole('dialog', { name: 'Drawer title New folder' });
expect(drawer).toBeInTheDocument();
expect(within(drawer).getByRole('heading', { name: 'New folder' })).toBeInTheDocument();
expect(within(drawer).getByText(`Location: ${mockParentFolderTitle}`)).toBeInTheDocument();
});
it('should only render dashboard items when folder creation is disabled', async () => {
render(<CreateNewButton canCreateDashboard canCreateFolder={false} />);
const newButton = screen.getByText('New');
await userEvent.click(newButton);
expect(screen.getByText('New Dashboard')).toBeInTheDocument();
expect(screen.getByText('New dashboard')).toBeInTheDocument();
expect(screen.getByText('Import')).toBeInTheDocument();
expect(screen.queryByText('New Folder')).not.toBeInTheDocument();
expect(screen.queryByText('New folder')).not.toBeInTheDocument();
});
it('should only render folder item when dashboard creation is disabled', async () => {
@ -42,8 +59,8 @@ describe('NewActionsButton', () => {
const newButton = screen.getByText('New');
await userEvent.click(newButton);
expect(screen.queryByText('New Dashboard')).not.toBeInTheDocument();
expect(screen.queryByText('New dashboard')).not.toBeInTheDocument();
expect(screen.queryByText('Import')).not.toBeInTheDocument();
expect(screen.getByText('New Folder')).toBeInTheDocument();
expect(screen.getByText('New folder')).toBeInTheDocument();
});
});

View File

@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Button, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { createNewFolder } from 'app/features/folders/state/actions';
import {
getNewDashboardPhrase,
getNewFolderPhrase,
@ -8,41 +10,78 @@ import {
getNewPhrase,
} from 'app/features/search/tempI18nPhrases';
interface Props {
import { NewFolderForm } from './NewFolderForm';
const mapDispatchToProps = {
createNewFolder,
};
const connector = connect(null, mapDispatchToProps);
interface OwnProps {
parentFolderTitle?: string;
/**
* Pass a folder UID in which the dashboard or folder will be created
*/
inFolder?: string;
parentFolderUid?: string;
canCreateFolder: boolean;
canCreateDashboard: boolean;
}
export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder }: Props) {
type Props = OwnProps & ConnectedProps<typeof connector>;
function CreateNewButton({
parentFolderTitle,
parentFolderUid,
canCreateDashboard,
canCreateFolder,
createNewFolder,
}: Props) {
const [isOpen, setIsOpen] = useState(false);
const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false);
const onCreateFolder = (folderName: string) => {
createNewFolder(folderName, parentFolderUid);
setShowNewFolderDrawer(false);
};
const newMenu = (
<Menu>
{canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/new', inFolder)} label={getNewDashboardPhrase()} />
)}
{canCreateFolder && (
<MenuItem url={addFolderUidToUrl('/dashboards/folder/new', inFolder)} label={getNewFolderPhrase()} />
<MenuItem url={addFolderUidToUrl('/dashboard/new', parentFolderUid)} label={getNewDashboardPhrase()} />
)}
{canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />}
{canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/import', inFolder)} label={getImportPhrase()} />
<MenuItem url={addFolderUidToUrl('/dashboard/import', parentFolderUid)} label={getImportPhrase()} />
)}
</Menu>
);
return (
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button>
{getNewPhrase()}
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
</Dropdown>
<>
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button>
{getNewPhrase()}
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
</Dropdown>
{showNewFolderDrawer && (
<Drawer
title={getNewFolderPhrase()}
subtitle={parentFolderTitle ? `Location: ${parentFolderTitle}` : undefined}
scrollableContent
onClose={() => setShowNewFolderDrawer(false)}
size="sm"
>
<NewFolderForm onConfirm={onCreateFolder} onCancel={() => setShowNewFolderDrawer(false)} />
</Drawer>
)}
</>
);
}
export default connector(CreateNewButton);
/**
*
* @param url without any parameters

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Button, Input, Form, Field, HorizontalGroup } from '@grafana/ui';
import { validationSrv } from '../../manage-dashboards/services/ValidationSrv';
interface Props {
onConfirm: (folderName: string) => void;
onCancel: () => void;
}
interface FormModel {
folderName: string;
}
const initialFormModel: FormModel = { folderName: '' };
export function NewFolderForm({ onCancel, onConfirm }: Props) {
const validateFolderName = async (folderName: string) => {
try {
await validationSrv.validateNewFolderName(folderName);
return true;
} catch (e) {
if (e instanceof Error) {
return e.message;
} else {
throw e;
}
}
};
return (
<Form defaultValues={initialFormModel} onSubmit={(form: FormModel) => onConfirm(form.folderName)}>
{({ register, errors }) => (
<>
<Field
label="Folder name"
invalid={!!errors.folderName}
error={errors.folderName && errors.folderName.message}
>
<Input
id="folder-name-input"
{...register('folderName', {
required: 'Folder name is required.',
validate: async (v) => await validateFolderName(v),
})}
/>
</Field>
<HorizontalGroup>
<Button variant="secondary" fill="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Create</Button>
</HorizontalGroup>
</>
)}
</Form>
);
}

View File

@ -437,23 +437,25 @@ function updateStatePageNavFromProps(props: Props, state: State): State {
};
}
// Check if folder changed
const { folderTitle, folderUid } = dashboard.meta;
if (folderTitle && folderUid && pageNav && pageNav.parentItem?.text !== folderTitle) {
if (folderUid && pageNav) {
if (config.featureToggles.nestedFolders) {
const folderNavModel = folderUid ? getNavModel(navIndex, `folder-dashboards-${folderUid}`).main : undefined;
const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
pageNav = {
...pageNav,
parentItem: folderNavModel,
};
} else {
pageNav = {
...pageNav,
parentItem: {
text: folderTitle,
url: `/dashboards/f/${dashboard.meta.folderUid}`,
},
};
// Check if folder changed
if (folderTitle && pageNav.parentItem?.text !== folderTitle) {
pageNav = {
...pageNav,
parentItem: {
text: folderTitle,
url: `/dashboards/f/${dashboard.meta.folderUid}`,
},
};
}
}
}

View File

@ -50,14 +50,11 @@ export class DashboardSrv {
return new DashboardModel(dashboard, meta);
}
setCurrent(dashboard: DashboardModel) {
setCurrent(dashboard: DashboardModel | undefined) {
this.dashboard = dashboard;
}
getCurrent(): DashboardModel | undefined {
if (!this.dashboard) {
console.warn('Calling getDashboardSrv().getCurrent() without calling getDashboardSrv().setCurrent() first.');
}
return this.dashboard;
}

View File

@ -768,6 +768,7 @@ export class DashboardModel implements TimeModel {
for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) {
const option = selectedOptions[optionIndex];
const rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex);
setScopedVars(rowCopy, option);
const rowHeight = this.getRowHeight(rowCopy);
@ -955,6 +956,10 @@ export class DashboardModel implements TimeModel {
row.collapsed = false;
const rowPanels = row.panels ?? [];
const hasRepeat = rowPanels.some((p: PanelModel) => p.repeat);
// This is set only for the row being repeated.
const rowRepeatVariable = row.repeat;
if (rowPanels.length > 0) {
// Use first panel to figure out if it was moved or pushed
// If the panel doesn't have gridPos.y, use the row gridPos.y instead.
@ -969,6 +974,18 @@ export class DashboardModel implements TimeModel {
let yMax = row.gridPos.y;
for (const panel of rowPanels) {
// When expanding original row that's repeated, set scopedVars for repeated row panels.
if (rowRepeatVariable) {
const variable = this.getPanelRepeatVariable(row);
panel.scopedVars ??= {};
if (variable) {
const selectedOptions = this.getSelectedVariableOptions(variable);
panel.scopedVars = {
...panel.scopedVars,
[variable.name]: selectedOptions[0],
};
}
}
// set the y gridPos if it wasn't already set
panel.gridPos.y ?? (panel.gridPos.y = row.gridPos.y); // (Safari 13.1 lacks ??= support)
// make sure y is adjusted (in case row moved while collapsed)

View File

@ -9,6 +9,7 @@ import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLev
import { loadPluginDashboards } from '../../plugins/admin/state/actions';
import { cancelVariables } from '../../variables/state/actions';
import { getDashboardSrv } from '../services/DashboardSrv';
import { getTimeSrv } from '../services/TimeSrv';
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
@ -124,10 +125,12 @@ export const cleanUpDashboardAndVariables = (): ThunkResult<void> => (dispatch,
}
getTimeSrv().stopAutoRefresh();
dispatch(cleanUpDashboard());
dispatch(removeAllPanels());
dashboardWatcher.leave();
getDashboardSrv().setCurrent(undefined);
};
export const updateTimeZoneDashboard =

View File

@ -104,6 +104,12 @@ async function fetchDashboard(
return dashDTO;
}
case DashboardRoutes.New: {
// only the folder API has information about ancestors
// get parent folder (if it exists) and put it in the store
// this will be used to populate the full breadcrumb trail
if (config.featureToggles.nestedFolders && args.urlFolderUid) {
await dispatch(getFolderByUid(args.urlFolderUid));
}
return getNewDashboardModelData(args.urlFolderUid, args.panelType);
}
case DashboardRoutes.Path: {

View File

@ -70,7 +70,6 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => {
font-family: ${theme.typography.fontFamilyMonospace};
font-size: ${theme.typography.bodySmall.fontSize};
width: 100%;
margin-bottom: ${theme.spacing(2.25)}; /* This is to make sure the last row's LogRowMenu is not cut off. */
`,
contextBackground: css`
background: ${hoverBgColor};

View File

@ -254,7 +254,8 @@ export class DatasourceSrv implements DataSourceService {
// Support for multi-value variables with only one selected datasource
dsValue = dsValue[0];
}
const dsSettings = !Array.isArray(dsValue) && this.settingsMapByName[dsValue];
const dsSettings =
!Array.isArray(dsValue) && (this.settingsMapByName[dsValue] || this.settingsMapByUid[dsValue]);
if (dsSettings) {
const key = `$\{${variable.name}\}`;

View File

@ -18,6 +18,13 @@ const templateSrv: any = {
value: 'BBB',
},
},
{
type: 'datasource',
name: 'datasourceByUid',
current: {
value: 'uid-code-DDDD',
},
},
{
type: 'datasource',
name: 'datasourceDefault',
@ -32,6 +39,7 @@ const templateSrv: any = {
}
let result = v.replace('${datasource}', 'BBB');
result = result.replace('${datasourceByUid}', 'DDDD');
result = result.replace('${datasourceDefault}', 'default');
return result;
},
@ -103,6 +111,12 @@ describe('datasource_srv', () => {
meta: { metrics: true },
isDefault: true,
},
DDDD: {
type: 'test-db',
name: 'DDDD',
uid: 'uid-code-DDDD',
meta: { metrics: true },
},
Jaeger: {
type: 'jaeger-db',
name: 'Jaeger',
@ -165,7 +179,7 @@ describe('datasource_srv', () => {
expect(dataSourceSrv.getInstanceSettings({ uid: 'uid-code-mmm' })).toBe(ds);
});
it('should work with variable', () => {
it('should work with variable by ds name', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasource}');
expect(ds?.name).toBe('${datasource}');
expect(ds?.uid).toBe('${datasource}');
@ -177,6 +191,18 @@ describe('datasource_srv', () => {
`);
});
it('should work with variable by ds value (uid)', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasourceByUid}');
expect(ds?.name).toBe('${datasourceByUid}');
expect(ds?.uid).toBe('${datasourceByUid}');
expect(ds?.rawRef).toMatchInlineSnapshot(`
{
"type": "test-db",
"uid": "uid-code-DDDD",
}
`);
});
it('should work with variable via scopedVars', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasource}', {
datasource: { text: 'Prom', value: 'uid-code-aaa' },
@ -247,7 +273,7 @@ describe('datasource_srv', () => {
describe('when getting external metric sources', () => {
it('should return list of explore sources', () => {
const externalSources = dataSourceSrv.getExternal();
expect(externalSources.length).toBe(6);
expect(externalSources.length).toBe(7);
});
});
@ -260,8 +286,9 @@ describe('datasource_srv', () => {
it('Can get list of data sources with variables: true', () => {
const list = dataSourceSrv.getList({ metrics: true, variables: true });
expect(list[0].name).toBe('${datasourceDefault}');
expect(list[1].name).toBe('${datasource}');
expect(list[0].name).toBe('${datasourceByUid}');
expect(list[1].name).toBe('${datasourceDefault}');
expect(list[2].name).toBe('${datasource}');
});
it('Can get list of data sources with tracing: true', () => {
@ -300,6 +327,14 @@ describe('datasource_srv', () => {
"type": "test-db",
"uid": "uid-code-BBB",
},
{
"meta": {
"metrics": true,
},
"name": "DDDD",
"type": "test-db",
"uid": "uid-code-DDDD",
},
{
"meta": {
"annotations": true,

View File

@ -10,11 +10,11 @@ export function getSearchPlaceholder(includePanels = false) {
}
export function getNewDashboardPhrase() {
return t('search.dashboard-actions.new-dashboard', 'New Dashboard');
return t('search.dashboard-actions.new-dashboard', 'New dashboard');
}
export function getNewFolderPhrase() {
return t('search.dashboard-actions.new-folder', 'New Folder');
return t('search.dashboard-actions.new-folder', 'New folder');
}
export function getImportPhrase() {

View File

@ -66,6 +66,7 @@ export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions,
const outputFrame: DataFrame = {
fields: [],
length: frame.length,
refId: frame.refId,
};
for (const field of frame.fields) {
@ -85,7 +86,6 @@ export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions,
output.push(outputFrame);
}
return output;
}

View File

@ -42,7 +42,7 @@ export const dataSourceVariableSlice = createSlice({
}
if (isValid(source, regex)) {
options.push({ text: source.name, value: source.name, selected: false });
options.push({ text: source.name, value: source.uid, selected: false });
}
if (isDefault(source, regex)) {

View File

@ -3,7 +3,7 @@ import { DataSourceInstanceSettings, DataSourceJsonData, DataSourcePluginMeta }
export function getDataSourceInstanceSetting(name: string, meta: DataSourcePluginMeta): DataSourceInstanceSettings {
return {
id: 1,
uid: '',
uid: name,
type: '',
name,
meta,

View File

@ -3,14 +3,20 @@ import { VariableOption, VariableWithOptions } from 'app/features/variables/type
import { VariableBuilder } from './variableBuilder';
export class OptionsVariableBuilder<T extends VariableWithOptions> extends VariableBuilder<T> {
withOptions(...texts: string[]) {
withOptions(...options: Array<string | { text: string; value: string }>) {
this.variable.options = [];
for (let index = 0; index < texts.length; index++) {
this.variable.options.push({
text: texts[index],
value: texts[index],
selected: false,
});
for (let index = 0; index < options.length; index++) {
const option = options[index];
if (typeof option === 'string') {
this.variable.options.push({
text: option,
value: option,
selected: false,
});
} else {
this.variable.options.push({ ...option, selected: false });
}
}
return this;
}

View File

@ -381,6 +381,74 @@ describe('shared actions', () => {
});
});
describe('and not multivalue, but with currentValue specified', () => {
const A = { text: 'A', value: 'a-uid' };
const B = { text: 'B', value: 'b-uid' };
const C = { text: 'C', value: 'c-uid' };
it.each`
withOptions | currentText | currentValue | defaultValue | expected
${[A, B, C]} | ${undefined} | ${undefined} | ${undefined} | ${A}
${[A, B, C]} | ${'B'} | ${'b-uid'} | ${undefined} | ${B}
${[A, B, C]} | ${'B'} | ${undefined} | ${undefined} | ${B}
${[A, B, C]} | ${undefined} | ${'b-uid'} | ${undefined} | ${B}
${[A, B, C]} | ${'Old B'} | ${'b-uid'} | ${undefined} | ${B}
${[A, B, C]} | ${undefined} | ${'x-uid'} | ${'b-uid'} | ${B}
${[A, B, C]} | ${undefined} | ${'b-uid'} | ${'c-uid'} | ${B}
${[A, B, C]} | ${undefined} | ${'x-uid'} | ${undefined} | ${A}
${undefined} | ${undefined} | ${'b-uid'} | ${undefined} | ${'should not dispatch setCurrentVariableValue'}
`(
'then correct actions are dispatched',
async ({ withOptions, currentText, currentValue, defaultValue, expected }) => {
let custom;
const key = 'key';
if (!withOptions) {
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withCurrent(currentText, currentValue)
.withoutOptions()
.build();
} else {
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withOptions(...withOptions)
.withCurrent(currentText, currentValue)
.build();
}
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(
validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
true
);
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
const expectedActions: AnyAction[] = withOptions
? [
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expected.text, value: expected.value, selected: false } }
)
)
),
]
: [];
expect(dispatchedActions).toEqual(expectedActions);
return true;
});
}
);
});
describe('and multivalue', () => {
it.each`
withOptions | withCurrent | defaultValue | expectedText | expectedSelected

View File

@ -60,6 +60,7 @@ import {
ensureStringValues,
ExtendedUrlQueryMap,
getCurrentText,
getCurrentValue,
getVariableRefresh,
hasOngoingTransaction,
toKeyedVariableIdentifier,
@ -522,14 +523,16 @@ export const validateVariableSelectionState = (
// 1. find the current value
const text = getCurrentText(variableInState);
option = variableInState.options?.find((v) => v.text === text);
const value = getCurrentValue(variableInState);
option = variableInState.options?.find((v: VariableOption) => v.text === text || v.value === value);
if (option) {
return setValue(variableInState, option);
}
// 2. find the default value
if (defaultValue) {
option = variableInState.options?.find((v) => v.text === defaultValue);
option = variableInState.options?.find((v) => v.text === defaultValue || v.value === defaultValue);
if (option) {
return setValue(variableInState, option);
}

View File

@ -8,7 +8,7 @@ const CHEAT_SHEET_ITEMS = [
},
];
const InfluxCheatSheet = (props: any) => (
export const InfluxCheatSheet = () => (
<div>
<h2>InfluxDB Cheat Sheet</h2>
{CHEAT_SHEET_ITEMS.map((item) => (
@ -19,5 +19,3 @@ const InfluxCheatSheet = (props: any) => (
))}
</div>
);
export default InfluxCheatSheet;

View File

@ -1,11 +1,7 @@
import React, { PureComponent } from 'react';
import React from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
import { InfluxCheatSheet } from './InfluxCheatSheet';
import InfluxCheatSheet from './InfluxCheatSheet';
export default class InfluxStartPage extends PureComponent<QueryEditorHelpProps> {
render() {
return <InfluxCheatSheet onClickExample={this.props.onClickExample} />;
}
export function InfluxStartPage() {
return <InfluxCheatSheet />;
}

View File

@ -4,7 +4,7 @@ import { HorizontalGroup, InlineFormLabel, Input, Select, TextArea } from '@graf
import { InfluxQuery } from '../../../../../types';
import { DEFAULT_RESULT_FORMAT, RESULT_FORMATS } from '../../../constants';
import { useShadowedState } from '../../hooks/useShadowedState';
import { useShadowedState } from '../hooks/useShadowedState';
type Props = {
query: InfluxQuery;

View File

@ -0,0 +1,44 @@
import { renderHook } from '@testing-library/react-hooks';
import config from 'app/core/config';
import { getMockDS, getMockDSInstanceSettings, mockBackendService } from '../../../../../specs/mocks';
import { useRetentionPolicies } from './useRetentionPolicies';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
}));
describe('useRetentionPolicies', () => {
it('should return all policies when influxdbBackendMigration feature toggle enabled', async () => {
const instanceSettings = getMockDSInstanceSettings();
const datasource = getMockDS(instanceSettings);
mockBackendService(response);
config.featureToggles.influxdbBackendMigration = true;
const { result, waitForNextUpdate } = renderHook(() => useRetentionPolicies(datasource));
await waitForNextUpdate();
expect(result.current.retentionPolicies.length).toEqual(4);
expect(result.current.retentionPolicies[0]).toEqual('autogen');
});
});
const response = {
data: {
results: {
metadataQuery: {
status: 200,
frames: [
{
schema: {
refId: 'metadataQuery',
fields: [{ name: 'value', type: 'string', typeInfo: { frame: 'string' } }],
},
data: { values: [['autogen', 'bar', '5m_avg', '1m_avg']] },
},
],
},
},
},
};

View File

@ -0,0 +1,15 @@
import { useEffect, useState } from 'react';
import InfluxDatasource from '../../../../../datasource';
import { getAllPolicies } from '../../../../../influxql_metadata_query';
export const useRetentionPolicies = (datasource: InfluxDatasource) => {
const [retentionPolicies, setRetentionPolicies] = useState<string[]>([]);
useEffect(() => {
getAllPolicies(datasource).then((data) => {
setRetentionPolicies(data);
});
}, [datasource]);
return { retentionPolicies };
};

View File

@ -0,0 +1,7 @@
// it is possible to add fields into the `InfluxQueryTag` structures, and they do work,
// but in some cases, when we do metadata queries, we have to remove them from the queries.
import { InfluxQueryTag } from '../../../../../types';
export function filterTags(parts: InfluxQueryTag[], allTagKeys: Set<string>): InfluxQueryTag[] {
return parts.filter((t) => t.key.endsWith('::tag') || allTagKeys.has(t.key + '::tag'));
}

View File

@ -0,0 +1,12 @@
import { TypedVariableModel } from '@grafana/data/src';
import { getTemplateSrv } from '@grafana/runtime/src';
export function getTemplateVariableOptions(wrapper: (v: TypedVariableModel) => string) {
return (
getTemplateSrv()
.getVariables()
// we make them regex-params, i'm not 100% sure why.
// probably because this way multi-value variables work ok too.
.map(wrapper)
);
}

View File

@ -0,0 +1,16 @@
// helper function to make it easy to call this from the widget-render-code
import { TypedVariableModel } from '@grafana/data/src';
import { getTemplateVariableOptions } from './getTemplateVariableOptions';
export function withTemplateVariableOptions(
optionsPromise: Promise<string[]>,
wrapper: (v: TypedVariableModel) => string,
filter?: string
): Promise<string[]> {
let templateVariableOptions = getTemplateVariableOptions(wrapper);
if (filter) {
templateVariableOptions = templateVariableOptions.filter((tvo) => tvo.indexOf(filter) > -1);
}
return optionsPromise.then((options) => [...templateVariableOptions, ...options]);
}

View File

@ -0,0 +1,9 @@
import { TypedVariableModel } from '@grafana/data/src';
export function wrapRegex(v: TypedVariableModel): string {
return `/^$${v.name}$/`;
}
export function wrapPure(v: TypedVariableModel): string {
return `$${v.name}`;
}

View File

@ -3,7 +3,7 @@ import React from 'react';
import { Input } from '@grafana/ui';
import { useShadowedState } from '../../hooks/useShadowedState';
import { useShadowedState } from '../hooks/useShadowedState';
import { paddingRightClass } from './styles';

View File

@ -6,7 +6,7 @@ import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { AsyncSelect, InlineLabel, Input, Select } from '@grafana/ui';
import { useShadowedState } from '../../hooks/useShadowedState';
import { useShadowedState } from '../hooks/useShadowedState';
// this file is a simpler version of `grafana-ui / SegmentAsync.tsx`
// with some changes:

View File

@ -1,9 +1,7 @@
import { css } from '@emotion/css';
import React, { useId, useMemo } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2, TypedVariableModel } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { GrafanaTheme2 } from '@grafana/data';
import { InlineLabel, SegmentSection, useStyles2 } from '@grafana/ui';
import InfluxDatasource from '../../../../../datasource';
@ -25,7 +23,11 @@ import {
} from '../../../../../queryUtils';
import { InfluxQuery, InfluxQueryTag } from '../../../../../types';
import { DEFAULT_RESULT_FORMAT } from '../../../constants';
import { useRetentionPolicies } from '../hooks/useRetentionPolicies';
import { filterTags } from '../utils/filterTags';
import { getNewGroupByPartOptions, getNewSelectPartOptions, makePartList } from '../utils/partListUtils';
import { withTemplateVariableOptions } from '../utils/withTemplateVariableOptions';
import { wrapPure, wrapRegex } from '../utils/wrapper';
import { FormatAsSection } from './FormatAsSection';
import { FromSection } from './FromSection';
@ -41,43 +43,6 @@ type Props = {
datasource: InfluxDatasource;
};
function wrapRegex(v: TypedVariableModel): string {
return `/^$${v.name}$/`;
}
function wrapPure(v: TypedVariableModel): string {
return `$${v.name}`;
}
function getTemplateVariableOptions(wrapper: (v: TypedVariableModel) => string) {
return (
getTemplateSrv()
.getVariables()
// we make them regex-params, i'm not 100% sure why.
// probably because this way multi-value variables work ok too.
.map(wrapper)
);
}
// helper function to make it easy to call this from the widget-render-code
function withTemplateVariableOptions(
optionsPromise: Promise<string[]>,
wrapper: (v: TypedVariableModel) => string,
filter?: string
): Promise<string[]> {
let templateVariableOptions = getTemplateVariableOptions(wrapper);
if (filter) {
templateVariableOptions = templateVariableOptions.filter((tvo) => tvo.indexOf(filter) > -1);
}
return optionsPromise.then((options) => [...templateVariableOptions, ...options]);
}
// it is possible to add fields into the `InfluxQueryTag` structures, and they do work,
// but in some cases, when we do metadata queries, we have to remove them from the queries.
function filterTags(parts: InfluxQueryTag[], allTagKeys: Set<string>): InfluxQueryTag[] {
return parts.filter((t) => t.key.endsWith('::tag') || allTagKeys.has(t.key + '::tag'));
}
export const VisualInfluxQLEditor = (props: Props): JSX.Element => {
const uniqueId = useId();
const formatAsId = `influxdb-qe-format-as-${uniqueId}`;
@ -87,9 +52,7 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => {
const query = normalizeQuery(props.query);
const { datasource } = props;
const { measurement, policy } = query;
const policyData = useAsync(() => getAllPolicies(datasource), [datasource]);
const retentionPolicies = !!policyData.error ? [] : policyData.value ?? [];
const { retentionPolicies } = useRetentionPolicies(datasource);
const allTagKeys = useMemo(async () => {
const tagKeys = (await getTagKeysForMeasurementAndTags(datasource, [], measurement, policy)).map(

View File

@ -2,7 +2,7 @@ import { DataSourcePlugin } from '@grafana/data';
import ConfigEditor from './components/editor/config/ConfigEditor';
import { QueryEditor } from './components/editor/query/QueryEditor';
import InfluxStartPage from './components/editor/query/influxql/InfluxStartPage';
import { InfluxStartPage } from './components/editor/query/influxql/InfluxStartPage';
import VariableQueryEditor from './components/editor/variable/VariableQueryEditor';
import InfluxDatasource from './datasource';

View File

@ -58,6 +58,6 @@ export function getMockDSInstanceSettings(): DataSourceInstanceSettings<InfluxOp
module: '',
baseUrl: '',
},
jsonData: { version: InfluxVersion.InfluxQL, httpMode: 'POST' },
jsonData: { version: InfluxVersion.InfluxQL, httpMode: 'POST', dbName: 'site' },
};
}

View File

@ -17,7 +17,7 @@ import { LoadingState } from '@grafana/schema';
import { LokiDatasource } from './datasource';
import { splitTimeRange as splitLogsTimeRange } from './logsTimeSplitting';
import { splitTimeRange as splitMetricTimeRange } from './metricTimeSplitting';
import { isLogsQuery, isQueryWithDistinct } from './queryUtils';
import { isLogsQuery, isQueryWithDistinct, isQueryWithRangeVariable } from './queryUtils';
import { combineResponses } from './responseUtils';
import { trackGroupedQueries } from './tracking';
import { LokiGroupedRequest, LokiQuery, LokiQueryType } from './types';
@ -213,13 +213,19 @@ function getNextRequestPointers(requests: LokiGroupedRequest[], requestGroup: nu
};
}
function querySupporstSplitting(query: LokiQuery) {
return query.queryType !== LokiQueryType.Instant && !isQueryWithDistinct(query.expr);
function querySupportsSplitting(query: LokiQuery) {
return (
query.queryType !== LokiQueryType.Instant &&
!isQueryWithDistinct(query.expr) &&
// Queries with $__range variable should not be split because then the interpolated $__range variable is incorrect
// because it is interpolated on the backend with the split timeRange
!isQueryWithRangeVariable(query.expr)
);
}
export function runSplitQuery(datasource: LokiDatasource, request: DataQueryRequest<LokiQuery>) {
const queries = request.targets.filter((query) => !query.hide);
const [nonSplittingQueries, normalQueries] = partition(queries, (query) => !querySupporstSplitting(query));
const [nonSplittingQueries, normalQueries] = partition(queries, (query) => !querySupportsSplitting(query));
const [logQueries, metricQueries] = partition(normalQueries, (query) => isLogsQuery(query.expr));
request.queryGroupId = uuidv4();

View File

@ -10,6 +10,7 @@ import {
obfuscate,
requestSupportsSplitting,
isQueryWithDistinct,
isQueryWithRangeVariable,
} from './queryUtils';
import { LokiQuery, LokiQueryType } from './types';
@ -294,6 +295,28 @@ describe('isQueryWithDistinct', () => {
});
});
describe('isQueryWithRangeVariableDuration', () => {
it('identifies queries using $__range variable', () => {
expect(isQueryWithRangeVariable('rate({job="grafana"}[$__range])')).toBe(true);
});
it('identifies queries using $__range_s variable', () => {
expect(isQueryWithRangeVariable('rate({job="grafana"}[$__range_s])')).toBe(true);
});
it('identifies queries using $__range_ms variable', () => {
expect(isQueryWithRangeVariable('rate({job="grafana"}[$__range_ms])')).toBe(true);
});
it('does not return false positives', () => {
expect(isQueryWithRangeVariable('rate({job="grafana"} | logfmt | value="$__range" [5m])')).toBe(false);
expect(isQueryWithRangeVariable('rate({job="grafana"} | logfmt | value="[$__range]" [5m])')).toBe(false);
expect(isQueryWithRangeVariable('rate({job="grafana"} [$range])')).toBe(false);
expect(isQueryWithRangeVariable('rate({job="grafana"} [$_range])')).toBe(false);
expect(isQueryWithRangeVariable('rate({job="grafana"} [$_range_ms])')).toBe(false);
});
});
describe('getParserFromQuery', () => {
it('returns no parser', () => {
expect(getParserFromQuery('{job="grafana"}')).toBeUndefined();

View File

@ -18,6 +18,7 @@ import {
Matcher,
Identifier,
Distinct,
Range,
} from '@grafana/lezer-logql';
import { DataQuery } from '@grafana/schema';
@ -303,6 +304,22 @@ export function isQueryWithDistinct(query: string): boolean {
return hasDistinct;
}
export function isQueryWithRangeVariable(query: string): boolean {
let hasRangeVariableDuration = false;
const tree = parser.parse(query);
tree.iterate({
enter: ({ type, from, to }): false | void => {
if (type.id === Range) {
if (query.substring(from, to).match(/\[\$__range(_s|_ms)?/)) {
hasRangeVariableDuration = true;
return false;
}
}
},
});
return hasRangeVariableDuration;
}
export function getStreamSelectorsFromQuery(query: string): string[] {
const labelMatcherPositions = getStreamSelectorPositions(query);

View File

@ -1,4 +1,4 @@
import { DataFrameView, dateTime, createDataFrame } from '@grafana/data';
import { DataFrameView, dateTime, createDataFrame, FieldType } from '@grafana/data';
import { createGraphFrames, mapPromMetricsToServiceMap } from './graphTransform';
import { bigResponse } from './testResponse';
@ -59,6 +59,26 @@ describe('createGraphFrames', () => {
});
});
it('assigns correct field type even if values are numbers', async () => {
const range = {
from: dateTime('2000-01-01T00:00:00'),
to: dateTime('2000-01-01T00:01:00'),
};
const { nodes } = mapPromMetricsToServiceMap([{ data: [serverIsANumber, serverIsANumber] }], {
...range,
raw: range,
});
expect(nodes.fields).toMatchObject([
{ name: 'id', values: ['0', '1'], type: FieldType.string },
{ name: 'title', values: ['0', '1'], type: FieldType.string },
{ name: 'mainstat', values: [NaN, NaN], type: FieldType.number },
{ name: 'secondarystat', values: [10, 20], type: FieldType.number },
{ name: 'arc__success', values: [1, 1], type: FieldType.number },
{ name: 'arc__failed', values: [0, 0], type: FieldType.number },
]);
});
describe('mapPromMetricsToServiceMap', () => {
it('transforms prom metrics to service graph', async () => {
const range = {
@ -191,3 +211,16 @@ const invalidFailedPromMetric = createDataFrame({
{ name: 'Value #traces_service_graph_request_failed_total', values: [20, 40] },
],
});
const serverIsANumber = createDataFrame({
refId: 'traces_service_graph_request_total',
fields: [
{ name: 'Time', values: [1628169788000, 1628169788000] },
{ name: 'client', values: ['0', '1'] },
{ name: 'instance', values: ['127.0.0.1:12345', '127.0.0.1:12345'] },
{ name: 'job', values: ['local_scrape', 'local_scrape'] },
{ name: 'server', values: ['0', '1'] },
{ name: 'tempo_config', values: ['default', 'default'] },
{ name: 'Value #traces_service_graph_request_total', values: [10, 20] },
],
});

View File

@ -7,6 +7,7 @@ import {
MutableDataFrame,
NodeGraphDataFrameFieldNames as Fields,
TimeRange,
FieldType,
} from '@grafana/data';
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '../../../core/utils/tracing';
@ -188,28 +189,35 @@ function createServiceMapDataFrames() {
}
const nodes = createDF('Nodes', [
{ name: Fields.id },
{ name: Fields.title, config: { displayName: 'Service name' } },
{ name: Fields.mainStat, config: { unit: 'ms/r', displayName: 'Average response time' } },
{ name: Fields.id, type: FieldType.string },
{ name: Fields.title, type: FieldType.string, config: { displayName: 'Service name' } },
{ name: Fields.mainStat, type: FieldType.number, config: { unit: 'ms/r', displayName: 'Average response time' } },
{
name: Fields.secondaryStat,
type: FieldType.number,
config: { unit: 'r/sec', displayName: 'Requests per second' },
},
{
name: Fields.arc + 'success',
type: FieldType.number,
config: { displayName: 'Success', color: { fixedColor: 'green', mode: FieldColorModeId.Fixed } },
},
{
name: Fields.arc + 'failed',
type: FieldType.number,
config: { displayName: 'Failed', color: { fixedColor: 'red', mode: FieldColorModeId.Fixed } },
},
]);
const edges = createDF('Edges', [
{ name: Fields.id },
{ name: Fields.source },
{ name: Fields.target },
{ name: Fields.mainStat, config: { unit: 'ms/r', displayName: 'Average response time' } },
{ name: Fields.secondaryStat, config: { unit: 'r/sec', displayName: 'Requests per second' } },
{ name: Fields.id, type: FieldType.string },
{ name: Fields.source, type: FieldType.string },
{ name: Fields.target, type: FieldType.string },
{ name: Fields.mainStat, type: FieldType.number, config: { unit: 'ms/r', displayName: 'Average response time' } },
{
name: Fields.secondaryStat,
type: FieldType.number,
config: { unit: 'r/sec', displayName: 'Requests per second' },
},
]);
return [nodes, edges];

View File

@ -32,9 +32,12 @@ export const pointerMoveListener = (evt: MapBrowserEvent<MouseEvent>, panel: Geo
if (panel.state.measureMenuActive) {
return true;
}
if (!panel.map || panel.state.ttipOpen) {
// Eject out of this function if map is not loaded or valid tooltip is already open
if (!panel.map || (panel.state.ttipOpen && panel.state?.ttip?.layers?.length)) {
return false;
}
const mouse = evt.originalEvent;
const pixel = panel.map.getEventPixel(mouse);
const hover = toLonLat(panel.map.getCoordinateFromPixel(pixel));

View File

@ -108,7 +108,20 @@ export function prepareHeatmapData(
})!,
][0];
} else {
rowsHeatmap = frames[0];
let frame = frames[0];
let numberFields = frame.fields.filter((field) => field.type === FieldType.number);
let allNamesNumeric = numberFields.every((field) => !Number.isNaN(parseSampleValue(field.name)));
if (allNamesNumeric) {
numberFields.sort((a, b) => parseSampleValue(a.name) - parseSampleValue(b.name));
rowsHeatmap = {
...frame,
fields: [frame.fields.find((f) => f.type === FieldType.time)!, ...numberFields],
};
} else {
rowsHeatmap = frame;
}
}
}

View File

@ -1,4 +1,4 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import React, { useCallback, useMemo, useRef, useLayoutEffect, useState } from 'react';
import {
@ -42,7 +42,7 @@ export const LogsPanel = ({
id,
}: LogsPanelProps) => {
const isAscending = sortOrder === LogsSortOrder.Ascending;
const style = useStyles2(getStyles(title, isAscending));
const style = useStyles2(getStyles);
const [scrollTop, setScrollTop] = useState(0);
const logsContainerRef = useRef<HTMLDivElement>(null);
@ -95,7 +95,7 @@ export const LogsPanel = ({
}
const renderCommonLabels = () => (
<div className={style.labelContainer}>
<div className={cx(style.labelContainer, isAscending && style.labelContainerAscending)}>
<span className={style.label}>Common labels:</span>
<LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
</div>
@ -127,20 +127,21 @@ export const LogsPanel = ({
);
};
const getStyles = (title: string, isAscending: boolean) => (theme: GrafanaTheme2) => ({
container: css`
margin-bottom: ${theme.spacing(1.5)};
//We can remove this hot-fix when we fix panel menu with no title overflowing top of all panels
margin-top: ${theme.spacing(!title ? 2.5 : 0)};
`,
labelContainer: css`
margin: ${isAscending ? theme.spacing(0.5, 0, 0.5, 0) : theme.spacing(0, 0, 0.5, 0.5)};
display: flex;
align-items: center;
`,
label: css`
margin-right: ${theme.spacing(0.5)};
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
`,
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
marginBottom: theme.spacing(1.5),
}),
labelContainer: css({
margin: theme.spacing(0, 0, 0.5, 0.5),
display: 'flex',
alignItems: 'center',
}),
labelContainerAscending: css({
margin: theme.spacing(0.5, 0, 0.5, 0),
}),
label: css({
marginRight: theme.spacing(0.5),
fontSize: theme.typography.bodySmall.fontSize,
fontWeight: theme.typography.fontWeightMedium,
}),
});

View File

@ -408,8 +408,8 @@
"dashboard-actions": {
"import": "Import",
"new": "New",
"new-dashboard": "New Dashboard",
"new-folder": "New Folder"
"new-dashboard": "New dashboard",
"new-folder": "New folder"
},
"folder-view": {
"go-to-folder": "Go to folder",

View File

@ -408,8 +408,8 @@
"dashboard-actions": {
"import": "Ĩmpőřŧ",
"new": "Ńęŵ",
"new-dashboard": "Ńęŵ Đäşĥþőäřđ",
"new-folder": "Ńęŵ Főľđęř"
"new-dashboard": "Ńęŵ đäşĥþőäřđ",
"new-folder": "Ńęŵ ƒőľđęř"
},
"folder-view": {
"go-to-folder": "Ğő ŧő ƒőľđęř",

441
yarn.lock
View File

@ -42,6 +42,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/code-frame@npm:7.22.5"
dependencies:
"@babel/highlight": ^7.22.5
checksum: cfe804f518f53faaf9a1d3e0f9f74127ab9a004912c3a16fda07fb6a633393ecb9918a053cb71804204c1b7ec3d49e1699604715e2cfb0c9f7bc4933d324ebb6
languageName: node
linkType: hard
"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.21.4, @babel/compat-data@npm:^7.22.0, @babel/compat-data@npm:^7.22.3":
version: 7.22.3
resolution: "@babel/compat-data@npm:7.22.3"
@ -119,6 +128,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/generator@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/generator@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
"@jridgewell/gen-mapping": ^0.3.2
"@jridgewell/trace-mapping": ^0.3.17
jsesc: ^2.5.1
checksum: efa64da70ca88fe69f05520cf5feed6eba6d30a85d32237671488cc355fdc379fe2c3246382a861d49574c4c2f82a317584f8811e95eb024e365faff3232b49d
languageName: node
linkType: hard
"@babel/helper-annotate-as-pure@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-annotate-as-pure@npm:7.18.6"
@ -128,6 +149,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-annotate-as-pure@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-annotate-as-pure@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: 53da330f1835c46f26b7bf4da31f7a496dee9fd8696cca12366b94ba19d97421ce519a74a837f687749318f94d1a37f8d1abcbf35e8ed22c32d16373b2f6198d
languageName: node
linkType: hard
"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.18.6":
version: 7.18.9
resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.18.9"
@ -172,6 +202,25 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-create-class-features-plugin@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-create-class-features-plugin@npm:7.22.5"
dependencies:
"@babel/helper-annotate-as-pure": ^7.22.5
"@babel/helper-environment-visitor": ^7.22.5
"@babel/helper-function-name": ^7.22.5
"@babel/helper-member-expression-to-functions": ^7.22.5
"@babel/helper-optimise-call-expression": ^7.22.5
"@babel/helper-replace-supers": ^7.22.5
"@babel/helper-skip-transparent-expression-wrappers": ^7.22.5
"@babel/helper-split-export-declaration": ^7.22.5
semver: ^6.3.0
peerDependencies:
"@babel/core": ^7.0.0
checksum: f1e91deae06dbee6dd956c0346bca600adfbc7955427795d9d8825f0439a3c3290c789ba2b4a02a1cdf6c1a1bd163dfa16d3d5e96b02a8efb639d2a774e88ed9
languageName: node
linkType: hard
"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.1":
version: 7.22.1
resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.1"
@ -208,6 +257,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-environment-visitor@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-environment-visitor@npm:7.22.5"
checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1
languageName: node
linkType: hard
"@babel/helper-explode-assignable-expression@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-explode-assignable-expression@npm:7.18.6"
@ -227,6 +283,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-function-name@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-function-name@npm:7.22.5"
dependencies:
"@babel/template": ^7.22.5
"@babel/types": ^7.22.5
checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a
languageName: node
linkType: hard
"@babel/helper-hoist-variables@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-hoist-variables@npm:7.18.6"
@ -236,6 +302,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-hoist-variables@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-hoist-variables@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc
languageName: node
linkType: hard
"@babel/helper-member-expression-to-functions@npm:^7.0.0, @babel/helper-member-expression-to-functions@npm:^7.22.0":
version: 7.22.3
resolution: "@babel/helper-member-expression-to-functions@npm:7.22.3"
@ -245,6 +320,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-member-expression-to-functions@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-member-expression-to-functions@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: 4bd5791529c280c00743e8bdc669ef0d4cd1620d6e3d35e0d42b862f8262bc2364973e5968007f960780344c539a4b9cf92ab41f5b4f94560a9620f536de2a39
languageName: node
linkType: hard
"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-module-imports@npm:7.18.6"
@ -263,6 +347,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-module-imports@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-module-imports@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad
languageName: node
linkType: hard
"@babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.20.11, @babel/helper-module-transforms@npm:^7.21.2, @babel/helper-module-transforms@npm:^7.21.5, @babel/helper-module-transforms@npm:^7.22.1":
version: 7.22.1
resolution: "@babel/helper-module-transforms@npm:7.22.1"
@ -279,6 +372,22 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-module-transforms@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-module-transforms@npm:7.22.5"
dependencies:
"@babel/helper-environment-visitor": ^7.22.5
"@babel/helper-module-imports": ^7.22.5
"@babel/helper-simple-access": ^7.22.5
"@babel/helper-split-export-declaration": ^7.22.5
"@babel/helper-validator-identifier": ^7.22.5
"@babel/template": ^7.22.5
"@babel/traverse": ^7.22.5
"@babel/types": ^7.22.5
checksum: 8985dc0d971fd17c467e8b84fe0f50f3dd8610e33b6c86e5b3ca8e8859f9448bcc5c84e08a2a14285ef388351c0484797081c8f05a03770bf44fc27bf4900e68
languageName: node
linkType: hard
"@babel/helper-optimise-call-expression@npm:^7.0.0, @babel/helper-optimise-call-expression@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-optimise-call-expression@npm:7.18.6"
@ -288,6 +397,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-optimise-call-expression@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-optimise-call-expression@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: c70ef6cc6b6ed32eeeec4482127e8be5451d0e5282d5495d5d569d39eb04d7f1d66ec99b327f45d1d5842a9ad8c22d48567e93fc502003a47de78d122e355f7c
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3":
version: 7.20.2
resolution: "@babel/helper-plugin-utils@npm:7.20.2"
@ -302,6 +420,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-plugin-utils@npm:7.22.5"
checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5
languageName: node
linkType: hard
"@babel/helper-remap-async-to-generator@npm:^7.18.9":
version: 7.18.9
resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9"
@ -330,6 +455,20 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-replace-supers@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-replace-supers@npm:7.22.5"
dependencies:
"@babel/helper-environment-visitor": ^7.22.5
"@babel/helper-member-expression-to-functions": ^7.22.5
"@babel/helper-optimise-call-expression": ^7.22.5
"@babel/template": ^7.22.5
"@babel/traverse": ^7.22.5
"@babel/types": ^7.22.5
checksum: af29deff6c6dc3fa2d1a517390716aa3f4d329855e8689f1d5c3cb07c1b898e614a5e175f1826bb58e9ff1480e6552885a71a9a0ba5161787aaafa2c79b216cc
languageName: node
linkType: hard
"@babel/helper-simple-access@npm:^7.20.2":
version: 7.20.2
resolution: "@babel/helper-simple-access@npm:7.20.2"
@ -348,6 +487,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-simple-access@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-simple-access@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2
languageName: node
linkType: hard
"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0":
version: 7.20.0
resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0"
@ -357,6 +505,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: 1012ef2295eb12dc073f2b9edf3425661e9b8432a3387e62a8bc27c42963f1f216ab3124228015c748770b2257b4f1fda882ca8fa34c0bf485e929ae5bc45244
languageName: node
linkType: hard
"@babel/helper-split-export-declaration@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-split-export-declaration@npm:7.18.6"
@ -366,6 +523,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-split-export-declaration@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-split-export-declaration@npm:7.22.5"
dependencies:
"@babel/types": ^7.22.5
checksum: d10e05a02f49c1f7c578cea63d2ac55356501bbf58856d97ac9bfde4957faee21ae97c7f566aa309e38a256eef58b58e5b670a7f568b362c00e93dfffe072650
languageName: node
linkType: hard
"@babel/helper-string-parser@npm:^7.19.4":
version: 7.19.4
resolution: "@babel/helper-string-parser@npm:7.19.4"
@ -380,6 +546,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-string-parser@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-string-parser@npm:7.22.5"
checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1":
version: 7.19.1
resolution: "@babel/helper-validator-identifier@npm:7.19.1"
@ -387,6 +560,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-validator-identifier@npm:7.22.5"
checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea
languageName: node
linkType: hard
"@babel/helper-validator-option@npm:^7.21.0":
version: 7.21.0
resolution: "@babel/helper-validator-option@npm:7.21.0"
@ -394,6 +574,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-option@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-validator-option@npm:7.22.5"
checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3
languageName: node
linkType: hard
"@babel/helper-wrap-function@npm:^7.18.9":
version: 7.20.5
resolution: "@babel/helper-wrap-function@npm:7.20.5"
@ -428,6 +615,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/highlight@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/highlight@npm:7.22.5"
dependencies:
"@babel/helper-validator-identifier": ^7.22.5
chalk: ^2.0.0
js-tokens: ^4.0.0
checksum: f61ae6de6ee0ea8d9b5bcf2a532faec5ab0a1dc0f7c640e5047fc61630a0edb88b18d8c92eb06566d30da7a27db841aca11820ecd3ebe9ce514c9350fbed39c4
languageName: node
linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.9, @babel/parser@npm:^7.22.0, @babel/parser@npm:^7.22.4":
version: 7.22.4
resolution: "@babel/parser@npm:7.22.4"
@ -446,6 +644,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/parser@npm:7.22.5"
bin:
parser: ./bin/babel-parser.js
checksum: 470ebba516417ce8683b36e2eddd56dcfecb32c54b9bb507e28eb76b30d1c3e618fd0cfeee1f64d8357c2254514e1a19e32885cfb4e73149f4ae875436a6d59c
languageName: node
linkType: hard
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6"
@ -802,6 +1009,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-syntax-jsx@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/plugin-syntax-jsx@npm:7.22.5"
dependencies:
"@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce
languageName: node
linkType: hard
"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3":
version: 7.10.4
resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4"
@ -901,6 +1119,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-syntax-typescript@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/plugin-syntax-typescript@npm:7.22.5"
dependencies:
"@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a
languageName: node
linkType: hard
"@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6"
@ -1253,6 +1482,19 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-modules-commonjs@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5"
dependencies:
"@babel/helper-module-transforms": ^7.22.5
"@babel/helper-plugin-utils": ^7.22.5
"@babel/helper-simple-access": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa
languageName: node
linkType: hard
"@babel/plugin-transform-modules-systemjs@npm:^7.20.11, @babel/plugin-transform-modules-systemjs@npm:^7.22.3":
version: 7.22.3
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.3"
@ -1607,6 +1849,20 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-typescript@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/plugin-transform-typescript@npm:7.22.5"
dependencies:
"@babel/helper-annotate-as-pure": ^7.22.5
"@babel/helper-create-class-features-plugin": ^7.22.5
"@babel/helper-plugin-utils": ^7.22.5
"@babel/plugin-syntax-typescript": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: d12f1ca1ef1f2a54432eb044d2999705d1205ebe211c2a7f05b12e8eb2d2a461fd7657b5486b2f2f1efe7c0c0dc8e80725b767073d40fe4ae059a7af057b05e4
languageName: node
linkType: hard
"@babel/plugin-transform-unicode-escapes@npm:^7.18.10":
version: 7.18.10
resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.10"
@ -1894,18 +2150,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/preset-typescript@npm:7.21.5":
version: 7.21.5
resolution: "@babel/preset-typescript@npm:7.21.5"
"@babel/preset-typescript@npm:7.22.5":
version: 7.22.5
resolution: "@babel/preset-typescript@npm:7.22.5"
dependencies:
"@babel/helper-plugin-utils": ^7.21.5
"@babel/helper-validator-option": ^7.21.0
"@babel/plugin-syntax-jsx": ^7.21.4
"@babel/plugin-transform-modules-commonjs": ^7.21.5
"@babel/plugin-transform-typescript": ^7.21.3
"@babel/helper-plugin-utils": ^7.22.5
"@babel/helper-validator-option": ^7.22.5
"@babel/plugin-syntax-jsx": ^7.22.5
"@babel/plugin-transform-modules-commonjs": ^7.22.5
"@babel/plugin-transform-typescript": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: e7b35c435139eec1d6bd9f57e8f3eb79bfc2da2c57a34ad9e9ea848ba4ecd72791cf4102df456604ab07c7f4518525b0764754b6dd5898036608b351e0792448
checksum: 7be1670cb4404797d3a473bd72d66eb2b3e0f2f8a672a5e40bdb0812cc66085ec84bcd7b896709764cabf042fdc6b7f2d4755ac7cce10515eb596ff61dab5154
languageName: node
linkType: hard
@ -1976,6 +2232,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/template@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/template@npm:7.22.5"
dependencies:
"@babel/code-frame": ^7.22.5
"@babel/parser": ^7.22.5
"@babel/types": ^7.22.5
checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3
languageName: node
linkType: hard
"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.21.4, @babel/traverse@npm:~7.21.2":
version: 7.21.4
resolution: "@babel/traverse@npm:7.21.4"
@ -2012,6 +2279,24 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/traverse@npm:7.22.5"
dependencies:
"@babel/code-frame": ^7.22.5
"@babel/generator": ^7.22.5
"@babel/helper-environment-visitor": ^7.22.5
"@babel/helper-function-name": ^7.22.5
"@babel/helper-hoist-variables": ^7.22.5
"@babel/helper-split-export-declaration": ^7.22.5
"@babel/parser": ^7.22.5
"@babel/types": ^7.22.5
debug: ^4.1.0
globals: ^11.1.0
checksum: 560931422dc1761f2df723778dcb4e51ce0d02e560cf2caa49822921578f49189a5a7d053b78a32dca33e59be886a6b2200a6e24d4ae9b5086ca0ba803815694
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.5, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.5, @babel/types@npm:^7.22.0, @babel/types@npm:^7.22.3, @babel/types@npm:^7.22.4, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.22.4
resolution: "@babel/types@npm:7.22.4"
@ -2034,6 +2319,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/types@npm:7.22.5"
dependencies:
"@babel/helper-string-parser": ^7.22.5
"@babel/helper-validator-identifier": ^7.22.5
to-fast-properties: ^2.0.0
checksum: c13a9c1dc7d2d1a241a2f8363540cb9af1d66e978e8984b400a20c4f38ba38ca29f06e26a0f2d49a70bad9e57615dac09c35accfddf1bb90d23cd3e0a0bab892
languageName: node
linkType: hard
"@base2/pretty-print-object@npm:1.0.1":
version: 1.0.1
resolution: "@base2/pretty-print-object@npm:1.0.1"
@ -3463,25 +3759,26 @@ __metadata:
languageName: node
linkType: hard
"@grafana/faro-core@npm:1.0.2, @grafana/faro-core@npm:^1.0.2":
version: 1.0.2
resolution: "@grafana/faro-core@npm:1.0.2"
"@grafana/faro-core@npm:1.1.0, @grafana/faro-core@npm:^1.1.0":
version: 1.1.0
resolution: "@grafana/faro-core@npm:1.1.0"
dependencies:
"@opentelemetry/api": ^1.4.0
"@opentelemetry/api": ^1.4.1
"@opentelemetry/api-metrics": ^0.33.0
"@opentelemetry/otlp-transformer": ^0.35.0
checksum: 09c3c6687b955aacd17de73dca319dbb2abda8dd26f5c038618266ffefbc1f915cfe12167cd4b49db5c639ab70b7141bb9794c3cbdc682bb63174f95df9d9dfd
"@opentelemetry/otlp-transformer": ^0.37.0
murmurhash-js: ^1.0.0
checksum: 1e8504f3cab4ead385347002e265e6e7b32900704fa10dfe19ee72fe5bc50c9acb2fc6fc46f8e7d8efdc796fac9d06ba6d1b5a6512b47f09928c52df2a5fc3dd
languageName: node
linkType: hard
"@grafana/faro-web-sdk@npm:1.0.2":
version: 1.0.2
resolution: "@grafana/faro-web-sdk@npm:1.0.2"
"@grafana/faro-web-sdk@npm:1.1.0":
version: 1.1.0
resolution: "@grafana/faro-web-sdk@npm:1.1.0"
dependencies:
"@grafana/faro-core": ^1.0.2
"@grafana/faro-core": ^1.1.0
ua-parser-js: ^1.0.32
web-vitals: ^3.1.1
checksum: ca04add8469778d73e1bc15f057d063d93a88b9d76325c79060848ad3211fa929e30b6a85365981f7bcef023704f9f92f67678311bfed13dd521399c0f00e92f
checksum: e4fad2ff3c7d2d3cbc9e485e05a078a4ec0bc216907ed05361f91071611b7ce4256ba9cc8bbf6b23068efd02d32dfd42cfcd5d8a2b868c8ba006db27cd71c2a7
languageName: node
linkType: hard
@ -3521,7 +3818,7 @@ __metadata:
dependencies:
"@grafana/data": 10.1.0-pre
"@grafana/e2e-selectors": 10.1.0-pre
"@grafana/faro-web-sdk": 1.0.2
"@grafana/faro-web-sdk": 1.1.0
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 10.1.0-pre
"@rollup/plugin-commonjs": 23.0.2
@ -3624,7 +3921,7 @@ __metadata:
"@emotion/react": 11.10.6
"@grafana/data": 10.1.0-pre
"@grafana/e2e-selectors": 10.1.0-pre
"@grafana/faro-web-sdk": 1.0.2
"@grafana/faro-web-sdk": 1.1.0
"@grafana/schema": 10.1.0-pre
"@grafana/tsconfig": ^1.2.0-rc1
"@leeoniya/ufuzzy": 1.0.6
@ -5816,7 +6113,7 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/api@npm:1.4.0, @opentelemetry/api@npm:^1.4.0":
"@opentelemetry/api@npm:1.4.0":
version: 1.4.0
resolution: "@opentelemetry/api@npm:1.4.0"
checksum: 8dc522194e20d2e8aa6cac155dbce19d3fc9cfac59e953ece1064158c6348ccd9560ee99d2f2381e82c2f8c9a129b57fa7b640027383315504de1fa712b6d7f1
@ -5830,6 +6127,13 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/api@npm:^1.4.1":
version: 1.4.1
resolution: "@opentelemetry/api@npm:1.4.1"
checksum: e783c40d1a518abf9c4c5d65223237c1392cd9a6c53ac6e2c3ef0c05ff7266e3dfc4fd9874316dae0dcb7a97950878deb513bcbadfaad653d48f0215f2a0911b
languageName: node
linkType: hard
"@opentelemetry/core@npm:0.25.0":
version: 0.25.0
resolution: "@opentelemetry/core@npm:0.25.0"
@ -5842,14 +6146,14 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/core@npm:1.9.1":
version: 1.9.1
resolution: "@opentelemetry/core@npm:1.9.1"
"@opentelemetry/core@npm:1.11.0":
version: 1.11.0
resolution: "@opentelemetry/core@npm:1.11.0"
dependencies:
"@opentelemetry/semantic-conventions": 1.9.1
"@opentelemetry/semantic-conventions": 1.11.0
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.5.0"
checksum: 5581a809e2caff142136734634f45255ce9f1ed701cf38629b9e17d91a8d15449b467fb3a7f3d0d8b076f653090e50cc31d3b1db4cfefeda9b6b901c60581024
checksum: 866e4a85dde90d228e1822fc7238953d7353ddc1528c2936253a1c8dd47b260fe45551d9172cf97b6e34d851ee5204b391ebc66ab244e1994ac6d816e790c78e
languageName: node
linkType: hard
@ -5868,17 +6172,17 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/otlp-transformer@npm:^0.35.0":
version: 0.35.1
resolution: "@opentelemetry/otlp-transformer@npm:0.35.1"
"@opentelemetry/otlp-transformer@npm:^0.37.0":
version: 0.37.0
resolution: "@opentelemetry/otlp-transformer@npm:0.37.0"
dependencies:
"@opentelemetry/core": 1.9.1
"@opentelemetry/resources": 1.9.1
"@opentelemetry/sdk-metrics": 1.9.1
"@opentelemetry/sdk-trace-base": 1.9.1
"@opentelemetry/core": 1.11.0
"@opentelemetry/resources": 1.11.0
"@opentelemetry/sdk-metrics": 1.11.0
"@opentelemetry/sdk-trace-base": 1.11.0
peerDependencies:
"@opentelemetry/api": ">=1.3.0 <1.5.0"
checksum: e0a68b2be28d5535aaaa58be31a5e85b268b42025c8b3a34498f00b019fbd172530b1b484c4257fd330c5890007130eedec288b8fe8f2b85176198f9ffc2507e
checksum: 353a0f29d27a6b969d1458de7d829da06866bce58e9e24a9ae98d463056fecc1120a88660a9e1a72643e4a3e9de0fd731e54ded02c7abb99118cf7992229234d
languageName: node
linkType: hard
@ -5894,15 +6198,15 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/resources@npm:1.9.1":
version: 1.9.1
resolution: "@opentelemetry/resources@npm:1.9.1"
"@opentelemetry/resources@npm:1.11.0":
version: 1.11.0
resolution: "@opentelemetry/resources@npm:1.11.0"
dependencies:
"@opentelemetry/core": 1.9.1
"@opentelemetry/semantic-conventions": 1.9.1
"@opentelemetry/core": 1.11.0
"@opentelemetry/semantic-conventions": 1.11.0
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.5.0"
checksum: cf15e5faa698df3f0abcee35f7b4271c019b6cb81cb521b07793fe622c716d9c6873216219879afd57a28202f748a839ecaf28e04268e490004f14bbb850c96e
checksum: 3d4577d06f166d7efcf33ecc909115502d30cdf6bc53536c1672a8e76071447f3a01b2546159c7f44440c83549807e1d546ff1a0d4c169c8368013ed1d7d75f5
languageName: node
linkType: hard
@ -5920,16 +6224,16 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/sdk-metrics@npm:1.9.1":
version: 1.9.1
resolution: "@opentelemetry/sdk-metrics@npm:1.9.1"
"@opentelemetry/sdk-metrics@npm:1.11.0":
version: 1.11.0
resolution: "@opentelemetry/sdk-metrics@npm:1.11.0"
dependencies:
"@opentelemetry/core": 1.9.1
"@opentelemetry/resources": 1.9.1
"@opentelemetry/core": 1.11.0
"@opentelemetry/resources": 1.11.0
lodash.merge: 4.6.2
peerDependencies:
"@opentelemetry/api": ">=1.3.0 <1.5.0"
checksum: 08e8215841da74ffc36f2e5c414ab4230e5a2676a4329f9c98fbde4b162b6166612ab48bcc701cebf8ded3e2b5a68981f72987751df4393e0839089fa0c5ee13
checksum: 158ebcb79fc08f7e1530dcd2f0bfb9afbe2668396721df98fc7a7f5a75ceed4bceb7102aae0efcc24cd9bd5c92b51aa55a86f2cc904b538a45641ca0315b2e06
languageName: node
linkType: hard
@ -5947,16 +6251,16 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/sdk-trace-base@npm:1.9.1":
version: 1.9.1
resolution: "@opentelemetry/sdk-trace-base@npm:1.9.1"
"@opentelemetry/sdk-trace-base@npm:1.11.0":
version: 1.11.0
resolution: "@opentelemetry/sdk-trace-base@npm:1.11.0"
dependencies:
"@opentelemetry/core": 1.9.1
"@opentelemetry/resources": 1.9.1
"@opentelemetry/semantic-conventions": 1.9.1
"@opentelemetry/core": 1.11.0
"@opentelemetry/resources": 1.11.0
"@opentelemetry/semantic-conventions": 1.11.0
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.5.0"
checksum: f9448132686b1a8c1fde7539845a2b31bcb315c3bbabccb20a18142db80eeed433b3713e2761151348c1b626ad00183f4b7e9b9868d1a8ab8c541dce1d082f38
checksum: 64f00f8ede6f9fe5749021b820c412a78d984883dbe7db0ab2ed24fb3e2db0bcb9b928361cccb839b8a89007391fac0ad18386140394cc165716299b9b3b37e9
languageName: node
linkType: hard
@ -5967,6 +6271,13 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:1.11.0":
version: 1.11.0
resolution: "@opentelemetry/semantic-conventions@npm:1.11.0"
checksum: 310f2aad2a958b02ea2e661251e202989c662306e85a1c27bc99dec9c4d42fce5fc9b34bf21169276798acdae9d52af3b7ec19d8e5e9bb420fc145d0b901823c
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:1.14.0":
version: 1.14.0
resolution: "@opentelemetry/semantic-conventions@npm:1.14.0"
@ -5974,13 +6285,6 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:1.9.1":
version: 1.9.1
resolution: "@opentelemetry/semantic-conventions@npm:1.9.1"
checksum: 6217ba14b8f0068a3400f054c1f9d918b2e22d1cf8d31112baa712b8d196e207aed7421c6df37ed1403f7109f51c7834c230cbe180313eee07db6f7e0a7797bc
languageName: node
linkType: hard
"@parcel/watcher@npm:2.0.4":
version: 2.0.4
resolution: "@parcel/watcher@npm:2.0.4"
@ -18261,7 +18565,7 @@ __metadata:
"@babel/plugin-transform-typescript": 7.22.3
"@babel/preset-env": 7.22.4
"@babel/preset-react": 7.22.3
"@babel/preset-typescript": 7.21.5
"@babel/preset-typescript": 7.22.5
"@babel/runtime": 7.22.3
"@betterer/betterer": 5.4.0
"@betterer/cli": 5.4.0
@ -18279,8 +18583,8 @@ __metadata:
"@grafana/eslint-config": 5.1.0
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules"
"@grafana/experimental": 1.4.2
"@grafana/faro-core": 1.0.2
"@grafana/faro-web-sdk": 1.0.2
"@grafana/faro-core": 1.1.0
"@grafana/faro-web-sdk": 1.1.0
"@grafana/google-sdk": 0.1.1
"@grafana/lezer-logql": 0.1.5
"@grafana/monaco-logql": ^0.0.7
@ -23174,6 +23478,13 @@ __metadata:
languageName: node
linkType: hard
"murmurhash-js@npm:^1.0.0":
version: 1.0.0
resolution: "murmurhash-js@npm:1.0.0"
checksum: 083cea92a11bc9eb25be1446fc92eded3f49731bc1ad34fa8023afd68c234d1dd59458d70eb20e667b1383bedeeb8dfb1a16c89913b6ffe3584fd22fb598739d
languageName: node
linkType: hard
"mutationobserver-shim@npm:0.3.7":
version: 0.3.7
resolution: "mutationobserver-shim@npm:0.3.7"