SIA Chrome Extension Push Notifications (#78)

* SIA Chrome Extension Push Notifications
This commit is contained in:
Alberto Rodríguez 2018-02-22 17:08:34 -08:00 коммит произвёл GitHub
Родитель f48ea7fb87
Коммит 1b989f6353
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
47 изменённых файлов: 1098 добавлений и 180 удалений

Просмотреть файл

@ -12,6 +12,7 @@
"transform-object-rest-spread",
"syntax-class-properties",
"transform-class-properties",
"transform-async-to-generator",
"react-hot-loader/babel",
["module-resolver", {
"root": ["./src"],

2
.gitignore поставляемый
Просмотреть файл

@ -2,6 +2,8 @@
logs
*.log
dist/*
# Runtime data
pids
*.pid

2
app.js
Просмотреть файл

@ -18,6 +18,8 @@ if (process.env.NODE_ENV !== 'dist') {
historyApiFallback: true
}))
app.use('/favicon.ico', express.static(path.join(__dirname, 'src/static/favicon.ico')))
app.use(webpackHotMiddleware(compiler, {
log: console.log,
path: '/__webpack_hmr',

Просмотреть файл

@ -1,27 +1,32 @@
const path = require('path')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const env = process.env.REACT_WEBPACK_ENV
let constants
try {
constants = require(`./${env}.const`)
} catch (ex) { // TODO: Catch only file not found.
console.log(`${env}.const not found.`, ex)
constants = require('./defaultConstants')
} catch (ex) {
if (ex.code && ex.code === 'MODULE_NOT_FOUND') {
console.log(`${env}.const not found. Falling back to defaultConstants.`)
constants = require('./defaultConstants')
} else {
throw new Error(`Unknown error while loading ${env}.const. ${ex}`)
}
}
const srcPath = path.join(__dirname, '/../src')
const siaRoot = path.join(__dirname, '..')
const publicPath = '/assets/'
const config = {
entry: {
app: ['babel-polyfill'],
appInsights: path.join(srcPath, 'appInsights')
appInsights: path.join(siaRoot, 'src/appInsights')
},
devtool: 'eval',
output: {
path: path.join(__dirname, '/../dist/assets'),
path: path.join(siaRoot, 'dist/assets'),
filename: '[name].js',
publicPath: publicPath
},
@ -33,15 +38,7 @@ const config = {
noInfo: false
},
resolve: {
extensions: ['.js'],
alias: {
actions: `${srcPath}/actions/`,
sources: `${srcPath}/sources/`,
stores: `${srcPath}/stores/`,
styles: `${srcPath}/styles/`,
config: `${srcPath}/config/`,
'react/lib/ReactMount': 'react-dom/lib/ReactMount'
}
extensions: ['.js']
},
module: {
rules: [
@ -85,7 +82,15 @@ const config = {
'process.env.NODE_ENV': `"${env}"`,
'constants': JSON.stringify(constants)
}),
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /de/)
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /de/),
new CopyWebpackPlugin([
{ from: path.join(siaRoot, 'src/extensionHooks/manifest.json'), to: path.join(siaRoot, 'dist') },
{ from: path.join(siaRoot, 'src/static'), to: path.join(siaRoot, 'dist/static') },
{ from: path.join(siaRoot, 'src/static/favicon.ico'), to: path.join(siaRoot, 'dist') },
{ from: path.join(siaRoot, 'src/extensionHooks/extension.html'), to: path.join(siaRoot, 'dist') },
{ from: path.join(siaRoot, 'src/index.html'), to: path.join(siaRoot, 'dist') }
],
{ copyUnmodified: true })
]
}

Просмотреть файл

@ -4,6 +4,8 @@ module.exports = {
baseUrl: 'http://localhost:50000/',
eventUiUrl: 'http://localhost:3000/',
authRedirectUri: 'http://localhost:3000/',
retries: 2,
@ -42,6 +44,6 @@ module.exports = {
},
instrumentationKey: 'APP INSIGHT GOES HERE',
useAppInsights: false
}
}

2
dist/README.md поставляемый
Просмотреть файл

@ -1,2 +0,0 @@
# About the dist folder
After building the dist version of your project, the generated files are stored in this folder. You should keep it under version control.

Двоичные данные
dist/assets/012cf6a10129e2275d79d6adac7f3b02.woff поставляемый

Двоичный файл не отображается.

Двоичные данные
dist/assets/570eb83859dc23dd0eec423a49e147fe.woff2 поставляемый

Двоичный файл не отображается.

Двоичные данные
dist/assets/a37b0c01c0baf1888ca812cc0508f6e2.ttf поставляемый

Двоичный файл не отображается.

2
dist/assets/app.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

1
dist/assets/app.js.map поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

2
dist/assets/appInsights.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

1
dist/assets/appInsights.js.map поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Двоичные данные
dist/assets/e79bfd88537def476913f3ed52f4f4b3.eot поставляемый

Двоичный файл не отображается.

16
dist/assets/manifest.json поставляемый
Просмотреть файл

@ -1,16 +0,0 @@
{
"name": "SIA",
"description": "SIA",
"version": "0.0.0.2",
"manifest_version": 2,
"browser_action": {
"default_popup": "extension.html"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"author": "microsoft",
"permissions": [
"identity",
"tabs",
"https://*/"
]
}

Двоичные данные
dist/favicon.ico поставляемый

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 100 KiB

24
dist/index.html поставляемый
Просмотреть файл

@ -1,24 +0,0 @@
<!doctype html>
<html>
<!--
To collect end-user usage analytics about your application,
insert the following script into each page you want to track.
Place this code immediately before the closing </head> tag,
and before any other scripts. Your first data will appear
automatically in just a few seconds.
-->
<script type="text/javascript" src="/assets/appInsights.js"></script>
<head>
<meta charset="utf-8">
<title>SRE Incident Assistant</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div id="siaApp">APPLICATION CONTENT</div>
<script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script>
<script type="text/javascript" src="/assets/app.js"></script>
</body>
</html>

6
dist/static/README.md поставляемый
Просмотреть файл

@ -1,6 +0,0 @@
# static
Files and directories that you put in `static` will be copied to the
`dist/static` directory during the build step. Use it to provide
arbitrary static assets that can be referenced by path in your
application.

Двоичные данные
dist/static/favicon.ico поставляемый

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 202 KiB

291
package-lock.json сгенерированный
Просмотреть файл

@ -335,7 +335,7 @@
"anymatch": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
"integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
"integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=",
"dev": true,
"requires": {
"micromatch": "2.3.11",
@ -345,7 +345,7 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=",
"dev": true
},
"are-we-there-yet": {
@ -380,7 +380,7 @@
"arr-flatten": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
"integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
"integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
"dev": true
},
"array-filter": {
@ -822,7 +822,7 @@
"babel-loader": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.2.tgz",
"integrity": "sha512-jRwlFbINAeyDStqK6Dd5YuY0k5YuzQUvlz2ZamuXrXmxav3pNqe9vfJ402+2G+OmlJSXxCOpB6Uz0INM7RQe2A==",
"integrity": "sha1-9svhInEPGqKvTYgcbVtUNYyiQSY=",
"dev": true,
"requires": {
"find-cache-dir": "1.0.0",
@ -1336,7 +1336,7 @@
"babel-preset-env": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz",
"integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==",
"integrity": "sha1-oYtWTMm5r99KrleuPBsNmRiOb0g=",
"dev": true,
"requires": {
"babel-plugin-check-es2015-constants": "6.22.0",
@ -1490,7 +1490,7 @@
"babylon": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
"integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=",
"dev": true
},
"backo2": {
@ -1514,7 +1514,7 @@
"base64-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
"integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=",
"dev": true
},
"base64id": {
@ -1586,7 +1586,7 @@
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=",
"dev": true
},
"body-parser": {
@ -1710,7 +1710,7 @@
"browserify": {
"version": "14.5.0",
"resolved": "https://registry.npmjs.org/browserify/-/browserify-14.5.0.tgz",
"integrity": "sha512-gKfOsNQv/toWz+60nSPfYzuwSEdzvV2WdxrVPUbPD/qui44rAkB3t3muNtmmGYHqrG56FGwX9SUEQmzNLAeS7g==",
"integrity": "sha1-C7vOUhrNbk0dVNjpNlAI77hanMU=",
"dev": true,
"requires": {
"JSONStream": "1.3.1",
@ -2131,7 +2131,7 @@
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=",
"dev": true,
"requires": {
"inherits": "2.0.3",
@ -2141,7 +2141,7 @@
"circular-json": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
"integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
"integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY=",
"dev": true
},
"clap": {
@ -2436,7 +2436,7 @@
"copy-concurrently": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
"integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
"integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=",
"dev": true,
"requires": {
"aproba": "1.2.0",
@ -2447,6 +2447,59 @@
"run-queue": "1.0.3"
}
},
"copy-webpack-plugin": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.4.1.tgz",
"integrity": "sha512-ojaz8MpS3zoLJT/JbYMusYM+dCEArhW24hGAUPYPydTCS+87NFh2TWr85sywG3So4Q4E68QoerqQ+Ns1g0fhDg==",
"dev": true,
"requires": {
"cacache": "10.0.2",
"find-cache-dir": "1.0.0",
"globby": "7.1.1",
"is-glob": "4.0.0",
"loader-utils": "0.2.17",
"minimatch": "3.0.4",
"p-limit": "1.1.0",
"serialize-javascript": "1.4.0"
},
"dependencies": {
"globby": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
"integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
"dev": true,
"requires": {
"array-union": "1.0.2",
"dir-glob": "2.0.0",
"glob": "7.1.2",
"ignore": "3.3.5",
"pify": "3.0.0",
"slash": "1.0.0"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
"integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
"dev": true,
"requires": {
"is-extglob": "2.1.1"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
}
}
},
"copyfiles": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-1.2.0.tgz",
@ -2510,7 +2563,7 @@
"cross-env": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.1.3.tgz",
"integrity": "sha512-UOokgwvDzCT0mqRSLEkJzUhYXB1vK3E5UgDrD41QiXsm9UetcW2rCGHYz/O3p873lMJ1VZbFCF9Izkwh7nYR5A==",
"integrity": "sha1-+K4Y+qyHaSsKi00vcADU7DqF39c=",
"dev": true,
"requires": {
"cross-spawn": "5.1.0",
@ -2975,6 +3028,33 @@
"randombytes": "2.0.5"
}
},
"dir-glob": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz",
"integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==",
"dev": true,
"requires": {
"arrify": "1.0.1",
"path-type": "3.0.0"
},
"dependencies": {
"path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
"integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
"dev": true,
"requires": {
"pify": "3.0.0"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
}
}
},
"discontinuous-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
@ -3108,7 +3188,7 @@
"electron-releases": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/electron-releases/-/electron-releases-2.1.0.tgz",
"integrity": "sha512-cyKFD1bTE/UgULXfaueIN1k5EPFzs+FRc/rvCY5tIynefAPqopQEgjr0EzY+U3Dqrk/G4m9tXSPuZ77v6dL/Rw==",
"integrity": "sha1-xWFL+BHxds48g242igYleCNB/U4=",
"dev": true
},
"electron-to-chromium": {
@ -3515,7 +3595,7 @@
"espree": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz",
"integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==",
"integrity": "sha1-dWrai5eenc/NswqtjRqTBKkF4co=",
"dev": true,
"requires": {
"acorn": "5.3.0",
@ -3525,7 +3605,7 @@
"acorn": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz",
"integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==",
"integrity": "sha1-dEbTlFnFT7SagObuZHgUm5QOyCI=",
"dev": true
}
}
@ -3592,7 +3672,7 @@
"evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=",
"dev": true,
"requires": {
"md5.js": "1.3.4",
@ -4029,7 +4109,7 @@
"file-loader": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.6.tgz",
"integrity": "sha512-873ztuL+/hfvXbLDJ262PGO6XjERnybJu2gW1/5j8HUfxSiFJI9Hj/DhZ50ZGRUxBvuNiazb/cM2rh9pqrxP6Q==",
"integrity": "sha1-e5qPLFjwCnf930npQPesl4o+oOg=",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
@ -4108,6 +4188,37 @@
}
}
},
"find-cache-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
"integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
"dev": true,
"requires": {
"commondir": "1.0.1",
"make-dir": "1.0.0",
"pkg-dir": "2.0.0"
},
"dependencies": {
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"dev": true,
"requires": {
"locate-path": "2.0.0"
}
},
"pkg-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
"integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
"dev": true,
"requires": {
"find-up": "2.1.0"
}
}
}
},
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@ -4447,15 +4558,12 @@
"full-icu": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/full-icu/-/full-icu-1.2.0.tgz",
"integrity": "sha512-0B/oH/5WnTM0grN0/F91WN1zdLx+IKH0uOlAqj6DWhRQTGFCj645elI46fKykRSWeafPrIOIRzAL7co1Vku5pQ==",
"requires": {
"icu4c-data": "0.59.2"
}
"integrity": "sha512-0B/oH/5WnTM0grN0/F91WN1zdLx+IKH0uOlAqj6DWhRQTGFCj645elI46fKykRSWeafPrIOIRzAL7co1Vku5pQ=="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=",
"dev": true
},
"function.prototype.name": {
@ -4560,7 +4668,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@ -4609,7 +4717,7 @@
"globals": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
"integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
"integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=",
"dev": true
},
"globby": {
@ -4743,7 +4851,7 @@
"hash.js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
"integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
"integrity": "sha1-NA3tvmKQGHFRweodd3o0SJNd+EY=",
"dev": true,
"requires": {
"inherits": "2.0.3",
@ -4792,7 +4900,7 @@
"history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
"integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
"integrity": "sha1-IrXH8xYzxbgCHH9KipVKwTnujVs=",
"requires": {
"invariant": "2.2.2",
"loose-envify": "1.3.1",
@ -4830,7 +4938,7 @@
"hosted-git-info": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
"integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
"integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=",
"dev": true
},
"html-comment-regex": {
@ -4969,7 +5077,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -4978,7 +5086,7 @@
"chalk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
"integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
"integrity": "sha1-tepI78nBeT3MybR2fJORTT8tUro=",
"dev": true,
"requires": {
"ansi-styles": "3.2.0",
@ -5023,7 +5131,7 @@
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
"integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
"integrity": "sha1-BYoCHRthn33fOYDXEuo1kM5949U=",
"dev": true,
"requires": {
"has-flag": "2.0.0"
@ -5031,11 +5139,6 @@
}
}
},
"icu4c-data": {
"version": "0.59.2",
"resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.59.2.tgz",
"integrity": "sha1-Xf97e+4H/fp6ybtgHMe4gEaha+Y="
},
"ieee754": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
@ -5529,13 +5632,13 @@
"istanbul-lib-coverage": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz",
"integrity": "sha512-0+1vDkmzxqJIn5rcoEqapSB4DmPxE31EtI2dF2aCkV5esN9EWHxZ0dwgDClivMXJqE7zaYQxq30hj5L0nlTN5Q==",
"integrity": "sha1-c7+5mIhSmUFck9OKPprfeEp3qdo=",
"dev": true
},
"istanbul-lib-instrument": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz",
"integrity": "sha512-RQmXeQ7sphar7k7O1wTNzVczF9igKpaeGQAG9qR2L+BS4DCJNTI9nytRmIVYevwO0bbq+2CXvJmYDuz0gMrywA==",
"integrity": "sha1-JQsws1MeXTJRKZ/dZLCyydtrVY4=",
"dev": true,
"requires": {
"babel-generator": "6.26.0",
@ -5989,7 +6092,7 @@
"karma-babel-preprocessor": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/karma-babel-preprocessor/-/karma-babel-preprocessor-7.0.0.tgz",
"integrity": "sha512-k8YUot8ZAAYhAeUxOsOGUEXW7AlB6SkoIVGfavEBCAdGHzWuraOBoR2wCxxdePUCvcItIxSUyQnOj6DuZdEJYA==",
"integrity": "sha1-GHVtgY+XpeiPkZAmdM2RMBd6jc4=",
"dev": true
},
"karma-chai": {
@ -6048,7 +6151,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -6057,7 +6160,7 @@
"chalk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
"integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
"integrity": "sha1-tepI78nBeT3MybR2fJORTT8tUro=",
"dev": true,
"requires": {
"ansi-styles": "3.2.0",
@ -6122,7 +6225,7 @@
"karma-webpack": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-2.0.9.tgz",
"integrity": "sha512-F1j3IG/XhiMzcunAXbWXH95uizjzr3WdTzmVWlta8xqxcCtAu9FByCb4sccIMxaVFAefpgnUW9KlCo0oLvIX6A==",
"integrity": "sha1-YciAkffdkQY1E0wDKyZqRlr/tX8=",
"dev": true,
"requires": {
"async": "0.9.2",
@ -6147,13 +6250,13 @@
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=",
"dev": true
},
"webpack-dev-middleware": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz",
"integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==",
"integrity": "sha1-+PwRIM47T8VoDO7LQ9d3lmshEF4=",
"dev": true,
"requires": {
"memory-fs": "0.4.1",
@ -6564,7 +6667,7 @@
"lru-cache": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
"integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
"integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=",
"dev": true,
"requires": {
"pseudomap": "1.0.2",
@ -6674,7 +6777,7 @@
"material-ui": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.20.0.tgz",
"integrity": "sha512-wkHkeU1SaGfCrtwIzBOl5vynNNNzVGW27ql0Ue5HZLB4WyRQ3YohJBdKa5lBrH5JD/Cgae7IzrP7cVWDyKpeLQ==",
"integrity": "sha1-hUEbtZyRbJx3A/Kdz/xE46Z9URE=",
"requires": {
"babel-runtime": "6.26.0",
"inline-style-prefixer": "3.0.8",
@ -6925,7 +7028,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
@ -6975,7 +7078,7 @@
"mocha": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz",
"integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==",
"integrity": "sha1-fYbPvPNcuCnidUwy4XNV7AUzh5Q=",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
@ -6993,7 +7096,7 @@
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=",
"dev": true
},
"debug": {
@ -7026,7 +7129,7 @@
"supports-color": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
"integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=",
"dev": true,
"requires": {
"has-flag": "2.0.0"
@ -7342,7 +7445,7 @@
"normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
"integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=",
"dev": true,
"requires": {
"hosted-git-info": "2.5.0",
@ -7436,7 +7539,7 @@
"nyc": {
"version": "11.4.1",
"resolved": "https://registry.npmjs.org/nyc/-/nyc-11.4.1.tgz",
"integrity": "sha512-5eCZpvaksFVjP2rt1r60cfXmt3MUtsQDw8bAzNqNEr4WLvUMLgiVENMf/B9bE9YAX0mGVvaGA3v9IS9ekNqB1Q==",
"integrity": "sha1-E/335+8i0CfGHRdHWPaXimj09eU=",
"dev": true,
"requires": {
"archy": "1.0.0",
@ -9842,7 +9945,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -9900,7 +10003,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -9980,7 +10083,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -10060,7 +10163,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -10261,7 +10364,7 @@
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=",
"requires": {
"asap": "2.0.6"
}
@ -10467,7 +10570,7 @@
"randomatic": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
"integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
"integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=",
"dev": true,
"requires": {
"is-number": "3.0.0",
@ -10508,7 +10611,7 @@
"randombytes": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz",
"integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==",
"integrity": "sha1-3ACaJGuNCaF3tLegrne8Vw9LG3k=",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
@ -10546,7 +10649,7 @@
"react": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz",
"integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==",
"integrity": "sha1-oxvS2rib/2XUITT6GH8k0FTCc7o=",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
@ -10583,7 +10686,7 @@
"react-dom": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz",
"integrity": "sha512-zpGAdwHVn9K0091d+hr+R0qrjoJ84cIBFL2uU60KvWBPfZ7LPSrfqviTxGHWN0sjPZb2hxWzMexwrvJdKePvjg==",
"integrity": "sha1-aQAxeGAcDKGbcJszqDNp/mEkwEQ=",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
@ -10615,7 +10718,7 @@
"react-hot-loader": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-3.1.3.tgz",
"integrity": "sha512-d7nZf78irxoGN5PY4zd6CSgZiroOhvIWzRast3qwTn4sSnBwlt08kV8WMQ9mitmxEdlCTwZt+5ClrRSjxWguMQ==",
"integrity": "sha1-b5KHcyaVjHywE0tRJHRReGkSYII=",
"requires": {
"global": "4.3.2",
"react-deep-force-update": "2.1.1",
@ -10677,7 +10780,7 @@
"react-redux": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz",
"integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==",
"integrity": "sha1-I+06T5hjWdaLUhLqqmgeYNZXSUY=",
"requires": {
"hoist-non-react-statics": "2.3.1",
"invariant": "2.2.2",
@ -10697,7 +10800,7 @@
"react-router": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.2.0.tgz",
"integrity": "sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==",
"integrity": "sha1-Yfez43cNrrJAYtrj7t7xsFQVWYY=",
"requires": {
"history": "4.7.2",
"hoist-non-react-statics": "2.3.1",
@ -10731,7 +10834,7 @@
"react-router-dom": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.2.2.tgz",
"integrity": "sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==",
"integrity": "sha1-yKgd863Fi7qKdngulGy9Tq5km40=",
"requires": {
"history": "4.7.2",
"invariant": "2.2.2",
@ -10752,7 +10855,7 @@
"react-test-renderer": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.2.0.tgz",
"integrity": "sha512-Kd4gJFtpNziR9ElOE/C23LeflKLZPRpNQYWP3nQBY43SJ5a+xyEGSeMrm2zxNKXcnCbBS/q1UpD9gqd5Dv+rew==",
"integrity": "sha1-vd8lmmuPzYVV8BKvyOrMI4hyohE=",
"requires": {
"fbjs": "0.8.16",
"object-assign": "4.1.1",
@ -10813,7 +10916,7 @@
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
"integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=",
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
@ -10859,7 +10962,7 @@
"redbox-react": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/redbox-react/-/redbox-react-1.5.0.tgz",
"integrity": "sha512-mdxArOI3sF8K5Nay5NG+lv/VW516TbXjjd4h1wcV1Iy4IMDQPnCayjoQXBAycAFSME4nyXRUXCjHxsw2rYpVRw==",
"integrity": "sha1-BNqxFVfSZlG/NWKmfCKs5WxdOWc=",
"requires": {
"error-stack-parser": "1.3.6",
"object-assign": "4.1.1",
@ -10942,7 +11045,7 @@
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"integrity": "sha1-BrcxIyFZAdJdBlvjQusCa8HIU3s=",
"requires": {
"lodash": "4.17.4",
"lodash-es": "4.17.4",
@ -10953,7 +11056,7 @@
"redux-mock-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.4.0.tgz",
"integrity": "sha512-y+SGh/SONWwqs4DiyHjd0H6NMgz368wXDiUjSHuOnMEr4dN9PmjV6N3bNvxoILaIQ7zeVKclLyxsCQ2TwGZfEw==",
"integrity": "sha1-zch2UPV1nyk1iP7MnKwrBX2VGQ0=",
"requires": {
"lodash.isplainobject": "4.0.6"
}
@ -10961,12 +11064,12 @@
"redux-persist": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-5.4.0.tgz",
"integrity": "sha512-Ohp7g5WRx2rDyDk/bKKCgbD3ES6ahGPWxFEp3QPx2opwst0jJEbZKigZtQewUeJZnomN/7Zo3QglPDOZ6EdYEw=="
"integrity": "sha1-oQYjE1RqnUym+ScUZNGPc26Mo5Q="
},
"redux-persist-transform-filter": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/redux-persist-transform-filter/-/redux-persist-transform-filter-0.0.16.tgz",
"integrity": "sha512-P7vBmrhOlGWO6Qtw1VoJ2cPzlb4PdPtJjppcJK0uSmcg5TCYxBVla27mxBZzday4CCy0wsYqDq+UI/1E84Yk9Q==",
"integrity": "sha1-DCHcFmd0wS+z8JD3ovXqkPe6DOo=",
"requires": {
"lodash.forin": "4.4.0",
"lodash.get": "4.4.2",
@ -10995,7 +11098,7 @@
"regenerator-transform": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
"integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==",
"integrity": "sha1-HkmWg3Ix2ot/PPQRTXG1aRoGgN0=",
"dev": true,
"requires": {
"babel-runtime": "6.26.0",
@ -11006,7 +11109,7 @@
"regex-cache": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
"integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
"integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=",
"dev": true,
"requires": {
"is-equal-shallow": "0.1.3"
@ -11185,7 +11288,7 @@
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
"integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
"integrity": "sha1-fpriHtgV/WOrGJre7mTcgx7vqHk="
},
"restore-cursor": {
"version": "1.0.1",
@ -11220,7 +11323,7 @@
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
"dev": true,
"requires": {
"glob": "7.1.2"
@ -11279,12 +11382,12 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
"integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=",
"dev": true
},
"schema-utils": {
@ -11492,7 +11595,7 @@
"source-list-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
"integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==",
"integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=",
"dev": true
},
"source-map": {
@ -11513,7 +11616,7 @@
"sourcemapped-stacktrace": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.8.tgz",
"integrity": "sha512-OkVoI7GQOLl/laR1qsSo1c87tS8kF2VXhQq2SrQCDdXufBAcm8FgXogWso96ciMYoDtTw1Dn70CVdwYzoYs6Pg==",
"integrity": "sha1-a3o/Gm+xX21A5wHiPOQEVTSA1og=",
"requires": {
"source-map": "0.5.6"
},
@ -11579,7 +11682,7 @@
"ssri": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-5.0.0.tgz",
"integrity": "sha512-728D4yoQcQm1ooZvSbywLkV1RjfITZXh0oWrhM/lnsx3nAHx7LsRGJWB/YyvoceAYRq98xqbstiN4JBv1/wNHg==",
"integrity": "sha1-E8GTkLYGyCHyoQ0Cs1HBcpuU2M8=",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
@ -11753,7 +11856,7 @@
"stream-http": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz",
"integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==",
"integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=",
"dev": true,
"requires": {
"builtin-status-codes": "3.0.0",
@ -11821,7 +11924,7 @@
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
"requires": {
"safe-buffer": "5.1.1"
}
@ -11871,7 +11974,7 @@
"style-loader": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.1.tgz",
"integrity": "sha512-IRE+ijgojrygQi3rsqT0U4dd+UcPCqcVvauZpCnQrGAlEe+FUIyrK93bUDScamesjP08JlQNsFJU+KmPedP5Og==",
"integrity": "sha1-WR/8gLzv4mi3fF2evAUF13Jhn4U=",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
@ -12013,7 +12116,7 @@
"test-exclude": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.1.1.tgz",
"integrity": "sha512-35+Asrsk3XHJDBgf/VRFexPgh3UyETv8IAn/LRTiZjVy6rjPVqdEk8dJcJYBzl1w0XCJM48lvTy8SfEsCWS4nA==",
"integrity": "sha1-TYSWSwlmsAh+zDNKLOAC09k0HiY=",
"dev": true,
"requires": {
"arrify": "1.0.1",
@ -12428,7 +12531,7 @@
"url-loader": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.6.2.tgz",
"integrity": "sha512-h3qf9TNn53BpuXTTcpC+UehiRrl0Cv45Yr/xWayApjw6G8Bg2dGke7rIwDQ39piciWCWrC+WiqLjOh3SUp9n0Q==",
"integrity": "sha1-oAenEJYg6dmI0UvOZ3od7Lmpk/c=",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
@ -12510,7 +12613,7 @@
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==",
"integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=",
"dev": true
},
"uws": {
@ -12542,7 +12645,7 @@
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
"integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
"integrity": "sha1-xb3S9U7gk8BIOdcc4uR1imiQq8c="
},
"vendors": {
"version": "1.0.1",
@ -12595,7 +12698,7 @@
"webpack": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz",
"integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==",
"integrity": "sha1-UpG4dQeM8qv0K90jr+P4+WwX1yU=",
"dev": true,
"requires": {
"acorn": "5.3.0",
@ -12625,7 +12728,7 @@
"acorn": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz",
"integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==",
"integrity": "sha1-dEbTlFnFT7SagObuZHgUm5QOyCI=",
"dev": true
},
"ajv": {
@ -12964,7 +13067,7 @@
"webpack-hot-middleware": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.21.0.tgz",
"integrity": "sha512-P6xiOLy10QlSVSO7GanU9PLxN6zLLQ7RG16MPTvmFwf2KUG7jMp6m+fmdgsR7xoaVVLA7OlX3YO6JjoZEKjCuA==",
"integrity": "sha1-ezwROnpLMByR4HSVc8eqsotBS1I=",
"dev": true,
"requires": {
"ansi-html": "0.0.7",
@ -12988,7 +13091,7 @@
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
"dev": true,
"requires": {
"color-convert": "1.9.0"
@ -12997,7 +13100,7 @@
"chalk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
"integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
"integrity": "sha1-tepI78nBeT3MybR2fJORTT8tUro=",
"dev": true,
"requires": {
"ansi-styles": "3.2.0",
@ -13082,7 +13185,7 @@
"which": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
"integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
"integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=",
"dev": true,
"requires": {
"isexe": "2.0.0"

Просмотреть файл

@ -6,8 +6,7 @@
"main": "",
"scripts": {
"clean": "rimraf dist/*",
"copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist",
"dist": "npm run copy & webpack --env=dist",
"dist": "npm run clean && cross-env NODE_ENV=dist webpack --env=dist --progress --profile --colors --display-error-details --display-reasons --debug",
"lint": "standard \"src/**/*.js\"",
"lint:fix": "standard --fix \"src/**/*.js\"",
"posttest": "npm run lint",
@ -82,6 +81,7 @@
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"chai": "^4.1.2",
"copy-webpack-plugin": "^4.3.1",
"copyfiles": "^1.2.0",
"cross-env": "^5.1.3",
"css-loader": "^0.28.7",

Просмотреть файл

@ -1,6 +1,8 @@
import { DateTime } from 'luxon'
import { paginationActions, updatePagination, reduxBackedPromise } from 'actions/actionHelpers'
import * as filterActions from 'actions/filterActions'
import * as notificationActions from 'actions/notificationActions'
export const EVENTS = 'EVENTS'
export const REQUEST_EVENT = 'REQUEST_EVENT'
@ -62,6 +64,11 @@ export const getEventActionSet = (incidentId, eventId) => ({
}),
succeed: (event) => (dispatch) => {
dispatch(notificationActions.emitNotification({
event,
incidentId: event.incidentId
}))
dispatch({
type: RECEIVE_EVENT,
event,

Просмотреть файл

@ -0,0 +1,36 @@
export const NOTIFICATIONS_READ = 'NOTIFICATIONS_READ'
export const EMIT_NOTIFICATION = 'EMIT_NOTIFICATION'
export const NOTIFICATION_EMITTED = 'NOTIFICATION_EMITTED'
export const TOGGLE_RECEIVE_PUSH_NOTIFICATIONS = 'TOGGLE_RECEIVE_PUSH_NOTIFICATIONS'
export const NOTIFICATION_CLICKED = 'NOTIFICATION_CLICKED'
export const NOTIFICATION_REMOVED = 'NOTIFICATION_REMOVED'
export const notificationClicked = (notificationId, buttonIndex) => ({
type: NOTIFICATION_CLICKED,
notificationId,
buttonIndex
})
export const notificationRemoved = (notificationId) => ({
type: NOTIFICATION_REMOVED,
notificationId
})
export const notificationEmitted = (eventId, notificationId, buttonActions) => ({
type: NOTIFICATION_EMITTED,
eventId,
notificationId,
buttonActions
})
export const emitNotification = (notification) => ({
type: EMIT_NOTIFICATION,
notification
})
export const toggleReceivePushNotifications = (enable) => ({
type: TOGGLE_RECEIVE_PUSH_NOTIFICATIONS,
enable
})
export const notificationsRead = { type: NOTIFICATIONS_READ }

Просмотреть файл

@ -0,0 +1,10 @@
import { Component } from 'react'
class ExtensionBadge extends Component {
render () {
chrome.browserAction.setBadgeText({ text: this.props.text })
return null
}
}
export default ExtensionBadge

Просмотреть файл

@ -0,0 +1,84 @@
import { Component } from 'react'
import { connect } from 'react-redux'
import config from 'config'
import { fillTemplate, LoadTextFromEvent } from 'services/playbookService'
import { notificationEmitted } from 'actions/notificationActions'
export class Notification extends Component {
componentDidMount () {
if (!this.props.isEmitted) {
this.createNotification()
}
}
render () {
if (this.props.isClicked) {
this.handleClicks()
}
return null
}
handleClicks () {
const { buttonActions, buttonIndex } = this.props
const buttonAction = buttonIndex != null
? buttonActions[buttonIndex]
: buttonActions.find(buttonAction => buttonAction.default)
if (buttonAction) {
chrome.tabs.create({ url: buttonAction.url })
}
}
createNotification () {
const { notificationEmitted, event } = this.props
const buttonActions = this.createButtonActions()
const notificationBody = this.createNotificationBody(buttonActions)
chrome.notifications.create(notificationBody, (notificationId) => {
notificationEmitted(event.id, notificationId, buttonActions)
})
}
createNotificationBody (buttonActions) {
const { event, ticket, eventType, engagement } = this.props
const severity = ticket.data ? ticket.data.severity : null
const title = ticket.data ? ticket.data.title : null
return {
type: 'basic',
iconUrl: `/static/sia-blue-icon-128.png`,
title: `${ticket.originId} | ${ticket.status} ${severity ? '| Sev ' + severity : ''}`,
message: LoadTextFromEvent(event, eventType, ticket, engagement),
contextMessage: title || `SRE Incident Assistant | Event ID: ${event.id}`,
// Only two buttons allowed in chrome notifications
buttons: buttonActions.slice(0, 2).map(buttonAction => ({
title: buttonAction.title,
iconUrl: '/static/sia-green-icon-128.png'
}))
}
}
createButtonActions () {
const { actions, event, ticket, eventType, engagement } = this.props
const ticketSystem = config.ticketSystems[ticket.ticketSystemId || 1]
const defaultActions = [{
title: 'Open in SIA',
url: `${config.eventUiUrl}tickets/${ticket.originId}`,
default: true
}, {
title: `Open in ${ticketSystem.name}`,
url: `${ticketSystem.ticketUriPrefix}${ticket.originId}${ticketSystem.ticketUriSuffix}`
}]
const playbookActions = actions
? actions.filter(action => action.actionTemplate.isUrl)
.map(action => ({
title: `${action.name}: ${action.actionTemplate.name}`,
url: fillTemplate(action.actionTemplate, event, ticket, eventType, engagement)
})) : []
return playbookActions.concat(defaultActions)
}
}
const mapDispatchToProps = {
notificationEmitted
}
export default connect(null, mapDispatchToProps)(Notification)

Просмотреть файл

@ -0,0 +1,85 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as incidentActions from 'actions/incidentActions'
import ExtensionBadge from 'components/Extension/ExtensionBadge'
import Notification from 'components/Extension/Notification'
class Notifications extends Component {
componentDidUpdate () {
if (!this.props.enabled) {
return
}
this.fetchIncidentsIfNeeded()
}
fetchIncidentsIfNeeded () {
const { notifications, incidents, incidentsFetching, fetchIncident } = this.props
const incidentIds = [...new Set(notifications.map(n => n.incidentId))]
incidentIds.forEach(incidentId => {
const incidentIsLoaded = incidents[incidentId]
const incidentIsFetching = incidentsFetching.includes(incidentId)
if (incidentIsLoaded || incidentIsFetching) {
return
}
fetchIncident(incidentId)
})
}
render () {
const { enabled, notifications, unreadCount, eventTypes, incidents } = this.props
if (!enabled || unreadCount === 0) {
return <ExtensionBadge text='' />
}
const notificationsToRender = notifications.filter(n => incidents[n.incidentId])
.map(notification => {
const eventType = eventTypes[notification.event.eventTypeId]
const actions = eventType ? eventType.actions : null
const ticket = incidents[notification.incidentId].primaryTicket
const engagement = null // TODO
return <Notification
key={notification.event.id}
{...notification}
actions={actions}
engagement={engagement}
eventType={eventType}
ticket={ticket}
/>
})
return (
<div>
<ExtensionBadge text={unreadCount > 99 ? '99+' : unreadCount.toString()} />
{ notificationsToRender }
</div>)
}
}
export const mapStateToProps = (state) => {
const enabled = state.notifications.options.receivePushNotificationsEnabled
if (!enabled) {
return { enabled }
}
return {
enabled,
notifications: state.notifications.list,
unreadCount: state.notifications.unreadCount,
eventTypes: state.eventTypes.records,
incidents: state.incidents.map,
engagements: state.engagements.list,
incidentsFetching: state.incidents.fetchingByIncidentId
}
}
const mapDispatchToProps = {
fetchIncident: incidentActions.fetchIncident
}
export default connect(mapStateToProps, mapDispatchToProps)(Notifications)

Просмотреть файл

@ -14,6 +14,8 @@ import incidentRedirect from 'components/Incident/incidentRedirect'
import Home from 'components/Home'
import TopNav from 'components/TopNav/TopNav'
import Debug from 'components/Debug'
import { isChromeExtensionBackground } from 'services/notificationService'
import Notifications from 'components/Extension/Notifications'
const history = createBrowserHistory()
@ -27,6 +29,7 @@ export default class MainComponent extends React.Component {
<EnsureLoggedInContainer>
<Router history={history} >
<div>
{ isChromeExtensionBackground() ? <Notifications /> : null }
<TopNav />
<Route exact path='/' component={Home} />
<Route exact path='/extension.html' component={Home} />

Просмотреть файл

@ -1,16 +1,12 @@
let config
try {
if (typeof constants !== 'undefined') {
// The global 'constants' is being defined by Webpack in /cfg/[dev|dist|test|localhost].js
// eslint-disable-next-line no-undef
config = constants
} catch (ex) { // required for Travis and Code Coverage
try {
const testConstants = require('../../cfg/test.const.js')
config = testConstants.default || testConstants
} catch (ex) {
const defaultConstants = require('../../cfg/defaultConstants')
config = defaultConstants.default || defaultConstants
}
} else {
const defaultConstants = require('../../cfg/defaultConstants')
config = defaultConstants.default || defaultConstants
}
export default config

Просмотреть файл

@ -5,6 +5,7 @@ import { ListenForScreenSize } from 'actions/styleActions'
import incidentApp from 'reducers'
import { persistStore } from 'redux-persist'
import { getFilterFromUrl } from 'actions/filterActions'
import { configureNotificationService } from 'services/notificationService'
const urlFilter = getFilterFromUrl(window.location.search)
const reducer = incidentApp(urlFilter)
@ -15,4 +16,6 @@ export const persistor = persistStore(store)
establishSignalRConnection(store.dispatch)
configureNotificationService(store.dispatch)
ListenForScreenSize(window, store)

Просмотреть файл

@ -6,9 +6,10 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="/favicon.ico">
</head>
<body>
<div id="siaApp" style="height:1000px" >APPLICATION CONTENT</div>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript" src="/assets/app.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,33 @@
{
"name": "SIA",
"description": "SRE Incident Assistant",
"version": "0.0.0.3",
"icons": {
"128": "/static/sia-blue-icon-128.png",
"24": "/static/sia-blue-icon-24.png",
"19": "/static/sia-blue-icon-19.png",
"16": "/static/sia-blue-icon-16.png"
},
"manifest_version": 2,
"browser_action": {
"default_icon": {
"128": "/static/sia-blue-icon-128.png",
"24": "/static/sia-blue-icon-24.png",
"19": "/static/sia-blue-icon-19.png",
"16": "/static/sia-blue-icon-16.png"
},
"default_popup": "/extension.html"
},
"background": {
"page": "/extension.html"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"author": "microsoft",
"permissions": [
"contextMenus",
"notifications",
"identity",
"tabs",
"https://*/"
]
}

Двоичные данные
src/favicon.ico

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 100 KiB

Просмотреть файл

@ -7,10 +7,10 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="/favicon.ico">
</head>
<body>
<div id="siaApp">APPLICATION CONTENT</div>
<script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script>
<script type="text/javascript" src="/assets/app.js"></script>
</body>

Просмотреть файл

@ -9,6 +9,7 @@ import signalR from 'reducers/signalRReducer'
import forms from 'reducers/formReducer'
import eventTypes from 'reducers/eventTypeReducer'
import globalActions from 'reducers/globalActionReducer'
import notifications from 'reducers/notificationReducer'
const rootReducer = (filters) => combineReducers({
incidents,
@ -20,7 +21,8 @@ const rootReducer = (filters) => combineReducers({
expandSection,
signalR,
eventTypes,
globalActions
globalActions,
notifications
})
export default rootReducer

Просмотреть файл

@ -0,0 +1,65 @@
import { combineReducers } from 'redux'
import * as notificationActions from 'actions/notificationActions'
const notificationOptionsInitialState = {
receivePushNotificationsEnabled: true
}
export function notificationOptionsReducer (state = notificationOptionsInitialState, action) {
switch (action.type) {
case notificationActions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS:
return {
...state,
receivePushNotificationsEnabled: action.enable
}
default:
return state
}
}
const unreadNotificationCountInitialState = 0
export function unreadNotificationCountReducer (state = unreadNotificationCountInitialState, action) {
switch (action.type) {
case notificationActions.EMIT_NOTIFICATION:
return state + 1
case notificationActions.NOTIFICATIONS_READ:
return 0
default:
return state
}
}
const notificationListInitialState = []
export function notificationListReducer (state = notificationListInitialState, action) {
switch (action.type) {
case notificationActions.EMIT_NOTIFICATION:
return state.concat(action.notification)
case notificationActions.NOTIFICATION_EMITTED:
return state.map(notification =>
notification.event.id === action.eventId
? {
...notification,
notificationId: action.notificationId,
isEmitted: true,
buttonActions: action.buttonActions
}
: notification)
case notificationActions.NOTIFICATION_CLICKED:
return state.map(notification =>
notification.notificationId === action.notificationId
? { ...notification, isClicked: true, buttonIndex: action.buttonIndex }
: notification)
case notificationActions.NOTIFICATION_REMOVED:
return state.filter(notification => notification.notificationId !== action.notificationId)
default:
return state
}
}
export default combineReducers({
options: notificationOptionsReducer,
unreadCount: unreadNotificationCountReducer,
list: notificationListReducer
})

Просмотреть файл

@ -0,0 +1,73 @@
import * as actions from 'actions/notificationActions'
export const extensionMessages = {
popupOnLoad: 'SIA_EXTENSION_POPUP_ON_LOAD'
}
export const isChromeExtension = () => typeof chrome !== 'undefined' &&
typeof window !== 'undefined' &&
chrome.extension != null &&
chrome.extension.getBackgroundPage != null
export const isChromeExtensionBackground = () => isChromeExtension() &&
chrome.extension.getBackgroundPage() === window
export const isChromeExtensionPopup = () => isChromeExtension() &&
chrome.extension.getBackgroundPage() !== window
export const configureNotificationService = (dispatch) => {
if (isChromeExtensionPopup()) {
sendPopupLoadEvent()
}
if (isChromeExtensionBackground()) {
chrome.browserAction.setBadgeBackgroundColor({ color: '#FFB900' })
registerExtensionListeners(dispatch)
createContextMenus()
}
}
export const createContextMenus = () => {
chrome.contextMenus.removeAll()
chrome.contextMenus.create({
id: actions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS,
type: 'checkbox',
checked: true,
title: 'Receive Push Notifications',
contexts: ['all']
})
}
export const sendPopupLoadEvent = () => {
chrome.runtime.sendMessage({ messageId: extensionMessages.popupOnLoad })
}
export const registerExtensionListeners = (dispatch) => {
chrome.runtime.onMessage.addListener((request, sender) => {
if (request.messageId === extensionMessages.popupOnLoad) {
dispatch(actions.notificationsRead)
}
})
chrome.contextMenus.onClicked.addListener((info, tab) => {
switch (info.menuItemId) {
case actions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS:
dispatch(actions.toggleReceivePushNotifications(info.checked))
break
default:
break
}
})
chrome.notifications.onClicked.addListener((notificationId) => {
dispatch(actions.notificationClicked(notificationId))
})
chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
dispatch(actions.notificationClicked(notificationId, buttonIndex))
})
chrome.notifications.onClosed.addListener(notificationId => {
dispatch(actions.notificationRemoved(notificationId))
})
}

Двоичные данные
src/static/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB

Двоичные данные
src/static/sia-blue-icon-128.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.2 KiB

Двоичные данные
src/static/sia-blue-icon-16.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 734 B

Двоичные данные
src/static/sia-blue-icon-19.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 888 B

Двоичные данные
src/static/sia-blue-icon-24.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичные данные
src/static/sia-green-icon-128.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.5 KiB

Двоичные данные
src/static/sia-orange-icon-128.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.5 KiB

Двоичные данные
src/static/sia-yellow-icon-128.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.4 KiB

Просмотреть файл

@ -0,0 +1,170 @@
import { expect } from 'chai'
import React from 'react'
import { shallow } from 'enzyme'
import 'test/helpers/configureEnzyme'
import { Notification } from 'components/Extension/Notification'
/* eslint-disable no-unused-expressions */
describe('Notification component', function () {
const originalWindow = global.window
const originalChrome = global.chrome
after(function () {
// Since we are messing with global objects we need to reset them back to the original
// objects to avoid impacting other test suites.
global.window = originalWindow
global.chrome = originalChrome
})
beforeEach(function () {
global.window = {}
global.chrome = {}
})
const baseProps = {
event: { id: 'eventId0' },
actions: [],
engagement: {},
eventType: {},
ticket: {}
}
context('when not emitted yet', function () {
it('should create a notification', function () {
let notificationBodyResult
global.chrome = {
notifications: {
create: notificationBody => { notificationBodyResult = notificationBody }
}
}
shallow(<Notification {...baseProps} />)
expect(notificationBodyResult).to.not.be.undefined
})
it('should dispatch notificationEmitted', function () {
global.chrome = {
notifications: {
create: (body, afterCreate) => { afterCreate('notificationId0') }
}
}
let eventIdResult
let notificationIdResult
let buttonActionsResult
const props = {
...baseProps,
notificationEmitted: (eventId, notificationId, buttonActions) => {
eventIdResult = eventId
notificationIdResult = notificationId
buttonActionsResult = buttonActions
}
}
shallow(<Notification {...props} />)
expect(eventIdResult).to.equal('eventId0')
expect(notificationIdResult).to.equal('notificationId0')
expect(buttonActionsResult).to.not.be.undefined
})
describe('createButtonActions', function () {
it('should create two base actions if no playbook actions provided', function () {
let notificationBodyResult
global.chrome = {
notifications: {
create: (notificationBody, afterCreate) => {
notificationBodyResult = notificationBody
afterCreate('notificationId0')
}
}
}
let buttonActionsResult
const props = {
...baseProps,
notificationEmitted: (eventId, notificationId, buttonActions) => {
buttonActionsResult = buttonActions
}
}
shallow(<Notification {...props} />)
const notificationButtonTitles = notificationBodyResult.buttons.map(b => b.title)
const buttonActionTitles = buttonActionsResult.map(b => b.title)
expect(buttonActionsResult).to.have.lengthOf(2)
expect(notificationBodyResult.buttons).to.have.lengthOf(2)
expect(notificationButtonTitles).to.deep.equal(buttonActionTitles)
})
it('should create at most two buttons based on the playbook actions provided', function () {
let notificationBodyResult
global.chrome = {
notifications: {
create: (notificationBody, afterCreate) => {
notificationBodyResult = notificationBody
afterCreate('notificationId0')
}
}
}
let buttonActionsResult
const props = {
...baseProps,
notificationEmitted: (eventId, notificationId, buttonActions) => {
buttonActionsResult = buttonActions
},
actions: [{
actionTemplate: { name: 'actionTemplate0', isUrl: true },
name: 'playbookAction0'
}, {
actionTemplate: { name: 'actionTemplate1', isUrl: true },
name: 'playbookAction1'
}]
}
shallow(<Notification {...props} />)
const notificationButtonTitles = notificationBodyResult.buttons.map(b => b.title)
const buttonActionTitles = buttonActionsResult.slice(0, 2).map(b => b.title)
expect(buttonActionsResult).to.have.lengthOf(4)
expect(notificationBodyResult.buttons).to.have.lengthOf(2)
expect(notificationButtonTitles).to.deep.equal(buttonActionTitles)
expect(notificationButtonTitles[0]).to.contain(props.actions[0].name)
})
})
})
context('when emitted already', function () {
it('should not create a notification', function () {
let notificationBodyResult
global.chrome = {
notifications: {
create: notificationBody => { notificationBodyResult = notificationBody }
}
}
const props = {...baseProps, isEmitted: true}
const shallowNotification = shallow(<Notification {...props} />)
expect(shallowNotification.length).to.equal(1)
expect(notificationBodyResult).to.be.undefined
})
})
context('when notification clicked', function () {
const clickedProps = {
...baseProps,
isEmitted: true,
isClicked: true,
buttonActions: [{url: 'url0'}, {url: 'url1', default: true}, {url: 'url2'}]
}
it('should open a new tab pointing to the button action url', function () {
let urlResult
global.chrome.tabs = { create: tab => { urlResult = tab.url } }
shallow(<Notification {...{...clickedProps, buttonIndex: 0}} />)
expect(urlResult).to.equal(clickedProps.buttonActions[0].url)
shallow(<Notification {...{...clickedProps, buttonIndex: 2}} />)
expect(urlResult).to.equal(clickedProps.buttonActions[2].url)
})
it('should fallback to the default action url when clicked in body instead of button', function () {
let urlResult
global.chrome.tabs = { create: tab => { urlResult = tab.url } }
shallow(<Notification {...clickedProps} />)
const defaultActionUrl = clickedProps.buttonActions.find(buttonAction => buttonAction.default).url
expect(urlResult).to.equal(defaultActionUrl)
})
})
})

Просмотреть файл

@ -0,0 +1,167 @@
import { expect } from 'chai'
import * as reducer from 'reducers/notificationReducer'
import * as actions from 'actions/notificationActions'
/* eslint-disable no-unused-expressions */
describe('Chrome Extension Push Notifications Reducer', function () {
describe('Notification Options Reducer', function () {
it('should enable and disable the receivePushNotificationsEnabled flag', function () {
const result = reducer.notificationOptionsReducer(undefined, {
type: actions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS,
enable: true
})
const result2 = reducer.notificationOptionsReducer(undefined, {
type: actions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS,
enable: false
})
const result3 = reducer.notificationOptionsReducer({ receivePushNotificationsEnabled: true }, {
type: actions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS,
enable: false
})
expect(result.receivePushNotificationsEnabled).to.be.true
expect(result2.receivePushNotificationsEnabled).to.be.false
expect(result3.receivePushNotificationsEnabled).to.be.false
})
})
describe('Unread Notification Count Reducer', function () {
it('should increment unreadNotificationCount by one with every notification emitted', function () {
const action = { type: actions.EMIT_NOTIFICATION }
const result1 = reducer.unreadNotificationCountReducer(undefined, action)
const result2 = reducer.unreadNotificationCountReducer(7, action)
expect(result1).to.equal(1)
expect(result2).to.equal(8)
})
it('should reset to zero when notifications are read', function () {
const action = { type: actions.NOTIFICATIONS_READ }
const result1 = reducer.unreadNotificationCountReducer(undefined, action)
const result2 = reducer.unreadNotificationCountReducer(7, action)
expect(result1).to.equal(0)
expect(result2).to.equal(0)
})
})
describe('Notification List Reducer', function () {
context('when emit is invoked', function () {
it('should add the notification to the list', function () {
const notification = {}
const action = { type: actions.EMIT_NOTIFICATION, notification }
const resultList1 = reducer.notificationListReducer(undefined, action)
const resultList2 = reducer.notificationListReducer([{}, {}, {}], action)
expect(resultList1).to.contain(notification)
expect(resultList1).to.have.lengthOf(1)
expect(resultList2).to.contain(notification)
expect(resultList2).to.have.lengthOf(4)
})
})
context('when notification emitted', function () {
it('should add the notification id', function () {
const notification1 = { event: {id: 'Event1'} }
const notification2 = { event: {id: 'Event2'} }
const action = {
type: actions.NOTIFICATION_EMITTED,
eventId: notification1.event.id,
notificationId: 'Notification1'
}
const resultList = reducer.notificationListReducer([notification1, notification2], action)
const resultNotification1 = resultList.find(n => n.event.id === notification1.event.id)
const resultNotification2 = resultList.find(n => n.event.id === notification2.event.id)
expect(resultNotification1.notificationId).to.equal(action.notificationId)
expect(resultNotification2.notificationId).to.be.undefined
})
it('should set isEmitted to true', function () {
const notification1 = { event: {id: 'Event1'} }
const notification2 = { event: {id: 'Event2'} }
const action = {
type: actions.NOTIFICATION_EMITTED,
eventId: notification1.event.id
}
const resultList = reducer.notificationListReducer([notification1, notification2], action)
const resultNotification1 = resultList.find(n => n.event.id === notification1.event.id)
const resultNotification2 = resultList.find(n => n.event.id === notification2.event.id)
expect(resultNotification1.isEmitted).to.be.true
expect(resultNotification2.isEmitted).to.not.be.true
})
it('should add the action buttons', function () {
const buttonActions = {}
const notification1 = { event: {id: 'Event1'} }
const notification2 = { event: {id: 'Event2'} }
const action = {
type: actions.NOTIFICATION_EMITTED,
eventId: notification1.event.id,
buttonActions
}
const resultList = reducer.notificationListReducer([notification1, notification2], action)
const resultNotification1 = resultList.find(n => n.event.id === notification1.event.id)
const resultNotification2 = resultList.find(n => n.event.id === notification2.event.id)
expect(resultNotification1.buttonActions).to.equal(buttonActions)
expect(resultNotification2.buttonActions).to.not.equal(buttonActions)
})
})
context('when notification clicked', function () {
it('should set isClicked to true', function () {
const notification1 = { notificationId: 'Notification1' }
const notification2 = { notificationId: 'Notification2' }
const action = {
type: actions.NOTIFICATION_CLICKED,
notificationId: notification1.notificationId,
buttonIndex: 0
}
const resultList = reducer.notificationListReducer([notification1, notification2], action)
const resultNotification1 = resultList.find(n => n.notificationId === notification1.notificationId)
const resultNotification2 = resultList.find(n => n.notificationId === notification2.notificationId)
expect(resultNotification1.isClicked).to.be.true
expect(resultNotification2.isClicked).to.not.be.true
})
it('should set the proper button index', function () {
const notification1 = { notificationId: 'Notification1' }
const notification2 = { notificationId: 'Notification2' }
const action1 = {
type: actions.NOTIFICATION_CLICKED,
notificationId: notification1.notificationId,
buttonIndex: 0
}
const action2 = {
type: actions.NOTIFICATION_CLICKED,
notificationId: notification2.notificationId,
buttonIndex: 1
}
const resultList1 = reducer.notificationListReducer([notification1, notification2], action1)
const resultList2 = reducer.notificationListReducer(resultList1, action2)
const result1Notification1 = resultList1.find(n => n.notificationId === notification1.notificationId)
const result1Notification2 = resultList1.find(n => n.notificationId === notification2.notificationId)
const result2Notification1 = resultList2.find(n => n.notificationId === notification1.notificationId)
const result2Notification2 = resultList2.find(n => n.notificationId === notification2.notificationId)
expect(result1Notification1.buttonIndex).to.equal(action1.buttonIndex)
expect(result1Notification2.buttonIndex).to.be.undefined
expect(result2Notification1.buttonIndex).to.equal(action1.buttonIndex)
expect(result2Notification2.buttonIndex).to.equal(action2.buttonIndex)
})
})
context('when notification removed', function () {
it('should be eliminated from the list', function () {
const notification1 = { notificationId: 'Notification1' }
const notification2 = { notificationId: 'Notification2' }
const action = {
type: actions.NOTIFICATION_REMOVED,
notificationId: notification1.notificationId
}
const resultList = reducer.notificationListReducer([notification1, notification2], action)
expect(resultList).to.not.contain(notification1)
expect(resultList).to.contain(notification2)
expect(resultList).to.have.lengthOf(1)
})
})
})
})

Просмотреть файл

@ -0,0 +1,122 @@
import { expect } from 'chai'
import * as service from 'services/notificationService'
import * as actions from 'actions/notificationActions'
/* eslint-disable no-unused-expressions */
describe('Chrome Extension Push Notifications Service', function () {
const originalWindow = global.window
const originalChrome = global.chrome
after(function () {
// Since we are messing with global objects we need to reset them back to the original
// objects to avoid impacting other test suites.
global.window = originalWindow
global.chrome = originalChrome
})
beforeEach(function () {
global.window = {}
global.chrome = {}
})
context('when there is no chrome.extension object', function () {
it('should not be considered a chrome extension', () => {
const isChromeExtensionResult = service.isChromeExtension()
expect(isChromeExtensionResult).to.be.false
})
})
context('when running as a chrome extension', function () {
beforeEach(function () {
global.chrome = { extension: { getBackgroundPage: () => null } }
})
it('should be considered a chrome extension', () => {
const isChromeExtensionResult = service.isChromeExtension()
expect(isChromeExtensionResult).to.be.true
})
context('when running as a chrome extension background', function () {
beforeEach(function () {
global.chrome = {
extension: { getBackgroundPage: () => global.window },
browserAction: { setBadgeBackgroundColor: () => null },
runtime: { onMessage: { addListener: () => null } },
contextMenus: {
onClicked: { addListener: () => null },
create: () => null,
removeAll: () => null
},
notifications: {
onClicked: { addListener: () => null },
onButtonClicked: { addListener: () => null },
onClosed: { addListener: () => null }
}
}
})
it('should be considered a chrome extension background', () => {
const isChromeExtensionBackgroundResult = service.isChromeExtensionBackground()
expect(isChromeExtensionBackgroundResult).to.be.true
})
it('should not be considered a chrome extension popup', () => {
const isChromeExtensionPopupResult = service.isChromeExtensionPopup()
expect(isChromeExtensionPopupResult).to.be.false
})
it('should register chrome event listeners', () => {
let runtimeOnMessageListenerAdded = false
let contextMenusOnClickedListenerAdded = false
let notificationsOnClickedListenerAdded = false
let notificationsOnButtonClickedListenerAdded = false
let notificationsOnClosedListenerAdded = false
global.chrome.runtime.onMessage.addListener = () => { runtimeOnMessageListenerAdded = true }
global.chrome.contextMenus.onClicked.addListener = () => { contextMenusOnClickedListenerAdded = true }
global.chrome.notifications = {
onClicked: { addListener: () => { notificationsOnClickedListenerAdded = true } },
onButtonClicked: { addListener: () => { notificationsOnButtonClickedListenerAdded = true } },
onClosed: { addListener: () => { notificationsOnClosedListenerAdded = true } }
}
service.configureNotificationService()
expect(runtimeOnMessageListenerAdded).to.be.true
expect(contextMenusOnClickedListenerAdded).to.be.true
expect(notificationsOnClickedListenerAdded).to.be.true
expect(notificationsOnButtonClickedListenerAdded).to.be.true
expect(notificationsOnClosedListenerAdded).to.be.true
})
it('should create a context menu option to toggle receive push notifications', function () {
const menuIdsResult = []
global.chrome.contextMenus.create = (options) => { menuIdsResult.push(options.id) }
service.configureNotificationService()
expect(menuIdsResult).to.include(actions.TOGGLE_RECEIVE_PUSH_NOTIFICATIONS)
})
})
context('when running as a chrome extension popup', function () {
beforeEach(function () {
global.chrome = { extension: { getBackgroundPage: () => {} } }
})
it('should be considered a chrome extension popup', () => {
const isChromeExtensionPopupResult = service.isChromeExtensionPopup()
expect(isChromeExtensionPopupResult).to.be.true
})
it('should not be considered a chrome extension background', () => {
const isChromeExtensionBackgroundResult = service.isChromeExtensionBackground()
expect(isChromeExtensionBackgroundResult).to.be.false
})
it('should send a popupOnLoad message', () => {
let msgIdResults = []
global.chrome.runtime = { sendMessage: msg => { msgIdResults.push(msg.messageId) } }
service.configureNotificationService()
expect(msgIdResults).to.include(service.extensionMessages.popupOnLoad)
})
})
})
})