Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2024-11-14 11:37:47 +03:00
commit b88f42ca82
13 files changed
+122 -68

No files matched your search

@@ -1,4 +1,4 @@
import { stringToJsRegex } from '../../text/string';
import { stringStartsAsRegEx, stringToJsRegex } from '../../text/string';
import { DataFrame } from '../../types/dataFrame';
import { FrameMatcherInfo } from '../../types/transformations';
@@ -12,9 +12,20 @@ const refIdMatcher: FrameMatcherInfo<string> = {
defaultOptions: 'A',
get: (pattern: string) => {
const regex = stringToJsRegex(pattern);
let regex: RegExp | null = null;
if (stringStartsAsRegEx(pattern)) {
try {
regex = stringToJsRegex(pattern);
} catch (error) {
if (error instanceof Error) {
console.warn(error.message);
}
}
}
return (frame: DataFrame) => {
return regex.test(frame.refId || '');
return regex?.test(frame.refId || '') ?? frame.refId === pattern;
};
},
@@ -42,7 +42,7 @@ describe('filterByRefId transformer', () => {
const cfg = {
id: DataTransformerID.filterByRefId,
options: {
include: 'A|B',
include: '/A|B/',
},
};
+10 -37
View File
@@ -234,13 +234,9 @@ func (s *QueryData) fetch(traceCtx context.Context, client *client.Client, q *mo
func (s *QueryData) rangeQuery(ctx context.Context, c *client.Client, q *models.Query, enablePrometheusDataplaneFlag bool) backend.DataResponse {
res, err := c.QueryRange(ctx, q)
if err != nil {
return addErrorSourceToDataResponse(err)
}
if res.StatusCode/100 != 2 {
// for differentiating the source of http errors by status code
return backend.DataResponse{
ErrorSource: backend.ErrorSourceFromHTTPStatus(res.StatusCode),
Error: err,
Status: backend.StatusBadGateway,
}
}
@@ -257,22 +253,16 @@ func (s *QueryData) rangeQuery(ctx context.Context, c *client.Client, q *models.
func (s *QueryData) instantQuery(ctx context.Context, c *client.Client, q *models.Query, enablePrometheusDataplaneFlag bool) backend.DataResponse {
res, err := c.QueryInstant(ctx, q)
if err != nil {
// confirm that it is a downstream error
return addErrorSourceToDataResponse(err)
return backend.DataResponse{
Error: err,
Status: backend.StatusBadGateway,
}
}
if res.StatusCode/100 != 2 {
// This is only for health check fall back scenario
// add more details to the health check error
if q.RefId == "__healthcheck__" {
return backend.DataResponse{
Error: errors.New(res.Status),
ErrorSource: backend.ErrorSourceFromHTTPStatus(res.StatusCode),
}
}
// for differentiating the source of http errors by status code
// This is only for health check fall back scenario
if res.StatusCode != 200 && q.RefId == "__healthcheck__" {
return backend.DataResponse{
ErrorSource: backend.ErrorSourceFromHTTPStatus(res.StatusCode),
Error: errors.New(res.Status),
}
}
@@ -289,13 +279,8 @@ func (s *QueryData) instantQuery(ctx context.Context, c *client.Client, q *model
func (s *QueryData) exemplarQuery(ctx context.Context, c *client.Client, q *models.Query, enablePrometheusDataplaneFlag bool) backend.DataResponse {
res, err := c.QueryExemplars(ctx, q)
if err != nil {
return addErrorSourceToDataResponse(err)
}
if res.StatusCode/100 != 2 {
// for differentiating the source of http errors by status code
return backend.DataResponse{
ErrorSource: backend.ErrorSourceFromHTTPStatus(res.StatusCode),
Error: err,
}
}
@@ -319,15 +304,3 @@ func addDataResponse(res *backend.DataResponse, dr *backend.DataResponse) {
}
dr.Frames = append(dr.Frames, res.Frames...)
}
func addErrorSourceToDataResponse(err error) backend.DataResponse {
response := backend.DataResponse{
Error: err,
Status: backend.StatusBadGateway,
}
if backend.IsDownstreamHTTPError(err) {
response.ErrorSource = backend.ErrorSourceDownstream
}
return response
}
+5 -1
View File
@@ -435,12 +435,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
)
}
if hasAccess(ac.EvalAny(contactPointsPerms...)) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications",
Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share",
})
}
+3
View File
@@ -4,6 +4,7 @@ import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/type
import { AccessControlAction } from 'app/types';
import { PERMISSIONS_CONTACT_POINTS } from './unified/components/contact-points/permissions';
import { PERMISSIONS_TEMPLATES } from './unified/components/templates/permissions';
import { evaluateAccess } from './unified/utils/access-control';
export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
@@ -104,6 +105,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
...PERMISSIONS_CONTACT_POINTS,
...PERMISSIONS_TEMPLATES,
]),
component: importAlertingComponent(
() =>
@@ -150,6 +152,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
roles: evaluateAccess([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
...PERMISSIONS_TEMPLATES,
]),
component: importAlertingComponent(
() => import(/* webpackChunkName: "Templates" */ 'app/features/alerting/unified/Templates')
@@ -164,31 +164,39 @@ const NotificationTemplatesTab = () => {
);
};
const useTabQueryParam = () => {
const useTabQueryParam = (defaultTab: ActiveTab) => {
const [queryParams, setQueryParams] = useURLSearchParams();
const param = useMemo(() => {
const queryParam = queryParams.get('tab');
if (!queryParam || !Object.values(ActiveTab).map(String).includes(queryParam)) {
return ActiveTab.ContactPoints;
return defaultTab;
}
return queryParam || ActiveTab.ContactPoints;
}, [queryParams]);
return queryParam || defaultTab;
}, [defaultTab, queryParams]);
const setParam = (tab: ActiveTab) => setQueryParams({ tab });
return [param, setParam] as const;
};
export const ContactPointsPageContents = () => {
const { selectedAlertmanager } = useAlertmanager();
const [activeTab, setActiveTab] = useTabQueryParam();
const [, showContactPointsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
const [, showTemplatesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationTemplate);
// Depending on permissions, user may not have access to all tabs,
// but we can default to picking the first one that they definitely _do_ have access to
const defaultTab = [
showContactPointsTab && ActiveTab.ContactPoints,
showTemplatesTab && ActiveTab.NotificationTemplates,
].filter((tab) => !!tab)[0];
const [activeTab, setActiveTab] = useTabQueryParam(defaultTab);
const { contactPoints } = useContactPointsWithStatus({
alertmanager: selectedAlertmanager!,
});
const [_, showTemplatesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationTemplate);
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
@@ -198,12 +206,14 @@ export const ContactPointsPageContents = () => {
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} />
<Stack direction="column">
<TabsBar>
<Tab
label="Contact Points"
active={showingContactPoints}
counter={contactPoints.length}
onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)}
/>
{showContactPointsTab && (
<Tab
label="Contact Points"
active={showingContactPoints}
counter={contactPoints.length}
onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)}
/>
)}
{showTemplatesTab && (
<Tab
label="Notification Templates"
@@ -17,9 +17,5 @@ export const PERMISSIONS_CONTACT_POINTS_MODIFY = [
/**
* List of all permissions that allow contact points read/write functionality
*
* Any permission in this list will also be checked for whether the built-in Grafana Alertmanager is shown
* (as the implication is that if they have one of these permissions, then they should be able to see Grafana AM in the AM selector)
*/
export const PERMISSIONS_CONTACT_POINTS = [...PERMISSIONS_CONTACT_POINTS_READ, ...PERMISSIONS_CONTACT_POINTS_MODIFY];
@@ -166,7 +166,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
/>
</Authorize>
)}
<Authorize actions={[AlertmanagerAction.CreateContactPoint]}>
<Authorize actions={[AlertmanagerAction.CreateNotificationTemplate]}>
<ActionIcon
to={makeAMLink(
`/alerting/notifications/templates/${encodeURIComponent(uid)}/duplicate`,
@@ -1,5 +1,8 @@
import 'core-js/stable/structured-clone';
import { MemoryHistoryBuildOptions } from 'history';
import { ComponentProps, ReactNode } from 'react';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { screen, waitFor } from 'test/test-utils';
import { render, screen, waitFor } from 'test/test-utils';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { config } from '@grafana/runtime';
@@ -8,6 +11,7 @@ import {
setOnCallFeatures,
setOnCallIntegrations,
} from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
@@ -15,10 +19,21 @@ import { AccessControlAction } from 'app/types';
import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi';
import { grantUserPermissions } from '../../../mocks';
import { captureRequests } from '../../../mocks/server/events';
import { renderWithProvider } from '../../contact-points/ContactPoints.test';
import { GrafanaReceiverForm } from './GrafanaReceiverForm';
const renderWithProvider = (
children: ReactNode,
historyOptions?: MemoryHistoryBuildOptions,
providerProps?: Partial<ComponentProps<typeof AlertmanagerProvider>>
) =>
render(
<AlertmanagerProvider accessType="notification" {...providerProps}>
{children}
</AlertmanagerProvider>,
{ historyOptions }
);
setupMswServer();
const ui = {
@@ -0,0 +1,11 @@
import { AccessControlAction } from 'app/types';
/**
* List of all permissions that allow templates read/write functionality
*/
export const PERMISSIONS_TEMPLATES = [
AccessControlAction.AlertingTemplatesRead,
AccessControlAction.AlertingTemplatesWrite,
AccessControlAction.AlertingTemplatesDelete,
];
@@ -231,17 +231,41 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
// TODO: Move this into the permissions config and generalise that code to allow for an array of permissions
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversCreate : null
),
[AlertmanagerAction.ViewContactPoint]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.DeleteContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
[AlertmanagerAction.ViewContactPoint]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversRead : null
),
[AlertmanagerAction.UpdateContactPoint]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
),
[AlertmanagerAction.DeleteContactPoint]: toAbility(
hasConfigurationAPI,
notificationsPermissions.delete,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
),
// At the time of writing, only Grafana flavored alertmanager supports exporting,
// and if a user can view the contact point, then they can also export it
// So the only check we make is if the alertmanager is Grafana flavored
[AlertmanagerAction.ExportContactPoint]: [isGrafanaFlavoredAlertmanager, isGrafanaFlavoredAlertmanager],
// -- notification templates --
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(
hasConfigurationAPI,
notificationsPermissions.create,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
),
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesRead : null
),
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
),
[AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
// -- notification policies --
[AlertmanagerAction.CreateNotificationPolicy]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
@@ -12,6 +12,7 @@ import { PromApplication, RulesSourceApplication } from 'app/types/unified-alert
import { alertmanagerApi } from '../api/alertmanagerApi';
import { PERMISSIONS_CONTACT_POINTS } from '../components/contact-points/permissions';
import { PERMISSIONS_TEMPLATES } from '../components/templates/permissions';
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
import { isAlertManagerWithConfigAPI } from '../state/AlertmanagerContext';
@@ -151,6 +152,7 @@ export function getAlertManagerDataSourcesByPermission(permission: 'instance' |
const builtinAlertmanagerPermissions = [
...Object.values(permissions).flatMap((permissions) => permissions.grafana),
...PERMISSIONS_CONTACT_POINTS,
...PERMISSIONS_TEMPLATES,
];
const hasPermissionsForInternalAlertmanager = builtinAlertmanagerPermissions.some((permission) =>
+5
View File
@@ -134,6 +134,11 @@ export enum AccessControlAction {
AlertingReceiversWrite = 'alert.notifications.receivers:write',
AlertingReceiversRead = 'alert.notifications.receivers:read',
// Alerting templates actions
AlertingTemplatesRead = 'alert.notifications.templates:read',
AlertingTemplatesWrite = 'alert.notifications.templates:write',
AlertingTemplatesDelete = 'alert.notifications.templates:delete',
ActionAPIKeysRead = 'apikeys:read',
ActionAPIKeysCreate = 'apikeys:create',
ActionAPIKeysDelete = 'apikeys:delete',