mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/main' into alerting/notification-policies-preview-in-alert-creation
This commit is contained in:
commit
8c539da81b
@ -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"],
|
||||
|
63
CHANGELOG.md
63
CHANGELOG.md
@ -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
|
||||
|
||||
|
225
contribute/style-guides/testing.md
Normal file
225
contribute/style-guides/testing.md
Normal 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();
|
||||
});
|
||||
});
|
||||
```
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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" >}}).
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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/)
|
||||
|
||||
|
@ -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. |
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 ""
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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®ion=westus×pan=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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}}},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
48
public/app/core/components/Select/OrgPicker.test.tsx
Normal file
48
public/app/core/components/Select/OrgPicker.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
@ -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>;
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 =
|
||||
|
@ -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: {
|
||||
|
@ -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};
|
||||
|
@ -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}\}`;
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 />;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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']] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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'));
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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}`;
|
||||
}
|
@ -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';
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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' },
|
||||
};
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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] },
|
||||
],
|
||||
});
|
||||
|
@ -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];
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}),
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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
441
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user