├── codecov.yml ├── public ├── favicon.ico ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── manifest.json └── index.html ├── .travis.yml ├── flow-typed ├── npm │ ├── flow-bin_v0.x.x.js │ ├── eslint-plugin-prettier_vx.x.x.js │ ├── redux-thunk_vx.x.x.js │ ├── eslint-config-prettier_vx.x.x.js │ ├── redux-logger_vx.x.x.js │ ├── prettier_vx.x.x.js │ ├── gh-pages_vx.x.x.js │ ├── react-redux_v5.x.x.js │ ├── redux_v3.x.x.js │ ├── bootstrap_vx.x.x.js │ ├── react-scripts_vx.x.x.js │ ├── eslint-plugin-promise_vx.x.x.js │ ├── flow-typed_vx.x.x.js │ ├── eslint-plugin-node_vx.x.x.js │ ├── eslint-plugin-import_vx.x.x.js │ └── eslint-plugin-flowtype_vx.x.x.js └── overrides │ ├── photon-ant.js │ ├── Notification.js │ └── Window.js ├── .flowconfig ├── update_version.sh ├── config-overrides.js ├── .gitignore ├── src ├── index.js ├── PollbotAPI.js ├── create-store.js ├── App.css ├── PollbotAPI.test.js ├── reducers.js ├── auth0.js ├── __snapshots__ │ └── App.test.js.snap ├── registerServiceWorker.js ├── actions.js ├── types.js ├── sagas.js ├── auth0.test.js ├── actions.test.js ├── reducers.test.js ├── App.js └── sagas.test.js ├── .eslintrc.js ├── README.md ├── package.json └── LICENSE /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | false 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/delivery-dashboard/master/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/delivery-dashboard/master/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/delivery-dashboard/master/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/delivery-dashboard/master/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/delivery-dashboard/master/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | script: 9 | - yarn run check 10 | - yarn run flow-coverage 11 | - yarn run test-coverage && codecov 12 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*node_modules/photon-ant.* 3 | .*node_modules/flow-coverage-report.* 4 | .*node_modules/draft-js.* 5 | .*node_modules/rc-editor-core.* 6 | 7 | [include] 8 | 9 | [libs] 10 | 11 | [lints] 12 | 13 | [options] 14 | -------------------------------------------------------------------------------- /flow-typed/overrides/photon-ant.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // The `photon-ant` NPM package does not have any flow types associated with it, 4 | // defining this module here prevents `flow` from throwing errors when running. 5 | 6 | declare module 'photon-ant' { 7 | declare module.exports: any; 8 | } 9 | -------------------------------------------------------------------------------- /update_version.sh: -------------------------------------------------------------------------------- 1 | NAME="Delivery Dashboard" 2 | SOURCE=$(git config remote.origin.url | sed -e 's|git@|https://|g' | sed -e 's|github.com:|github.com/|g') 3 | VERSION=$(git describe --always --tag) 4 | COMMIT=$(git log --pretty=format:'%H' -n 1) 5 | echo "{\"name\":\"${NAME}\",\"version\":\"${VERSION}\",\"source\":\"${SOURCE}\",\"commit\":\"${COMMIT}\"}" > src/version.json 6 | 7 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const {injectBabelPlugin} = require('react-app-rewired'); 2 | const rewireLess = require('react-app-rewire-less'); 3 | 4 | // eslint-disable-next-line 5 | module.exports = function override(config, env) { 6 | config = injectBabelPlugin( 7 | ['import', {libraryName: 'antd', style: true}], 8 | config, 9 | ); 10 | 11 | config = rewireLess(config, env); 12 | 13 | return config; 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | src/version.json 4 | 5 | # dependencies 6 | /node_modules 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # flow 15 | /flow-coverage 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ConnectedApp from './App'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | import {Provider} from 'react-redux'; 7 | import createStore from './create-store'; 8 | 9 | const root = document && document.getElementById('root'); 10 | 11 | if (root) { 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | root, 17 | ); 18 | registerServiceWorker(); 19 | } 20 | -------------------------------------------------------------------------------- /flow-typed/overrides/Notification.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // This file will be automagically used by Flow to type "Notification". 4 | declare class Notification { 5 | constructor(string, ?Object): Notification, 6 | permission: 'denied' | 'granted' | 'default', 7 | actions: string[], 8 | badge: string, 9 | body: string, 10 | data: Object, 11 | dir: 'ltr' | 'rtl', 12 | lang: string, 13 | tag: string, 14 | icon: string, 15 | image: string, 16 | requireInteraction: boolean, 17 | silent: boolean, 18 | timestamp: number, 19 | title: string, 20 | vibrate: number[], 21 | onclick(event: Event): void, 22 | onerror(event: Event): void, 23 | static requestPermission(): Promise<*>, 24 | static close(): void, 25 | } 26 | -------------------------------------------------------------------------------- /flow-typed/overrides/Window.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // This file will be automagically used by Flow to type "window". 4 | type Location = { 5 | href: string, 6 | protocol: string, 7 | host: string, 8 | hostname: string, 9 | port: string, 10 | username: string, 11 | password: string, 12 | origin: string, 13 | pathname: string, 14 | search: string, 15 | hash: string, 16 | assign: (url: string) => void, 17 | reload: () => void, 18 | replace: (url: string) => void, 19 | toString: () => string, 20 | }; 21 | 22 | declare class Window extends EventTarget { 23 | innerHeight: number, 24 | innerWidth: number, 25 | location: Location, 26 | onhashchange: (event: Event) => void, 27 | } 28 | 29 | declare var window: Window; 30 | -------------------------------------------------------------------------------- /src/PollbotAPI.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | APIVersionData, 4 | CheckResult, 5 | ProductVersions, 6 | Product, 7 | ReleaseInfo, 8 | } from './types'; 9 | 10 | export const SERVER = 'https://pollbot.dev.mozaws.net/v1'; 11 | 12 | export async function getOngoingVersions( 13 | product: Product, 14 | ): Promise { 15 | const response = await fetch(`${SERVER}/${product}/ongoing-versions`); 16 | return response.json(); 17 | } 18 | 19 | export async function getReleaseInfo( 20 | product: Product, 21 | version: string, 22 | ): Promise { 23 | const response = await fetch(`${SERVER}/${product}/${version}`); 24 | return response.json(); 25 | } 26 | 27 | export async function checkStatus(url: string): Promise { 28 | const response = await fetch(url); 29 | return response.json(); 30 | } 31 | 32 | export async function getPollbotVersion(): Promise { 33 | const response = await fetch(`${SERVER}/__version__`); 34 | return response.json(); 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-prettier_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 41fe1aa0010e12c989ea295042c9d2f6 2 | // flow-typed version: <>/eslint-plugin-prettier_v^2.2.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-prettier' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-prettier' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-prettier/eslint-plugin-prettier' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'eslint-plugin-prettier/eslint-plugin-prettier.js' { 31 | declare module.exports: $Exports<'eslint-plugin-prettier/eslint-plugin-prettier'>; 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | parser: 'babel-eslint', 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:react/recommended', 12 | 'plugin:flowtype/recommended', 13 | 'prettier', 14 | 'prettier/flowtype', 15 | 'prettier/react', 16 | ], 17 | parserOptions: { 18 | ecmaVersion: '2017', 19 | ecmaFeatures: { 20 | experimentalObjectRestSpread: true, 21 | jsx: true, 22 | }, 23 | sourceType: 'module', 24 | }, 25 | plugins: ['react', 'import', 'prettier', 'flowtype'], 26 | rules: { 27 | 'prettier/prettier': [ 28 | 'error', 29 | { 30 | singleQuote: true, 31 | trailingComma: 'all', 32 | bracketSpacing: false, 33 | jsxBracketSameLine: false, 34 | parser: 'flow', 35 | }, 36 | ], 37 | 38 | // overriding recommended rules 39 | 'no-console': ['error', {allow: ['log', 'warn', 'error']}], 40 | 'no-unused-vars': [ 41 | 'error', 42 | {argsIgnorePattern: '^_', varsIgnorePattern: '^_'}, 43 | ], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/create-store.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createStore, applyMiddleware, compose} from 'redux'; 4 | import {deliveryDashboard} from './reducers'; 5 | import createSagaMiddleware from 'redux-saga'; 6 | import thunkMiddleware from 'redux-thunk'; 7 | import {createLogger} from 'redux-logger'; 8 | import type {Store} from './types'; 9 | import {rootSaga} from './sagas'; 10 | 11 | /** 12 | * Isolate the store creation into a function, so that it can be used outside of the 13 | * app's execution context, e.g. for testing. 14 | * @return {object} Redux store. 15 | */ 16 | export default function initializeStore(): Store { 17 | const loggerMiddleware = createLogger(); 18 | const sagaMiddleware = createSagaMiddleware(); 19 | // This is the middleware needed for the redux-devtools extension. 20 | const composeEnhancers = 21 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 22 | 23 | let store: Store = createStore( 24 | deliveryDashboard, 25 | // $FlowFixMe 26 | composeEnhancers( 27 | applyMiddleware( 28 | sagaMiddleware, 29 | thunkMiddleware, // lets us dispatch() functions 30 | loggerMiddleware, // neat middleware that logs actions 31 | ), 32 | ), 33 | ); 34 | sagaMiddleware.run(rootSaga); 35 | 36 | return store; 37 | } 38 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | 23 | Delivery Dashboard 24 | 25 | 26 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delivery Dashboard 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/mozilla/delivery-dashboard.svg)](https://greenkeeper.io/) 4 | 5 | Product Delivery's Web Client to Microservices. 6 | 7 | The Product Delivery team is implementing various Microservices such as 8 | [PollBot](https://github.com/mozilla/PollBot), and this dashboard aims at 9 | displaying information from those in a centralized place. 10 | 11 | ## Table of Contents 12 | 13 | - [Cloning and getting into the Project Dir](#cloning-and-getting-into-the-project-dir) 14 | - [Setting up the development environment](#setting-up-the-development-environment) 15 | - [Starting the dev server](#starting-the-dev-server) 16 | - [Building](#building) 17 | - [Deploying to gh-pages](#deploying-to-gh-pages) 18 | - [Launching testsuite](#launching-testsuite) 19 | 20 | 21 | 22 | ## Cloning and getting into the Project Dir 23 | 24 | To clone this repository simply type 25 | 26 | $ git clone https://github.com/mozilla/delivery-dashboard && cd delivery-dashboard 27 | $ cd delivery-dashboard 28 | 29 | ## Setting up the development environment 30 | 31 | You would need to install `yarn`. You can use: 32 | 33 | $ npm install -g yarn 34 | 35 | You can then use `yarn` to install the project dependencies: 36 | 37 | $ yarn install 38 | 39 | ## Starting the dev server 40 | 41 | To start the dev server type the following command: 42 | 43 | $ yarn start 44 | 45 | ### Building 46 | 47 | To build this app, type the command below: 48 | 49 | $ yarn build 50 | 51 | ### Deploying to gh-pages 52 | 53 | $ yarn deploy 54 | 55 | The app should be deployed to 56 | https://[your-github-username].github.io/delivery-dashboard/ 57 | 58 | ## Launching testsuite 59 | 60 | To run the testsuite, simply type: 61 | 62 | $ yarn test 63 | -------------------------------------------------------------------------------- /flow-typed/npm/redux-thunk_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f4c4b4a278e8c8646bb2a10775fb0345 2 | // flow-typed version: <>/redux-thunk_v^2.2.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'redux-thunk' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'redux-thunk' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'redux-thunk/dist/redux-thunk' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'redux-thunk/dist/redux-thunk.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'redux-thunk/es/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'redux-thunk/lib/index' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'redux-thunk/src/index' { 42 | declare module.exports: any; 43 | } 44 | 45 | // Filename aliases 46 | declare module 'redux-thunk/dist/redux-thunk.js' { 47 | declare module.exports: $Exports<'redux-thunk/dist/redux-thunk'>; 48 | } 49 | declare module 'redux-thunk/dist/redux-thunk.min.js' { 50 | declare module.exports: $Exports<'redux-thunk/dist/redux-thunk.min'>; 51 | } 52 | declare module 'redux-thunk/es/index.js' { 53 | declare module.exports: $Exports<'redux-thunk/es/index'>; 54 | } 55 | declare module 'redux-thunk/lib/index.js' { 56 | declare module.exports: $Exports<'redux-thunk/lib/index'>; 57 | } 58 | declare module 'redux-thunk/src/index.js' { 59 | declare module.exports: $Exports<'redux-thunk/src/index'>; 60 | } 61 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-config-prettier_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 1d4364bee4d0fb8598a0097fed1e3bb3 2 | // flow-typed version: <>/eslint-config-prettier_v^2.3.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-config-prettier' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-config-prettier' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-config-prettier/bin/cli' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-config-prettier/bin/validators' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-config-prettier/flowtype' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-config-prettier/react' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'eslint-config-prettier/bin/cli.js' { 43 | declare module.exports: $Exports<'eslint-config-prettier/bin/cli'>; 44 | } 45 | declare module 'eslint-config-prettier/bin/validators.js' { 46 | declare module.exports: $Exports<'eslint-config-prettier/bin/validators'>; 47 | } 48 | declare module 'eslint-config-prettier/flowtype.js' { 49 | declare module.exports: $Exports<'eslint-config-prettier/flowtype'>; 50 | } 51 | declare module 'eslint-config-prettier/index' { 52 | declare module.exports: $Exports<'eslint-config-prettier'>; 53 | } 54 | declare module 'eslint-config-prettier/index.js' { 55 | declare module.exports: $Exports<'eslint-config-prettier'>; 56 | } 57 | declare module 'eslint-config-prettier/react.js' { 58 | declare module.exports: $Exports<'eslint-config-prettier/react'>; 59 | } 60 | -------------------------------------------------------------------------------- /flow-typed/npm/redux-logger_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 1c32d4b78525e3795df9e08bea020537 2 | // flow-typed version: <>/redux-logger_v^3.0.6/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'redux-logger' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'redux-logger' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'redux-logger/dist/redux-logger' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'redux-logger/src/core' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'redux-logger/src/defaults' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'redux-logger/src/diff' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'redux-logger/src/helpers' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'redux-logger/src/index' { 46 | declare module.exports: any; 47 | } 48 | 49 | // Filename aliases 50 | declare module 'redux-logger/dist/redux-logger.js' { 51 | declare module.exports: $Exports<'redux-logger/dist/redux-logger'>; 52 | } 53 | declare module 'redux-logger/src/core.js' { 54 | declare module.exports: $Exports<'redux-logger/src/core'>; 55 | } 56 | declare module 'redux-logger/src/defaults.js' { 57 | declare module.exports: $Exports<'redux-logger/src/defaults'>; 58 | } 59 | declare module 'redux-logger/src/diff.js' { 60 | declare module.exports: $Exports<'redux-logger/src/diff'>; 61 | } 62 | declare module 'redux-logger/src/helpers.js' { 63 | declare module.exports: $Exports<'redux-logger/src/helpers'>; 64 | } 65 | declare module 'redux-logger/src/index.js' { 66 | declare module.exports: $Exports<'redux-logger/src/index'>; 67 | } 68 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: inherit; 3 | min-height: 100%; 4 | position: relative; 5 | width: inherit; 6 | } 7 | 8 | body { 9 | background-color: #ececec; 10 | margin-bottom: 54px; 11 | } 12 | 13 | header { 14 | align-items: center; 15 | background-color: #fff; 16 | display: flex; 17 | height: 80px; 18 | justify-content: space-between; 19 | margin-bottom: 24px; 20 | padding: 0 48px; 21 | } 22 | 23 | header h1 { 24 | margin: 0; 25 | } 26 | 27 | .ant-layout-sider { 28 | background-color: #fff; 29 | border-right: none; 30 | } 31 | 32 | .ant-layout-sider li { 33 | padding: 5px; 34 | } 35 | 36 | .ant-layout-content { 37 | padding-left: 20px; 38 | border-left: 1px solid #e9e9e9; 39 | } 40 | 41 | .mainContent { 42 | background-color: #fff; 43 | border-radius: 4px; 44 | margin: 0 48px; 45 | margin-bottom: 24px; 46 | padding: 24px; 47 | } 48 | 49 | /* List of check results for a given release */ 50 | .dashboard { 51 | display: grid; 52 | grid-template-columns: repeat(auto-fit, minmax(30%, 1fr)); 53 | grid-gap: 20px; 54 | } 55 | 56 | .ant-card .ant-card-body { 57 | word-wrap: break-word; 58 | } 59 | 60 | 61 | footer { 62 | background-color: #fff; 63 | bottom: 0; 64 | height: 30px; 65 | line-height: 30px; 66 | position: absolute; 67 | text-align: center; 68 | width: 100%; 69 | } 70 | 71 | 72 | @media (max-width: 992px) { 73 | .user { 74 | right: 5px; 75 | } 76 | 77 | .dashboard { 78 | display: block; 79 | } 80 | 81 | .mainContent { 82 | border-radius: 0; 83 | margin: 0; 84 | padding: 5px; 85 | } 86 | 87 | .ant-card { 88 | margin-bottom: 5px; 89 | } 90 | 91 | .ant-layout-sider { 92 | background-color: #fff; 93 | border-right: 1px solid #e9e9e9; 94 | left: -5px; 95 | top: -110px; 96 | } 97 | 98 | .ant-layout-sider .menu { 99 | padding: 5px; 100 | } 101 | 102 | .ant-layout-sider-zero-width-trigger { 103 | top: 20px; 104 | } 105 | 106 | .ant-layout-content { 107 | padding-left: 0; 108 | border-left: none; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "2.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "eslint-plugin-react": "^7.3.0", 7 | "gh-pages": "^1.0.0", 8 | "photon-ant": "0.1.4", 9 | "react": "^16.0.0", 10 | "react-app-rewire-less": "^2.1.0", 11 | "react-dom": "^16.0.0", 12 | "react-redux": "^5.0.6", 13 | "react-scripts": "1.1.0", 14 | "redux": "^3.7.2", 15 | "redux-logger": "^3.0.6", 16 | "redux-saga": "^0.16.0", 17 | "redux-thunk": "^2.2.0", 18 | "yarn": "^1.2.1" 19 | }, 20 | "homepage": "https://mozilla.github.io/delivery-dashboard/", 21 | "homepage_dev": "https://mozilla.github.io/delivery-dashboard/dev/", 22 | "scripts": { 23 | "start": "react-app-rewired start", 24 | "prestart": "yarn run version-file", 25 | "build": "react-app-rewired build", 26 | "build:readme": "toctoc -w -d 2 README.md", 27 | "prebuild": "yarn run version-file", 28 | "test": "NODE_ENV=test react-app-rewired test --env=jsdom --silent", 29 | "test-coverage": "yarn run test --coverage --collectCoverageFrom=src/**/*js --collectCoverageFrom=!src/index.js --collectCoverageFrom=!src/registerServiceWorker.js", 30 | "eject": "react-app-rewired eject", 31 | "deploy": "yarn run build && gh-pages --add --dist build/", 32 | "deploy-dev": "PUBLIC_URL=$npm_package_homepage_dev yarn run deploy --dest dev/", 33 | "lint": "eslint *.js src", 34 | "lint-fix": "yarn lint --fix", 35 | "flow": "flow", 36 | "flow-coverage": "flow-coverage-report -i 'src/**/*.js' -x 'src/**/*.test.js' -t html -t text", 37 | "check": "yarn lint && yarn flow", 38 | "precheck": "yarn run version-file", 39 | "version-file": "./update_version.sh" 40 | }, 41 | "devDependencies": { 42 | "babel-plugin-import": "^1.5.0", 43 | "codecov": "^3.0.0", 44 | "enzyme": "^3.0.0", 45 | "enzyme-adapter-react-16": "^1.0.0", 46 | "eslint": "^4.4.1", 47 | "eslint-config-prettier": "^2.3.0", 48 | "eslint-plugin-flowtype": "^2.35.0", 49 | "eslint-plugin-import": "^2.7.0", 50 | "eslint-plugin-node": "^5.1.1", 51 | "eslint-plugin-prettier": "^2.2.0", 52 | "eslint-plugin-promise": "^3.5.0", 53 | "flow-bin": "^0.64.0", 54 | "flow-coverage-report": "^0.4.0", 55 | "flow-typed": "^2.1.5", 56 | "prettier": "^1.5.3", 57 | "react-app-rewired": "^1.2.9", 58 | "react-test-renderer": "^16.0.0", 59 | "redux-mock-store": "^1.3.0", 60 | "toctoc": "^0.2.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/PollbotAPI.test.js: -------------------------------------------------------------------------------- 1 | /* Functional tests of the Pollbot API */ 2 | 3 | import { 4 | checkStatus, 5 | getPollbotVersion, 6 | getOngoingVersions, 7 | getReleaseInfo, 8 | } from './PollbotAPI'; 9 | 10 | describe('getOngoingVersions', () => { 11 | it('retrieves the list of ongoing versions', async () => { 12 | const channelVersions = await getOngoingVersions('firefox'); 13 | expect(channelVersions).toMatchObject({ 14 | beta: expect.any(String), 15 | esr: expect.any(String), 16 | nightly: expect.any(String), 17 | release: expect.any(String), 18 | }); 19 | }); 20 | }); 21 | 22 | describe('getReleaseInfo', () => { 23 | it('retrieves the release information for firefox', async () => { 24 | const releaseInfo = await getReleaseInfo('firefox', '50.0'); 25 | expect(releaseInfo).toMatchObject({ 26 | channel: expect.stringMatching(/nightly|beta|release|esr/), 27 | checks: expect.any(Array), 28 | product: 'firefox', 29 | version: '50.0', 30 | }); 31 | releaseInfo.checks.map(check => { 32 | expect(check).toMatchObject({ 33 | title: expect.any(String), 34 | url: expect.any(String), 35 | }); 36 | }); 37 | }); 38 | it('retrieves the release information for devedition', async () => { 39 | const releaseInfo = await getReleaseInfo('devedition', '59.0b3'); 40 | expect(releaseInfo).toMatchObject({ 41 | channel: expect.stringMatching(/nightly|beta|release|esr|aurora/), 42 | checks: expect.any(Array), 43 | product: 'devedition', 44 | version: '59.0b3', 45 | }); 46 | releaseInfo.checks.map(check => { 47 | expect(check).toMatchObject({ 48 | title: expect.any(String), 49 | url: expect.any(String), 50 | }); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('checkStatus', () => { 56 | it('retrieves the status of a given check', async () => { 57 | const status = await checkStatus( 58 | 'https://pollbot.dev.mozaws.net/v1/firefox/50.0/product-details', 59 | ); 60 | expect(status).toEqual({ 61 | link: 'https://product-details.mozilla.org/1.0/firefox.json', 62 | status: 'exists', 63 | message: 'We found product-details information about version 50.0', 64 | }); 65 | }); 66 | }); 67 | 68 | describe('getPollbotVersion', () => { 69 | it('retrieves the version from Pollbot', async () => { 70 | const version = await getPollbotVersion(); 71 | expect(version).toMatchObject({ 72 | commit: expect.any(String), 73 | name: 'pollbot', 74 | source: expect.any(String), 75 | version: expect.any(String), 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /flow-typed/npm/prettier_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: fcaaf74b7566b8ad352783ee583ce4ce 2 | // flow-typed version: <>/prettier_v^1.5.3/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'prettier' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'prettier' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'prettier/bin/prettier' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'prettier/parser-babylon' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'prettier/parser-flow' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'prettier/parser-graphql' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'prettier/parser-json' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'prettier/parser-parse5' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'prettier/parser-postcss' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'prettier/parser-typescript' { 54 | declare module.exports: any; 55 | } 56 | 57 | // Filename aliases 58 | declare module 'prettier/bin/prettier.js' { 59 | declare module.exports: $Exports<'prettier/bin/prettier'>; 60 | } 61 | declare module 'prettier/index' { 62 | declare module.exports: $Exports<'prettier'>; 63 | } 64 | declare module 'prettier/index.js' { 65 | declare module.exports: $Exports<'prettier'>; 66 | } 67 | declare module 'prettier/parser-babylon.js' { 68 | declare module.exports: $Exports<'prettier/parser-babylon'>; 69 | } 70 | declare module 'prettier/parser-flow.js' { 71 | declare module.exports: $Exports<'prettier/parser-flow'>; 72 | } 73 | declare module 'prettier/parser-graphql.js' { 74 | declare module.exports: $Exports<'prettier/parser-graphql'>; 75 | } 76 | declare module 'prettier/parser-json.js' { 77 | declare module.exports: $Exports<'prettier/parser-json'>; 78 | } 79 | declare module 'prettier/parser-parse5.js' { 80 | declare module.exports: $Exports<'prettier/parser-parse5'>; 81 | } 82 | declare module 'prettier/parser-postcss.js' { 83 | declare module.exports: $Exports<'prettier/parser-postcss'>; 84 | } 85 | declare module 'prettier/parser-typescript.js' { 86 | declare module.exports: $Exports<'prettier/parser-typescript'>; 87 | } 88 | -------------------------------------------------------------------------------- /src/reducers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | ADD_CHECK_RESULT, 5 | REFRESH_CHECK_RESULT, 6 | ADD_SERVER_ERROR, 7 | SET_VERSION, 8 | UPDATE_PRODUCT_VERSIONS, 9 | UPDATE_POLLBOT_VERSION, 10 | UPDATE_RELEASE_INFO, 11 | LOGGED_IN, 12 | LOGGED_OUT, 13 | LOGIN_REQUESTED, 14 | UPDATE_USER_INFO, 15 | } from './types'; 16 | import type {Action, State} from './types'; 17 | 18 | export const initialState: State = { 19 | version: ['firefox', ''], 20 | productVersions: {firefox: {}, devedition: {}}, 21 | releaseInfo: null, 22 | checkResults: {}, 23 | pollbotVersion: null, 24 | shouldRefresh: false, 25 | login: LOGGED_OUT, 26 | userInfo: null, 27 | errors: [], 28 | }; 29 | 30 | export function deliveryDashboard( 31 | state: State = initialState, 32 | action: Action, 33 | ): State { 34 | let errors; 35 | let updatedCheckResults; 36 | switch (action.type) { 37 | case ADD_CHECK_RESULT: 38 | return Object.assign({}, state, { 39 | checkResults: Object.assign({}, state.checkResults, { 40 | [action.title]: action.result, 41 | }), 42 | shouldRefresh: 43 | action.result.status !== 'exists' ? true : state.shouldRefresh, 44 | }); 45 | case REFRESH_CHECK_RESULT: 46 | updatedCheckResults = Object.assign({}, state.checkResults); 47 | delete updatedCheckResults[action.title]; 48 | return Object.assign({}, state, { 49 | checkResults: updatedCheckResults, 50 | }); 51 | case ADD_SERVER_ERROR: 52 | errors = state.errors.slice(); 53 | errors.push([action.title, action.err]); 54 | return Object.assign({}, state, { 55 | errors: errors, 56 | shouldRefresh: true, 57 | }); 58 | case SET_VERSION: 59 | return Object.assign({}, state, { 60 | version: [action.product, action.version], 61 | checkResults: {}, 62 | shouldRefresh: false, 63 | errors: [], 64 | }); 65 | case UPDATE_PRODUCT_VERSIONS: 66 | return Object.assign({}, state, { 67 | productVersions: Object.assign({}, state.productVersions, { 68 | [action.product]: action.versions, 69 | }), 70 | }); 71 | case UPDATE_RELEASE_INFO: 72 | return Object.assign({}, state, { 73 | releaseInfo: action.releaseInfo, 74 | }); 75 | case UPDATE_POLLBOT_VERSION: 76 | return Object.assign({}, state, { 77 | pollbotVersion: action.version, 78 | }); 79 | case LOGGED_IN: 80 | return Object.assign({}, state, { 81 | login: LOGGED_IN, 82 | }); 83 | case LOGGED_OUT: 84 | return Object.assign({}, state, { 85 | login: LOGGED_OUT, 86 | userInfo: null, 87 | }); 88 | case LOGIN_REQUESTED: 89 | return Object.assign({}, state, { 90 | login: LOGIN_REQUESTED, 91 | }); 92 | case UPDATE_USER_INFO: 93 | return Object.assign({}, state, { 94 | userInfo: action.userInfo, 95 | }); 96 | default: 97 | return state; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /flow-typed/npm/gh-pages_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: bba8024b0bcc3d1e872576fbd75e5521 2 | // flow-typed version: <>/gh-pages_v^1.0.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'gh-pages' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'gh-pages' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'gh-pages/lib/git' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'gh-pages/lib/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'gh-pages/lib/util' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'gh-pages/plugin' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'gh-pages/test/bin/gh-pages.spec' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'gh-pages/test/helper' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'gh-pages/test/integration/basic.spec' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'gh-pages/test/integration/dest.spec' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'gh-pages/test/integration/fixtures/include/expected/good' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'gh-pages/test/integration/fixtures/include/local/good' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'gh-pages/test/integration/include.spec' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'gh-pages/test/lib/util.spec' { 70 | declare module.exports: any; 71 | } 72 | 73 | // Filename aliases 74 | declare module 'gh-pages/lib/git.js' { 75 | declare module.exports: $Exports<'gh-pages/lib/git'>; 76 | } 77 | declare module 'gh-pages/lib/index.js' { 78 | declare module.exports: $Exports<'gh-pages/lib/index'>; 79 | } 80 | declare module 'gh-pages/lib/util.js' { 81 | declare module.exports: $Exports<'gh-pages/lib/util'>; 82 | } 83 | declare module 'gh-pages/plugin.js' { 84 | declare module.exports: $Exports<'gh-pages/plugin'>; 85 | } 86 | declare module 'gh-pages/test/bin/gh-pages.spec.js' { 87 | declare module.exports: $Exports<'gh-pages/test/bin/gh-pages.spec'>; 88 | } 89 | declare module 'gh-pages/test/helper.js' { 90 | declare module.exports: $Exports<'gh-pages/test/helper'>; 91 | } 92 | declare module 'gh-pages/test/integration/basic.spec.js' { 93 | declare module.exports: $Exports<'gh-pages/test/integration/basic.spec'>; 94 | } 95 | declare module 'gh-pages/test/integration/dest.spec.js' { 96 | declare module.exports: $Exports<'gh-pages/test/integration/dest.spec'>; 97 | } 98 | declare module 'gh-pages/test/integration/fixtures/include/expected/good.js' { 99 | declare module.exports: $Exports<'gh-pages/test/integration/fixtures/include/expected/good'>; 100 | } 101 | declare module 'gh-pages/test/integration/fixtures/include/local/good.js' { 102 | declare module.exports: $Exports<'gh-pages/test/integration/fixtures/include/local/good'>; 103 | } 104 | declare module 'gh-pages/test/integration/include.spec.js' { 105 | declare module.exports: $Exports<'gh-pages/test/integration/include.spec'>; 106 | } 107 | declare module 'gh-pages/test/lib/util.spec.js' { 108 | declare module.exports: $Exports<'gh-pages/test/lib/util.spec'>; 109 | } 110 | -------------------------------------------------------------------------------- /flow-typed/npm/react-redux_v5.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f0d96df48e9abc14bcc1405ba2a47dde 2 | // flow-typed version: 83053e4020/react-redux_v5.x.x/flow_>=v0.53.x 3 | 4 | // flow-typed signature: 8db7b853f57c51094bf0ab8b2650fd9c 5 | // flow-typed version: ab8db5f14d/react-redux_v5.x.x/flow_>=v0.30.x 6 | 7 | import type { Dispatch, Store } from "redux"; 8 | 9 | declare module "react-redux" { 10 | /* 11 | 12 | S = State 13 | A = Action 14 | OP = OwnProps 15 | SP = StateProps 16 | DP = DispatchProps 17 | 18 | */ 19 | 20 | declare type MapStateToProps = ( 21 | state: S, 22 | ownProps: OP 23 | ) => ((state: S, ownProps: OP) => SP) | SP; 24 | 25 | declare type MapDispatchToProps = 26 | | ((dispatch: Dispatch, ownProps: OP) => DP) 27 | | DP; 28 | 29 | declare type MergeProps = ( 30 | stateProps: SP, 31 | dispatchProps: DP, 32 | ownProps: OP 33 | ) => P; 34 | 35 | declare type Context = { store: Store<*, *> }; 36 | 37 | declare class ConnectedComponent extends React$Component { 38 | static WrappedComponent: Class>, 39 | getWrappedInstance(): React$Component

, 40 | props: OP, 41 | state: void 42 | } 43 | 44 | declare type ConnectedComponentClass = Class< 45 | ConnectedComponent 46 | >; 47 | 48 | declare type Connector = ( 49 | component: React$ComponentType

50 | ) => ConnectedComponentClass; 51 | 52 | declare class Provider extends React$Component<{ 53 | store: Store, 54 | children?: any 55 | }> {} 56 | 57 | declare function createProvider( 58 | storeKey?: string, 59 | subKey?: string 60 | ): Provider<*, *>; 61 | 62 | declare type ConnectOptions = { 63 | pure?: boolean, 64 | withRef?: boolean 65 | }; 66 | 67 | declare type Null = null | void; 68 | 69 | declare function connect( 70 | ...rest: Array // <= workaround for https://github.com/facebook/flow/issues/2360 71 | ): Connector } & OP>>; 72 | 73 | declare function connect( 74 | mapStateToProps: Null, 75 | mapDispatchToProps: Null, 76 | mergeProps: Null, 77 | options: ConnectOptions 78 | ): Connector } & OP>>; 79 | 80 | declare function connect( 81 | mapStateToProps: MapStateToProps, 82 | mapDispatchToProps: Null, 83 | mergeProps: Null, 84 | options?: ConnectOptions 85 | ): Connector } & OP>>; 86 | 87 | declare function connect( 88 | mapStateToProps: Null, 89 | mapDispatchToProps: MapDispatchToProps, 90 | mergeProps: Null, 91 | options?: ConnectOptions 92 | ): Connector>; 93 | 94 | declare function connect( 95 | mapStateToProps: MapStateToProps, 96 | mapDispatchToProps: MapDispatchToProps, 97 | mergeProps: Null, 98 | options?: ConnectOptions 99 | ): Connector>; 100 | 101 | declare function connect( 102 | mapStateToProps: MapStateToProps, 103 | mapDispatchToProps: Null, 104 | mergeProps: MergeProps, 105 | options?: ConnectOptions 106 | ): Connector; 107 | 108 | declare function connect( 109 | mapStateToProps: MapStateToProps, 110 | mapDispatchToProps: MapDispatchToProps, 111 | mergeProps: MergeProps, 112 | options?: ConnectOptions 113 | ): Connector; 114 | } 115 | -------------------------------------------------------------------------------- /src/auth0.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var auth0: any; 4 | 5 | export const AUTH0_CLIENT_ID = 'WYRYpJyS5DnDyxLTRVGCQGCWGo2KNQLN'; 6 | export const AUTH0_DOMAIN = 'minimal-demo-iam.auth0.com'; 7 | export const AUTH0_CALLBACK_URL = window.location.href; 8 | 9 | export type AuthResult = { 10 | accessToken: string, 11 | idToken: string, 12 | expiresIn: number, 13 | }; 14 | 15 | export type HashParser = (() => void, string, AuthResult) => void; 16 | 17 | export type WebAuth = { 18 | parseHash: ((string, AuthResult) => void) => void, 19 | authorize: () => void, 20 | client: { 21 | userInfo: (string, (string, AuthResult) => void) => void, 22 | }, 23 | }; 24 | 25 | export type UserInfo = any; 26 | 27 | export function webAuthHandler( 28 | callback: AuthResult => void, 29 | err: string, 30 | authResult: AuthResult, 31 | ) { 32 | if (err) { 33 | throw new Error(err); 34 | } 35 | if (authResult && authResult.accessToken && authResult.idToken) { 36 | window.location.hash = ''; 37 | setSession(authResult); 38 | if (callback) { 39 | callback(authResult); 40 | } 41 | } 42 | } 43 | 44 | export function initWebAuth() { 45 | const webAuth = new auth0.WebAuth({ 46 | domain: AUTH0_DOMAIN, 47 | clientID: AUTH0_CLIENT_ID, 48 | redirectUri: AUTH0_CALLBACK_URL, 49 | audience: 'http://minimal-demo-iam.localhost:8000', // 'https://' + AUTH0_DOMAIN + '/userinfo', 50 | responseType: 'token id_token', 51 | scope: 'openid profile', 52 | }); 53 | return webAuth; 54 | } 55 | 56 | export function setSession(authResult: AuthResult) { 57 | // Set the time that the access token will expire at. 58 | const expiresAt = JSON.stringify( 59 | authResult.expiresIn * 1000 + new Date().getTime(), 60 | ); 61 | localStorage.setItem('session', JSON.stringify(authResult)); 62 | localStorage.setItem('expires_at', expiresAt); 63 | } 64 | 65 | export function login(initFunc: () => WebAuth = initWebAuth) { 66 | const webAuth = initFunc(); 67 | webAuth.authorize(); 68 | } 69 | 70 | export function logout() { 71 | // Remove tokens and expiry time from localStorage. 72 | localStorage.removeItem('session'); 73 | localStorage.removeItem('expires_at'); 74 | } 75 | 76 | // Check if the user has logged in. 77 | export function checkLogin( 78 | onLoggedIn: () => void, 79 | initFunc: () => WebAuth = initWebAuth, 80 | handler: HashParser = webAuthHandler, 81 | ) { 82 | try { 83 | const webAuth = initFunc(); 84 | const boundHandler = handler.bind(null, onLoggedIn); 85 | webAuth.parseHash(boundHandler); 86 | } catch (err) { 87 | console.error('Login failed', err); 88 | } 89 | } 90 | 91 | export function isAuthenticated() { 92 | // Check whether the current time is past the access token's expiry time. 93 | const session = localStorage.getItem('session'); 94 | if (!session) { 95 | return false; 96 | } 97 | const expiresAt = JSON.parse(localStorage.getItem('expires_at') || '0'); 98 | return new Date().getTime() < expiresAt; 99 | } 100 | 101 | export function handleUserInfo( 102 | onUserInfo: UserInfo => void, 103 | err: string, 104 | profile: UserInfo, 105 | ) { 106 | if (err) { 107 | throw new Error(err); 108 | } 109 | if (onUserInfo) { 110 | onUserInfo(profile); 111 | } 112 | } 113 | 114 | export function fetchUserInfo( 115 | callback: UserInfo => void, 116 | initFunc: () => WebAuth = initWebAuth, 117 | ) { 118 | const session = localStorage.getItem('session'); 119 | if (!session) { 120 | return; 121 | } 122 | const auth = JSON.parse(session); 123 | if (!auth.accessToken) { 124 | return; 125 | } 126 | const webAuth = initFunc(); 127 | webAuth.client.userInfo( 128 | auth.accessToken, 129 | handleUserInfo.bind(null, callback), 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /src/__snapshots__/App.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders without crashing 1`] = ` 4 |

5 |
6 |

7 | 10 | Delivery Dashboard 11 | 12 |

13 |
14 |
17 |
28 |
31 |
34 |

35 | Firefox Releases 36 |

37 |
    38 |
  • 39 | 40 | Nightly 41 | : 42 |
    45 | 48 | 49 | 50 | 51 | 52 | 53 |
    54 |
    55 |
  • 56 |
  • 57 | 58 | Beta 59 | : 60 |
    63 | 66 | 67 | 68 | 69 | 70 | 71 |
    72 |
    73 |
  • 74 |
  • 75 | 76 | Devedition 77 | : 78 |
    81 | 84 | 85 | 86 | 87 | 88 | 89 |
    90 |
    91 |
  • 92 |
  • 93 | 94 | Release 95 | : 96 |
    99 | 102 | 103 | 104 | 105 | 106 | 107 |
    108 |
    109 |
  • 110 |
  • 111 | 112 | Esr 113 | : 114 |
    117 | 120 | 121 | 122 | 123 | 124 | 125 |
    126 |
    127 |
  • 128 |
129 |
130 |
131 |
132 |
135 |

136 | Learn more about a specific version. 137 | 138 | Select a version number from the left menu. 139 | 140 |

141 |
142 |
143 | 154 |
155 | `; 156 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 86993bd000012d3e1ef10d757d16952d 2 | // flow-typed version: a165222d28/redux_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'redux' { 5 | /* 6 | 7 | S = State 8 | A = Action 9 | D = Dispatch 10 | R = Result of a thunk action 11 | 12 | */ 13 | 14 | declare type GetState = () => S; 15 | declare type ThunkAction = ( 16 | dispatch: Dispatch, 17 | GetState, 18 | ) => R; 19 | declare type ThunkDispatch = (action: ThunkAction) => R; 20 | declare type DispatchAPI = (action: A) => A; 21 | declare type PlainDispatch}> = DispatchAPI; 22 | declare type Dispatch = PlainDispatch & ThunkDispatch; 23 | 24 | declare type MiddlewareAPI> = { 25 | dispatch: D, 26 | getState(): S, 27 | }; 28 | 29 | declare type Store> = { 30 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 31 | dispatch: D, 32 | getState(): S, 33 | subscribe(listener: () => void): () => void, 34 | replaceReducer(nextReducer: Reducer): void, 35 | }; 36 | 37 | declare type Reducer = (state: S, action: A) => S; 38 | 39 | declare type CombinedReducer = ( 40 | state: ($Shape & {}) | void, 41 | action: A, 42 | ) => S; 43 | 44 | declare type Middleware> = ( 45 | api: MiddlewareAPI, 46 | ) => (next: D) => D; 47 | 48 | declare type StoreCreator> = { 49 | (reducer: Reducer, enhancer?: StoreEnhancer): Store, 50 | ( 51 | reducer: Reducer, 52 | preloadedState: S, 53 | enhancer?: StoreEnhancer, 54 | ): Store, 55 | }; 56 | 57 | declare type StoreEnhancer> = ( 58 | next: StoreCreator, 59 | ) => StoreCreator; 60 | 61 | declare function createStore( 62 | reducer: Reducer, 63 | enhancer?: StoreEnhancer, 64 | ): Store; 65 | declare function createStore( 66 | reducer: Reducer, 67 | preloadedState: S, 68 | enhancer?: StoreEnhancer, 69 | ): Store; 70 | 71 | declare function applyMiddleware( 72 | ...middlewares: Array> 73 | ): StoreEnhancer; 74 | 75 | declare type ActionCreator = (...args: Array) => A; 76 | declare type ActionCreators = {[key: K]: ActionCreator}; 77 | 78 | declare function bindActionCreators< 79 | S, 80 | A, 81 | C: ActionCreator, 82 | D: DispatchAPI, 83 | >( 84 | actionCreator: C, 85 | dispatch: D, 86 | ): C; 87 | declare function bindActionCreators< 88 | S, 89 | A, 90 | K, 91 | C: ActionCreators, 92 | D: DispatchAPI, 93 | >( 94 | actionCreators: C, 95 | dispatch: D, 96 | ): C; 97 | 98 | declare function combineReducers( 99 | reducers: O, 100 | ): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; 101 | 102 | declare function compose(ab: (a: A) => B): (a: A) => B; 103 | declare function compose( 104 | bc: (b: B) => C, 105 | ab: (a: A) => B, 106 | ): (a: A) => C; 107 | declare function compose( 108 | cd: (c: C) => D, 109 | bc: (b: B) => C, 110 | ab: (a: A) => B, 111 | ): (a: A) => D; 112 | declare function compose( 113 | de: (d: D) => E, 114 | cd: (c: C) => D, 115 | bc: (b: B) => C, 116 | ab: (a: A) => B, 117 | ): (a: A) => E; 118 | declare function compose( 119 | ef: (e: E) => F, 120 | de: (d: D) => E, 121 | cd: (c: C) => D, 122 | bc: (b: B) => C, 123 | ab: (a: A) => B, 124 | ): (a: A) => F; 125 | declare function compose( 126 | fg: (f: F) => G, 127 | ef: (e: E) => F, 128 | de: (d: D) => E, 129 | cd: (c: C) => D, 130 | bc: (b: B) => C, 131 | ab: (a: A) => B, 132 | ): (a: A) => G; 133 | declare function compose( 134 | gh: (g: G) => H, 135 | fg: (f: F) => G, 136 | ef: (e: E) => F, 137 | de: (d: D) => E, 138 | cd: (c: C) => D, 139 | bc: (b: B) => C, 140 | ab: (a: A) => B, 141 | ): (a: A) => H; 142 | declare function compose( 143 | hi: (h: H) => I, 144 | gh: (g: G) => H, 145 | fg: (f: F) => G, 146 | ef: (e: E) => F, 147 | de: (d: D) => E, 148 | cd: (c: C) => D, 149 | bc: (b: B) => C, 150 | ab: (a: A) => B, 151 | ): (a: A) => I; 152 | } 153 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the "N+1" visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost: boolean = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 19 | ), 20 | ); 21 | 22 | export default function register() { 23 | if ( 24 | process.env.NODE_ENV === 'production' && 25 | 'serviceWorker' in navigator && 26 | process.env.PUBLIC_URL && 27 | navigator.serviceWorker 28 | ) { 29 | // The URL constructor is available in all browsers that support SW. 30 | const publicUrl = new URL( 31 | process.env.PUBLIC_URL, 32 | window.location.toString(), 33 | ); 34 | if (publicUrl.origin !== window.location.origin) { 35 | // Our service worker won't work if PUBLIC_URL is on a different origin 36 | // from what our page is served on. This might happen if a CDN is used to 37 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 38 | return; 39 | } 40 | 41 | window.addEventListener('load', () => { 42 | if (process.env.PUBLIC_URL) { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (!isLocalhost) { 46 | // Is not local host. Just register service worker 47 | registerValidSW(swUrl); 48 | } else { 49 | // This is running on localhost. Lets check if a service worker still exists or not. 50 | checkValidServiceWorker(swUrl); 51 | } 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl) { 58 | navigator.serviceWorker && 59 | navigator.serviceWorker 60 | .register(swUrl) 61 | .then(registration => { 62 | registration.onupdatefound = () => { 63 | const installingWorker = registration.installing; 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === 'installed') { 66 | if ( 67 | navigator.serviceWorker && 68 | navigator.serviceWorker.controller 69 | ) { 70 | // At this point, the old content will have been purged and 71 | // the fresh content will have been added to the cache. 72 | // It's the perfect time to display a "New content is 73 | // available; please refresh." message in your web app. 74 | console.log('New content is available; please refresh.'); 75 | } else { 76 | // At this point, everything has been precached. 77 | // It's the perfect time to display a 78 | // "Content is cached for offline use." message. 79 | console.log('Content is cached for offline use.'); 80 | } 81 | } 82 | }; 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error('Error during service worker registration:', error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get('content-type').indexOf('javascript') === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker && 101 | navigator.serviceWorker.ready.then(registration => { 102 | registration.unregister().then(() => { 103 | window.location.reload(); 104 | }); 105 | }); 106 | } else { 107 | // Service worker found. Proceed as normal. 108 | registerValidSW(swUrl); 109 | } 110 | }) 111 | .catch(() => { 112 | console.log( 113 | 'No internet connection found. App is running in offline mode.', 114 | ); 115 | }); 116 | } 117 | 118 | export function unregister() { 119 | if ('serviceWorker' in navigator && navigator.serviceWorker) { 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister(); 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | ADD_CHECK_RESULT, 5 | REFRESH_CHECK_RESULT, 6 | ADD_SERVER_ERROR, 7 | SET_VERSION, 8 | UPDATE_PRODUCT_VERSIONS, 9 | UPDATE_POLLBOT_VERSION, 10 | UPDATE_RELEASE_INFO, 11 | REQUEST_ONGOING_VERSIONS, 12 | REQUEST_POLLBOT_VERSION, 13 | UPDATE_URL, 14 | REFRESH_STATUS, 15 | REQUEST_STATUS, 16 | REQUEST_LOGIN, 17 | REQUEST_LOGOUT, 18 | LOGGED_IN, 19 | LOGGED_OUT, 20 | LOGIN_REQUESTED, 21 | UPDATE_USER_INFO, 22 | } from './types'; 23 | import type { 24 | AddCheckResult, 25 | AddServerError, 26 | APIVersionData, 27 | CheckResult, 28 | LoginRequested, 29 | LoggedIn, 30 | LoggedOut, 31 | VersionsDict, 32 | Product, 33 | RefreshCheckResult, 34 | RefreshStatus, 35 | ReleaseInfo, 36 | RequestLogin, 37 | RequestLogout, 38 | RequestOngoingVersions, 39 | RequestPollbotVersion, 40 | RequestStatus, 41 | SetVersion, 42 | UpdateProductVersions, 43 | UpdatePollbotVersion, 44 | UpdateReleaseInfo, 45 | UpdateUrl, 46 | UpdateUserInfo, 47 | } from './types'; 48 | 49 | // Small utility function. 50 | export const localUrlFromVersion = ([product, version]: [Product, string]) => 51 | `#pollbot/${product}/${version}`; 52 | 53 | /* 54 | * action creators 55 | */ 56 | 57 | export function setVersion(product: Product, version: string): SetVersion { 58 | return {type: SET_VERSION, product, version}; 59 | } 60 | 61 | export const sortByVersion = (a: string, b: string) => { 62 | const partsA = a.split('.'); 63 | const partsB = b.split('.'); 64 | if (partsA.length < 2 || partsB.length < 2) { 65 | // Bogus version, list it last. 66 | return 1; 67 | } 68 | let i = 0; 69 | while (partsA[i] === partsB[i] && i <= partsA.length) { 70 | // Skip all the parts that are equal. 71 | i++; 72 | } 73 | if (!partsA[i] || !partsB[i]) { 74 | // Both versions have the same first parts, but one may have more parts, eg 75 | // 56.0 and 56.0.1. 76 | return partsB.length - partsA.length; 77 | } 78 | // We have been through all the similar parts, we now have to deal with the 79 | // first part which is different. 80 | const subPartRegex = /^(\d+)([a-zA-Z]+)?(\d+)?([a-zA-Z]+)?/; // Eg: 0b12pre 81 | const subPartA = partsA[i].match(subPartRegex); // Eg: ["0b1pre", "0", "b", "12", "pre"] 82 | const subPartB = partsB[i].match(subPartRegex); 83 | if (!subPartA || !subPartB) { 84 | // Bogus version, list it last. 85 | return 1; 86 | } 87 | if (subPartA[1] !== subPartB[1]) { 88 | return parseInt(subPartB[1], 10) - parseInt(subPartA[1], 10); 89 | } 90 | if (subPartA[2] !== subPartB[2]) { 91 | // Suffix like 'a' or 'b'. 92 | if (subPartA[2] && !subPartB[2]) { 93 | return 1; 94 | } 95 | if (subPartB[2] && !subPartA[2]) { 96 | return -1; 97 | } 98 | return subPartB[2].localeCompare(subPartA[2]); 99 | } 100 | return parseInt(subPartB[3], 10) - parseInt(subPartA[3], 10); 101 | }; 102 | 103 | export const capitalize = (item: string) => 104 | item.charAt(0).toUpperCase() + item.slice(1); 105 | 106 | export const capitalizeChannel = ([channel, version]: [string, string]) => [ 107 | capitalize(channel), 108 | version, 109 | ]; 110 | 111 | export function updateProductVersions( 112 | product: Product, 113 | versions: VersionsDict, 114 | ): UpdateProductVersions { 115 | return {type: UPDATE_PRODUCT_VERSIONS, product, versions}; 116 | } 117 | 118 | export function updatePollbotVersion( 119 | version: APIVersionData, 120 | ): UpdatePollbotVersion { 121 | return {type: UPDATE_POLLBOT_VERSION, version}; 122 | } 123 | 124 | export function updateReleaseInfo(releaseInfo: ReleaseInfo): UpdateReleaseInfo { 125 | return {type: UPDATE_RELEASE_INFO, releaseInfo}; 126 | } 127 | 128 | export function addCheckResult( 129 | title: string, 130 | result: CheckResult, 131 | ): AddCheckResult { 132 | return {type: ADD_CHECK_RESULT, title, result}; 133 | } 134 | 135 | export function refreshCheckResult(title: string): RefreshCheckResult { 136 | return {type: REFRESH_CHECK_RESULT, title}; 137 | } 138 | 139 | export function addServerError(title: string, err: string): AddServerError { 140 | return {type: ADD_SERVER_ERROR, title, err}; 141 | } 142 | 143 | export function loggedIn(): LoggedIn { 144 | return {type: LOGGED_IN}; 145 | } 146 | 147 | export function loggedOut(): LoggedOut { 148 | return {type: LOGGED_OUT}; 149 | } 150 | 151 | export function loginRequested(): LoginRequested { 152 | return {type: LOGIN_REQUESTED}; 153 | } 154 | 155 | // For sagas 156 | export function requestPollbotVersion(): RequestPollbotVersion { 157 | return {type: REQUEST_POLLBOT_VERSION}; 158 | } 159 | 160 | export function requestOngoingVersions(): RequestOngoingVersions { 161 | return {type: REQUEST_ONGOING_VERSIONS}; 162 | } 163 | 164 | export function updateUrl(): UpdateUrl { 165 | return {type: UPDATE_URL}; 166 | } 167 | 168 | export function refreshStatus(): RefreshStatus { 169 | return {type: REFRESH_STATUS}; 170 | } 171 | 172 | export function requestStatus( 173 | product: Product, 174 | version: string, 175 | ): RequestStatus { 176 | return {type: REQUEST_STATUS, product, version}; 177 | } 178 | 179 | export function requestLogin(): RequestLogin { 180 | return {type: REQUEST_LOGIN}; 181 | } 182 | 183 | export function requestLogout(): RequestLogout { 184 | return {type: REQUEST_LOGOUT}; 185 | } 186 | 187 | export function updateUserInfo(userInfo: any): UpdateUserInfo { 188 | return {type: UPDATE_USER_INFO, userInfo}; 189 | } 190 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | Store as ReduxStore, 4 | ThunkAction as ReduxThunkAction, 5 | Dispatch as ReduxDispatch, 6 | GetState as ReduxGetState, 7 | } from 'redux'; 8 | 9 | export const products = ['firefox', 'devedition']; 10 | 11 | /* 12 | * state types 13 | */ 14 | export type Product = 'firefox' | 'devedition'; 15 | export type Status = 'missing' | 'exists' | 'incomplete' | 'error'; 16 | 17 | export type ChannelVersion = [string, string]; 18 | export type ChannelVersions = ChannelVersion[]; 19 | export type VersionsDict = {[channel: string]: string}; 20 | export type ProductVersions = { 21 | [product: Product]: VersionsDict, 22 | }; 23 | 24 | export type CheckInfo = { 25 | +url: string, 26 | +title: string, 27 | +actionable: boolean, 28 | }; 29 | 30 | export type ReleaseInfo = { 31 | +channel: string, 32 | +product: Product, 33 | +version: string, 34 | +checks: CheckInfo[], 35 | +message: string, 36 | +status: number, 37 | }; 38 | 39 | export type CheckResult = { 40 | +status: Status, 41 | +message: string, 42 | +link: string, 43 | }; 44 | 45 | export type CheckResults = { 46 | [check: string]: CheckResult, 47 | }; 48 | 49 | export type APIVersionData = { 50 | name: string, 51 | version: string, 52 | source: string, 53 | commit: string, 54 | }; 55 | 56 | /* Error: [title, errorMessage] */ 57 | export type Error = [string, string]; 58 | 59 | export type Login = 'LOGGED_OUT' | 'LOGIN_REQUESTED' | 'LOGGED_IN'; 60 | export const LOGGED_OUT = 'LOGGED_OUT'; 61 | export const LOGIN_REQUESTED = 'LOGIN_REQUESTED'; 62 | export const LOGGED_IN = 'LOGGED_IN'; 63 | 64 | export type State = { 65 | +version: [Product, string], 66 | +productVersions: ProductVersions, 67 | +releaseInfo: ?ReleaseInfo, 68 | +checkResults: CheckResults, 69 | +pollbotVersion: ?APIVersionData, 70 | +shouldRefresh: boolean, 71 | +login: Login, 72 | +userInfo: any, 73 | +errors: Error[], 74 | }; 75 | 76 | /* 77 | * action types 78 | */ 79 | export const ADD_CHECK_RESULT = 'ADD_CHECK_RESULT'; 80 | export const REFRESH_CHECK_RESULT = 'REFRESH_CHECK_RESULT'; 81 | export const ADD_SERVER_ERROR = 'ADD_SERVER_ERROR'; 82 | export const SET_VERSION = 'SET_VERSION'; 83 | export const UPDATE_PRODUCT_VERSIONS = 'UPDATE_PRODUCT_VERSIONS'; 84 | export const UPDATE_RELEASE_INFO = 'UPDATE_RELEASE_INFO'; 85 | export const UPDATE_POLLBOT_VERSION = 'UPDATE_POLLBOT_VERSION'; 86 | export const UPDATE_USER_INFO = 'UPDATE_USER_INFO'; 87 | 88 | export type AddCheckResult = {| 89 | type: 'ADD_CHECK_RESULT', 90 | title: string, 91 | result: CheckResult, 92 | |}; 93 | export type RefreshCheckResult = {| 94 | type: 'REFRESH_CHECK_RESULT', 95 | title: string, 96 | |}; 97 | export type AddServerError = {| 98 | type: 'ADD_SERVER_ERROR', 99 | title: string, 100 | err: string, 101 | |}; 102 | export type SetVersion = {| 103 | type: 'SET_VERSION', 104 | product: Product, 105 | version: string, 106 | |}; 107 | export type UpdateProductVersions = {| 108 | type: 'UPDATE_PRODUCT_VERSIONS', 109 | versions: VersionsDict, 110 | product: Product, 111 | |}; 112 | export type UpdateReleaseInfo = {| 113 | type: 'UPDATE_RELEASE_INFO', 114 | releaseInfo: ReleaseInfo, 115 | |}; 116 | export type UpdatePollbotVersion = {| 117 | type: 'UPDATE_POLLBOT_VERSION', 118 | version: APIVersionData, 119 | |}; 120 | export type LoggedIn = {| 121 | type: 'LOGGED_IN', 122 | |}; 123 | export type LoggedOut = {| 124 | type: 'LOGGED_OUT', 125 | |}; 126 | export type LoginRequested = {| 127 | type: 'LOGIN_REQUESTED', 128 | |}; 129 | export type UpdateUserInfo = {| 130 | type: 'UPDATE_USER_INFO', 131 | userInfo: any, 132 | |}; 133 | 134 | /* 135 | * saga types 136 | */ 137 | export const REQUEST_ONGOING_VERSIONS = 'REQUEST_ONGOING_VERSIONS'; 138 | export const REQUEST_POLLBOT_VERSION = 'REQUEST_POLLBOT_VERSION'; 139 | export const UPDATE_URL = 'UPDATE_URL'; 140 | export const REFRESH_STATUS = 'REFRESH_STATUS'; 141 | export const REQUEST_STATUS = 'REQUEST_STATUS'; 142 | export const REQUEST_LOGIN = 'REQUEST_LOGIN'; 143 | export const REQUEST_LOGOUT = 'REQUEST_LOGOUT'; 144 | 145 | export type RequestOngoingVersions = {| 146 | type: 'REQUEST_ONGOING_VERSIONS', 147 | |}; 148 | 149 | export type RequestPollbotVersion = {| 150 | type: 'REQUEST_POLLBOT_VERSION', 151 | |}; 152 | 153 | export type UpdateUrl = {| 154 | type: 'UPDATE_URL', 155 | |}; 156 | 157 | export type RefreshStatus = {| 158 | type: 'REFRESH_STATUS', 159 | |}; 160 | 161 | export type RequestStatus = {| 162 | type: 'REQUEST_STATUS', 163 | product: Product, 164 | version: string, 165 | |}; 166 | 167 | export type RequestLogin = {| 168 | type: 'REQUEST_LOGIN', 169 | |}; 170 | 171 | export type RequestLogout = {| 172 | type: 'REQUEST_LOGOUT', 173 | |}; 174 | 175 | export type Action = 176 | | AddCheckResult 177 | | RefreshCheckResult 178 | | AddServerError 179 | | LoggedIn 180 | | LoggedOut 181 | | LoginRequested 182 | | RefreshStatus 183 | | RequestLogin 184 | | RequestLogout 185 | | RequestOngoingVersions 186 | | RequestPollbotVersion 187 | | RequestStatus 188 | | SetVersion 189 | | UpdateProductVersions 190 | | UpdatePollbotVersion 191 | | UpdateReleaseInfo 192 | | UpdateUrl 193 | | UpdateUserInfo; 194 | 195 | /* 196 | * Redux types 197 | */ 198 | export type GetState = ReduxGetState; 199 | export type ThunkAction = ReduxThunkAction; 200 | export type Store = ReduxStore; 201 | export type Dispatch = ReduxDispatch; 202 | -------------------------------------------------------------------------------- /flow-typed/npm/bootstrap_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: d8559a945f9a3d9421c8ee54517de793 2 | // flow-typed version: <>/bootstrap_v3/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'bootstrap' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'bootstrap' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'bootstrap/Gruntfile' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'bootstrap/dist/js/bootstrap' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'bootstrap/dist/js/bootstrap.min' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'bootstrap/dist/js/npm' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'bootstrap/grunt/bs-commonjs-generator' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'bootstrap/grunt/bs-glyphicons-data-generator' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'bootstrap/grunt/bs-lessdoc-parser' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'bootstrap/grunt/bs-raw-files-generator' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'bootstrap/grunt/change-version' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'bootstrap/js/affix' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'bootstrap/js/alert' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'bootstrap/js/button' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'bootstrap/js/carousel' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'bootstrap/js/collapse' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'bootstrap/js/dropdown' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'bootstrap/js/modal' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'bootstrap/js/popover' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'bootstrap/js/scrollspy' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'bootstrap/js/tab' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'bootstrap/js/tooltip' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'bootstrap/js/transition' { 106 | declare module.exports: any; 107 | } 108 | 109 | // Filename aliases 110 | declare module 'bootstrap/Gruntfile.js' { 111 | declare module.exports: $Exports<'bootstrap/Gruntfile'>; 112 | } 113 | declare module 'bootstrap/dist/js/bootstrap.js' { 114 | declare module.exports: $Exports<'bootstrap/dist/js/bootstrap'>; 115 | } 116 | declare module 'bootstrap/dist/js/bootstrap.min.js' { 117 | declare module.exports: $Exports<'bootstrap/dist/js/bootstrap.min'>; 118 | } 119 | declare module 'bootstrap/dist/js/npm.js' { 120 | declare module.exports: $Exports<'bootstrap/dist/js/npm'>; 121 | } 122 | declare module 'bootstrap/grunt/bs-commonjs-generator.js' { 123 | declare module.exports: $Exports<'bootstrap/grunt/bs-commonjs-generator'>; 124 | } 125 | declare module 'bootstrap/grunt/bs-glyphicons-data-generator.js' { 126 | declare module.exports: $Exports<'bootstrap/grunt/bs-glyphicons-data-generator'>; 127 | } 128 | declare module 'bootstrap/grunt/bs-lessdoc-parser.js' { 129 | declare module.exports: $Exports<'bootstrap/grunt/bs-lessdoc-parser'>; 130 | } 131 | declare module 'bootstrap/grunt/bs-raw-files-generator.js' { 132 | declare module.exports: $Exports<'bootstrap/grunt/bs-raw-files-generator'>; 133 | } 134 | declare module 'bootstrap/grunt/change-version.js' { 135 | declare module.exports: $Exports<'bootstrap/grunt/change-version'>; 136 | } 137 | declare module 'bootstrap/js/affix.js' { 138 | declare module.exports: $Exports<'bootstrap/js/affix'>; 139 | } 140 | declare module 'bootstrap/js/alert.js' { 141 | declare module.exports: $Exports<'bootstrap/js/alert'>; 142 | } 143 | declare module 'bootstrap/js/button.js' { 144 | declare module.exports: $Exports<'bootstrap/js/button'>; 145 | } 146 | declare module 'bootstrap/js/carousel.js' { 147 | declare module.exports: $Exports<'bootstrap/js/carousel'>; 148 | } 149 | declare module 'bootstrap/js/collapse.js' { 150 | declare module.exports: $Exports<'bootstrap/js/collapse'>; 151 | } 152 | declare module 'bootstrap/js/dropdown.js' { 153 | declare module.exports: $Exports<'bootstrap/js/dropdown'>; 154 | } 155 | declare module 'bootstrap/js/modal.js' { 156 | declare module.exports: $Exports<'bootstrap/js/modal'>; 157 | } 158 | declare module 'bootstrap/js/popover.js' { 159 | declare module.exports: $Exports<'bootstrap/js/popover'>; 160 | } 161 | declare module 'bootstrap/js/scrollspy.js' { 162 | declare module.exports: $Exports<'bootstrap/js/scrollspy'>; 163 | } 164 | declare module 'bootstrap/js/tab.js' { 165 | declare module.exports: $Exports<'bootstrap/js/tab'>; 166 | } 167 | declare module 'bootstrap/js/tooltip.js' { 168 | declare module.exports: $Exports<'bootstrap/js/tooltip'>; 169 | } 170 | declare module 'bootstrap/js/transition.js' { 171 | declare module.exports: $Exports<'bootstrap/js/transition'>; 172 | } 173 | -------------------------------------------------------------------------------- /flow-typed/npm/react-scripts_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 86fc485e18ff280b037ccdf7267c45da 2 | // flow-typed version: <>/react-scripts_v1.0.11/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'react-scripts' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'react-scripts' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'react-scripts/bin/react-scripts' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'react-scripts/config/env' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'react-scripts/config/jest/babelTransform' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'react-scripts/config/jest/cssTransform' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'react-scripts/config/jest/fileTransform' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'react-scripts/config/paths' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'react-scripts/config/polyfills' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'react-scripts/config/webpack.config.dev' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'react-scripts/config/webpack.config.prod' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'react-scripts/config/webpackDevServer.config' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'react-scripts/scripts/build' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'react-scripts/scripts/eject' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'react-scripts/scripts/init' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'react-scripts/scripts/start' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'react-scripts/scripts/test' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'react-scripts/scripts/utils/createJestConfig' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'react-scripts/template/src/App' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'react-scripts/template/src/App.test' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'react-scripts/template/src/index' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'react-scripts/template/src/registerServiceWorker' { 102 | declare module.exports: any; 103 | } 104 | 105 | // Filename aliases 106 | declare module 'react-scripts/bin/react-scripts.js' { 107 | declare module.exports: $Exports<'react-scripts/bin/react-scripts'>; 108 | } 109 | declare module 'react-scripts/config/env.js' { 110 | declare module.exports: $Exports<'react-scripts/config/env'>; 111 | } 112 | declare module 'react-scripts/config/jest/babelTransform.js' { 113 | declare module.exports: $Exports<'react-scripts/config/jest/babelTransform'>; 114 | } 115 | declare module 'react-scripts/config/jest/cssTransform.js' { 116 | declare module.exports: $Exports<'react-scripts/config/jest/cssTransform'>; 117 | } 118 | declare module 'react-scripts/config/jest/fileTransform.js' { 119 | declare module.exports: $Exports<'react-scripts/config/jest/fileTransform'>; 120 | } 121 | declare module 'react-scripts/config/paths.js' { 122 | declare module.exports: $Exports<'react-scripts/config/paths'>; 123 | } 124 | declare module 'react-scripts/config/polyfills.js' { 125 | declare module.exports: $Exports<'react-scripts/config/polyfills'>; 126 | } 127 | declare module 'react-scripts/config/webpack.config.dev.js' { 128 | declare module.exports: $Exports<'react-scripts/config/webpack.config.dev'>; 129 | } 130 | declare module 'react-scripts/config/webpack.config.prod.js' { 131 | declare module.exports: $Exports<'react-scripts/config/webpack.config.prod'>; 132 | } 133 | declare module 'react-scripts/config/webpackDevServer.config.js' { 134 | declare module.exports: $Exports<'react-scripts/config/webpackDevServer.config'>; 135 | } 136 | declare module 'react-scripts/scripts/build.js' { 137 | declare module.exports: $Exports<'react-scripts/scripts/build'>; 138 | } 139 | declare module 'react-scripts/scripts/eject.js' { 140 | declare module.exports: $Exports<'react-scripts/scripts/eject'>; 141 | } 142 | declare module 'react-scripts/scripts/init.js' { 143 | declare module.exports: $Exports<'react-scripts/scripts/init'>; 144 | } 145 | declare module 'react-scripts/scripts/start.js' { 146 | declare module.exports: $Exports<'react-scripts/scripts/start'>; 147 | } 148 | declare module 'react-scripts/scripts/test.js' { 149 | declare module.exports: $Exports<'react-scripts/scripts/test'>; 150 | } 151 | declare module 'react-scripts/scripts/utils/createJestConfig.js' { 152 | declare module.exports: $Exports<'react-scripts/scripts/utils/createJestConfig'>; 153 | } 154 | declare module 'react-scripts/template/src/App.js' { 155 | declare module.exports: $Exports<'react-scripts/template/src/App'>; 156 | } 157 | declare module 'react-scripts/template/src/App.test.js' { 158 | declare module.exports: $Exports<'react-scripts/template/src/App.test'>; 159 | } 160 | declare module 'react-scripts/template/src/index.js' { 161 | declare module.exports: $Exports<'react-scripts/template/src/index'>; 162 | } 163 | declare module 'react-scripts/template/src/registerServiceWorker.js' { 164 | declare module.exports: $Exports<'react-scripts/template/src/registerServiceWorker'>; 165 | } 166 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-promise_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3d0aaab25f54d91d51e12553286b5694 2 | // flow-typed version: <>/eslint-plugin-promise_v^3.5.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-promise' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-promise' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-promise/rules/always-return' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-promise/rules/avoid-new' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-promise/rules/catch-or-return' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-promise/rules/lib/has-promise-callback' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-promise/rules/lib/is-callback' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-promise/rules/lib/is-inside-callback' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-promise/rules/lib/is-inside-promise' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-promise/rules/lib/is-named-callback' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-promise/rules/lib/is-promise' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-promise/rules/no-callback-in-promise' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-promise/rules/no-native' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-promise/rules/no-nesting' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-promise/rules/no-promise-in-callback' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-promise/rules/no-return-wrap' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-promise/rules/param-names' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-promise/rules/prefer-await-to-callbacks' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-promise/rules/prefer-await-to-then' { 90 | declare module.exports: any; 91 | } 92 | 93 | // Filename aliases 94 | declare module 'eslint-plugin-promise/index' { 95 | declare module.exports: $Exports<'eslint-plugin-promise'>; 96 | } 97 | declare module 'eslint-plugin-promise/index.js' { 98 | declare module.exports: $Exports<'eslint-plugin-promise'>; 99 | } 100 | declare module 'eslint-plugin-promise/rules/always-return.js' { 101 | declare module.exports: $Exports<'eslint-plugin-promise/rules/always-return'>; 102 | } 103 | declare module 'eslint-plugin-promise/rules/avoid-new.js' { 104 | declare module.exports: $Exports<'eslint-plugin-promise/rules/avoid-new'>; 105 | } 106 | declare module 'eslint-plugin-promise/rules/catch-or-return.js' { 107 | declare module.exports: $Exports<'eslint-plugin-promise/rules/catch-or-return'>; 108 | } 109 | declare module 'eslint-plugin-promise/rules/lib/has-promise-callback.js' { 110 | declare module.exports: $Exports<'eslint-plugin-promise/rules/lib/has-promise-callback'>; 111 | } 112 | declare module 'eslint-plugin-promise/rules/lib/is-callback.js' { 113 | declare module.exports: $Exports<'eslint-plugin-promise/rules/lib/is-callback'>; 114 | } 115 | declare module 'eslint-plugin-promise/rules/lib/is-inside-callback.js' { 116 | declare module.exports: $Exports<'eslint-plugin-promise/rules/lib/is-inside-callback'>; 117 | } 118 | declare module 'eslint-plugin-promise/rules/lib/is-inside-promise.js' { 119 | declare module.exports: $Exports<'eslint-plugin-promise/rules/lib/is-inside-promise'>; 120 | } 121 | declare module 'eslint-plugin-promise/rules/lib/is-named-callback.js' { 122 | declare module.exports: $Exports<'eslint-plugin-promise/rules/lib/is-named-callback'>; 123 | } 124 | declare module 'eslint-plugin-promise/rules/lib/is-promise.js' { 125 | declare module.exports: $Exports<'eslint-plugin-promise/rules/lib/is-promise'>; 126 | } 127 | declare module 'eslint-plugin-promise/rules/no-callback-in-promise.js' { 128 | declare module.exports: $Exports<'eslint-plugin-promise/rules/no-callback-in-promise'>; 129 | } 130 | declare module 'eslint-plugin-promise/rules/no-native.js' { 131 | declare module.exports: $Exports<'eslint-plugin-promise/rules/no-native'>; 132 | } 133 | declare module 'eslint-plugin-promise/rules/no-nesting.js' { 134 | declare module.exports: $Exports<'eslint-plugin-promise/rules/no-nesting'>; 135 | } 136 | declare module 'eslint-plugin-promise/rules/no-promise-in-callback.js' { 137 | declare module.exports: $Exports<'eslint-plugin-promise/rules/no-promise-in-callback'>; 138 | } 139 | declare module 'eslint-plugin-promise/rules/no-return-wrap.js' { 140 | declare module.exports: $Exports<'eslint-plugin-promise/rules/no-return-wrap'>; 141 | } 142 | declare module 'eslint-plugin-promise/rules/param-names.js' { 143 | declare module.exports: $Exports<'eslint-plugin-promise/rules/param-names'>; 144 | } 145 | declare module 'eslint-plugin-promise/rules/prefer-await-to-callbacks.js' { 146 | declare module.exports: $Exports<'eslint-plugin-promise/rules/prefer-await-to-callbacks'>; 147 | } 148 | declare module 'eslint-plugin-promise/rules/prefer-await-to-then.js' { 149 | declare module.exports: $Exports<'eslint-plugin-promise/rules/prefer-await-to-then'>; 150 | } 151 | -------------------------------------------------------------------------------- /src/sagas.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | REQUEST_ONGOING_VERSIONS, 5 | REQUEST_POLLBOT_VERSION, 6 | UPDATE_URL, 7 | REFRESH_STATUS, 8 | REQUEST_STATUS, 9 | REQUEST_LOGIN, 10 | REQUEST_LOGOUT, 11 | products, 12 | } from './types'; 13 | import type { 14 | APIVersionData, 15 | CheckResult, 16 | VersionsDict, 17 | Product, 18 | ReleaseInfo, 19 | RequestStatus, 20 | State, 21 | } from './types'; 22 | import {all, call, put, select, takeEvery} from 'redux-saga/effects'; 23 | import { 24 | checkStatus, 25 | getOngoingVersions, 26 | getPollbotVersion, 27 | getReleaseInfo, 28 | } from './PollbotAPI'; 29 | import { 30 | addCheckResult, 31 | addServerError, 32 | localUrlFromVersion, 33 | loggedOut, 34 | loginRequested, 35 | refreshCheckResult, 36 | setVersion, 37 | updateProductVersions, 38 | updatePollbotVersion, 39 | updateReleaseInfo, 40 | } from './actions'; 41 | import {login, logout} from './auth0'; 42 | 43 | type Saga = Generator<*, void, *>; 44 | 45 | // Fetching the version from the Pollbot service. 46 | export function* fetchPollbotVersion(): Saga { 47 | try { 48 | const version: APIVersionData = yield call(getPollbotVersion); 49 | yield put(updatePollbotVersion(version)); 50 | } catch (err) { 51 | console.error('Failed getting the pollbot version', err); 52 | } 53 | } 54 | 55 | export function* fetchAndUpdateVersions(product: Product): Saga { 56 | try { 57 | const versions: VersionsDict = yield call(getOngoingVersions, product); 58 | yield put(updateProductVersions(product, versions)); 59 | } catch (err) { 60 | console.error( 61 | 'Failed getting the latest channel versions for product: ' + product, 62 | err, 63 | ); 64 | } 65 | } 66 | 67 | // Fetching the ongoing versions. 68 | export function* fetchOngoingVersions(): Saga { 69 | yield all(products.map(product => call(fetchAndUpdateVersions, product))); 70 | } 71 | 72 | // Update the url from the version stored in the state. 73 | export function* updateUrl(): Saga { 74 | const state: State = yield select(); 75 | window.location.hash = localUrlFromVersion(state.version); 76 | } 77 | 78 | export function* checkResultAndUpdateAndNotify( 79 | title: string, 80 | url: string, 81 | prevResult: CheckResult, 82 | ): Saga { 83 | const notifyChanges = (checkTitle, status) => { 84 | if (Notification.permission === 'granted') { 85 | new Notification(`${checkTitle}: status changed (${status}).`); 86 | } 87 | }; 88 | 89 | // Make sure the check we're refreshing is shown as being refreshed. 90 | yield put(refreshCheckResult(title)); 91 | yield call(checkResultAndUpdate, title, url); 92 | const state: State = yield select(); 93 | const result: CheckResult = state.checkResults && state.checkResults[title]; 94 | if (prevResult && result && prevResult.status !== result.status) { 95 | notifyChanges(title, result.status); 96 | } 97 | } 98 | 99 | // Refreshing a status for the current version. 100 | export function* refreshStatus(): Saga { 101 | const state: State = yield select(); 102 | // Save previous results so we can check if something changed. 103 | const prevResults = state.checkResults; 104 | if (state.releaseInfo && state.releaseInfo.checks) { 105 | yield all( 106 | state.releaseInfo.checks 107 | // only refresh checks that were failing. 108 | .filter(({title}) => state.checkResults[title].status !== 'exists') 109 | .map(({url, title}) => 110 | call(checkResultAndUpdateAndNotify, title, url, prevResults[title]), 111 | ), 112 | ); 113 | } 114 | } 115 | 116 | export function* checkResultAndUpdate(title: string, url: string): Saga { 117 | try { 118 | const result = yield call(checkStatus, url); 119 | yield put(addCheckResult(title, result)); 120 | } catch (err) { 121 | console.error(`Failed getting ${title} check result`, err); 122 | yield put(addServerError(title, err)); 123 | } 124 | } 125 | 126 | // Requesting a status for a new version. 127 | export function* requestStatus(action: RequestStatus): Saga { 128 | let {product, version} = action; 129 | let {productVersions} = yield select(); 130 | try { 131 | if ( 132 | Object.keys(productVersions).length === 0 || 133 | !productVersions.hasOwnProperty(product) || 134 | Object.keys(productVersions[product]).length === 0 135 | ) { 136 | // We don't have the product channel versions yet. 137 | const versions = yield call(getOngoingVersions, product); 138 | yield put(updateProductVersions(product, versions)); 139 | // We now have the product channel versions. 140 | ({productVersions} = yield select()); 141 | } 142 | if (productVersions[product].hasOwnProperty(version)) { 143 | version = productVersions[product][version]; 144 | } 145 | yield put(setVersion(product, version)); 146 | yield call(updateUrl); 147 | const releaseInfo: ReleaseInfo = yield call( 148 | getReleaseInfo, 149 | product, 150 | version, 151 | ); 152 | yield put(updateReleaseInfo(releaseInfo)); 153 | yield all( 154 | releaseInfo.checks.map(({url, title}) => 155 | call(checkResultAndUpdate, title, url), 156 | ), 157 | ); 158 | } catch (err) { 159 | console.error( 160 | `Failed getting the release info for ${product} ${version}`, 161 | err, 162 | ); 163 | } 164 | } 165 | 166 | // Requesting a auth0 login. 167 | export function* requestLogin(): Saga { 168 | try { 169 | yield put(loginRequested()); 170 | yield call(login); 171 | } catch (err) { 172 | console.error('Login failed', err); 173 | yield put(loggedOut()); 174 | } 175 | } 176 | 177 | // Requesting a logout. 178 | export function* requestLogout(): Saga { 179 | try { 180 | yield call(logout); 181 | yield put(loggedOut()); 182 | } catch (err) { 183 | console.error('Logout failed', err); 184 | } 185 | } 186 | 187 | // Root saga. 188 | export function* rootSaga(): Saga { 189 | yield all([ 190 | takeEvery(REQUEST_ONGOING_VERSIONS, fetchOngoingVersions), 191 | takeEvery(REQUEST_POLLBOT_VERSION, fetchPollbotVersion), 192 | takeEvery(UPDATE_URL, updateUrl), 193 | takeEvery(REFRESH_STATUS, refreshStatus), 194 | takeEvery(REQUEST_STATUS, requestStatus), 195 | takeEvery(REQUEST_LOGIN, requestLogin), 196 | takeEvery(REQUEST_LOGOUT, requestLogout), 197 | ]); 198 | } 199 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-typed_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3776925c1d52327216ef3e0bd161b882 2 | // flow-typed version: <>/flow-typed_v^2.1.5/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'flow-typed' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'flow-typed' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'flow-typed/dist/cli' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'flow-typed/dist/commands/create-stub' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'flow-typed/dist/commands/install' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'flow-typed/dist/commands/runTests' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'flow-typed/dist/commands/search' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'flow-typed/dist/commands/update-cache' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'flow-typed/dist/commands/update' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'flow-typed/dist/commands/validateDefs' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'flow-typed/dist/commands/version' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'flow-typed/dist/lib/cacheRepoUtils' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'flow-typed/dist/lib/codeSign' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'flow-typed/dist/lib/fileUtils' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'flow-typed/dist/lib/flowProjectUtils' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'flow-typed/dist/lib/flowVersion' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'flow-typed/dist/lib/git' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'flow-typed/dist/lib/github' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'flow-typed/dist/lib/isInFlowTypedRepo' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'flow-typed/dist/lib/libDefs' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'flow-typed/dist/lib/node' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'flow-typed/dist/lib/npm/npmLibDefs' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'flow-typed/dist/lib/npm/npmProjectUtils' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'flow-typed/dist/lib/semver' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'flow-typed/dist/lib/stubUtils' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'flow-typed/dist/lib/validationErrors' { 118 | declare module.exports: any; 119 | } 120 | 121 | // Filename aliases 122 | declare module 'flow-typed/dist/cli.js' { 123 | declare module.exports: $Exports<'flow-typed/dist/cli'>; 124 | } 125 | declare module 'flow-typed/dist/commands/create-stub.js' { 126 | declare module.exports: $Exports<'flow-typed/dist/commands/create-stub'>; 127 | } 128 | declare module 'flow-typed/dist/commands/install.js' { 129 | declare module.exports: $Exports<'flow-typed/dist/commands/install'>; 130 | } 131 | declare module 'flow-typed/dist/commands/runTests.js' { 132 | declare module.exports: $Exports<'flow-typed/dist/commands/runTests'>; 133 | } 134 | declare module 'flow-typed/dist/commands/search.js' { 135 | declare module.exports: $Exports<'flow-typed/dist/commands/search'>; 136 | } 137 | declare module 'flow-typed/dist/commands/update-cache.js' { 138 | declare module.exports: $Exports<'flow-typed/dist/commands/update-cache'>; 139 | } 140 | declare module 'flow-typed/dist/commands/update.js' { 141 | declare module.exports: $Exports<'flow-typed/dist/commands/update'>; 142 | } 143 | declare module 'flow-typed/dist/commands/validateDefs.js' { 144 | declare module.exports: $Exports<'flow-typed/dist/commands/validateDefs'>; 145 | } 146 | declare module 'flow-typed/dist/commands/version.js' { 147 | declare module.exports: $Exports<'flow-typed/dist/commands/version'>; 148 | } 149 | declare module 'flow-typed/dist/lib/cacheRepoUtils.js' { 150 | declare module.exports: $Exports<'flow-typed/dist/lib/cacheRepoUtils'>; 151 | } 152 | declare module 'flow-typed/dist/lib/codeSign.js' { 153 | declare module.exports: $Exports<'flow-typed/dist/lib/codeSign'>; 154 | } 155 | declare module 'flow-typed/dist/lib/fileUtils.js' { 156 | declare module.exports: $Exports<'flow-typed/dist/lib/fileUtils'>; 157 | } 158 | declare module 'flow-typed/dist/lib/flowProjectUtils.js' { 159 | declare module.exports: $Exports<'flow-typed/dist/lib/flowProjectUtils'>; 160 | } 161 | declare module 'flow-typed/dist/lib/flowVersion.js' { 162 | declare module.exports: $Exports<'flow-typed/dist/lib/flowVersion'>; 163 | } 164 | declare module 'flow-typed/dist/lib/git.js' { 165 | declare module.exports: $Exports<'flow-typed/dist/lib/git'>; 166 | } 167 | declare module 'flow-typed/dist/lib/github.js' { 168 | declare module.exports: $Exports<'flow-typed/dist/lib/github'>; 169 | } 170 | declare module 'flow-typed/dist/lib/isInFlowTypedRepo.js' { 171 | declare module.exports: $Exports<'flow-typed/dist/lib/isInFlowTypedRepo'>; 172 | } 173 | declare module 'flow-typed/dist/lib/libDefs.js' { 174 | declare module.exports: $Exports<'flow-typed/dist/lib/libDefs'>; 175 | } 176 | declare module 'flow-typed/dist/lib/node.js' { 177 | declare module.exports: $Exports<'flow-typed/dist/lib/node'>; 178 | } 179 | declare module 'flow-typed/dist/lib/npm/npmLibDefs.js' { 180 | declare module.exports: $Exports<'flow-typed/dist/lib/npm/npmLibDefs'>; 181 | } 182 | declare module 'flow-typed/dist/lib/npm/npmProjectUtils.js' { 183 | declare module.exports: $Exports<'flow-typed/dist/lib/npm/npmProjectUtils'>; 184 | } 185 | declare module 'flow-typed/dist/lib/semver.js' { 186 | declare module.exports: $Exports<'flow-typed/dist/lib/semver'>; 187 | } 188 | declare module 'flow-typed/dist/lib/stubUtils.js' { 189 | declare module.exports: $Exports<'flow-typed/dist/lib/stubUtils'>; 190 | } 191 | declare module 'flow-typed/dist/lib/validationErrors.js' { 192 | declare module.exports: $Exports<'flow-typed/dist/lib/validationErrors'>; 193 | } 194 | -------------------------------------------------------------------------------- /src/auth0.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH0_CALLBACK_URL, 3 | AUTH0_CLIENT_ID, 4 | AUTH0_DOMAIN, 5 | checkLogin, 6 | fetchUserInfo, 7 | handleUserInfo, 8 | initWebAuth, 9 | isAuthenticated, 10 | login, 11 | logout, 12 | setSession, 13 | webAuthHandler, 14 | } from './auth0'; 15 | 16 | // Mock the localStorage API. 17 | global.localStorage = (function() { 18 | var store = {}; 19 | 20 | return { 21 | getItem: function(key) { 22 | return store[key] || null; 23 | }, 24 | removeItem: function(key) { 25 | delete store[key]; 26 | }, 27 | setItem: function(key, value) { 28 | store[key] = value.toString(); 29 | }, 30 | clear: function() { 31 | store = {}; 32 | }, 33 | }; 34 | })(); 35 | 36 | const webAuthMock = { 37 | authorize: jest.fn(), 38 | parseHash: jest.fn(), 39 | client: {userInfo: jest.fn()}, 40 | }; 41 | 42 | global.auth0 = { 43 | WebAuth: jest.fn(() => webAuthMock), 44 | }; 45 | 46 | const authResult = { 47 | accessToken: 'access token', 48 | idToken: 'id token', 49 | expiresIn: 100, 50 | }; 51 | 52 | describe('webAuthHandler', () => { 53 | const callback = jest.fn(); 54 | it('throws an error', () => { 55 | expect(() => webAuthHandler(callback, 'some error', 'some result')).toThrow( 56 | 'some error', 57 | ); 58 | }); 59 | it('sets the session and runs the callback', () => { 60 | window.location = {hash: 'some hash'}; 61 | 62 | webAuthHandler(callback, null, authResult); 63 | expect(window.location.hash).toEqual(''); 64 | expect(callback).toHaveBeenCalledWith(authResult); 65 | expect(callback).toHaveBeenCalledWith(authResult); 66 | }); 67 | }); 68 | 69 | describe('initWebAuth', () => { 70 | it('returns an initialized web auth', () => { 71 | const webAuth = initWebAuth(); 72 | expect(global.auth0.WebAuth).toHaveBeenCalledWith({ 73 | domain: AUTH0_DOMAIN, 74 | clientID: AUTH0_CLIENT_ID, 75 | redirectUri: AUTH0_CALLBACK_URL, 76 | audience: 'http://minimal-demo-iam.localhost:8000', // 'https://' + AUTH0_DOMAIN + '/userinfo', 77 | responseType: 'token id_token', 78 | scope: 'openid profile', 79 | }); 80 | expect(webAuth).toEqual(webAuthMock); 81 | }); 82 | }); 83 | 84 | describe('setSession', () => { 85 | it('sets the auth result in the local storage', () => { 86 | global.Date = jest.fn(() => ({ 87 | getTime: () => 0, 88 | })); 89 | setSession(authResult); 90 | expect(global.localStorage.getItem('session')).toEqual( 91 | JSON.stringify(authResult), 92 | ); 93 | expect(global.localStorage.getItem('expires_at')).toEqual( 94 | JSON.stringify(100 * 1000), 95 | ); 96 | }); 97 | }); 98 | 99 | describe('login', () => { 100 | it('initializes the webAuth and authorizes', () => { 101 | const mockAuthorize = jest.fn(); 102 | const mockInitWebAuth = jest.fn(() => ({authorize: mockAuthorize})); 103 | login(mockInitWebAuth); 104 | expect(mockInitWebAuth).toHaveBeenCalledTimes(1); 105 | expect(mockAuthorize).toHaveBeenCalledTimes(1); 106 | }); 107 | }); 108 | 109 | describe('logout', () => { 110 | it('removes the entries from the localStorage', () => { 111 | global.localStorage.clear(); 112 | expect(global.localStorage.getItem('session')).toEqual(null); 113 | expect(global.localStorage.getItem('expires_at')).toEqual(null); 114 | logout(); 115 | expect(global.localStorage.getItem('session')).toEqual(null); 116 | expect(global.localStorage.getItem('expires_at')).toEqual(null); 117 | 118 | global.localStorage.setItem('session', 'a session'); 119 | global.localStorage.setItem('expires_at', 'an expiration delay'); 120 | logout(); 121 | expect(global.localStorage.getItem('session')).toEqual(null); 122 | expect(global.localStorage.getItem('expires_at')).toEqual(null); 123 | }); 124 | }); 125 | 126 | describe('checkLogin', () => { 127 | it('checks if logged in', () => { 128 | const onLoggedIn = jest.fn(); 129 | const parseHash = jest.fn(); 130 | const handler = jest.fn(); 131 | const initWebAuth = jest.fn(() => ({parseHash: parseHash})); 132 | checkLogin(onLoggedIn, initWebAuth, handler); 133 | expect(initWebAuth).toHaveBeenCalledTimes(1); 134 | expect(parseHash).toHaveBeenCalledTimes(1); 135 | }); 136 | it('logs a message in the console if the login failed', () => { 137 | const onLoggedIn = jest.fn(); 138 | const parseHash = jest.fn(() => { 139 | throw new Error('foo'); 140 | }); 141 | const handler = jest.fn(); 142 | const initWebAuth = jest.fn(() => ({parseHash: parseHash})); 143 | global.console.error = jest.fn(); 144 | checkLogin(onLoggedIn, initWebAuth, handler); 145 | expect(global.console.error).toHaveBeenCalledWith( 146 | 'Login failed', 147 | new Error('foo'), 148 | ); 149 | }); 150 | }); 151 | 152 | describe('isAuthenticated', () => { 153 | it('returns true if authenticated', () => { 154 | global.Date = jest.fn(() => ({ 155 | getTime: () => 0, 156 | })); 157 | global.localStorage.setItem('session', 'a session'); 158 | global.localStorage.setItem('expires_at', 100); 159 | expect(isAuthenticated()).toBe(true); 160 | }); 161 | it('returns false if there is no session', () => { 162 | global.localStorage.clear(); 163 | global.localStorage.setItem('expires_at', 100); 164 | expect(isAuthenticated()).toBe(false); 165 | }); 166 | it('returns false if the expiration date is in the past', () => { 167 | global.Date = jest.fn(() => ({ 168 | getTime: () => 1000, 169 | })); 170 | global.localStorage.setItem('session', 'a session'); 171 | global.localStorage.setItem('expires_at', 100); 172 | expect(isAuthenticated()).toBe(false); 173 | }); 174 | }); 175 | 176 | describe('handleUserInfo', () => { 177 | it('calls the callback with the user info', () => { 178 | const onUserInfo = jest.fn(); 179 | handleUserInfo(onUserInfo, null, 'some profile'); 180 | expect(onUserInfo).toHaveBeenCalledWith('some profile'); 181 | }); 182 | it('throws if there was an error', () => { 183 | const onUserInfo = jest.fn(); 184 | expect(() => handleUserInfo(onUserInfo, 'some error')).toThrow( 185 | 'some error', 186 | ); 187 | }); 188 | }); 189 | 190 | describe('fetchUserInfo', () => { 191 | it('returns user info if authenticated', () => { 192 | const userInfo = jest.fn(); 193 | const initWebAuth = jest.fn(() => ({client: {userInfo: userInfo}})); 194 | global.localStorage.setItem('session', '{"accessToken": "abc"}'); 195 | fetchUserInfo(jest.fn(), initWebAuth); 196 | expect(userInfo).toHaveBeenCalledTimes(1); 197 | expect(userInfo.mock.calls[0][0]).toEqual('abc'); 198 | }); 199 | it('returns undefined if not authenticated', () => { 200 | global.localStorage.setItem('session', ''); 201 | expect(fetchUserInfo(jest.fn(), initWebAuth)).toEqual(undefined); 202 | 203 | global.localStorage.setItem('session', '{}'); 204 | expect(fetchUserInfo(jest.fn(), initWebAuth)).toEqual(undefined); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /src/actions.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_CHECK_RESULT, 3 | REFRESH_CHECK_RESULT, 4 | ADD_SERVER_ERROR, 5 | SET_VERSION, 6 | UPDATE_PRODUCT_VERSIONS, 7 | UPDATE_POLLBOT_VERSION, 8 | UPDATE_RELEASE_INFO, 9 | REQUEST_ONGOING_VERSIONS, 10 | REQUEST_POLLBOT_VERSION, 11 | UPDATE_URL, 12 | REFRESH_STATUS, 13 | REQUEST_STATUS, 14 | LOGGED_IN, 15 | LOGGED_OUT, 16 | LOGIN_REQUESTED, 17 | REQUEST_LOGIN, 18 | REQUEST_LOGOUT, 19 | UPDATE_USER_INFO, 20 | } from './types'; 21 | import { 22 | addCheckResult, 23 | addServerError, 24 | capitalizeChannel, 25 | loggedIn, 26 | loggedOut, 27 | loginRequested, 28 | requestOngoingVersions, 29 | requestPollbotVersion, 30 | refreshCheckResult, 31 | refreshStatus, 32 | requestLogin, 33 | requestLogout, 34 | requestStatus, 35 | setVersion, 36 | sortByVersion, 37 | updateProductVersions, 38 | updatePollbotVersion, 39 | updateReleaseInfo, 40 | updateUrl, 41 | updateUserInfo, 42 | } from './actions'; 43 | 44 | describe('action creators', () => { 45 | it('returns a UPDATE_VERSION_INPUT action for setVersion', () => { 46 | expect(setVersion('firefox', '123')).toEqual({ 47 | type: SET_VERSION, 48 | product: 'firefox', 49 | version: '123', 50 | }); 51 | }); 52 | it('returns a UPDATE_PRODUCT_VERSIONS action for updateProductVersions', () => { 53 | const channelVersions = { 54 | nightly: '57.0a1', 55 | beta: '56.0b12', 56 | release: '55.0.3', 57 | esr: '52.3.0esr', 58 | }; 59 | expect(updateProductVersions('firefox', channelVersions)).toEqual({ 60 | type: UPDATE_PRODUCT_VERSIONS, 61 | product: 'firefox', 62 | versions: channelVersions, 63 | }); 64 | }); 65 | it('returns a UPDATE_POLLBOT_VERSION action for updatePollbotVersion', () => { 66 | const pollbotVersion = { 67 | name: 'pollbot', 68 | source: 'https://github.com/mozilla/PollBot.git', 69 | version: '0.2.1-22-g8e09a0f', 70 | commit: '8e09a0f8e995344ea24fbb940a6bddc17e0edaed', 71 | }; 72 | expect(updatePollbotVersion(pollbotVersion)).toEqual({ 73 | type: UPDATE_POLLBOT_VERSION, 74 | version: pollbotVersion, 75 | }); 76 | }); 77 | it('returns a UPDATE_RELEASE_INFO action for updateReleaseInfo', () => { 78 | const releaseInfo = { 79 | product: 'firefox', 80 | checks: [ 81 | { 82 | title: 'Archive Release', 83 | url: 'https://pollbot.dev.mozaws.net/v1/firefox/55.0.3/archive', 84 | }, 85 | { 86 | title: 'Balrog update rules', 87 | url: 'https://pollbot.dev.mozaws.net/v1/firefox/55.0.3/balrog-rules', 88 | }, 89 | { 90 | title: 'Download links', 91 | url: 92 | 'https://pollbot.dev.mozaws.net/v1/firefox/55.0.3/bedrock/download-links', 93 | }, 94 | { 95 | title: 'Product details', 96 | url: 97 | 'https://pollbot.dev.mozaws.net/v1/firefox/55.0.3/product-details', 98 | }, 99 | { 100 | title: 'Release notes', 101 | url: 102 | 'https://pollbot.dev.mozaws.net/v1/firefox/55.0.3/bedrock/release-notes', 103 | }, 104 | { 105 | title: 'Security advisories', 106 | url: 107 | 'https://pollbot.dev.mozaws.net/v1/firefox/55.0.3/bedrock/security-advisories', 108 | }, 109 | ], 110 | version: '55.0.3', 111 | channel: 'release', 112 | }; 113 | expect(updateReleaseInfo(releaseInfo)).toEqual({ 114 | type: UPDATE_RELEASE_INFO, 115 | releaseInfo: releaseInfo, 116 | }); 117 | }); 118 | it('returns a ADD_CHECK_RESULT action for addCheckResult', () => { 119 | const checkResult = { 120 | link: 'https://archive.mozilla.org/pub/firefox/releases/55.0.3/', 121 | status: 'exists', 122 | message: 123 | 'The archive exists at https://archive.mozilla.org/pub/firefox/releases/55.0.3/ and all 95 locales are present for all platforms (linux-i686, linux-x86_64, mac, win32, win64)', 124 | }; 125 | expect(addCheckResult('some check', checkResult)).toEqual({ 126 | type: ADD_CHECK_RESULT, 127 | title: 'some check', 128 | result: checkResult, 129 | }); 130 | }); 131 | it('returns a REFRESH_CHECK_RESULT action for refreshCheckResult', () => { 132 | expect(refreshCheckResult('some check')).toEqual({ 133 | type: REFRESH_CHECK_RESULT, 134 | title: 'some check', 135 | }); 136 | }); 137 | it('returns a ADD_SERVER_ERROR action for addServerError', () => { 138 | expect(addServerError('some check', 'some error')).toEqual({ 139 | type: ADD_SERVER_ERROR, 140 | title: 'some check', 141 | err: 'some error', 142 | }); 143 | }); 144 | it('returns a LOGGED_IN action for loggedIn', () => { 145 | expect(loggedIn()).toEqual({type: LOGGED_IN}); 146 | }); 147 | it('returns a LOGGED_OUT action for loggedOut', () => { 148 | expect(loggedOut()).toEqual({type: LOGGED_OUT}); 149 | }); 150 | it('returns a LOGIN_REQUESTED action for loginRequested', () => { 151 | expect(loginRequested()).toEqual({type: LOGIN_REQUESTED}); 152 | }); 153 | }); 154 | 155 | describe('sagas action creator', () => { 156 | it('handles a REQUEST_ONGOING_VERSIONS action for requestOngoingVersions', () => { 157 | expect(requestOngoingVersions()).toEqual({ 158 | type: REQUEST_ONGOING_VERSIONS, 159 | }); 160 | }); 161 | it('handles a REQUEST_POLLBOT_VERSION action for requestPollbotVersion', () => { 162 | expect(requestPollbotVersion()).toEqual({type: REQUEST_POLLBOT_VERSION}); 163 | }); 164 | it('handles a UPDATE_URL action for updateUrl', () => { 165 | expect(updateUrl()).toEqual({type: UPDATE_URL}); 166 | }); 167 | it('handles a REFRESH_STATUS action for refreshStatus', () => { 168 | expect(refreshStatus()).toEqual({type: REFRESH_STATUS}); 169 | }); 170 | it('handles a REQUEST_STATUS action for requestStatus', () => { 171 | expect(requestStatus('firefox', '50.0')).toEqual({ 172 | type: REQUEST_STATUS, 173 | product: 'firefox', 174 | version: '50.0', 175 | }); 176 | }); 177 | it('handles a REQUEST_LOGIN action for requestLogin', () => { 178 | expect(requestLogin()).toEqual({type: REQUEST_LOGIN}); 179 | }); 180 | it('handles a REQUEST_LOGOUT action for requestLogout', () => { 181 | expect(requestLogout()).toEqual({type: REQUEST_LOGOUT}); 182 | }); 183 | it('handles a UPDATE_USER_INFO action for updateUserInfo', () => { 184 | expect(updateUserInfo()).toEqual({type: UPDATE_USER_INFO}); 185 | }); 186 | }); 187 | 188 | describe('sortByVersion helper', () => { 189 | it('sorts bogus versions', () => { 190 | expect(sortByVersion('0', '56.0')).toEqual(1); 191 | }); 192 | it('sorts equal versions', () => { 193 | expect(sortByVersion('56.0', '56.0')).toEqual(0); 194 | }); 195 | it('sorts similar versions', () => { 196 | expect(sortByVersion('56.0', '56.0.1')).toEqual(1); 197 | }); 198 | it('sorts release versions', () => { 199 | expect(sortByVersion('56.0', '57.0')).toEqual(1); 200 | expect(sortByVersion('56.0.1', '56.0.2')).toEqual(1); 201 | }); 202 | it('sorts release and beta versions', () => { 203 | expect(sortByVersion('56.0b1', '56.0')).toEqual(1); 204 | expect(sortByVersion('56.0', '56.0b1')).toEqual(-1); 205 | }); 206 | it('sorts alpha and beta versions', () => { 207 | expect(sortByVersion('56.0a1', '56.0b1')).toEqual(1); 208 | }); 209 | it('sorts beta versions', () => { 210 | expect(sortByVersion('56.0b1', '56.0b2')).toEqual(1); 211 | }); 212 | it('sorts bogus sub versions', () => { 213 | expect(sortByVersion('56.0', '56.a')).toEqual(1); 214 | }); 215 | it('sorts random versions', () => { 216 | expect(sortByVersion('55.0.3', '57.0b3')).toBeGreaterThan(0); 217 | }); 218 | }); 219 | 220 | describe('capitalizeChannel helper', () => { 221 | it('uppercases the first letter of the channel', () => { 222 | expect(capitalizeChannel(['foo', 'bar'])).toEqual(['Foo', 'bar']); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /src/reducers.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_CHECK_RESULT, 3 | ADD_SERVER_ERROR, 4 | LOGGED_IN, 5 | LOGGED_OUT, 6 | LOGIN_REQUESTED, 7 | REFRESH_CHECK_RESULT, 8 | SET_VERSION, 9 | UPDATE_PRODUCT_VERSIONS, 10 | UPDATE_POLLBOT_VERSION, 11 | UPDATE_RELEASE_INFO, 12 | UPDATE_USER_INFO, 13 | } from './types'; 14 | import {deliveryDashboard, initialState} from './reducers'; 15 | 16 | const stateWith = stateCrumbs => Object.assign({}, initialState, stateCrumbs); 17 | 18 | describe('deliveryDashboard reducer', () => { 19 | it('returns the initial state', () => { 20 | expect(deliveryDashboard(undefined, {})).toEqual(initialState); 21 | }); 22 | it('handles ADD_CHECK_RESULT', () => { 23 | const checkResult = { 24 | status: 'exists', 25 | message: 'successful test', 26 | link: 'some url', 27 | }; 28 | expect( 29 | deliveryDashboard(undefined, { 30 | type: ADD_CHECK_RESULT, 31 | title: 'some test', 32 | result: checkResult, 33 | }), 34 | ).toEqual( 35 | stateWith({ 36 | checkResults: { 37 | 'some test': checkResult, 38 | }, 39 | shouldRefresh: false, 40 | }), 41 | ); 42 | 43 | const otherCheckResult = { 44 | status: 'exists', 45 | message: 'successful test', 46 | link: 'some url', 47 | }; 48 | expect( 49 | deliveryDashboard( 50 | stateWith({ 51 | checkResults: { 52 | 'some test': checkResult, 53 | }, 54 | }), 55 | { 56 | type: ADD_CHECK_RESULT, 57 | title: 'some other test', 58 | result: otherCheckResult, 59 | }, 60 | ), 61 | ).toEqual( 62 | stateWith({ 63 | checkResults: { 64 | 'some test': checkResult, 65 | 'some other test': otherCheckResult, 66 | }, 67 | shouldRefresh: false, 68 | }), 69 | ); 70 | 71 | const failingCheckResult = { 72 | status: 'incomplete', 73 | message: 'successful test', 74 | link: 'some url', 75 | }; 76 | expect( 77 | deliveryDashboard( 78 | stateWith({ 79 | checkResults: { 80 | 'some test': checkResult, 81 | }, 82 | }), 83 | { 84 | type: ADD_CHECK_RESULT, 85 | title: 'some other test', 86 | result: failingCheckResult, 87 | }, 88 | ), 89 | ).toEqual( 90 | stateWith({ 91 | checkResults: { 92 | 'some test': checkResult, 93 | 'some other test': failingCheckResult, 94 | }, 95 | shouldRefresh: true, 96 | }), 97 | ); 98 | }); 99 | it('handles REFRESH_CHECK_RESULT', () => { 100 | const checkResult = { 101 | status: 'exists', 102 | message: 'successful test', 103 | link: 'some url', 104 | }; 105 | expect( 106 | deliveryDashboard(undefined, { 107 | type: REFRESH_CHECK_RESULT, 108 | title: 'some test', 109 | }), 110 | ).toEqual( 111 | stateWith({ 112 | checkResults: {}, 113 | }), 114 | ); 115 | 116 | const failingCheckResult = { 117 | status: 'incomplete', 118 | message: 'successful test', 119 | link: 'some url', 120 | }; 121 | expect( 122 | deliveryDashboard( 123 | stateWith({ 124 | checkResults: { 125 | 'some test': checkResult, 126 | 'some other test': failingCheckResult, 127 | }, 128 | }), 129 | { 130 | type: REFRESH_CHECK_RESULT, 131 | title: 'some other test', 132 | result: failingCheckResult, 133 | }, 134 | ), 135 | ).toEqual( 136 | stateWith({ 137 | checkResults: { 138 | 'some test': checkResult, 139 | }, 140 | }), 141 | ); 142 | }); 143 | it('handles ADD_SERVER_ERROR', () => { 144 | expect( 145 | deliveryDashboard(undefined, { 146 | type: ADD_SERVER_ERROR, 147 | title: 'some check', 148 | err: 'some error', 149 | }), 150 | ).toEqual( 151 | stateWith({errors: [['some check', 'some error']], shouldRefresh: true}), 152 | ); 153 | expect( 154 | deliveryDashboard( 155 | stateWith({ 156 | errors: [['some check', 'some error']], 157 | }), 158 | { 159 | type: ADD_SERVER_ERROR, 160 | title: 'some other check', 161 | err: 'some other error', 162 | }, 163 | ), 164 | ).toEqual( 165 | stateWith({ 166 | errors: [ 167 | ['some check', 'some error'], 168 | ['some other check', 'some other error'], 169 | ], 170 | shouldRefresh: true, 171 | }), 172 | ); 173 | }); 174 | it('handles SET_VERSION', () => { 175 | expect( 176 | deliveryDashboard(undefined, { 177 | type: SET_VERSION, 178 | product: 'firefox', 179 | version: '50.0', 180 | }), 181 | ).toEqual(stateWith({version: ['firefox', '50.0'], shouldRefresh: false})); 182 | expect( 183 | deliveryDashboard( 184 | stateWith({ 185 | version: ['firefox', '50.0'], 186 | }), 187 | { 188 | type: SET_VERSION, 189 | product: 'firefox', 190 | version: '51.0', 191 | }, 192 | ), 193 | ).toEqual(stateWith({version: ['firefox', '51.0'], shouldRefresh: false})); 194 | }); 195 | it('handles UPDATE_PRODUCT_VERSIONS', () => { 196 | expect( 197 | deliveryDashboard(undefined, { 198 | type: UPDATE_PRODUCT_VERSIONS, 199 | product: 'firefox', 200 | versions: {release: '0.1.2'}, 201 | }), 202 | ).toEqual( 203 | stateWith({ 204 | productVersions: { 205 | firefox: {release: '0.1.2'}, 206 | devedition: {}, 207 | }, 208 | }), 209 | ); 210 | expect( 211 | deliveryDashboard( 212 | stateWith({ 213 | productVersions: { 214 | firefox: {release: '0.1.2'}, 215 | devedition: {}, 216 | }, 217 | }), 218 | { 219 | type: UPDATE_PRODUCT_VERSIONS, 220 | product: 'firefox', 221 | versions: {nightly: '1.2.3', release: '0.1.3'}, 222 | }, 223 | ), 224 | ).toEqual( 225 | stateWith({ 226 | productVersions: { 227 | firefox: {nightly: '1.2.3', release: '0.1.3'}, 228 | devedition: {}, 229 | }, 230 | }), 231 | ); 232 | }); 233 | it('handles UPDATE_RELEASE_INFO', () => { 234 | expect( 235 | deliveryDashboard(undefined, { 236 | type: UPDATE_RELEASE_INFO, 237 | releaseInfo: 'some new release info', 238 | }), 239 | ).toEqual(stateWith({releaseInfo: 'some new release info'})); 240 | expect( 241 | deliveryDashboard( 242 | stateWith({ 243 | releaseInfo: 'some release info', 244 | }), 245 | { 246 | type: UPDATE_RELEASE_INFO, 247 | releaseInfo: 'some new release info', 248 | }, 249 | ), 250 | ).toEqual(stateWith({releaseInfo: 'some new release info'})); 251 | }); 252 | it('handles UPDATE_POLLBOT_VERSION', () => { 253 | expect( 254 | deliveryDashboard(undefined, { 255 | type: UPDATE_POLLBOT_VERSION, 256 | version: 'some new pollbot version', 257 | }), 258 | ).toEqual(stateWith({pollbotVersion: 'some new pollbot version'})); 259 | expect( 260 | deliveryDashboard( 261 | stateWith({ 262 | pollbotVersion: 'some pollbot version', 263 | }), 264 | { 265 | type: UPDATE_POLLBOT_VERSION, 266 | version: 'some new pollbot version', 267 | }, 268 | ), 269 | ).toEqual(stateWith({pollbotVersion: 'some new pollbot version'})); 270 | }); 271 | it('handles LOGGED_IN', () => { 272 | expect( 273 | deliveryDashboard(undefined, { 274 | type: LOGGED_IN, 275 | }), 276 | ).toEqual(stateWith({login: LOGGED_IN})); 277 | expect( 278 | deliveryDashboard( 279 | stateWith({ 280 | login: LOGGED_OUT, 281 | }), 282 | { 283 | type: LOGGED_IN, 284 | }, 285 | ), 286 | ).toEqual(stateWith({login: LOGGED_IN})); 287 | }); 288 | it('handles LOGGED_OUT', () => { 289 | expect( 290 | deliveryDashboard(undefined, { 291 | type: LOGGED_OUT, 292 | }), 293 | ).toEqual(stateWith({login: LOGGED_OUT})); 294 | expect( 295 | deliveryDashboard( 296 | stateWith({ 297 | login: LOGGED_IN, 298 | }), 299 | { 300 | type: LOGGED_OUT, 301 | }, 302 | ), 303 | ).toEqual(stateWith({login: LOGGED_OUT})); 304 | }); 305 | it('handles LOGIN_REQUESTED', () => { 306 | expect( 307 | deliveryDashboard(undefined, { 308 | type: LOGIN_REQUESTED, 309 | }), 310 | ).toEqual(stateWith({login: LOGIN_REQUESTED})); 311 | expect( 312 | deliveryDashboard( 313 | stateWith({ 314 | login: LOGGED_OUT, 315 | }), 316 | { 317 | type: LOGIN_REQUESTED, 318 | }, 319 | ), 320 | ).toEqual(stateWith({login: LOGIN_REQUESTED})); 321 | }); 322 | it('handles UPDATE_USER_INFO', () => { 323 | expect( 324 | deliveryDashboard(undefined, { 325 | type: UPDATE_USER_INFO, 326 | userInfo: 'foo', 327 | }), 328 | ).toEqual(stateWith({userInfo: 'foo'})); 329 | expect( 330 | deliveryDashboard( 331 | stateWith({ 332 | userInfo: 'foo', 333 | }), 334 | { 335 | type: UPDATE_USER_INFO, 336 | userInfo: 'bar', 337 | }, 338 | ), 339 | ).toEqual(stateWith({userInfo: 'bar'})); 340 | }); 341 | }); 342 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-node_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3c7a94f6087a2127edbbeba9a38ed723 2 | // flow-typed version: <>/eslint-plugin-node_v^5.1.1/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-node' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-node' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-node/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-node/lib/rules' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-node/lib/rules/exports-style' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-node/lib/rules/no-deprecated-api' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-node/lib/rules/no-extraneous-import' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-node/lib/rules/no-extraneous-require' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-node/lib/rules/no-hide-core-modules' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-node/lib/rules/no-missing-import' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-node/lib/rules/no-missing-require' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-node/lib/rules/no-unpublished-bin' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-node/lib/rules/no-unpublished-import' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-node/lib/rules/no-unpublished-require' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-node/lib/rules/no-unsupported-features' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-node/lib/rules/process-exit-as-throw' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-node/lib/rules/shebang' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-node/lib/util/cache' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-node/lib/util/check-existence' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-node/lib/util/check-extraneous' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-plugin-node/lib/util/check-publish' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-plugin-node/lib/util/deprecated-apis' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-plugin-node/lib/util/exists' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-plugin-node/lib/util/features' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-plugin-node/lib/util/get-allow-modules' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-plugin-node/lib/util/get-convert-path' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-plugin-node/lib/util/get-import-export-targets' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-plugin-node/lib/util/get-npmignore' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-plugin-node/lib/util/get-package-json' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-plugin-node/lib/util/get-require-targets' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-plugin-node/lib/util/get-resolve-paths' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-plugin-node/lib/util/get-try-extensions' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-plugin-node/lib/util/get-value-if-string' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-plugin-node/lib/util/import-target' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'eslint-plugin-node/lib/util/strip-import-path-params' { 154 | declare module.exports: any; 155 | } 156 | 157 | // Filename aliases 158 | declare module 'eslint-plugin-node/lib/index.js' { 159 | declare module.exports: $Exports<'eslint-plugin-node/lib/index'>; 160 | } 161 | declare module 'eslint-plugin-node/lib/rules.js' { 162 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules'>; 163 | } 164 | declare module 'eslint-plugin-node/lib/rules/exports-style.js' { 165 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/exports-style'>; 166 | } 167 | declare module 'eslint-plugin-node/lib/rules/no-deprecated-api.js' { 168 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-deprecated-api'>; 169 | } 170 | declare module 'eslint-plugin-node/lib/rules/no-extraneous-import.js' { 171 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-extraneous-import'>; 172 | } 173 | declare module 'eslint-plugin-node/lib/rules/no-extraneous-require.js' { 174 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-extraneous-require'>; 175 | } 176 | declare module 'eslint-plugin-node/lib/rules/no-hide-core-modules.js' { 177 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-hide-core-modules'>; 178 | } 179 | declare module 'eslint-plugin-node/lib/rules/no-missing-import.js' { 180 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-missing-import'>; 181 | } 182 | declare module 'eslint-plugin-node/lib/rules/no-missing-require.js' { 183 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-missing-require'>; 184 | } 185 | declare module 'eslint-plugin-node/lib/rules/no-unpublished-bin.js' { 186 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-unpublished-bin'>; 187 | } 188 | declare module 'eslint-plugin-node/lib/rules/no-unpublished-import.js' { 189 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-unpublished-import'>; 190 | } 191 | declare module 'eslint-plugin-node/lib/rules/no-unpublished-require.js' { 192 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-unpublished-require'>; 193 | } 194 | declare module 'eslint-plugin-node/lib/rules/no-unsupported-features.js' { 195 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/no-unsupported-features'>; 196 | } 197 | declare module 'eslint-plugin-node/lib/rules/process-exit-as-throw.js' { 198 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/process-exit-as-throw'>; 199 | } 200 | declare module 'eslint-plugin-node/lib/rules/shebang.js' { 201 | declare module.exports: $Exports<'eslint-plugin-node/lib/rules/shebang'>; 202 | } 203 | declare module 'eslint-plugin-node/lib/util/cache.js' { 204 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/cache'>; 205 | } 206 | declare module 'eslint-plugin-node/lib/util/check-existence.js' { 207 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/check-existence'>; 208 | } 209 | declare module 'eslint-plugin-node/lib/util/check-extraneous.js' { 210 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/check-extraneous'>; 211 | } 212 | declare module 'eslint-plugin-node/lib/util/check-publish.js' { 213 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/check-publish'>; 214 | } 215 | declare module 'eslint-plugin-node/lib/util/deprecated-apis.js' { 216 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/deprecated-apis'>; 217 | } 218 | declare module 'eslint-plugin-node/lib/util/exists.js' { 219 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/exists'>; 220 | } 221 | declare module 'eslint-plugin-node/lib/util/features.js' { 222 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/features'>; 223 | } 224 | declare module 'eslint-plugin-node/lib/util/get-allow-modules.js' { 225 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-allow-modules'>; 226 | } 227 | declare module 'eslint-plugin-node/lib/util/get-convert-path.js' { 228 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-convert-path'>; 229 | } 230 | declare module 'eslint-plugin-node/lib/util/get-import-export-targets.js' { 231 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-import-export-targets'>; 232 | } 233 | declare module 'eslint-plugin-node/lib/util/get-npmignore.js' { 234 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-npmignore'>; 235 | } 236 | declare module 'eslint-plugin-node/lib/util/get-package-json.js' { 237 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-package-json'>; 238 | } 239 | declare module 'eslint-plugin-node/lib/util/get-require-targets.js' { 240 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-require-targets'>; 241 | } 242 | declare module 'eslint-plugin-node/lib/util/get-resolve-paths.js' { 243 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-resolve-paths'>; 244 | } 245 | declare module 'eslint-plugin-node/lib/util/get-try-extensions.js' { 246 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-try-extensions'>; 247 | } 248 | declare module 'eslint-plugin-node/lib/util/get-value-if-string.js' { 249 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/get-value-if-string'>; 250 | } 251 | declare module 'eslint-plugin-node/lib/util/import-target.js' { 252 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/import-target'>; 253 | } 254 | declare module 'eslint-plugin-node/lib/util/strip-import-path-params.js' { 255 | declare module.exports: $Exports<'eslint-plugin-node/lib/util/strip-import-path-params'>; 256 | } 257 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-import_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3db8c2dfefd22de282fb3302d0b19523 2 | // flow-typed version: <>/eslint-plugin-import_v^2.7.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-import' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-import' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-import/config/electron' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-import/config/errors' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-import/config/react-native' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-import/config/react' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-import/config/recommended' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-import/config/stage-0' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-import/config/warnings' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-import/lib/ExportMap' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-import/lib/core/importType' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-import/lib/core/staticRequire' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-import/lib/importDeclaration' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-import/lib/index' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-import/lib/rules/default' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-import/lib/rules/export' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-import/lib/rules/extensions' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-import/lib/rules/first' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-import/lib/rules/imports-first' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-import/lib/rules/max-dependencies' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-plugin-import/lib/rules/named' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-plugin-import/lib/rules/namespace' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-plugin-import/lib/rules/newline-after-import' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-plugin-import/lib/rules/no-absolute-path' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-plugin-import/lib/rules/no-amd' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-plugin-import/lib/rules/no-anonymous-default-export' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-plugin-import/lib/rules/no-commonjs' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-plugin-import/lib/rules/no-deprecated' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-plugin-import/lib/rules/no-duplicates' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-plugin-import/lib/rules/no-dynamic-require' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-plugin-import/lib/rules/no-extraneous-dependencies' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-plugin-import/lib/rules/no-internal-modules' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-plugin-import/lib/rules/no-mutable-exports' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default-member' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default' { 154 | declare module.exports: any; 155 | } 156 | 157 | declare module 'eslint-plugin-import/lib/rules/no-named-default' { 158 | declare module.exports: any; 159 | } 160 | 161 | declare module 'eslint-plugin-import/lib/rules/no-namespace' { 162 | declare module.exports: any; 163 | } 164 | 165 | declare module 'eslint-plugin-import/lib/rules/no-nodejs-modules' { 166 | declare module.exports: any; 167 | } 168 | 169 | declare module 'eslint-plugin-import/lib/rules/no-restricted-paths' { 170 | declare module.exports: any; 171 | } 172 | 173 | declare module 'eslint-plugin-import/lib/rules/no-unassigned-import' { 174 | declare module.exports: any; 175 | } 176 | 177 | declare module 'eslint-plugin-import/lib/rules/no-unresolved' { 178 | declare module.exports: any; 179 | } 180 | 181 | declare module 'eslint-plugin-import/lib/rules/no-webpack-loader-syntax' { 182 | declare module.exports: any; 183 | } 184 | 185 | declare module 'eslint-plugin-import/lib/rules/order' { 186 | declare module.exports: any; 187 | } 188 | 189 | declare module 'eslint-plugin-import/lib/rules/prefer-default-export' { 190 | declare module.exports: any; 191 | } 192 | 193 | declare module 'eslint-plugin-import/lib/rules/unambiguous' { 194 | declare module.exports: any; 195 | } 196 | 197 | declare module 'eslint-plugin-import/memo-parser/index' { 198 | declare module.exports: any; 199 | } 200 | 201 | // Filename aliases 202 | declare module 'eslint-plugin-import/config/electron.js' { 203 | declare module.exports: $Exports<'eslint-plugin-import/config/electron'>; 204 | } 205 | declare module 'eslint-plugin-import/config/errors.js' { 206 | declare module.exports: $Exports<'eslint-plugin-import/config/errors'>; 207 | } 208 | declare module 'eslint-plugin-import/config/react-native.js' { 209 | declare module.exports: $Exports<'eslint-plugin-import/config/react-native'>; 210 | } 211 | declare module 'eslint-plugin-import/config/react.js' { 212 | declare module.exports: $Exports<'eslint-plugin-import/config/react'>; 213 | } 214 | declare module 'eslint-plugin-import/config/recommended.js' { 215 | declare module.exports: $Exports<'eslint-plugin-import/config/recommended'>; 216 | } 217 | declare module 'eslint-plugin-import/config/stage-0.js' { 218 | declare module.exports: $Exports<'eslint-plugin-import/config/stage-0'>; 219 | } 220 | declare module 'eslint-plugin-import/config/warnings.js' { 221 | declare module.exports: $Exports<'eslint-plugin-import/config/warnings'>; 222 | } 223 | declare module 'eslint-plugin-import/lib/ExportMap.js' { 224 | declare module.exports: $Exports<'eslint-plugin-import/lib/ExportMap'>; 225 | } 226 | declare module 'eslint-plugin-import/lib/core/importType.js' { 227 | declare module.exports: $Exports<'eslint-plugin-import/lib/core/importType'>; 228 | } 229 | declare module 'eslint-plugin-import/lib/core/staticRequire.js' { 230 | declare module.exports: $Exports<'eslint-plugin-import/lib/core/staticRequire'>; 231 | } 232 | declare module 'eslint-plugin-import/lib/importDeclaration.js' { 233 | declare module.exports: $Exports<'eslint-plugin-import/lib/importDeclaration'>; 234 | } 235 | declare module 'eslint-plugin-import/lib/index.js' { 236 | declare module.exports: $Exports<'eslint-plugin-import/lib/index'>; 237 | } 238 | declare module 'eslint-plugin-import/lib/rules/default.js' { 239 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/default'>; 240 | } 241 | declare module 'eslint-plugin-import/lib/rules/export.js' { 242 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/export'>; 243 | } 244 | declare module 'eslint-plugin-import/lib/rules/extensions.js' { 245 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/extensions'>; 246 | } 247 | declare module 'eslint-plugin-import/lib/rules/first.js' { 248 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/first'>; 249 | } 250 | declare module 'eslint-plugin-import/lib/rules/imports-first.js' { 251 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/imports-first'>; 252 | } 253 | declare module 'eslint-plugin-import/lib/rules/max-dependencies.js' { 254 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/max-dependencies'>; 255 | } 256 | declare module 'eslint-plugin-import/lib/rules/named.js' { 257 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/named'>; 258 | } 259 | declare module 'eslint-plugin-import/lib/rules/namespace.js' { 260 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/namespace'>; 261 | } 262 | declare module 'eslint-plugin-import/lib/rules/newline-after-import.js' { 263 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/newline-after-import'>; 264 | } 265 | declare module 'eslint-plugin-import/lib/rules/no-absolute-path.js' { 266 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-absolute-path'>; 267 | } 268 | declare module 'eslint-plugin-import/lib/rules/no-amd.js' { 269 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-amd'>; 270 | } 271 | declare module 'eslint-plugin-import/lib/rules/no-anonymous-default-export.js' { 272 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-anonymous-default-export'>; 273 | } 274 | declare module 'eslint-plugin-import/lib/rules/no-commonjs.js' { 275 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-commonjs'>; 276 | } 277 | declare module 'eslint-plugin-import/lib/rules/no-deprecated.js' { 278 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-deprecated'>; 279 | } 280 | declare module 'eslint-plugin-import/lib/rules/no-duplicates.js' { 281 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-duplicates'>; 282 | } 283 | declare module 'eslint-plugin-import/lib/rules/no-dynamic-require.js' { 284 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-dynamic-require'>; 285 | } 286 | declare module 'eslint-plugin-import/lib/rules/no-extraneous-dependencies.js' { 287 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-extraneous-dependencies'>; 288 | } 289 | declare module 'eslint-plugin-import/lib/rules/no-internal-modules.js' { 290 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-internal-modules'>; 291 | } 292 | declare module 'eslint-plugin-import/lib/rules/no-mutable-exports.js' { 293 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-mutable-exports'>; 294 | } 295 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default-member.js' { 296 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-as-default-member'>; 297 | } 298 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default.js' { 299 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-as-default'>; 300 | } 301 | declare module 'eslint-plugin-import/lib/rules/no-named-default.js' { 302 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-default'>; 303 | } 304 | declare module 'eslint-plugin-import/lib/rules/no-namespace.js' { 305 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-namespace'>; 306 | } 307 | declare module 'eslint-plugin-import/lib/rules/no-nodejs-modules.js' { 308 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-nodejs-modules'>; 309 | } 310 | declare module 'eslint-plugin-import/lib/rules/no-restricted-paths.js' { 311 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-restricted-paths'>; 312 | } 313 | declare module 'eslint-plugin-import/lib/rules/no-unassigned-import.js' { 314 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-unassigned-import'>; 315 | } 316 | declare module 'eslint-plugin-import/lib/rules/no-unresolved.js' { 317 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-unresolved'>; 318 | } 319 | declare module 'eslint-plugin-import/lib/rules/no-webpack-loader-syntax.js' { 320 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-webpack-loader-syntax'>; 321 | } 322 | declare module 'eslint-plugin-import/lib/rules/order.js' { 323 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/order'>; 324 | } 325 | declare module 'eslint-plugin-import/lib/rules/prefer-default-export.js' { 326 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/prefer-default-export'>; 327 | } 328 | declare module 'eslint-plugin-import/lib/rules/unambiguous.js' { 329 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/unambiguous'>; 330 | } 331 | declare module 'eslint-plugin-import/memo-parser/index.js' { 332 | declare module.exports: $Exports<'eslint-plugin-import/memo-parser/index'>; 333 | } 334 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import 'photon-ant'; 3 | import * as React from 'react'; 4 | import {Alert, Button, Card, Icon, Layout, Spin, Tooltip} from 'antd'; 5 | import './App.css'; 6 | import {connect} from 'react-redux'; 7 | import type {MapStateToProps} from 'react-redux'; 8 | import { 9 | capitalize, 10 | localUrlFromVersion, 11 | loggedIn, 12 | requestOngoingVersions, 13 | requestPollbotVersion, 14 | refreshStatus, 15 | requestLogin, 16 | requestLogout, 17 | requestStatus, 18 | updateUserInfo, 19 | } from './actions'; 20 | import type { 21 | APIVersionData, 22 | CheckResult, 23 | CheckResults, 24 | Dispatch, 25 | Error, 26 | Login, 27 | ProductVersions, 28 | Product, 29 | ReleaseInfo, 30 | State, 31 | Status, 32 | } from './types'; 33 | import {products} from './types'; 34 | import {LOGGED_IN, LOGGED_OUT, LOGIN_REQUESTED} from './types'; 35 | import {checkLogin, fetchUserInfo, isAuthenticated} from './auth0'; 36 | 37 | const deliveryDashboardVersionData: APIVersionData = require('./version.json'); 38 | 39 | function requestNotificationPermission(): void { 40 | if ( 41 | Notification.permission !== 'denied' && 42 | Notification.permission !== 'granted' 43 | ) { 44 | Notification.requestPermission(); 45 | } 46 | } 47 | 48 | export const parseUrl = ( 49 | url: string, 50 | ): ?{service: string, product: Product, version: string} => { 51 | const re = /^#(\w+)\/(\w+)\/([^/]+)\/?/; // Eg: #pollbot/firefox/50.0 52 | const parsed: ?(string[]) = url.match(re); 53 | if (!parsed) { 54 | return null; 55 | } 56 | const [_, service, product, version] = parsed; 57 | const maybeProduct = products.find(p => p === product); 58 | if (!maybeProduct) { 59 | // unsupported/unrecognized product. 60 | return null; 61 | } 62 | return { 63 | service: service, 64 | product: maybeProduct, 65 | version: version, 66 | }; 67 | }; 68 | 69 | type AppProps = { 70 | checkResults: CheckResults, 71 | dispatch: Dispatch, 72 | pollbotVersion: APIVersionData, 73 | shouldRefresh: boolean, 74 | login: Login, 75 | errors: Error[], 76 | }; 77 | export class App extends React.Component { 78 | refreshIntervalId: ?IntervalID; 79 | 80 | constructor(props: AppProps): void { 81 | super(props); 82 | this.refreshIntervalId = null; 83 | } 84 | 85 | setUpAutoRefresh(): void { 86 | if (this.props.shouldRefresh) { 87 | if (this.refreshIntervalId) { 88 | // The auto-refresh is already enabled. 89 | return; 90 | } 91 | this.refreshIntervalId = setInterval( 92 | () => this.props.dispatch(refreshStatus()), 93 | 60000, 94 | ); 95 | } else { 96 | this.stopAutoRefresh(); 97 | } 98 | } 99 | 100 | stopAutoRefresh(): void { 101 | if (this.refreshIntervalId) { 102 | clearInterval(this.refreshIntervalId); 103 | this.refreshIntervalId = null; 104 | } 105 | } 106 | 107 | onUserInfo = (userInfo: any): void => { 108 | this.props.dispatch(updateUserInfo(userInfo)); 109 | }; 110 | 111 | onLoggedIn = (): void => { 112 | this.props.dispatch(loggedIn()); 113 | fetchUserInfo(this.onUserInfo); 114 | }; 115 | 116 | componentDidMount(): void { 117 | this.props.dispatch(requestPollbotVersion()); 118 | this.props.dispatch(requestOngoingVersions()); 119 | // Setup notifications. 120 | requestNotificationPermission(); 121 | // Listen to url hash changes. 122 | window.onhashchange = this.versionFromHash; 123 | // Check if we have a version in the url. 124 | this.versionFromHash(); 125 | // If we just came back from an auth0 login, we should have the needed info 126 | // in the hash. 127 | checkLogin(this.onLoggedIn); 128 | // Maybe we were already logged in. 129 | if (isAuthenticated()) { 130 | this.onLoggedIn(); 131 | } 132 | } 133 | 134 | componentDidUpdate(): void { 135 | this.setUpAutoRefresh(); 136 | } 137 | 138 | componentWillUnmount(): void { 139 | this.stopAutoRefresh(); 140 | } 141 | 142 | versionFromHash = (): void => { 143 | const parsedUrl = parseUrl(window.location.hash); 144 | if (parsedUrl) { 145 | this.props.dispatch(requestStatus(parsedUrl.product, parsedUrl.version)); 146 | } 147 | }; 148 | 149 | onLoginRequested = (): void => { 150 | this.props.dispatch(requestLogin()); 151 | }; 152 | 153 | onLogoutRequested = (): void => { 154 | this.props.dispatch(requestLogout()); 155 | }; 156 | 157 | render() { 158 | return ( 159 |
160 |
161 |

162 | Delivery Dashboard 163 |

164 | {/* We don't need the login button yet, and don't have an auth0 account/profile for it 165 |
166 | 171 |
172 | */} 173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 |
184 | Delivery dashboard version:{' '} 185 | 186 |  -- Pollbot version:{' '} 187 | 188 |
189 |
190 | ); 191 | } 192 | } 193 | const connectedAppMapStateToProps: MapStateToProps<*, *, *> = ( 194 | state: State, 195 | ) => ({ 196 | checkResults: state.checkResults, 197 | pollbotVersion: state.pollbotVersion, 198 | shouldRefresh: state.shouldRefresh, 199 | login: state.login, 200 | errors: state.errors, 201 | }); 202 | export const ConnectedApp = connect( 203 | connectedAppMapStateToProps, 204 | (dispatch: Dispatch) => ({dispatch: dispatch}), 205 | )(App); 206 | 207 | type LoginButtonProps = { 208 | onLoginRequested: () => void, 209 | onLogoutRequested: () => void, 210 | loginState: Login, 211 | }; 212 | 213 | export function LoginButton({ 214 | onLoginRequested, 215 | onLogoutRequested, 216 | loginState, 217 | }: LoginButtonProps) { 218 | switch (loginState) { 219 | case LOGGED_IN: 220 | return ( 221 | 224 | ); 225 | case LOGIN_REQUESTED: 226 | return ( 227 | 230 | ); 231 | case LOGGED_OUT: 232 | default: 233 | return ( 234 | 237 | ); 238 | } 239 | } 240 | 241 | const sideBarMapStateToProps: MapStateToProps<*, *, *> = (state: State) => ({ 242 | versions: state.productVersions, 243 | }); 244 | const SideBar = connect(sideBarMapStateToProps)(ReleasesMenu); 245 | 246 | type ReleasesMenuPropType = { 247 | versions: ProductVersions, 248 | }; 249 | 250 | export function ReleasesMenu({versions}: ReleasesMenuPropType) { 251 | const getVersion = (product, channel) => { 252 | const capitalizedChannel = capitalize(channel); 253 | if (versions.hasOwnProperty(product) && versions[product][channel]) { 254 | return ( 255 | {`${capitalizedChannel}: ${versions[product][channel]}`} 258 | ); 259 | } else { 260 | return ( 261 | 262 | {capitalizedChannel}: 263 | 264 | ); 265 | } 266 | }; 267 | return ( 268 |
269 |

Firefox Releases

270 |
    271 |
  • {getVersion('firefox', 'nightly')}
  • 272 |
  • {getVersion('firefox', 'beta')}
  • 273 |
  • {getVersion('devedition', 'devedition')}
  • 274 |
  • {getVersion('firefox', 'release')}
  • 275 |
  • {getVersion('firefox', 'esr')}
  • 276 |
277 |
278 | ); 279 | } 280 | 281 | const currentReleaseMapStateToProps: MapStateToProps<*, *, *> = ( 282 | state: State, 283 | ) => ({ 284 | checkResults: state.checkResults, 285 | releaseInfo: state.releaseInfo, 286 | productVersion: state.version, 287 | }); 288 | const CurrentRelease = connect(currentReleaseMapStateToProps)(Dashboard); 289 | 290 | type ErrorsPropType = { 291 | errors: Error[], 292 | }; 293 | 294 | export function Errors({errors}: ErrorsPropType) { 295 | if (!errors || errors.length === 0) { 296 | return null; 297 | } 298 | return ( 299 |
300 | {errors.map(error => { 301 | const [title, err] = error; 302 | return ( 303 | 309 | ); 310 | })} 311 |
312 |
313 | ); 314 | } 315 | 316 | type DashboardPropType = { 317 | checkResults: CheckResults, 318 | releaseInfo: ?ReleaseInfo, 319 | productVersion: [Product, string], 320 | }; 321 | 322 | export function Dashboard({ 323 | releaseInfo, 324 | checkResults, 325 | productVersion, 326 | }: DashboardPropType) { 327 | const [product, version] = productVersion; 328 | if (version === '') { 329 | return ( 330 |

331 | Learn more about a specific version. 332 | Select a version number from the left menu. 333 |

334 | ); 335 | } else if (!releaseInfo) { 336 | return ; 337 | } else if (releaseInfo.message) { 338 | return ; 339 | } else { 340 | return ( 341 |
342 |

343 | {capitalize(product)} {version}{' '} 344 | 348 |

349 |
350 | {releaseInfo.checks.map(check => 351 | // Map on the checklist to display the results in the same order. 352 | DisplayCheckResult( 353 | check.title, 354 | check.actionable, 355 | checkResults[check.title], 356 | ), 357 | )} 358 |
359 |
360 | ); 361 | } 362 | } 363 | 364 | type OverallStatusPropType = { 365 | checkResults: CheckResults, 366 | releaseInfo: ReleaseInfo, 367 | }; 368 | 369 | export function OverallStatus({ 370 | releaseInfo, 371 | checkResults, 372 | }: OverallStatusPropType) { 373 | const checksStatus = releaseInfo.checks.map( 374 | check => checkResults[check.title], 375 | ); 376 | const allChecksCompleted = !checksStatus.some( 377 | result => typeof result === 'undefined', 378 | ); 379 | if (!allChecksCompleted) { 380 | return ; 381 | } 382 | 383 | let actionableChecks = []; 384 | let nonActionableChecks = []; 385 | releaseInfo.checks.map(check => { 386 | if (check.actionable) { 387 | actionableChecks.push(checkResults[check.title].status); 388 | } else { 389 | nonActionableChecks.push(checkResults[check.title].status); 390 | } 391 | return check; 392 | }); 393 | let type; 394 | let message; 395 | if (actionableChecks.some(status => status !== 'exists')) { 396 | type = 'error'; 397 | message = 'Some checks failed'; 398 | } else { 399 | type = 'success'; 400 | message = 'All checks are successful'; 401 | } 402 | return ( 403 | 404 | ); 405 | } 406 | 407 | export function DisplayCheckResult( 408 | title: string, 409 | actionable: boolean, 410 | checkResult: ?CheckResult, 411 | ) { 412 | let titleContent = title; 413 | if (!actionable) { 414 | titleContent = ( 415 |
416 | 417 | {title} 418 | 419 |
420 | ); 421 | } 422 | return ( 423 | 429 | {checkResult ? ( 430 | 436 | ) : ( 437 | 438 | )} 439 | 440 | ); 441 | } 442 | 443 | export function DisplayStatus({ 444 | status, 445 | message, 446 | url, 447 | actionable, 448 | }: { 449 | status: Status, 450 | message: string, 451 | url: string, 452 | actionable: boolean, 453 | }) { 454 | const getLabelClass = (status, actionable) => { 455 | if (status === 'error') { 456 | return 'error'; 457 | } 458 | if (status === 'exists') { 459 | return 'success'; 460 | } 461 | if (actionable) { 462 | return 'warning'; 463 | } 464 | return 'info'; // It's a non actionable item. 465 | }; 466 | return ( 467 | 468 | 473 | 474 | ); 475 | } 476 | 477 | function VersionLink({versionData}: {versionData: APIVersionData}) { 478 | if (!versionData) { 479 | return null; 480 | } 481 | const {commit, source, version} = versionData; 482 | const sourceUrl = source.replace(/\.git/, ''); 483 | const url = `${sourceUrl}/commit/${commit}`; 484 | return {version}; 485 | } 486 | 487 | export default ConnectedApp; 488 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-flowtype_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 9b9f4b2213dcf5733183fc4dac75db5a 2 | // flow-typed version: <>/eslint-plugin-flowtype_v^2.35.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-flowtype' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-flowtype' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-flowtype/bin/readmeAssertions' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-flowtype/dist/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-flowtype/dist/rules/booleanStyle' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-flowtype/dist/rules/defineFlowType' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-flowtype/dist/rules/delimiterDangle' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-flowtype/dist/rules/genericSpacing' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-flowtype/dist/rules/noDupeKeys' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-flowtype/dist/rules/noPrimitiveConstructorTypes' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-flowtype/dist/rules/noTypesMissingFileAnnotation' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-flowtype/dist/rules/noWeakTypes' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-flowtype/dist/rules/requireParameterType' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-flowtype/dist/rules/requireReturnType' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-flowtype/dist/rules/requireValidFileAnnotation' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-flowtype/dist/rules/requireVariableType' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-flowtype/dist/rules/semi' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-flowtype/dist/rules/sortKeys' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-flowtype/dist/rules/spaceAfterTypeColon' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeGenericBracket' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeTypeColon' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateFunctions' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeIndexer' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeProperty' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateReturnType' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypeCastExpression' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypical' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/index' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/reporter' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-plugin-flowtype/dist/rules/typeIdMatch' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-plugin-flowtype/dist/rules/unionIntersectionSpacing' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-plugin-flowtype/dist/rules/useFlowType' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-plugin-flowtype/dist/rules/validSyntax' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'eslint-plugin-flowtype/dist/utilities/checkFlowFileAnnotation' { 154 | declare module.exports: any; 155 | } 156 | 157 | declare module 'eslint-plugin-flowtype/dist/utilities/fuzzyStringMatch' { 158 | declare module.exports: any; 159 | } 160 | 161 | declare module 'eslint-plugin-flowtype/dist/utilities/getParameterName' { 162 | declare module.exports: any; 163 | } 164 | 165 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenAfterParens' { 166 | declare module.exports: any; 167 | } 168 | 169 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenBeforeParens' { 170 | declare module.exports: any; 171 | } 172 | 173 | declare module 'eslint-plugin-flowtype/dist/utilities/index' { 174 | declare module.exports: any; 175 | } 176 | 177 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFile' { 178 | declare module.exports: any; 179 | } 180 | 181 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFileAnnotation' { 182 | declare module.exports: any; 183 | } 184 | 185 | declare module 'eslint-plugin-flowtype/dist/utilities/iterateFunctionNodes' { 186 | declare module.exports: any; 187 | } 188 | 189 | declare module 'eslint-plugin-flowtype/dist/utilities/quoteName' { 190 | declare module.exports: any; 191 | } 192 | 193 | declare module 'eslint-plugin-flowtype/dist/utilities/spacingFixers' { 194 | declare module.exports: any; 195 | } 196 | 197 | // Filename aliases 198 | declare module 'eslint-plugin-flowtype/bin/readmeAssertions.js' { 199 | declare module.exports: $Exports<'eslint-plugin-flowtype/bin/readmeAssertions'>; 200 | } 201 | declare module 'eslint-plugin-flowtype/dist/index.js' { 202 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/index'>; 203 | } 204 | declare module 'eslint-plugin-flowtype/dist/rules/booleanStyle.js' { 205 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/booleanStyle'>; 206 | } 207 | declare module 'eslint-plugin-flowtype/dist/rules/defineFlowType.js' { 208 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/defineFlowType'>; 209 | } 210 | declare module 'eslint-plugin-flowtype/dist/rules/delimiterDangle.js' { 211 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/delimiterDangle'>; 212 | } 213 | declare module 'eslint-plugin-flowtype/dist/rules/genericSpacing.js' { 214 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/genericSpacing'>; 215 | } 216 | declare module 'eslint-plugin-flowtype/dist/rules/noDupeKeys.js' { 217 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noDupeKeys'>; 218 | } 219 | declare module 'eslint-plugin-flowtype/dist/rules/noPrimitiveConstructorTypes.js' { 220 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noPrimitiveConstructorTypes'>; 221 | } 222 | declare module 'eslint-plugin-flowtype/dist/rules/noTypesMissingFileAnnotation.js' { 223 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noTypesMissingFileAnnotation'>; 224 | } 225 | declare module 'eslint-plugin-flowtype/dist/rules/noWeakTypes.js' { 226 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noWeakTypes'>; 227 | } 228 | declare module 'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter.js' { 229 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter'>; 230 | } 231 | declare module 'eslint-plugin-flowtype/dist/rules/requireParameterType.js' { 232 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireParameterType'>; 233 | } 234 | declare module 'eslint-plugin-flowtype/dist/rules/requireReturnType.js' { 235 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireReturnType'>; 236 | } 237 | declare module 'eslint-plugin-flowtype/dist/rules/requireValidFileAnnotation.js' { 238 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireValidFileAnnotation'>; 239 | } 240 | declare module 'eslint-plugin-flowtype/dist/rules/requireVariableType.js' { 241 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireVariableType'>; 242 | } 243 | declare module 'eslint-plugin-flowtype/dist/rules/semi.js' { 244 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/semi'>; 245 | } 246 | declare module 'eslint-plugin-flowtype/dist/rules/sortKeys.js' { 247 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/sortKeys'>; 248 | } 249 | declare module 'eslint-plugin-flowtype/dist/rules/spaceAfterTypeColon.js' { 250 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/spaceAfterTypeColon'>; 251 | } 252 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeGenericBracket.js' { 253 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/spaceBeforeGenericBracket'>; 254 | } 255 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeTypeColon.js' { 256 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/spaceBeforeTypeColon'>; 257 | } 258 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateFunctions.js' { 259 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateFunctions'>; 260 | } 261 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeIndexer.js' { 262 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeIndexer'>; 263 | } 264 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeProperty.js' { 265 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeProperty'>; 266 | } 267 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateReturnType.js' { 268 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateReturnType'>; 269 | } 270 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypeCastExpression.js' { 271 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypeCastExpression'>; 272 | } 273 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypical.js' { 274 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypical'>; 275 | } 276 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/index.js' { 277 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/index'>; 278 | } 279 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/reporter.js' { 280 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/reporter'>; 281 | } 282 | declare module 'eslint-plugin-flowtype/dist/rules/typeIdMatch.js' { 283 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeIdMatch'>; 284 | } 285 | declare module 'eslint-plugin-flowtype/dist/rules/unionIntersectionSpacing.js' { 286 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/unionIntersectionSpacing'>; 287 | } 288 | declare module 'eslint-plugin-flowtype/dist/rules/useFlowType.js' { 289 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/useFlowType'>; 290 | } 291 | declare module 'eslint-plugin-flowtype/dist/rules/validSyntax.js' { 292 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/validSyntax'>; 293 | } 294 | declare module 'eslint-plugin-flowtype/dist/utilities/checkFlowFileAnnotation.js' { 295 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/checkFlowFileAnnotation'>; 296 | } 297 | declare module 'eslint-plugin-flowtype/dist/utilities/fuzzyStringMatch.js' { 298 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/fuzzyStringMatch'>; 299 | } 300 | declare module 'eslint-plugin-flowtype/dist/utilities/getParameterName.js' { 301 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/getParameterName'>; 302 | } 303 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenAfterParens.js' { 304 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/getTokenAfterParens'>; 305 | } 306 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenBeforeParens.js' { 307 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/getTokenBeforeParens'>; 308 | } 309 | declare module 'eslint-plugin-flowtype/dist/utilities/index.js' { 310 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/index'>; 311 | } 312 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFile.js' { 313 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/isFlowFile'>; 314 | } 315 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFileAnnotation.js' { 316 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/isFlowFileAnnotation'>; 317 | } 318 | declare module 'eslint-plugin-flowtype/dist/utilities/iterateFunctionNodes.js' { 319 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/iterateFunctionNodes'>; 320 | } 321 | declare module 'eslint-plugin-flowtype/dist/utilities/quoteName.js' { 322 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/quoteName'>; 323 | } 324 | declare module 'eslint-plugin-flowtype/dist/utilities/spacingFixers.js' { 325 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/spacingFixers'>; 326 | } 327 | -------------------------------------------------------------------------------- /src/sagas.test.js: -------------------------------------------------------------------------------- 1 | import {all, call, put, select, takeEvery} from 'redux-saga/effects'; 2 | import {cloneableGenerator} from 'redux-saga/utils'; 3 | import { 4 | checkStatus, 5 | getOngoingVersions, 6 | getPollbotVersion, 7 | getReleaseInfo, 8 | } from './PollbotAPI'; 9 | import { 10 | addCheckResult, 11 | addServerError, 12 | loggedOut, 13 | loginRequested, 14 | refreshCheckResult, 15 | setVersion, 16 | updateProductVersions, 17 | updatePollbotVersion, 18 | updateReleaseInfo, 19 | } from './actions'; 20 | import { 21 | REFRESH_STATUS, 22 | REQUEST_LOGIN, 23 | REQUEST_LOGOUT, 24 | REQUEST_ONGOING_VERSIONS, 25 | REQUEST_POLLBOT_VERSION, 26 | REQUEST_STATUS, 27 | UPDATE_URL, 28 | } from './types'; 29 | import { 30 | checkResultAndUpdate, 31 | checkResultAndUpdateAndNotify, 32 | fetchAndUpdateVersions, 33 | fetchOngoingVersions, 34 | fetchPollbotVersion, 35 | refreshStatus, 36 | requestLogin, 37 | requestLogout, 38 | requestStatus, 39 | rootSaga, 40 | updateUrl, 41 | } from './sagas'; 42 | import {login, logout} from './auth0'; 43 | 44 | describe('sagas', () => { 45 | it('handles fetchPollbotVersion', () => { 46 | const data = {}; 47 | data.saga = cloneableGenerator(fetchPollbotVersion)(); 48 | 49 | const pollbotVersion = { 50 | name: 'pollbot', 51 | source: 'https://github.com/mozilla/PollBot.git', 52 | version: '0.2.1-22-g8e09a0f', 53 | commit: '8e09a0f8e995344ea24fbb940a6bddc17e0edaed', 54 | }; 55 | expect(data.saga.next().value).toEqual(call(getPollbotVersion)); 56 | 57 | // Clone to test success and failure of getPollbotVersion. 58 | data.sagaThrow = data.saga.clone(); 59 | 60 | expect(data.saga.next(pollbotVersion).value).toEqual( 61 | put(updatePollbotVersion(pollbotVersion)), 62 | ); 63 | expect(data.saga.next().done).toBe(true); 64 | 65 | console.error = jest.fn(); 66 | data.sagaThrow.throw('error'); 67 | expect(console.error).toHaveBeenCalledWith( 68 | 'Failed getting the pollbot version', 69 | 'error', 70 | ); 71 | expect(data.sagaThrow.next().done).toBe(true); 72 | }); 73 | 74 | it('handles fetchOngoingVersions', () => { 75 | const data = {}; 76 | data.saga = cloneableGenerator(fetchOngoingVersions)(); 77 | 78 | expect(data.saga.next().value).toEqual( 79 | all([ 80 | call(fetchAndUpdateVersions, 'firefox'), 81 | call(fetchAndUpdateVersions, 'devedition'), 82 | ]), 83 | ); 84 | expect(data.saga.next().done).toBe(true); 85 | }); 86 | 87 | it('handles fetchAndUpdateVersions', () => { 88 | const data = {}; 89 | data.saga = cloneableGenerator(fetchAndUpdateVersions)('firefox'); 90 | 91 | const channelVersions = { 92 | nightly: '57.0a1', 93 | beta: '56.0b12', 94 | release: '55.0.3', 95 | esr: '52.3.0esr', 96 | }; 97 | expect(data.saga.next().value).toEqual(call(getOngoingVersions, 'firefox')); 98 | 99 | // Clone to test success and failure of getOngoingVersions. 100 | data.sagaThrow = data.saga.clone(); 101 | 102 | expect(data.saga.next(channelVersions).value).toEqual( 103 | put(updateProductVersions('firefox', channelVersions)), 104 | ); 105 | expect(data.saga.next().done).toBe(true); 106 | 107 | console.error = jest.fn(); 108 | data.sagaThrow.throw('error'); 109 | expect(console.error).toHaveBeenCalledWith( 110 | 'Failed getting the latest channel versions for product: firefox', 111 | 'error', 112 | ); 113 | expect(data.sagaThrow.next().done).toBe(true); 114 | }); 115 | 116 | it('handles updateUrl', () => { 117 | const saga = updateUrl(); 118 | 119 | expect(saga.next().value).toEqual(select()); 120 | expect(window.location.hash).not.toEqual('#pollbot/firefox/50.0'); 121 | saga.next({version: ['firefox', '50.0']}); 122 | expect(window.location.hash).toEqual('#pollbot/firefox/50.0'); 123 | }); 124 | 125 | it('notifies using checkResultAndUpdateAndNotify', () => { 126 | // Mock the Notification API call. 127 | global.Notification = jest.fn(); 128 | global.Notification.permission = 'granted'; 129 | 130 | const checkResult = { 131 | status: 'exists', 132 | message: 'check succesful', 133 | link: 'some link', 134 | }; 135 | const checkResultFailing = { 136 | status: 'incomplete', 137 | message: 'check incomplete', 138 | link: 'some link', 139 | }; 140 | 141 | const data = {}; 142 | data.saga = cloneableGenerator(checkResultAndUpdateAndNotify)( 143 | 'some test', 144 | 'some url', 145 | checkResultFailing, 146 | ); 147 | 148 | expect(data.saga.next().value).toEqual( 149 | put(refreshCheckResult('some test')), 150 | ); 151 | 152 | expect(data.saga.next().value).toEqual( 153 | call(checkResultAndUpdate, 'some test', 'some url'), 154 | ); 155 | expect(data.saga.next().value).toEqual(select()); 156 | 157 | // Clone to test the branch where there's no change in the result. 158 | data.sagaResultUnchanged = data.saga.clone(); 159 | 160 | // No notification if the result hasn't changed. 161 | expect( 162 | data.sagaResultUnchanged.next({ 163 | checkResults: {'some test': checkResultFailing}, 164 | }).done, 165 | ).toBe(true); 166 | expect(global.Notification).toHaveBeenCalledTimes(0); 167 | 168 | // Notify if the result has changed. 169 | expect( 170 | data.saga.next({ 171 | checkResults: {'some test': checkResult}, 172 | }).done, 173 | ).toBe(true); 174 | expect(global.Notification).toHaveBeenCalledTimes(1); 175 | expect(global.Notification).toHaveBeenCalledWith( 176 | 'some test: status changed (exists).', 177 | ); 178 | }); 179 | 180 | it('handles refreshStatus', () => { 181 | const data = {}; 182 | data.saga = cloneableGenerator(refreshStatus)(); 183 | 184 | const releaseInfo = { 185 | channel: 'release', 186 | product: 'firefox', 187 | version: '50.0', 188 | checks: [ 189 | { 190 | title: 'some test', 191 | url: 'some url', 192 | }, 193 | { 194 | title: 'some other test', 195 | url: 'some other url', 196 | }, 197 | ], 198 | }; 199 | const checkResult = { 200 | status: 'exists', 201 | message: 'check succesful', 202 | link: 'some link', 203 | }; 204 | const checkResultFailing = { 205 | status: 'incomplete', 206 | message: 'check incomplete', 207 | link: 'some link', 208 | }; 209 | 210 | expect(data.saga.next().value).toEqual(select()); 211 | 212 | expect( 213 | data.saga.next({ 214 | version: '50.0', 215 | releaseInfo: releaseInfo, 216 | checkResults: { 217 | 'some test': checkResult, 218 | 'some other test': checkResultFailing, 219 | }, 220 | }).value, 221 | ).toEqual( 222 | all([ 223 | call( 224 | checkResultAndUpdateAndNotify, 225 | 'some other test', 226 | 'some other url', 227 | checkResultFailing, 228 | ), 229 | ]), 230 | ); 231 | expect(data.saga.next().done).toBe(true); 232 | }); 233 | 234 | it('checks result and updates state using checkResultAndUpdate', () => { 235 | const data = {}; 236 | data.saga = cloneableGenerator(checkResultAndUpdate)( 237 | 'some test', 238 | 'some url', 239 | ); 240 | 241 | const checkResult = { 242 | status: 'exists', 243 | message: 'check succesful', 244 | link: 'some link', 245 | }; 246 | 247 | expect(data.saga.next().value).toEqual(call(checkStatus, 'some url')); 248 | 249 | // Clone to test success and failure of checkStatus. 250 | data.sagaThrow = data.saga.clone(); 251 | 252 | // checkStatus throws an error. 253 | console.error = jest.fn(); 254 | expect(data.sagaThrow.throw('error').value).toEqual( 255 | put(addServerError('some test', 'error')), 256 | ); 257 | expect(console.error).toHaveBeenCalledWith( 258 | 'Failed getting some test check result', 259 | 'error', 260 | ); 261 | expect(data.sagaThrow.next().done).toBe(true); 262 | 263 | // checkStatus completes correctly. 264 | expect(data.saga.next(checkResult).value).toEqual( 265 | put(addCheckResult('some test', checkResult)), 266 | ); 267 | }); 268 | 269 | it('handles requestStatus', () => { 270 | const data = {}; 271 | data.saga = cloneableGenerator(requestStatus)({ 272 | product: 'firefox', 273 | version: '50.0', 274 | }); 275 | 276 | const releaseInfo = { 277 | channel: 'release', 278 | product: 'firefox', 279 | version: '50.0', 280 | checks: [ 281 | { 282 | title: 'some test', 283 | url: 'some url', 284 | }, 285 | { 286 | title: 'some other test', 287 | url: 'some other url', 288 | }, 289 | ], 290 | }; 291 | 292 | expect(data.saga.next().value).toEqual(select()); 293 | expect( 294 | data.saga.next({ 295 | productVersions: { 296 | firefox: { 297 | release: '50.0', 298 | }, 299 | }, 300 | }).value, 301 | ).toEqual(put(setVersion('firefox', '50.0'))); 302 | expect(data.saga.next().value).toEqual(call(updateUrl)); 303 | expect(data.saga.next().value).toEqual( 304 | call(getReleaseInfo, 'firefox', '50.0'), 305 | ); 306 | 307 | // Clone to test success and failure of getReleaseInfo. 308 | data.sagaThrow = data.saga.clone(); 309 | 310 | // getReleaseInfo throws an error. 311 | console.error = jest.fn(); 312 | data.sagaThrow.throw('error'); 313 | expect(console.error).toHaveBeenCalledWith( 314 | 'Failed getting the release info for firefox 50.0', 315 | 'error', 316 | ); 317 | expect(data.sagaThrow.next().done).toBe(true); 318 | 319 | // getReleaseInfo completes correctly. 320 | expect(data.saga.next(releaseInfo).value).toEqual( 321 | put(updateReleaseInfo(releaseInfo)), 322 | ); 323 | expect(data.saga.next().value).toEqual( 324 | all([ 325 | call(checkResultAndUpdate, 'some test', 'some url'), 326 | call(checkResultAndUpdate, 'some other test', 'some other url'), 327 | ]), 328 | ); 329 | expect(data.saga.next().done).toBe(true); 330 | }); 331 | 332 | it('handles requestStatus with a canonical url (using the channel)', () => { 333 | const data = {}; 334 | // Request status for "release", it should in turn set version for "50.0". 335 | data.saga = cloneableGenerator(requestStatus)({ 336 | product: 'firefox', 337 | version: 'release', 338 | }); 339 | 340 | const releaseInfo = { 341 | channel: 'release', 342 | product: 'firefox', 343 | version: '50.0', 344 | checks: [ 345 | { 346 | title: 'some test', 347 | url: 'some url', 348 | }, 349 | { 350 | title: 'some other test', 351 | url: 'some other url', 352 | }, 353 | ], 354 | }; 355 | 356 | expect(data.saga.next().value).toEqual(select()); 357 | expect( 358 | data.saga.next({ 359 | productVersions: { 360 | firefox: { 361 | release: '50.0', 362 | }, 363 | }, 364 | }).value, 365 | ).toEqual(put(setVersion('firefox', '50.0'))); 366 | expect(data.saga.next().value).toEqual(call(updateUrl)); 367 | expect(data.saga.next().value).toEqual( 368 | call(getReleaseInfo, 'firefox', '50.0'), 369 | ); 370 | 371 | // Clone to test success and failure of getReleaseInfo. 372 | data.sagaThrow = data.saga.clone(); 373 | 374 | // getReleaseInfo throws an error. 375 | console.error = jest.fn(); 376 | data.sagaThrow.throw('error'); 377 | expect(console.error).toHaveBeenCalledWith( 378 | 'Failed getting the release info for firefox 50.0', 379 | 'error', 380 | ); 381 | expect(data.sagaThrow.next().done).toBe(true); 382 | 383 | // getReleaseInfo completes correctly. 384 | expect(data.saga.next(releaseInfo).value).toEqual( 385 | put(updateReleaseInfo(releaseInfo)), 386 | ); 387 | expect(data.saga.next().value).toEqual( 388 | all([ 389 | call(checkResultAndUpdate, 'some test', 'some url'), 390 | call(checkResultAndUpdate, 'some other test', 'some other url'), 391 | ]), 392 | ); 393 | expect(data.saga.next().done).toBe(true); 394 | }); 395 | 396 | it('handles requestStatus with a canonical url (using the channel) with a cold cache', () => { 397 | const data = {}; 398 | // Request status for "release", it should in turn set version for "50.0". 399 | data.saga = cloneableGenerator(requestStatus)({ 400 | product: 'firefox', 401 | version: 'release', 402 | }); 403 | 404 | const releaseInfo = { 405 | channel: 'release', 406 | product: 'firefox', 407 | version: '50.0', 408 | checks: [ 409 | { 410 | title: 'some test', 411 | url: 'some url', 412 | }, 413 | { 414 | title: 'some other test', 415 | url: 'some other url', 416 | }, 417 | ], 418 | }; 419 | 420 | expect(data.saga.next().value).toEqual(select()); 421 | expect(data.saga.next({productVersions: {}}).value).toEqual( 422 | call(getOngoingVersions, 'firefox'), 423 | ); 424 | expect(data.saga.next({release: '50.0'}).value).toEqual( 425 | put(updateProductVersions('firefox', {release: '50.0'})), 426 | ); 427 | 428 | expect(data.saga.next().value).toEqual(select()); 429 | expect( 430 | data.saga.next({ 431 | productVersions: {firefox: {release: '50.0'}}, 432 | }).value, 433 | ).toEqual(put(setVersion('firefox', '50.0'))); 434 | expect(data.saga.next().value).toEqual(call(updateUrl)); 435 | expect(data.saga.next().value).toEqual( 436 | call(getReleaseInfo, 'firefox', '50.0'), 437 | ); 438 | 439 | // Clone to test success and failure of getReleaseInfo. 440 | data.sagaThrow = data.saga.clone(); 441 | 442 | // getReleaseInfo throws an error. 443 | console.error = jest.fn(); 444 | data.sagaThrow.throw('error'); 445 | expect(console.error).toHaveBeenCalledWith( 446 | 'Failed getting the release info for firefox 50.0', 447 | 'error', 448 | ); 449 | expect(data.sagaThrow.next().done).toBe(true); 450 | 451 | // getReleaseInfo completes correctly. 452 | expect(data.saga.next(releaseInfo).value).toEqual( 453 | put(updateReleaseInfo(releaseInfo)), 454 | ); 455 | expect(data.saga.next().value).toEqual( 456 | all([ 457 | call(checkResultAndUpdate, 'some test', 'some url'), 458 | call(checkResultAndUpdate, 'some other test', 'some other url'), 459 | ]), 460 | ); 461 | expect(data.saga.next().done).toBe(true); 462 | }); 463 | 464 | it('handles requestLogin', () => { 465 | const data = {}; 466 | data.saga = cloneableGenerator(requestLogin)(); 467 | 468 | expect(data.saga.next().value).toEqual(put(loginRequested())); 469 | expect(data.saga.next().value).toEqual(call(login)); 470 | 471 | // Clone to test success and failure of getReleaseInfo. 472 | data.sagaThrow = data.saga.clone(); 473 | 474 | // login throws an error. 475 | console.error = jest.fn(); 476 | expect(data.sagaThrow.throw('error').value).toEqual(put(loggedOut())); 477 | expect(console.error).toHaveBeenCalledWith('Login failed', 'error'); 478 | expect(data.sagaThrow.next().done).toBe(true); 479 | 480 | // login completes correctly. 481 | expect(data.saga.next().done).toBe(true); 482 | }); 483 | 484 | it('handles requestLogout', () => { 485 | const data = {}; 486 | data.saga = cloneableGenerator(requestLogout)(); 487 | 488 | expect(data.saga.next().value).toEqual(call(logout)); 489 | expect(data.saga.next().value).toEqual(put(loggedOut())); 490 | 491 | // Clone to test success and failure of getReleaseInfo. 492 | data.sagaThrow = data.saga.clone(); 493 | 494 | // login throws an error. 495 | console.error = jest.fn(); 496 | expect(data.sagaThrow.throw('error').done).toBe(true); 497 | expect(console.error).toHaveBeenCalledWith('Logout failed', 'error'); 498 | 499 | // login completes correctly. 500 | expect(data.saga.next().done).toBe(true); 501 | }); 502 | }); 503 | 504 | describe('rootSaga', () => { 505 | it('uses takeEvery on each saga available', () => { 506 | const saga = rootSaga(); 507 | expect(saga.next().value).toEqual( 508 | all([ 509 | takeEvery(REQUEST_ONGOING_VERSIONS, fetchOngoingVersions), 510 | takeEvery(REQUEST_POLLBOT_VERSION, fetchPollbotVersion), 511 | takeEvery(UPDATE_URL, updateUrl), 512 | takeEvery(REFRESH_STATUS, refreshStatus), 513 | takeEvery(REQUEST_STATUS, requestStatus), 514 | takeEvery(REQUEST_LOGIN, requestLogin), 515 | takeEvery(REQUEST_LOGOUT, requestLogout), 516 | ]), 517 | ); 518 | expect(saga.next().done).toBe(true); 519 | }); 520 | }); 521 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------