mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Declare incident from a firing alert (#61178)
This commit is contained in:
@@ -6,7 +6,8 @@ import { Provider } from 'react-redux';
|
|||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
|
|
||||||
import { locationService, setDataSourceSrv, logInfo } from '@grafana/runtime';
|
import { locationService, setDataSourceSrv, logInfo, setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||||
import * as actions from 'app/features/alerting/unified/state/actions';
|
import * as actions from 'app/features/alerting/unified/state/actions';
|
||||||
@@ -133,6 +134,10 @@ const ui = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
|
});
|
||||||
|
|
||||||
describe('RuleList', () => {
|
describe('RuleList', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
contextSrv.isEditor = true;
|
contextSrv.isEditor = true;
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { Provider } from 'react-redux';
|
|||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { byRole } from 'testing-library-selector';
|
import { byRole } from 'testing-library-selector';
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService, setBackendSrv } from '@grafana/runtime';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
@@ -70,6 +71,10 @@ const mocks = {
|
|||||||
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
|
});
|
||||||
|
|
||||||
describe('RuleViewer', () => {
|
describe('RuleViewer', () => {
|
||||||
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { rest } from 'msw';
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
|
||||||
|
// bit of setup to mock HTTP request responses
|
||||||
|
import 'whatwg-fetch';
|
||||||
|
import { SupportedPlugin } from './PluginBridge';
|
||||||
|
|
||||||
|
export const NON_EXISTING_PLUGIN = '__does_not_exist__';
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
rest.get(`/api/plugins/${NON_EXISTING_PLUGIN}/settings`, async (_req, res, ctx) => res(ctx.status(404))),
|
||||||
|
rest.get(`/api/plugins/${SupportedPlugin.Incident}/settings`, async (_req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.json({
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export { server };
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { screen, render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
|
import { createBridgeURL, PluginBridge, SupportedPlugin } from './PluginBridge';
|
||||||
|
import { server, NON_EXISTING_PLUGIN } from './PluginBridge.mock';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
|
server.listen({ onUnhandledRequest: 'error' });
|
||||||
|
});
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
|
describe('createBridgeURL', () => {
|
||||||
|
it('should work with path', () => {
|
||||||
|
expect(createBridgeURL(SupportedPlugin.Incident, '/incidents/declare')).toBe(
|
||||||
|
'/a/grafana-incident-app/incidents/declare'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with path and options', () => {
|
||||||
|
expect(createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title: 'My Incident' })).toBe(
|
||||||
|
'/a/grafana-incident-app/incidents/declare?title=My+Incident'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<PluginBridge />', () => {
|
||||||
|
it('should render notInstalled component', async () => {
|
||||||
|
render(<PluginBridge plugin={NON_EXISTING_PLUGIN} notInstalledFallback={<div>plugin not installed</div>} />);
|
||||||
|
expect(await screen.findByText('plugin not installed')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render loading and installed component', async () => {
|
||||||
|
render(
|
||||||
|
<PluginBridge plugin={SupportedPlugin.Incident} loadingComponent={<>Loading...</>}>
|
||||||
|
Plugin installed!
|
||||||
|
</PluginBridge>
|
||||||
|
);
|
||||||
|
expect(await screen.findByText('Loading...')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('Plugin installed!')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React, { FC, ReactElement } from 'react';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
|
import { PluginMeta } from '@grafana/data';
|
||||||
|
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||||
|
|
||||||
|
export enum SupportedPlugin {
|
||||||
|
Incident = 'grafana-incident-app',
|
||||||
|
OnCall = 'grafana-oncall-app',
|
||||||
|
MachineLearning = 'grafana-ml-app',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginID = SupportedPlugin | string;
|
||||||
|
|
||||||
|
export interface PluginBridgeProps {
|
||||||
|
plugin: PluginID;
|
||||||
|
// shows an optional component when the plugin is not installed
|
||||||
|
notInstalledFallback?: ReactElement;
|
||||||
|
// shows an optional component when we're checking if the plugin is installed
|
||||||
|
loadingComponent?: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginBridgeHookResponse {
|
||||||
|
loading: boolean;
|
||||||
|
installed?: boolean;
|
||||||
|
error?: Error;
|
||||||
|
settings?: PluginMeta<{}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PluginBridge: FC<PluginBridgeProps> = ({ children, plugin, loadingComponent, notInstalledFallback }) => {
|
||||||
|
const { loading, installed } = usePluginBridge(plugin);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return loadingComponent ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installed) {
|
||||||
|
return notInstalledFallback ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function usePluginBridge(plugin: PluginID): PluginBridgeHookResponse {
|
||||||
|
const { loading, error, value } = useAsync(() => getPluginSettings(plugin, { showErrorAlert: false }));
|
||||||
|
|
||||||
|
const installed = value && !error && !loading;
|
||||||
|
const enabled = value?.enabled;
|
||||||
|
const isLoading = loading && !value;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return { loading: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installed || !enabled) {
|
||||||
|
return { loading: false, installed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loading, installed: true, settings: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBridgeURL(plugin: PluginID, path?: string, options?: Record<string, string>) {
|
||||||
|
const searchParams = new URLSearchParams(options).toString();
|
||||||
|
const pluginPath = `/a/${plugin}${path}`;
|
||||||
|
|
||||||
|
return pluginPath + (searchParams ? '?' + searchParams : '');
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Button, Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { createBridgeURL, usePluginBridge, SupportedPlugin } from '../PluginBridge';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
severity?: 'minor' | 'major' | 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeclareIncident: FC<Props> = ({ title = '', severity = '' }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const bridgeURL = createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title, severity });
|
||||||
|
|
||||||
|
const { loading, installed, settings } = usePluginBridge(SupportedPlugin.Incident);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading === true && (
|
||||||
|
<Button size="sm" type="button" disabled>
|
||||||
|
Declare Incident
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{installed === false && (
|
||||||
|
<Tooltip content={'Grafana Incident is not installed or is not configured correctly'}>
|
||||||
|
<Button size="sm" type="button" disabled>
|
||||||
|
Declare Incident
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{settings && (
|
||||||
|
<Button size="sm" type="button" onClick={() => history.push(bridgeURL)}>
|
||||||
|
Declare Incident
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { byRole } from 'testing-library-selector';
|
import { byRole } from 'testing-library-selector';
|
||||||
|
|
||||||
|
import { setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
@@ -30,7 +32,8 @@ const ui = {
|
|||||||
|
|
||||||
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
|
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeAll(() => {
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +41,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
describe('Grafana rules action buttons in details', () => {
|
describe('Grafana rules action buttons in details', () => {
|
||||||
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
|
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
|
||||||
|
|
||||||
it('Should not render Edit button for users with the update permission', () => {
|
it('Should not render Edit button for users with the update permission', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||||
|
|
||||||
@@ -47,9 +50,10 @@ describe('RuleDetails RBAC', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||||
|
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not render Delete button for users with the delete permission', () => {
|
it('Should not render Delete button for users with the delete permission', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
||||||
|
|
||||||
@@ -58,9 +62,10 @@ describe('RuleDetails RBAC', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
||||||
|
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not render Silence button for users wihout the instance create permission', () => {
|
it('Should not render Silence button for users wihout the instance create permission', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||||
|
|
||||||
@@ -69,9 +74,10 @@ describe('RuleDetails RBAC', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
|
expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
|
||||||
|
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should render Silence button for users with the instance create permissions', () => {
|
it('Should render Silence button for users with the instance create permissions', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
jest
|
jest
|
||||||
.spyOn(contextSrv, 'hasPermission')
|
.spyOn(contextSrv, 'hasPermission')
|
||||||
@@ -81,13 +87,14 @@ describe('RuleDetails RBAC', () => {
|
|||||||
renderRuleDetails(grafanaRule);
|
renderRuleDetails(grafanaRule);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
|
expect(await ui.actionButtons.silence.find()).toBeInTheDocument();
|
||||||
|
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Cloud rules action buttons', () => {
|
describe('Cloud rules action buttons', () => {
|
||||||
const cloudRule = getCloudRule({ name: 'Cloud' });
|
const cloudRule = getCloudRule({ name: 'Cloud' });
|
||||||
|
|
||||||
it('Should not render Edit button for users with the update permission', () => {
|
it('Should not render Edit button for users with the update permission', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||||
|
|
||||||
@@ -96,9 +103,10 @@ describe('RuleDetails RBAC', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||||
|
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not render Delete button for users with the delete permission', () => {
|
it('Should not render Delete button for users with the delete permission', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
||||||
|
|
||||||
@@ -107,6 +115,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
||||||
|
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useAppNotification } from 'app/core/copy/appNotification';
|
|||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AccessControlAction, useDispatch } from 'app/types';
|
import { AccessControlAction, useDispatch } from 'app/types';
|
||||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||||
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||||
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
||||||
@@ -18,7 +19,8 @@ import { Annotation } from '../../utils/constants';
|
|||||||
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
import { createExploreLink, makeRuleBasedSilenceLink } from '../../utils/misc';
|
import { createExploreLink, makeRuleBasedSilenceLink } from '../../utils/misc';
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
|
import { DeclareIncident } from '../bridges/DeclareIncidentButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rule: CombinedRule;
|
rule: CombinedRule;
|
||||||
@@ -74,6 +76,8 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
const rulesSourceName = getRulesSourceName(rulesSource);
|
const rulesSourceName = getRulesSourceName(rulesSource);
|
||||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||||
|
|
||||||
|
const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing;
|
||||||
|
|
||||||
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
|
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
|
||||||
|
|
||||||
const returnTo = location.pathname + location.search;
|
const returnTo = location.pathname + location.search;
|
||||||
@@ -82,8 +86,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
|
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
size="sm"
|
||||||
size="xs"
|
|
||||||
key="explore"
|
key="explore"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
icon="chart-line"
|
icon="chart-line"
|
||||||
@@ -97,8 +100,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (rule.annotations[Annotation.runbookURL]) {
|
if (rule.annotations[Annotation.runbookURL]) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
size="sm"
|
||||||
size="xs"
|
|
||||||
key="runbook"
|
key="runbook"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
icon="book"
|
icon="book"
|
||||||
@@ -114,8 +116,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (dashboardUID) {
|
if (dashboardUID) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
size="sm"
|
||||||
size="xs"
|
|
||||||
key="dashboard"
|
key="dashboard"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
icon="apps"
|
icon="apps"
|
||||||
@@ -129,8 +130,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (panelId) {
|
if (panelId) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
size="sm"
|
||||||
size="xs"
|
|
||||||
key="panel"
|
key="panel"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
icon="apps"
|
icon="apps"
|
||||||
@@ -147,8 +147,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) {
|
if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
size="sm"
|
||||||
size="xs"
|
|
||||||
key="silence"
|
key="silence"
|
||||||
icon="bell-slash"
|
icon="bell-slash"
|
||||||
target="__blank"
|
target="__blank"
|
||||||
@@ -162,7 +161,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (alertId) {
|
if (alertId) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Fragment key="history">
|
<Fragment key="history">
|
||||||
<Button className={style.button} size="xs" icon="history" onClick={() => showStateHistoryModal()}>
|
<Button size="sm" icon="history" onClick={() => showStateHistoryModal()}>
|
||||||
Show state history
|
Show state history
|
||||||
</Button>
|
</Button>
|
||||||
{StateHistoryModal}
|
{StateHistoryModal}
|
||||||
@@ -170,6 +169,14 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFiringRule) {
|
||||||
|
buttons.push(
|
||||||
|
<Fragment key="declare-incident">
|
||||||
|
<DeclareIncident title={rule.name} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isViewMode) {
|
if (isViewMode) {
|
||||||
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
|
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
|
||||||
const sourceName = getRulesSourceName(rulesSource);
|
const sourceName = getRulesSourceName(rulesSource);
|
||||||
@@ -188,7 +195,6 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
onClipboardError={(copiedText) => {
|
onClipboardError={(copiedText) => {
|
||||||
notifyApp.error('Error while copying URL', copiedText);
|
notifyApp.error('Error while copying URL', copiedText);
|
||||||
}}
|
}}
|
||||||
className={style.button}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
getText={buildShareUrl}
|
getText={buildShareUrl}
|
||||||
>
|
>
|
||||||
@@ -197,7 +203,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
);
|
);
|
||||||
|
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
|
<LinkButton size="sm" key="edit" variant="secondary" icon="pen" href={editURL}>
|
||||||
Edit
|
Edit
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
);
|
);
|
||||||
@@ -206,8 +212,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
|
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<Button
|
<Button
|
||||||
className={style.button}
|
size="sm"
|
||||||
size="xs"
|
|
||||||
type="button"
|
type="button"
|
||||||
key="delete"
|
key="delete"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -253,8 +258,4 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
border-bottom: solid 1px ${theme.colors.border.medium};
|
border-bottom: solid 1px ${theme.colors.border.medium};
|
||||||
`,
|
`,
|
||||||
button: css`
|
|
||||||
height: 24px;
|
|
||||||
font-size: ${theme.typography.size.sm};
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('PluginSettings', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(response).toEqual(testPluginResponse);
|
expect(response).toEqual(testPluginResponse);
|
||||||
expect(getRequestSpy).toHaveBeenCalledTimes(1);
|
expect(getRequestSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(getRequestSpy).toHaveBeenCalledWith('/api/plugins/test/settings');
|
expect(getRequestSpy).toHaveBeenCalledWith('/api/plugins/test/settings', undefined, undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch settings from cache when it has a hit', async () => {
|
it('should fetch settings from cache when it has a hit', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PluginMeta } from '@grafana/data';
|
import { PluginMeta } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
type PluginCache = {
|
type PluginCache = {
|
||||||
[key: string]: PluginMeta;
|
[key: string]: PluginMeta;
|
||||||
@@ -7,13 +7,13 @@ type PluginCache = {
|
|||||||
|
|
||||||
const pluginInfoCache: PluginCache = {};
|
const pluginInfoCache: PluginCache = {};
|
||||||
|
|
||||||
export function getPluginSettings(pluginId: string): Promise<PluginMeta> {
|
export function getPluginSettings(pluginId: string, options?: Partial<BackendSrvRequest>): Promise<PluginMeta> {
|
||||||
const v = pluginInfoCache[pluginId];
|
const v = pluginInfoCache[pluginId];
|
||||||
if (v) {
|
if (v) {
|
||||||
return Promise.resolve(v);
|
return Promise.resolve(v);
|
||||||
}
|
}
|
||||||
return getBackendSrv()
|
return getBackendSrv()
|
||||||
.get(`/api/plugins/${pluginId}/settings`)
|
.get(`/api/plugins/${pluginId}/settings`, undefined, undefined, options)
|
||||||
.then((settings: any) => {
|
.then((settings: any) => {
|
||||||
pluginInfoCache[pluginId] = settings;
|
pluginInfoCache[pluginId] = settings;
|
||||||
return settings;
|
return settings;
|
||||||
|
|||||||
Reference in New Issue
Block a user