feat(ui): allow silencing alerts

This adds a modal dialog for silencing alerts
This commit is contained in:
Łukasz Mierzwa
2018-07-31 16:29:52 +02:00
parent b3709927f7
commit 18344959d7
28 changed files with 1731 additions and 68 deletions

439
ui/package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

View File

@@ -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

View File

@@ -1,5 +1,6 @@
const QueryOperators = Object.freeze({
Equal: "="
Equal: "=",
Regex: "=~"
});
const StaticLabels = Object.freeze({

View File

@@ -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-${

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -1,3 +0,0 @@
.modal-header .close {
line-height: 1.6;
}

View File

@@ -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>&times;</span>
</button>
</nav>
<button
type="button"
className="close"
onClick={this.toggle.hide}
>
<span>&times;</span>
</button>
</div>
<div className="modal-body">
{this.tab.current === TabNames.Help ? <Help /> : null}

View 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 };

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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}

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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;
}

View 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 };

View 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 };

View 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 };

View 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%;
}
}

View 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">&times;</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 };

View 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 };