mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: New list view (Part 1) (#95039)
* initial commit * update styles * wip * update list view * update translations * abstract components * metadata separator * refactor * cleanup * fix tests * WIP * translations * refactor to use maps and type-safety * WIP * UI updates * Rule action buttons early draft * recording rules * WIP typescript errors * implement action button loading * move section loader etc * add placeholder for group actions * Change files structure, remove CombinedRule from AlertRuleMenu * Refactor fetching data sources with ruler * Fix tests * Unify data source features * move files * make actions column wider * update translations * Update tests to reflect code changes * Remove direct buildinfo usages * Fix useCanSilence hook * Add missing translations, fix lint errors * PR feedback * update test * Remove featureDiscovery mock from a test --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
parent
65097d4b54
commit
b73ab15878
@ -1909,9 +1909,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rules/RuleListStateSection.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
@ -2056,17 +2054,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"]
|
||||
],
|
||||
"public/app/features/alerting/unified/rule-list/RuleList.v1.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/rule-list/RuleList.v2.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/state/actions.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/types/receiver-form.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
|
@ -38,6 +38,7 @@
|
||||
"unicons/chart-line",
|
||||
"unicons/check",
|
||||
"unicons/check-circle",
|
||||
"unicons/times-circle",
|
||||
"unicons/circle",
|
||||
"unicons/clipboard-alt",
|
||||
"unicons/clock-nine",
|
||||
|
@ -46,141 +46,142 @@ import u1035 from '../../../img/icons/unicons/channel-add.svg';
|
||||
import u1036 from '../../../img/icons/unicons/chart-line.svg';
|
||||
import u1037 from '../../../img/icons/unicons/check.svg';
|
||||
import u1038 from '../../../img/icons/unicons/check-circle.svg';
|
||||
import u1039 from '../../../img/icons/unicons/circle.svg';
|
||||
import u1040 from '../../../img/icons/unicons/clipboard-alt.svg';
|
||||
import u1041 from '../../../img/icons/unicons/clock-nine.svg';
|
||||
import u1042 from '../../../img/icons/unicons/cloud.svg';
|
||||
import u1043 from '../../../img/icons/unicons/cloud-download.svg';
|
||||
import u1044 from '../../../img/icons/unicons/code-branch.svg';
|
||||
import u1045 from '../../../img/icons/unicons/cog.svg';
|
||||
import u1046 from '../../../img/icons/unicons/columns.svg';
|
||||
import u1047 from '../../../img/icons/unicons/comment-alt.svg';
|
||||
import u1048 from '../../../img/icons/unicons/comment-alt-share.svg';
|
||||
import u1049 from '../../../img/icons/unicons/comments-alt.svg';
|
||||
import u1050 from '../../../img/icons/unicons/compass.svg';
|
||||
import u1051 from '../../../img/icons/unicons/copy.svg';
|
||||
import u1052 from '../../../img/icons/unicons/corner-down-right-alt.svg';
|
||||
import u1053 from '../../../img/icons/unicons/cube.svg';
|
||||
import u1054 from '../../../img/icons/unicons/dashboard.svg';
|
||||
import u1055 from '../../../img/icons/unicons/database.svg';
|
||||
import u1056 from '../../../img/icons/unicons/document-info.svg';
|
||||
import u1057 from '../../../img/icons/unicons/download-alt.svg';
|
||||
import u1058 from '../../../img/icons/unicons/draggabledots.svg';
|
||||
import u1059 from '../../../img/icons/unicons/edit.svg';
|
||||
import u1060 from '../../../img/icons/unicons/ellipsis-v.svg';
|
||||
import u1061 from '../../../img/icons/unicons/ellipsis-h.svg';
|
||||
import u1062 from '../../../img/icons/unicons/envelope.svg';
|
||||
import u1063 from '../../../img/icons/unicons/exchange-alt.svg';
|
||||
import u1064 from '../../../img/icons/unicons/exclamation-circle.svg';
|
||||
import u1065 from '../../../img/icons/unicons/exclamation-triangle.svg';
|
||||
import u1066 from '../../../img/icons/unicons/external-link-alt.svg';
|
||||
import u1067 from '../../../img/icons/unicons/eye.svg';
|
||||
import u1068 from '../../../img/icons/unicons/eye-slash.svg';
|
||||
import u1069 from '../../../img/icons/unicons/file-alt.svg';
|
||||
import u1070 from '../../../img/icons/unicons/file-blank.svg';
|
||||
import u1071 from '../../../img/icons/unicons/filter.svg';
|
||||
import u1072 from '../../../img/icons/unicons/folder.svg';
|
||||
import u1073 from '../../../img/icons/unicons/folder-open.svg';
|
||||
import u1074 from '../../../img/icons/unicons/folder-plus.svg';
|
||||
import u1075 from '../../../img/icons/unicons/folder-upload.svg';
|
||||
import u1076 from '../../../img/icons/unicons/forward.svg';
|
||||
import u1077 from '../../../img/icons/unicons/graph-bar.svg';
|
||||
import u1078 from '../../../img/icons/unicons/history.svg';
|
||||
import u1079 from '../../../img/icons/unicons/history-alt.svg';
|
||||
import u1080 from '../../../img/icons/unicons/home-alt.svg';
|
||||
import u1081 from '../../../img/icons/unicons/import.svg';
|
||||
import u1082 from '../../../img/icons/unicons/info.svg';
|
||||
import u1083 from '../../../img/icons/unicons/info-circle.svg';
|
||||
import u1084 from '../../../img/icons/unicons/k6.svg';
|
||||
import u1085 from '../../../img/icons/unicons/key-skeleton-alt.svg';
|
||||
import u1086 from '../../../img/icons/unicons/keyboard.svg';
|
||||
import u1087 from '../../../img/icons/unicons/link.svg';
|
||||
import u1088 from '../../../img/icons/unicons/list-ul.svg';
|
||||
import u1089 from '../../../img/icons/unicons/lock.svg';
|
||||
import u1090 from '../../../img/icons/unicons/minus.svg';
|
||||
import u1091 from '../../../img/icons/unicons/minus-circle.svg';
|
||||
import u1092 from '../../../img/icons/unicons/mobile-android.svg';
|
||||
import u1093 from '../../../img/icons/unicons/monitor.svg';
|
||||
import u1094 from '../../../img/icons/unicons/pause.svg';
|
||||
import u1095 from '../../../img/icons/unicons/pen.svg';
|
||||
import u1096 from '../../../img/icons/unicons/play.svg';
|
||||
import u1097 from '../../../img/icons/unicons/plug.svg';
|
||||
import u1098 from '../../../img/icons/unicons/plus.svg';
|
||||
import u1099 from '../../../img/icons/unicons/plus-circle.svg';
|
||||
import u1100 from '../../../img/icons/unicons/power.svg';
|
||||
import u1101 from '../../../img/icons/unicons/presentation-play.svg';
|
||||
import u1102 from '../../../img/icons/unicons/process.svg';
|
||||
import u1103 from '../../../img/icons/unicons/question-circle.svg';
|
||||
import u1104 from '../../../img/icons/unicons/repeat.svg';
|
||||
import u1105 from '../../../img/icons/unicons/rocket.svg';
|
||||
import u1106 from '../../../img/icons/unicons/rss.svg';
|
||||
import u1107 from '../../../img/icons/unicons/save.svg';
|
||||
import u1108 from '../../../img/icons/unicons/search.svg';
|
||||
import u1109 from '../../../img/icons/unicons/search-minus.svg';
|
||||
import u1110 from '../../../img/icons/unicons/search-plus.svg';
|
||||
import u1111 from '../../../img/icons/unicons/share-alt.svg';
|
||||
import u1112 from '../../../img/icons/unicons/shield.svg';
|
||||
import u1113 from '../../../img/icons/unicons/signal.svg';
|
||||
import u1114 from '../../../img/icons/unicons/signin.svg';
|
||||
import u1115 from '../../../img/icons/unicons/signout.svg';
|
||||
import u1116 from '../../../img/icons/unicons/sitemap.svg';
|
||||
import u1117 from '../../../img/icons/unicons/slack.svg';
|
||||
import u1118 from '../../../img/icons/unicons/sliders-v-alt.svg';
|
||||
import u1119 from '../../../img/icons/unicons/sort-amount-down.svg';
|
||||
import u1120 from '../../../img/icons/unicons/sort-amount-up.svg';
|
||||
import u1121 from '../../../img/icons/unicons/square-shape.svg';
|
||||
import u1122 from '../../../img/icons/unicons/star.svg';
|
||||
import u1123 from '../../../img/icons/unicons/step-backward.svg';
|
||||
import u1124 from '../../../img/icons/unicons/sync.svg';
|
||||
import u1125 from '../../../img/icons/unicons/stopwatch.svg';
|
||||
import u1126 from '../../../img/icons/unicons/table.svg';
|
||||
import u1127 from '../../../img/icons/unicons/tag-alt.svg';
|
||||
import u1128 from '../../../img/icons/unicons/times.svg';
|
||||
import u1129 from '../../../img/icons/unicons/trash-alt.svg';
|
||||
import u1130 from '../../../img/icons/unicons/unlock.svg';
|
||||
import u1131 from '../../../img/icons/unicons/upload.svg';
|
||||
import u1132 from '../../../img/icons/unicons/user.svg';
|
||||
import u1133 from '../../../img/icons/unicons/users-alt.svg';
|
||||
import u1134 from '../../../img/icons/unicons/wrap-text.svg';
|
||||
import u1135 from '../../../img/icons/unicons/cloud-upload.svg';
|
||||
import u1136 from '../../../img/icons/unicons/credit-card.svg';
|
||||
import u1137 from '../../../img/icons/unicons/file-copy-alt.svg';
|
||||
import u1138 from '../../../img/icons/unicons/fire.svg';
|
||||
import u1139 from '../../../img/icons/unicons/hourglass.svg';
|
||||
import u1140 from '../../../img/icons/unicons/layer-group.svg';
|
||||
import u1141 from '../../../img/icons/unicons/layers-alt.svg';
|
||||
import u1142 from '../../../img/icons/unicons/line-alt.svg';
|
||||
import u1143 from '../../../img/icons/unicons/list-ui-alt.svg';
|
||||
import u1144 from '../../../img/icons/unicons/message.svg';
|
||||
import u1145 from '../../../img/icons/unicons/palette.svg';
|
||||
import u1146 from '../../../img/icons/unicons/percentage.svg';
|
||||
import u1147 from '../../../img/icons/unicons/shield-exclamation.svg';
|
||||
import u1148 from '../../../img/icons/unicons/plus-square.svg';
|
||||
import u1149 from '../../../img/icons/unicons/x.svg';
|
||||
import u1150 from '../../../img/icons/unicons/capture.svg';
|
||||
import u1151 from '../../../img/icons/custom/gf-grid.svg';
|
||||
import u1152 from '../../../img/icons/custom/gf-landscape.svg';
|
||||
import u1153 from '../../../img/icons/custom/gf-layout-simple.svg';
|
||||
import u1154 from '../../../img/icons/custom/gf-portrait.svg';
|
||||
import u1155 from '../../../img/icons/custom/gf-show-context.svg';
|
||||
import u1156 from '../../../img/icons/custom/gf-bar-alignment-after.svg';
|
||||
import u1157 from '../../../img/icons/custom/gf-bar-alignment-before.svg';
|
||||
import u1158 from '../../../img/icons/custom/gf-bar-alignment-center.svg';
|
||||
import u1159 from '../../../img/icons/custom/gf-interpolation-linear.svg';
|
||||
import u1160 from '../../../img/icons/custom/gf-interpolation-smooth.svg';
|
||||
import u1161 from '../../../img/icons/custom/gf-interpolation-step-after.svg';
|
||||
import u1162 from '../../../img/icons/custom/gf-interpolation-step-before.svg';
|
||||
import u1163 from '../../../img/icons/custom/gf-logs.svg';
|
||||
import u1164 from '../../../img/icons/custom/gf-movepane-left.svg';
|
||||
import u1165 from '../../../img/icons/custom/gf-movepane-right.svg';
|
||||
import u1166 from '../../../img/icons/mono/favorite.svg';
|
||||
import u1167 from '../../../img/icons/mono/grafana.svg';
|
||||
import u1168 from '../../../img/icons/mono/heart.svg';
|
||||
import u1169 from '../../../img/icons/mono/heart-break.svg';
|
||||
import u1170 from '../../../img/icons/mono/panel-add.svg';
|
||||
import u1171 from '../../../img/icons/mono/library-panel.svg';
|
||||
import u1172 from '../../../img/icons/unicons/record-audio.svg';
|
||||
import u1173 from '../../../img/icons/solid/bookmark.svg';
|
||||
import u1039 from '../../../img/icons/unicons/times-circle.svg';
|
||||
import u1040 from '../../../img/icons/unicons/circle.svg';
|
||||
import u1041 from '../../../img/icons/unicons/clipboard-alt.svg';
|
||||
import u1042 from '../../../img/icons/unicons/clock-nine.svg';
|
||||
import u1043 from '../../../img/icons/unicons/cloud.svg';
|
||||
import u1044 from '../../../img/icons/unicons/cloud-download.svg';
|
||||
import u1045 from '../../../img/icons/unicons/code-branch.svg';
|
||||
import u1046 from '../../../img/icons/unicons/cog.svg';
|
||||
import u1047 from '../../../img/icons/unicons/columns.svg';
|
||||
import u1048 from '../../../img/icons/unicons/comment-alt.svg';
|
||||
import u1049 from '../../../img/icons/unicons/comment-alt-share.svg';
|
||||
import u1050 from '../../../img/icons/unicons/comments-alt.svg';
|
||||
import u1051 from '../../../img/icons/unicons/compass.svg';
|
||||
import u1052 from '../../../img/icons/unicons/copy.svg';
|
||||
import u1053 from '../../../img/icons/unicons/corner-down-right-alt.svg';
|
||||
import u1054 from '../../../img/icons/unicons/cube.svg';
|
||||
import u1055 from '../../../img/icons/unicons/dashboard.svg';
|
||||
import u1056 from '../../../img/icons/unicons/database.svg';
|
||||
import u1057 from '../../../img/icons/unicons/document-info.svg';
|
||||
import u1058 from '../../../img/icons/unicons/download-alt.svg';
|
||||
import u1059 from '../../../img/icons/unicons/draggabledots.svg';
|
||||
import u1060 from '../../../img/icons/unicons/edit.svg';
|
||||
import u1061 from '../../../img/icons/unicons/ellipsis-v.svg';
|
||||
import u1062 from '../../../img/icons/unicons/ellipsis-h.svg';
|
||||
import u1063 from '../../../img/icons/unicons/envelope.svg';
|
||||
import u1064 from '../../../img/icons/unicons/exchange-alt.svg';
|
||||
import u1065 from '../../../img/icons/unicons/exclamation-circle.svg';
|
||||
import u1066 from '../../../img/icons/unicons/exclamation-triangle.svg';
|
||||
import u1067 from '../../../img/icons/unicons/external-link-alt.svg';
|
||||
import u1068 from '../../../img/icons/unicons/eye.svg';
|
||||
import u1069 from '../../../img/icons/unicons/eye-slash.svg';
|
||||
import u1070 from '../../../img/icons/unicons/file-alt.svg';
|
||||
import u1071 from '../../../img/icons/unicons/file-blank.svg';
|
||||
import u1072 from '../../../img/icons/unicons/filter.svg';
|
||||
import u1073 from '../../../img/icons/unicons/folder.svg';
|
||||
import u1074 from '../../../img/icons/unicons/folder-open.svg';
|
||||
import u1075 from '../../../img/icons/unicons/folder-plus.svg';
|
||||
import u1076 from '../../../img/icons/unicons/folder-upload.svg';
|
||||
import u1077 from '../../../img/icons/unicons/forward.svg';
|
||||
import u1078 from '../../../img/icons/unicons/graph-bar.svg';
|
||||
import u1079 from '../../../img/icons/unicons/history.svg';
|
||||
import u1080 from '../../../img/icons/unicons/history-alt.svg';
|
||||
import u1081 from '../../../img/icons/unicons/home-alt.svg';
|
||||
import u1082 from '../../../img/icons/unicons/import.svg';
|
||||
import u1083 from '../../../img/icons/unicons/info.svg';
|
||||
import u1084 from '../../../img/icons/unicons/info-circle.svg';
|
||||
import u1085 from '../../../img/icons/unicons/k6.svg';
|
||||
import u1086 from '../../../img/icons/unicons/key-skeleton-alt.svg';
|
||||
import u1087 from '../../../img/icons/unicons/keyboard.svg';
|
||||
import u1088 from '../../../img/icons/unicons/link.svg';
|
||||
import u1089 from '../../../img/icons/unicons/list-ul.svg';
|
||||
import u1090 from '../../../img/icons/unicons/lock.svg';
|
||||
import u1091 from '../../../img/icons/unicons/minus.svg';
|
||||
import u1092 from '../../../img/icons/unicons/minus-circle.svg';
|
||||
import u1093 from '../../../img/icons/unicons/mobile-android.svg';
|
||||
import u1094 from '../../../img/icons/unicons/monitor.svg';
|
||||
import u1095 from '../../../img/icons/unicons/pause.svg';
|
||||
import u1096 from '../../../img/icons/unicons/pen.svg';
|
||||
import u1097 from '../../../img/icons/unicons/play.svg';
|
||||
import u1098 from '../../../img/icons/unicons/plug.svg';
|
||||
import u1099 from '../../../img/icons/unicons/plus.svg';
|
||||
import u1100 from '../../../img/icons/unicons/plus-circle.svg';
|
||||
import u1101 from '../../../img/icons/unicons/power.svg';
|
||||
import u1102 from '../../../img/icons/unicons/presentation-play.svg';
|
||||
import u1103 from '../../../img/icons/unicons/process.svg';
|
||||
import u1104 from '../../../img/icons/unicons/question-circle.svg';
|
||||
import u1105 from '../../../img/icons/unicons/repeat.svg';
|
||||
import u1106 from '../../../img/icons/unicons/rocket.svg';
|
||||
import u1107 from '../../../img/icons/unicons/rss.svg';
|
||||
import u1108 from '../../../img/icons/unicons/save.svg';
|
||||
import u1109 from '../../../img/icons/unicons/search.svg';
|
||||
import u1110 from '../../../img/icons/unicons/search-minus.svg';
|
||||
import u1111 from '../../../img/icons/unicons/search-plus.svg';
|
||||
import u1112 from '../../../img/icons/unicons/share-alt.svg';
|
||||
import u1113 from '../../../img/icons/unicons/shield.svg';
|
||||
import u1114 from '../../../img/icons/unicons/signal.svg';
|
||||
import u1115 from '../../../img/icons/unicons/signin.svg';
|
||||
import u1116 from '../../../img/icons/unicons/signout.svg';
|
||||
import u1117 from '../../../img/icons/unicons/sitemap.svg';
|
||||
import u1118 from '../../../img/icons/unicons/slack.svg';
|
||||
import u1119 from '../../../img/icons/unicons/sliders-v-alt.svg';
|
||||
import u1120 from '../../../img/icons/unicons/sort-amount-down.svg';
|
||||
import u1121 from '../../../img/icons/unicons/sort-amount-up.svg';
|
||||
import u1122 from '../../../img/icons/unicons/square-shape.svg';
|
||||
import u1123 from '../../../img/icons/unicons/star.svg';
|
||||
import u1124 from '../../../img/icons/unicons/step-backward.svg';
|
||||
import u1125 from '../../../img/icons/unicons/sync.svg';
|
||||
import u1126 from '../../../img/icons/unicons/stopwatch.svg';
|
||||
import u1127 from '../../../img/icons/unicons/table.svg';
|
||||
import u1128 from '../../../img/icons/unicons/tag-alt.svg';
|
||||
import u1129 from '../../../img/icons/unicons/times.svg';
|
||||
import u1130 from '../../../img/icons/unicons/trash-alt.svg';
|
||||
import u1131 from '../../../img/icons/unicons/unlock.svg';
|
||||
import u1132 from '../../../img/icons/unicons/upload.svg';
|
||||
import u1133 from '../../../img/icons/unicons/user.svg';
|
||||
import u1134 from '../../../img/icons/unicons/users-alt.svg';
|
||||
import u1135 from '../../../img/icons/unicons/wrap-text.svg';
|
||||
import u1136 from '../../../img/icons/unicons/cloud-upload.svg';
|
||||
import u1137 from '../../../img/icons/unicons/credit-card.svg';
|
||||
import u1138 from '../../../img/icons/unicons/file-copy-alt.svg';
|
||||
import u1139 from '../../../img/icons/unicons/fire.svg';
|
||||
import u1140 from '../../../img/icons/unicons/hourglass.svg';
|
||||
import u1141 from '../../../img/icons/unicons/layer-group.svg';
|
||||
import u1142 from '../../../img/icons/unicons/layers-alt.svg';
|
||||
import u1143 from '../../../img/icons/unicons/line-alt.svg';
|
||||
import u1144 from '../../../img/icons/unicons/list-ui-alt.svg';
|
||||
import u1145 from '../../../img/icons/unicons/message.svg';
|
||||
import u1146 from '../../../img/icons/unicons/palette.svg';
|
||||
import u1147 from '../../../img/icons/unicons/percentage.svg';
|
||||
import u1148 from '../../../img/icons/unicons/shield-exclamation.svg';
|
||||
import u1149 from '../../../img/icons/unicons/plus-square.svg';
|
||||
import u1150 from '../../../img/icons/unicons/x.svg';
|
||||
import u1151 from '../../../img/icons/unicons/capture.svg';
|
||||
import u1152 from '../../../img/icons/custom/gf-grid.svg';
|
||||
import u1153 from '../../../img/icons/custom/gf-landscape.svg';
|
||||
import u1154 from '../../../img/icons/custom/gf-layout-simple.svg';
|
||||
import u1155 from '../../../img/icons/custom/gf-portrait.svg';
|
||||
import u1156 from '../../../img/icons/custom/gf-show-context.svg';
|
||||
import u1157 from '../../../img/icons/custom/gf-bar-alignment-after.svg';
|
||||
import u1158 from '../../../img/icons/custom/gf-bar-alignment-before.svg';
|
||||
import u1159 from '../../../img/icons/custom/gf-bar-alignment-center.svg';
|
||||
import u1160 from '../../../img/icons/custom/gf-interpolation-linear.svg';
|
||||
import u1161 from '../../../img/icons/custom/gf-interpolation-smooth.svg';
|
||||
import u1162 from '../../../img/icons/custom/gf-interpolation-step-after.svg';
|
||||
import u1163 from '../../../img/icons/custom/gf-interpolation-step-before.svg';
|
||||
import u1164 from '../../../img/icons/custom/gf-logs.svg';
|
||||
import u1165 from '../../../img/icons/custom/gf-movepane-left.svg';
|
||||
import u1166 from '../../../img/icons/custom/gf-movepane-right.svg';
|
||||
import u1167 from '../../../img/icons/mono/favorite.svg';
|
||||
import u1168 from '../../../img/icons/mono/grafana.svg';
|
||||
import u1169 from '../../../img/icons/mono/heart.svg';
|
||||
import u1170 from '../../../img/icons/mono/heart-break.svg';
|
||||
import u1171 from '../../../img/icons/mono/panel-add.svg';
|
||||
import u1172 from '../../../img/icons/mono/library-panel.svg';
|
||||
import u1173 from '../../../img/icons/unicons/record-audio.svg';
|
||||
import u1174 from '../../../img/icons/solid/bookmark.svg';
|
||||
// do not edit this list directly
|
||||
// the list of icons live here: @grafana/ui/components/Icon/cached.json
|
||||
|
||||
@ -253,141 +254,142 @@ export function initIconCache() {
|
||||
cacheItem(u1036, resolvePath('unicons/chart-line.svg'));
|
||||
cacheItem(u1037, resolvePath('unicons/check.svg'));
|
||||
cacheItem(u1038, resolvePath('unicons/check-circle.svg'));
|
||||
cacheItem(u1039, resolvePath('unicons/circle.svg'));
|
||||
cacheItem(u1040, resolvePath('unicons/clipboard-alt.svg'));
|
||||
cacheItem(u1041, resolvePath('unicons/clock-nine.svg'));
|
||||
cacheItem(u1042, resolvePath('unicons/cloud.svg'));
|
||||
cacheItem(u1043, resolvePath('unicons/cloud-download.svg'));
|
||||
cacheItem(u1044, resolvePath('unicons/code-branch.svg'));
|
||||
cacheItem(u1045, resolvePath('unicons/cog.svg'));
|
||||
cacheItem(u1046, resolvePath('unicons/columns.svg'));
|
||||
cacheItem(u1047, resolvePath('unicons/comment-alt.svg'));
|
||||
cacheItem(u1048, resolvePath('unicons/comment-alt-share.svg'));
|
||||
cacheItem(u1049, resolvePath('unicons/comments-alt.svg'));
|
||||
cacheItem(u1050, resolvePath('unicons/compass.svg'));
|
||||
cacheItem(u1051, resolvePath('unicons/copy.svg'));
|
||||
cacheItem(u1052, resolvePath('unicons/corner-down-right-alt.svg'));
|
||||
cacheItem(u1053, resolvePath('unicons/cube.svg'));
|
||||
cacheItem(u1054, resolvePath('unicons/dashboard.svg'));
|
||||
cacheItem(u1055, resolvePath('unicons/database.svg'));
|
||||
cacheItem(u1056, resolvePath('unicons/document-info.svg'));
|
||||
cacheItem(u1057, resolvePath('unicons/download-alt.svg'));
|
||||
cacheItem(u1058, resolvePath('unicons/draggabledots.svg'));
|
||||
cacheItem(u1059, resolvePath('unicons/edit.svg'));
|
||||
cacheItem(u1060, resolvePath('unicons/ellipsis-v.svg'));
|
||||
cacheItem(u1061, resolvePath('unicons/ellipsis-h.svg'));
|
||||
cacheItem(u1062, resolvePath('unicons/envelope.svg'));
|
||||
cacheItem(u1063, resolvePath('unicons/exchange-alt.svg'));
|
||||
cacheItem(u1064, resolvePath('unicons/exclamation-circle.svg'));
|
||||
cacheItem(u1065, resolvePath('unicons/exclamation-triangle.svg'));
|
||||
cacheItem(u1066, resolvePath('unicons/external-link-alt.svg'));
|
||||
cacheItem(u1067, resolvePath('unicons/eye.svg'));
|
||||
cacheItem(u1068, resolvePath('unicons/eye-slash.svg'));
|
||||
cacheItem(u1069, resolvePath('unicons/file-alt.svg'));
|
||||
cacheItem(u1070, resolvePath('unicons/file-blank.svg'));
|
||||
cacheItem(u1071, resolvePath('unicons/filter.svg'));
|
||||
cacheItem(u1072, resolvePath('unicons/folder.svg'));
|
||||
cacheItem(u1073, resolvePath('unicons/folder-open.svg'));
|
||||
cacheItem(u1074, resolvePath('unicons/folder-plus.svg'));
|
||||
cacheItem(u1075, resolvePath('unicons/folder-upload.svg'));
|
||||
cacheItem(u1076, resolvePath('unicons/forward.svg'));
|
||||
cacheItem(u1077, resolvePath('unicons/graph-bar.svg'));
|
||||
cacheItem(u1078, resolvePath('unicons/history.svg'));
|
||||
cacheItem(u1079, resolvePath('unicons/history-alt.svg'));
|
||||
cacheItem(u1080, resolvePath('unicons/home-alt.svg'));
|
||||
cacheItem(u1081, resolvePath('unicons/import.svg'));
|
||||
cacheItem(u1082, resolvePath('unicons/info.svg'));
|
||||
cacheItem(u1083, resolvePath('unicons/info-circle.svg'));
|
||||
cacheItem(u1084, resolvePath('unicons/k6.svg'));
|
||||
cacheItem(u1085, resolvePath('unicons/key-skeleton-alt.svg'));
|
||||
cacheItem(u1086, resolvePath('unicons/keyboard.svg'));
|
||||
cacheItem(u1087, resolvePath('unicons/link.svg'));
|
||||
cacheItem(u1088, resolvePath('unicons/list-ul.svg'));
|
||||
cacheItem(u1089, resolvePath('unicons/lock.svg'));
|
||||
cacheItem(u1090, resolvePath('unicons/minus.svg'));
|
||||
cacheItem(u1091, resolvePath('unicons/minus-circle.svg'));
|
||||
cacheItem(u1092, resolvePath('unicons/mobile-android.svg'));
|
||||
cacheItem(u1093, resolvePath('unicons/monitor.svg'));
|
||||
cacheItem(u1094, resolvePath('unicons/pause.svg'));
|
||||
cacheItem(u1095, resolvePath('unicons/pen.svg'));
|
||||
cacheItem(u1096, resolvePath('unicons/play.svg'));
|
||||
cacheItem(u1097, resolvePath('unicons/plug.svg'));
|
||||
cacheItem(u1098, resolvePath('unicons/plus.svg'));
|
||||
cacheItem(u1099, resolvePath('unicons/plus-circle.svg'));
|
||||
cacheItem(u1100, resolvePath('unicons/power.svg'));
|
||||
cacheItem(u1101, resolvePath('unicons/presentation-play.svg'));
|
||||
cacheItem(u1102, resolvePath('unicons/process.svg'));
|
||||
cacheItem(u1103, resolvePath('unicons/question-circle.svg'));
|
||||
cacheItem(u1104, resolvePath('unicons/repeat.svg'));
|
||||
cacheItem(u1105, resolvePath('unicons/rocket.svg'));
|
||||
cacheItem(u1106, resolvePath('unicons/rss.svg'));
|
||||
cacheItem(u1107, resolvePath('unicons/save.svg'));
|
||||
cacheItem(u1108, resolvePath('unicons/search.svg'));
|
||||
cacheItem(u1109, resolvePath('unicons/search-minus.svg'));
|
||||
cacheItem(u1110, resolvePath('unicons/search-plus.svg'));
|
||||
cacheItem(u1111, resolvePath('unicons/share-alt.svg'));
|
||||
cacheItem(u1112, resolvePath('unicons/shield.svg'));
|
||||
cacheItem(u1113, resolvePath('unicons/signal.svg'));
|
||||
cacheItem(u1114, resolvePath('unicons/signin.svg'));
|
||||
cacheItem(u1115, resolvePath('unicons/signout.svg'));
|
||||
cacheItem(u1116, resolvePath('unicons/sitemap.svg'));
|
||||
cacheItem(u1117, resolvePath('unicons/slack.svg'));
|
||||
cacheItem(u1118, resolvePath('unicons/sliders-v-alt.svg'));
|
||||
cacheItem(u1119, resolvePath('unicons/sort-amount-down.svg'));
|
||||
cacheItem(u1120, resolvePath('unicons/sort-amount-up.svg'));
|
||||
cacheItem(u1121, resolvePath('unicons/square-shape.svg'));
|
||||
cacheItem(u1122, resolvePath('unicons/star.svg'));
|
||||
cacheItem(u1123, resolvePath('unicons/step-backward.svg'));
|
||||
cacheItem(u1124, resolvePath('unicons/sync.svg'));
|
||||
cacheItem(u1125, resolvePath('unicons/stopwatch.svg'));
|
||||
cacheItem(u1126, resolvePath('unicons/table.svg'));
|
||||
cacheItem(u1127, resolvePath('unicons/tag-alt.svg'));
|
||||
cacheItem(u1128, resolvePath('unicons/times.svg'));
|
||||
cacheItem(u1129, resolvePath('unicons/trash-alt.svg'));
|
||||
cacheItem(u1130, resolvePath('unicons/unlock.svg'));
|
||||
cacheItem(u1131, resolvePath('unicons/upload.svg'));
|
||||
cacheItem(u1132, resolvePath('unicons/user.svg'));
|
||||
cacheItem(u1133, resolvePath('unicons/users-alt.svg'));
|
||||
cacheItem(u1134, resolvePath('unicons/wrap-text.svg'));
|
||||
cacheItem(u1135, resolvePath('unicons/cloud-upload.svg'));
|
||||
cacheItem(u1136, resolvePath('unicons/credit-card.svg'));
|
||||
cacheItem(u1137, resolvePath('unicons/file-copy-alt.svg'));
|
||||
cacheItem(u1138, resolvePath('unicons/fire.svg'));
|
||||
cacheItem(u1139, resolvePath('unicons/hourglass.svg'));
|
||||
cacheItem(u1140, resolvePath('unicons/layer-group.svg'));
|
||||
cacheItem(u1141, resolvePath('unicons/layers-alt.svg'));
|
||||
cacheItem(u1142, resolvePath('unicons/line-alt.svg'));
|
||||
cacheItem(u1143, resolvePath('unicons/list-ui-alt.svg'));
|
||||
cacheItem(u1144, resolvePath('unicons/message.svg'));
|
||||
cacheItem(u1145, resolvePath('unicons/palette.svg'));
|
||||
cacheItem(u1146, resolvePath('unicons/percentage.svg'));
|
||||
cacheItem(u1147, resolvePath('unicons/shield-exclamation.svg'));
|
||||
cacheItem(u1148, resolvePath('unicons/plus-square.svg'));
|
||||
cacheItem(u1149, resolvePath('unicons/x.svg'));
|
||||
cacheItem(u1150, resolvePath('unicons/capture.svg'));
|
||||
cacheItem(u1151, resolvePath('custom/gf-grid.svg'));
|
||||
cacheItem(u1152, resolvePath('custom/gf-landscape.svg'));
|
||||
cacheItem(u1153, resolvePath('custom/gf-layout-simple.svg'));
|
||||
cacheItem(u1154, resolvePath('custom/gf-portrait.svg'));
|
||||
cacheItem(u1155, resolvePath('custom/gf-show-context.svg'));
|
||||
cacheItem(u1156, resolvePath('custom/gf-bar-alignment-after.svg'));
|
||||
cacheItem(u1157, resolvePath('custom/gf-bar-alignment-before.svg'));
|
||||
cacheItem(u1158, resolvePath('custom/gf-bar-alignment-center.svg'));
|
||||
cacheItem(u1159, resolvePath('custom/gf-interpolation-linear.svg'));
|
||||
cacheItem(u1160, resolvePath('custom/gf-interpolation-smooth.svg'));
|
||||
cacheItem(u1161, resolvePath('custom/gf-interpolation-step-after.svg'));
|
||||
cacheItem(u1162, resolvePath('custom/gf-interpolation-step-before.svg'));
|
||||
cacheItem(u1163, resolvePath('custom/gf-logs.svg'));
|
||||
cacheItem(u1164, resolvePath('custom/gf-movepane-left.svg'));
|
||||
cacheItem(u1165, resolvePath('custom/gf-movepane-right.svg'));
|
||||
cacheItem(u1166, resolvePath('mono/favorite.svg'));
|
||||
cacheItem(u1167, resolvePath('mono/grafana.svg'));
|
||||
cacheItem(u1168, resolvePath('mono/heart.svg'));
|
||||
cacheItem(u1169, resolvePath('mono/heart-break.svg'));
|
||||
cacheItem(u1170, resolvePath('mono/panel-add.svg'));
|
||||
cacheItem(u1171, resolvePath('mono/library-panel.svg'));
|
||||
cacheItem(u1172, resolvePath('unicons/record-audio.svg'));
|
||||
cacheItem(u1173, resolvePath('solid/bookmark.svg'));
|
||||
cacheItem(u1039, resolvePath('unicons/times-circle.svg'));
|
||||
cacheItem(u1040, resolvePath('unicons/circle.svg'));
|
||||
cacheItem(u1041, resolvePath('unicons/clipboard-alt.svg'));
|
||||
cacheItem(u1042, resolvePath('unicons/clock-nine.svg'));
|
||||
cacheItem(u1043, resolvePath('unicons/cloud.svg'));
|
||||
cacheItem(u1044, resolvePath('unicons/cloud-download.svg'));
|
||||
cacheItem(u1045, resolvePath('unicons/code-branch.svg'));
|
||||
cacheItem(u1046, resolvePath('unicons/cog.svg'));
|
||||
cacheItem(u1047, resolvePath('unicons/columns.svg'));
|
||||
cacheItem(u1048, resolvePath('unicons/comment-alt.svg'));
|
||||
cacheItem(u1049, resolvePath('unicons/comment-alt-share.svg'));
|
||||
cacheItem(u1050, resolvePath('unicons/comments-alt.svg'));
|
||||
cacheItem(u1051, resolvePath('unicons/compass.svg'));
|
||||
cacheItem(u1052, resolvePath('unicons/copy.svg'));
|
||||
cacheItem(u1053, resolvePath('unicons/corner-down-right-alt.svg'));
|
||||
cacheItem(u1054, resolvePath('unicons/cube.svg'));
|
||||
cacheItem(u1055, resolvePath('unicons/dashboard.svg'));
|
||||
cacheItem(u1056, resolvePath('unicons/database.svg'));
|
||||
cacheItem(u1057, resolvePath('unicons/document-info.svg'));
|
||||
cacheItem(u1058, resolvePath('unicons/download-alt.svg'));
|
||||
cacheItem(u1059, resolvePath('unicons/draggabledots.svg'));
|
||||
cacheItem(u1060, resolvePath('unicons/edit.svg'));
|
||||
cacheItem(u1061, resolvePath('unicons/ellipsis-v.svg'));
|
||||
cacheItem(u1062, resolvePath('unicons/ellipsis-h.svg'));
|
||||
cacheItem(u1063, resolvePath('unicons/envelope.svg'));
|
||||
cacheItem(u1064, resolvePath('unicons/exchange-alt.svg'));
|
||||
cacheItem(u1065, resolvePath('unicons/exclamation-circle.svg'));
|
||||
cacheItem(u1066, resolvePath('unicons/exclamation-triangle.svg'));
|
||||
cacheItem(u1067, resolvePath('unicons/external-link-alt.svg'));
|
||||
cacheItem(u1068, resolvePath('unicons/eye.svg'));
|
||||
cacheItem(u1069, resolvePath('unicons/eye-slash.svg'));
|
||||
cacheItem(u1070, resolvePath('unicons/file-alt.svg'));
|
||||
cacheItem(u1071, resolvePath('unicons/file-blank.svg'));
|
||||
cacheItem(u1072, resolvePath('unicons/filter.svg'));
|
||||
cacheItem(u1073, resolvePath('unicons/folder.svg'));
|
||||
cacheItem(u1074, resolvePath('unicons/folder-open.svg'));
|
||||
cacheItem(u1075, resolvePath('unicons/folder-plus.svg'));
|
||||
cacheItem(u1076, resolvePath('unicons/folder-upload.svg'));
|
||||
cacheItem(u1077, resolvePath('unicons/forward.svg'));
|
||||
cacheItem(u1078, resolvePath('unicons/graph-bar.svg'));
|
||||
cacheItem(u1079, resolvePath('unicons/history.svg'));
|
||||
cacheItem(u1080, resolvePath('unicons/history-alt.svg'));
|
||||
cacheItem(u1081, resolvePath('unicons/home-alt.svg'));
|
||||
cacheItem(u1082, resolvePath('unicons/import.svg'));
|
||||
cacheItem(u1083, resolvePath('unicons/info.svg'));
|
||||
cacheItem(u1084, resolvePath('unicons/info-circle.svg'));
|
||||
cacheItem(u1085, resolvePath('unicons/k6.svg'));
|
||||
cacheItem(u1086, resolvePath('unicons/key-skeleton-alt.svg'));
|
||||
cacheItem(u1087, resolvePath('unicons/keyboard.svg'));
|
||||
cacheItem(u1088, resolvePath('unicons/link.svg'));
|
||||
cacheItem(u1089, resolvePath('unicons/list-ul.svg'));
|
||||
cacheItem(u1090, resolvePath('unicons/lock.svg'));
|
||||
cacheItem(u1091, resolvePath('unicons/minus.svg'));
|
||||
cacheItem(u1092, resolvePath('unicons/minus-circle.svg'));
|
||||
cacheItem(u1093, resolvePath('unicons/mobile-android.svg'));
|
||||
cacheItem(u1094, resolvePath('unicons/monitor.svg'));
|
||||
cacheItem(u1095, resolvePath('unicons/pause.svg'));
|
||||
cacheItem(u1096, resolvePath('unicons/pen.svg'));
|
||||
cacheItem(u1097, resolvePath('unicons/play.svg'));
|
||||
cacheItem(u1098, resolvePath('unicons/plug.svg'));
|
||||
cacheItem(u1099, resolvePath('unicons/plus.svg'));
|
||||
cacheItem(u1100, resolvePath('unicons/plus-circle.svg'));
|
||||
cacheItem(u1101, resolvePath('unicons/power.svg'));
|
||||
cacheItem(u1102, resolvePath('unicons/presentation-play.svg'));
|
||||
cacheItem(u1103, resolvePath('unicons/process.svg'));
|
||||
cacheItem(u1104, resolvePath('unicons/question-circle.svg'));
|
||||
cacheItem(u1105, resolvePath('unicons/repeat.svg'));
|
||||
cacheItem(u1106, resolvePath('unicons/rocket.svg'));
|
||||
cacheItem(u1107, resolvePath('unicons/rss.svg'));
|
||||
cacheItem(u1108, resolvePath('unicons/save.svg'));
|
||||
cacheItem(u1109, resolvePath('unicons/search.svg'));
|
||||
cacheItem(u1110, resolvePath('unicons/search-minus.svg'));
|
||||
cacheItem(u1111, resolvePath('unicons/search-plus.svg'));
|
||||
cacheItem(u1112, resolvePath('unicons/share-alt.svg'));
|
||||
cacheItem(u1113, resolvePath('unicons/shield.svg'));
|
||||
cacheItem(u1114, resolvePath('unicons/signal.svg'));
|
||||
cacheItem(u1115, resolvePath('unicons/signin.svg'));
|
||||
cacheItem(u1116, resolvePath('unicons/signout.svg'));
|
||||
cacheItem(u1117, resolvePath('unicons/sitemap.svg'));
|
||||
cacheItem(u1118, resolvePath('unicons/slack.svg'));
|
||||
cacheItem(u1119, resolvePath('unicons/sliders-v-alt.svg'));
|
||||
cacheItem(u1120, resolvePath('unicons/sort-amount-down.svg'));
|
||||
cacheItem(u1121, resolvePath('unicons/sort-amount-up.svg'));
|
||||
cacheItem(u1122, resolvePath('unicons/square-shape.svg'));
|
||||
cacheItem(u1123, resolvePath('unicons/star.svg'));
|
||||
cacheItem(u1124, resolvePath('unicons/step-backward.svg'));
|
||||
cacheItem(u1125, resolvePath('unicons/sync.svg'));
|
||||
cacheItem(u1126, resolvePath('unicons/stopwatch.svg'));
|
||||
cacheItem(u1127, resolvePath('unicons/table.svg'));
|
||||
cacheItem(u1128, resolvePath('unicons/tag-alt.svg'));
|
||||
cacheItem(u1129, resolvePath('unicons/times.svg'));
|
||||
cacheItem(u1130, resolvePath('unicons/trash-alt.svg'));
|
||||
cacheItem(u1131, resolvePath('unicons/unlock.svg'));
|
||||
cacheItem(u1132, resolvePath('unicons/upload.svg'));
|
||||
cacheItem(u1133, resolvePath('unicons/user.svg'));
|
||||
cacheItem(u1134, resolvePath('unicons/users-alt.svg'));
|
||||
cacheItem(u1135, resolvePath('unicons/wrap-text.svg'));
|
||||
cacheItem(u1136, resolvePath('unicons/cloud-upload.svg'));
|
||||
cacheItem(u1137, resolvePath('unicons/credit-card.svg'));
|
||||
cacheItem(u1138, resolvePath('unicons/file-copy-alt.svg'));
|
||||
cacheItem(u1139, resolvePath('unicons/fire.svg'));
|
||||
cacheItem(u1140, resolvePath('unicons/hourglass.svg'));
|
||||
cacheItem(u1141, resolvePath('unicons/layer-group.svg'));
|
||||
cacheItem(u1142, resolvePath('unicons/layers-alt.svg'));
|
||||
cacheItem(u1143, resolvePath('unicons/line-alt.svg'));
|
||||
cacheItem(u1144, resolvePath('unicons/list-ui-alt.svg'));
|
||||
cacheItem(u1145, resolvePath('unicons/message.svg'));
|
||||
cacheItem(u1146, resolvePath('unicons/palette.svg'));
|
||||
cacheItem(u1147, resolvePath('unicons/percentage.svg'));
|
||||
cacheItem(u1148, resolvePath('unicons/shield-exclamation.svg'));
|
||||
cacheItem(u1149, resolvePath('unicons/plus-square.svg'));
|
||||
cacheItem(u1150, resolvePath('unicons/x.svg'));
|
||||
cacheItem(u1151, resolvePath('unicons/capture.svg'));
|
||||
cacheItem(u1152, resolvePath('custom/gf-grid.svg'));
|
||||
cacheItem(u1153, resolvePath('custom/gf-landscape.svg'));
|
||||
cacheItem(u1154, resolvePath('custom/gf-layout-simple.svg'));
|
||||
cacheItem(u1155, resolvePath('custom/gf-portrait.svg'));
|
||||
cacheItem(u1156, resolvePath('custom/gf-show-context.svg'));
|
||||
cacheItem(u1157, resolvePath('custom/gf-bar-alignment-after.svg'));
|
||||
cacheItem(u1158, resolvePath('custom/gf-bar-alignment-before.svg'));
|
||||
cacheItem(u1159, resolvePath('custom/gf-bar-alignment-center.svg'));
|
||||
cacheItem(u1160, resolvePath('custom/gf-interpolation-linear.svg'));
|
||||
cacheItem(u1161, resolvePath('custom/gf-interpolation-smooth.svg'));
|
||||
cacheItem(u1162, resolvePath('custom/gf-interpolation-step-after.svg'));
|
||||
cacheItem(u1163, resolvePath('custom/gf-interpolation-step-before.svg'));
|
||||
cacheItem(u1164, resolvePath('custom/gf-logs.svg'));
|
||||
cacheItem(u1165, resolvePath('custom/gf-movepane-left.svg'));
|
||||
cacheItem(u1166, resolvePath('custom/gf-movepane-right.svg'));
|
||||
cacheItem(u1167, resolvePath('mono/favorite.svg'));
|
||||
cacheItem(u1168, resolvePath('mono/grafana.svg'));
|
||||
cacheItem(u1169, resolvePath('mono/heart.svg'));
|
||||
cacheItem(u1170, resolvePath('mono/heart-break.svg'));
|
||||
cacheItem(u1171, resolvePath('mono/panel-add.svg'));
|
||||
cacheItem(u1172, resolvePath('mono/library-panel.svg'));
|
||||
cacheItem(u1173, resolvePath('unicons/record-audio.svg'));
|
||||
cacheItem(u1174, resolvePath('solid/bookmark.svg'));
|
||||
// do not edit this list directly
|
||||
// the list of icons live here: @grafana/ui/components/Icon/cached.json
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { withErrorBoundary } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertWarning } from './AlertWarning';
|
||||
@ -13,7 +11,6 @@ import { ExistingRuleEditor } from './ExistingRuleEditor';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||
import { useURLSearchParams } from './hooks/useURLSearchParams';
|
||||
import { fetchRulesSourceBuildInfoAction } from './state/actions';
|
||||
import { useRulesAccess } from './utils/accessControlHooks';
|
||||
import * as ruleId from './utils/rule-id';
|
||||
|
||||
@ -47,7 +44,6 @@ const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['ty
|
||||
};
|
||||
|
||||
const RuleEditor = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [searchParams] = useURLSearchParams();
|
||||
const params = useParams<RuleEditorPathParams>();
|
||||
const { type } = params;
|
||||
@ -57,22 +53,9 @@ const RuleEditor = () => {
|
||||
const copyFromId = searchParams.get('copyFrom') ?? undefined;
|
||||
const copyFromIdentifier = ruleId.tryParse(copyFromId);
|
||||
|
||||
const { loading = true } = useAsync(async () => {
|
||||
if (identifier) {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: identifier.ruleSourceName }));
|
||||
}
|
||||
if (copyFromIdentifier) {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: copyFromIdentifier.ruleSourceName }));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
|
||||
|
||||
const getContent = useCallback(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) {
|
||||
return <AlertWarning title="Cannot create rules">Sorry! You are not allowed to create rules.</AlertWarning>;
|
||||
}
|
||||
@ -90,10 +73,10 @@ const RuleEditor = () => {
|
||||
}
|
||||
// new alert rule
|
||||
return <AlertRuleForm />;
|
||||
}, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier, loading]);
|
||||
}, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier]);
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} navId="alert-list" pageNav={getPageNav(identifier, type)}>
|
||||
<AlertingPageWrapper navId="alert-list" pageNav={getPageNav(identifier, type)}>
|
||||
{getContent()}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
|
@ -2,20 +2,19 @@ import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
|
||||
import { screen, waitForElementToBeRemoved } from 'test/test-utils';
|
||||
import { byText } from 'testing-library-selector';
|
||||
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromApiFeatures, PromApplication } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { searchFolders } from '../../manage-dashboards/state/actions';
|
||||
|
||||
import { discoverFeatures } from './api/buildInfo';
|
||||
import { discoverFeaturesByUid } from './api/buildInfo';
|
||||
import { fetchRulerRulesGroup } from './api/ruler';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { setupMswServer } from './mockApi';
|
||||
import { grantUserPermissions, mockDataSource, MockDataSourceSrv } from './mocks';
|
||||
import * as config from './utils/config';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
import { grantUserPermissions, mockDataSource } from './mocks';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { DataSourceType, GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
|
||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
@ -49,10 +48,19 @@ jest.mock('./components/rule-editor/util', () => {
|
||||
});
|
||||
|
||||
const dataSources = {
|
||||
grafana: mockDataSource(
|
||||
{
|
||||
type: 'datasource',
|
||||
uid: GRAFANA_RULES_SOURCE_NAME,
|
||||
name: GRAFANA_DATASOURCE_NAME,
|
||||
},
|
||||
{ alerting: true }
|
||||
),
|
||||
// can edit rules
|
||||
loki: mockDataSource(
|
||||
{
|
||||
type: DataSourceType.Loki,
|
||||
uid: 'loki-with-ruler',
|
||||
name: 'loki with ruler',
|
||||
},
|
||||
{ alerting: true }
|
||||
@ -61,9 +69,8 @@ const dataSources = {
|
||||
{
|
||||
type: DataSourceType.Loki,
|
||||
name: 'loki disabled for alerting',
|
||||
jsonData: {
|
||||
manageAlerts: false,
|
||||
},
|
||||
uid: 'loki-without-alerting',
|
||||
jsonData: { manageAlerts: false },
|
||||
},
|
||||
{ alerting: true }
|
||||
),
|
||||
@ -72,6 +79,7 @@ const dataSources = {
|
||||
{
|
||||
type: DataSourceType.Prometheus,
|
||||
name: 'cortex with ruler',
|
||||
uid: 'cortex-with-ruler',
|
||||
isDefault: true,
|
||||
},
|
||||
{ alerting: true }
|
||||
@ -81,6 +89,7 @@ const dataSources = {
|
||||
{
|
||||
type: DataSourceType.Loki,
|
||||
name: 'loki with local rule store',
|
||||
uid: 'loki-with-local-rule-store',
|
||||
},
|
||||
{ alerting: true }
|
||||
),
|
||||
@ -89,6 +98,7 @@ const dataSources = {
|
||||
{
|
||||
type: DataSourceType.Loki,
|
||||
name: 'cortex without ruler api',
|
||||
uid: 'cortex-without-ruler-api',
|
||||
},
|
||||
{ alerting: true }
|
||||
),
|
||||
@ -97,27 +107,19 @@ const dataSources = {
|
||||
{
|
||||
type: 'splunk',
|
||||
name: 'splunk',
|
||||
uid: 'splunk',
|
||||
},
|
||||
{ alerting: true }
|
||||
),
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: jest.fn(() => ({
|
||||
getInstanceSettings: () => dataSources.prom,
|
||||
get: () => dataSources.prom,
|
||||
getList: () => Object.values(dataSources),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.spyOn(config, 'getAllDataSources');
|
||||
setupDataSources(...Object.values(dataSources));
|
||||
|
||||
const mocks = {
|
||||
getAllDataSources: jest.mocked(config.getAllDataSources),
|
||||
// getAllDataSources: jest.mocked(config.getAllDataSources),
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
api: {
|
||||
discoverFeatures: jest.mocked(discoverFeatures),
|
||||
discoverFeaturesByUid: jest.mocked(discoverFeaturesByUid),
|
||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
||||
},
|
||||
};
|
||||
@ -147,18 +149,18 @@ describe('RuleEditor cloud: checking editable data sources', () => {
|
||||
});
|
||||
|
||||
it('for cloud alerts, should only allow to select editable rules sources', async () => {
|
||||
mocks.api.discoverFeatures.mockImplementation(async (dataSourceName) => {
|
||||
if (dataSourceName === 'loki with ruler' || dataSourceName === 'cortex with ruler') {
|
||||
mocks.api.discoverFeaturesByUid.mockImplementation(async (dataSourceUid) => {
|
||||
if (dataSourceUid === dataSources.loki.uid || dataSourceUid === dataSources.prom.uid) {
|
||||
return getDiscoverFeaturesMock(PromApplication.Cortex, { rulerApiEnabled: true });
|
||||
}
|
||||
if (dataSourceName === 'loki with local rule store') {
|
||||
if (dataSourceUid === dataSources.loki_local_rule_store.uid) {
|
||||
return getDiscoverFeaturesMock(PromApplication.Cortex);
|
||||
}
|
||||
if (dataSourceName === 'cortex without ruler api') {
|
||||
if (dataSourceUid === dataSources.prom_no_ruler_api.uid) {
|
||||
return getDiscoverFeaturesMock(PromApplication.Cortex);
|
||||
}
|
||||
|
||||
throw new Error(`${dataSourceName} not handled`);
|
||||
throw new Error(`${dataSourceUid} not handled`);
|
||||
});
|
||||
|
||||
mocks.api.fetchRulerRulesGroup.mockImplementation(async ({ dataSourceName }) => {
|
||||
@ -179,8 +181,6 @@ describe('RuleEditor cloud: checking editable data sources', () => {
|
||||
return null;
|
||||
});
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.searchFolders.mockResolvedValue([]);
|
||||
|
||||
// render rule editor, select mimir/loki managed alerts
|
||||
|
@ -74,14 +74,12 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
]);
|
||||
|
||||
const dataSources = {
|
||||
default: mockDataSource(
|
||||
{
|
||||
default: mockDataSource({
|
||||
uid: 'mimir',
|
||||
type: 'prometheus',
|
||||
name: 'Prom',
|
||||
name: 'Mimir',
|
||||
isDefault: true,
|
||||
},
|
||||
{ alerting: false }
|
||||
),
|
||||
}),
|
||||
};
|
||||
setupDataSources(dataSources.default);
|
||||
setFolderResponse(mockFolder(folder));
|
||||
|
@ -6,7 +6,7 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { mockFeatureDiscoveryApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
@ -16,6 +16,7 @@ import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor
|
||||
import { grantUserPermissions, mockDataSource } from './mocks';
|
||||
import { grafanaRulerGroup, grafanaRulerRule } from './mocks/grafanaRulerApi';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { buildInfoResponse } from './testSetup/featureDiscovery';
|
||||
import * as config from './utils/config';
|
||||
|
||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
@ -45,7 +46,7 @@ const mocks = {
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
const server = setupMswServer();
|
||||
|
||||
describe('RuleEditor grafana managed rules', () => {
|
||||
beforeEach(() => {
|
||||
@ -80,6 +81,8 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
};
|
||||
|
||||
setupDataSources(dataSources.default);
|
||||
mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.default, buildInfoResponse.mimir);
|
||||
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.searchFolders.mockResolvedValue([
|
||||
{
|
||||
|
@ -20,7 +20,7 @@ import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerti
|
||||
|
||||
import * as analytics from './Analytics';
|
||||
import RuleList from './RuleList';
|
||||
import { discoverFeatures } from './api/buildInfo';
|
||||
import { discoverFeaturesByUid } from './api/buildInfo';
|
||||
import { fetchRules } from './api/prometheus';
|
||||
import * as apiRuler from './api/ruler';
|
||||
import { fetchRulerRules } from './api/ruler';
|
||||
@ -68,7 +68,7 @@ const mocks = {
|
||||
rulesInSameGroupHaveInvalidForMock: jest.mocked(actions.rulesInSameGroupHaveInvalidFor),
|
||||
|
||||
api: {
|
||||
discoverFeatures: jest.mocked(discoverFeatures),
|
||||
discoverFeaturesByUid: jest.mocked(discoverFeaturesByUid),
|
||||
fetchRules: jest.mocked(fetchRules),
|
||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
||||
rulerBuilderMock: jest.mocked(apiRuler.rulerUrlBuilder),
|
||||
@ -185,7 +185,7 @@ describe('RuleList', () => {
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Prometheus,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
@ -279,7 +279,7 @@ describe('RuleList', () => {
|
||||
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
@ -430,7 +430,7 @@ describe('RuleList', () => {
|
||||
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
||||
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
@ -577,7 +577,7 @@ describe('RuleList', () => {
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
@ -688,7 +688,7 @@ describe('RuleList', () => {
|
||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(testDatasources));
|
||||
setDataSourceSrv(new MockDataSourceSrv(testDatasources));
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
@ -855,7 +855,7 @@ describe('RuleList', () => {
|
||||
|
||||
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
||||
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
@ -880,7 +880,7 @@ describe('RuleList', () => {
|
||||
|
||||
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
||||
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
mocks.api.discoverFeaturesByUid.mockResolvedValue({
|
||||
application: PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
|
@ -25,9 +25,9 @@ import { isCloudRuleIdentifier, isPrometheusRuleIdentifier } from '../utils/rule
|
||||
import { alertingApi, WithNotificationOptions } from './alertingApi';
|
||||
import {
|
||||
FetchPromRulesFilter,
|
||||
getRulesFilterSearchParams,
|
||||
groupRulesByFileName,
|
||||
paramsWithMatcherAndState,
|
||||
getRulesFilterSearchParams,
|
||||
} from './prometheus';
|
||||
import { FetchRulerRulesFilter, rulerUrlBuilder } from './ruler';
|
||||
|
||||
@ -173,9 +173,21 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
dashboardUid?: string;
|
||||
panelId?: number;
|
||||
limitAlerts?: number;
|
||||
maxGroups?: number;
|
||||
excludeAlerts?: boolean;
|
||||
}
|
||||
>({
|
||||
query: ({ ruleSourceName, namespace, groupName, ruleName, dashboardUid, panelId, limitAlerts }) => {
|
||||
query: ({
|
||||
ruleSourceName,
|
||||
namespace,
|
||||
groupName,
|
||||
ruleName,
|
||||
dashboardUid,
|
||||
panelId,
|
||||
limitAlerts,
|
||||
maxGroups,
|
||||
excludeAlerts,
|
||||
}) => {
|
||||
const queryParams: Record<string, string | undefined> = {
|
||||
rule_group: groupName,
|
||||
rule_name: ruleName,
|
||||
@ -195,6 +207,14 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
set(queryParams, PrometheusAPIFilters.LimitAlerts, String(limitAlerts));
|
||||
}
|
||||
|
||||
if (maxGroups) {
|
||||
set(queryParams, 'max_groups', maxGroups);
|
||||
}
|
||||
|
||||
if (excludeAlerts) {
|
||||
set(queryParams, 'exclude_alerts', 'true');
|
||||
}
|
||||
|
||||
return {
|
||||
url: `api/prometheus/${getDatasourceAPIUid(ruleSourceName)}/api/v1/rules`,
|
||||
params: queryParams,
|
||||
|
@ -95,7 +95,7 @@ describe('discoverDataSourceFeatures', () => {
|
||||
|
||||
const response = await discoverDataSourceFeatures({ url: '/datasource/proxy', name: 'Loki', type: 'loki' });
|
||||
|
||||
expect(response.application).toBe(PromApplication.Cortex);
|
||||
expect(response.application).toBe('Loki');
|
||||
expect(response.features.rulerApiEnabled).toBe(true);
|
||||
|
||||
expect(mocks.fetchTestRulerRulesGroup).toHaveBeenCalledTimes(1);
|
||||
|
@ -9,26 +9,24 @@ import {
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
|
||||
import { getDataSourceByName, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { getDataSourceByName, getRulesDataSourceByUID, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
import { fetchRules } from './prometheus';
|
||||
import { fetchTestRulerRulesGroup } from './ruler';
|
||||
|
||||
/**
|
||||
* Attempt to fetch buildinfo from our component
|
||||
*/
|
||||
export async function discoverFeatures(dataSourceName: string): Promise<PromApiFeatures> {
|
||||
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||
export async function discoverFeaturesByUid(dataSourceUid: string): Promise<PromApiFeatures> {
|
||||
if (dataSourceUid === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return {
|
||||
application: 'grafana',
|
||||
features: {
|
||||
rulerApiEnabled: true,
|
||||
},
|
||||
};
|
||||
} satisfies PromApiFeatures;
|
||||
}
|
||||
|
||||
const dsConfig = getDataSourceByName(dataSourceName);
|
||||
const dsConfig = getRulesDataSourceByUID(dataSourceUid);
|
||||
if (!dsConfig) {
|
||||
throw new Error(`Cannot find data source configuration for ${dataSourceName}`);
|
||||
throw new Error(`Cannot find data source configuration for ${dataSourceUid}`);
|
||||
}
|
||||
|
||||
const { url, name, type } = dsConfig;
|
||||
@ -78,7 +76,8 @@ export async function discoverDataSourceFeatures(dsSettings: {
|
||||
const rulerSupported = await hasRulerSupport(name);
|
||||
|
||||
return {
|
||||
application: PromApplication.Cortex,
|
||||
// if we were not trying to discover ruler support for a "loki" type data source then assume it's Cortex.
|
||||
application: type === 'loki' ? 'Loki' : PromApplication.Cortex,
|
||||
features: {
|
||||
rulerApiEnabled: rulerSupported,
|
||||
},
|
||||
|
@ -1,17 +1,32 @@
|
||||
import { RulerDataSourceConfig } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertmanagerApiFeatures, PromApplication } from '../../../../types/unified-alerting-dto';
|
||||
import { withPerformanceLogging } from '../Analytics';
|
||||
import { getRulesDataSource, isGrafanaRulesSource } from '../utils/datasource';
|
||||
import {
|
||||
AlertmanagerApiFeatures,
|
||||
PromApplication,
|
||||
RulesSourceApplication,
|
||||
} from '../../../../types/unified-alerting-dto';
|
||||
import {
|
||||
getDataSourceUID,
|
||||
getRulesDataSourceByUID,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
isGrafanaRulesSource,
|
||||
} from '../utils/datasource';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
import { discoverAlertmanagerFeatures, discoverFeatures } from './buildInfo';
|
||||
import { discoverAlertmanagerFeatures, discoverFeaturesByUid } from './buildInfo';
|
||||
|
||||
export const GRAFANA_RULER_CONFIG: RulerDataSourceConfig = {
|
||||
dataSourceName: 'grafana',
|
||||
apiVersion: 'legacy',
|
||||
};
|
||||
|
||||
interface RulesSourceFeatures {
|
||||
name: string;
|
||||
uid: string;
|
||||
application: RulesSourceApplication;
|
||||
rulerConfig?: RulerDataSourceConfig;
|
||||
}
|
||||
|
||||
export const featureDiscoveryApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
discoverAmFeatures: build.query<AlertmanagerApiFeatures, { amSourceName: string }>({
|
||||
@ -25,36 +40,46 @@ export const featureDiscoveryApi = alertingApi.injectEndpoints({
|
||||
},
|
||||
}),
|
||||
|
||||
discoverDsFeatures: build.query<{ rulerConfig?: RulerDataSourceConfig }, { rulesSourceName: string }>({
|
||||
queryFn: async ({ rulesSourceName }) => {
|
||||
if (isGrafanaRulesSource(rulesSourceName)) {
|
||||
return { data: { rulerConfig: GRAFANA_RULER_CONFIG } };
|
||||
discoverDsFeatures: build.query<RulesSourceFeatures, { rulesSourceName: string } | { uid: string }>({
|
||||
queryFn: async (rulesSourceIdentifier) => {
|
||||
const dataSourceUID = getDataSourceUID(rulesSourceIdentifier);
|
||||
if (!dataSourceUID) {
|
||||
return { error: new Error(`Unable to find data source for ${rulesSourceIdentifier}`) };
|
||||
}
|
||||
|
||||
const dsSettings = getRulesDataSource(rulesSourceName);
|
||||
if (!dsSettings) {
|
||||
return { error: new Error(`Missing data source configuration for ${rulesSourceName}`) };
|
||||
if (isGrafanaRulesSource(dataSourceUID)) {
|
||||
return {
|
||||
data: {
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
uid: GRAFANA_RULES_SOURCE_NAME,
|
||||
application: 'grafana',
|
||||
rulerConfig: GRAFANA_RULER_CONFIG,
|
||||
} satisfies RulesSourceFeatures,
|
||||
};
|
||||
}
|
||||
|
||||
const discoverFeaturesWithLogging = withPerformanceLogging(
|
||||
'unifiedalerting/featureDiscoveryApi/discoverDsFeatures',
|
||||
discoverFeatures,
|
||||
{
|
||||
dataSourceName: rulesSourceName,
|
||||
endpoint: 'unifiedalerting/featureDiscoveryApi/discoverDsFeatures',
|
||||
const dataSourceSettings = dataSourceUID ? getRulesDataSourceByUID(dataSourceUID) : undefined;
|
||||
if (!dataSourceSettings) {
|
||||
return { error: new Error(`Missing data source configuration for ${rulesSourceIdentifier}`) };
|
||||
}
|
||||
);
|
||||
|
||||
const dsFeatures = await discoverFeaturesWithLogging(dsSettings.name);
|
||||
const features = await discoverFeaturesByUid(dataSourceSettings.uid);
|
||||
|
||||
const rulerConfig: RulerDataSourceConfig | undefined = dsFeatures.features.rulerApiEnabled
|
||||
? {
|
||||
dataSourceName: dsSettings.name,
|
||||
apiVersion: dsFeatures.application === PromApplication.Cortex ? 'legacy' : 'config',
|
||||
}
|
||||
const rulerConfig = features.features.rulerApiEnabled
|
||||
? ({
|
||||
dataSourceName: dataSourceSettings.name,
|
||||
apiVersion: features.application === PromApplication.Cortex ? 'legacy' : 'config',
|
||||
} satisfies RulerDataSourceConfig)
|
||||
: undefined;
|
||||
|
||||
return { data: { rulerConfig } };
|
||||
return {
|
||||
data: {
|
||||
name: dataSourceSettings.name,
|
||||
uid: dataSourceSettings.uid,
|
||||
application: features.application,
|
||||
rulerConfig,
|
||||
} satisfies RulesSourceFeatures,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { Menu } from '@grafana/ui';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import {
|
||||
isGrafanaRulerRule,
|
||||
isGrafanaRulerRulePaused,
|
||||
getRuleGroupLocationFromCombinedRule,
|
||||
} from 'app/features/alerting/unified/utils/rules';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from 'app/features/alerting/unified/utils/rules';
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { usePauseRuleInGroup } from '../hooks/ruleGroup/usePauseAlertRule';
|
||||
import { isLoading } from '../hooks/useAsync';
|
||||
import { stringifyErrorLike } from '../utils/misc';
|
||||
|
||||
interface Props {
|
||||
rule: CombinedRule;
|
||||
rule: RulerRuleDTO;
|
||||
groupIdentifier: RuleGroupIdentifier;
|
||||
/**
|
||||
* Method invoked after the request to change the paused state has completed
|
||||
*/
|
||||
@ -23,11 +21,11 @@ interface Props {
|
||||
* Menu item to display correct text for pausing/resuming an alert,
|
||||
* and triggering API call to do so
|
||||
*/
|
||||
const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
|
||||
const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => {
|
||||
const notifyApp = useAppNotification();
|
||||
const [pauseRule, updateState] = usePauseRuleInGroup();
|
||||
|
||||
const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule);
|
||||
const isPaused = isGrafanaRulerRule(rule) && isGrafanaRulerRulePaused(rule);
|
||||
const icon = isPaused ? 'play' : 'pause';
|
||||
const title = isPaused ? 'Resume evaluation' : 'Pause evaluation';
|
||||
|
||||
@ -35,15 +33,14 @@ const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
|
||||
* Triggers API call to update the current rule to the new `is_paused` state
|
||||
*/
|
||||
const setRulePause = async (newIsPaused: boolean) => {
|
||||
if (!isGrafanaRulerRule(rule.rulerRule)) {
|
||||
if (!isGrafanaRulerRule(rule)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ruleGroupId = getRuleGroupLocationFromCombinedRule(rule);
|
||||
const ruleUID = rule.rulerRule.grafana_alert.uid;
|
||||
const ruleUID = rule.grafana_alert.uid;
|
||||
|
||||
await pauseRule.execute(ruleGroupId, ruleUID, newIsPaused);
|
||||
await pauseRule.execute(groupIdentifier, ruleUID, newIsPaused);
|
||||
} catch (error) {
|
||||
notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`);
|
||||
return;
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
|
||||
import { fetchAllPromBuildInfoAction } from '../../state/actions';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
@ -17,20 +14,18 @@ interface Props {
|
||||
}
|
||||
|
||||
export function CloudRulesSourcePicker({ value, disabled, ...props }: Props): JSX.Element {
|
||||
const rulesSourcesWithRuler = useRulesSourcesWithRuler();
|
||||
|
||||
const { loading = true } = useAsync(() => dispatch(fetchAllPromBuildInfoAction()), [dispatch]);
|
||||
const { rulesSourcesWithRuler: dataSourcesWithRuler, isLoading } = useRulesSourcesWithRuler();
|
||||
|
||||
const dataSourceFilter = useCallback(
|
||||
(ds: DataSourceInstanceSettings): boolean => {
|
||||
return !!rulesSourcesWithRuler.find(({ id }) => id === ds.id);
|
||||
return dataSourcesWithRuler.some(({ uid }) => uid === ds.uid);
|
||||
},
|
||||
[rulesSourcesWithRuler]
|
||||
[dataSourcesWithRuler]
|
||||
);
|
||||
|
||||
return (
|
||||
<DataSourcePicker
|
||||
disabled={loading || disabled}
|
||||
disabled={isLoading || disabled}
|
||||
noDefault
|
||||
alerting
|
||||
filter={dataSourceFilter}
|
||||
|
@ -8,13 +8,14 @@ import { byRole } from 'testing-library-selector';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import RuleEditor from 'app/features/alerting/unified/RuleEditor';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { mockFeatureDiscoveryApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure';
|
||||
import { captureRequests, serializeRequests } from 'app/features/alerting/unified/mocks/server/events';
|
||||
import { FOLDER_TITLE_HAPPY_PATH } from 'app/features/alerting/unified/mocks/server/handlers/search';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
|
||||
import { buildInfoResponse } from 'app/features/alerting/unified/testSetup/featureDiscovery';
|
||||
import { DataSourceType, GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -28,7 +29,7 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
|
||||
jest.setTimeout(60 * 1000);
|
||||
|
||||
setupMswServer();
|
||||
const server = setupMswServer();
|
||||
|
||||
const dataSources = {
|
||||
default: mockDataSource(
|
||||
@ -44,6 +45,7 @@ const dataSources = {
|
||||
type: DataSourceType.Alertmanager,
|
||||
}),
|
||||
};
|
||||
|
||||
setupDataSources(dataSources.default, dataSources.am);
|
||||
|
||||
const selectFolderAndGroup = async () => {
|
||||
@ -76,6 +78,8 @@ describe('Can create a new grafana managed alert using simplified routing', () =
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
]);
|
||||
|
||||
mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.default, buildInfoResponse.mimir);
|
||||
});
|
||||
|
||||
it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => {
|
||||
|
@ -29,12 +29,10 @@ import {
|
||||
expressionTypes,
|
||||
ReducerMode,
|
||||
} from 'app/features/expressions/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
|
||||
import { useURLSearchParams } from '../../../hooks/useURLSearchParams';
|
||||
import { fetchAllPromBuildInfoAction } from '../../../state/actions';
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource';
|
||||
import { isPromOrLokiQuery, PromOrLokiQuery } from '../../../utils/rule-form';
|
||||
@ -184,12 +182,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
||||
}
|
||||
}, [isAdvancedMode, expressionQueries, isGrafanaAlertingType, setSimpleCondition]);
|
||||
|
||||
const dispatchReduxAction = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatchReduxAction(fetchAllPromBuildInfoAction());
|
||||
}, [dispatchReduxAction]);
|
||||
|
||||
const rulesSourcesWithRuler = useRulesSourcesWithRuler();
|
||||
const { rulesSourcesWithRuler } = useRulesSourcesWithRuler();
|
||||
|
||||
const runQueriesPreview = useCallback(
|
||||
(condition?: string) => {
|
||||
@ -322,7 +315,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
||||
[runQueriesPreview, setValue, updateExpressionAndDatasource]
|
||||
);
|
||||
|
||||
const recordingRuleDefaultDatasource = rulesSourcesWithRuler[0];
|
||||
// Using dataSourcesWithRuler[0] gives incorrect types - no undefined
|
||||
// Using at(0) provides a safe type with undefined
|
||||
const recordingRuleDefaultDatasource = rulesSourcesWithRuler.at(0);
|
||||
|
||||
useEffect(() => {
|
||||
clearPreviewData();
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { useStyles2, Stack } from '@grafana/ui';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
|
||||
import { fetchAllPromBuildInfoAction } from '../../../state/actions';
|
||||
import { RuleFormType } from '../../../types/rule-form';
|
||||
|
||||
import { GrafanaManagedRuleType } from './GrafanaManagedAlert';
|
||||
@ -19,13 +16,9 @@ interface RuleTypePickerProps {
|
||||
}
|
||||
|
||||
const RuleTypePicker = ({ selected, onChange, enabledTypes }: RuleTypePickerProps) => {
|
||||
const rulesSourcesWithRuler = useRulesSourcesWithRuler();
|
||||
const { rulesSourcesWithRuler } = useRulesSourcesWithRuler();
|
||||
const hasLotexDatasources = !isEmpty(rulesSourcesWithRuler);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAllPromBuildInfoAction());
|
||||
}, []);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const handleChange = (type: RuleFormType) => {
|
||||
|
@ -4,22 +4,23 @@ import appEvents from 'app/core/app_events';
|
||||
import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
|
||||
import MoreButton from 'app/features/alerting/unified/components/MoreButton';
|
||||
import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugins/useRulePluginLinkExtensions';
|
||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
import { Rule, RuleGroupIdentifier, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
|
||||
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
|
||||
|
||||
interface Props {
|
||||
rule: CombinedRule;
|
||||
promRule: Rule;
|
||||
rulerRule?: RulerRuleDTO;
|
||||
identifier: RuleIdentifier;
|
||||
showCopyLinkButton?: boolean;
|
||||
groupIdentifier: RuleGroupIdentifier;
|
||||
handleSilence: () => void;
|
||||
handleDelete: (rule: CombinedRule) => void;
|
||||
handleDelete: (rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifier) => void;
|
||||
handleDuplicateRule: (identifier: RuleIdentifier) => void;
|
||||
onPauseChange?: () => void;
|
||||
buttonSize?: ComponentSize;
|
||||
@ -30,9 +31,10 @@ interface Props {
|
||||
* dropdown menu
|
||||
*/
|
||||
const AlertRuleMenu = ({
|
||||
rule,
|
||||
promRule,
|
||||
rulerRule,
|
||||
identifier,
|
||||
showCopyLinkButton,
|
||||
groupIdentifier,
|
||||
handleSilence,
|
||||
handleDelete,
|
||||
handleDuplicateRule,
|
||||
@ -40,22 +42,30 @@ const AlertRuleMenu = ({
|
||||
buttonSize,
|
||||
}: Props) => {
|
||||
// check all abilities and permissions
|
||||
const [pauseSupported, pauseAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Pause);
|
||||
const [pauseSupported, pauseAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Pause);
|
||||
const canPause = pauseSupported && pauseAllowed;
|
||||
|
||||
const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
|
||||
const [deleteSupported, deleteAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Delete);
|
||||
const canDelete = deleteSupported && deleteAllowed;
|
||||
|
||||
const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
|
||||
const [duplicateSupported, duplicateAllowed] = useRulerRuleAbility(
|
||||
rulerRule,
|
||||
groupIdentifier,
|
||||
AlertRuleAction.Duplicate
|
||||
);
|
||||
const canDuplicate = duplicateSupported && duplicateAllowed;
|
||||
|
||||
const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
|
||||
const [silenceSupported, silenceAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Silence);
|
||||
const canSilence = silenceSupported && silenceAllowed;
|
||||
|
||||
const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
|
||||
const [exportSupported, exportAllowed] = useRulerRuleAbility(
|
||||
rulerRule,
|
||||
groupIdentifier,
|
||||
AlertRuleAction.ModifyExport
|
||||
);
|
||||
const canExport = exportSupported && exportAllowed;
|
||||
|
||||
const ruleExtensionLinks = useRulePluginLinkExtension(rule);
|
||||
const ruleExtensionLinks = useRulePluginLinkExtension(promRule, groupIdentifier);
|
||||
|
||||
const extensionsAvailable = ruleExtensionLinks.length > 0;
|
||||
|
||||
@ -63,21 +73,25 @@ const AlertRuleMenu = ({
|
||||
* Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana.
|
||||
* We should show it in development mode
|
||||
*/
|
||||
// @TODO Migrate "declare incident button" to plugin links extensions
|
||||
const shouldShowDeclareIncidentButton =
|
||||
(!isOpenSourceEdition() || isLocalDevEnv()) &&
|
||||
isAlertingRule(rule.promRule) &&
|
||||
rule.promRule.state === PromAlertingRuleState.Firing;
|
||||
const shareUrl = createShareLink(rule.namespace.rulesSource, rule);
|
||||
isAlertingRule(promRule) &&
|
||||
promRule.state === PromAlertingRuleState.Firing;
|
||||
|
||||
const shareUrl = createShareLink(identifier);
|
||||
|
||||
const showDivider =
|
||||
[canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) &&
|
||||
[showCopyLinkButton, canExport].some(Boolean);
|
||||
[canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) && [canExport].some(Boolean);
|
||||
|
||||
const menuItems = (
|
||||
<>
|
||||
{canPause && <MenuItemPauseRule rule={rule} onPauseChange={onPauseChange} />}
|
||||
{canPause && rulerRule && (
|
||||
<MenuItemPauseRule rule={rulerRule} groupIdentifier={groupIdentifier} onPauseChange={onPauseChange} />
|
||||
)}
|
||||
{canSilence && <Menu.Item label="Silence notifications" icon="bell-slash" onClick={handleSilence} />}
|
||||
{shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={rule.name} url={''} />}
|
||||
{/* TODO Migrate Declare Incident to plugin links extensions */}
|
||||
{shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={promRule.name} url={''} />}
|
||||
{canDuplicate && <Menu.Item label="Duplicate" icon="copy" onClick={() => handleDuplicateRule(identifier)} />}
|
||||
{showDivider && <Menu.Divider />}
|
||||
{shareUrl && <Menu.Item label="Copy link" icon="share-alt" onClick={() => copyToClipboard(shareUrl)} />}
|
||||
@ -96,10 +110,15 @@ const AlertRuleMenu = ({
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
{canDelete && rulerRule && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => handleDelete(rule)} />
|
||||
<Menu.Item
|
||||
label="Delete"
|
||||
icon="trash-alt"
|
||||
destructive
|
||||
onClick={() => handleDelete(rulerRule, groupIdentifier)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@ -3,21 +3,23 @@ import { useState, useCallback, useMemo } from 'react';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { ConfirmModal } from '@grafana/ui';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { shouldUsePrometheusRulesPrimary } from '../../featureToggles';
|
||||
import { useDeleteRuleFromGroup } from '../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||
import { usePrometheusConsistencyCheck } from '../../hooks/usePrometheusConsistencyCheck';
|
||||
import { fetchPromAndRulerRulesAction, fetchRulerRulesAction } from '../../state/actions';
|
||||
import { fromRulerRuleAndRuleGroupIdentifier } from '../../utils/rule-id';
|
||||
import { getRuleGroupLocationFromCombinedRule, isCloudRuleIdentifier } from '../../utils/rules';
|
||||
import { isCloudRuleIdentifier } from '../../utils/rules';
|
||||
|
||||
type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void];
|
||||
type DeleteModalHook = [JSX.Element, (rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifier) => void, () => void];
|
||||
type DeleteRuleInfo = { rule: RulerRuleDTO; groupIdentifier: RuleGroupIdentifier } | undefined;
|
||||
|
||||
const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
|
||||
|
||||
export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
|
||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule | undefined>();
|
||||
const [ruleToDelete, setRuleToDelete] = useState<DeleteRuleInfo>();
|
||||
const [deleteRuleFromGroup] = useDeleteRuleFromGroup();
|
||||
const { waitForRemoval } = usePrometheusConsistencyCheck();
|
||||
|
||||
@ -25,30 +27,29 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
|
||||
setRuleToDelete(undefined);
|
||||
}, []);
|
||||
|
||||
const showModal = useCallback((rule: CombinedRule) => {
|
||||
setRuleToDelete(rule);
|
||||
const showModal = useCallback((rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifier) => {
|
||||
setRuleToDelete({ rule, groupIdentifier });
|
||||
}, []);
|
||||
|
||||
const deleteRule = useCallback(
|
||||
async (rule?: CombinedRule) => {
|
||||
if (!rule?.rulerRule) {
|
||||
const deleteRule = useCallback(async () => {
|
||||
if (!ruleToDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ruleGroupIdentifier = getRuleGroupLocationFromCombinedRule(rule);
|
||||
const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, rule.rulerRule);
|
||||
const { rule, groupIdentifier } = ruleToDelete;
|
||||
|
||||
await deleteRuleFromGroup.execute(ruleGroupIdentifier, ruleIdentifier);
|
||||
const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(groupIdentifier, rule);
|
||||
await deleteRuleFromGroup.execute(groupIdentifier, ruleIdentifier);
|
||||
|
||||
// refetch rules for this rules source
|
||||
// @TODO remove this when we moved everything to RTKQ – then the endpoint will simply invalidate the tags
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: ruleGroupIdentifier.dataSourceName }));
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: groupIdentifier.dataSourceName }));
|
||||
|
||||
if (prometheusRulesPrimary && isCloudRuleIdentifier(ruleIdentifier)) {
|
||||
await waitForRemoval(ruleIdentifier);
|
||||
} else {
|
||||
// Without this the delete popup will close and the user will still see the deleted rule
|
||||
await dispatch(fetchRulerRulesAction({ rulesSourceName: ruleGroupIdentifier.dataSourceName }));
|
||||
await dispatch(fetchRulerRulesAction({ rulesSourceName: groupIdentifier.dataSourceName }));
|
||||
}
|
||||
|
||||
dismissModal();
|
||||
@ -56,9 +57,7 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
|
||||
if (redirectToListView) {
|
||||
locationService.replace('/alerting/list');
|
||||
}
|
||||
},
|
||||
[deleteRuleFromGroup, dismissModal, redirectToListView, waitForRemoval]
|
||||
);
|
||||
}, [deleteRuleFromGroup, dismissModal, ruleToDelete, redirectToListView, waitForRemoval]);
|
||||
|
||||
const modal = useMemo(
|
||||
() => (
|
||||
@ -68,11 +67,11 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
|
||||
body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
|
||||
confirmText="Yes, delete"
|
||||
icon="exclamation-triangle"
|
||||
onConfirm={() => deleteRule(ruleToDelete)}
|
||||
onConfirm={deleteRule}
|
||||
onDismiss={dismissModal}
|
||||
/>
|
||||
),
|
||||
[deleteRule, dismissModal, ruleToDelete]
|
||||
[ruleToDelete, deleteRule, dismissModal]
|
||||
);
|
||||
|
||||
return [modal, showModal, dismissModal];
|
||||
|
@ -65,18 +65,17 @@ const RuleViewer = () => {
|
||||
// we want to be able to show a modal if the rule has been provisioned explain the limitations
|
||||
// of duplicating provisioned alert rules
|
||||
const [duplicateRuleIdentifier, setDuplicateRuleIdentifier] = useState<RuleIdentifier>();
|
||||
const { annotations, promRule, rulerRule } = rule;
|
||||
|
||||
const { annotations, promRule } = rule;
|
||||
const hasError = isErrorHealth(rule.promRule?.health);
|
||||
|
||||
const hasError = isErrorHealth(promRule?.health);
|
||||
const isAlertType = isAlertingRule(promRule);
|
||||
|
||||
const isFederatedRule = isFederatedRuleGroup(rule.group);
|
||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule);
|
||||
const isProvisioned = isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance);
|
||||
const isPaused = isGrafanaRulerRule(rulerRule) && isGrafanaRulerRulePaused(rulerRule);
|
||||
|
||||
const showError = hasError && !isPaused;
|
||||
const ruleOrigin = getRulePluginOrigin(rule);
|
||||
const ruleOrigin = rulerRule ? getRulePluginOrigin(rulerRule) : getRulePluginOrigin(promRule);
|
||||
|
||||
const summary = annotations[Annotation.summary];
|
||||
|
||||
@ -90,12 +89,12 @@ const RuleViewer = () => {
|
||||
name={title}
|
||||
paused={isPaused}
|
||||
state={isAlertType ? promRule.state : undefined}
|
||||
health={rule.promRule?.health}
|
||||
ruleType={rule.promRule?.type}
|
||||
health={promRule?.health}
|
||||
ruleType={promRule?.type}
|
||||
ruleOrigin={ruleOrigin}
|
||||
/>
|
||||
)}
|
||||
actions={<RuleActionsButtons rule={rule} showCopyLinkButton rulesSource={rule.namespace.rulesSource} />}
|
||||
actions={<RuleActionsButtons rule={rule} rulesSource={rule.namespace.rulesSource} />}
|
||||
info={createMetadata(rule)}
|
||||
subTitle={
|
||||
<Stack direction="column">
|
||||
@ -327,7 +326,7 @@ function isValidTab(tab: UrlQueryValue): tab is ActiveTab {
|
||||
function usePageNav(rule: CombinedRule) {
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
|
||||
const { annotations, promRule } = rule;
|
||||
const { annotations, promRule, rulerRule } = rule;
|
||||
|
||||
const summary = annotations[Annotation.summary];
|
||||
const isAlertType = isAlertingRule(promRule);
|
||||
@ -336,8 +335,8 @@ function usePageNav(rule: CombinedRule) {
|
||||
const namespaceName = decodeGrafanaNamespace(rule.namespace).name;
|
||||
const groupName = rule.group.name;
|
||||
|
||||
const isGrafanaAlertRule = isGrafanaRulerRule(rule.rulerRule) && isAlertType;
|
||||
const isRecordingRuleType = isRecordingRule(rule.promRule);
|
||||
const isGrafanaAlertRule = isGrafanaRulerRule(rulerRule) && isAlertType;
|
||||
const isRecordingRuleType = isRecordingRule(promRule);
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
...defaultPageNav,
|
||||
|
@ -27,17 +27,13 @@ interface Props {
|
||||
export const CloudRules = ({ namespaces, expandAll }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const dsConfigs = useUnifiedAlertingSelector((state) => state.dataSources);
|
||||
const promRules = useUnifiedAlertingSelector((state) => state.promRules);
|
||||
const rulesDataSources = useMemo(getRulesDataSources, []);
|
||||
const groupsWithNamespaces = useCombinedGroupNamespace(namespaces);
|
||||
|
||||
const dataSourcesLoading = useMemo(
|
||||
() =>
|
||||
rulesDataSources.filter(
|
||||
(ds) => isAsyncRequestStatePending(promRules[ds.name]) || isAsyncRequestStatePending(dsConfigs[ds.name])
|
||||
),
|
||||
[promRules, dsConfigs, rulesDataSources]
|
||||
() => rulesDataSources.filter((ds) => isAsyncRequestStatePending(promRules[ds.name])),
|
||||
[promRules, rulesDataSources]
|
||||
);
|
||||
|
||||
const hasSomeResults = rulesDataSources.some((ds) => Boolean(promRules[ds.name]?.result?.length));
|
||||
|
@ -15,15 +15,20 @@ import * as React from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, Button, Icon, Modal, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { dispatch, getState } from 'app/store/store';
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import {
|
||||
CombinedRuleGroup,
|
||||
CombinedRuleNamespace,
|
||||
RuleGroupIdentifier,
|
||||
RulerDataSourceConfig,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { useReorderRuleForRuleGroup } from '../../hooks/ruleGroup/useUpdateRuleGroup';
|
||||
import { isLoading } from '../../hooks/useAsync';
|
||||
import { swapItems, SwapOperation } from '../../reducers/ruler/ruleGroups';
|
||||
import { fetchRulerRulesAction, getDataSourceRulerConfig } from '../../state/actions';
|
||||
import { fetchRulerRulesAction } from '../../state/actions';
|
||||
import { isCloudRulesSource } from '../../utils/datasource';
|
||||
import { hashRulerRule } from '../../utils/rule-id';
|
||||
import {
|
||||
@ -38,6 +43,7 @@ interface ModalProps {
|
||||
group: CombinedRuleGroup;
|
||||
onClose: () => void;
|
||||
folderUid?: string;
|
||||
rulerConfig: RulerDataSourceConfig;
|
||||
}
|
||||
|
||||
type RulerRuleWithUID = { uid: string } & RulerRuleDTO;
|
||||
@ -52,11 +58,9 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||
|
||||
// The list of rules might have been filtered before we get to this reordering modal
|
||||
// We need to grab the full (unfiltered) list
|
||||
const dataSourceName = rulesSourceToDataSourceName(namespace.rulesSource);
|
||||
const rulerConfig = getDataSourceRulerConfig(getState, dataSourceName);
|
||||
const { currentData: ruleGroup, isLoading: loadingRules } = alertRuleApi.endpoints.getRuleGroupForNamespace.useQuery(
|
||||
{
|
||||
rulerConfig,
|
||||
rulerConfig: props.rulerConfig,
|
||||
namespace: folderUid ?? namespace.name,
|
||||
group: group.name,
|
||||
},
|
||||
@ -97,8 +101,10 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||
);
|
||||
|
||||
const updateRulesOrder = useCallback(async () => {
|
||||
const dataSourceName = rulesSourceToDataSourceName(namespace.rulesSource);
|
||||
|
||||
const ruleGroupIdentifier: RuleGroupIdentifier = {
|
||||
dataSourceName: rulesSourceToDataSourceName(namespace.rulesSource),
|
||||
dataSourceName,
|
||||
groupName: group.name,
|
||||
namespaceName: folderUid ?? namespace.name,
|
||||
};
|
||||
@ -107,16 +113,7 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||
// TODO: Remove once RTKQ is more prevalently used
|
||||
await dispatch(fetchRulerRulesAction({ rulesSourceName: dataSourceName }));
|
||||
onClose();
|
||||
}, [
|
||||
namespace.rulesSource,
|
||||
namespace.name,
|
||||
group.name,
|
||||
folderUid,
|
||||
reorderRulesInGroup,
|
||||
operations,
|
||||
dataSourceName,
|
||||
onClose,
|
||||
]);
|
||||
}, [namespace.rulesSource, namespace.name, group.name, folderUid, reorderRulesInGroup, operations, onClose]);
|
||||
|
||||
// assign unique but stable identifiers to each (alerting / recording) rule
|
||||
const rulesWithUID: RulerRuleWithUID[] = rulesList.map((rulerRule) => ({
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { produce } from 'immer';
|
||||
import { render, screen, userEvent } from 'test/test-utils';
|
||||
import { byLabelText, byRole } from 'testing-library-selector';
|
||||
|
||||
import { config, setPluginLinksHook } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { mockFeatureDiscoveryApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import {
|
||||
getCloudRule,
|
||||
getGrafanaRule,
|
||||
@ -14,11 +13,13 @@ import {
|
||||
mockGrafanaRulerRule,
|
||||
mockPromAlertingRule,
|
||||
} from 'app/features/alerting/unified/mocks';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
setupMswServer();
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { buildInfoResponse } from '../../testSetup/featureDiscovery';
|
||||
|
||||
const server = setupMswServer();
|
||||
jest.mock('app/core/services/context_srv');
|
||||
const mockContextSrv = jest.mocked(contextSrv);
|
||||
|
||||
@ -35,6 +36,8 @@ const grantAllPermissions = () => {
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
AccessControlAction.AlertingRuleDelete,
|
||||
AccessControlAction.AlertingInstanceCreate,
|
||||
AccessControlAction.AlertingRuleExternalRead,
|
||||
AccessControlAction.AlertingRuleExternalWrite,
|
||||
]);
|
||||
mockContextSrv.hasPermissionInMetadata.mockImplementation(() => true);
|
||||
mockContextSrv.hasPermission.mockImplementation(() => true);
|
||||
@ -58,6 +61,9 @@ setPluginLinksHook(() => ({
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
const mimirDs = mockDataSource({ uid: 'mimir', name: 'Mimir' });
|
||||
setupDataSources(mimirDs);
|
||||
|
||||
const clickCopyLink = async () => {
|
||||
const user = userEvent.setup();
|
||||
await user.click(await ui.moreButton.find());
|
||||
@ -70,7 +76,7 @@ describe('RuleActionsButtons', () => {
|
||||
grantAllPermissions();
|
||||
const mockRule = getGrafanaRule();
|
||||
|
||||
render(<RuleActionsButtons rule={mockRule} rulesSource="grafana" showCopyLinkButton />);
|
||||
render(<RuleActionsButtons rule={mockRule} rulesSource="grafana" />);
|
||||
|
||||
await user.click(await ui.moreButton.find());
|
||||
|
||||
@ -93,26 +99,10 @@ describe('RuleActionsButtons', () => {
|
||||
it('renders correct options for Cloud rule', async () => {
|
||||
const user = userEvent.setup();
|
||||
grantAllPermissions();
|
||||
const mockRule = getCloudRule();
|
||||
const dataSource = mockDataSource({ id: 1 });
|
||||
const mockRule = getCloudRule(undefined, { rulesSource: mimirDs });
|
||||
mockFeatureDiscoveryApi(server).discoverDsFeatures(mimirDs, buildInfoResponse.mimir);
|
||||
|
||||
const defaultState = configureStore().getState();
|
||||
render(<RuleActionsButtons rule={mockRule} rulesSource={dataSource} />, {
|
||||
preloadedState: produce(defaultState, (store) => {
|
||||
store.unifiedAlerting.dataSources[dataSource.name] = {
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
result: {
|
||||
id: 'test-ds',
|
||||
name: dataSource.name,
|
||||
rulerConfig: {
|
||||
dataSourceName: dataSource.name,
|
||||
apiVersion: 'config',
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
render(<RuleActionsButtons rule={mockRule} rulesSource={mimirDs} />);
|
||||
|
||||
await user.click(await ui.moreButton.find());
|
||||
|
||||
@ -162,14 +152,16 @@ describe('RuleActionsButtons', () => {
|
||||
});
|
||||
|
||||
it('copies correct URL for cloud rule', async () => {
|
||||
const mockRule = getCloudRule();
|
||||
const promDataSource = mockDataSource({ name: 'Prometheus-2' });
|
||||
|
||||
render(<RuleActionsButtons rule={mockRule} rulesSource="grafana" />);
|
||||
const mockRule = getCloudRule({ name: 'pod-1-cpu-firing' });
|
||||
|
||||
render(<RuleActionsButtons rule={mockRule} rulesSource={promDataSource} />);
|
||||
|
||||
await clickCopyLink();
|
||||
|
||||
expect(await navigator.clipboard.readText()).toBe(
|
||||
'http://localhost:3000/sub/alerting/Prometheus-2/mockRule/find'
|
||||
'http://localhost:3000/sub/alerting/Prometheus-2/pod-1-cpu-firing/find'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource';
|
||||
import { createViewLink } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isGrafanaAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { getRuleGroupLocationFromCombinedRule, isGrafanaAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
import { RedirectToCloneRule } from './CloneRule';
|
||||
@ -32,13 +32,12 @@ interface Props {
|
||||
*/
|
||||
compact?: boolean;
|
||||
showViewButton?: boolean;
|
||||
showCopyLinkButton?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Action** buttons to show for an alert rule - e.g. "View", "Edit", "More..."
|
||||
*/
|
||||
export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton, rule, rulesSource }: Props) => {
|
||||
export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const redirectToListView = compact ? false : true;
|
||||
@ -65,6 +64,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton
|
||||
const sourceName = getRulesSourceName(rulesSource);
|
||||
|
||||
const identifier = ruleId.fromCombinedRule(sourceName, rule);
|
||||
const groupIdentifier = getRuleGroupLocationFromCombinedRule(rule);
|
||||
|
||||
if (showViewButton) {
|
||||
buttons.push(
|
||||
@ -93,15 +93,23 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton
|
||||
);
|
||||
}
|
||||
|
||||
if (!rule.promRule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={1} alignItems="center" wrap="nowrap">
|
||||
{buttons}
|
||||
<AlertRuleMenu
|
||||
buttonSize={buttonSize}
|
||||
rule={rule}
|
||||
rulerRule={rule.rulerRule}
|
||||
promRule={rule.promRule}
|
||||
identifier={identifier}
|
||||
showCopyLinkButton={showCopyLinkButton}
|
||||
handleDelete={() => showDeleteModal(rule)}
|
||||
groupIdentifier={groupIdentifier}
|
||||
handleDelete={() => {
|
||||
if (rule.rulerRule) {
|
||||
showDeleteModal(rule.rulerRule, groupIdentifier);
|
||||
}
|
||||
}}
|
||||
handleSilence={() => setShowSilenceDrawer(true)}
|
||||
handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
|
||||
onPauseChange={() => {
|
||||
@ -113,6 +121,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton
|
||||
// on tag invalidation (or optimistic cache updates) for this
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME, limitAlerts }));
|
||||
}}
|
||||
buttonSize={buttonSize}
|
||||
/>
|
||||
{deleteModal}
|
||||
{isGrafanaAlertingRule(rule.rulerRule) && showSilenceDrawer && (
|
||||
|
@ -15,17 +15,12 @@ import { isRulerNotSupportedResponse } from '../../utils/rules';
|
||||
export function RuleListErrors(): ReactElement {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [closed, setClosed] = useLocalStorage('grafana.unifiedalerting.hideErrors', false);
|
||||
const dataSourceConfigRequests = useUnifiedAlertingSelector((state) => state.dataSources);
|
||||
const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const errors = useMemo((): JSX.Element[] => {
|
||||
const [dataSourceConfigErrors, promRequestErrors, rulerRequestErrors] = [
|
||||
dataSourceConfigRequests,
|
||||
promRuleRequests,
|
||||
rulerRuleRequests,
|
||||
].map((requests) =>
|
||||
const [promRequestErrors, rulerRequestErrors] = [promRuleRequests, rulerRuleRequests].map((requests) =>
|
||||
getRulesDataSources().reduce<Array<{ error: SerializedError; dataSource: DataSourceInstanceSettings }>>(
|
||||
(result, dataSource) => {
|
||||
const error = requests[dataSource.name]?.error;
|
||||
@ -49,18 +44,6 @@ export function RuleListErrors(): ReactElement {
|
||||
result.push(<>Failed to load Grafana rules config: {grafanaRulerError.message || 'Unknown error.'}</>);
|
||||
}
|
||||
|
||||
dataSourceConfigErrors.forEach(({ dataSource, error }) => {
|
||||
result.push(
|
||||
<>
|
||||
Failed to load the data source configuration for{' '}
|
||||
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
|
||||
{dataSource.name}
|
||||
</a>
|
||||
: {error.message || 'Unknown error.'}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
promRequestErrors.forEach(({ dataSource, error }) =>
|
||||
result.push(
|
||||
<>
|
||||
@ -86,7 +69,7 @@ export function RuleListErrors(): ReactElement {
|
||||
);
|
||||
|
||||
return result;
|
||||
}, [dataSourceConfigRequests, promRuleRequests, rulerRuleRequests, styles.dsLink]);
|
||||
}, [promRuleRequests, rulerRuleRequests, styles.dsLink]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -102,7 +102,7 @@ const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: C
|
||||
return null;
|
||||
}
|
||||
|
||||
const originMeta = getRulePluginOrigin(rule);
|
||||
const originMeta = getRulePluginOrigin(rule.promRule);
|
||||
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
@ -116,7 +116,7 @@ const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: C
|
||||
labels={rule.promRule?.labels}
|
||||
isProvisioned={isProvisioned}
|
||||
instancesCount={instancesCount}
|
||||
namespace={rule.namespace}
|
||||
namespace={rule.namespace.name}
|
||||
group={rule.group.name}
|
||||
actions={<RuleActionsButtons compact rule={rule} rulesSource={rule.namespace.rulesSource} />}
|
||||
origin={originMeta}
|
||||
|
@ -8,6 +8,7 @@ import { Badge, ConfirmModal, Icon, Spinner, Stack, Tooltip, useStyles2 } from '
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier, RulesSource } from 'app/types/unified-alerting';
|
||||
|
||||
import { LogMessages, logInfo } from '../../Analytics';
|
||||
import { featureDiscoveryApi } from '../../api/featureDiscoveryApi';
|
||||
import { useDeleteRuleGroup } from '../../hooks/ruleGroup/useDeleteRuleGroup';
|
||||
import { useFolder } from '../../hooks/useFolder';
|
||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||
@ -36,8 +37,12 @@ interface Props {
|
||||
viewMode: ViewMode;
|
||||
}
|
||||
|
||||
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||
|
||||
export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: Props) => {
|
||||
const { rulesSource } = namespace;
|
||||
const rulesSourceName = getRulesSourceName(rulesSource);
|
||||
|
||||
const [deleteRuleGroup] = useDeleteRuleGroup();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@ -54,6 +59,8 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
|
||||
}, [expandAll]);
|
||||
|
||||
const { hasRuler, rulerRulesLoaded } = useHasRuler(namespace.rulesSource);
|
||||
const { currentData: dsFeatures } = useDiscoverDsFeaturesQuery({ rulesSourceName });
|
||||
|
||||
const rulerRule = group.rules[0]?.rulerRule;
|
||||
const folderUID = (rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid) || undefined;
|
||||
const { folder } = useFolder(folderUID);
|
||||
@ -276,12 +283,13 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
|
||||
folderUid={folderUID}
|
||||
/>
|
||||
)}
|
||||
{isReorderingGroup && (
|
||||
{isReorderingGroup && dsFeatures?.rulerConfig && (
|
||||
<ReorderCloudGroupModal
|
||||
group={group}
|
||||
folderUid={folderUID}
|
||||
namespace={namespace}
|
||||
onClose={() => setIsReorderingGroup(false)}
|
||||
rulerConfig={dsFeatures.rulerConfig}
|
||||
/>
|
||||
)}
|
||||
<ConfirmModal
|
||||
|
@ -4,7 +4,7 @@ import { byRole } from 'testing-library-selector';
|
||||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
|
||||
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
|
||||
import { AlertRuleAction, useAlertRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import { getCloudRule, getGrafanaRule } from '../../mocks';
|
||||
|
||||
import { RulesTable } from './RulesTable';
|
||||
@ -12,6 +12,10 @@ import { RulesTable } from './RulesTable';
|
||||
jest.mock('../../hooks/useAbilities');
|
||||
|
||||
const mocks = {
|
||||
// This is a bit unfortunate, but we need to mock both abilities
|
||||
// RuleActionButtons still needs to use the useAlertRuleAbility hook
|
||||
// whereas AlertRuleMenu has already been refactored to use useRulerRuleAbility
|
||||
useRulerRuleAbility: jest.mocked(useRulerRuleAbility),
|
||||
useAlertRuleAbility: jest.mocked(useAlertRuleAbility),
|
||||
};
|
||||
|
||||
@ -45,6 +49,9 @@ describe('RulesTable RBAC', () => {
|
||||
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
|
||||
|
||||
it('Should not render Edit button for users without the update permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
@ -55,6 +62,9 @@ describe('RulesTable RBAC', () => {
|
||||
});
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
@ -67,15 +77,22 @@ describe('RulesTable RBAC', () => {
|
||||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[grafanaRule]} />);
|
||||
|
||||
expect(await ui.actionButtons.edit.find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
@ -103,6 +120,9 @@ describe('RulesTable RBAC', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.useRulerRuleAbility.mockImplementation(() => {
|
||||
return [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation(() => {
|
||||
return [true, true];
|
||||
});
|
||||
@ -135,6 +155,9 @@ describe('RulesTable RBAC', () => {
|
||||
const cloudRule = getCloudRule({ name: 'Cloud' });
|
||||
|
||||
it('Should not render Edit button for users without the update permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
@ -145,6 +168,9 @@ describe('RulesTable RBAC', () => {
|
||||
});
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
@ -156,6 +182,9 @@ describe('RulesTable RBAC', () => {
|
||||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
@ -166,6 +195,9 @@ describe('RulesTable RBAC', () => {
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
|
@ -214,9 +214,9 @@ function useColumns(
|
||||
label: '',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => {
|
||||
const rulerRule = rule.rulerRule;
|
||||
const { promRule, rulerRule } = rule;
|
||||
|
||||
const originMeta = getRulePluginOrigin(rule);
|
||||
const originMeta = getRulePluginOrigin(promRule ?? rulerRule);
|
||||
if (originMeta) {
|
||||
return <PluginOriginBadge pluginId={originMeta.pluginId} />;
|
||||
}
|
||||
|
@ -9,12 +9,13 @@ import {
|
||||
import { useFolder } from 'app/features/alerting/unified/hooks/useFolder';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { CombinedRule, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
|
||||
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { getRulesSourceName } from '../utils/datasource';
|
||||
import { isAdmin } from '../utils/misc';
|
||||
import { isFederatedRuleGroup, isGrafanaRecordingRule, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules';
|
||||
|
||||
@ -155,6 +156,30 @@ export function useAlertRuleAbilities(rule: CombinedRule, actions: AlertRuleActi
|
||||
}, [abilities, actions]);
|
||||
}
|
||||
|
||||
export function useRulerRuleAbility(
|
||||
rule: RulerRuleDTO | undefined,
|
||||
groupIdentifier: RuleGroupIdentifier,
|
||||
action: AlertRuleAction
|
||||
): Ability {
|
||||
const abilities = useAllRulerRuleAbilities(rule, groupIdentifier);
|
||||
|
||||
return useMemo(() => {
|
||||
return abilities[action];
|
||||
}, [abilities, action]);
|
||||
}
|
||||
|
||||
export function useRulerRuleAbilities(
|
||||
rule: RulerRuleDTO,
|
||||
groupIdentifier: RuleGroupIdentifier,
|
||||
actions: AlertRuleAction[]
|
||||
): Ability[] {
|
||||
const abilities = useAllRulerRuleAbilities(rule, groupIdentifier);
|
||||
|
||||
return useMemo(() => {
|
||||
return actions.map((action) => abilities[action]);
|
||||
}, [abilities, actions]);
|
||||
}
|
||||
|
||||
// This hook is being called a lot in different places
|
||||
// In some cases multiple times for ~80 rules (e.g. on the list page)
|
||||
// We need to investigate further if some of these calls are redundant
|
||||
@ -169,13 +194,13 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRul
|
||||
loading,
|
||||
} = useIsRuleEditable(rulesSourceName, rule.rulerRule);
|
||||
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
|
||||
const canSilence = useCanSilence(rule);
|
||||
const canSilence = useCanSilence(rule.rulerRule);
|
||||
|
||||
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
|
||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
const isFederated = isFederatedRuleGroup(rule.group);
|
||||
const isGrafanaManagedAlertRule = isGrafanaRulerRule(rule.rulerRule);
|
||||
const isPluginProvided = isPluginProvidedRule(rule);
|
||||
const isPluginProvided = isPluginProvidedRule(rule.rulerRule);
|
||||
|
||||
// if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
|
||||
const immutableRule = isProvisioned || isFederated || isPluginProvided;
|
||||
@ -206,6 +231,52 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRul
|
||||
return abilities;
|
||||
}
|
||||
|
||||
export function useAllRulerRuleAbilities(
|
||||
rule: RulerRuleDTO | undefined,
|
||||
groupIdentifier: RuleGroupIdentifier
|
||||
): Abilities<AlertRuleAction> {
|
||||
const rulesSourceName = groupIdentifier.dataSourceName;
|
||||
|
||||
const { isEditable, isRemovable, isRulerAvailable = false, loading } = useIsRuleEditable(rulesSourceName, rule);
|
||||
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
|
||||
const canSilence = useCanSilence(rule);
|
||||
|
||||
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
|
||||
const isProvisioned = isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance);
|
||||
// const isFederated = isFederatedRuleGroup();
|
||||
const isFederated = false;
|
||||
const isGrafanaManagedAlertRule = isGrafanaRulerRule(rule);
|
||||
const isPluginProvided = isPluginProvidedRule(rule);
|
||||
|
||||
// if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
|
||||
const immutableRule = isProvisioned || isFederated || isPluginProvided;
|
||||
|
||||
// while we gather info, pretend it's not supported
|
||||
const MaybeSupported = loading ? NotSupported : isRulerAvailable;
|
||||
const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;
|
||||
|
||||
// Creating duplicates of plugin-provided rules does not seem to make a lot of sense
|
||||
const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;
|
||||
|
||||
const rulesPermissions = getRulesPermissions(rulesSourceName);
|
||||
|
||||
const abilities: Abilities<AlertRuleAction> = {
|
||||
[AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
|
||||
[AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read),
|
||||
[AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
|
||||
[AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
|
||||
[AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
|
||||
[AlertRuleAction.Silence]: canSilence,
|
||||
[AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed],
|
||||
[AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
|
||||
};
|
||||
|
||||
return abilities;
|
||||
}, [rule, loading, isRulerAvailable, rulesSourceName, isEditable, isRemovable, canSilence, exportAllowed]);
|
||||
|
||||
return abilities;
|
||||
}
|
||||
|
||||
export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
const {
|
||||
selectedAlertmanager,
|
||||
@ -335,23 +406,26 @@ export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability
|
||||
}, [abilities, actions]);
|
||||
}
|
||||
|
||||
const { useGetGrafanaAlertingConfigurationStatusQuery } = alertmanagerApi;
|
||||
/**
|
||||
* We don't want to show the silence button if either
|
||||
* 1. the user has no permissions to create silences
|
||||
* 2. the admin has configured to only send instances to external AMs
|
||||
*/
|
||||
function useCanSilence(rule: CombinedRule): [boolean, boolean] {
|
||||
const rulesSource = rule.namespace.rulesSource;
|
||||
const isGrafanaManagedRule = rulesSource === GRAFANA_RULES_SOURCE_NAME;
|
||||
const isGrafanaRecording = isGrafanaRecordingRule(rule.rulerRule);
|
||||
function useCanSilence(rule?: RulerRuleDTO): [boolean, boolean] {
|
||||
const folderUID = isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined;
|
||||
const { loading: folderIsLoading, folder } = useFolder(folderUID);
|
||||
|
||||
const { currentData: amConfigStatus, isLoading } =
|
||||
alertmanagerApi.endpoints.getGrafanaAlertingConfigurationStatus.useQuery(undefined, {
|
||||
skip: !isGrafanaManagedRule,
|
||||
const isGrafanaManagedRule = rule && isGrafanaRulerRule(rule);
|
||||
const isGrafanaRecording = rule && isGrafanaRecordingRule(rule);
|
||||
|
||||
const { currentData: amConfigStatus, isLoading } = useGetGrafanaAlertingConfigurationStatusQuery(undefined, {
|
||||
skip: !isGrafanaManagedRule || !rule,
|
||||
});
|
||||
|
||||
const folderUID = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.namespace_uid : undefined;
|
||||
const { loading: folderIsLoading, folder } = useFolder(folderUID);
|
||||
if (!rule) {
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
// we don't support silencing when the rule is not a Grafana managed alerting rule
|
||||
// we simply don't know what Alertmanager the ruler is sending alerts to
|
||||
|
@ -240,7 +240,11 @@ export function useRuleWithLocation({
|
||||
}): RequestState<RuleWithLocation> {
|
||||
const ruleSource = getRulesSourceFromIdentifier(ruleIdentifier);
|
||||
|
||||
const { dsFeatures, isLoadingDsFeatures } = useDataSourceFeatures(ruleIdentifier.ruleSourceName);
|
||||
const { data: dsFeatures, isLoading: isLoadingDsFeatures } =
|
||||
featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({
|
||||
rulesSourceName: ruleIdentifier.ruleSourceName,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: isLoadingRuleLocation,
|
||||
error: ruleLocationError,
|
||||
|
@ -272,7 +272,7 @@ const reduceGroups = (filterState: RulesFilter) => {
|
||||
}
|
||||
|
||||
if ('plugins' in matchesFilterFor && filterState.plugins === 'hide') {
|
||||
matchesFilterFor.plugins = !isPluginProvidedRule(rule);
|
||||
matchesFilterFor.plugins = rule.rulerRule && !isPluginProvidedRule(rule.rulerRule);
|
||||
}
|
||||
|
||||
if ('contactPoint' in matchesFilterFor) {
|
||||
|
@ -3,9 +3,13 @@ import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction, FolderDTO, StoreState } from 'app/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction, FolderDTO } from 'app/types';
|
||||
|
||||
import { mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule, mockUnifiedAlertingStore } from '../mocks';
|
||||
import { mockFeatureDiscoveryApi, setupMswServer } from '../mockApi';
|
||||
import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks';
|
||||
import { setupDataSources } from '../testSetup/datasources';
|
||||
import { buildInfoResponse } from '../testSetup/featureDiscovery';
|
||||
|
||||
import { useFolder } from './useFolder';
|
||||
import { useIsRuleEditable } from './useIsRuleEditable';
|
||||
@ -16,6 +20,14 @@ const mocks = {
|
||||
useFolder: jest.mocked(useFolder),
|
||||
};
|
||||
|
||||
const server = setupMswServer();
|
||||
|
||||
const dataSources = {
|
||||
mimir: mockDataSource({ uid: 'mimir', name: 'Mimir' }),
|
||||
};
|
||||
|
||||
setupDataSources(dataSources.mimir);
|
||||
|
||||
describe('useIsRuleEditable', () => {
|
||||
describe('RBAC enabled', () => {
|
||||
describe('Grafana rules', () => {
|
||||
@ -95,13 +107,17 @@ describe('useIsRuleEditable', () => {
|
||||
beforeEach(() => {
|
||||
mocks.useFolder.mockReturnValue({ loading: false });
|
||||
contextSrv.isEditor = true;
|
||||
|
||||
mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.mimir, buildInfoResponse.mimir);
|
||||
});
|
||||
|
||||
it('Should allow editing and deleting when the user has alert rule external write permission', async () => {
|
||||
mockPermissions([AccessControlAction.AlertingRuleExternalWrite]);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
const { result } = renderHook(() => useIsRuleEditable('cortex', mockRulerAlertingRule()), { wrapper });
|
||||
const { result } = renderHook(() => useIsRuleEditable(dataSources.mimir.name, mockRulerAlertingRule()), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.isEditable).toBe(true);
|
||||
@ -112,7 +128,9 @@ describe('useIsRuleEditable', () => {
|
||||
mockPermissions([]);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
const { result } = renderHook(() => useIsRuleEditable('cortex', mockRulerAlertingRule()), { wrapper });
|
||||
const { result } = renderHook(() => useIsRuleEditable(dataSources.mimir.name, mockRulerAlertingRule()), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.isEditable).toBe(false);
|
||||
@ -133,31 +151,7 @@ function mockPermissions(grantedPermissions: AccessControlAction[]) {
|
||||
}
|
||||
|
||||
function getProviderWrapper() {
|
||||
const dataSources = getMockedDataSources();
|
||||
const store = mockUnifiedAlertingStore({ dataSources });
|
||||
const store = configureStore();
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function getMockedDataSources(): StoreState['unifiedAlerting']['dataSources'] {
|
||||
return {
|
||||
grafana: {
|
||||
loading: false,
|
||||
dispatched: false,
|
||||
result: {
|
||||
id: 'grafana',
|
||||
name: 'grafana',
|
||||
rulerConfig: { dataSourceName: 'grafana', apiVersion: 'legacy' },
|
||||
},
|
||||
},
|
||||
cortex: {
|
||||
loading: false,
|
||||
dispatched: false,
|
||||
result: {
|
||||
id: 'cortex',
|
||||
name: 'Cortex',
|
||||
rulerConfig: { dataSourceName: 'cortex', apiVersion: 'legacy' },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { getRulesPermissions } from '../utils/access-control';
|
||||
import { isGrafanaRulerRule } from '../utils/rules';
|
||||
|
||||
import { useFolder } from './useFolder';
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
|
||||
interface ResultBag {
|
||||
isRulerAvailable?: boolean;
|
||||
@ -16,7 +15,6 @@ interface ResultBag {
|
||||
}
|
||||
|
||||
export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ResultBag {
|
||||
const dataSources = useUnifiedAlertingSelector((state) => state.dataSources);
|
||||
const { currentData: dsFeatures, isLoading } = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({
|
||||
rulesSourceName,
|
||||
});
|
||||
@ -62,8 +60,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
|
||||
}
|
||||
|
||||
// prom rules are only editable by users with Editor role and only if rules source supports editing
|
||||
const isRulerAvailable =
|
||||
Boolean(dataSources[rulesSourceName]?.result?.rulerConfig) || Boolean(dsFeatures?.rulerConfig);
|
||||
const isRulerAvailable = Boolean(dsFeatures?.rulerConfig);
|
||||
const canEditCloudRules = contextSrv.hasPermission(rulePermission.update);
|
||||
const canRemoveCloudRules = contextSrv.hasPermission(rulePermission.delete);
|
||||
|
||||
@ -71,6 +68,6 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
|
||||
isRulerAvailable,
|
||||
isEditable: canEditCloudRules && isRulerAvailable,
|
||||
isRemovable: canRemoveCloudRules && isRulerAvailable,
|
||||
loading: isLoading || dataSources[rulesSourceName]?.loading,
|
||||
loading: isLoading,
|
||||
};
|
||||
}
|
||||
|
@ -1,19 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { PromBasedDataSource } from 'app/types/unified-alerting';
|
||||
|
||||
import { getDataSourceByName } from '../utils/datasource';
|
||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||
import { getRulesDataSources } from '../utils/datasource';
|
||||
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
const { useLazyDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||
|
||||
export function useRulesSourcesWithRuler(): DataSourceInstanceSettings[] {
|
||||
const dataSources = useUnifiedAlertingSelector((state) => state.dataSources);
|
||||
export function useRulesSourcesWithRuler(): {
|
||||
rulesSourcesWithRuler: DataSourceInstanceSettings[];
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const [rulesSourcesWithRuler, setRulesSourcesWithRuler] = useState<DataSourceInstanceSettings[]>([]);
|
||||
const [discoverDsFeatures, { isLoading }] = useLazyDiscoverDsFeaturesQuery();
|
||||
|
||||
const dataSourcesWithRuler = Object.values(dataSources)
|
||||
.map((ds) => ds.result)
|
||||
.filter((ds): ds is PromBasedDataSource => Boolean(ds?.rulerConfig));
|
||||
// try fetching rules for each prometheus to see if it has ruler
|
||||
useEffect(() => {
|
||||
const dataSources = getRulesDataSources();
|
||||
dataSources.forEach(async (ds) => {
|
||||
const { data: dsFeatures } = await discoverDsFeatures({ uid: ds.uid }, true);
|
||||
if (dsFeatures?.rulerConfig) {
|
||||
setRulesSourcesWithRuler((prev) => [...prev, ds]);
|
||||
}
|
||||
});
|
||||
}, [discoverDsFeatures]);
|
||||
|
||||
return dataSourcesWithRuler
|
||||
.map((ds) => getDataSourceByName(ds.name))
|
||||
.filter((dsConfig): dsConfig is DataSourceInstanceSettings => Boolean(dsConfig));
|
||||
return { rulesSourcesWithRuler, isLoading };
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { produce } from 'immer';
|
||||
import { isEmpty, pick } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
@ -778,15 +779,21 @@ export function getGrafanaRule(override?: Partial<CombinedRule>, rulerOverride?:
|
||||
});
|
||||
}
|
||||
|
||||
export function getCloudRule(override?: Partial<CombinedRule>) {
|
||||
export function getCloudRule(override?: Partial<CombinedRule>, nsOverride?: Partial<CombinedRuleNamespace>) {
|
||||
const promOverride = pick(override, ['name', 'labels', 'annotations']);
|
||||
const rulerOverride = pick(override, ['name', 'labels', 'annotations']);
|
||||
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Cortex',
|
||||
rulesSource: mockDataSource(),
|
||||
...nsOverride,
|
||||
},
|
||||
promRule: mockPromAlertingRule(),
|
||||
rulerRule: mockRulerAlertingRule(),
|
||||
promRule: mockPromAlertingRule(isEmpty(promOverride) ? undefined : promOverride),
|
||||
rulerRule: mockRulerAlertingRule(
|
||||
isEmpty(rulerOverride) ? undefined : { ...rulerOverride, alert: rulerOverride.name }
|
||||
),
|
||||
...override,
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { PluginExtensionPoints } from '@grafana/data';
|
||||
import { usePluginLinks } from '@grafana/runtime';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { CombinedRule, Rule, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { getRulePluginOrigin } from '../utils/rules';
|
||||
@ -21,12 +21,12 @@ export interface AlertingRuleExtensionContext extends BaseRuleExtensionContext {
|
||||
|
||||
export interface RecordingRuleExtensionContext extends BaseRuleExtensionContext {}
|
||||
|
||||
export function useRulePluginLinkExtension(rule: CombinedRule) {
|
||||
const ruleExtensionPoint = useRuleExtensionPoint(rule);
|
||||
export function useRulePluginLinkExtension(rule: Rule, groupIdentifier: RuleGroupIdentifier) {
|
||||
const ruleExtensionPoint = useRuleExtensionPoint(rule, groupIdentifier);
|
||||
const { links } = usePluginLinks(ruleExtensionPoint);
|
||||
|
||||
const ruleOrigin = getRulePluginOrigin(rule);
|
||||
const ruleType = rule.promRule?.type;
|
||||
const ruleType = rule.type;
|
||||
if (!ruleOrigin || !ruleType) {
|
||||
return [];
|
||||
}
|
||||
@ -57,9 +57,9 @@ interface EmptyExtensionPoint {
|
||||
|
||||
type RuleExtensionPoint = AlertingRuleExtensionPoint | RecordingRuleExtensionPoint | EmptyExtensionPoint;
|
||||
|
||||
function useRuleExtensionPoint(rule: CombinedRule): RuleExtensionPoint {
|
||||
return useMemo(() => {
|
||||
const ruleType = rule.promRule?.type;
|
||||
function useRuleExtensionPoint(rule: Rule, groupIdentifier: RuleGroupIdentifier): RuleExtensionPoint {
|
||||
return useMemo<RuleExtensionPoint>(() => {
|
||||
const ruleType = rule.type;
|
||||
|
||||
switch (ruleType) {
|
||||
case PromRuleType.Alerting:
|
||||
@ -67,11 +67,11 @@ function useRuleExtensionPoint(rule: CombinedRule): RuleExtensionPoint {
|
||||
extensionPointId: PluginExtensionPoints.AlertingAlertingRuleAction,
|
||||
context: {
|
||||
name: rule.name,
|
||||
namespace: rule.namespace.name,
|
||||
group: rule.group.name,
|
||||
namespace: groupIdentifier.namespaceName,
|
||||
group: groupIdentifier.groupName,
|
||||
expression: rule.query,
|
||||
labels: rule.labels,
|
||||
annotations: rule.annotations,
|
||||
labels: rule.labels ?? {},
|
||||
annotations: rule.annotations ?? {},
|
||||
},
|
||||
};
|
||||
case PromRuleType.Recording:
|
||||
@ -79,14 +79,14 @@ function useRuleExtensionPoint(rule: CombinedRule): RuleExtensionPoint {
|
||||
extensionPointId: PluginExtensionPoints.AlertingRecordingRuleAction,
|
||||
context: {
|
||||
name: rule.name,
|
||||
namespace: rule.namespace.name,
|
||||
group: rule.group.name,
|
||||
namespace: groupIdentifier.namespaceName,
|
||||
group: groupIdentifier.groupName,
|
||||
expression: rule.query,
|
||||
labels: rule.labels,
|
||||
labels: rule.labels ?? {},
|
||||
},
|
||||
};
|
||||
default:
|
||||
return { extensionPointId: '' };
|
||||
}
|
||||
}, [rule]);
|
||||
}, [groupIdentifier, rule]);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { urlUtil } from '@grafana/data';
|
||||
import { logInfo } from '@grafana/runtime';
|
||||
import { Button, LinkButton, Stack, withErrorBoundary } from '@grafana/ui';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
@ -49,8 +50,9 @@ const RuleListV1 = () => {
|
||||
|
||||
const hasActiveLabelsFilter = filterState.labels.length > 0;
|
||||
|
||||
const queryParamView = queryParams.view as keyof typeof VIEWS;
|
||||
const view = VIEWS[queryParamView] ? queryParamView : 'groups';
|
||||
const queryParamView = queryParams.view;
|
||||
const viewType = queryParamView === 'state' || queryParamView === 'groups' ? queryParamView : 'groups';
|
||||
const view = VIEWS[viewType] ? viewType : 'groups';
|
||||
|
||||
const ViewComponent = VIEWS[view];
|
||||
|
||||
@ -161,7 +163,7 @@ export function CreateAlertButton() {
|
||||
icon="plus"
|
||||
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
|
||||
>
|
||||
New alert rule
|
||||
<Trans i18nKey="alerting.rule-list.new-alert-rule">New alert rule</Trans>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
@ -1,216 +1,370 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
import { useAsyncFn, useInterval, useMeasure } from 'react-use';
|
||||
import { PropsWithChildren, ReactNode, useMemo } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||
import { Button, LinkButton, LoadingBar, useStyles2, withErrorBoundary } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
Dropdown,
|
||||
Icon,
|
||||
IconButton,
|
||||
LinkButton,
|
||||
Menu,
|
||||
Pagination,
|
||||
Stack,
|
||||
Text,
|
||||
useStyles2,
|
||||
withErrorBoundary,
|
||||
} from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { Rule, RuleGroupIdentifier, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulesSourceApplication } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
|
||||
import { logInfo, LogMessages, trackRuleListNavigation } from '../Analytics';
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
|
||||
import RulesFilter from '../components/rules/Filter/RulesFilter.v1';
|
||||
import { NoRulesSplash } from '../components/rules/NoRulesCTA';
|
||||
import { INSTANCES_DISPLAY_LIMIT } from '../components/rules/RuleDetails';
|
||||
import { RuleListErrors } from '../components/rules/RuleListErrors';
|
||||
import { RuleStats } from '../components/rules/RuleStats';
|
||||
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
|
||||
import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces';
|
||||
import { useFilteredRules, useRulesFilter } from '../hooks/useFilteredRules';
|
||||
import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector';
|
||||
import { fetchAllPromAndRulerRulesAction } from '../state/actions';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
|
||||
import { getAllRulesSourceNames, getApplicationFromRulesSource, getRulesSourceUniqueKey } from '../utils/datasource';
|
||||
import { makeFolderAlertsLink } from '../utils/misc';
|
||||
import { Spacer } from '../components/Spacer';
|
||||
import { WithReturnButton } from '../components/WithReturnButton';
|
||||
import RulesFilter from '../components/rules/Filter/RulesFilter';
|
||||
import { getAllRulesSources, isGrafanaRulesSource } from '../utils/datasource';
|
||||
import { equal, fromRule, fromRulerRule, hashRule, stringifyIdentifier } from '../utils/rule-id';
|
||||
import { getRulePluginOrigin, isAlertingRule, isRecordingRule } from '../utils/rules';
|
||||
import { createRelativeUrl } from '../utils/url';
|
||||
|
||||
import { EvaluationGroupWithRules } from './components/EvaluationGroupWithRules';
|
||||
import Namespace from './components/Namespace';
|
||||
import { AlertRuleListItem, RecordingRuleListItem, UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||
import { ListGroup } from './components/ListGroup';
|
||||
import { ListSection } from './components/ListSection';
|
||||
import { DataSourceIcon } from './components/Namespace';
|
||||
import { ActionsLoader, RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { LoadingIndicator } from './components/RuleGroup';
|
||||
|
||||
// make sure we ask for 1 more so we show the "show x more" button
|
||||
const LIMIT_ALERTS = INSTANCES_DISPLAY_LIMIT + 1;
|
||||
const noop = () => {};
|
||||
const { usePrometheusRuleNamespacesQuery, useGetRuleGroupForNamespaceQuery } = alertRuleApi;
|
||||
|
||||
const RuleList = withErrorBoundary(
|
||||
() => {
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles2(getStyles);
|
||||
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
|
||||
const onFilterCleared = useCallback(() => setExpandAll(false), []);
|
||||
|
||||
const { filterState, hasActiveFilters } = useRulesFilter();
|
||||
|
||||
const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
|
||||
const loading = rulesDataSourceNames.some(
|
||||
(name) => promRuleRequests[name]?.loading || rulerRuleRequests[name]?.loading
|
||||
);
|
||||
|
||||
const promRequests = Object.entries(promRuleRequests);
|
||||
const rulerRequests = Object.entries(rulerRuleRequests);
|
||||
|
||||
const allPromLoaded = promRequests.every(
|
||||
([_, state]) => state.dispatched && (state?.result !== undefined || state?.error !== undefined)
|
||||
);
|
||||
const allRulerLoaded = rulerRequests.every(
|
||||
([_, state]) => state.dispatched && (state?.result !== undefined || state?.error !== undefined)
|
||||
);
|
||||
|
||||
const allPromEmpty = promRequests.every(([_, state]) => state.dispatched && state?.result?.length === 0);
|
||||
|
||||
const allRulerEmpty = rulerRequests.every(([_, state]) => {
|
||||
const rulerRules = Object.entries(state?.result ?? {});
|
||||
const noRules = rulerRules.every(([_, result]) => result?.length === 0);
|
||||
return noRules && state.dispatched;
|
||||
});
|
||||
|
||||
const limitAlerts = hasActiveFilters ? undefined : LIMIT_ALERTS;
|
||||
// Trigger data refresh only when the RULE_LIST_POLL_INTERVAL_MS elapsed since the previous load FINISHED
|
||||
const [_, fetchRules] = useAsyncFn(async () => {
|
||||
if (!loading) {
|
||||
await dispatch(fetchAllPromAndRulerRulesAction(false, { limitAlerts }));
|
||||
}
|
||||
}, [loading, limitAlerts, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
trackRuleListNavigation().catch(() => {});
|
||||
}, []);
|
||||
|
||||
// fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
|
||||
useEffect(() => {
|
||||
dispatch(fetchAllPromAndRulerRulesAction(false, { limitAlerts }));
|
||||
}, [dispatch, limitAlerts]);
|
||||
useInterval(fetchRules, RULE_LIST_POLL_INTERVAL_MS);
|
||||
|
||||
// Show splash only when we loaded all of the data sources and none of them has alerts
|
||||
const hasNoAlertRulesCreatedYet =
|
||||
allPromLoaded && allPromEmpty && promRequests.length > 0 && allRulerEmpty && allRulerLoaded;
|
||||
const hasAlertRulesCreated = !hasNoAlertRulesCreatedYet;
|
||||
|
||||
const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces();
|
||||
const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState);
|
||||
|
||||
const sortedNamespaces = filteredNamespaces.sort((a: CombinedRuleNamespace, b: CombinedRuleNamespace) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
const ruleSources = getAllRulesSources();
|
||||
|
||||
return (
|
||||
// We don't want to show the Loading... indicator for the whole page.
|
||||
// We show separate indicators for Grafana-managed and Cloud rules
|
||||
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={hasAlertRulesCreated && <CreateAlertButton />}>
|
||||
<RuleListErrors />
|
||||
<RulesFilter onClear={onFilterCleared} />
|
||||
{hasAlertRulesCreated && (
|
||||
<>
|
||||
<div className={styles.break} />
|
||||
<div className={styles.buttonsContainer}>
|
||||
<div className={styles.statsContainer}>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
className={styles.expandAllButton}
|
||||
icon={expandAll ? 'angle-double-up' : 'angle-double-down'}
|
||||
variant="secondary"
|
||||
onClick={() => setExpandAll(!expandAll)}
|
||||
>
|
||||
{expandAll ? 'Collapse all' : 'Expand all'}
|
||||
</Button>
|
||||
)}
|
||||
<RuleStats namespaces={filteredNamespaces} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasNoAlertRulesCreatedYet && <NoRulesSplash />}
|
||||
{hasAlertRulesCreated && (
|
||||
<>
|
||||
<LoadingIndicator visible={loading} />
|
||||
<ul className={styles.rulesTree} role="tree" aria-label="List of alert rules">
|
||||
{sortedNamespaces.map((namespace) => {
|
||||
const { rulesSource, uid } = namespace;
|
||||
|
||||
const application = getApplicationFromRulesSource(rulesSource);
|
||||
const href = application === 'grafana' && uid ? makeFolderAlertsLink(uid, namespace.name) : undefined;
|
||||
|
||||
return (
|
||||
<Namespace
|
||||
key={getRulesSourceUniqueKey(rulesSource) + namespace.name}
|
||||
href={href}
|
||||
name={namespace.name}
|
||||
application={application}
|
||||
>
|
||||
{namespace.groups
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((group) => (
|
||||
<EvaluationGroupWithRules key={group.name} group={group} rulesSource={rulesSource} />
|
||||
))}
|
||||
</Namespace>
|
||||
);
|
||||
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={null}>
|
||||
<RulesFilter onClear={() => {}} />
|
||||
<Stack direction="column" gap={1}>
|
||||
{ruleSources.map((ruleSource) => {
|
||||
if (isGrafanaRulesSource(ruleSource)) {
|
||||
return <GrafanaDataSourceLoader key={ruleSource} />;
|
||||
} else {
|
||||
return <DataSourceLoader key={ruleSource.uid} uid={ruleSource.uid} name={ruleSource.name} />;
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
},
|
||||
{ style: 'page' }
|
||||
);
|
||||
|
||||
const LoadingIndicator = ({ visible = false }) => {
|
||||
const [measureRef, { width }] = useMeasure<HTMLDivElement>();
|
||||
return <div ref={measureRef}>{visible && <LoadingBar width={width} />}</div>;
|
||||
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||
|
||||
interface DataSourceLoaderProps {
|
||||
name: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
const GrafanaDataSourceLoader = () => {
|
||||
return <DataSourceSection name="Grafana" application="grafana" isLoading={true}></DataSourceSection>;
|
||||
};
|
||||
|
||||
const DataSourceLoader = ({ uid, name }: DataSourceLoaderProps) => {
|
||||
const { data: dataSourceInfo, isLoading } = useDiscoverDsFeaturesQuery({ uid });
|
||||
|
||||
if (isLoading) {
|
||||
return <DataSourceSection loader={<Skeleton width={250} height={16} />} />;
|
||||
}
|
||||
|
||||
// 2. grab prometheus rule groups with max_groups if supported
|
||||
if (dataSourceInfo) {
|
||||
const rulerEnabled = Boolean(dataSourceInfo.rulerConfig);
|
||||
|
||||
return (
|
||||
<PaginatedDataSourceLoader
|
||||
ruleSourceName={dataSourceInfo.name}
|
||||
rulerEnabled={rulerEnabled}
|
||||
uid={uid}
|
||||
name={name}
|
||||
application={dataSourceInfo.application}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface PaginatedDataSourceLoaderProps extends Pick<DataSourceSectionProps, 'application' | 'uid' | 'name'> {
|
||||
ruleSourceName: string;
|
||||
rulerEnabled?: boolean;
|
||||
}
|
||||
|
||||
function PaginatedDataSourceLoader({
|
||||
ruleSourceName,
|
||||
rulerEnabled = false,
|
||||
name,
|
||||
uid,
|
||||
application,
|
||||
}: PaginatedDataSourceLoaderProps) {
|
||||
const { data: ruleNamespaces = [], isLoading } = usePrometheusRuleNamespacesQuery({
|
||||
ruleSourceName,
|
||||
maxGroups: 25,
|
||||
limitAlerts: 0,
|
||||
excludeAlerts: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<DataSourceSection name={name} application={application} uid={uid} isLoading={isLoading}>
|
||||
<Stack direction="column" gap={1}>
|
||||
{ruleNamespaces.map((namespace) => (
|
||||
<ListSection
|
||||
key={namespace.name}
|
||||
title={
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Icon name="folder" /> {namespace.name}
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
{namespace.groups.map((group) => (
|
||||
<ListGroup
|
||||
key={group.name}
|
||||
name={group.name}
|
||||
isOpen={false}
|
||||
actions={
|
||||
<>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item label="Edit" icon="pen" data-testid="edit-group-action" />
|
||||
<Menu.Item label="Re-order rules" icon="flip" />
|
||||
<Menu.Divider />
|
||||
<Menu.Item label="Export" icon="download-alt" />
|
||||
<Menu.Item label="Delete" icon="trash-alt" destructive />
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<IconButton name="ellipsis-h" aria-label="rule group actions" />
|
||||
</Dropdown>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{group.rules.map((rule) => {
|
||||
const groupIdentifier: RuleGroupIdentifier = {
|
||||
dataSourceName: ruleSourceName,
|
||||
groupName: group.name,
|
||||
namespaceName: namespace.name,
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertRuleLoader
|
||||
key={hashRule(rule)}
|
||||
rule={rule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
rulerEnabled={rulerEnabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
))}
|
||||
</ListSection>
|
||||
))}
|
||||
{!isLoading && <Pagination currentPage={1} numberOfPages={0} onNavigate={noop} />}
|
||||
</Stack>
|
||||
</DataSourceSection>
|
||||
);
|
||||
}
|
||||
|
||||
interface AlertRuleLoaderProps {
|
||||
rule: Rule;
|
||||
groupIdentifier: RuleGroupIdentifier;
|
||||
rulerEnabled?: boolean;
|
||||
}
|
||||
|
||||
function AlertRuleLoader({ rule, groupIdentifier, rulerEnabled = false }: AlertRuleLoaderProps) {
|
||||
const { dataSourceName, namespaceName, groupName } = groupIdentifier;
|
||||
|
||||
const ruleIdentifier = fromRule(dataSourceName, namespaceName, groupName, rule);
|
||||
const href = createViewLinkFromIdentifier(ruleIdentifier);
|
||||
const originMeta = getRulePluginOrigin(rule);
|
||||
|
||||
// @TODO work with context API to propagate rulerConfig and such
|
||||
const { data: dataSourceInfo } = useDiscoverDsFeaturesQuery({ rulesSourceName: dataSourceName });
|
||||
|
||||
// @TODO refactor this to use a separate hook (useRuleWithLocation() and useCombinedRule() seems to introduce infinite loading / recursion)
|
||||
const {
|
||||
isLoading,
|
||||
data: rulerRuleGroup,
|
||||
// error,
|
||||
} = useGetRuleGroupForNamespaceQuery(
|
||||
{
|
||||
namespace: namespaceName,
|
||||
group: groupName,
|
||||
rulerConfig: dataSourceInfo?.rulerConfig!,
|
||||
},
|
||||
{ skip: !dataSourceInfo?.rulerConfig }
|
||||
);
|
||||
|
||||
const rulerRule = useMemo(() => {
|
||||
if (!rulerRuleGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
return rulerRuleGroup.rules.find((rule) =>
|
||||
equal(fromRulerRule(dataSourceName, namespaceName, groupName, rule), ruleIdentifier)
|
||||
);
|
||||
}, [dataSourceName, groupName, namespaceName, ruleIdentifier, rulerRuleGroup]);
|
||||
|
||||
// 1. get the rule from the ruler API with "ruleWithLocation"
|
||||
// 1.1 skip this if this datasource does not have a ruler
|
||||
//
|
||||
// 2.1 render action buttons
|
||||
// 2.2 render provisioning badge and contact point metadata, etc.
|
||||
|
||||
const actions = useMemo(() => {
|
||||
if (!rulerEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <ActionsLoader />;
|
||||
}
|
||||
|
||||
if (rulerRule) {
|
||||
return <RuleActionsButtons rule={rulerRule} promRule={rule} groupIdentifier={groupIdentifier} compact />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [groupIdentifier, isLoading, rule, rulerEnabled, rulerRule]);
|
||||
|
||||
if (isAlertingRule(rule)) {
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
name={rule.name}
|
||||
href={href}
|
||||
summary={rule.annotations?.summary}
|
||||
state={rule.state}
|
||||
health={rule.health}
|
||||
error={rule.lastError}
|
||||
labels={rule.labels}
|
||||
isProvisioned={undefined}
|
||||
instancesCount={undefined}
|
||||
actions={actions}
|
||||
origin={originMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isRecordingRule(rule)) {
|
||||
return (
|
||||
<RecordingRuleListItem
|
||||
name={rule.name}
|
||||
href={href}
|
||||
health={rule.health}
|
||||
error={rule.lastError}
|
||||
labels={rule.labels}
|
||||
isProvisioned={undefined}
|
||||
actions={null}
|
||||
origin={originMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <UnknownRuleListItem rule={rule} groupIdentifier={groupIdentifier} />;
|
||||
}
|
||||
|
||||
function createViewLinkFromIdentifier(identifier: RuleIdentifier, returnTo?: string) {
|
||||
const paramId = encodeURIComponent(stringifyIdentifier(identifier));
|
||||
const paramSource = encodeURIComponent(identifier.ruleSourceName);
|
||||
|
||||
return createRelativeUrl(`/alerting/${paramSource}/${paramId}/view`, returnTo ? { returnTo } : {});
|
||||
}
|
||||
|
||||
interface DataSourceSectionProps extends PropsWithChildren {
|
||||
uid?: string;
|
||||
name?: string;
|
||||
loader?: ReactNode;
|
||||
application?: RulesSourceApplication;
|
||||
isLoading?: boolean;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
const DataSourceSection = ({
|
||||
uid,
|
||||
name,
|
||||
application,
|
||||
children,
|
||||
loader,
|
||||
isLoading = false,
|
||||
description = null,
|
||||
}: DataSourceSectionProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={1}>
|
||||
<Stack direction="column" gap={0}>
|
||||
{isLoading && <LoadingIndicator />}
|
||||
<div className={styles.dataSourceSectionTitle}>
|
||||
{loader ?? (
|
||||
<Stack alignItems="center">
|
||||
{application && <DataSourceIcon application={application} />}
|
||||
{name && (
|
||||
<Text variant="body" weight="bold">
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
{description && (
|
||||
<>
|
||||
{'·'}
|
||||
{description}
|
||||
</>
|
||||
)}
|
||||
<Spacer />
|
||||
{uid && (
|
||||
<WithReturnButton
|
||||
title="alert rules"
|
||||
component={
|
||||
<LinkButton variant="secondary" size="sm" href={`/connections/datasources/edit/${uid}`}>
|
||||
<Trans i18nKey="alerting.rule-list.configure-datasource">Configure</Trans>
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
<div className={styles.itemsWrapper}>{children}</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
rulesTree: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
itemsWrapper: css({
|
||||
position: 'relative',
|
||||
marginLeft: theme.spacing(1.5),
|
||||
|
||||
'&:before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
|
||||
marginLeft: `-${theme.spacing(1.5)}`,
|
||||
borderLeft: `solid 1px ${theme.colors.border.weak}`,
|
||||
},
|
||||
}),
|
||||
break: css({
|
||||
width: '100%',
|
||||
height: 0,
|
||||
marginBottom: theme.spacing(2),
|
||||
borderBottom: `solid 1px ${theme.colors.border.medium}`,
|
||||
}),
|
||||
buttonsContainer: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
statsContainer: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
expandAllButton: css({
|
||||
marginRight: theme.spacing(1),
|
||||
dataSourceSectionTitle: css({
|
||||
background: theme.colors.background.secondary,
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
|
||||
|
||||
border: `solid 1px ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
}),
|
||||
});
|
||||
|
||||
export default RuleList;
|
||||
|
||||
export function CreateAlertButton() {
|
||||
const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
|
||||
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
|
||||
|
||||
const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
|
||||
|
||||
if (canCreateGrafanaRules || canCreateCloudRules) {
|
||||
return (
|
||||
<LinkButton
|
||||
href={urlUtil.renderUrl('alerting/new/alerting', { returnTo: location.pathname + location.search })}
|
||||
icon="plus"
|
||||
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
|
||||
>
|
||||
New alert rule
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
153
public/app/features/alerting/unified/rule-list/StateView.tsx
Normal file
153
public/app/features/alerting/unified/rule-list/StateView.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Counter, Pagination, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
||||
import { CombinedRule, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { usePagination } from '..//hooks/usePagination';
|
||||
import { calculateTotalInstances } from '../components/rule-viewer/RuleViewer';
|
||||
import { ListSection } from '../rule-list/components/ListSection';
|
||||
import { createViewLink } from '../utils/misc';
|
||||
import { hashRule } from '../utils/rule-id';
|
||||
import {
|
||||
getRuleGroupLocationFromCombinedRule,
|
||||
getRulePluginOrigin,
|
||||
isAlertingRule,
|
||||
isGrafanaRulerRule,
|
||||
} from '../utils/rules';
|
||||
|
||||
import { AlertRuleListItem } from './components/AlertRuleListItem';
|
||||
import { ActionsLoader, RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
|
||||
interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
}
|
||||
|
||||
type GroupedRules = Map<PromAlertingRuleState, CombinedRule[]>;
|
||||
|
||||
export const StateView = ({ namespaces }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const groupedRules = useMemo(() => {
|
||||
const result: GroupedRules = new Map([
|
||||
[PromAlertingRuleState.Firing, []],
|
||||
[PromAlertingRuleState.Pending, []],
|
||||
[PromAlertingRuleState.Inactive, []],
|
||||
]);
|
||||
|
||||
namespaces.forEach((namespace) =>
|
||||
namespace.groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
// We might hit edge cases where there type = alerting, but there is no state.
|
||||
// In this case, we shouldn't try to group these alerts in the state view
|
||||
// Even though we handle this at the API layer, this is a last catch point for any edge cases
|
||||
if (rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state) {
|
||||
result.get(rule.promRule.state)?.push(rule);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
result.forEach((rules) => rules.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
return result;
|
||||
}, [namespaces]);
|
||||
|
||||
const entries = groupedRules.entries();
|
||||
|
||||
return (
|
||||
<ul className={styles.columnStack} role="tree">
|
||||
{Array.from(entries).map(([state, rules]) => (
|
||||
<RulesByState key={state} state={state} rules={rules} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const STATE_TITLES: Record<PromAlertingRuleState, string> = {
|
||||
[PromAlertingRuleState.Firing]: 'Firing',
|
||||
[PromAlertingRuleState.Pending]: 'Pending',
|
||||
[PromAlertingRuleState.Inactive]: 'Normal',
|
||||
};
|
||||
|
||||
const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: CombinedRule[] }) => {
|
||||
const { page, pageItems, numberOfPages, onPageChange } = usePagination(rules, 1, DEFAULT_PER_PAGE_PAGINATION);
|
||||
|
||||
const isFiringState = state !== PromAlertingRuleState.Firing;
|
||||
const hasRulesMatchingState = rules.length > 0;
|
||||
|
||||
return (
|
||||
<ListSection
|
||||
title={
|
||||
<Stack alignItems="center" gap={0}>
|
||||
{STATE_TITLES[state] ?? 'Unknown'}
|
||||
<Counter value={rules.length} />
|
||||
</Stack>
|
||||
}
|
||||
collapsed={isFiringState || hasRulesMatchingState}
|
||||
pagination={
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
numberOfPages={numberOfPages}
|
||||
onNavigate={onPageChange}
|
||||
hideWhenSinglePage={true}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{pageItems.map((rule) => {
|
||||
const { rulerRule, promRule } = rule;
|
||||
|
||||
const isProvisioned = isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance);
|
||||
const instancesCount = isAlertingRule(rule.promRule) ? calculateTotalInstances(rule.instanceTotals) : undefined;
|
||||
const groupIdentifier = getRuleGroupLocationFromCombinedRule(rule);
|
||||
|
||||
if (!promRule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originMeta = getRulePluginOrigin(promRule);
|
||||
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
key={hashRule(promRule)}
|
||||
name={rule.name}
|
||||
href={createViewLink(rule.namespace.rulesSource, rule)}
|
||||
summary={rule.annotations.summary}
|
||||
state={state}
|
||||
health={rule.promRule?.health}
|
||||
error={rule.promRule?.lastError}
|
||||
labels={rule.promRule?.labels}
|
||||
isProvisioned={isProvisioned}
|
||||
instancesCount={instancesCount}
|
||||
namespace={rule.namespace.name}
|
||||
group={rule.group.name}
|
||||
actions={
|
||||
rule.rulerRule ? (
|
||||
<RuleActionsButtons
|
||||
compact
|
||||
rule={rule.rulerRule}
|
||||
promRule={promRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
/>
|
||||
) : (
|
||||
<ActionsLoader />
|
||||
)
|
||||
}
|
||||
origin={originMeta}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListSection>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
columnStack: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
});
|
@ -1,12 +1,11 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isEmpty } from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Icon, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { CombinedRule, CombinedRuleNamespace, RuleHealth } from 'app/types/unified-alerting';
|
||||
import { Rule, RuleGroupIdentifier, RuleHealth } from 'app/types/unified-alerting';
|
||||
import { Labels, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { logError } from '../../Analytics';
|
||||
@ -35,7 +34,7 @@ interface AlertRuleListItemProps {
|
||||
evaluationInterval?: string;
|
||||
labels?: Labels;
|
||||
instancesCount?: number;
|
||||
namespace?: CombinedRuleNamespace;
|
||||
namespace?: string;
|
||||
group?: string;
|
||||
// used for alert rules that use simplified routing
|
||||
contactPoint?: string;
|
||||
@ -91,7 +90,7 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(labels)) {
|
||||
if (labelsSize(labels) > 0) {
|
||||
metadata.push(
|
||||
<MetaText icon="tag-alt">
|
||||
<TextLink href={href} variant="bodySmall" color="primary" inline={false}>
|
||||
@ -139,6 +138,39 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type RecordingRuleListItemProps = Omit<AlertRuleListItemProps, 'summary' | 'state' | 'instancesCount' | 'contactPoint'>;
|
||||
|
||||
export function RecordingRuleListItem({
|
||||
name,
|
||||
href,
|
||||
health,
|
||||
isProvisioned,
|
||||
error,
|
||||
isPaused,
|
||||
origin,
|
||||
}: RecordingRuleListItemProps) {
|
||||
return (
|
||||
<ListItem
|
||||
title={
|
||||
<Stack direction="row" alignItems="center">
|
||||
<TextLink href={href} inline={false}>
|
||||
{name}
|
||||
</TextLink>
|
||||
{origin && <PluginOriginBadge pluginId={origin.pluginId} size="sm" />}
|
||||
{/* show provisioned badge only when it also doesn't have plugin origin */}
|
||||
{isProvisioned && !origin && <ProvisioningBadge />}
|
||||
{/* let's not show labels for now, but maybe users would be interested later? Or maybe show them only in the list view? */}
|
||||
{/* {labels && <AlertLabels labels={labels} size="xs" />} */}
|
||||
</Stack>
|
||||
}
|
||||
description={<Summary error={error} />}
|
||||
icon={<RuleListIcon recording={true} health={health} isPaused={isPaused} />}
|
||||
actions={null}
|
||||
meta={[]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface SummaryProps {
|
||||
content?: string;
|
||||
error?: string;
|
||||
@ -203,13 +235,14 @@ function EvaluationMetadata({ lastEvaluation, evaluationInterval, state }: Evalu
|
||||
}
|
||||
|
||||
interface UnknownRuleListItemProps {
|
||||
rule: CombinedRule;
|
||||
rule: Rule;
|
||||
groupIdentifier: RuleGroupIdentifier;
|
||||
}
|
||||
|
||||
export const UnknownRuleListItem = ({ rule }: UnknownRuleListItemProps) => {
|
||||
export const UnknownRuleListItem = ({ rule, groupIdentifier }: UnknownRuleListItemProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const ruleContext = { namespace: rule.namespace.name, group: rule.group.name, name: rule.name };
|
||||
const ruleContext = { ...groupIdentifier, name: rule.name };
|
||||
logError(new Error('unknown rule type'), ruleContext);
|
||||
|
||||
return (
|
||||
@ -219,7 +252,7 @@ export const UnknownRuleListItem = ({ rule }: UnknownRuleListItemProps) => {
|
||||
<Trans i18nKey="alerting.alert-rules.rule-definition">Rule definition</Trans>
|
||||
</summary>
|
||||
<pre>
|
||||
<code>{JSON.stringify(rule.rulerRule, null, 2)}</code>
|
||||
<code>{JSON.stringify(rule, null, 2)}</code>
|
||||
</pre>
|
||||
</details>
|
||||
</Alert>
|
||||
@ -227,7 +260,7 @@ export const UnknownRuleListItem = ({ rule }: UnknownRuleListItemProps) => {
|
||||
};
|
||||
|
||||
interface RuleLocationProps {
|
||||
namespace: CombinedRuleNamespace;
|
||||
namespace: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
@ -235,7 +268,7 @@ export const RuleLocation = ({ namespace, group }: RuleLocationProps) => (
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<Icon size="xs" name="folder" />
|
||||
<Stack direction="row" alignItems="center" gap={0}>
|
||||
{namespace.name}
|
||||
{namespace}
|
||||
<Icon size="sm" name="angle-right" />
|
||||
{group}
|
||||
</Stack>
|
||||
|
@ -17,7 +17,14 @@ interface EvaluationGroupProps extends PropsWithChildren {
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
const EvaluationGroup = ({ name, provenance, interval, onToggle, isOpen = false, children }: EvaluationGroupProps) => {
|
||||
export const EvaluationGroup = ({
|
||||
name,
|
||||
provenance,
|
||||
interval,
|
||||
onToggle,
|
||||
isOpen = false,
|
||||
children,
|
||||
}: EvaluationGroupProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isProvisioned = Boolean(provenance);
|
||||
|
||||
@ -78,5 +85,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
margin: `-${theme.spacing(0.5)}`,
|
||||
}),
|
||||
});
|
||||
|
||||
export default EvaluationGroup;
|
||||
|
@ -1,81 +0,0 @@
|
||||
import { size } from 'lodash';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { CombinedRuleGroup, RulesSource } from 'app/types/unified-alerting';
|
||||
|
||||
import { createViewLink } from '../../utils/misc';
|
||||
import { hashRulerRule } from '../../utils/rule-id';
|
||||
import { isAlertingRule, isGrafanaRulerRule, isRecordingRule } from '../../utils/rules';
|
||||
|
||||
import { AlertRuleListItem, UnknownRuleListItem } from './AlertRuleListItem';
|
||||
import EvaluationGroup from './EvaluationGroup';
|
||||
|
||||
export interface EvaluationGroupWithRulesProps {
|
||||
group: CombinedRuleGroup;
|
||||
rulesSource: RulesSource;
|
||||
}
|
||||
|
||||
export const EvaluationGroupWithRules = ({ group, rulesSource }: EvaluationGroupWithRulesProps) => {
|
||||
const [open, toggleOpen] = useToggle(false);
|
||||
|
||||
return (
|
||||
<EvaluationGroup name={group.name} interval={group.interval} isOpen={open} onToggle={toggleOpen}>
|
||||
{group.rules.map((rule, index) => {
|
||||
const { rulerRule, promRule, annotations } = rule;
|
||||
|
||||
// don't render anything if we don't have the rule definition yet
|
||||
if (!rulerRule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// keep in mind that we may not have a promRule for the ruler rule – this happens when the target
|
||||
// rule source is eventually consistent - it may know about the rule definition but not its state
|
||||
const isAlertingPromRule = isAlertingRule(promRule);
|
||||
|
||||
if (isAlertingRule(rule.promRule) || isRecordingRule(rule.promRule)) {
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
key={hashRulerRule(rulerRule)}
|
||||
state={isAlertingPromRule ? promRule?.state : undefined}
|
||||
health={promRule?.health}
|
||||
error={promRule?.lastError}
|
||||
name={rule.name}
|
||||
labels={rulerRule.labels}
|
||||
lastEvaluation={promRule?.lastEvaluation}
|
||||
evaluationInterval={group.interval}
|
||||
instancesCount={isAlertingPromRule ? size(promRule.alerts) : undefined}
|
||||
href={createViewLink(rulesSource, rule)}
|
||||
summary={annotations?.summary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGrafanaRulerRule(rulerRule)) {
|
||||
const contactPoint = rulerRule.grafana_alert.notification_settings?.receiver;
|
||||
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
key={rulerRule.grafana_alert.uid}
|
||||
name={rulerRule.grafana_alert.title}
|
||||
state={isAlertingPromRule ? promRule?.state : undefined}
|
||||
health={promRule?.health}
|
||||
error={promRule?.lastError}
|
||||
labels={rulerRule.labels}
|
||||
isPaused={rulerRule.grafana_alert.is_paused}
|
||||
lastEvaluation={promRule?.lastEvaluation}
|
||||
evaluationInterval={group.interval}
|
||||
instancesCount={isAlertingPromRule ? size(promRule.alerts) : undefined}
|
||||
href={createViewLink(rulesSource, rule)}
|
||||
summary={rule.annotations?.summary}
|
||||
isProvisioned={Boolean(rulerRule.grafana_alert.provenance)}
|
||||
contactPoint={contactPoint}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// if we get here it means we don't really know how to render this rule
|
||||
return <UnknownRuleListItem key={hashRulerRule(rulerRule)} rule={rule} />;
|
||||
})}
|
||||
</EvaluationGroup>
|
||||
);
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
@ -13,36 +14,39 @@ interface GroupProps extends PropsWithChildren {
|
||||
metaRight?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
isOpen?: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export const Group = ({
|
||||
export const ListGroup = ({
|
||||
name,
|
||||
description,
|
||||
onToggle,
|
||||
isOpen = false,
|
||||
isOpen = true,
|
||||
metaRight = null,
|
||||
actions = null,
|
||||
children,
|
||||
}: GroupProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [open, toggle] = useToggle(isOpen);
|
||||
|
||||
return (
|
||||
<div className={styles.groupWrapper} role="treeitem" aria-expanded={isOpen} aria-selected="false">
|
||||
<div className={styles.groupWrapper} role="treeitem" aria-expanded={open} aria-selected="false">
|
||||
<GroupHeader
|
||||
onToggle={onToggle}
|
||||
isOpen={isOpen}
|
||||
onToggle={() => toggle()}
|
||||
isOpen={open}
|
||||
description={description}
|
||||
name={name}
|
||||
metaRight={metaRight}
|
||||
actions={actions}
|
||||
/>
|
||||
{isOpen && <div role="group">{children}</div>}
|
||||
{open && <div role="group">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupHeader = (props: GroupProps) => {
|
||||
type GroupHeaderProps = GroupProps & {
|
||||
onToggle: () => void;
|
||||
};
|
||||
|
||||
const GroupHeader = (props: GroupHeaderProps) => {
|
||||
const { name, description, metaRight = null, actions = null, isOpen = false, onToggle } = props;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -50,9 +54,9 @@ const GroupHeader = (props: GroupProps) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Stack alignItems="center" gap={0}>
|
||||
<IconButton
|
||||
name={isOpen ? 'angle-right' : 'angle-down'}
|
||||
name={isOpen ? 'angle-down' : 'angle-right'}
|
||||
onClick={onToggle}
|
||||
aria-label={t('common.collapse', 'Collapse')}
|
||||
/>
|
||||
@ -76,7 +80,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
headerWrapper: css({
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
|
||||
padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
|
||||
|
||||
background: theme.colors.background.secondary,
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const ListSection = ({
|
||||
<li className={styles.wrapper} role="treeitem" aria-selected="false">
|
||||
<div className={styles.sectionTitle}>
|
||||
<Stack alignItems="center">
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Stack alignItems="center" gap={0}>
|
||||
<IconButton
|
||||
name={isCollapsed ? 'angle-right' : 'angle-down'}
|
||||
onClick={toggleCollapsed}
|
||||
@ -65,18 +65,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
border: `solid 1px ${theme.colors.border.weak}`,
|
||||
borderBottom: 'none',
|
||||
|
||||
marginLeft: theme.spacing(3),
|
||||
|
||||
'&:before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
|
||||
borderLeft: `solid 1px ${theme.colors.border.weak}`,
|
||||
|
||||
marginTop: 0,
|
||||
marginLeft: `-${theme.spacing(2.5)}`,
|
||||
},
|
||||
marginLeft: theme.spacing(1.5),
|
||||
}),
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
@ -88,7 +77,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
|
||||
|
||||
background: theme.colors.background.secondary,
|
||||
|
||||
border: `solid 1px ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
}),
|
||||
|
@ -21,7 +21,7 @@ const Namespace = ({ children, name, href, application }: NamespaceProps) => {
|
||||
<li className={styles.namespaceWrapper} role="treeitem" aria-selected="false">
|
||||
<div className={styles.namespaceTitle}>
|
||||
<Stack alignItems={'center'} gap={1}>
|
||||
<NamespaceIcon application={application} />
|
||||
<DataSourceIcon application={application} />
|
||||
{href ? (
|
||||
<WithReturnButton
|
||||
title="Alert rules"
|
||||
@ -49,7 +49,7 @@ interface NamespaceIconProps {
|
||||
application?: RulesSourceApplication;
|
||||
}
|
||||
|
||||
const NamespaceIcon = ({ application }: NamespaceIconProps) => {
|
||||
export const DataSourceIcon = ({ application }: NamespaceIconProps) => {
|
||||
switch (application) {
|
||||
case PromApplication.Prometheus:
|
||||
return (
|
||||
@ -64,11 +64,11 @@ const NamespaceIcon = ({ application }: NamespaceIconProps) => {
|
||||
return (
|
||||
<img width={16} height={16} src="public/app/plugins/datasource/prometheus/img/mimir_logo.svg" alt="Mimir" />
|
||||
);
|
||||
case 'loki':
|
||||
case 'Loki':
|
||||
return <img width={16} height={16} src="public/app/plugins/datasource/loki/img/loki_icon.svg" alt="Loki" />;
|
||||
case 'grafana':
|
||||
default:
|
||||
return <Icon name="folder" />;
|
||||
return <Icon name="grafana" />;
|
||||
}
|
||||
};
|
||||
|
||||
@ -101,10 +101,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
namespaceTitle: css({
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
|
||||
|
||||
background: theme.colors.background.secondary,
|
||||
// background: theme.colors.background.secondary,
|
||||
|
||||
border: `solid 1px ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
// border: `solid 1px ${theme.colors.border.weak}`,
|
||||
// borderRadius: theme.shape.radius.default,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,114 @@
|
||||
import { useState } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { LinkButton, Stack } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu';
|
||||
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
|
||||
import { RedirectToCloneRule } from 'app/features/alerting/unified/components/rules/CloneRule';
|
||||
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
|
||||
import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer';
|
||||
import { useRulesFilter } from 'app/features/alerting/unified/hooks/useFilteredRules';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { Rule, RuleGroupIdentifier, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isGrafanaAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
interface Props {
|
||||
rule: RulerRuleDTO;
|
||||
promRule: Rule;
|
||||
groupIdentifier: RuleGroupIdentifier;
|
||||
/**
|
||||
* Should we show the buttons in a "compact" state?
|
||||
* i.e. without text and using smaller button sizes
|
||||
*/
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
// For now this is just a copy of RuleActionsButtons.tsx but with the View button removed.
|
||||
// This is only done to keep the new list behind a feature flag and limit changes in the existing components
|
||||
export const RuleActionsButtons = ({ compact, rule, promRule, groupIdentifier }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const redirectToListView = compact ? false : true;
|
||||
const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
|
||||
|
||||
const [showSilenceDrawer, setShowSilenceDrawer] = useState<boolean>(false);
|
||||
|
||||
const [redirectToClone, setRedirectToClone] = useState<
|
||||
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
|
||||
>(undefined);
|
||||
|
||||
const { namespaceName, groupName, dataSourceName } = groupIdentifier;
|
||||
const { hasActiveFilters } = useRulesFilter();
|
||||
|
||||
const isProvisioned = isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance);
|
||||
|
||||
const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rule, groupIdentifier, AlertRuleAction.Update);
|
||||
|
||||
const canEditRule = editRuleSupported && editRuleAllowed;
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
const buttonSize = compact ? 'sm' : 'md';
|
||||
|
||||
const identifier = ruleId.fromRulerRule(dataSourceName, namespaceName, groupName, rule);
|
||||
|
||||
if (canEditRule) {
|
||||
const identifier = ruleId.fromRulerRule(dataSourceName, namespaceName, groupName, rule);
|
||||
|
||||
const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`);
|
||||
|
||||
buttons.push(
|
||||
<LinkButton title="Edit" size={buttonSize} key="edit" variant="secondary" icon="pen" href={editURL}>
|
||||
<Trans i18nKey="common.edit">Edit</Trans>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={1} alignItems="center" wrap="nowrap">
|
||||
{buttons}
|
||||
<AlertRuleMenu
|
||||
buttonSize={buttonSize}
|
||||
rulerRule={rule}
|
||||
promRule={promRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
identifier={identifier}
|
||||
handleDelete={() => showDeleteModal(rule, groupIdentifier)}
|
||||
handleSilence={() => setShowSilenceDrawer(true)}
|
||||
handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
|
||||
onPauseChange={() => {
|
||||
// Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect
|
||||
// of breaking some unrelated tests in Policy.test.tsx due to mocking approach
|
||||
const limitAlerts = hasActiveFilters ? undefined : INSTANCES_DISPLAY_LIMIT + 1;
|
||||
// Trigger a re-fetch of the rules table
|
||||
// TODO: Migrate rules table functionality to RTK Query, so we instead rely
|
||||
// on tag invalidation (or optimistic cache updates) for this
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME, limitAlerts }));
|
||||
}}
|
||||
/>
|
||||
{deleteModal}
|
||||
{isGrafanaAlertingRule(rule) && showSilenceDrawer && (
|
||||
<AlertmanagerProvider accessType="instance">
|
||||
<SilenceGrafanaRuleDrawer rulerRule={rule} onClose={() => setShowSilenceDrawer(false)} />
|
||||
</AlertmanagerProvider>
|
||||
)}
|
||||
{redirectToClone?.identifier && (
|
||||
<RedirectToCloneRule
|
||||
identifier={redirectToClone.identifier}
|
||||
isProvisioned={redirectToClone.isProvisioned}
|
||||
onDismiss={() => setRedirectToClone(undefined)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActionsLoader = () => <Skeleton width={50} height={16} />;
|
@ -10,7 +10,7 @@ import { usePagination } from '../../hooks/usePagination';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
|
||||
import { AlertRuleListItem } from './AlertRuleListItem';
|
||||
import EvaluationGroup from './EvaluationGroup';
|
||||
import { EvaluationGroup } from './EvaluationGroup';
|
||||
import { SkeletonListItem } from './ListItem';
|
||||
|
||||
interface EvaluationGroupLoaderProps {
|
||||
@ -75,6 +75,16 @@ export const EvaluationGroupLoader = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingIndicator = () => {
|
||||
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<LoadingBar width={width} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupLoadingIndicator = () => {
|
||||
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
||||
|
||||
|
@ -10,18 +10,12 @@ import {
|
||||
Receiver,
|
||||
TestReceiversAlert,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { FolderDTO, StoreState, ThunkResult } from 'app/types';
|
||||
import {
|
||||
PromBasedDataSource,
|
||||
RuleIdentifier,
|
||||
RuleNamespace,
|
||||
RulerDataSourceConfig,
|
||||
StateHistoryItem,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { PromApplication, RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
import { FolderDTO, ThunkResult } from 'app/types';
|
||||
import { RuleIdentifier, RuleNamespace, StateHistoryItem } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||
import { withPerformanceLogging, withPromRulesMetadataLogging, withRulerRulesMetadataLogging } from '../Analytics';
|
||||
import { withPromRulesMetadataLogging, withRulerRulesMetadataLogging } from '../Analytics';
|
||||
import {
|
||||
deleteAlertManagerConfig,
|
||||
fetchAlertGroups,
|
||||
@ -30,43 +24,16 @@ import {
|
||||
} from '../api/alertmanager';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { fetchAnnotations } from '../api/annotations';
|
||||
import { discoverFeatures } from '../api/buildInfo';
|
||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||
import { FetchRulerRulesFilter, fetchRulerRules } from '../api/ruler';
|
||||
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames, getRulesDataSource } from '../utils/datasource';
|
||||
import { getAllRulesSourceNames } from '../utils/datasource';
|
||||
import { makeAMLink } from '../utils/misc';
|
||||
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
|
||||
import { withAppEvents, withSerializedError } from '../utils/redux';
|
||||
import { getAlertInfo } from '../utils/rules';
|
||||
import { safeParsePrometheusDuration } from '../utils/time';
|
||||
|
||||
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
|
||||
const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
|
||||
const dsConfig = dataSources[rulesSourceName]?.result;
|
||||
const dsError = dataSources[rulesSourceName]?.error;
|
||||
|
||||
// @TODO use aggregateError but add support for it in "stringifyErrorLike"
|
||||
if (!dsConfig) {
|
||||
const error = new Error(`Data source configuration is not available for "${rulesSourceName}" data source`);
|
||||
if (dsError) {
|
||||
error.cause = dsError;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return dsConfig;
|
||||
}
|
||||
|
||||
export function getDataSourceRulerConfig(getState: () => unknown, rulesSourceName: string) {
|
||||
const dsConfig = getDataSourceConfig(getState, rulesSourceName);
|
||||
if (!dsConfig.rulerConfig) {
|
||||
throw new Error(`Ruler API is not available for ${rulesSourceName}`);
|
||||
}
|
||||
|
||||
return dsConfig.rulerConfig;
|
||||
}
|
||||
|
||||
export const fetchPromRulesAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchPromRules',
|
||||
async (
|
||||
@ -87,8 +54,6 @@ export const fetchPromRulesAction = createAsyncThunk(
|
||||
},
|
||||
thunkAPI
|
||||
): Promise<RuleNamespace[]> => {
|
||||
await thunkAPI.dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
|
||||
|
||||
const fetchRulesWithLogging = withPromRulesMetadataLogging('unifiedalerting/fetchPromRules', fetchRules, {
|
||||
dataSourceName: rulesSourceName,
|
||||
thunk: 'unifiedalerting/fetchPromRules',
|
||||
@ -112,8 +77,13 @@ export const fetchRulerRulesAction = createAsyncThunk(
|
||||
},
|
||||
{ dispatch, getState }
|
||||
): Promise<RulerRulesConfigDTO | null> => {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
|
||||
const rulerConfig = getDataSourceRulerConfig(getState, rulesSourceName);
|
||||
const { data: dsFeatures } = await dispatch(
|
||||
featureDiscoveryApi.endpoints.discoverDsFeatures.initiate({ rulesSourceName })
|
||||
);
|
||||
|
||||
if (!dsFeatures?.rulerConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fetchRulerRulesWithLogging = withRulerRulesMetadataLogging(
|
||||
'unifiedalerting/fetchRulerRules',
|
||||
@ -124,7 +94,7 @@ export const fetchRulerRulesAction = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
return await withSerializedError(fetchRulerRulesWithLogging(rulerConfig, filter));
|
||||
return await withSerializedError(fetchRulerRulesWithLogging(dsFeatures.rulerConfig, filter));
|
||||
}
|
||||
);
|
||||
|
||||
@ -143,89 +113,18 @@ export function fetchPromAndRulerRulesAction({
|
||||
matcher?: Matcher[];
|
||||
state?: string[];
|
||||
}): ThunkResult<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
|
||||
const dsConfig = getDataSourceConfig(getState, rulesSourceName);
|
||||
|
||||
await dispatch(fetchPromRulesAction({ rulesSourceName, identifier, filter, limitAlerts, matcher, state }));
|
||||
if (dsConfig.rulerConfig) {
|
||||
await dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: memoize this or move to RTK Query so we can cache results!
|
||||
export function fetchAllPromBuildInfoAction(): ThunkResult<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
const allRequests = getAllRulesSourceNames().map((rulesSourceName) =>
|
||||
dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }))
|
||||
const { data: dsFeatures } = await dispatch(
|
||||
featureDiscoveryApi.endpoints.discoverDsFeatures.initiate({ rulesSourceName })
|
||||
);
|
||||
|
||||
await Promise.allSettled(allRequests);
|
||||
await Promise.all([
|
||||
dispatch(fetchPromRulesAction({ rulesSourceName, identifier, filter, limitAlerts, matcher, state })),
|
||||
dsFeatures?.rulerConfig ? dispatch(fetchRulerRulesAction({ rulesSourceName })) : Promise.resolve(),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchRulesSourceBuildInfoAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchPromBuildinfo',
|
||||
async ({ rulesSourceName }: { rulesSourceName: string }): Promise<PromBasedDataSource> => {
|
||||
return withSerializedError<PromBasedDataSource>(
|
||||
(async (): Promise<PromBasedDataSource> => {
|
||||
if (rulesSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return {
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
id: GRAFANA_RULES_SOURCE_NAME,
|
||||
rulerConfig: {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
apiVersion: 'legacy',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ds = getRulesDataSource(rulesSourceName);
|
||||
if (!ds) {
|
||||
throw new Error(`Missing data source configuration for ${rulesSourceName}`);
|
||||
}
|
||||
|
||||
const { id, name } = ds;
|
||||
|
||||
const discoverFeaturesWithLogging = withPerformanceLogging(
|
||||
'unifiedalerting/fetchPromBuildinfo',
|
||||
discoverFeatures,
|
||||
{
|
||||
dataSourceName: rulesSourceName,
|
||||
thunk: 'unifiedalerting/fetchPromBuildinfo',
|
||||
}
|
||||
);
|
||||
|
||||
const buildInfo = await discoverFeaturesWithLogging(name);
|
||||
|
||||
const rulerConfig: RulerDataSourceConfig | undefined = buildInfo.features.rulerApiEnabled
|
||||
? {
|
||||
dataSourceName: name,
|
||||
apiVersion: buildInfo.application === PromApplication.Cortex ? 'legacy' : 'config',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
id: id,
|
||||
rulerConfig,
|
||||
};
|
||||
})()
|
||||
);
|
||||
},
|
||||
{
|
||||
condition: ({ rulesSourceName }, { getState }) => {
|
||||
const dataSources: AsyncRequestMapSlice<PromBasedDataSource> = (getState() as StoreState).unifiedAlerting
|
||||
.dataSources;
|
||||
const hasLoaded = Boolean(dataSources[rulesSourceName]?.result);
|
||||
const hasError = Boolean(dataSources[rulesSourceName]?.error);
|
||||
|
||||
return !(hasLoaded || hasError);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface FetchPromRulesRulesActionProps {
|
||||
filter?: FetchPromRulesFilter;
|
||||
limitAlerts?: number;
|
||||
@ -242,18 +141,18 @@ export function fetchAllPromAndRulerRulesAction(
|
||||
|
||||
await Promise.allSettled(
|
||||
getAllRulesSourceNames().map(async (rulesSourceName) => {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
|
||||
const { data: dsFeatures } = await dispatch(
|
||||
featureDiscoveryApi.endpoints.discoverDsFeatures.initiate({ rulesSourceName })
|
||||
);
|
||||
|
||||
const { promRules, rulerRules, dataSources } = getStore().unifiedAlerting;
|
||||
const dataSourceConfig = dataSources[rulesSourceName].result;
|
||||
const { promRules, rulerRules } = getStore().unifiedAlerting;
|
||||
|
||||
if (!dataSourceConfig) {
|
||||
if (!dsFeatures) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldLoadProm = force || !promRules[rulesSourceName]?.loading;
|
||||
const shouldLoadRuler =
|
||||
(force || !rulerRules[rulesSourceName]?.loading) && Boolean(dataSourceConfig.rulerConfig);
|
||||
const shouldLoadRuler = (force || !rulerRules[rulesSourceName]?.loading) && Boolean(dsFeatures?.rulerConfig);
|
||||
|
||||
await Promise.allSettled([
|
||||
shouldLoadProm && dispatch(fetchPromRulesAction({ rulesSourceName, ...options })),
|
||||
|
@ -9,17 +9,11 @@ import {
|
||||
fetchGrafanaAnnotationsAction,
|
||||
fetchPromRulesAction,
|
||||
fetchRulerRulesAction,
|
||||
fetchRulesSourceBuildInfoAction,
|
||||
testReceiversAction,
|
||||
updateAlertManagerConfigAction,
|
||||
} from './actions';
|
||||
|
||||
export const reducer = combineReducers({
|
||||
dataSources: createAsyncMapSlice(
|
||||
'dataSources',
|
||||
fetchRulesSourceBuildInfoAction,
|
||||
({ rulesSourceName }) => rulesSourceName
|
||||
).reducer,
|
||||
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer,
|
||||
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName)
|
||||
.reducer,
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { RulesSource } from 'app/types/unified-alerting';
|
||||
import { PromApplication, RulesSourceApplication } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { PERMISSIONS_CONTACT_POINTS } from '../components/contact-points/permissions';
|
||||
@ -22,6 +21,8 @@ import { getAllDataSources } from './config';
|
||||
export const GRAFANA_RULES_SOURCE_NAME = 'grafana';
|
||||
export const GRAFANA_DATASOURCE_NAME = '-- Grafana --';
|
||||
|
||||
export type RulesSourceIdentifier = { rulesSourceName: string } | { uid: string };
|
||||
|
||||
export enum DataSourceType {
|
||||
Alertmanager = 'alertmanager',
|
||||
Loki = 'loki',
|
||||
@ -39,12 +40,15 @@ export interface AlertManagerDataSource {
|
||||
export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus];
|
||||
|
||||
export function getRulesDataSources() {
|
||||
if (!contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead)) {
|
||||
const hasReadPermission = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead);
|
||||
const hasWritePermission = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
|
||||
if (!hasReadPermission && !hasWritePermission) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getAllDataSources()
|
||||
.filter((ds) => RulesDataSourceTypes.includes(ds.type) && ds.jsonData.manageAlerts !== false)
|
||||
.filter((ds) => RulesDataSourceTypes.includes(ds.type))
|
||||
.filter((ds) => isDataSourceManagingAlerts(ds))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
@ -56,6 +60,10 @@ export function getRulesDataSource(rulesSourceName: string) {
|
||||
return getRulesDataSources().find((x) => x.name === rulesSourceName);
|
||||
}
|
||||
|
||||
export function getRulesDataSourceByUID(uid: string) {
|
||||
return getRulesDataSources().find((x) => x.uid === uid);
|
||||
}
|
||||
|
||||
export function getAlertManagerDataSources() {
|
||||
return getAllDataSources()
|
||||
.filter(isAlertmanagerDataSourceInstance)
|
||||
@ -203,7 +211,7 @@ export function getAllRulesSources(): RulesSource[] {
|
||||
const availableRulesSources: RulesSource[] = getRulesDataSources();
|
||||
|
||||
if (contextSrv.hasPermission(AccessControlAction.AlertingRuleRead)) {
|
||||
availableRulesSources.push(GRAFANA_RULES_SOURCE_NAME);
|
||||
availableRulesSources.unshift(GRAFANA_RULES_SOURCE_NAME);
|
||||
}
|
||||
|
||||
return availableRulesSources;
|
||||
@ -242,6 +250,10 @@ export function getDataSourceByName(name: string): DataSourceInstanceSettings<Da
|
||||
return getAllDataSources().find((source) => source.name === name);
|
||||
}
|
||||
|
||||
export function getDataSourceByUid(dsUid: string): DataSourceInstanceSettings<DataSourceJsonData> | undefined {
|
||||
return getAllDataSources().find((source) => source.uid === dsUid);
|
||||
}
|
||||
|
||||
export function getAlertmanagerDataSourceByName(name: string) {
|
||||
return getAllDataSources()
|
||||
.filter(isAlertmanagerDataSourceInstance)
|
||||
@ -277,6 +289,22 @@ export function getDatasourceAPIUid(dataSourceName: string) {
|
||||
return ds.uid;
|
||||
}
|
||||
|
||||
export function getDataSourceUID(rulesSourceIdentifier: RulesSourceIdentifier) {
|
||||
if ('uid' in rulesSourceIdentifier) {
|
||||
return rulesSourceIdentifier.uid;
|
||||
}
|
||||
|
||||
if (rulesSourceIdentifier.rulesSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return GRAFANA_RULES_SOURCE_NAME;
|
||||
}
|
||||
|
||||
const ds = getRulesDataSource(rulesSourceIdentifier.rulesSourceName);
|
||||
if (!ds) {
|
||||
return undefined;
|
||||
}
|
||||
return ds.uid;
|
||||
}
|
||||
|
||||
export function getFirstCompatibleDataSource(): DataSourceInstanceSettings<DataSourceJsonData> | undefined {
|
||||
return getDataSourceSrv().getList({ alerting: true })[0];
|
||||
}
|
||||
@ -291,20 +319,3 @@ export function getDefaultOrFirstCompatibleDataSource(): DataSourceInstanceSetti
|
||||
export function isDataSourceManagingAlerts(ds: DataSourceInstanceSettings<DataSourceJsonData>) {
|
||||
return ds.jsonData.manageAlerts !== false; //if this prop is undefined it defaults to true
|
||||
}
|
||||
|
||||
export function getApplicationFromRulesSource(rulesSource: RulesSource): RulesSourceApplication {
|
||||
if (isGrafanaRulesSource(rulesSource)) {
|
||||
return 'grafana';
|
||||
}
|
||||
|
||||
// @TODO use buildinfo
|
||||
if ('prometheusType' in rulesSource.jsonData) {
|
||||
return rulesSource.jsonData?.prometheusType ?? PromApplication.Prometheus;
|
||||
}
|
||||
|
||||
if (rulesSource.type === 'loki') {
|
||||
return 'loki';
|
||||
}
|
||||
|
||||
return PromApplication.Prometheus; // assume Prometheus if nothing matches
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { Labels } from '../../../../types/unified-alerting-dto';
|
||||
import { Label } from '../components/rules/state-history/common';
|
||||
|
||||
@ -35,7 +37,11 @@ export function arrayKeyValuesToObject(
|
||||
|
||||
export const GRAFANA_ORIGIN_LABEL = '__grafana_origin';
|
||||
|
||||
export function labelsSize(labels: Labels) {
|
||||
export function labelsSize(labels?: Labels) {
|
||||
if (isEmpty(labels)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Object.keys(labels).filter((key) => !isPrivateLabelKey(key)).length;
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,21 @@ import { config, isFetchError } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { escapePathSeparators } from 'app/features/alerting/unified/utils/rule-id';
|
||||
import { alertInstanceKey, isGrafanaRulerRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import {
|
||||
alertInstanceKey,
|
||||
isCloudRuleIdentifier,
|
||||
isGrafanaRuleIdentifier,
|
||||
isPrometheusRuleIdentifier,
|
||||
} from 'app/features/alerting/unified/utils/rules';
|
||||
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
||||
import { Alert, CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting';
|
||||
import {
|
||||
Alert,
|
||||
CombinedRule,
|
||||
FilterState,
|
||||
RuleIdentifier,
|
||||
RulesSource,
|
||||
SilenceFilterState,
|
||||
} from 'app/types/unified-alerting';
|
||||
import {
|
||||
GrafanaAlertState,
|
||||
PromAlertingRuleState,
|
||||
@ -16,7 +28,7 @@ import {
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
|
||||
import { getRulesSourceName, isCloudRulesSource } from './datasource';
|
||||
import { getRulesSourceName } from './datasource';
|
||||
import { getMatcherQueryParams } from './matchers';
|
||||
import * as ruleId from './rule-id';
|
||||
import { createAbsoluteUrl, createRelativeUrl } from './url';
|
||||
@ -67,13 +79,13 @@ export function createMuteTimingLink(muteTimingName: string, alertManagerSourceN
|
||||
});
|
||||
}
|
||||
|
||||
export function createShareLink(ruleSource: RulesSource, rule: CombinedRule): string | undefined {
|
||||
if (isCloudRulesSource(ruleSource)) {
|
||||
export function createShareLink(ruleIdentifier: RuleIdentifier): string | undefined {
|
||||
if (isCloudRuleIdentifier(ruleIdentifier) || isPrometheusRuleIdentifier(ruleIdentifier)) {
|
||||
return createAbsoluteUrl(
|
||||
`/alerting/${encodeURIComponent(ruleSource.name)}/${encodeURIComponent(escapePathSeparators(rule.name))}/find`
|
||||
`/alerting/${encodeURIComponent(ruleIdentifier.ruleSourceName)}/${encodeURIComponent(escapePathSeparators(ruleIdentifier.ruleName))}/find`
|
||||
);
|
||||
} else if (isGrafanaRulerRule(rule.rulerRule)) {
|
||||
return createAbsoluteUrl(`/alerting/grafana/${rule.rulerRule.grafana_alert.uid}/view`);
|
||||
} else if (isGrafanaRuleIdentifier(ruleIdentifier)) {
|
||||
return createAbsoluteUrl(`/alerting/grafana/${ruleIdentifier.uid}/view`);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
mockCombinedRule,
|
||||
mockCombinedRuleGroup,
|
||||
mockGrafanaRulerRule,
|
||||
mockPromAlertingRule,
|
||||
mockRuleWithLocation,
|
||||
mockRulerAlertingRule,
|
||||
} from '../mocks';
|
||||
@ -20,21 +21,21 @@ import {
|
||||
|
||||
describe('getRuleOrigin', () => {
|
||||
it('returns undefined when no origin label is present', () => {
|
||||
const rule = mockCombinedRule({
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: {},
|
||||
});
|
||||
expect(getRulePluginOrigin(rule)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when origin label does not match expected format', () => {
|
||||
const rule = mockCombinedRule({
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { [GRAFANA_ORIGIN_LABEL]: 'invalid_format' },
|
||||
});
|
||||
expect(getRulePluginOrigin(rule)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when plugin is not installed', () => {
|
||||
const rule = mockCombinedRule({
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/uninstalled_plugin' },
|
||||
});
|
||||
expect(getRulePluginOrigin(rule)).toBeUndefined();
|
||||
@ -64,7 +65,7 @@ describe('getRuleOrigin', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const rule = mockCombinedRule({
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' },
|
||||
});
|
||||
expect(getRulePluginOrigin(rule)).toEqual({ pluginId: 'installed_plugin' });
|
||||
|
@ -176,10 +176,12 @@ export interface RulePluginOrigin {
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export function getRulePluginOrigin(rule: CombinedRule): RulePluginOrigin | undefined {
|
||||
// com.grafana.origin=plugin/<plugin-identifier>
|
||||
// Prom and Mimir do not support dots in label names 😔
|
||||
const origin = rule.labels[GRAFANA_ORIGIN_LABEL];
|
||||
export function getRulePluginOrigin(rule?: Rule | RulerRuleDTO): RulePluginOrigin | undefined {
|
||||
if (!rule) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const origin = rule.labels?.[GRAFANA_ORIGIN_LABEL];
|
||||
if (!origin) {
|
||||
return undefined;
|
||||
}
|
||||
@ -203,7 +205,7 @@ function isPluginInstalled(pluginId: string) {
|
||||
return Boolean(config.apps[pluginId]);
|
||||
}
|
||||
|
||||
export function isPluginProvidedRule(rule: CombinedRule): boolean {
|
||||
export function isPluginProvidedRule(rule?: Rule | RulerRuleDTO): boolean {
|
||||
return Boolean(getRulePluginOrigin(rule));
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ export enum PromApplication {
|
||||
Thanos = 'Thanos',
|
||||
}
|
||||
|
||||
export type RulesSourceApplication = PromApplication | 'loki' | 'grafana';
|
||||
export type RulesSourceApplication = PromApplication | 'Loki' | 'grafana';
|
||||
|
||||
export interface PromBuildInfoResponse {
|
||||
data: {
|
||||
@ -96,7 +96,7 @@ export interface PromBuildInfoResponse {
|
||||
}
|
||||
|
||||
export interface PromApiFeatures {
|
||||
application?: PromApplication;
|
||||
application: RulesSourceApplication;
|
||||
features: {
|
||||
rulerApiEnabled: boolean;
|
||||
};
|
||||
|
@ -280,6 +280,10 @@
|
||||
"success": "Successfully updated rule group"
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "Configure",
|
||||
"new-alert-rule": "New alert rule"
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "Creating",
|
||||
"deleting": "Deleting",
|
||||
|
@ -280,6 +280,10 @@
|
||||
"success": "Ŝūččęşşƒūľľy ūpđäŧęđ řūľę ģřőūp"
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "Cőʼnƒįģūřę",
|
||||
"new-alert-rule": "Ńęŵ äľęřŧ řūľę"
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "Cřęäŧįʼnģ",
|
||||
"deleting": "Đęľęŧįʼnģ",
|
||||
|
Loading…
Reference in New Issue
Block a user