mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 09:42:13 +08:00
Alerting: mocking and testing of Alerting package (#105342)
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -440,6 +440,7 @@
|
|||||||
/packages/grafana-ui/src/utils/storybook/ @grafana/grafana-frontend-platform
|
/packages/grafana-ui/src/utils/storybook/ @grafana/grafana-frontend-platform
|
||||||
/packages/grafana-alerting/ @grafana/alerting-frontend
|
/packages/grafana-alerting/ @grafana/alerting-frontend
|
||||||
/packages/grafana-i18n/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
/packages/grafana-i18n/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
||||||
|
/packages/grafana-test-utils @grafana/grafana-frontend-platform
|
||||||
|
|
||||||
# root files, mostly frontend
|
# root files, mostly frontend
|
||||||
/.browserslistrc @grafana/frontend-ops
|
/.browserslistrc @grafana/frontend-ops
|
||||||
|
@ -40,24 +40,30 @@
|
|||||||
"codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts"
|
"codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^9.7.0",
|
||||||
|
"@grafana/test-utils": "workspace:*",
|
||||||
"@grafana/tsconfig": "^2.0.0",
|
"@grafana/tsconfig": "^2.0.0",
|
||||||
"@rtk-query/codegen-openapi": "^2.0.0",
|
"@rtk-query/codegen-openapi": "^2.0.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/lodash": "^4",
|
"@types/lodash": "^4",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.18",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"type-fest": "^4.40.0",
|
"type-fest": "^4.40.0",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@grafana/runtime": "^12.0.0-pre",
|
"@grafana/runtime": "^12.0.0-pre",
|
||||||
"@grafana/ui": "^12.0.0-pre",
|
"@grafana/ui": "^12.0.0-pre",
|
||||||
|
"@reduxjs/toolkit": "^2.8.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.8.0",
|
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
|||||||
|
|
||||||
// ℹ️ append versions here to generate additional API clients
|
// ℹ️ append versions here to generate additional API clients
|
||||||
const VERSIONS = ['v0alpha1'] as const;
|
const VERSIONS = ['v0alpha1'] as const;
|
||||||
|
const GROUP = 'notifications.alerting.grafana.app' as const;
|
||||||
|
|
||||||
type OutputFile = Omit<ConfigFile, 'outputFile'>;
|
type OutputFile = Omit<ConfigFile, 'outputFile'>;
|
||||||
type OutputFiles = Record<string, OutputFile>;
|
type OutputFiles = Record<string, OutputFile>;
|
||||||
@ -19,7 +20,7 @@ const outputFiles = VERSIONS.reduce<OutputFiles>((acc, version) => {
|
|||||||
|
|
||||||
// ℹ️ these snapshots are generated by running "go test pkg/tests/apis/openapi_test.go" and "scripts/process-specs.ts",
|
// ℹ️ these snapshots are generated by running "go test pkg/tests/apis/openapi_test.go" and "scripts/process-specs.ts",
|
||||||
// see the README in the "openapi_snapshots" directory
|
// see the README in the "openapi_snapshots" directory
|
||||||
const schemaFile = `../../../data/openapi/notifications.alerting.grafana.app-${version}.json`;
|
const schemaFile = `../../../data/openapi/${GROUP}-${version}.json`;
|
||||||
|
|
||||||
// ℹ️ make sure there is a API file in each versioned directory
|
// ℹ️ make sure there is a API file in each versioned directory
|
||||||
const apiFile = `../src/grafana/api/${version}/api.ts`;
|
const apiFile = `../src/grafana/api/${version}/api.ts`;
|
||||||
|
@ -1239,7 +1239,7 @@ export type ObjectMeta = {
|
|||||||
Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
|
Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
|
||||||
uid?: string;
|
uid?: string;
|
||||||
};
|
};
|
||||||
export type Integration = {
|
export type ReceiverIntegration = {
|
||||||
disableResolveMessage?: boolean;
|
disableResolveMessage?: boolean;
|
||||||
secureFields?: {
|
secureFields?: {
|
||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
@ -1251,9 +1251,59 @@ export type Integration = {
|
|||||||
uid?: string;
|
uid?: string;
|
||||||
};
|
};
|
||||||
export type ReceiverSpec = {
|
export type ReceiverSpec = {
|
||||||
integrations: Integration[];
|
integrations: ReceiverIntegration[];
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
export type ReceiverstatusOperatorState = {
|
||||||
|
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||||
|
descriptiveState?: string;
|
||||||
|
/** details contains any extra information that is operator-specific */
|
||||||
|
details?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||||
|
lastEvaluation: string;
|
||||||
|
/** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
export type ReceiverStatus = {
|
||||||
|
/** additionalFields is reserved for future use */
|
||||||
|
additionalFields?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||||
|
operatorStates?: {
|
||||||
|
[key: string]: ReceiverstatusOperatorState;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type Receiver = {
|
||||||
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
|
apiVersion?: string;
|
||||||
|
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
||||||
|
kind?: string;
|
||||||
|
metadata: ObjectMeta;
|
||||||
|
/** Spec is the spec of the Receiver */
|
||||||
|
spec: ReceiverSpec;
|
||||||
|
status: ReceiverStatus;
|
||||||
|
};
|
||||||
|
export type ListMeta = {
|
||||||
|
/** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */
|
||||||
|
continue?: string;
|
||||||
|
/** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */
|
||||||
|
remainingItemCount?: number;
|
||||||
|
/** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */
|
||||||
|
resourceVersion?: string;
|
||||||
|
/** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */
|
||||||
|
selfLink?: string;
|
||||||
|
};
|
||||||
|
export type ReceiverList = {
|
||||||
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
|
apiVersion?: string;
|
||||||
|
items: Receiver[];
|
||||||
|
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
||||||
|
kind?: string;
|
||||||
|
metadata: ListMeta;
|
||||||
|
};
|
||||||
export type StatusCause = {
|
export type StatusCause = {
|
||||||
/** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.
|
/** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.
|
||||||
|
|
||||||
@ -1280,16 +1330,6 @@ export type StatusDetails = {
|
|||||||
/** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
|
/** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
|
||||||
uid?: string;
|
uid?: string;
|
||||||
};
|
};
|
||||||
export type ListMeta = {
|
|
||||||
/** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */
|
|
||||||
continue?: string;
|
|
||||||
/** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */
|
|
||||||
remainingItemCount?: number;
|
|
||||||
/** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */
|
|
||||||
resourceVersion?: string;
|
|
||||||
/** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */
|
|
||||||
selfLink?: string;
|
|
||||||
};
|
|
||||||
export type Status = {
|
export type Status = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
apiVersion?: string;
|
apiVersion?: string;
|
||||||
@ -1308,51 +1348,56 @@ export type Status = {
|
|||||||
/** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */
|
/** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */
|
||||||
status?: string;
|
status?: string;
|
||||||
};
|
};
|
||||||
export type Receiver = {
|
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
|
||||||
apiVersion?: string;
|
|
||||||
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
|
||||||
kind?: string;
|
|
||||||
metadata: ObjectMeta;
|
|
||||||
/** Spec is the spec of the Receiver */
|
|
||||||
spec: ReceiverSpec;
|
|
||||||
status: Status;
|
|
||||||
};
|
|
||||||
export type ReceiverList = {
|
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
|
||||||
apiVersion?: string;
|
|
||||||
items: Receiver[];
|
|
||||||
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
|
||||||
kind?: string;
|
|
||||||
metadata: ListMeta;
|
|
||||||
};
|
|
||||||
export type Patch = object;
|
export type Patch = object;
|
||||||
export type RouteDefaults = {
|
export type RoutingTreeRouteDefaults = {
|
||||||
group_by?: string[];
|
group_by?: string[];
|
||||||
group_interval?: string;
|
group_interval?: string;
|
||||||
group_wait?: string;
|
group_wait?: string;
|
||||||
receiver: string;
|
receiver: string;
|
||||||
repeat_interval?: string;
|
repeat_interval?: string;
|
||||||
};
|
};
|
||||||
export type Matcher = {
|
export type RoutingTreeMatcher = {
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
export type Route = {
|
export type RoutingTreeRoute = {
|
||||||
|
active_time_intervals?: string[];
|
||||||
continue: boolean;
|
continue: boolean;
|
||||||
group_by?: string[];
|
group_by?: string[];
|
||||||
group_interval?: string;
|
group_interval?: string;
|
||||||
group_wait?: string;
|
group_wait?: string;
|
||||||
matchers?: Matcher[];
|
matchers?: RoutingTreeMatcher[];
|
||||||
mute_time_intervals?: string[];
|
mute_time_intervals?: string[];
|
||||||
receiver?: string;
|
receiver?: string;
|
||||||
repeat_interval?: string;
|
repeat_interval?: string;
|
||||||
routes?: Route[];
|
routes?: RoutingTreeRoute[];
|
||||||
};
|
};
|
||||||
export type RoutingtreeSpec = {
|
export type RoutingTreeSpec = {
|
||||||
defaults: RouteDefaults;
|
defaults: RoutingTreeRouteDefaults;
|
||||||
routes: Route[];
|
routes: RoutingTreeRoute[];
|
||||||
|
};
|
||||||
|
export type RoutingTreestatusOperatorState = {
|
||||||
|
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||||
|
descriptiveState?: string;
|
||||||
|
/** details contains any extra information that is operator-specific */
|
||||||
|
details?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||||
|
lastEvaluation: string;
|
||||||
|
/** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
export type RoutingTreeStatus = {
|
||||||
|
/** additionalFields is reserved for future use */
|
||||||
|
additionalFields?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||||
|
operatorStates?: {
|
||||||
|
[key: string]: RoutingTreestatusOperatorState;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type RoutingTree = {
|
export type RoutingTree = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
@ -1361,8 +1406,8 @@ export type RoutingTree = {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
metadata: ObjectMeta;
|
metadata: ObjectMeta;
|
||||||
/** Spec is the spec of the RoutingTree */
|
/** Spec is the spec of the RoutingTree */
|
||||||
spec: RoutingtreeSpec;
|
spec: RoutingTreeSpec;
|
||||||
status: Status;
|
status: RoutingTreeStatus;
|
||||||
};
|
};
|
||||||
export type RoutingTreeList = {
|
export type RoutingTreeList = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
@ -1372,10 +1417,32 @@ export type RoutingTreeList = {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
metadata: ListMeta;
|
metadata: ListMeta;
|
||||||
};
|
};
|
||||||
export type TemplategroupSpec = {
|
export type TemplateGroupSpec = {
|
||||||
content: string;
|
content: string;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
export type TemplateGroupstatusOperatorState = {
|
||||||
|
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||||
|
descriptiveState?: string;
|
||||||
|
/** details contains any extra information that is operator-specific */
|
||||||
|
details?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||||
|
lastEvaluation: string;
|
||||||
|
/** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
export type TemplateGroupStatus = {
|
||||||
|
/** additionalFields is reserved for future use */
|
||||||
|
additionalFields?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||||
|
operatorStates?: {
|
||||||
|
[key: string]: TemplateGroupstatusOperatorState;
|
||||||
|
};
|
||||||
|
};
|
||||||
export type TemplateGroup = {
|
export type TemplateGroup = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
apiVersion?: string;
|
apiVersion?: string;
|
||||||
@ -1383,8 +1450,8 @@ export type TemplateGroup = {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
metadata: ObjectMeta;
|
metadata: ObjectMeta;
|
||||||
/** Spec is the spec of the TemplateGroup */
|
/** Spec is the spec of the TemplateGroup */
|
||||||
spec: TemplategroupSpec;
|
spec: TemplateGroupSpec;
|
||||||
status: Status;
|
status: TemplateGroupStatus;
|
||||||
};
|
};
|
||||||
export type TemplateGroupList = {
|
export type TemplateGroupList = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
@ -1394,21 +1461,43 @@ export type TemplateGroupList = {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
metadata: ListMeta;
|
metadata: ListMeta;
|
||||||
};
|
};
|
||||||
export type TimeRange = {
|
export type TimeIntervalTimeRange = {
|
||||||
end_time: string;
|
end_time: string;
|
||||||
start_time: string;
|
start_time: string;
|
||||||
};
|
};
|
||||||
export type Interval = {
|
export type TimeIntervalInterval = {
|
||||||
days_of_month?: string[];
|
days_of_month?: string[];
|
||||||
location?: string;
|
location?: string;
|
||||||
months?: string[];
|
months?: string[];
|
||||||
times?: TimeRange[];
|
times?: TimeIntervalTimeRange[];
|
||||||
weekdays?: string[];
|
weekdays?: string[];
|
||||||
years?: string[];
|
years?: string[];
|
||||||
};
|
};
|
||||||
export type TimeintervalSpec = {
|
export type TimeIntervalSpec = {
|
||||||
name: string;
|
name: string;
|
||||||
time_intervals: Interval[];
|
time_intervals: TimeIntervalInterval[];
|
||||||
|
};
|
||||||
|
export type TimeIntervalstatusOperatorState = {
|
||||||
|
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||||
|
descriptiveState?: string;
|
||||||
|
/** details contains any extra information that is operator-specific */
|
||||||
|
details?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||||
|
lastEvaluation: string;
|
||||||
|
/** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
export type TimeIntervalStatus = {
|
||||||
|
/** additionalFields is reserved for future use */
|
||||||
|
additionalFields?: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
/** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||||
|
operatorStates?: {
|
||||||
|
[key: string]: TimeIntervalstatusOperatorState;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type TimeInterval = {
|
export type TimeInterval = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
@ -1417,8 +1506,8 @@ export type TimeInterval = {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
metadata: ObjectMeta;
|
metadata: ObjectMeta;
|
||||||
/** Spec is the spec of the TimeInterval */
|
/** Spec is the spec of the TimeInterval */
|
||||||
spec: TimeintervalSpec;
|
spec: TimeIntervalSpec;
|
||||||
status: Status;
|
status: TimeIntervalStatus;
|
||||||
};
|
};
|
||||||
export type TimeIntervalList = {
|
export type TimeIntervalList = {
|
||||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||||
|
@ -2,8 +2,7 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
|||||||
|
|
||||||
import { getAPIBaseURL, getAPIReducerPath } from '../util';
|
import { getAPIBaseURL, getAPIReducerPath } from '../util';
|
||||||
|
|
||||||
const VERSION = 'v0alpha1';
|
import { GROUP, VERSION } from './const';
|
||||||
const GROUP = 'notifications.alerting.grafana.app';
|
|
||||||
|
|
||||||
const baseUrl = getAPIBaseURL(GROUP, VERSION);
|
const baseUrl = getAPIBaseURL(GROUP, VERSION);
|
||||||
const reducerPath = getAPIReducerPath(GROUP, VERSION);
|
const reducerPath = getAPIReducerPath(GROUP, VERSION);
|
||||||
@ -11,7 +10,9 @@ const reducerPath = getAPIReducerPath(GROUP, VERSION);
|
|||||||
export const api = createApi({
|
export const api = createApi({
|
||||||
reducerPath,
|
reducerPath,
|
||||||
baseQuery: fetchBaseQuery({
|
baseQuery: fetchBaseQuery({
|
||||||
baseUrl,
|
// Set URL correctly so MSW can intercept requests
|
||||||
|
// https://mswjs.io/docs/runbook#rtk-query-requests-are-not-intercepted
|
||||||
|
baseUrl: new URL(baseUrl, globalThis.location.origin).href,
|
||||||
}),
|
}),
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export const VERSION = 'v0alpha1' as const;
|
||||||
|
export const GROUP = 'notifications.alerting.grafana.app' as const;
|
@ -0,0 +1,74 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { Factory } from 'fishery';
|
||||||
|
|
||||||
|
import { DEFAULT_NAMESPACE, generateResourceVersion, generateTitle, generateUID } from '../../../../mocks/util';
|
||||||
|
import { GROUP, VERSION } from '../../const';
|
||||||
|
import {
|
||||||
|
ContactPoint,
|
||||||
|
ContactPointMetadataAnnotations,
|
||||||
|
EnhancedListReceiverApiResponse,
|
||||||
|
Integration,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
import { AlertingEntityMetadataAnnotationsFactory } from './common';
|
||||||
|
|
||||||
|
export const ListReceiverApiResponseFactory = Factory.define<EnhancedListReceiverApiResponse>(() => ({
|
||||||
|
kind: 'ReceiverList',
|
||||||
|
apiVersion: `${GROUP}/${VERSION}`,
|
||||||
|
metadata: {},
|
||||||
|
items: ContactPointFactory.buildList(5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ContactPointFactory = Factory.define<ContactPoint>(() => {
|
||||||
|
const title = generateTitle();
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadata: {
|
||||||
|
name: btoa(title),
|
||||||
|
namespace: DEFAULT_NAMESPACE,
|
||||||
|
uid: generateUID(),
|
||||||
|
resourceVersion: generateResourceVersion(),
|
||||||
|
annotations: ContactPointMetadataAnnotationsFactory.build(),
|
||||||
|
},
|
||||||
|
spec: ContactPointSpecFactory.build({ title }),
|
||||||
|
status: {},
|
||||||
|
} satisfies ContactPoint;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContactPointSpecFactory = Factory.define<ContactPoint['spec']>(() => ({
|
||||||
|
title: generateTitle(),
|
||||||
|
// use two unique random integrations by default
|
||||||
|
integrations: faker.helpers.uniqueArray(IntegrationUnion, 2).map((integration) => integration.build()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const GenericIntegrationFactory = Factory.define<Integration>(() => ({
|
||||||
|
type: 'generic',
|
||||||
|
disableResolveMessage: false,
|
||||||
|
settings: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const EmailIntegrationFactory = Factory.define<Integration>(() => ({
|
||||||
|
type: 'email',
|
||||||
|
settings: {
|
||||||
|
addresses: faker.internet.email(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SlackIntegrationFactory = Factory.define<Integration>(() => ({
|
||||||
|
type: 'slack',
|
||||||
|
settings: {
|
||||||
|
mentionChannel: '#alerts',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const IntegrationUnion = [EmailIntegrationFactory, SlackIntegrationFactory];
|
||||||
|
|
||||||
|
// by default the contact points will be in use by a route and a rule
|
||||||
|
export const ContactPointMetadataAnnotationsFactory = Factory.define<ContactPointMetadataAnnotations>(() => ({
|
||||||
|
'grafana.com/access/canReadSecrets': 'true',
|
||||||
|
'grafana.com/inUse/routes': '1',
|
||||||
|
'grafana.com/inUse/rules': '1',
|
||||||
|
...AlertingEntityMetadataAnnotationsFactory.build(),
|
||||||
|
}));
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Factory } from 'fishery';
|
||||||
|
|
||||||
|
import { AlertingEntityMetadataAnnotations } from '../../types';
|
||||||
|
|
||||||
|
export const AlertingEntityMetadataAnnotationsFactory = Factory.define<AlertingEntityMetadataAnnotations>(() => ({
|
||||||
|
'grafana.com/access/canAdmin': 'true',
|
||||||
|
'grafana.com/access/canDelete': 'true',
|
||||||
|
'grafana.com/access/canWrite': 'true',
|
||||||
|
'grafana.com/provenance': '',
|
||||||
|
}));
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { CreateReceiverApiResponse } from '../../../api.gen';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
|
||||||
|
export function createReceiverHandler(
|
||||||
|
data: CreateReceiverApiResponse | ((info: Parameters<Parameters<typeof http.post>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.post(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { DeleteReceiverApiResponse } from '../../../api.gen';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
|
||||||
|
export function deleteReceiverHandler(
|
||||||
|
data: DeleteReceiverApiResponse | ((info: Parameters<Parameters<typeof http.delete>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.delete(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { DeletecollectionReceiverApiResponse } from '../../../api.gen';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
|
||||||
|
export function deletecollectionReceiverHandler(
|
||||||
|
data: DeletecollectionReceiverApiResponse | ((info: Parameters<Parameters<typeof http.delete>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.delete(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { GetReceiverApiResponse } from '../../../api.gen';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
|
||||||
|
export function getReceiverHandler(
|
||||||
|
data: GetReceiverApiResponse | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.get(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
export { createReceiverHandler } from './createReceiverHandler';
|
||||||
|
export { deletecollectionReceiverHandler } from './deletecollectionReceiverHandler';
|
||||||
|
export { deleteReceiverHandler } from './deleteReceiverHandler';
|
||||||
|
export { getReceiverHandler } from './getReceiverHandler';
|
||||||
|
export { listReceiverHandler } from './listReceiverHandler';
|
||||||
|
export { replaceReceiverHandler } from './replaceReceiverHandler';
|
||||||
|
export { updateReceiverHandler } from './updateReceiverHandler';
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
import { EnhancedListReceiverApiResponse } from '../../../types';
|
||||||
|
|
||||||
|
export function listReceiverHandler(
|
||||||
|
data: EnhancedListReceiverApiResponse | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.get(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { ReplaceReceiverApiResponse } from '../../../api.gen';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
|
||||||
|
export function replaceReceiverHandler(
|
||||||
|
data: ReplaceReceiverApiResponse | ((info: Parameters<Parameters<typeof http.put>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.put(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
|
||||||
|
import { getAPIBaseURLForMocks } from '../../../../../mocks/util';
|
||||||
|
import { UpdateReceiverApiResponse } from '../../../api.gen';
|
||||||
|
import { GROUP, VERSION } from '../../../const';
|
||||||
|
|
||||||
|
export function updateReceiverHandler(
|
||||||
|
data: UpdateReceiverApiResponse | ((info: Parameters<Parameters<typeof http.patch>[1]>[0]) => Response)
|
||||||
|
) {
|
||||||
|
return http.patch(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
return data(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json(data);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
export { createReceiverHandler } from './ReceiverHandlers/createReceiverHandler';
|
||||||
|
export { deletecollectionReceiverHandler } from './ReceiverHandlers/deletecollectionReceiverHandler';
|
||||||
|
export { deleteReceiverHandler } from './ReceiverHandlers/deleteReceiverHandler';
|
||||||
|
export { getReceiverHandler } from './ReceiverHandlers/getReceiverHandler';
|
||||||
|
export { listReceiverHandler } from './ReceiverHandlers/listReceiverHandler';
|
||||||
|
export { replaceReceiverHandler } from './ReceiverHandlers/replaceReceiverHandler';
|
||||||
|
export { updateReceiverHandler } from './ReceiverHandlers/updateReceiverHandler';
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { MergeDeep, MergeExclusive, OverrideProperties } from 'type-fest';
|
import { MergeDeep, MergeExclusive, OverrideProperties } from 'type-fest';
|
||||||
|
|
||||||
import type { ListReceiverApiResponse, Receiver, Integration as ReceiverIntegration } from './api.gen';
|
import type { ListReceiverApiResponse, Receiver, ReceiverIntegration } from './api.gen';
|
||||||
|
|
||||||
type GenericIntegration = OverrideProperties<
|
type GenericIntegration = OverrideProperties<
|
||||||
ReceiverIntegration,
|
ReceiverIntegration,
|
||||||
@ -58,12 +58,31 @@ export type Integration = EmailIntegration | SlackIntegration | GenericIntegrati
|
|||||||
export type ContactPoint = MergeDeep<
|
export type ContactPoint = MergeDeep<
|
||||||
Receiver,
|
Receiver,
|
||||||
{
|
{
|
||||||
|
metadata: {
|
||||||
|
annotations: ContactPointMetadataAnnotations;
|
||||||
|
};
|
||||||
spec: {
|
spec: {
|
||||||
integrations: Integration[];
|
integrations: Integration[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ContactPointMetadataAnnotations = AlertingEntityMetadataAnnotations &
|
||||||
|
Partial<{
|
||||||
|
// reading secrets is unique to contact points / receivers
|
||||||
|
'grafana.com/access/canReadSecrets': 'true' | 'false';
|
||||||
|
'grafana.com/inUse/routes': `${number}`;
|
||||||
|
'grafana.com/inUse/rules': `${number}`;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type AlertingEntityMetadataAnnotations = Partial<{
|
||||||
|
'grafana.com/access/canAdmin': 'true' | 'false';
|
||||||
|
'grafana.com/access/canDelete': 'true' | 'false';
|
||||||
|
'grafana.com/access/canWrite': 'true' | 'false';
|
||||||
|
// used for provisioning to identify what system created the entity
|
||||||
|
'grafana.com/provenance': string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type EnhancedListReceiverApiResponse = OverrideProperties<
|
export type EnhancedListReceiverApiResponse = OverrideProperties<
|
||||||
ListReceiverApiResponse,
|
ListReceiverApiResponse,
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
ContactPointFactory,
|
||||||
|
EmailIntegrationFactory,
|
||||||
|
ListReceiverApiResponseFactory,
|
||||||
|
SlackIntegrationFactory,
|
||||||
|
} from '../../../api/v0alpha1/mocks/fakes/Receivers';
|
||||||
|
import { listReceiverHandler } from '../../../api/v0alpha1/mocks/handlers/ReceiverHandlers';
|
||||||
|
|
||||||
|
export const simpleContactPointsList = ListReceiverApiResponseFactory.build({
|
||||||
|
items: [
|
||||||
|
// contact point with for testing with multiple different integrations – should show "email, slack" description
|
||||||
|
ContactPointFactory.build({
|
||||||
|
spec: {
|
||||||
|
integrations: [EmailIntegrationFactory.build(), SlackIntegrationFactory.build()],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// contact point for testing "email (2)" description
|
||||||
|
ContactPointFactory.build({
|
||||||
|
spec: {
|
||||||
|
integrations: EmailIntegrationFactory.buildList(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// contact point for testing "empty contact point" description
|
||||||
|
ContactPointFactory.build({
|
||||||
|
spec: { integrations: [] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// export the simple contact points list as a separate list of handlers (scenario) so we can load it in the front-end
|
||||||
|
export const simpleContactPointsListScenario = [listReceiverHandler(simpleContactPointsList)];
|
||||||
|
|
||||||
|
// the default export will allow us to load this scenario on the front-end using the MSW web worker
|
||||||
|
export default simpleContactPointsListScenario;
|
@ -0,0 +1,54 @@
|
|||||||
|
import { setupMockServer } from '@grafana/test-utils/server';
|
||||||
|
|
||||||
|
import { render, screen, within } from '../../../../../tests/test-utils';
|
||||||
|
import { getContactPointDescription } from '../../utils';
|
||||||
|
|
||||||
|
import { ContactPointSelector } from './ContactPointSelector';
|
||||||
|
import { simpleContactPointsList, simpleContactPointsListScenario } from './ContactPointSelector.test.scenario';
|
||||||
|
|
||||||
|
const server = setupMockServer();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
server.use(...simpleContactPointsListScenario);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// @TODO remove or move this, required for testing combobox 😮💨
|
||||||
|
const mockGetBoundingClientRect = jest.fn(() => ({
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
}));
|
||||||
|
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
|
||||||
|
value: mockGetBoundingClientRect,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listing contact points', () => {
|
||||||
|
it('should show a sorted list of contact points', async () => {
|
||||||
|
const onChangeHandler = jest.fn();
|
||||||
|
|
||||||
|
// render the contact point selector
|
||||||
|
const { user } = render(<ContactPointSelector onChange={onChangeHandler} />);
|
||||||
|
await user.click(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
// make sure all options are rendered
|
||||||
|
expect(await screen.findAllByRole('option')).toHaveLength(simpleContactPointsList.items.length);
|
||||||
|
|
||||||
|
for (const item of simpleContactPointsList.items) {
|
||||||
|
const option = await screen.findByRole('option', { name: new RegExp(item.spec.title) });
|
||||||
|
expect(option).toBeInTheDocument();
|
||||||
|
expect(within(option).getByText(getContactPointDescription(item))).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
// test interaction with combobox and handler contract
|
||||||
|
const firstContactPoint = simpleContactPointsList.items[0];
|
||||||
|
|
||||||
|
const firstOption = await screen.findByText(firstContactPoint.spec.title);
|
||||||
|
await user.click(firstOption);
|
||||||
|
expect(onChangeHandler).toHaveBeenCalledWith(firstContactPoint);
|
||||||
|
});
|
||||||
|
});
|
@ -2,9 +2,9 @@ import { chain } from 'lodash';
|
|||||||
|
|
||||||
import { Combobox, ComboboxOption } from '@grafana/ui';
|
import { Combobox, ComboboxOption } from '@grafana/ui';
|
||||||
|
|
||||||
import { ContactPoint } from '../../api/v0alpha1/types';
|
import type { ContactPoint } from '../../../api/v0alpha1/types';
|
||||||
import { useListContactPointsv0alpha1 } from '../hooks/useContactPoints';
|
import { useListContactPointsv0alpha1 } from '../../hooks/useContactPoints';
|
||||||
import { getContactPointDescription } from '../utils';
|
import { getContactPointDescription } from '../../utils';
|
||||||
|
|
||||||
const collator = new Intl.Collator('en', { sensitivity: 'accent' });
|
const collator = new Intl.Collator('en', { sensitivity: 'accent' });
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import { TypedUseQueryHookResult, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { type TypedUseQueryHookResult, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
|
||||||
import { type ListReceiverApiArg, alertingAPI } from '../../api/v0alpha1/api.gen';
|
import { type ListReceiverApiArg, alertingAPI } from '../../api/v0alpha1/api.gen';
|
||||||
import type { EnhancedListReceiverApiResponse } from '../../api/v0alpha1/types';
|
import type { EnhancedListReceiverApiResponse } from '../../api/v0alpha1/types';
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
ContactPointFactory,
|
||||||
|
EmailIntegrationFactory,
|
||||||
|
GenericIntegrationFactory,
|
||||||
|
SlackIntegrationFactory,
|
||||||
|
} from '../api/v0alpha1/mocks/fakes/Receivers';
|
||||||
|
|
||||||
|
import { getContactPointDescription } from './utils';
|
||||||
|
|
||||||
|
describe('getContactPointDescription', () => {
|
||||||
|
it('should show description for single integration', () => {
|
||||||
|
const contactPoint = ContactPointFactory.build({
|
||||||
|
spec: {
|
||||||
|
integrations: [SlackIntegrationFactory.build()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getContactPointDescription(contactPoint)).toBe('slack');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show description for mixed contact points', () => {
|
||||||
|
const contactPoint = ContactPointFactory.build({
|
||||||
|
spec: {
|
||||||
|
integrations: [EmailIntegrationFactory.build(), SlackIntegrationFactory.build()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getContactPointDescription(contactPoint)).toBe('email, slack');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show description for several of the same type', () => {
|
||||||
|
const contactPoint = ContactPointFactory.build({
|
||||||
|
spec: {
|
||||||
|
integrations: EmailIntegrationFactory.buildList(3),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getContactPointDescription(contactPoint)).toBe('email (3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show description for empty contact point', () => {
|
||||||
|
const contactPoint = ContactPointFactory.build({ spec: { integrations: [] } });
|
||||||
|
expect(getContactPointDescription(contactPoint)).toBe('<empty contact point>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show description for generic / unknown contact point integration', () => {
|
||||||
|
const contactPoint = ContactPointFactory.build({
|
||||||
|
spec: { integrations: [GenericIntegrationFactory.build({ type: 'generic' })] },
|
||||||
|
});
|
||||||
|
expect(getContactPointDescription(contactPoint)).toBe('generic');
|
||||||
|
});
|
||||||
|
});
|
15
packages/grafana-alerting/src/grafana/mocks/util.ts
Normal file
15
packages/grafana-alerting/src/grafana/mocks/util.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { upperFirst } from 'lodash';
|
||||||
|
|
||||||
|
export const DEFAULT_NAMESPACE = 'default' as const;
|
||||||
|
|
||||||
|
// this function is used for MSW endpoints
|
||||||
|
export const getAPIBaseURLForMocks = (group: string, version: string, path: `/${string}` = '/') =>
|
||||||
|
`/apis/${group}/${version}/namespaces/default${path}` as const;
|
||||||
|
|
||||||
|
// example: "Likeable sea lion"
|
||||||
|
export const generateTitle = () => upperFirst(`${faker.word.adjective()} ${faker.animal.type()}`);
|
||||||
|
|
||||||
|
export const generateResourceVersion = () => faker.string.alphanumeric({ length: 16, casing: 'lower' });
|
||||||
|
|
||||||
|
export const generateUID = () => faker.string.alphanumeric({ length: 32, casing: 'mixed' });
|
@ -5,7 +5,13 @@
|
|||||||
// Contact Points
|
// Contact Points
|
||||||
export * from './grafana/api/v0alpha1/types';
|
export * from './grafana/api/v0alpha1/types';
|
||||||
export { useListContactPointsv0alpha1 } from './grafana/contactPoints/hooks/useContactPoints';
|
export { useListContactPointsv0alpha1 } from './grafana/contactPoints/hooks/useContactPoints';
|
||||||
export { ContactPointSelector } from './grafana/contactPoints/components/ContactPointSelector';
|
export { ContactPointSelector } from './grafana/contactPoints/components/ContactPointSelector/ContactPointSelector';
|
||||||
|
|
||||||
// Low-level API hooks
|
// Low-level API hooks
|
||||||
export { alertingAPI as alertingAPIv0alpha1 } from './grafana/api/v0alpha1/api.gen';
|
export { alertingAPI as alertingAPIv0alpha1 } from './grafana/api/v0alpha1/api.gen';
|
||||||
|
|
||||||
|
// model factories / mocks
|
||||||
|
export * as mocksV0alpha1 from './grafana/api/v0alpha1/mocks/fakes/Receivers';
|
||||||
|
|
||||||
|
// MSW handlers
|
||||||
|
export * as handlersV0alpha1 from './grafana/api/v0alpha1/mocks/handlers';
|
||||||
|
52
packages/grafana-alerting/tests/test-utils.tsx
Normal file
52
packages/grafana-alerting/tests/test-utils.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* ⚠️ @TODO this will eventually be replaced with "@grafana/test-utils", consider helping out instead of adding things here!
|
||||||
|
*/
|
||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { type RenderOptions, render } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
import { alertingAPIv0alpha1 } from '../src/unstable';
|
||||||
|
|
||||||
|
// create an empty store
|
||||||
|
const store = configureStore({
|
||||||
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPIv0alpha1.middleware),
|
||||||
|
reducer: {
|
||||||
|
[alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a wrapper component that implements all of the providers that components
|
||||||
|
* within the app will need
|
||||||
|
*/
|
||||||
|
const getDefaultWrapper = ({}: RenderOptions) => {
|
||||||
|
/**
|
||||||
|
* Returns a wrapper that should (eventually?) match the main `AppWrapper`, so any tests are rendering
|
||||||
|
* in mostly the same providers as a "real" hierarchy
|
||||||
|
*/
|
||||||
|
return function Wrapper({ children }: React.PropsWithChildren) {
|
||||||
|
return <Provider store={store}>{children}</Provider>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended [@testing-library/react render](https://testing-library.com/docs/react-testing-library/api/#render)
|
||||||
|
* method which wraps the passed element in all of the necessary Providers,
|
||||||
|
* so it can render correctly in the context of the application
|
||||||
|
*/
|
||||||
|
const customRender = (ui: React.ReactNode, renderOptions: RenderOptions = {}) => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const Providers = renderOptions.wrapper || getDefaultWrapper(renderOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderResult: render(ui, { wrapper: Providers, ...renderOptions }),
|
||||||
|
user,
|
||||||
|
store,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from '@testing-library/react';
|
||||||
|
export { customRender as render, userEvent };
|
@ -4,9 +4,16 @@
|
|||||||
"declarationDir": "./compiled",
|
"declarationDir": "./compiled",
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"rootDirs": ["."]
|
"rootDirs": ["."],
|
||||||
|
"moduleResolution": "bundler"
|
||||||
},
|
},
|
||||||
"exclude": ["dist/**/*"],
|
"exclude": ["dist/**/*"],
|
||||||
"extends": "@grafana/tsconfig",
|
"extends": "@grafana/tsconfig",
|
||||||
"include": ["typings/jest", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts", "src/**/*.ts*"]
|
"include": ["typings/jest", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts", "src/**/*.ts*"],
|
||||||
|
"ts-node": {
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "Node"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ export function setupMockServer(
|
|||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default server;
|
export default server;
|
||||||
|
81
yarn.lock
81
yarn.lock
@ -2291,6 +2291,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@faker-js/faker@npm:^9.7.0":
|
||||||
|
version: 9.7.0
|
||||||
|
resolution: "@faker-js/faker@npm:9.7.0"
|
||||||
|
checksum: 10/5167cc179bbf5f140b957f600af24772ca8c6e7e42405c312c25a6b4278b851b84bd8e3065f80e667dd4c35dd5aeb0ed490b0b1872f908d58ffae1cef9a5ef72
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@fingerprintjs/fingerprintjs@npm:^3.4.2":
|
"@fingerprintjs/fingerprintjs@npm:^3.4.2":
|
||||||
version: 3.4.2
|
version: 3.4.2
|
||||||
resolution: "@fingerprintjs/fingerprintjs@npm:3.4.2"
|
resolution: "@fingerprintjs/fingerprintjs@npm:3.4.2"
|
||||||
@ -2918,20 +2925,26 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@grafana/alerting@workspace:packages/grafana-alerting"
|
resolution: "@grafana/alerting@workspace:packages/grafana-alerting"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@faker-js/faker": "npm:^9.7.0"
|
||||||
|
"@grafana/test-utils": "workspace:*"
|
||||||
"@grafana/tsconfig": "npm:^2.0.0"
|
"@grafana/tsconfig": "npm:^2.0.0"
|
||||||
"@reduxjs/toolkit": "npm:^2.8.0"
|
|
||||||
"@rtk-query/codegen-openapi": "npm:^2.0.0"
|
"@rtk-query/codegen-openapi": "npm:^2.0.0"
|
||||||
|
"@testing-library/jest-dom": "npm:^6.6.3"
|
||||||
|
"@testing-library/react": "npm:^16.3.0"
|
||||||
|
"@testing-library/user-event": "npm:^14.6.1"
|
||||||
"@types/lodash": "npm:^4"
|
"@types/lodash": "npm:^4"
|
||||||
"@types/react": "npm:18.3.18"
|
"@types/react": "npm:18.3.18"
|
||||||
"@types/react-dom": "npm:18.3.5"
|
"@types/react-dom": "npm:18.3.5"
|
||||||
lodash: "npm:^4.17.21"
|
lodash: "npm:^4.17.21"
|
||||||
react: "npm:18.3.1"
|
react: "npm:18.3.1"
|
||||||
react-dom: "npm:18.3.1"
|
react-dom: "npm:18.3.1"
|
||||||
|
react-redux: "npm:^9.2.0"
|
||||||
type-fest: "npm:^4.40.0"
|
type-fest: "npm:^4.40.0"
|
||||||
typescript: "npm:5.7.3"
|
typescript: "npm:5.7.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@grafana/runtime": ^12.0.0-pre
|
"@grafana/runtime": ^12.0.0-pre
|
||||||
"@grafana/ui": ^12.0.0-pre
|
"@grafana/ui": ^12.0.0-pre
|
||||||
|
"@reduxjs/toolkit": ^2.8.0
|
||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
@ -6553,28 +6566,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@reduxjs/toolkit@npm:^2.8.0":
|
|
||||||
version: 2.8.1
|
|
||||||
resolution: "@reduxjs/toolkit@npm:2.8.1"
|
|
||||||
dependencies:
|
|
||||||
"@standard-schema/spec": "npm:^1.0.0"
|
|
||||||
"@standard-schema/utils": "npm:^0.3.0"
|
|
||||||
immer: "npm:^10.0.3"
|
|
||||||
redux: "npm:^5.0.1"
|
|
||||||
redux-thunk: "npm:^3.1.0"
|
|
||||||
reselect: "npm:^5.1.0"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
|
||||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
react-redux:
|
|
||||||
optional: true
|
|
||||||
checksum: 10/388e39af72d42935bb644b8121c6b0de043bba51ab378f532476f7c77b79bde625f7c8dc5a6bc1095448008a08568c17876d958df87eeae1cfcb72214907e7da
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@remix-run/router@npm:1.19.1":
|
"@remix-run/router@npm:1.19.1":
|
||||||
version: 1.19.1
|
version: 1.19.1
|
||||||
resolution: "@remix-run/router@npm:1.19.1"
|
resolution: "@remix-run/router@npm:1.19.1"
|
||||||
@ -7094,20 +7085,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@standard-schema/spec@npm:^1.0.0":
|
|
||||||
version: 1.0.0
|
|
||||||
resolution: "@standard-schema/spec@npm:1.0.0"
|
|
||||||
checksum: 10/aee780cc1431888ca4b9aba9b24ffc8f3073fc083acc105e3951481478a2f4dc957796931b2da9e2d8329584cf211e4542275f188296c1cdff3ed44fd93a8bc8
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@standard-schema/utils@npm:^0.3.0":
|
|
||||||
version: 0.3.0
|
|
||||||
resolution: "@standard-schema/utils@npm:0.3.0"
|
|
||||||
checksum: 10/7084f875d322792f2e0a5904009434c8374b9345b09ba89828b68fd56fa3c2b366d35bf340d9e8c72736ef01793c2f70d350c372ed79845dc3566c58d34b4b51
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@storybook/addon-a11y@npm:^8.6.2":
|
"@storybook/addon-a11y@npm:^8.6.2":
|
||||||
version: 8.6.2
|
version: 8.6.2
|
||||||
resolution: "@storybook/addon-a11y@npm:8.6.2"
|
resolution: "@storybook/addon-a11y@npm:8.6.2"
|
||||||
@ -8538,7 +8515,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@testing-library/jest-dom@npm:6.6.3, @testing-library/jest-dom@npm:^6.1.2":
|
"@testing-library/jest-dom@npm:6.6.3, @testing-library/jest-dom@npm:^6.1.2, @testing-library/jest-dom@npm:^6.6.3":
|
||||||
version: 6.6.3
|
version: 6.6.3
|
||||||
resolution: "@testing-library/jest-dom@npm:6.6.3"
|
resolution: "@testing-library/jest-dom@npm:6.6.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8573,6 +8550,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@testing-library/react@npm:^16.3.0":
|
||||||
|
version: 16.3.0
|
||||||
|
resolution: "@testing-library/react@npm:16.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": "npm:^7.12.5"
|
||||||
|
peerDependencies:
|
||||||
|
"@testing-library/dom": ^10.0.0
|
||||||
|
"@types/react": ^18.0.0 || ^19.0.0
|
||||||
|
"@types/react-dom": ^18.0.0 || ^19.0.0
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
checksum: 10/0ee9e31dd0d2396a924682d0e61a4ecc6bfab8eaff23dbf8a72c3c2ce22c116fa578148baeb4de75b968ef99d22e6e6aa0a00dba40286f71184918bb6bb5b06a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@testing-library/user-event@npm:14.5.2":
|
"@testing-library/user-event@npm:14.5.2":
|
||||||
version: 14.5.2
|
version: 14.5.2
|
||||||
resolution: "@testing-library/user-event@npm:14.5.2"
|
resolution: "@testing-library/user-event@npm:14.5.2"
|
||||||
@ -8582,7 +8579,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@testing-library/user-event@npm:14.6.1":
|
"@testing-library/user-event@npm:14.6.1, @testing-library/user-event@npm:^14.6.1":
|
||||||
version: 14.6.1
|
version: 14.6.1
|
||||||
resolution: "@testing-library/user-event@npm:14.6.1"
|
resolution: "@testing-library/user-event@npm:14.6.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -16750,11 +16747,11 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"fishery@npm:^2.2.2":
|
"fishery@npm:^2.2.2":
|
||||||
version: 2.2.3
|
version: 2.3.1
|
||||||
resolution: "fishery@npm:2.2.3"
|
resolution: "fishery@npm:2.3.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash.mergewith: "npm:^4.6.2"
|
lodash.mergewith: "npm:^4.6.2"
|
||||||
checksum: 10/a8855c1949524996d2ba3b84cf6fe13f2a8ade8296e090bedbc58d2879dc481d85a0aced1e33f80b1a1ec9cb1a2eb7562494c2ae514915730d14285694f595bf
|
checksum: 10/45011c4e9a8447357c2a1096db31db8175322f0193d05a00e3741c1bd1972c86451761f3b9cd25847421b16680f7e3a7c82ae7e010676b7e172d8a86040e394a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user