mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
feat(ui): allow silencing alerts
This adds a modal dialog for silencing alerts
This commit is contained in:
439
ui/package-lock.json
generated
439
ui/package-lock.json
generated
@@ -13,6 +13,88 @@
|
||||
"prop-types": "15.6.2"
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-imports": {
|
||||
"version": "7.0.0-beta.51",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.51.tgz",
|
||||
"integrity": "sha1-zgBCgEX7t9XrwOp7+DV4nxU2arI=",
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-beta.51",
|
||||
"lodash": "4.17.10"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.0.0-beta.51",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.51.tgz",
|
||||
"integrity": "sha1-2AK3tUO1g2x3iqaReXq/APPZfqk=",
|
||||
"requires": {
|
||||
"esutils": "2.0.2",
|
||||
"lodash": "4.17.10",
|
||||
"to-fast-properties": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@emotion/babel-utils": {
|
||||
"version": "0.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.9.tgz",
|
||||
"integrity": "sha512-QN2+TP+x5QWuOGUv8TZwdMiF8PHgBQiLx646rKZBnakgc9gLYFi+gsROVxE6YTNHSaEv0fWsFjDasDyiWSJlDg==",
|
||||
"requires": {
|
||||
"@emotion/hash": "0.6.5",
|
||||
"@emotion/memoize": "0.6.5",
|
||||
"@emotion/serialize": "0.9.0",
|
||||
"convert-source-map": "1.5.1",
|
||||
"find-root": "1.1.0",
|
||||
"source-map": "0.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@emotion/hash": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.5.tgz",
|
||||
"integrity": "sha512-JlZbn5+adseTdDPTUkx/O1/UZbhaGR5fCLLWQDCIJ4eP9fJcVdP/qjlTveEX6mkNoJHWFbZ47wArWQQ0Qk6nMA=="
|
||||
},
|
||||
"@emotion/memoize": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.5.tgz",
|
||||
"integrity": "sha512-n1USr7yICA4LFIv7z6kKsXM8rZJxd1btKCBmDewlit+3OJ2j4bDfgXTAxTHYbPkHS/eztHmFWfsbxW2Pu5mDqA=="
|
||||
},
|
||||
"@emotion/serialize": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.0.tgz",
|
||||
"integrity": "sha512-ScuBRGxHCyAEN8YgQSsxtG5ddmP9+Of8WkxC7hidhGTxKhq3lgeCu5cFk2WdAMrpYgEd0U4g4QW/1YrCOGpAsA==",
|
||||
"requires": {
|
||||
"@emotion/hash": "0.6.5",
|
||||
"@emotion/memoize": "0.6.5",
|
||||
"@emotion/unitless": "0.6.6",
|
||||
"@emotion/utils": "0.8.1"
|
||||
}
|
||||
},
|
||||
"@emotion/stylis": {
|
||||
"version": "0.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.6.12.tgz",
|
||||
"integrity": "sha512-yS+t7l5FeYeiIyADyqjFBJvdotpphHb2S3mP4qak5BpV7ODvxuyAVF24IchEslW+A1MWHAhn5SiOW6GZIumiEQ=="
|
||||
},
|
||||
"@emotion/unitless": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.6.tgz",
|
||||
"integrity": "sha512-zbd1vXRpGWCgDLsXqITReL+eqYJ95PYyWrVCCuMLBDb2LGA/HdxrZHJri6Fe+tKHihBOiCK1kbu+3Ij8aNEjzA=="
|
||||
},
|
||||
"@emotion/utils": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.1.tgz",
|
||||
"integrity": "sha512-dEv1n+IFtlvLQ8/FsTOtBCC1aNT4B5abE8ODF5wk2tpWnjvgGNRMvHCeJGbVHjFfer4h8MH2w9c2/6eoJHclMg=="
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.2.tgz",
|
||||
@@ -59,8 +141,7 @@
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.5",
|
||||
@@ -121,6 +202,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"add-dom-event-listener": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz",
|
||||
"integrity": "sha1-j67SxBAIchzxEdodMNmVuFvkK+0=",
|
||||
"requires": {
|
||||
"object-assign": "4.1.1"
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz",
|
||||
@@ -776,6 +865,59 @@
|
||||
"babel-types": "6.26.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-emotion": {
|
||||
"version": "9.2.6",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.6.tgz",
|
||||
"integrity": "sha512-aCRXUPm2pwaUqUtpQ2Gzbn5EeOD2RyUDTQDJl5Yqwg1RLQPs3OvnB6Xt6GUrMomMISxuwFrxuWfBMajHv74UjQ==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "7.0.0-beta.51",
|
||||
"@emotion/babel-utils": "0.6.9",
|
||||
"@emotion/hash": "0.6.5",
|
||||
"@emotion/memoize": "0.6.5",
|
||||
"@emotion/stylis": "0.6.12",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-plugin-macros": "2.3.0",
|
||||
"babel-plugin-syntax-jsx": "6.18.0",
|
||||
"convert-source-map": "1.5.1",
|
||||
"find-root": "1.1.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"source-map": "0.5.7",
|
||||
"touch": "1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-core": {
|
||||
"version": "6.26.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
|
||||
"integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
|
||||
"requires": {
|
||||
"babel-code-frame": "6.26.0",
|
||||
"babel-generator": "6.26.1",
|
||||
"babel-helpers": "6.24.1",
|
||||
"babel-messages": "6.23.0",
|
||||
"babel-register": "6.26.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"babel-template": "6.26.0",
|
||||
"babel-traverse": "6.26.0",
|
||||
"babel-types": "6.26.0",
|
||||
"babylon": "6.18.0",
|
||||
"convert-source-map": "1.5.1",
|
||||
"debug": "2.6.9",
|
||||
"json5": "0.5.1",
|
||||
"lodash": "4.17.10",
|
||||
"minimatch": "3.0.4",
|
||||
"path-is-absolute": "1.0.1",
|
||||
"private": "0.1.8",
|
||||
"slash": "1.0.0",
|
||||
"source-map": "0.5.7"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-plugin-istanbul": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz",
|
||||
@@ -792,6 +934,55 @@
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz",
|
||||
"integrity": "sha1-r+3IU70/jcNUjqZx++adA8wsF2c="
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.3.0.tgz",
|
||||
"integrity": "sha512-Y9h4dQMlzUUKATfNEN+sgiwND/+PGiAkjSW+qwyATIvYMk1y39XmaLHXKI2VojplqtOWQry0y6CumvDw+qETvQ==",
|
||||
"requires": {
|
||||
"cosmiconfig": "4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"cosmiconfig": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz",
|
||||
"integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==",
|
||||
"requires": {
|
||||
"is-directory": "0.3.1",
|
||||
"js-yaml": "3.12.0",
|
||||
"parse-json": "4.0.0",
|
||||
"require-from-string": "2.0.2"
|
||||
}
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
|
||||
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
|
||||
"requires": {
|
||||
"argparse": "1.0.10",
|
||||
"esprima": "4.0.1"
|
||||
}
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
|
||||
"requires": {
|
||||
"error-ex": "1.3.1",
|
||||
"json-parse-better-errors": "1.0.2"
|
||||
}
|
||||
},
|
||||
"require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-plugin-syntax-async-functions": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
|
||||
@@ -1975,6 +2166,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz",
|
||||
@@ -2126,11 +2322,24 @@
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz",
|
||||
"integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ=="
|
||||
},
|
||||
"component-classes": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz",
|
||||
"integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=",
|
||||
"requires": {
|
||||
"component-indexof": "0.0.3"
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
||||
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
|
||||
},
|
||||
"component-indexof": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/component-indexof/-/component-indexof-0.0.3.tgz",
|
||||
"integrity": "sha1-EdCRMSI5648yyPJa6csAL/6NPCQ="
|
||||
},
|
||||
"compressible": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz",
|
||||
@@ -2300,6 +2509,20 @@
|
||||
"elliptic": "6.4.0"
|
||||
}
|
||||
},
|
||||
"create-emotion": {
|
||||
"version": "9.2.6",
|
||||
"resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.6.tgz",
|
||||
"integrity": "sha512-4g46va26lw6DPfKF7HeWY3OI/qoaNSwpvO+li8dMydZfC6f6+ZffwlYHeIyAhGR8Z8C8c0H9J1pJbQRtb9LScw==",
|
||||
"requires": {
|
||||
"@emotion/hash": "0.6.5",
|
||||
"@emotion/memoize": "0.6.5",
|
||||
"@emotion/stylis": "0.6.12",
|
||||
"@emotion/unitless": "0.6.6",
|
||||
"csstype": "2.5.6",
|
||||
"stylis": "3.5.3",
|
||||
"stylis-rule-sheet": "0.0.10"
|
||||
}
|
||||
},
|
||||
"create-error-class": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
|
||||
@@ -2394,6 +2617,15 @@
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
|
||||
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4="
|
||||
},
|
||||
"css-animation": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/css-animation/-/css-animation-1.4.1.tgz",
|
||||
"integrity": "sha1-W4gTEl3g+7uwu+G0cq6EIhRpt6g=",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"component-classes": "1.2.6"
|
||||
}
|
||||
},
|
||||
"css-color-names": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
|
||||
@@ -2615,6 +2847,11 @@
|
||||
"cssom": "0.3.2"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.6.tgz",
|
||||
"integrity": "sha512-tKPyhy0FmfYD2KQYXD5GzkvAYLYj96cMLXr648CKGd3wBe0QqoPipImjGiLze9c8leJK8J3n7ap90tpk3E6HGQ=="
|
||||
},
|
||||
"currently-unhandled": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||
@@ -2859,6 +3096,11 @@
|
||||
"esutils": "2.0.2"
|
||||
}
|
||||
},
|
||||
"dom-align": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.8.0.tgz",
|
||||
"integrity": "sha512-B85D4ef2Gj5lw0rK0KM2+D5/pH7yqNxg2mB+E8uzFaolpm7RQmsxEfjyEuNiF8UBBkffumYDeKRzTzc3LePP+w=="
|
||||
},
|
||||
"dom-converter": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz",
|
||||
@@ -3001,6 +3243,15 @@
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
|
||||
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
|
||||
},
|
||||
"emotion": {
|
||||
"version": "9.2.6",
|
||||
"resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.6.tgz",
|
||||
"integrity": "sha512-95/EiWkADklxyy1y1qlJeX5Cepa7WfpJBJSBgbLkDCBzOnP4maluvz52xcV5UaObBTfVnEBq77Go6/bgF7+xaA==",
|
||||
"requires": {
|
||||
"babel-plugin-emotion": "9.2.6",
|
||||
"create-emotion": "9.2.6"
|
||||
}
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -3953,6 +4204,11 @@
|
||||
"pkg-dir": "2.0.0"
|
||||
}
|
||||
},
|
||||
"find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
@@ -6459,6 +6715,11 @@
|
||||
"resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
|
||||
"integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w=="
|
||||
},
|
||||
"json-parse-better-errors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
||||
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
@@ -6674,6 +6935,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
|
||||
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
|
||||
},
|
||||
"lodash._getnative": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
|
||||
"integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U="
|
||||
},
|
||||
"lodash._reinterpolate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
|
||||
@@ -6722,6 +6988,11 @@
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
|
||||
},
|
||||
"lodash.isarray": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-4.0.0.tgz",
|
||||
@@ -6737,6 +7008,23 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
|
||||
"integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0="
|
||||
},
|
||||
"lodash.keys": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
|
||||
"integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
|
||||
"requires": {
|
||||
"lodash._getnative": "3.9.1",
|
||||
"lodash.isarguments": "3.1.0",
|
||||
"lodash.isarray": "3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.isarray": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
|
||||
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U="
|
||||
}
|
||||
}
|
||||
},
|
||||
"lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -6780,6 +7068,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
|
||||
},
|
||||
"lodash.uniqueid": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniqueid/-/lodash.uniqueid-4.0.1.tgz",
|
||||
"integrity": "sha1-MmjyanyI5PSxdY1nknGBTjH6WyY="
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
|
||||
@@ -6997,6 +7290,11 @@
|
||||
"mimic-fn": "1.2.0"
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz",
|
||||
"integrity": "sha512-wdpOJ4XBejprGn/xhd1i2XR8Dv1A25FJeIvR7syQhQlz9eXsv+06llcvcmBxlWVGv4C73QBsWA8kxvZozzNwiQ=="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||
@@ -9512,6 +9810,77 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"rc-align": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.4.3.tgz",
|
||||
"integrity": "sha512-h5KgyB5IXYR7iKpYFcMr54cuQ2eozPCZ11kbXPG5+6CWvmyJ+c0R/yjndVndiNk2G3MKcTMbJNdDv5DIckLAxQ==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"dom-align": "1.8.0",
|
||||
"prop-types": "15.6.2",
|
||||
"rc-util": "4.5.1"
|
||||
}
|
||||
},
|
||||
"rc-animate": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.4.4.tgz",
|
||||
"integrity": "sha512-DjJLTUQj7XKKcuS8cczN0uOLfuSmgrVXFGieP1SZc87xUUTFGh8B/KjNmEtlfvxkSrSuVfb2rrEPER4SqKUtEA==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"css-animation": "1.4.1",
|
||||
"prop-types": "15.6.2"
|
||||
}
|
||||
},
|
||||
"rc-calendar": {
|
||||
"version": "9.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rc-calendar/-/rc-calendar-9.7.0.tgz",
|
||||
"integrity": "sha512-067i3TC0H/6N6ZIhtbe9o/+F955n9nujNxn6Hoi6dJNr0LXc6F7305EI8N9k3zycx9JAliFQ1LF+MeFQwWRwpw==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"classnames": "2.2.6",
|
||||
"create-react-class": "15.6.3",
|
||||
"moment": "2.22.2",
|
||||
"prop-types": "15.6.2",
|
||||
"rc-trigger": "2.5.4",
|
||||
"rc-util": "4.5.1"
|
||||
}
|
||||
},
|
||||
"rc-time-picker": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-time-picker/-/rc-time-picker-3.3.1.tgz",
|
||||
"integrity": "sha512-iCo6Fs6Bp/HjjSvdA+nv/yJEWSe+vDyunV57uVzZkW+4QDQ+BOvZGGwJcfL407u/eP1QKmeljZN8Iu3KjdKIGg==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"classnames": "2.2.6",
|
||||
"moment": "2.22.2",
|
||||
"prop-types": "15.6.2",
|
||||
"rc-trigger": "2.5.4"
|
||||
}
|
||||
},
|
||||
"rc-trigger": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-2.5.4.tgz",
|
||||
"integrity": "sha512-clgXOdazDW2qg4vTZSAExpvOuojPNuMoamG+SxAm5Ih+rpVcrtEiDlDZWY4yUHyfEWJZBzgbrr4np/z2FK6RfA==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"classnames": "2.2.6",
|
||||
"prop-types": "15.6.2",
|
||||
"rc-align": "2.4.3",
|
||||
"rc-animate": "2.4.4",
|
||||
"rc-util": "4.5.1"
|
||||
}
|
||||
},
|
||||
"rc-util": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.5.1.tgz",
|
||||
"integrity": "sha512-PdCmHyBBodZdw6Oaikt0l+/R79IcRXpYkTrqD/Rbl4ZdoOi61t5TtEe40Q+A7rkWG5U1xjcN+h8j9H6GdtnICw==",
|
||||
"requires": {
|
||||
"add-dom-event-listener": "1.0.2",
|
||||
"babel-runtime": "6.26.0",
|
||||
"prop-types": "15.6.2",
|
||||
"shallowequal": "0.2.2"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz",
|
||||
@@ -9603,6 +9972,14 @@
|
||||
"prop-types": "15.6.2"
|
||||
}
|
||||
},
|
||||
"react-input-autosize": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",
|
||||
"integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==",
|
||||
"requires": {
|
||||
"prop-types": "15.6.2"
|
||||
}
|
||||
},
|
||||
"react-input-range": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-input-range/-/react-input-range-1.3.0.tgz",
|
||||
@@ -9618,6 +9995,14 @@
|
||||
"integrity": "sha512-rI3cGFj/obHbBz156PvErrS5xc6f1eWyTwyV4mo0vF2lGgXgS+mm7EKD5buLJq6jNgIagQescGSVG2YzgXt8Yg==",
|
||||
"dev": true
|
||||
},
|
||||
"react-json-pretty": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/react-json-pretty/-/react-json-pretty-1.7.9.tgz",
|
||||
"integrity": "sha512-5ATsy6b/+0OvaJqDEXl6afvgg09O3/L+U5kglE+lGMu/3hVcgGPM2jqMcqqr2eAANROvX9ewKQXDh5huDljFfg==",
|
||||
"requires": {
|
||||
"create-react-class": "15.6.3"
|
||||
}
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
@@ -9893,6 +10278,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-select": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-2.0.0.tgz",
|
||||
"integrity": "sha512-i2yWg8tbsY37iPimIvQ0TtIrAzxgGWQTRDZrZPQ2QVNkyHPxDartMkzf2x2Enm6wRkt9I5+pEKSIcvkwIkkiAQ==",
|
||||
"requires": {
|
||||
"classnames": "2.2.6",
|
||||
"emotion": "9.2.6",
|
||||
"memoize-one": "4.0.0",
|
||||
"prop-types": "15.6.2",
|
||||
"raf": "3.4.0",
|
||||
"react-input-autosize": "2.2.1",
|
||||
"react-transition-group": "2.4.0"
|
||||
}
|
||||
},
|
||||
"react-test-renderer": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.4.2.tgz",
|
||||
@@ -10606,6 +11005,14 @@
|
||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.0.0.tgz",
|
||||
"integrity": "sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc="
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-0.2.2.tgz",
|
||||
"integrity": "sha1-HjL9W8q2rWiKSBLLDMBO/HXHAU4=",
|
||||
"requires": {
|
||||
"lodash.keys": "3.1.2"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
@@ -11078,6 +11485,16 @@
|
||||
"schema-utils": "0.3.0"
|
||||
}
|
||||
},
|
||||
"stylis": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.3.tgz",
|
||||
"integrity": "sha512-TxU0aAscJghF9I3V9q601xcK3Uw1JbXvpsBGj/HULqexKOKlOEzzlIpLFRbKkCK990ccuxfXUqmPbIIo7Fq/cQ=="
|
||||
},
|
||||
"stylis-rule-sheet": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
|
||||
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
||||
@@ -11332,6 +11749,24 @@
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
|
||||
"integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk="
|
||||
},
|
||||
"touch": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-1.0.0.tgz",
|
||||
"integrity": "sha1-RJy+LbrlqMgDjjDXH6D/RklHxN4=",
|
||||
"requires": {
|
||||
"nopt": "1.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"nopt": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||
"integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
|
||||
"requires": {
|
||||
"abbrev": "1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.2.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"fast-deep-equal": "2.0.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"lodash.uniqueid": "4.0.1",
|
||||
"mobx": "5.0.3",
|
||||
"mobx-react": "5.2.3",
|
||||
"mobx-stored": "1.0.2",
|
||||
@@ -22,11 +23,14 @@
|
||||
"prop-types": "15.6.2",
|
||||
"qs": "6.5.2",
|
||||
"raven-js": "3.26.4",
|
||||
"rc-calendar": "9.7.0",
|
||||
"rc-time-picker": "3.3.1",
|
||||
"react": "16.4.2",
|
||||
"react-autosuggest": "9.3.4",
|
||||
"react-dom": "16.4.2",
|
||||
"react-highlighter": "0.4.2",
|
||||
"react-input-range": "1.3.0",
|
||||
"react-json-pretty": "1.7.9",
|
||||
"react-linkify": "0.2.2",
|
||||
"react-masonry-infinite": "1.2.2",
|
||||
"react-moment": "0.7.9",
|
||||
@@ -34,6 +38,7 @@
|
||||
"react-popper": "1.0.2",
|
||||
"react-resize-detector": "3.1.1",
|
||||
"react-scripts": "1.1.4",
|
||||
"react-select": "2.0.0",
|
||||
"react-transition-group": "2.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Provider } from "mobx-react";
|
||||
|
||||
import { AlertStore, DecodeLocationSearch } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { NavBar } from "Components/NavBar";
|
||||
import { Grid } from "Components/Grid";
|
||||
import { Fetcher } from "Components/Fetcher";
|
||||
@@ -21,6 +22,7 @@ class App extends Component {
|
||||
|
||||
const { defaultFilters } = this.props;
|
||||
|
||||
this.silenceFormStore = new SilenceFormStore();
|
||||
this.settingsStore = new Settings();
|
||||
|
||||
let filters;
|
||||
@@ -50,11 +52,13 @@ class App extends Component {
|
||||
<NavBar
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
silenceFormStore={this.silenceFormStore}
|
||||
/>
|
||||
<Provider alertStore={this.alertStore}>
|
||||
<Grid
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
silenceFormStore={this.silenceFormStore}
|
||||
/>
|
||||
</Provider>
|
||||
<Fetcher
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const QueryOperators = Object.freeze({
|
||||
Equal: "="
|
||||
Equal: "=",
|
||||
Regex: "=~"
|
||||
});
|
||||
|
||||
const StaticLabels = Object.freeze({
|
||||
|
||||
@@ -10,12 +10,26 @@ import onClickOutside from "react-onclickoutside";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEllipsisV } from "@fortawesome/free-solid-svg-icons/faEllipsisV";
|
||||
import { faShareSquare } from "@fortawesome/free-solid-svg-icons/faShareSquare";
|
||||
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
|
||||
|
||||
import { FormatAPIFilterQuery } from "Stores/AlertStore";
|
||||
import { QueryOperators, StaticLabels, FormatQuery } from "Common/Query";
|
||||
|
||||
const onSilenceClick = (silenceFormStore, group) => {
|
||||
silenceFormStore.data.resetProgress();
|
||||
silenceFormStore.data.fillMatchersFromGroup(group);
|
||||
silenceFormStore.toggle.show();
|
||||
};
|
||||
|
||||
const MenuContent = onClickOutside(
|
||||
({ popperPlacement, popperRef, popperStyle, group, afterClick }) => {
|
||||
({
|
||||
popperPlacement,
|
||||
popperRef,
|
||||
popperStyle,
|
||||
group,
|
||||
afterClick,
|
||||
silenceFormStore
|
||||
}) => {
|
||||
let groupFilters = Object.keys(group.labels).map(name =>
|
||||
FormatQuery(name, QueryOperators.Equal, group.labels[name])
|
||||
);
|
||||
@@ -39,6 +53,12 @@ const MenuContent = onClickOutside(
|
||||
>
|
||||
<FontAwesomeIcon icon={faShareSquare} /> Link to this group
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => onSilenceClick(silenceFormStore, group)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBellSlash} /> Silence this group
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -54,7 +74,8 @@ MenuContent.propTypes = {
|
||||
const GroupMenu = observer(
|
||||
class GroupMenu extends Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.object.isRequired
|
||||
group: PropTypes.object.isRequired,
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
collapse = observable(
|
||||
@@ -76,7 +97,7 @@ const GroupMenu = observer(
|
||||
});
|
||||
|
||||
render() {
|
||||
const { group } = this.props;
|
||||
const { group, silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
@@ -108,6 +129,7 @@ const GroupMenu = observer(
|
||||
popperRef={ref}
|
||||
popperStyle={style}
|
||||
group={group}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterClick={this.collapse.hide}
|
||||
handleClickOutside={this.collapse.hide}
|
||||
outsideClickIgnoreClass={`components-grid-alertgroup-${
|
||||
|
||||
@@ -15,16 +15,17 @@ const GroupHeader = observer(
|
||||
class GroupHeader extends Component {
|
||||
static propTypes = {
|
||||
collapseStore: PropTypes.object.isRequired,
|
||||
group: PropTypes.object.isRequired
|
||||
group: PropTypes.object.isRequired,
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collapseStore, group } = this.props;
|
||||
const { collapseStore, group, silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<h5 className="card-title text-center mb-0">
|
||||
<span className="float-left">
|
||||
<GroupMenu group={group} />
|
||||
<GroupMenu group={group} silenceFormStore={silenceFormStore} />
|
||||
</span>
|
||||
<span className="float-right">
|
||||
<FilteringCounterBadge
|
||||
|
||||
@@ -13,6 +13,7 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
|
||||
import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus";
|
||||
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { GroupHeader } from "./GroupHeader";
|
||||
import { Alert } from "./Alert";
|
||||
import { GroupFooter } from "./GroupFooter";
|
||||
@@ -37,7 +38,8 @@ const AlertGroup = observer(
|
||||
afterUpdate: PropTypes.func.isRequired,
|
||||
group: PropTypes.object.isRequired,
|
||||
showAlertmanagers: PropTypes.bool.isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -117,7 +119,12 @@ const AlertGroup = observer(
|
||||
}
|
||||
|
||||
render() {
|
||||
const { group, showAlertmanagers, afterUpdate } = this.props;
|
||||
const {
|
||||
group,
|
||||
showAlertmanagers,
|
||||
afterUpdate,
|
||||
silenceFormStore
|
||||
} = this.props;
|
||||
|
||||
let footerAlertmanagers = [];
|
||||
let showAlertmanagersInFooter = false;
|
||||
@@ -151,7 +158,11 @@ const AlertGroup = observer(
|
||||
<div className="components-grid-alertgrid-alertgroup p-1">
|
||||
<div className="card">
|
||||
<div className="card-body px-2 pt-2 pb-1">
|
||||
<GroupHeader collapseStore={this.collapse} group={group} />
|
||||
<GroupHeader
|
||||
collapseStore={this.collapse}
|
||||
group={group}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
{this.collapse.value ? null : (
|
||||
<ul className="list-group mt-1">
|
||||
{group.alerts
|
||||
|
||||
@@ -11,6 +11,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { AlertGroup } from "./AlertGroup";
|
||||
import { GridSizesConfig } from "./Constants";
|
||||
|
||||
@@ -20,7 +21,8 @@ const AlertGrid = observer(
|
||||
class AlertGrid extends Component {
|
||||
static propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
|
||||
};
|
||||
|
||||
// store reference to generated masonry component so we can call it
|
||||
@@ -72,7 +74,7 @@ const AlertGrid = observer(
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alertStore, settingsStore } = this.props;
|
||||
const { alertStore, settingsStore, silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -105,6 +107,7 @@ const AlertGrid = observer(
|
||||
}
|
||||
afterUpdate={this.masonryRepack}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
))}
|
||||
</MasonryInfiniteScroller>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { AlertGrid } from "./AlertGrid";
|
||||
import { FatalError } from "./FatalError";
|
||||
import { UpstreamError } from "./UpstreamError";
|
||||
@@ -13,11 +14,12 @@ const Grid = observer(
|
||||
class Grid extends Component {
|
||||
static propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { alertStore, settingsStore } = this.props;
|
||||
const { alertStore, settingsStore, silenceFormStore } = this.props;
|
||||
|
||||
if (alertStore.status.error) {
|
||||
return <FatalError message={alertStore.status.error} />;
|
||||
@@ -35,7 +37,11 @@ const Grid = observer(
|
||||
/>
|
||||
))}
|
||||
|
||||
<AlertGrid alertStore={alertStore} settingsStore={settingsStore} />
|
||||
<AlertGrid
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.modal-header .close {
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -12,11 +12,11 @@ import { Settings } from "Stores/Settings";
|
||||
import { Configuration } from "./Configuration";
|
||||
import { Help } from "./Help";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const Tab = ({ title, active, onClick }) => (
|
||||
<a
|
||||
className={`nav-item nav-link cursor-pointer ${active ? "active" : ""}`}
|
||||
className={`nav-item nav-link cursor-pointer ${
|
||||
active ? "active" : "text-primary"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{title}
|
||||
@@ -76,17 +76,11 @@ const MainModal = observer(
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ul className="navbar-nav float-right">
|
||||
<li className="nav-item dropdown">
|
||||
<a
|
||||
className="nav-link mx-1 cursor-pointer"
|
||||
data-toggle="dropdown"
|
||||
onClick={this.toggle.toggle}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link cursor-pointer" onClick={this.toggle.toggle}>
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</a>
|
||||
</li>
|
||||
{this.toggle.show ? (
|
||||
<div
|
||||
className="modal d-block bg-primary-transparent-80"
|
||||
@@ -106,14 +100,14 @@ const MainModal = observer(
|
||||
active={this.tab.current === TabNames.Help}
|
||||
onClick={() => this.tab.setTab(TabNames.Help)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
onClick={this.toggle.hide}
|
||||
>
|
||||
<span>×</span>
|
||||
</button>
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
onClick={this.toggle.hide}
|
||||
>
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{this.tab.current === TabNames.Help ? <Help /> : null}
|
||||
|
||||
102
ui/src/Components/MultiSelect/index.js
Normal file
102
ui/src/Components/MultiSelect/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
|
||||
import CreatableSelect from "react-select/lib/Creatable";
|
||||
|
||||
const ReactSelectStyles = {
|
||||
control: (base, state) =>
|
||||
state.isFocused
|
||||
? {
|
||||
...base,
|
||||
outline: "0",
|
||||
outlineOffset: "-2px",
|
||||
boxShadow: "0 0 0 0.2rem rgba(69, 90, 100, 0.25)",
|
||||
borderRadius: "0.25rem",
|
||||
borderColor: "#819ba8",
|
||||
"&:hover": {
|
||||
borderColor: "#819ba8"
|
||||
}
|
||||
}
|
||||
: {
|
||||
...base,
|
||||
borderRadius: "0.25rem",
|
||||
borderColor: "#ced4da",
|
||||
"&:hover": { borderColor: "#ced4da" }
|
||||
},
|
||||
valueContainer: (base, state) =>
|
||||
state.isMulti
|
||||
? {
|
||||
...base,
|
||||
borderRadius: "0.25rem",
|
||||
backgroundColor: "#fff",
|
||||
paddingLeft: "4px",
|
||||
paddingRight: "4px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden"
|
||||
}
|
||||
: {
|
||||
...base,
|
||||
borderRadius: "0.25rem",
|
||||
backgroundColor: "#fff"
|
||||
},
|
||||
valueLabel: (base, state) => ({
|
||||
...base,
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
}),
|
||||
multiValue: (base, state) => ({
|
||||
...base,
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#95a5a6",
|
||||
"&:hover": {
|
||||
backgroundColor: "#95a5a6"
|
||||
}
|
||||
}),
|
||||
multiValueLabel: (base, state) => ({
|
||||
...base,
|
||||
color: "#fff",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
"&:hover": {
|
||||
color: "#fff"
|
||||
}
|
||||
}),
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
cursor: "pointer",
|
||||
color: "#fff",
|
||||
backgroundColor: "inherit",
|
||||
opacity: "0.4",
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
backgroundColor: "inherit",
|
||||
opacity: "0.75"
|
||||
}
|
||||
}),
|
||||
indicatorsContainer: (base, state) => ({
|
||||
...base,
|
||||
backgroundColor: "#fff",
|
||||
borderTopRightRadius: "0.25rem",
|
||||
borderBottomRightRadius: "0.25rem"
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
color: state.isSelected ? "#95a5a6" : "inherit",
|
||||
backgroundColor: "inherit",
|
||||
"&:hover": { color: "#fff", backgroundColor: "#455a64", cursor: "pointer" }
|
||||
})
|
||||
};
|
||||
|
||||
class MultiSelect extends CreatableSelect {
|
||||
renderProps = () => ({});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CreatableSelect styles={ReactSelectStyles} {...this.renderProps()} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { MultiSelect, ReactSelectStyles };
|
||||
@@ -1,5 +1,38 @@
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-menu.components-navbar-historymenu > .dropdown-item {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dropdown-menu.components-navbar-historymenu > .container {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.components-navbar-historymenu-labels {
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1px) and (max-width: 599px) {
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 600px) and (max-width: 999px) {
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1000px) {
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.component-history-button {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.component-history-button:last-child {
|
||||
margin-right: 0;
|
||||
|
||||
@@ -12,30 +12,6 @@ input.components-filterinput-wrapper {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown-menu.components-navbar-historymenu > .dropdown-item {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.components-navbar-historymenu-labels {
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1px) and (max-width: 599px) {
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 600px) and (max-width: 999px) {
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1000px) {
|
||||
.dropdown-menu.components-navbar-historymenu {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
input.components-filterinput-wrapper {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import ReactResizeDetector from "react-resize-detector";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { FetchIndicator } from "./FetchIndicator";
|
||||
import { FilterInput } from "./FilterInput";
|
||||
import { MainModal } from "Components/MainModal";
|
||||
import { SilenceModal } from "Components/SilenceModal";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -21,11 +23,21 @@ const NavBar = observer(
|
||||
class NavBar extends Component {
|
||||
static propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { alertStore, settingsStore } = this.props;
|
||||
const { alertStore, settingsStore, silenceFormStore } = this.props;
|
||||
|
||||
// if we have at least 2 filters then it's likely that filter input will
|
||||
// use 2 lines, so set right side icons on small screeens to column mode
|
||||
// for more compact layout
|
||||
const flexClass =
|
||||
alertStore.filters.values.length >= 2
|
||||
? "flex-column flex-sm-column flex-md-row flex-lg-row flex-xl-row"
|
||||
: "flex-row";
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<nav className="navbar fixed-top navbar-expand navbar-dark p-1 bg-primary-transparent d-inline-block">
|
||||
@@ -34,7 +46,16 @@ const NavBar = observer(
|
||||
{alertStore.info.totalAlerts}
|
||||
<FetchIndicator status={alertStore.status.value.toString()} />
|
||||
</span>
|
||||
<MainModal alertStore={alertStore} settingsStore={settingsStore} />
|
||||
<ul className={`navbar-nav float-right d-flex ${flexClass}`}>
|
||||
<SilenceModal
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
<MainModal
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</ul>
|
||||
<FilterInput
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
|
||||
87
ui/src/Components/SilenceModal/AlertManagerInput.js
Normal file
87
ui/src/Components/SilenceModal/AlertManagerInput.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import ReactSelect from "react-select";
|
||||
|
||||
import { MultiSelect, ReactSelectStyles } from "Components/MultiSelect";
|
||||
|
||||
const AlertmanagerInstancesToOptions = instances =>
|
||||
instances.map(i => ({
|
||||
label: i.name,
|
||||
value: i.uri
|
||||
}));
|
||||
|
||||
const AlertManagerInput = observer(
|
||||
class AlertManagerInput extends MultiSelect {
|
||||
static propTypes = {
|
||||
alertStore: PropTypes.object.isRequired,
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { alertStore, silenceFormStore } = props;
|
||||
|
||||
if (silenceFormStore.data.alertmanagers.length === 0) {
|
||||
silenceFormStore.data.alertmanagers = AlertmanagerInstancesToOptions(
|
||||
alertStore.data.upstreams.instances
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onChange = action((newValue, actionMeta) => {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
silenceFormStore.data.alertmanagers = newValue;
|
||||
});
|
||||
|
||||
componentDidUpdate() {
|
||||
const { alertStore, silenceFormStore } = this.props;
|
||||
|
||||
// get the list of last known alertmanagers
|
||||
const currentAlertmanagers = AlertmanagerInstancesToOptions(
|
||||
alertStore.data.upstreams.instances
|
||||
);
|
||||
|
||||
// now iterate what's set as silence form values and reset it if any
|
||||
// mismatch is detected (uri changed for example)
|
||||
for (const silenceAM of silenceFormStore.data.alertmanagers) {
|
||||
for (const currentAM of currentAlertmanagers) {
|
||||
if (
|
||||
silenceAM.label === currentAM.label &&
|
||||
silenceAM.value !== currentAM.value
|
||||
) {
|
||||
silenceFormStore.data.alertmanagers = AlertmanagerInstancesToOptions(
|
||||
alertStore.data.upstreams.instances
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alertStore, silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
styles={ReactSelectStyles}
|
||||
instanceId="silence-input-alertmanagers"
|
||||
defaultValue={silenceFormStore.data.alertmanagers}
|
||||
options={AlertmanagerInstancesToOptions(
|
||||
alertStore.data.upstreams.instances
|
||||
)}
|
||||
placeholder="Alertmanager"
|
||||
isMulti
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { AlertManagerInput };
|
||||
90
ui/src/Components/SilenceModal/LabelNameInput.js
Normal file
90
ui/src/Components/SilenceModal/LabelNameInput.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { MultiSelect } from "Components/MultiSelect";
|
||||
import { FormatUnseeBackendURI } from "Stores/AlertStore";
|
||||
|
||||
const LabelNameInput = observer(
|
||||
class LabelNameInput extends MultiSelect {
|
||||
static propTypes = {
|
||||
matcher: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
populateNameSuggestions = action(() => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
fetch(FormatUnseeBackendURI(`labelNames.json`))
|
||||
.then(
|
||||
result => result.json(),
|
||||
err => {
|
||||
return [];
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
matcher.suggestions.names = result.map(value => ({
|
||||
label: value,
|
||||
value: value
|
||||
}));
|
||||
})
|
||||
.catch(err => console.error(err.message));
|
||||
});
|
||||
|
||||
populateValueSuggestions = action(() => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
fetch(FormatUnseeBackendURI(`labelValues.json?name=${matcher.name}`))
|
||||
.then(
|
||||
result => result.json(),
|
||||
err => {
|
||||
return [];
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
matcher.suggestions.values = result.map(value => ({
|
||||
label: value,
|
||||
value: value
|
||||
}));
|
||||
})
|
||||
.catch(err => console.error(err.message));
|
||||
});
|
||||
|
||||
onChange = action((newValue, actionMeta) => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
matcher.name = newValue.value;
|
||||
|
||||
if (newValue) {
|
||||
this.populateValueSuggestions();
|
||||
}
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
const { matcher } = this.props;
|
||||
|
||||
this.populateNameSuggestions();
|
||||
if (matcher.name) {
|
||||
this.populateValueSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
renderProps = () => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
const value = matcher.name
|
||||
? { label: matcher.name, value: matcher.name }
|
||||
: null;
|
||||
|
||||
return {
|
||||
instanceId: `silence-input-label-name-${matcher.id}`,
|
||||
defaultValue: value,
|
||||
options: matcher.suggestions.names,
|
||||
placeholder: "Label name",
|
||||
onChange: this.onChange
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export { LabelNameInput };
|
||||
42
ui/src/Components/SilenceModal/LabelValueInput.js
Normal file
42
ui/src/Components/SilenceModal/LabelValueInput.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { MultiSelect } from "Components/MultiSelect";
|
||||
|
||||
const LabelValueInput = observer(
|
||||
class LabelValueInput extends MultiSelect {
|
||||
static propTypes = {
|
||||
matcher: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
onChange = action((newValue, actionMeta) => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
matcher.values = newValue;
|
||||
|
||||
// force regex if we have multiple values
|
||||
if (newValue.length > 1 && matcher.isRegex === false) {
|
||||
matcher.isRegex = true;
|
||||
} else if (newValue.length === 1 && matcher.isRegex === true) {
|
||||
matcher.isRegex = false;
|
||||
}
|
||||
});
|
||||
|
||||
renderProps = () => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
return {
|
||||
instanceId: `silence-input-label-value-${matcher.id}`,
|
||||
defaultValue: matcher.values,
|
||||
options: matcher.suggestions.values,
|
||||
placeholder: "Label value",
|
||||
isMulti: true,
|
||||
onChange: this.onChange
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export { LabelValueInput };
|
||||
163
ui/src/Components/SilenceModal/SilenceForm.js
Normal file
163
ui/src/Components/SilenceModal/SilenceForm.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { action, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
|
||||
import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
|
||||
import { faCommentDots } from "@fortawesome/free-solid-svg-icons/faCommentDots";
|
||||
import { faSave } from "@fortawesome/free-regular-svg-icons/faSave";
|
||||
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||
|
||||
import { AlertManagerInput } from "./AlertManagerInput";
|
||||
import { SilenceMatch } from "./SilenceMatch";
|
||||
import { SilenceStartEnd } from "./SilenceStartEnd";
|
||||
import { SilencePreview } from "./SilencePreview";
|
||||
|
||||
const IconInput = ({ icon, placeholder, value, onChange }) => (
|
||||
<div className="input-group mb-3">
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text">
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
required
|
||||
autoComplete="on"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
IconInput.propTypes = {
|
||||
icon: PropTypes.object.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const SilenceForm = observer(
|
||||
class SilenceForm extends Component {
|
||||
static propTypes = {
|
||||
alertStore: PropTypes.object.isRequired,
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
// store preview visibility state here, by default preview is collapsed
|
||||
// and user needs to expand it
|
||||
previewCollapse = observable(
|
||||
{
|
||||
hidden: true,
|
||||
toggle() {
|
||||
this.hidden = !this.hidden;
|
||||
}
|
||||
},
|
||||
{ toggle: action.bound },
|
||||
{ name: "Silence preview collpase toggle" }
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
if (silenceFormStore.data.matchers.length === 0) {
|
||||
silenceFormStore.data.addEmptyMatcher();
|
||||
}
|
||||
}
|
||||
|
||||
addMore = action(event => {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
silenceFormStore.data.addEmptyMatcher();
|
||||
});
|
||||
|
||||
onAuthorChange = action(event => {
|
||||
const { silenceFormStore } = this.props;
|
||||
silenceFormStore.data.author = event.target.value;
|
||||
});
|
||||
|
||||
onCommentChange = action(event => {
|
||||
const { silenceFormStore } = this.props;
|
||||
silenceFormStore.data.comment = event.target.value;
|
||||
});
|
||||
|
||||
handleSubmit = action(event => {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
silenceFormStore.data.inProgress = true;
|
||||
});
|
||||
|
||||
render() {
|
||||
const { alertStore, silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<AlertManagerInput
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</div>
|
||||
{silenceFormStore.data.matchers.map(matcher => (
|
||||
<SilenceMatch
|
||||
key={matcher.id}
|
||||
matcher={matcher}
|
||||
onDelete={() => {
|
||||
silenceFormStore.data.deleteMatcher(matcher.id);
|
||||
}}
|
||||
showDelete={silenceFormStore.data.matchers.length > 1}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary mb-3"
|
||||
onClick={this.addMore}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</button>
|
||||
<SilenceStartEnd silenceFormStore={silenceFormStore} />
|
||||
<IconInput
|
||||
placeholder="Author"
|
||||
icon={faUser}
|
||||
value={silenceFormStore.data.author}
|
||||
onChange={this.onAuthorChange}
|
||||
/>
|
||||
<IconInput
|
||||
placeholder="Comment"
|
||||
icon={faCommentDots}
|
||||
value={silenceFormStore.data.comment}
|
||||
onChange={this.onCommentChange}
|
||||
/>
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<a
|
||||
className="btn px-0 cursor-pointer text-muted"
|
||||
onClick={this.previewCollapse.toggle}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={this.previewCollapse.hidden ? faChevronUp : faChevronDown}
|
||||
/>
|
||||
</a>
|
||||
<button type="submit" className="btn btn-outline-primary">
|
||||
<FontAwesomeIcon icon={faSave} className="mr-1" />
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
{this.previewCollapse.hidden ? null : (
|
||||
<SilencePreview silenceFormStore={silenceFormStore} />
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { SilenceForm };
|
||||
81
ui/src/Components/SilenceModal/SilenceMatch.js
Normal file
81
ui/src/Components/SilenceModal/SilenceMatch.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
|
||||
import { LabelNameInput } from "./LabelNameInput";
|
||||
import { LabelValueInput } from "./LabelValueInput";
|
||||
|
||||
const SilenceMatch = observer(
|
||||
class SilenceMatch extends Component {
|
||||
static propTypes = {
|
||||
matcher: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.array.isRequired,
|
||||
isRegex: PropTypes.bool.isRequired
|
||||
}),
|
||||
showDelete: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onIsRegexChange = action(event => {
|
||||
const { matcher } = this.props;
|
||||
console.info(matcher.values);
|
||||
|
||||
// only allow to change value if we don't have multiple values
|
||||
if (matcher.values.length <= 1) {
|
||||
matcher.isRegex = event.target.checked;
|
||||
}
|
||||
});
|
||||
|
||||
render() {
|
||||
const { matcher, showDelete, onDelete } = this.props;
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-fill flex-lg-row flex-column mb-3">
|
||||
<div className="flex-shrink-0 flex-grow-0 flex-basis-25 pr-lg-2 pb-2 pb-lg-0">
|
||||
<LabelNameInput matcher={matcher} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex-grow-0 flex-basis-50 pr-lg-2 pb-2 pb-lg-0">
|
||||
<LabelValueInput matcher={matcher} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex-grow-1 flex-basis-auto form-check form-check-inline d-flex justify-content-between m-0">
|
||||
<span>
|
||||
<input
|
||||
id={`isRegex-${matcher.id}`}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
value=""
|
||||
checked={matcher.isRegex}
|
||||
onChange={this.onIsRegexChange}
|
||||
disabled={matcher.values.length > 1}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label cursor-pointer mr-3"
|
||||
htmlFor={`isRegex-${matcher.id}`}
|
||||
>
|
||||
Regex
|
||||
</label>
|
||||
</span>
|
||||
{showDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { SilenceMatch };
|
||||
27
ui/src/Components/SilenceModal/SilencePreview.js
Normal file
27
ui/src/Components/SilenceModal/SilencePreview.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import JSONPretty from "react-json-pretty";
|
||||
import "react-json-pretty/src/JSONPretty.monikai.css";
|
||||
|
||||
const SilencePreview = observer(
|
||||
class SilencePreview extends Component {
|
||||
static propTypes = {
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<JSONPretty json={silenceFormStore.data.toAlertmanagerPayload} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { SilencePreview };
|
||||
18
ui/src/Components/SilenceModal/SilenceStartEnd.css
Normal file
18
ui/src/Components/SilenceModal/SilenceStartEnd.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.rc-calendar {
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.rc-calendar-picker {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.rc-calendar-selected-day > .rc-calendar-date {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
background-color: #455a64;
|
||||
}
|
||||
|
||||
.rc-calendar-today .rc-calendar-date {
|
||||
border-color: #7b8a8b;
|
||||
}
|
||||
129
ui/src/Components/SilenceModal/SilenceStartEnd.js
Normal file
129
ui/src/Components/SilenceModal/SilenceStartEnd.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { observable, action, toJS } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import Picker from "rc-calendar/lib/Picker";
|
||||
import RangeCalendar from "rc-calendar/lib/RangeCalendar";
|
||||
import "rc-calendar/assets/index.css";
|
||||
|
||||
import TimePickerPanel from "rc-time-picker/lib/Panel";
|
||||
import "rc-time-picker/assets/index.css";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCalendarAlt } from "@fortawesome/free-solid-svg-icons/faCalendarAlt";
|
||||
|
||||
import "./SilenceStartEnd.css";
|
||||
|
||||
function disabledDate(current) {
|
||||
if (!current) return false;
|
||||
|
||||
const date = moment();
|
||||
date.hour(0);
|
||||
date.minute(0);
|
||||
date.second(0);
|
||||
return current.isBefore(date);
|
||||
}
|
||||
|
||||
function isValidRange(v) {
|
||||
return v && v[0] && v[1];
|
||||
}
|
||||
|
||||
const format = "YYYY-MM-DD HH:mm";
|
||||
function formatTimestamp(v) {
|
||||
return v ? v.format(format) : "";
|
||||
}
|
||||
|
||||
const timePickerElement = (
|
||||
<TimePickerPanel
|
||||
defaultValue={[
|
||||
moment().second(0),
|
||||
moment()
|
||||
.second(0)
|
||||
.add(1, "minute")
|
||||
]}
|
||||
showSecond={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const SilenceStartEnd = observer(
|
||||
class SilenceStartEnd extends Component {
|
||||
static propTypes = {
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
data = observable({
|
||||
hoverValue: []
|
||||
});
|
||||
|
||||
onChange = action(value => {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
if (value && value[0]) {
|
||||
silenceFormStore.data.startsAt = value[0];
|
||||
}
|
||||
if (value && value[1]) {
|
||||
silenceFormStore.data.endsAt = value[1];
|
||||
}
|
||||
});
|
||||
|
||||
onHoverChange = action(value => {
|
||||
this.data.hoverValue = value;
|
||||
});
|
||||
|
||||
render() {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
const now = moment().second(0);
|
||||
|
||||
const calendar = (
|
||||
<RangeCalendar
|
||||
hoverValue={toJS(this.data.hoverValue)}
|
||||
onHoverChange={this.onHoverChange}
|
||||
showWeekNumber={false}
|
||||
dateInputPlaceholder={["start", "end"]}
|
||||
defaultValue={[now, now.clone().add(1, "hour")]}
|
||||
timePicker={timePickerElement}
|
||||
disabledDate={disabledDate}
|
||||
format={format}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Picker
|
||||
value={[silenceFormStore.data.startsAt, silenceFormStore.data.endsAt]}
|
||||
onChange={this.onChange}
|
||||
calendar={calendar}
|
||||
>
|
||||
{({ value }) => {
|
||||
return (
|
||||
<div className="input-group mb-3">
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text">
|
||||
<FontAwesomeIcon icon={faCalendarAlt} />
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
placeholder="Silence start and end"
|
||||
className="form-control bg-white"
|
||||
value={
|
||||
(isValidRange(value) &&
|
||||
`${formatTimestamp(value[0])} - ${formatTimestamp(
|
||||
value[1]
|
||||
)}`) ||
|
||||
""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Picker>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { SilenceStartEnd };
|
||||
44
ui/src/Components/SilenceModal/SilenceSubmitController.js
Normal file
44
ui/src/Components/SilenceModal/SilenceSubmitController.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUndoAlt } from "@fortawesome/free-solid-svg-icons/faUndoAlt";
|
||||
|
||||
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
|
||||
|
||||
class SilenceSubmitController extends Component {
|
||||
static propTypes = {
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
{silenceFormStore.data.alertmanagers.map(am => (
|
||||
<SilenceSubmitProgress
|
||||
key={am.label}
|
||||
name={am.label}
|
||||
uri={am.value}
|
||||
payload={silenceFormStore.data.toAlertmanagerPayload}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="d-flex flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-success"
|
||||
onClick={silenceFormStore.data.resetProgress}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUndoAlt} className="pr-1" />
|
||||
Reset form
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { SilenceSubmitController };
|
||||
118
ui/src/Components/SilenceModal/SilenceSubmitProgress.js
Normal file
118
ui/src/Components/SilenceModal/SilenceSubmitProgress.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { action, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
|
||||
const SubmitState = Object.freeze({
|
||||
InProgress: "InProgress",
|
||||
Done: "Done",
|
||||
Failed: "Failed"
|
||||
});
|
||||
|
||||
const SubmitIcon = observer(({ stateValue }) => {
|
||||
if (stateValue === SubmitState.Done) {
|
||||
return <FontAwesomeIcon icon={faCheckCircle} className="text-success" />;
|
||||
}
|
||||
if (stateValue === SubmitState.Failed) {
|
||||
return (
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="text-danger" />
|
||||
);
|
||||
}
|
||||
return <FontAwesomeIcon icon={faCircleNotch} spin />;
|
||||
});
|
||||
|
||||
const SilenceLink = ({ uri, silenceId }) => (
|
||||
<a
|
||||
href={`${uri}/#/silences/${silenceId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{silenceId}
|
||||
</a>
|
||||
);
|
||||
SilenceLink.propTypes = {
|
||||
uri: PropTypes.string.isRequired,
|
||||
silenceId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const SilenceSubmitProgress = observer(
|
||||
class SilenceSubmitProgress extends Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
uri: PropTypes.string.isRequired,
|
||||
payload: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
submitState = observable(
|
||||
{
|
||||
value: SubmitState.InProgress,
|
||||
result: null,
|
||||
markDone(result) {
|
||||
this.result = result;
|
||||
this.value = SubmitState.Done;
|
||||
},
|
||||
markFailed(result) {
|
||||
this.result = result;
|
||||
this.value = SubmitState.Failed;
|
||||
}
|
||||
},
|
||||
{ markDone: action.bound, markFailed: action.bound }
|
||||
);
|
||||
|
||||
handleAlertmanagerRequest = () => {
|
||||
const { uri, payload } = this.props;
|
||||
|
||||
fetch(`${uri}/api/v1/silences`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(result => result.json())
|
||||
.then(result => this.parseAlertmanagerResponse(result))
|
||||
.catch(err => this.submitState.markFailed(err.message));
|
||||
};
|
||||
|
||||
parseAlertmanagerResponse = response => {
|
||||
const { uri } = this.props;
|
||||
|
||||
if (response.status === "success") {
|
||||
const link = (
|
||||
<SilenceLink uri={uri} silenceId={response.data.silenceId} />
|
||||
);
|
||||
this.submitState.markDone(link);
|
||||
} else if (response.status === "error") {
|
||||
this.submitState.markFailed(response.error);
|
||||
} else {
|
||||
this.submitState.markFailed(JSON.strigify(response));
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.handleAlertmanagerRequest();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name } = this.props;
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className="p-2 flex-fill">
|
||||
<SubmitIcon stateValue={this.submitState.value} />
|
||||
</div>
|
||||
<div className="p-2 flex-fill">{name}</div>
|
||||
<div className="p-2 flex-fill">{this.submitState.result}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { SilenceSubmitProgress };
|
||||
19
ui/src/Components/SilenceModal/index.css
Normal file
19
ui/src/Components/SilenceModal/index.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.flex-basis-auto {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.flex-basis-25 {
|
||||
flex-basis: 25%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.flex-basis-50 {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 992px) {
|
||||
.flex-basis-50 {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
86
ui/src/Components/SilenceModal/index.js
Normal file
86
ui/src/Components/SilenceModal/index.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
|
||||
|
||||
import { SilenceForm } from "./SilenceForm";
|
||||
import { SilenceSubmitController } from "./SilenceSubmitController";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const SilenceModal = observer(
|
||||
class SilenceModal extends Component {
|
||||
static propTypes = {
|
||||
alertStore: PropTypes.object.isRequired,
|
||||
silenceFormStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
const { silenceFormStore } = this.props;
|
||||
|
||||
document.body.classList.toggle(
|
||||
"modal-open",
|
||||
silenceFormStore.toggle.visible
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alertStore, silenceFormStore } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className="nav-link cursor-pointer"
|
||||
onClick={silenceFormStore.toggle.toggle}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBellSlash} />
|
||||
</a>
|
||||
</li>
|
||||
{silenceFormStore.toggle.visible ? (
|
||||
<div
|
||||
className="modal d-block bg-primary-transparent-80"
|
||||
role="dialog"
|
||||
>
|
||||
<div className="modal-dialog modal-lg" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Add new silence</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
onClick={silenceFormStore.toggle.hide}
|
||||
>
|
||||
<span className="align-middle">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{silenceFormStore.data.inProgress ? (
|
||||
<SilenceSubmitController
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
) : (
|
||||
<SilenceForm
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { SilenceModal };
|
||||
148
ui/src/Stores/SilenceFormStore.js
Normal file
148
ui/src/Stores/SilenceFormStore.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { observable, action, computed } from "mobx";
|
||||
|
||||
import uniqueId from "lodash.uniqueid";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
const NewEmptyMatcher = id => {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
name: "",
|
||||
values: [],
|
||||
suggestions: {
|
||||
names: [],
|
||||
values: []
|
||||
},
|
||||
isRegex: false
|
||||
};
|
||||
};
|
||||
|
||||
const ValueToObject = value => ({ label: value, value: value });
|
||||
|
||||
class SilenceFormStore {
|
||||
// this is used to store modal visibility toggle
|
||||
toggle = observable(
|
||||
{
|
||||
visible: false,
|
||||
toggle() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
},
|
||||
show() {
|
||||
this.visible = true;
|
||||
}
|
||||
},
|
||||
{ toggle: action.bound, hide: action.bound, show: action.bound }
|
||||
);
|
||||
|
||||
// form data is stored here, it's global (rather than attached to the form)
|
||||
// so it can be manipulated from other parts of the code
|
||||
// example: when user clicks a silence button on alert we should populate
|
||||
// this form from that alert so user can easily silence that alert
|
||||
data = observable(
|
||||
{
|
||||
inProgress: false,
|
||||
alertmanagers: [],
|
||||
matchers: [],
|
||||
startsAt: moment(),
|
||||
endsAt: moment().add(1, "hour"),
|
||||
comment: "",
|
||||
author: "",
|
||||
resetProgress() {
|
||||
this.inProgress = false;
|
||||
},
|
||||
// append a new empty matcher to the list
|
||||
addEmptyMatcher() {
|
||||
let m = NewEmptyMatcher();
|
||||
this.matchers.push(m);
|
||||
},
|
||||
deleteMatcher(id) {
|
||||
// only delete matchers if we have more than 1
|
||||
if (this.matchers.length > 1) {
|
||||
this.matchers = this.matchers.filter(m => m.id !== id);
|
||||
}
|
||||
},
|
||||
fillMatchersFromGroup(group) {
|
||||
let matchers = [];
|
||||
|
||||
// add matchers for all shared labels in this group
|
||||
for (const [key, value] of Object.entries(
|
||||
Object.assign({}, group.labels, group.shared.labels)
|
||||
)) {
|
||||
matchers.push({
|
||||
id: uniqueId(),
|
||||
name: key,
|
||||
values: [ValueToObject(value)],
|
||||
suggestions: {
|
||||
names: [],
|
||||
values: []
|
||||
},
|
||||
isRegex: false
|
||||
});
|
||||
}
|
||||
|
||||
// add matchers for all unique labels in this group
|
||||
let labels = {};
|
||||
for (const alert of group.alerts) {
|
||||
for (const [key, value] of Object.entries(alert.labels)) {
|
||||
if (!labels[key]) {
|
||||
labels[key] = new Set();
|
||||
}
|
||||
labels[key].add(value);
|
||||
}
|
||||
}
|
||||
for (const [key, values] of Object.entries(labels)) {
|
||||
matchers.push({
|
||||
id: uniqueId(),
|
||||
name: key,
|
||||
values: [...values].sort().map(value => ValueToObject(value)),
|
||||
suggestions: {
|
||||
names: [],
|
||||
values: []
|
||||
},
|
||||
isRegex: values.size > 1
|
||||
});
|
||||
}
|
||||
|
||||
this.matchers = matchers;
|
||||
},
|
||||
get toAlertmanagerPayload() {
|
||||
const payload = {
|
||||
matchers: this.matchers.map(m => ({
|
||||
name: m.name,
|
||||
value:
|
||||
m.values.length > 1
|
||||
? `(${m.values.map(v => v.value).join("|")})`
|
||||
: m.values.length === 1
|
||||
? m.values[0].value
|
||||
: "",
|
||||
isRegex: m.isRegex
|
||||
})),
|
||||
startsAt: this.startsAt
|
||||
.second(0)
|
||||
.millisecond(0)
|
||||
.toISOString(),
|
||||
endsAt: this.endsAt
|
||||
.second(59)
|
||||
.millisecond(0)
|
||||
.toISOString(),
|
||||
createdBy: this.author,
|
||||
comment: this.comment
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
},
|
||||
{
|
||||
resetProgress: action.bound,
|
||||
addEmptyMatcher: action.bound,
|
||||
deleteMatcher: action.bound,
|
||||
fillMatchersFromGroup: action.bound,
|
||||
toAlertmanagerPayload: computed
|
||||
},
|
||||
{ name: "Silence form store" }
|
||||
);
|
||||
}
|
||||
|
||||
export { SilenceFormStore };
|
||||
Reference in New Issue
Block a user