├── public ├── javascripts │ └── main.js ├── stylesheets │ └── main.css └── images │ └── favicon.png ├── project ├── build.properties ├── scaffold.sbt └── plugins.sbt ├── ansible ├── group_vars │ └── link-mobile-observations │ │ └── vars ├── dev │ ├── hosts │ └── group_vars │ │ └── all │ │ └── vars ├── prod │ ├── hosts │ └── group_vars │ │ └── all │ │ └── vars ├── qa │ ├── hosts │ └── group_vars │ │ └── all │ │ └── vars ├── local │ └── hosts ├── README.md ├── link-mobile-observations-build.yml ├── link-mobile-observations.yml ├── requirements.yml └── ansible.cfg ├── ui ├── test │ ├── mocha.opts │ ├── resources │ │ ├── WrittenFileRecord.json │ │ ├── ReceivedFileFailure.json │ │ ├── JoyrideStep.json │ │ ├── WrittenFileOrder.json │ │ ├── SearchedReceivedFileFailedRecord.json │ │ ├── SummariesOrder.json │ │ ├── ReceivedFileFailedRecord.json │ │ ├── PortInOrders.json │ │ └── WrittenFileState.js │ ├── components │ │ └── presentational │ │ │ ├── CountBoxTest.js │ │ │ ├── LoginErrorTest.js │ │ │ ├── LoginModalTest.js │ │ │ ├── DropDownItemTest.js │ │ │ ├── HeaderButtonTest.js │ │ │ ├── IconButtonTest.js │ │ │ ├── IndexContainerTest.js │ │ │ ├── TableSearchBoxTest.js │ │ │ ├── DataTableRowTest.js │ │ │ ├── DropDownDividerTest.js │ │ │ ├── FileRecordModalTest.js │ │ │ ├── HeaderContainerTest.js │ │ │ ├── ContentContainerTest.js │ │ │ ├── DataTableHeadTest.js │ │ │ ├── DropDownContainerTest.js │ │ │ ├── GridColumnWrapperTest.js │ │ │ ├── DataTableContainerTest.js │ │ │ ├── CircleThingyContainerTest.js │ │ │ ├── LoginFieldTest.js │ │ │ ├── DataTableToggleTest.js │ │ │ └── LoginBoxTest.js │ ├── loginTest.js │ ├── util │ │ ├── TableIconsTest.js │ │ ├── DefinedPathUtilsTest.js │ │ └── EqualTest.js │ ├── actions │ │ ├── JoyrideActionTest.js │ │ ├── LoginActionTest.js │ │ ├── ControlActionTest.js │ │ └── DataActionTest.js │ └── reducers │ │ ├── LoginReducerTest.js │ │ ├── JoyrideReducerTest.js │ │ ├── ControlReducerTest.js │ │ └── DataReducerTest.js ├── app │ └── src │ │ ├── images │ │ ├── Paddington_Icon.png │ │ ├── Paddington_Logo.png │ │ ├── PaddingtonBackground.png │ │ ├── PaddingtonBackground.xcf │ │ └── PaddingtonBackground-NoLogo.xcf │ │ ├── resources │ │ └── mappings │ │ │ ├── joyride │ │ │ ├── JRTypeMapping.json │ │ │ ├── index.js │ │ │ ├── GeneralJRMapping.json │ │ │ ├── PortInExpectedJRMapping.json │ │ │ ├── SubPortDashJRMapping.json │ │ │ ├── PortOutDashJRMapping.json │ │ │ └── PortInDashJRMapping.json │ │ │ ├── WrittenFileRecordMapping.json │ │ │ ├── ReceivedFileFailureMapping.json │ │ │ ├── WrittenFileMapping.json │ │ │ ├── SubPortExpectedMapping.json │ │ │ ├── PortInDashMapping.json │ │ │ ├── PortOutExpectedMapping.json │ │ │ ├── ReceivedFileRecordMapping.json │ │ │ ├── SubPortErrorMapping.json │ │ │ ├── ReceivedFileMapping.json │ │ │ ├── PortOutErrorMapping.json │ │ │ ├── PortInErrorMapping.json │ │ │ └── PortInExpectedMapping.json │ │ ├── scripts │ │ ├── actions │ │ │ ├── index.js │ │ │ ├── JoyrideAction.js │ │ │ ├── ControlAction.js │ │ │ ├── DataAction.js │ │ │ └── LoginAction.js │ │ ├── components │ │ │ ├── presentational │ │ │ │ ├── DropDownDivider.js │ │ │ │ ├── GridColumnWrapper.js │ │ │ │ ├── DataTableHead.js │ │ │ │ ├── HeaderButton.js │ │ │ │ ├── IndexContainer.js │ │ │ │ ├── LoginModal.js │ │ │ │ ├── CountBox.js │ │ │ │ ├── DataTableRow.js │ │ │ │ ├── DropDownItem.js │ │ │ │ ├── CircleThingyContainer.js │ │ │ │ ├── DataTableContainer.js │ │ │ │ ├── LoginError.js │ │ │ │ ├── DataTableToggle.js │ │ │ │ ├── IconButton.js │ │ │ │ ├── DropDownContainer.js │ │ │ │ ├── LoginField.js │ │ │ │ ├── HeaderContainer.js │ │ │ │ ├── FileRecordModal.js │ │ │ │ ├── TableSearchBox.js │ │ │ │ ├── ContentContainer.js │ │ │ │ ├── TableControlsWithSearchAndCount.js │ │ │ │ └── LoginBox.js │ │ │ ├── Login.jsx │ │ │ ├── Content.jsx │ │ │ ├── FakeCircleThingy.jsx │ │ │ ├── Header.jsx │ │ │ ├── LoginForm.jsx │ │ │ ├── DataTable.jsx │ │ │ └── ModalDataTable.jsx │ │ ├── list.js │ │ ├── login.js │ │ ├── index.js │ │ ├── util │ │ │ ├── TableIcons.js │ │ │ ├── DefinedPathUtils.js │ │ │ ├── Equal.js │ │ │ └── FilterUtils.js │ │ ├── store │ │ │ └── store.js │ │ ├── reducers │ │ │ ├── rootReducer.js │ │ │ ├── loginReducer.js │ │ │ ├── joyrideReducer.js │ │ │ ├── controlReducer.js │ │ │ └── dataReducer.js │ │ └── paddington-router.js │ │ └── styles │ │ ├── _variables.scss │ │ ├── main.scss │ │ └── _header.scss ├── node_modules │ ├── querystring │ │ ├── .Readme.md.un~ │ │ ├── .History.md.un~ │ │ ├── .package.json.un~ │ │ └── test │ │ │ └── .index.js.un~ │ ├── are-we-there-yet │ │ └── CHANGES.md~ │ ├── fsevents │ │ └── node_modules │ │ │ └── are-we-there-yet │ │ │ └── CHANGES.md~ │ └── in-publish │ │ └── README.md~ ├── .notbabelrc └── package.json ├── scripts ├── write-git-version ├── build-sbt-reactjs ├── deploy ├── write-version ├── build ├── prep-deploy └── package ├── .gitignore ├── .idea └── vcs.xml ├── config ├── ebextensions │ └── autoscale.config └── Dockerrun.aws.json ├── app ├── models │ ├── kinesis.scala │ └── model.scala ├── services │ └── HealthCheckService.scala ├── views │ ├── index.scala.html │ ├── login.scala.html │ └── list.scala.html ├── controllers │ ├── HealthController.scala │ └── IndexController.scala ├── modules │ ├── AwsClientsModule.scala │ └── KinesisModule.scala ├── actors │ ├── UserEventActor.scala │ ├── ProxyActor.scala │ └── KmeansActor.scala └── kinesis │ └── Processor.scala ├── defaults └── main.yml ├── samples ├── globalreachtoken.json ├── dwh-response.json └── globalreachcreatesubscriber.json ├── conf ├── routes ├── logback-test.xml ├── logback-prod.xml ├── logback.xml └── application.conf ├── .buildkite └── pipeline.yml └── README.md /public/javascripts/main.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.1 2 | -------------------------------------------------------------------------------- /ansible/group_vars/link-mobile-observations/vars: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /ansible/dev/hosts: -------------------------------------------------------------------------------- 1 | [localhost] 2 | localhost ansible_connection=local 3 | -------------------------------------------------------------------------------- /ansible/prod/hosts: -------------------------------------------------------------------------------- 1 | [localhost] 2 | localhost ansible_connection=local 3 | -------------------------------------------------------------------------------- /ansible/qa/hosts: -------------------------------------------------------------------------------- 1 | [localhost] 2 | localhost ansible_connection=local 3 | -------------------------------------------------------------------------------- /ui/test/mocha.opts: -------------------------------------------------------------------------------- 1 | test 2 | --recursive 3 | --require babel-register 4 | --reporter=nyan -------------------------------------------------------------------------------- /ui/test/resources/WrittenFileRecord.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "transactionNumber": "TRANS1" 4 | } 5 | ] -------------------------------------------------------------------------------- /scripts/write-git-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo GITTAG=`git log --format="%H" -n 1` > git-version.txt -------------------------------------------------------------------------------- /ansible/local/hosts: -------------------------------------------------------------------------------- 1 | [dev-03] 2 | localhost 3 | 4 | [dev-03:children] 5 | eb_deployer 6 | 7 | [eb_deployer] 8 | localhost 9 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /ansible/README.md: -------------------------------------------------------------------------------- 1 | See the [deployment section](../README.md#deployment) of the top-level README for how to set up an agent instance. 2 | -------------------------------------------------------------------------------- /ansible/link-mobile-observations-build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Build 3 | hosts: localhost 4 | vars_files: 5 | - "group_vars/{{ appname }}/vars" -------------------------------------------------------------------------------- /ui/app/src/images/Paddington_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/app/src/images/Paddington_Icon.png -------------------------------------------------------------------------------- /ui/app/src/images/Paddington_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/app/src/images/Paddington_Logo.png -------------------------------------------------------------------------------- /ui/app/src/images/PaddingtonBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/app/src/images/PaddingtonBackground.png -------------------------------------------------------------------------------- /ui/app/src/images/PaddingtonBackground.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/app/src/images/PaddingtonBackground.xcf -------------------------------------------------------------------------------- /ui/node_modules/querystring/.Readme.md.un~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/node_modules/querystring/.Readme.md.un~ -------------------------------------------------------------------------------- /ui/node_modules/querystring/.History.md.un~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/node_modules/querystring/.History.md.un~ -------------------------------------------------------------------------------- /ui/node_modules/querystring/.package.json.un~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/node_modules/querystring/.package.json.un~ -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/JRTypeMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "portInDash":"PID", 3 | "portOutDash":"POD", 4 | "subPortDash":"SPD", 5 | "portInExpected":"PIE" 6 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './ControlAction'; 2 | export * from './DataAction'; 3 | export * from './LoginAction'; 4 | export * from './JoyrideAction'; -------------------------------------------------------------------------------- /ui/node_modules/querystring/test/.index.js.un~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/node_modules/querystring/test/.index.js.un~ -------------------------------------------------------------------------------- /ui/app/src/images/PaddingtonBackground-NoLogo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplymathematics/link-mobile-observations/HEAD/ui/app/src/images/PaddingtonBackground-NoLogo.xcf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | **/target 4 | .g8/ 5 | logs/ 6 | project/target/ 7 | dynamodb-local/ 8 | *.sc 9 | ui/app/dist 10 | ui/node_modules 11 | ui/package-lock.json 12 | npm-debug.log -------------------------------------------------------------------------------- /ui/.notbabelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0" 6 | ], 7 | "env": { 8 | "test": { 9 | "plugins": ["istanbul"] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ansible/link-mobile-observations.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: provision link-mobile-observations 3 | hosts: localhost 4 | vars_files: 5 | - "group_vars/{{ appname }}/vars" 6 | roles: 7 | - eb-deployments 8 | -------------------------------------------------------------------------------- /config/ebextensions/autoscale.config: -------------------------------------------------------------------------------- 1 | Resources: 2 | AWSEBAutoScalingGroup: 3 | Type: "AWS::AutoScaling::AutoScalingGroup" 4 | Properties: 5 | HealthCheckType: "ELB" 6 | HealthCheckGracePeriod: "600" -------------------------------------------------------------------------------- /scripts/build-sbt-reactjs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | rm -rf sbt-react 5 | mkdir -p sbt-react 6 | cd sbt-react 7 | git clone git@github.com:dispalt/sbt-reactjs.git 8 | cd sbt-reactjs 9 | sbt publishLocal 10 | pwd 11 | cd ../.. -------------------------------------------------------------------------------- /scripts/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | LINK_ENV=${LINK_ENV:-dev} 6 | APPNAME=${APPNAME:-link-mobile-observations} 7 | 8 | echo Beginning deploy to $LINK_ENV 9 | 10 | /tmp/${APPNAME}-output-${LINK_ENV}/eb-deploy.sh 11 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DropDownDivider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DropDownDivider = () => { 4 | return ( 5 |
  • 6 | ); 7 | }; 8 | 9 | export default DropDownDivider; -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/index.js: -------------------------------------------------------------------------------- 1 | export * from "./GeneralJRMapping"; 2 | export * from "./PortInDashJRMapping"; 3 | export * from "./PortOutDashJRMapping"; 4 | export * from "./SubPortDashJRMapping"; 5 | export * from "./PortInExpectedJRMapping"; -------------------------------------------------------------------------------- /ansible/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - src: ssh://git@github.com/LinkNYC/eb-deployments.git 3 | scm: git 4 | version: master 5 | path: roles/ 6 | 7 | - src: ssh://git@github.com/LinkNYC/eb-dd-agent.git 8 | scm: git 9 | version: master 10 | path: roles/ 11 | -------------------------------------------------------------------------------- /project/scaffold.sbt: -------------------------------------------------------------------------------- 1 | // Defines scaffolding (found under .g8 folder) 2 | // http://www.foundweekends.org/giter8/scaffolding.html 3 | // sbt "g8Scaffold form" 4 | 5 | // not working yet with sbt 1 6 | //addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.8.0") 7 | -------------------------------------------------------------------------------- /scripts/write-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | export TERM=dump 5 | sbt -Dsbt.log.noformat=true "show version" | tail -2 | head -1 | cut -d ' ' -f2 | awk '{print "TAG="$1}' > version.txt 6 | echo Version 7 | cat ./version.txt 8 | # source ./version.txt 9 | # echo Version -> $TAG -------------------------------------------------------------------------------- /app/models/kinesis.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | case class WatchMessageEvents(eventType: String) 4 | 5 | case class UnwatchMessageEvents() 6 | 7 | case class UpdateMessageList(observations: List[Observation]) 8 | case class UpdateMessage(observations: Observation) 9 | 10 | case class Ping(ping: Long) 11 | 12 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/GridColumnWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GridColumnWrapper = ({ 4 | width, 5 | children 6 | }) => { 7 | return ( 8 |
    9 | {children} 10 |
    11 | ); 12 | }; 13 | 14 | export default GridColumnWrapper; -------------------------------------------------------------------------------- /ui/test/resources/ReceivedFileFailure.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "failedAt": "2017-05-30T11:58:25.61+01:00[Europe/London]", 4 | "failureCode": "INVALID_HEADER", 5 | "failureDescription": "The header was malformed. It should start with an 'HDR' block. The actual header found was 'i am not a valid header'", 6 | "failureType": "FILE_REJECTED_FAILURE" 7 | } 8 | ] -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DataTableHead.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DataTableHead = ({ 4 | headData 5 | }) => { 6 | let i = 0; 7 | 8 | return ( 9 | 10 | {headData.map(col => {col})} 11 | 12 | ); 13 | }; 14 | 15 | export default DataTableHead; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/HeaderButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HeaderButton = ({ 4 | url, 5 | onClickFunction, 6 | label 7 | }) => { 8 | return ( 9 |
  • 10 | { label } 11 |
  • 12 | ); 13 | }; 14 | 15 | export default HeaderButton; -------------------------------------------------------------------------------- /config/Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": ".dkr.ecr.us-east-1.amazonaws.com/intersection/link-mobile-observations:", 5 | "Update": "true" 6 | }, 7 | "Ports": [ 8 | { 9 | "ContainerPort": "9000" 10 | 11 | } 12 | ], 13 | "Logging": "/opt/docker/logs" 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ./scripts/build-sbt-reactjs 6 | 7 | echo "Tools built" 8 | echo "************************************************************" 9 | echo "************************************************************" 10 | echo "************************************************************" 11 | pwd 12 | sbt "npm install" 13 | sbt clean compile 14 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/IndexContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IndexContainer = ({ 4 | header, 5 | children, 6 | modals 7 | }) => { 8 | return ( 9 |
    10 | { header } 11 | { children } 12 | { modals } 13 |
    14 | ); 15 | }; 16 | 17 | export default IndexContainer; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/LoginModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginModal = ({ 4 | children 5 | }) => { 6 | return ( 7 |
    8 |
    9 | { children } 10 |
    11 |
    12 | ); 13 | }; 14 | 15 | export default LoginModal; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/CountBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CountBox = ({ 4 | label, 5 | value 6 | }) => { 7 | return ( 8 |
    9 | {label + ": " + value} 10 |
    11 | ); 12 | }; 13 | 14 | export default CountBox; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DataTableRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DataTableRow = ({ 4 | rowName, 5 | rowData 6 | }) => { 7 | let i = 0; 8 | 9 | return ( 10 | 11 | {rowData.map(col => {col})} 12 | 13 | ); 14 | }; 15 | 16 | export default DataTableRow; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DropDownItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DropDownItem = ({ 4 | name, 5 | label, 6 | onClickFunction 7 | }) => { 8 | return ( 9 |
  • 10 | { label } 12 |
  • 13 | ); 14 | }; 15 | 16 | export default DropDownItem; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/Login.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import LoginForm from './LoginForm.jsx'; 5 | import "materialize-css"; 6 | 7 | class Login extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | return ( 14 | 15 | ); 16 | } 17 | } 18 | 19 | export default Login; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/CircleThingyContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CircleThingyContainer = ({ 4 | id, 5 | name, 6 | children 7 | }) => { 8 | return ( 9 |
    10 |
    { name }
    11 | { children } 12 |
    13 | ); 14 | }; 15 | 16 | export default CircleThingyContainer; -------------------------------------------------------------------------------- /ui/app/src/scripts/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import 'babel-polyfill'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { Provider } from 'react-redux'; 7 | import List from './components/List.jsx'; 8 | import store from './store/store.js'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('index') 15 | ); -------------------------------------------------------------------------------- /ui/app/src/scripts/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import 'babel-polyfill'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { Provider } from 'react-redux'; 7 | import Login from './components/Login.jsx'; 8 | import store from './store/store'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('login') 15 | ); -------------------------------------------------------------------------------- /ui/app/src/scripts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import 'babel-polyfill'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { Provider } from 'react-redux'; 7 | import Index from './components/Index.jsx'; 8 | import store from './store/store.js'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('index') 15 | ); -------------------------------------------------------------------------------- /ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | ansible_managed = managed by intersection ansible 3 | transport = ssh 4 | vault_password_file = ~/.vault_pass.txt 5 | roles_path = ./roles 6 | force_color = 1 7 | retry_files_enabled = False 8 | gather_subset = !hardware 9 | 10 | [ssh_connection] 11 | ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=30m 12 | control_path = ~/.ssh/ansible-%%r@%%h:%%p 13 | pipelining = True 14 | 15 | -------------------------------------------------------------------------------- /ui/test/components/presentational/CountBoxTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import CountBox from '../../../app/src/scripts/components/presentational/CountBox'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) CountBox', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/app/src/scripts/util/TableIcons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from "react"; 4 | 5 | export function completedStatusIcon(isComplete) { 6 | return isComplete ? done : close; 7 | } 8 | 9 | export function errorStatusIcon(isError) { 10 | return isError ? error_outline : ""; 11 | } -------------------------------------------------------------------------------- /ui/test/components/presentational/LoginErrorTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import LoginError from '../../../app/src/scripts/components/presentational/LoginError'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) LoginError', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/LoginModalTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import LoginModal from '../../../app/src/scripts/components/presentational/LoginModal'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) LoginModal', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/DropDownItemTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DropDownItem from '../../../app/src/scripts/components/presentational/DropDownItem'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DropDownItem', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/HeaderButtonTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import HeaderButton from '../../../app/src/scripts/components/presentational/HeaderButton'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) HeaderButton', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/IconButtonTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import IconButton from '../../../app/src/scripts/components/presentational/IconButton'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) IconButton', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /ui/test/components/presentational/IndexContainerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import IndexContainer from '../../../app/src/scripts/components/presentational/IndexContainer'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) IndexContainer', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/TableSearchBoxTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import TableSearchBox from '../../../app/src/scripts/components/presentational/TableSearchBox'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) TableSearchBox', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DataTableContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DataTableContainer = ({ 4 | id, 5 | tableHead, 6 | tableRows 7 | }) => { 8 | return ( 9 | 10 | {tableHead} 11 | {tableRows} 12 |
    13 | ); 14 | }; 15 | 16 | export default DataTableContainer; -------------------------------------------------------------------------------- /ui/test/components/presentational/DataTableRowTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DataTableRow from '../../../app/src/scripts/components/presentational/DataTableRow'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DataTableRow', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/DropDownDividerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DropDownDivider from '../../../app/src/scripts/components/presentational/DropDownDivider'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DropDownDivider', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/FileRecordModalTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import FileRecordModal from '../../../app/src/scripts/components/presentational/FileRecordModal'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) FileRecordModal', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/HeaderContainerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import HeaderContainer from '../../../app/src/scripts/components/presentational/HeaderContainer'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) HeaderContainer', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/ContentContainerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import ContentContainer from '../../../app/src/scripts/components/presentational/ContentContainer'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) ContentContainer', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /app/services/HealthCheckService.scala: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient 6 | import play.api.{Configuration, Logger} 7 | 8 | @Singleton 9 | class HealthCheckService @Inject()(client: AmazonDynamoDBClient, configuration: Configuration) { 10 | 11 | val logger = Logger(getClass) 12 | 13 | def healthyString(): String = { 14 | configuration.get[String]("healthresponse") 15 | } 16 | } -------------------------------------------------------------------------------- /scripts/prep-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | LINK_ENV=${LINK_ENV:-dev} 6 | APPNAME=${APPNAME:-link-mobile-observations} 7 | 8 | echo Preparing deploy for ${LINK_ENV} 9 | 10 | if [ "${BUILDKITE}" == "true" ]; 11 | then 12 | buildkite-agent artifact download target/aws/"${APPNAME}"*.zip . 13 | fi 14 | 15 | 16 | cd ansible || exit 17 | ansible-galaxy install -f -r requirements.yml 18 | ansible-playbook -i ${LINK_ENV} ${APPNAME}.yml -e "appname=${APPNAME} env=${LINK_ENV}" 19 | -------------------------------------------------------------------------------- /ui/test/components/presentational/DataTableHeadTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DataTableHead from '../../../app/src/scripts/components/presentational/DataTableHead'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DataTableHead', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/DropDownContainerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DropDownContainer from '../../../app/src/scripts/components/presentational/DropDownContainer'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DropDownContainer', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/GridColumnWrapperTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import GridColumnWrapper from '../../../app/src/scripts/components/presentational/GridColumnWrapper'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) GridColumnWrapper', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/components/presentational/DataTableContainerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DataTableContainer from '../../../app/src/scripts/components/presentational/DataTableContainer'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DataTableContainer', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/test/resources/JoyrideStep.json: -------------------------------------------------------------------------------- 1 | { 2 | "joyride-replay-button": { 3 | "data": { 4 | "id": "GEN-REPLAY", 5 | "title": "Redisplay Guide", 6 | "text": "Click here any time to be shown the guide again.", 7 | "selector": "#joyride-replay-button", 8 | "position": "bottom", 9 | "style": { 10 | "mainColor": "#0d47a1", 11 | "beacon": { 12 | "inner": "#0d47a1", 13 | "outer": "#1565c0" 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /ui/test/components/presentational/CircleThingyContainerTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import CircleThingyContainer from '../../../app/src/scripts/components/presentational/CircleThingyContainer'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) CircleThingyContainer', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | }); -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/GeneralJRMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "joyride-replay-button": { 3 | "data": { 4 | "id": "GEN-REPLAY", 5 | "title": "Redisplay Guide", 6 | "text": "Click here any time to be shown the guide again.", 7 | "selector": "#joyride-replay-button", 8 | "position": "bottom", 9 | "style": { 10 | "mainColor": "#0d47a1", 11 | "beacon": { 12 | "inner": "#0d47a1", 13 | "outer": "#1565c0" 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /ui/test/resources/WrittenFileOrder.json: -------------------------------------------------------------------------------- 1 | { 2 | "entityId": { 3 | "absoluteFilePath": "/tmp/nfs/vfuk/vfukmnp01/SK201701121108VF005.REQ", 4 | "targetNetworkOperatorCode": "EE" 5 | }, 6 | "eventCount": 1, 7 | "fileType": "REQ", 8 | "lastEventAt": "2017-05-31T12:22:31.32+01:00[Europe/London]", 9 | "pendingRecordsCount": 1, 10 | "records": [ 11 | { 12 | "transactionNumber": "TRANS1" 13 | } 14 | ], 15 | "status": "COMPLETED", 16 | "writtenAt": "2017-05-31T12:22:31.32+01:00[Europe/London]" 17 | } 18 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/LoginError.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginError = ({ 4 | errorMessage 5 | }) => { 6 | return ( 7 |
    8 | 11 | {"Error: " + errorMessage} 12 |
    13 | ); 14 | }; 15 | 16 | export default LoginError; -------------------------------------------------------------------------------- /ui/test/components/presentational/LoginFieldTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | var LoginField = require('../../../app/src/scripts/components/presentational/LoginField').default; 5 | // import LoginField from '../../../app/src/scripts/components/presentational/LoginField'; 6 | 7 | const wrapper = shallow(); 8 | 9 | describe('(Component) LoginField', () => { 10 | it('renders without exploding', () => { 11 | expect(wrapper).to.have.lengthOf(1); 12 | }); 13 | }); -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DataTableToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DataTableToggle = ({ 4 | id, 5 | name, 6 | onClick, 7 | toggleVal 8 | }) => { 9 | return ( 10 | 12 | { (toggleVal ? "Hide" : "Show") + " " + name } 13 | 14 | ); 15 | }; 16 | 17 | export default DataTableToggle; -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stack: "{{ env }}-{{ appname }}" 3 | region: us-west-2 4 | env: "{{ lookup('env','USER') | default('qa') }}" 5 | eb_strategy: blue-green 6 | instance_type: t2.small 7 | key_name: common-tardis-ops-dev-v1 8 | phoenix_mode: on 9 | 10 | app_output_dir: "/tmp/{{ appname }}-output-{{ env }}" 11 | eb_deployer_yml: "eb_deployer-{{ env }}-{{ region }}.yml" 12 | solution_stack_name: "64bit Amazon Linux 2016.03 v2.1.0 running Docker 1.9.1" 13 | target: "../target/aws/{{ appname }}.zip" 14 | logstash_host: broker.internal.linksvc.com 15 | logstash_port: 6379 16 | idle_timeout: 300 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Play plugin 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.12") 3 | 4 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.3") 5 | 6 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5") 7 | 8 | addSbtPlugin("com.localytics" % "sbt-dynamodb" % "2.0.0") 9 | 10 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 11 | 12 | addSbtPlugin("com.github.mmizutani" % "sbt-play-gulp" % "0.2.0") 13 | 14 | addSbtPlugin("com.github.ddispaltro" % "sbt-reactjs" % "0.6.9-SNAPSHOT") 15 | 16 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2") 17 | -------------------------------------------------------------------------------- /ui/app/src/scripts/store/store.js: -------------------------------------------------------------------------------- 1 | import thunkMiddleware from 'redux-thunk'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import rootReducer from '../reducers/rootReducer.js'; 5 | import Immutable from 'immutable'; 6 | 7 | const composeEnhancers = composeWithDevTools({ 8 | serialize: { 9 | immutable: Immutable 10 | } 11 | }); 12 | 13 | const store = createStore( 14 | rootReducer, 15 | composeEnhancers( 16 | applyMiddleware( 17 | thunkMiddleware 18 | ) 19 | ) 20 | ); 21 | 22 | export default store; -------------------------------------------------------------------------------- /ui/app/src/scripts/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | // import { combineReducers } from 'redux-immutable'; 3 | import { reducer as FormReducer } from 'redux-form/immutable'; 4 | import loginReducer from './loginReducer.js'; 5 | import dataReducer from './dataReducer.js'; 6 | import controlReducer from './controlReducer.js'; 7 | import joyrideReducer from './joyrideReducer.js'; 8 | 9 | const rootReducer = combineReducers({ 10 | form: FormReducer, 11 | login: loginReducer, 12 | data: dataReducer, 13 | control: controlReducer, 14 | joyride: joyrideReducer 15 | }); 16 | 17 | export default rootReducer; -------------------------------------------------------------------------------- /ui/app/src/scripts/actions/JoyrideAction.js: -------------------------------------------------------------------------------- 1 | export const SET_STEP_INDEX = 'SET_STEP_INDEX'; 2 | 3 | export function setStepIndex(stepIndex) { 4 | return { 5 | type: SET_STEP_INDEX, 6 | content: { 7 | stepIndex: stepIndex 8 | } 9 | } 10 | } 11 | 12 | export const ADD_STEP = 'ADD_STEP'; 13 | 14 | export function addStep(step) { 15 | return { 16 | type: ADD_STEP, 17 | content: { 18 | step: step 19 | } 20 | } 21 | } 22 | 23 | export const CLEAR_STEPS = 'CLEAR_STEPS'; 24 | 25 | export function clearSteps() { 26 | return { 27 | type: CLEAR_STEPS 28 | } 29 | } -------------------------------------------------------------------------------- /ui/app/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // Header: 2 | $header-height: 54px; 3 | 4 | // Content: 5 | $completed-colour: #cdf9cd; 6 | $outstanding-colour: #f9cdcd; 7 | $table-alternate-colour: #f2f2f2; 8 | 9 | $welcome-padding: 15px; 10 | $welcome-top-margin: 1.5rem; 11 | $card-card-action-vertical-padding: 16px; 12 | $card-action-height: 9em; 13 | 14 | $modal-footer-height: 56px; 15 | 16 | $h5-vertical-margin: 0.72rem; 17 | $h5-font-size: 1.64rem; 18 | 19 | $btn-height: 36px; 20 | 21 | $btn-icon-color: #AAA; 22 | 23 | // Paddington Blues: 24 | $paddington-blue-light: #1565c0; 25 | $paddington-blue: #0d47a1; 26 | $paddington-blue-dark: darken(#0d47a1, 5%); 27 | -------------------------------------------------------------------------------- /ui/test/resources/SearchedReceivedFileFailedRecord.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "actionCode": "01", 4 | "actionStatus": "FF", 5 | "dno": "TEST_UNSEARCHABLE_FIELD_VALUE", 6 | "failure": { 7 | "failedAt": "2017-06-01T10:07:30.568+01:00[Europe/London]", 8 | "failureCode": "UNEXPECTED_BEHAVIOUR", 9 | "failureDescription": "Unknown error. Unable to parse line 'MANGOES', reason: String index out of range: 24", 10 | "failureType": "RECORD_FAILED_PROCESSING_FAILURE" 11 | }, 12 | "msisdn": "447985680876", 13 | "ono": "CN", 14 | "originalRecord": "MANGOES", 15 | "rno": "SK", 16 | "transactionNumber": "SK00000006" 17 | } 18 | ] -------------------------------------------------------------------------------- /ui/app/src/scripts/reducers/loginReducer.js: -------------------------------------------------------------------------------- 1 | import { LOGIN_REQUEST, LOGIN_RESPONSE, LOGIN_ERROR } from '../actions/LoginAction'; 2 | 3 | const initialState = { 4 | error: '' 5 | }; 6 | 7 | export default function(state = initialState, action) { 8 | switch (action.type) { 9 | case LOGIN_RESPONSE: 10 | return { 11 | ...state, 12 | error: '' 13 | }; 14 | case LOGIN_ERROR: 15 | return { 16 | ...state, 17 | error: action.content.message 18 | }; 19 | case LOGIN_REQUEST: 20 | default: 21 | return state; 22 | } 23 | } -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Paddington 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/IconButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const IconButton = ({ 4 | id, 5 | iconType, 6 | onClick, 7 | tooltip 8 | }) => { 9 | return ( 10 | 11 | 16 | { iconType } 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default IconButton; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/DropDownContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DropDownContainer = ({ 4 | name, 5 | label, 6 | children 7 | }) => { 8 | return ( 9 |
  • 10 | 11 | { label } {/*4*/} arrow_drop_down 13 | 14 | 15 |
      16 | { children } 17 |
    18 |
  • 19 | ); 20 | }; 21 | 22 | export default DropDownContainer; -------------------------------------------------------------------------------- /app/controllers/HealthController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import io.swagger.annotations.{Api, ApiResponse, ApiResponses} 6 | import play.api.mvc.InjectedController 7 | import services.HealthCheckService 8 | 9 | import scala.concurrent.Future 10 | 11 | @Api("/health") 12 | @Singleton 13 | class HealthController @Inject()(healthCheckService: HealthCheckService) 14 | extends InjectedController { 15 | 16 | @ApiResponses(value = Array(new ApiResponse(code = 200, message = "healthy", response = classOf[String]))) 17 | def health = Action.async { 18 | val isHealthy = healthCheckService.healthyString 19 | Future.successful(Ok(isHealthy)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/views/login.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Paddington - Login 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/LoginField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginField = ({ 4 | input, label, type, 5 | placeholder, icon, 6 | required, autoFocus 7 | }) => { 8 | return ( 9 |
    10 | {icon} 11 | 17 | 18 |
    19 | ); 20 | }; 21 | 22 | export default LoginField; -------------------------------------------------------------------------------- /ui/app/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "materialize-css/sass/materialize"; 2 | @import "fixed-data-table-2/dist/fixed-data-table"; 3 | @import "react-joyride/lib/react-joyride"; 4 | @import "variables"; 5 | @import "header"; 6 | @import "login"; 7 | @import "content"; 8 | 9 | html, body { 10 | margin:0; 11 | padding:0; 12 | height:100%; 13 | } 14 | 15 | #body-index { 16 | background-color: #fafafa; //was #eeeeee 17 | } 18 | 19 | .btn:hover, .btn-flat:hover, .btn-large:hover, .btn-floating:hover, .btn:focus, .btn-flat:focus, .btn-large:focus, .btn-floating:focus { 20 | color: #ffffff; 21 | background-color: $paddington-blue-light; 22 | } 23 | 24 | .hidden { 25 | position: absolute !important; 26 | top: -9999px !important; 27 | left: -9999px !important; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/WrittenFileRecordMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "File Records", 3 | "name": "WrittenFileRecordMapping", 4 | "filterTypes": [], 5 | "mappings": [ 6 | { 7 | "displayName": "MSISDN", 8 | "path": "$.msisdn", 9 | "searchable": true, 10 | "columnWidth": 120 11 | }, 12 | { 13 | "displayName": "DNO", 14 | "path": "$.dno", 15 | "searchable": true, 16 | "columnWidth": 40, 17 | "flexGrow": 2 18 | }, 19 | { 20 | "displayName": "RNO", 21 | "path": "$.rno", 22 | "searchable": true, 23 | "columnWidth": 40, 24 | "flexGrow": 2 25 | }, 26 | { 27 | "displayName": "ONO", 28 | "path": "$.ono", 29 | "searchable": true, 30 | "columnWidth": 40, 31 | "flexGrow": 2 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/HeaderContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HeaderContainer = ({ 4 | children 5 | }) => { 6 | return ( 7 |
    8 | 20 |
    21 | ); 22 | }; 23 | 24 | export default HeaderContainer; -------------------------------------------------------------------------------- /samples/globalreachtoken.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NlcnZpY2VzLnN0YWdpbmcub2R5c3N5cy5uZXQvYWNjb3VudC91c2Vycy8zNjEzNzkxMyIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1MTEyMDQwODQsImp0aSI6ImM0M2U2ZDYyLWU5MzQtNDg2MC05NjA0LTQ3NTJmYTc1ZTZmZiIsImNsaWVudF9pZCI6IkdSLUlOVDAwMS9pbnRlcnNlY3Rpb25Ab2R5c3N5cy5uZXQifQ.kylreBvQHvA6DEIg5wQzQuU8NPYjZUW-vBaPEVA4Y70zf9Fvp5DyFKSQFzexQF5bCBRj63wbScEw8LT46kOUEUApATJrfUqm1ULHuVxhZ9_3DZPZR6ctV7NxK2_75ojpAZXk4Df4FpnB6bO3PvkLFgB5krrnPwOArPZuhNn0ONK7DZ4Mv9-Vwvfn_CfdA0LHe8tIcGbFT35yCO2dJwbB3XM8IzE03N6rvNQjeWOgXIvvmwWsF4DE3epLJ87zeidKvdMJ_LYODuHAZUpNFwRBCKURsv22vP-w9uWCHL6I3PjJE0MtQDviyMBY5HzIcA40Z4Hx7sasQOnP_M3zdfHjtw", 3 | "token_type": "bearer", 4 | "expires_in": 4999, 5 | "scope": "all", 6 | "sub": "https://services.staging.odyssys.net/account/users/36137913", 7 | "jti": "c43e6d62-e934-4860-9604-4752fa75e6ff" 8 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/FileRecordModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FileRecordModal = ({ 4 | name, 5 | title = "File Records", 6 | controls, 7 | children 8 | }) => { 9 | return ( 10 |
    11 |
    12 |
    { title }
    13 |
    14 | { controls } 15 |
    16 |
    17 |
    18 | { children } 19 |
    20 |
    21 | Close 22 |
    23 |
    24 | ); 25 | }; 26 | 27 | export default FileRecordModal; -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/TableSearchBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TableSearchBox = ({ 4 | input, 5 | label, 6 | value, 7 | placeholder, 8 | onChangeMethod 9 | }) => { 10 | return ( 11 |
    12 | search 13 | onChangeMethod(element.target.value)}/> 20 |
    21 | ); 22 | }; 23 | 24 | export default TableSearchBox; -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/ReceivedFileFailureMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "File Failure", 3 | "name": "ReceivedFileFailureMapping", 4 | "filterTypes": [], 5 | "mappings": [ 6 | { 7 | "displayName": "Failed At", 8 | "path": "$.failedAt", 9 | "searchable": true, 10 | "columnWidth": 120, 11 | "flexGrow": 1 12 | }, 13 | { 14 | "displayName": "Failure Code", 15 | "path": "$.failureCode", 16 | "searchable": true, 17 | "columnWidth": 120, 18 | "flexGrow": 1 19 | }, 20 | { 21 | "displayName": "Failure Description", 22 | "path": "$.failureDescription", 23 | "searchable": true, 24 | "columnWidth": 120, 25 | "flexGrow": 1 26 | }, 27 | { 28 | "displayName": "Failure Type", 29 | "path": "$.failureType", 30 | "searchable": true, 31 | "columnWidth": 120, 32 | "flexGrow": 1 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /ui/test/components/presentational/DataTableToggleTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import DataTableToggle from '../../../app/src/scripts/components/presentational/DataTableToggle'; 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('(Component) DataTableToggle', () => { 9 | it('renders without exploding', () => { 10 | expect(wrapper).to.have.lengthOf(1); 11 | }); 12 | 13 | it('renders "Hide" when toggleVal is true', () => { 14 | let toggleTrueWrapper = shallow(); 15 | expect(toggleTrueWrapper.text()).to.contain("Hide"); 16 | }); 17 | 18 | it('renders "Show" when toggleVal is false', () => { 19 | let toggleFalseWrapper = shallow(); 20 | expect(toggleFalseWrapper.text()).to.contain("Show"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /ui/app/src/scripts/actions/ControlAction.js: -------------------------------------------------------------------------------- 1 | // import fetch from 'isomorphic-fetch'; 2 | 3 | export const SET_PAGE_TYPE = 'SET_PAGE_TYPE'; 4 | 5 | export function setPageType(pageType) { 6 | return { 7 | type: SET_PAGE_TYPE, 8 | content: { 9 | pageType: pageType 10 | } 11 | } 12 | } 13 | 14 | export const SET_SEARCH_TEXT = 'SET_SEARCH_TEXT'; 15 | 16 | export function setSearchText(searchText) { 17 | return { 18 | type: SET_SEARCH_TEXT, 19 | content: { 20 | searchText:searchText 21 | } 22 | } 23 | } 24 | 25 | export const TOGGLE_FILTER_CRITERIA = 'TOGGLE_FILTER_CRITERIA'; 26 | export const OUTSTANDING = 'OUTSTANDING'; 27 | export const COMPLETED = 'COMPLETED'; 28 | 29 | export function toggleFilterCriteria(criteria) { 30 | return { 31 | type: TOGGLE_FILTER_CRITERIA, 32 | content: { 33 | criteria: criteria 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/PortInExpectedJRMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "port-in-expected-table-export-button": { 3 | "data": { 4 | "id": "PIE-001", 5 | "title": "Export to CSV", 6 | "text": "This button can be used to download a CSV file containing all Port In Orders on this page.

    The CSV will include Port Date, Porting MSISDN, Sky MSISDN, DNO (provided by Syniverse) and Status as fields.

    Please note, the download takes into account any filters and searches you have active on the table, i.e. if 'Completed' orders are set to be hidden on the page, they will not be included in the download.", 7 | "selector": "#export-button", 8 | "position": "left", 9 | "allowClicksThruHole": true, 10 | "style": { 11 | "mainColor": "#0d47a1", 12 | "width": "42rem", 13 | "beacon": { 14 | "inner": "#0d47a1", 15 | "outer": "#1565c0" 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ui/test/resources/SummariesOrder.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "summaryType": "portIn", 4 | "summaryStage": "Accepted", 5 | "counts": { 6 | "notDone": 0, 7 | "done": 0, 8 | "error": 0 9 | } 10 | }, 11 | { 12 | "summaryType": "portIn", 13 | "summaryStage": "Secured", 14 | "counts": { 15 | "notDone": 0, 16 | "done": 0, 17 | "error": 0 18 | } 19 | }, 20 | { 21 | "summaryType": "portIn", 22 | "summaryStage": "Activated", 23 | "counts": { 24 | "notDone": 0, 25 | "done": 0, 26 | "error": 0 27 | } 28 | }, 29 | { 30 | "summaryType": "portIn", 31 | "summaryStage": "File Processed", 32 | "counts": { 33 | "notDone": 0, 34 | "done": 0, 35 | "error": 0 36 | } 37 | }, 38 | { 39 | "summaryType": "portIn", 40 | "summaryStage": "Completed", 41 | "counts": { 42 | "notDone": 0, 43 | "done": 0, 44 | "error": 0 45 | } 46 | } 47 | ] -------------------------------------------------------------------------------- /ui/test/components/presentational/LoginBoxTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import chai from 'chai' 4 | import chaiEnzyme from 'chai-enzyme' 5 | var expect = chai.expect; 6 | chai.use(chaiEnzyme()); 7 | import LoginBox from '../../../app/src/scripts/components/presentational/LoginBox'; 8 | 9 | const wrapper = shallow(); 10 | 11 | describe('(Component) LoginBox', () => { 12 | it('renders without exploding', () => { 13 | expect(wrapper).to.have.lengthOf(1); 14 | }); 15 | 16 | it('renders with an error class when an error is present', () => { 17 | let wrapperWithErrors = shallow(); 18 | expect(wrapperWithErrors.find("#loginBox")).to.have.className("error"); 19 | }); 20 | 21 | it('renders without an error class when an error is not present', () => { 22 | expect(wrapper.find("#loginBox")).to.not.have.className("error"); 23 | }); 24 | }); -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | GET /index controllers.IndexController.index 6 | GET /list controllers.IndexController.list 7 | GET /list/:id controllers.IndexController.filter(id: String) 8 | GET /type/:id controllers.IndexController.filterByType(id: String) 9 | GET /json controllers.IndexController.json 10 | 11 | # Websocket 12 | GET /ws controllers.WebsocketController.ws 13 | 14 | GET /health controllers.HealthController.health 15 | 16 | GET /swagger.json controllers.ApiHelpController.getResources 17 | 18 | # Assets 19 | GET /ui/*file com.github.mmizutani.playgulp.GulpAssets.at(file) 20 | GET /*file com.github.mmizutani.playgulp.GulpAssets.at(file) 21 | 22 | 23 | -------------------------------------------------------------------------------- /ui/app/src/scripts/paddington-router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var Router = require('react-router').Router; 6 | var browserHistory = require('react-router').browserHistory; 7 | 8 | // const rootRoute = { 9 | // getChildRoutes(partialNextState, callback) { 10 | // require.ensure([], function (require) { 11 | // callback(null, [ 12 | // require('./paddington'), 13 | // require('./login-page') 14 | // ]) 15 | // }) 16 | // } 17 | // } 18 | 19 | const rootRoute = { 20 | childRoutes: [ { 21 | path: '/', 22 | // component: require('./components/App'), 23 | childRoutes: [ 24 | require('./index'), 25 | require('./login') 26 | ] 27 | } ] 28 | }; 29 | 30 | ReactDOM.render( 33 | , document.getElementById('root')); -------------------------------------------------------------------------------- /samples/dwh-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "bk-01-126659", 4 | "latitude": "40.70098585", 5 | "longitude": "-73.94208019", 6 | "address": "2 GRAHAM AVENUE", 7 | "status": "Ready to Assign" 8 | }, 9 | { 10 | "id": "bk-01-138951", 11 | "latitude": "40.70167374876147", 12 | "longitude": "-73.94248964825573", 13 | "address": "15 DEBEVOISE STREET", 14 | "status": "Ready to Install" 15 | }, 16 | { 17 | "id": "bk-01-109091", 18 | "latitude": "40.70154219381847", 19 | "longitude": "-73.9421692830818", 20 | "address": "24 GRAHAM AVENUE", 21 | "status": "Ready to Install" 22 | }, 23 | { 24 | "id": "bk-02-142555", 25 | "latitude": "40.70367340871585", 26 | "longitude": "-73.98667304505466", 27 | "address": "44 JAY STREET", 28 | "status": "DoITT Rejected" 29 | }, 30 | { 31 | "id": "bk-01-143981", 32 | "latitude": "40.70193034387478", 33 | "longitude": "-73.94223923853747", 34 | "address": "32 GRAHAM AVENUE", 35 | "status": "Ready to Install" 36 | } 37 | ] -------------------------------------------------------------------------------- /ui/test/loginTest.js: -------------------------------------------------------------------------------- 1 | require('testdom')('
    ') 2 | import assert from 'assert'; 3 | // var rtu = require('react-addons-test-utils'); 4 | 5 | // login.handlePasswordChange() 6 | // handlePasswordChange: function(e) { 7 | // this.setState({ password: e.target.value }); 8 | // }, 9 | // 10 | 11 | describe('Array', function() { 12 | describe('#indexOf()', function() { 13 | it('should return -1 when the value is not present', function() { 14 | assert.equal(-1, [1,2,3].indexOf(4)); 15 | }); 16 | }); 17 | }); 18 | // describe('Login', function() { 19 | // describe('#handleUsernameChange()', function() { 20 | // it('should update state', function(done) { 21 | // var login = require("../login.jsx"); 22 | // var usernameInput = this.refs.username; 23 | // usernameInput = "Test User"; 24 | // rtu.Simulate.change(usernameInput); 25 | // assert.equal(this.state.username, "Test User", "Username was not updated."); 26 | // }); 27 | // }); 28 | // }); -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/WrittenFileMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Generated Files", 3 | "name": "WrittenFileMapping", 4 | "filterTypes": [], 5 | "mappings": [ 6 | { 7 | "displayName": "Filename", 8 | "path": "$.entityId.absoluteFilePath", 9 | "searchable": true, 10 | "columnWidth": 300, 11 | "flexGrow": 2, 12 | "isKey": true 13 | }, 14 | { 15 | "displayName": "File Type", 16 | "path": "$.fileType", 17 | "searchable": true, 18 | "columnWidth": 60 19 | }, 20 | { 21 | "displayName": "Date Generated", 22 | "path": "$.writtenAt", 23 | "type" : "DATE", 24 | "searchable": true, 25 | "columnWidth": 120 26 | }, 27 | { 28 | "displayName": "No. of Records", 29 | "path": "$.records.length", 30 | "columnWidth": 120 31 | }, 32 | { 33 | "displayName": "No. of Pending Records", 34 | "path": "$.pendingRecordsCount", 35 | "columnWidth": 120 36 | }, 37 | { 38 | "displayName": "View Records", 39 | "path": "", 40 | "columnWidth": 140 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /ansible/qa/group_vars/all/vars: -------------------------------------------------------------------------------- 1 | env: qa 2 | region: us-east-1 3 | key_name: link-non-prod-feb-2018 4 | instance_type: t2.small 5 | cert: arn:aws:acm:us-east-1:028957328603:certificate/879b03f9-a895-4ec0-a8c3-b23ecaf5125e 6 | dynamodb_url: jdbc:mysql://fuhgavi-mysql.cxuxt6izrok4.us-east-1.rds.amazonaws.com:3306/fuhgavi-qa?characterEncoding=UTF-8 7 | vpc: 8 | stack_outputs: 9 | BastionSecurityGroup: sg-afa875d2 10 | VpcAlarmTopicArn: arn:aws:sns:us-east-1:028957328603:qa-Sns-Vpc-01 11 | VpcId: vpc-e5e10683 12 | az0: us-east-1b 13 | az1: us-east-1c 14 | bastionIp: 52.2.112.72 15 | cidrPrivate0: 172.28.0.0/19 16 | cidrPrivate1: 172.28.32.0/19 17 | cidrPublic0: 172.28.240.0/22 18 | cidrPublic1: 172.28.244.0/22 19 | subnetPrivate0: subnet-e72b86ca 20 | subnetPrivate1: subnet-9459c5dd 21 | subnetPublic0: subnet-e42b86c9 22 | subnetPublic1: subnet-9759c5de 23 | subnets: "{{ vpc.stack_outputs.subnetPrivate0 }},{{ vpc.stack_outputs.subnetPrivate1 }}" 24 | elb_subnets: "{{ vpc.stack_outputs.subnetPublic0 }},{{ vpc.stack_outputs.subnetPublic1 }}" 25 | phoenix_mode: off 26 | 27 | asg_min_size: 1 28 | asg_max_size: 1 -------------------------------------------------------------------------------- /samples/globalreachcreatesubscriber.json: -------------------------------------------------------------------------------- 1 | { 2 | "href":"https://services.odyssys.net/subscriber/subscribers/3e82ae4e-799f-11e6-8b77-86f30ca893d3", 3 | "id":"3e82ae4e-799f-11e6-8b77-86f30ca893d3", 4 | "created":"2016-05-06T16:23:47.015Z", 5 | "createdUser":"https://services.odyssys.net/account/users/20000", 6 | "modified":"2016-05-06T16:23:47.015Z", 7 | "modifiedUser":"https://services.odyssys.net/account/users/20000", 8 | "ownerAccount":"https://services.odyssys.net/account/accounts/2901", 9 | "enabled":true, 10 | "radiusUsername":"john.doe@example.test", 11 | "groups":[ 12 | "https://services.odyssys.net/subscriber/subscriber-groups/6024fd86-799f-11e6-8b77-86f30ca893d3" 13 | ], 14 | "devices":[ 15 | { 16 | "mac":"67-11-F0-29-1A-4B", 17 | "authProvider":"one-time-sign-up", 18 | "portal":"https://services.odyssys.net/captive-portal/captive-portals/67007" 19 | }, 20 | { 21 | "mac":"C0-48-23-79-50-48" 22 | }, 23 | { 24 | "mac":"07-77-1B-D3-41-7F" 25 | } 26 | ], 27 | "metadata":{ 28 | "First Name":"John", 29 | "Last Name":"Doe", 30 | "Gender":"Male", 31 | "DOB":"1/1/1985" 32 | } 33 | } -------------------------------------------------------------------------------- /ui/node_modules/are-we-there-yet/CHANGES.md~: -------------------------------------------------------------------------------- 1 | Hi, figured we could actually use a changelog now: 2 | 3 | ## 1.1.3 2017-04-21 4 | 5 | * Improve documentation and limit files included in the distribution. 6 | 7 | ## 1.1.2 2016-03-15 8 | 9 | * Add tracker group cycle detection and tests for it 10 | 11 | ## 1.1.1 2016-01-29 12 | 13 | * Fix a typo in stream completion tracker 14 | 15 | ## 1.1.0 2016-01-29 16 | 17 | * Rewrote completion percent computation to be low impact– no more walking a 18 | tree of completion groups every time we need this info. Previously, with 19 | medium sized tree of completion groups, even a relatively modest number of 20 | calls to the top level `completed()` method would result in absurd numbers 21 | of calls overall as it walked down the tree. We now, instead, keep track as 22 | we bubble up changes, so the computation is limited to when data changes and 23 | to the depth of that one branch, instead of _every_ node. (Plus, we were already 24 | incurring _this_ cost, since we already bubbled out changes.) 25 | * Moved different tracker types out to their own files. 26 | * Made tests test for TOO MANY events too. 27 | * Standarized the source code formatting 28 | -------------------------------------------------------------------------------- /ui/node_modules/fsevents/node_modules/are-we-there-yet/CHANGES.md~: -------------------------------------------------------------------------------- 1 | Hi, figured we could actually use a changelog now: 2 | 3 | ## 1.1.3 2017-04-21 4 | 5 | * Improve documentation and limit files included in the distribution. 6 | 7 | ## 1.1.2 2016-03-15 8 | 9 | * Add tracker group cycle detection and tests for it 10 | 11 | ## 1.1.1 2016-01-29 12 | 13 | * Fix a typo in stream completion tracker 14 | 15 | ## 1.1.0 2016-01-29 16 | 17 | * Rewrote completion percent computation to be low impact– no more walking a 18 | tree of completion groups every time we need this info. Previously, with 19 | medium sized tree of completion groups, even a relatively modest number of 20 | calls to the top level `completed()` method would result in absurd numbers 21 | of calls overall as it walked down the tree. We now, instead, keep track as 22 | we bubble up changes, so the computation is limited to when data changes and 23 | to the depth of that one branch, instead of _every_ node. (Plus, we were already 24 | incurring _this_ cost, since we already bubbled out changes.) 25 | * Moved different tracker types out to their own files. 26 | * Made tests test for TOO MANY events too. 27 | * Standarized the source code formatting 28 | -------------------------------------------------------------------------------- /ansible/dev/group_vars/all/vars: -------------------------------------------------------------------------------- 1 | env: dev 2 | region: us-east-1 3 | key_name: link-non-prod-feb-2018 4 | instance_type: t2.small 5 | cert: arn:aws:acm:us-east-1:028957328603:certificate/879b03f9-a895-4ec0-a8c3-b23ecaf5125e 6 | vpc: 7 | stack_outputs: 8 | BastionSecurityGroup: sg-c2176cbf 9 | VpcAlarmTopicArn: arn:aws:sns:us-east-1:028957328603:dev-Sns-Vpc-01 10 | VpcId: vpc-b54684d3 11 | az0: us-east-1b 12 | az1: us-east-1c 13 | az2: us-east-1d 14 | bastionIp: 34.193.250.152 15 | cidrPrivate0: 172.29.0.0/19 16 | cidrPrivate1: 172.29.32.0/19 17 | cidrPrivate2: 172.29.64.0/19 18 | cidrPublic0: 172.29.240.0/22 19 | cidrPublic1: 172.29.244.0/22 20 | cidrPublic2: 172.29.248.0/22 21 | subnetPrivate0: subnet-2a21ac07 22 | subnetPrivate1: subnet-8dec90c4 23 | subnetPrivate2: subnet-420f8919 24 | subnetPublic0: subnet-2521ac08 25 | subnetPublic1: subnet-8cec90c5 26 | subnetPublic2: subnet-4d0f8916 27 | subnets: "{{ vpc.stack_outputs.subnetPrivate0 }},{{ vpc.stack_outputs.subnetPrivate1 }}" 28 | elb_subnets: "{{ vpc.stack_outputs.subnetPublic0 }},{{ vpc.stack_outputs.subnetPublic1 }}" 29 | phoenix_mode: off 30 | 31 | asg_min_size: 1 32 | asg_max_size: 1 33 | 34 | -------------------------------------------------------------------------------- /ui/app/src/styles/_header.scss: -------------------------------------------------------------------------------- 1 | nav { 2 | background-color: $paddington-blue; 3 | border-color: #080808; 4 | height: $header-height; 5 | line-height:54px; /*line-height and height must match or rollover effect is out of place*/ 6 | } 7 | 8 | nav .brand-logo { 9 | padding: 12px 0px 0px 15px; 10 | } 11 | 12 | /*Stuff for searchbar*/ 13 | 14 | nav .input-field input { 15 | height: 100%; 16 | font-size: 20px; 17 | //border: none; 18 | padding-left: 35px !important; 19 | } 20 | 21 | .input-field label.activeIcon { 22 | color: #fefefe !important; 23 | position: absolute; 24 | top: -11.5px !important; 25 | left: 5px !important; 26 | font-size: 14px; 27 | cursor: text; 28 | transition: .2s ease-out; 29 | } 30 | 31 | nav .input-field label i { 32 | color: #ffffff; 33 | transition: color .3s; 34 | } 35 | 36 | ul.dropdown-content { 37 | position: absolute !important; 38 | top: 54px !important; 39 | } 40 | 41 | ul .dropdown-content li a { 42 | color: $paddington-blue; 43 | } 44 | 45 | li .dropdown-button i { 46 | line-height: 56px !important;; 47 | } 48 | 49 | th { 50 | width: auto !important; 51 | } 52 | 53 | i.header-icon { 54 | height: inherit !important; 55 | line-height: inherit !important; 56 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/reducers/joyrideReducer.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | import { SET_STEP_INDEX, ADD_STEP, CLEAR_STEPS } from '../actions/JoyrideAction'; 3 | import * as JoyrideMappings from "../../resources/mappings/joyride"; 4 | 5 | function filterCompletedGuideSteps(steps) { 6 | try { 7 | return steps.filter(step => !JSON.parse(Cookies.get('completedGuideSteps')).includes(step.id)); 8 | } catch (e) { 9 | return steps; 10 | } 11 | } 12 | 13 | const initialState = { 14 | steps: [], 15 | stepIndex: 0, 16 | stepMapping: JoyrideMappings 17 | }; 18 | 19 | export default function(state = initialState, action) { 20 | switch (action.type) { 21 | case SET_STEP_INDEX: 22 | return { 23 | ...state, 24 | stepIndex: action.content.stepIndex 25 | }; 26 | case ADD_STEP: 27 | return { 28 | ...state, 29 | steps: state.steps.concat(filterCompletedGuideSteps([action.content.step])) 30 | }; 31 | case CLEAR_STEPS: 32 | return { 33 | ...state, 34 | steps: [] 35 | }; 36 | default: 37 | return state; 38 | } 39 | } -------------------------------------------------------------------------------- /ui/node_modules/in-publish/README.md~: -------------------------------------------------------------------------------- 1 | in-publish 2 | ========== 3 | 4 | Detect if we were run as a result of `npm publish`. This is intended to allow you to 5 | easily have prepublish lifecycle scripts that don't run when you run `npm install`. 6 | 7 | ``` 8 | $ npm install --save in-publish 9 | in-publish@1.0.0 node_modules/in-publish 10 | ``` 11 | 12 | Then edit your package.json to have: 13 | 14 | ```json 15 | "scripts": { 16 | "prepublish": "in-publish && thing-I-dont-want-on-dev-install || in-install" 17 | } 18 | ``` 19 | 20 | Now when you run: 21 | ``` 22 | $ npm install 23 | ``` 24 | Then `thing-I-dont-want-on-dev-install` won't be run, but... 25 | 26 | ``` 27 | $ npm publish 28 | ``` 29 | And `thing-I-dont-want-on-dev-install` will be run. 30 | 31 | Caveat Emptor 32 | ============= 33 | 34 | This detects that its running as a part of publish command in a terrible, 35 | terrible way. NPM dumps out its config object blindly into the environment 36 | prior to running commands. This includes the command line it was invoked 37 | with. This module determines if its being run as a result of publish by 38 | looking at that env var. This is not a part of the documented npm interface 39 | and so it is not guarenteed to be stable. 40 | 41 | -------------------------------------------------------------------------------- /ansible/prod/group_vars/all/vars: -------------------------------------------------------------------------------- 1 | env: prod 2 | region: us-east-1 3 | key_name: link-prod-feb-2018 4 | phoenix_mode: on 5 | instance_type: t2.small 6 | cert: arn:aws:acm:us-east-1:516822316844:certificate/4c98122e-1d8c-4f5f-a024-b1a3f90fcfbd 7 | vpc: 8 | stack_outputs: 9 | BastionSecurityGroup: sg-8afedcf7 10 | VpcAlarmTopicArn: arn:aws:sns:us-east-1:516822316844:prod-Sns-Vpc-01 11 | VpcId: vpc-08aa776e 12 | az0: us-east-1a 13 | az1: us-east-1c 14 | az2: us-east-1d 15 | bastionIp: 34.193.119.132 16 | cidrPrivate0: 172.30.0.0/19 17 | cidrPrivate1: 172.30.32.0/19 18 | cidrPrivate2: 172.30.64.0/19 19 | cidrPublic0: 172.30.240.0/22 20 | cidrPublic1: 172.30.244.0/22 21 | cidrPublic2: 172.30.248.0/22 22 | subnetPrivate0: subnet-1ef46045 23 | subnetPrivate1: subnet-9b23b0b6 24 | subnetPrivate2: subnet-e0d496a9 25 | subnetPublic0: subnet-1df46046 26 | subnetPublic1: subnet-9823b0b5 27 | subnetPublic2: subnet-e3d496aa 28 | subnets: "{{ vpc.stack_outputs.subnetPrivate0 }},{{ vpc.stack_outputs.subnetPrivate1 }},{{ vpc.stack_outputs.subnetPrivate2 }}" 29 | elb_subnets: "{{ vpc.stack_outputs.subnetPublic0 }},{{ vpc.stack_outputs.subnetPublic1 }},{{ vpc.stack_outputs.subnetPublic2 }}" 30 | asg_min_size: 1 31 | asg_max_size: 1 -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/SubPortExpectedMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sub Port List", 3 | "name": "SubPortExpectedMapping", 4 | "filterTypes": ["outstanding", "completed"], 5 | "mappings": [ 6 | { 7 | "displayName": "Received Date", 8 | "path": "$.recordReceivedAt", 9 | "type" : "DATE", 10 | "columnWidth": 120, 11 | "flexGrow": 1 12 | }, 13 | { 14 | "displayName": "Porting MSISDN", 15 | "path": "$.entityId.portingMsisdn", 16 | "searchable": true, 17 | "columnWidth": 120, 18 | "flexGrow": 1 19 | }, 20 | { 21 | "displayName": "RNO", 22 | "path": "$.noDetails.rno", 23 | "searchable": true, 24 | "columnWidth": 60, 25 | "flexGrow": 1 26 | }, 27 | { 28 | "displayName": "DNO", 29 | "path": "$.noDetails.dno", 30 | "searchable": true, 31 | "columnWidth": 60, 32 | "flexGrow": 1 33 | }, 34 | { 35 | "displayName": "Error", 36 | "path": "", 37 | "searchable": false, 38 | "columnWidth": 60, 39 | "flexGrow": 1 40 | }, 41 | { 42 | "displayName": "Status", 43 | "path": "$.status", 44 | "searchable": true, 45 | "columnWidth": 180, 46 | "flexGrow": 1 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /ui/test/resources/ReceivedFileFailedRecord.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "originalRecord": "CORRUPT LINE", 4 | "failure": { 5 | "failedAt": "2017-06-01T10:07:30.568+01:00[Europe/London]", 6 | "failureCode": "UNEXPECTED_BEHAVIOUR", 7 | "failureDescription": "Unknown error. Unable to parse line 'CORRUPT LINE', reason: String index out of range: 24", 8 | "failureType": "RECORD_FAILED_PROCESSING_FAILURE" 9 | }, 10 | "ono": "CN", 11 | "rno": "SK", 12 | "dno": "TEST_UNSEARCHABLE_FIELD_VALUE", 13 | "actionStatus": "FF", 14 | "actionCode": "01", 15 | "transactionNumber": "SK00000006", 16 | "msisdn": "447985680876" 17 | }, 18 | { 19 | "actionCode": "01", 20 | "actionStatus": "FF", 21 | "dno": "TEST_UNSEARCHABLE_FIELD_VALUE", 22 | "failure": { 23 | "failedAt": "2017-06-01T10:07:30.568+01:00[Europe/London]", 24 | "failureCode": "UNEXPECTED_BEHAVIOUR", 25 | "failureDescription": "Unknown error. Unable to parse line 'MANGOES', reason: String index out of range: 24", 26 | "failureType": "RECORD_FAILED_PROCESSING_FAILURE" 27 | }, 28 | "msisdn": "447985680876", 29 | "ono": "CN", 30 | "originalRecord": "MANGOES", 31 | "rno": "SK", 32 | "transactionNumber": "SK00000006" 33 | } 34 | ] -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/PortInDashMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Received Files2", 3 | "name": "PortInDashMapping", 4 | "filterTypes": ["completed"], 5 | "mappings": [ 6 | { 7 | "displayName": "Id", 8 | "path": "$.id.value", 9 | "searchable": true, 10 | "columnWidth": 200, 11 | "flexGrow": 1, 12 | "isKey": true 13 | }, 14 | { 15 | "displayName": "Type", 16 | "path": "$.id.type", 17 | "searchable": true, 18 | "columnWidth": 160 19 | }, 20 | { 21 | "displayName": "Timestamp", 22 | "path": "$.ts", 23 | "searchable": true, 24 | "type": "TIMESTAMP", 25 | "columnWidth": 260 26 | }, 27 | { 28 | "displayName": "Latitude", 29 | "path": "$.location.lon", 30 | "type" : "STRING", 31 | "searchable": true, 32 | "columnWidth": 360 33 | }, 34 | { 35 | "displayName": "Longitude", 36 | "path": "$.location.lat", 37 | "type": "STRING", 38 | "searchable": true, 39 | "columnWidth": 360 40 | }, 41 | { 42 | "displayName": "Accuracy", 43 | "path": "$.location.horizontal_accuracy", 44 | "type": "STRING", 45 | "searchable": true, 46 | "columnWidth": 360 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/PortOutExpectedMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Port Out List", 3 | "name": "PortOutExpectedMapping", 4 | "filterTypes": ["outstanding", "completed"], 5 | "mappings": [ 6 | { 7 | "displayName": "Port Date", 8 | "path": "$.entityId.portDate", 9 | "type" : "DATE", 10 | "searchable": true, 11 | "columnWidth": 120, 12 | "flexGrow": 1 13 | }, 14 | { 15 | "displayName": "Porting MSISDN", 16 | "path": "$.entityId.portingMsisdn", 17 | "searchable": true, 18 | "columnWidth": 120, 19 | "flexGrow": 1 20 | }, 21 | { 22 | "displayName": "ONO", 23 | "path": "$.noDetails.ono", 24 | "searchable": true, 25 | "columnWidth": 60, 26 | "flexGrow": 1 27 | }, 28 | { 29 | "displayName": "RNO", 30 | "path": "$.portOutSecuredDetails.rnoSyniverseCode", 31 | "searchable": true, 32 | "columnWidth": 60, 33 | "flexGrow": 1 34 | }, 35 | { 36 | "displayName": "Error", 37 | "path": "", 38 | "columnWidth": 60, 39 | "flexGrow": 1 40 | }, 41 | { 42 | "displayName": "Status", 43 | "path": "$.status", 44 | "searchable": true, 45 | "columnWidth": 120, 46 | "flexGrow": 1 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/ContentContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ContentContainer = ({ 4 | name, 5 | controls, 6 | actions, 7 | contentType, 8 | children 9 | }) => { 10 | return ( 11 |
    12 |
    13 |
    14 |
    15 |
    { name }
    16 |
    17 | { actions } 18 |
    19 |
    20 |
    21 | { controls } 22 |
    23 |
      24 |
    25 |
    26 |
    27 | { children } 28 |
    29 |
    30 |
    31 | ); 32 | }; 33 | 34 | export default ContentContainer; -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/ReceivedFileRecordMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Failed File Records", 3 | "name": "ReceivedFileRecordMapping", 4 | "filterTypes": [], 5 | "mappings": [ 6 | { 7 | "displayName": "MSISDN", 8 | "path": "$.msisdn", 9 | "searchable": true, 10 | "columnWidth": 120 11 | }, 12 | { 13 | "displayName": "DNO", 14 | "path": "$.dno", 15 | "searchable": true, 16 | "columnWidth": 40 17 | }, 18 | { 19 | "displayName": "RNO", 20 | "path": "$.rno", 21 | "searchable": true, 22 | "columnWidth": 40 23 | }, 24 | { 25 | "displayName": "ONO", 26 | "path": "$.ono", 27 | "searchable": true, 28 | "columnWidth": 40 29 | }, 30 | { 31 | "displayName": "Error Code", 32 | "path": "$.failure.failureCode", 33 | "searchable": true, 34 | "columnWidth": 120, 35 | "flexGrow": 2 36 | }, 37 | { 38 | "displayName": "Error Description", 39 | "path": "$.failure.failureDescription", 40 | "searchable": true, 41 | "columnWidth": 180, 42 | "flexGrow": 2 43 | }, 44 | { 45 | "displayName": "Original Record", 46 | "path": "$.originalRecord", 47 | "searchable": true, 48 | "columnWidth": 100, 49 | "flexGrow": 2 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /conf/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${ENVIRONMENT_NAME} %date %coloredLevel %logger{15} - %message%n%xException{10} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ui/test/util/TableIconsTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import chai from 'chai' 5 | import chaiEnzyme from 'chai-enzyme' 6 | chai.use(chaiEnzyme()); 7 | import * as Icons from '../../app/src/scripts/util/TableIcons'; 8 | 9 | describe("completedStatusIcon", function() { 10 | it("should return green 'done' icon if true", function () { 11 | let icon = shallow(Icons.completedStatusIcon(true)); 12 | 13 | expect(icon).to.have.text('done'); 14 | expect(icon).to.have.className('green-text'); 15 | }); 16 | 17 | it("should return red 'close' icon if true", function () { 18 | let icon = shallow(Icons.completedStatusIcon(false)); 19 | expect(icon).to.have.text('close'); 20 | expect(icon).to.have.className('red-text'); 21 | }); 22 | }); 23 | 24 | describe("errorStatusIcon", function() { 25 | it("should return red 'error_outline' icon if true", function () { 26 | let icon = shallow(Icons.errorStatusIcon(true)); 27 | expect(icon).to.have.text('error_outline'); 28 | expect(icon).to.have.className('red-text'); 29 | }); 30 | 31 | it("should return empty string if false", function () { 32 | let icon = Icons.errorStatusIcon(false); 33 | expect(icon).to.have.lengthOf(0); 34 | }); 35 | }); -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/TableControlsWithSearchAndCount.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TableSearchBox from './TableSearchBox'; 3 | import CountBox from './CountBox'; 4 | import GridColumnWrapper from './GridColumnWrapper'; 5 | 6 | const TableControlsWithSearchAndCount = ({ 7 | searchMethod, 8 | resultCount, 9 | searchText, 10 | children 11 | }) => { 12 | return ( 13 |
    14 | 15 | 17 | 18 | {/**/} 19 | {/*{children}*/} 20 | {/**/} 21 | 22 | {/**/} 23 | 24 |
    25 | ); 26 | }; 27 | 28 | export default TableControlsWithSearchAndCount; -------------------------------------------------------------------------------- /ui/test/actions/JoyrideActionTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as actionCreator from '../../app/src/scripts/actions/JoyrideAction' 3 | 4 | describe('(REDUX) JoyrideAction setStepIndex(int)', function() { 5 | it('should return object with type SET_STEP_INDEX and stepIndex content', function() { 6 | let stepIndex = 3; 7 | let action = actionCreator.setStepIndex(stepIndex); 8 | 9 | expect(action).to.deep.equal({ 10 | "type": "SET_STEP_INDEX", 11 | "content": { 12 | "stepIndex": stepIndex 13 | } 14 | }) 15 | }); 16 | }); 17 | 18 | describe('(REDUX) JoyrideAction addStep(object)', function() { 19 | it('should return object with type ADD_STEP and step content', function() { 20 | let step = {"step":"step"}; 21 | let action = actionCreator.addStep(step); 22 | 23 | expect(action).to.deep.equal({ 24 | "type": "ADD_STEP", 25 | "content": { 26 | "step": step 27 | } 28 | }) 29 | }); 30 | }); 31 | 32 | describe('(REDUX) JoyrideAction clearSteps()', function() { 33 | it('should return object with CLEAR_STEPS', function() { 34 | let action = actionCreator.clearSteps(); 35 | 36 | expect(action).to.deep.equal({ 37 | "type": "CLEAR_STEPS" 38 | }) 39 | }); 40 | }); -------------------------------------------------------------------------------- /ui/app/src/scripts/components/Content.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {connect} from 'react-redux'; 5 | import * as Actions from '../actions/JoyrideAction'; 6 | import ContentContainer from './presentational/ContentContainer'; 7 | import { callFunctionWithParamIfDefined } from '../util/DefinedPathUtils'; 8 | import $ from 'jquery'; 9 | window.jQuery = window.$ = $; 10 | require ("materialize-css"); 11 | 12 | class Content extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | componentDidMount() { 18 | Array.from(this.props.controls).forEach(control => 19 | callFunctionWithParamIfDefined(this.props.joyride, "stepMapping." + control.props.id + ".data" , this.props.addStep)); 20 | } 21 | 22 | render() { 23 | return ( 24 | 28 | { this.props.children } 29 | 30 | ); 31 | } 32 | } 33 | 34 | function mapStateToProps(state) { 35 | return { 36 | joyride: state.joyride 37 | } 38 | } 39 | 40 | export default connect(mapStateToProps, Actions)(Content); -------------------------------------------------------------------------------- /ui/app/src/scripts/reducers/controlReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_PAGE_TYPE, TOGGLE_FILTER_CRITERIA, COMPLETED, OUTSTANDING, SET_SEARCH_TEXT } from '../actions/ControlAction'; 2 | 3 | const initialState = { 4 | pageType: '', 5 | searchText: '', 6 | shouldShowCompleted: false, 7 | shouldShowOutstanding: true 8 | }; 9 | 10 | function toggleFilterCriteria(state, criteria) { 11 | switch (criteria) { 12 | case COMPLETED: 13 | return { 14 | ...state, 15 | shouldShowCompleted: !state.shouldShowCompleted 16 | }; 17 | case OUTSTANDING: 18 | return { 19 | ...state, 20 | shouldShowOutstanding: !state.shouldShowOutstanding 21 | }; 22 | default: 23 | return state; 24 | } 25 | } 26 | 27 | export default function(state = initialState, action) { 28 | switch (action.type) { 29 | case SET_PAGE_TYPE: 30 | return { 31 | ...state, 32 | pageType: action.content.pageType 33 | }; 34 | case SET_SEARCH_TEXT: 35 | return { 36 | ...state, 37 | searchText: action.content.searchText 38 | }; 39 | case TOGGLE_FILTER_CRITERIA: 40 | return toggleFilterCriteria(state, action.content.criteria); 41 | default: 42 | return state; 43 | } 44 | } -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - label: build 3 | command: scripts/build 4 | timeout_in_minutes: 15 5 | concurrency: 1 6 | concurrency_group: "link-mobile-observations" 7 | agents: 8 | queue: link-scala-dev-nyc 9 | 10 | - label: package 11 | command: scripts/package 12 | timeout_in_minutes: 5 13 | concurrency: 1 14 | concurrency_group: "link-mobile-observations" 15 | artifact_paths: target/aws/link-mobile-observations.zip 16 | agents: 17 | queue: link-scala-dev-nyc 18 | env: 19 | APPNAME: link-mobile-observations 20 | 21 | - block: dev 22 | 23 | - label: dev deploy 24 | command: scripts/prep-deploy && scripts/deploy 25 | timeout_in_minutes: 30 26 | concurrency: 1 27 | concurrency_group: "link-mobile-observations" 28 | agents: 29 | queue: link-scala-dev-nyc 30 | env: 31 | APPNAME: link-mobile-observations 32 | LINK_ENV: dev 33 | 34 | - block: qa 35 | 36 | - label: qa deploy 37 | command: scripts/prep-deploy && scripts/deploy 38 | timeout_in_minutes: 30 39 | agents: 40 | queue: link-scala-qa-nyc 41 | env: 42 | APPNAME: link-mobile-observations 43 | LINK_ENV: qa 44 | 45 | 46 | - block: release to production 47 | 48 | - label: production deploy 49 | command: scripts/prep-deploy && scripts/deploy 50 | timeout_in_minutes: 30 51 | agents: 52 | queue: link-scala-prod-nyc 53 | env: 54 | APPNAME: link-mobile-observations 55 | LINK_ENV: prod 56 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/SubPortErrorMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sub Port Errors", 3 | "name": "SubPortErrorMapping", 4 | "filterTypes": ["outstanding"], 5 | "mappings": [ 6 | { 7 | "displayName": "Received Date", 8 | "path": "$.recordReceivedAt", 9 | "type" : "DATE", 10 | "searchable": true, 11 | "columnWidth": 120 12 | }, 13 | { 14 | "displayName": "Porting MSISDN", 15 | "path": "$.portingMsisdn", 16 | "searchable": true, 17 | "columnWidth": 120 18 | }, 19 | { 20 | "displayName": "RNO", 21 | "path": "$.noDetails.rno", 22 | "searchable": true, 23 | "columnWidth": 60 24 | }, 25 | { 26 | "displayName": "DNO", 27 | "path": "$.noDetails.dno", 28 | "searchable": true, 29 | "columnWidth": 60 30 | }, 31 | { 32 | "displayName": "Failure Code", 33 | "path": "$.failure.failureCode", 34 | "searchable": true, 35 | "columnWidth": 240, 36 | "flexGrow": 1 37 | }, 38 | { 39 | "displayName": "Failure Description", 40 | "path": "$.failure.failureDescription", 41 | "columnWidth": 260, 42 | "flexGrow": 1 43 | }, 44 | { 45 | "displayName": "Time of Failure", 46 | "path": "$.failure.failedAt", 47 | "searchable": true, 48 | "columnWidth": 300 49 | }, 50 | { 51 | "displayName": "Retry Button", 52 | "path": "", 53 | "searchable": false, 54 | "columnWidth": 120 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/components/FakeCircleThingy.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from "react"; 3 | import {connect} from 'react-redux'; 4 | import * as Actions from '../actions'; 5 | import { CircleThingy } from './CircleThingy.jsx'; 6 | 7 | class FakeCircleThingy extends CircleThingy { 8 | 9 | tooltipFunction(tooltip, that) { 10 | if (tooltip.body && tooltip.body.length > 0 && tooltip.body[0].lines && tooltip.body[0].lines.length > 0) { 11 | let dataMapping = that.mapping[tooltip.dataPoints[0].index]; 12 | tooltip.body[0].lines[0] = dataMapping.display + ": " + (that.props[dataMapping.type + "Value"] ? that.props[dataMapping.type + "Value"] : 0); 13 | tooltip.width = that.getWidth(that, tooltip); 14 | } 15 | } 16 | 17 | /** 18 | * Overrides the CircleThingy componentDidMount method, to prevent adding steps for fake circles as well as real ones. 19 | */ 20 | componentDidMount() {} 21 | 22 | mapData() { 23 | return new Map([ 24 | ["done", this.props.doneValue ? this.props.doneValue : 0], 25 | ["error", this.props.errorValue ? this.props.errorValue : 0], 26 | ["notDone", this.props.notDoneValue ? this.props.notDoneValue : 0] 27 | ]); 28 | } 29 | } 30 | 31 | function mapStateToProps(state) { 32 | return { 33 | summaries: state.data.get("summaries").toJS(), 34 | joyride: state.joyride 35 | } 36 | } 37 | 38 | export default connect(mapStateToProps, Actions)(FakeCircleThingy); 39 | -------------------------------------------------------------------------------- /app/modules/AwsClientsModule.scala: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import javax.inject.Inject 4 | import actors.{KmeansActor, ProxyActor, UserEventActor, UserEventFactoryActor} 5 | import akka.actor.{Actor, ActorLogging} 6 | import akka.event.LoggingReceive 7 | import com.amazonaws.auth.DefaultAWSCredentialsProviderChain 8 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient 9 | import com.google.inject.{AbstractModule, Singleton} 10 | import play.api.libs.concurrent.AkkaGuiceSupport 11 | import play.api.{Configuration, Environment} 12 | 13 | class AwsClientsModule(environment: Environment, configuration: Configuration) extends AbstractModule with AkkaGuiceSupport { 14 | override def configure() = { 15 | val credentialsProviderChain = new DefaultAWSCredentialsProviderChain 16 | val dynamoDBClient = new AmazonDynamoDBClient(credentialsProviderChain) 17 | bind(classOf[AmazonDynamoDBClient]).toInstance(dynamoDBClient) 18 | 19 | // val s3Client = new AmazonS3Client(credentialsProviderChain) 20 | // bind(classOf[AmazonS3Client]).toInstance(s3Client) 21 | 22 | bindActor[UserEventFactoryActor]("userEventFactoryActor") 23 | bindActorFactory[UserEventActor, UserEventActor.Factory] 24 | bindActor[MyActor]("myActor") 25 | bindActor[ProxyActor]("proxyActor") 26 | bindActor[KmeansActor]("kActor") 27 | } 28 | 29 | } 30 | 31 | @Singleton 32 | class MyActor @Inject()() extends Actor with ActorLogging { 33 | def receive = LoggingReceive { 34 | case m => log.info(s"message ${m}") 35 | } 36 | 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/ReceivedFileMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Received Files", 3 | "name": "ReceivedFileMapping", 4 | "filterTypes": ["completed"], 5 | "mappings": [ 6 | { 7 | "displayName": "Filename", 8 | "path": "$.entityId.absoluteFilePath", 9 | "searchable": true, 10 | "columnWidth": 300, 11 | "flexGrow": 2, 12 | "isKey": true 13 | }, 14 | { 15 | "displayName": "File Type", 16 | "path": "$.fileType", 17 | "searchable": true, 18 | "columnWidth": 60 19 | }, 20 | { 21 | "displayName": "Date Received", 22 | "path": "$.acceptedAt", 23 | "type" : "DATE", 24 | "searchable": true, 25 | "columnWidth": 120 26 | }, 27 | { 28 | "displayName": "MNO", 29 | "path": "$.sourceNetworkOperator", 30 | "searchable": true, 31 | "columnWidth": 60 32 | }, 33 | { 34 | "displayName": "Status", 35 | "path": "$.status", 36 | "searchable": true, 37 | "columnWidth": 180, 38 | "flexGrow": 1 39 | }, 40 | { 41 | "displayName": "No. of Records", 42 | "path": "$.numberOfRecords", 43 | "columnWidth": 120 44 | }, 45 | { 46 | "displayName": "No. of Successful Records", 47 | "path": "$.successfulRecords.length", 48 | "columnWidth": 120 49 | }, 50 | { 51 | "displayName": "No. of Failed Records", 52 | "path": "$.failedRecords.length", 53 | "columnWidth": 120 54 | }, 55 | { 56 | "displayName": "View Failures", 57 | "path": "", 58 | "columnWidth": 140 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /ui/app/src/scripts/util/DefinedPathUtils.js: -------------------------------------------------------------------------------- 1 | function getDefined(obj, path) { 2 | let pathSegments = path.split('.'), 3 | i; 4 | 5 | for (i = 0; pathSegments[i]; i++) { 6 | obj = obj[pathSegments[i]]; 7 | } 8 | 9 | return obj; 10 | } 11 | 12 | /** 13 | * Attempts to retrieve a property from a given object based on a path. 14 | * 15 | * If any property in the given path is undefined, returns the provided alternate value instead. 16 | * 17 | * @param objParam The object to search for a property in. 18 | * @param pathParam The path to attempt to retrieve a property from. 19 | * @param alt The value to return in case of failure. 20 | * @returns {*} 21 | */ 22 | export function getDefinedOrElse(objParam, pathParam, alt) { 23 | try { 24 | let result = getDefined(objParam, pathParam); 25 | if (typeof result !== "undefined") return result; 26 | } catch (e) {} 27 | return alt; 28 | } 29 | 30 | /** 31 | * Attempts to call a given function, passing in a property from the given object based on a path as a parameter. 32 | * 33 | * If any property in the given path is undefined, no function will be called. 34 | * 35 | * @param objParam The object to search for a property in. 36 | * @param pathParam The path to attempt to retrieve a property from. 37 | * @param functionParam The function to call with the found parameter. 38 | * @returns {*} 39 | */ 40 | export function callFunctionWithParamIfDefined(objParam, pathParam, functionParam) { 41 | try { 42 | let result = getDefined(objParam, pathParam); 43 | if (typeof result !== "undefined") functionParam(result); 44 | } catch (e) {} 45 | } -------------------------------------------------------------------------------- /scripts/package: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CWD=$(pwd) 6 | APPNAME=${APPNAME:-link-mobile-observations} 7 | ZIP=target/aws/link-mobile-observations.zip 8 | 9 | cd ansible || exit 10 | rm -rf roles/* 11 | ansible-galaxy install -f -r requirements.yml 12 | ansible-playbook ${APPNAME}-build.yml -e "appname=${APPNAME}" 13 | cd $CWD 14 | 15 | ./scripts/write-version 16 | ./scripts/write-git-version 17 | source ./version.txt 18 | echo Version $TAG 19 | source ./git-version.txt 20 | echo Git tag 21 | cat ./git-version.txt 22 | 23 | AWS_ACCOUNT_ID=028957328603 24 | 25 | eval $(aws ecr get-login --registry-ids $AWS_ACCOUNT_ID --region us-east-1 --no-include-email) 26 | 27 | 28 | ./scripts/build-sbt-reactjs 29 | 30 | sbt "npm install" 31 | sbt clean stage docker:publishLocal 32 | 33 | docker tag $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/intersection/link-mobile-observations:$TAG $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/intersection/link-mobile-observations:$GITTAG 34 | docker push $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/intersection/link-mobile-observations:$GITTAG 35 | 36 | #Point Beanstalk configuration file to correct image 37 | sed -i='' "s//$AWS_ACCOUNT_ID/" config/Dockerrun.aws.json 38 | sed -i='' "s//$GITTAG/" config/Dockerrun.aws.json 39 | 40 | #Zip the dockerrun file and the ebextensions directory 41 | mkdir -p target/aws 42 | mv config/Dockerrun.aws.json . && mv config/ebextensions .ebextensions 43 | echo "---------------- Docker run --------------" 44 | cat Dockerrun.aws.json 45 | echo "---------------- Docker run --------------" 46 | zip -r $ZIP Dockerrun.aws.json .ebextensions config/link-mobile-observations-cloudformation.json -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/PortOutErrorMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Port Out Errors", 3 | "name": "PortOutErrorMapping", 4 | "filterTypes": ["outstanding"], 5 | "mappings": [ 6 | { 7 | "displayName": "Port Date", 8 | "path": "$.entityId.portDate", 9 | "type" : "DATE", 10 | "searchable": true, 11 | "columnWidth": 120 12 | }, 13 | { 14 | "displayName": "Porting MSISDN", 15 | "path": "$.entityId.portingMsisdn", 16 | "searchable": true, 17 | "columnWidth": 120 18 | }, 19 | { 20 | "displayName": "DNO", 21 | "path": "$.portOutSecuredDetails.dnoSyniverseCode", 22 | "searchable": true, 23 | "columnWidth": 60 24 | }, 25 | { 26 | "displayName": "ONO", 27 | "path": "$.noDetails.ono", 28 | "searchable": true, 29 | "columnWidth": 60 30 | }, 31 | { 32 | "displayName": "RNO", 33 | "path": "$.portOutSecuredDetails.rnoSyniverseCode", 34 | "searchable": true, 35 | "columnWidth": 60 36 | }, 37 | { 38 | "displayName": "Failure Code", 39 | "path": "$.failure.failureCode", 40 | "searchable": true, 41 | "columnWidth": 240, 42 | "flexGrow": 1 43 | }, 44 | { 45 | "displayName": "Failure Description", 46 | "path": "$.failure.failureDescription", 47 | "columnWidth": 260, 48 | "flexGrow": 1 49 | }, 50 | { 51 | "displayName": "Time of Failure", 52 | "path": "$.failure.failedAt", 53 | "searchable": true, 54 | "columnWidth": 300 55 | }, 56 | { 57 | "displayName": "Retry Button", 58 | "path": "", 59 | "columnWidth": 120 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/actions/DataAction.js: -------------------------------------------------------------------------------- 1 | // import fetch from 'isomorphic-fetch'; 2 | 3 | export const POPULATE_ORDERS = 'POPULATE_ORDERS'; 4 | 5 | export function populateOrders(orders) { 6 | // console.log("orders "+ orders) 7 | return { 8 | type: POPULATE_ORDERS, 9 | content: { 10 | orders: orders 11 | } 12 | } 13 | } 14 | 15 | export const UPDATE_ORDERS = 'UPDATE_ORDERS'; 16 | 17 | export function updateOrders(orders) { 18 | return { 19 | type: UPDATE_ORDERS, 20 | content: { 21 | orders: orders 22 | } 23 | } 24 | } 25 | 26 | export const REMOVE_ORDERS = 'REMOVE_ORDERS'; 27 | 28 | export function removeOrders(orders) { 29 | return { 30 | type: REMOVE_ORDERS, 31 | content: { 32 | orders: orders 33 | } 34 | } 35 | } 36 | 37 | export const CLEAR_ORDERS = 'CLEAR_ORDERS'; 38 | 39 | export function clearOrders() { 40 | return { 41 | type: CLEAR_ORDERS 42 | } 43 | } 44 | 45 | export const POPULATE_SUMMARIES = 'POPULATE_SUMMARIES'; 46 | 47 | export function populateSummaries(summaries) { 48 | return { 49 | type: POPULATE_SUMMARIES, 50 | content: { 51 | summaries: summaries 52 | } 53 | } 54 | } 55 | 56 | export const UPDATE_SUMMARIES = 'UPDATE_SUMMARIES'; 57 | 58 | export function updateSummaries(summaries) { 59 | return { 60 | type: UPDATE_SUMMARIES, 61 | content: { 62 | summaries: summaries 63 | } 64 | } 65 | } 66 | 67 | export const CLEAR_SUMMARIES = 'CLEAR_SUMMARIES'; 68 | 69 | export function clearSummaries() { 70 | return { 71 | type: CLEAR_SUMMARIES 72 | } 73 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/components/presentational/LoginBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginBox = ({ 4 | onSubmitMethod, 5 | error, 6 | children 7 | }) => { 8 | return ( 9 |
    10 |
    11 |
    12 |
    13 | 20 |
    21 | 22 | { error } 23 | 24 |
    25 | { children } 26 |
    27 | 31 | 33 |
    34 |
    35 |
    36 |
    37 |
    38 | ); 39 | }; 40 | 41 | export default LoginBox; -------------------------------------------------------------------------------- /ui/test/actions/LoginActionTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as actionCreator from '../../app/src/scripts/actions/LoginAction' 3 | 4 | describe('(REDUX) LoginAction loginRequest()', function() { 5 | it('should return object with type LOGIN_REQUEST', function() { 6 | let action = actionCreator.loginRequest(); 7 | expect(action).to.deep.equal({"type": "LOGIN_REQUEST"}) 8 | }); 9 | }); 10 | 11 | describe('(REDUX) LoginAction loginResponse()', function() { 12 | it('should return object with type LOGIN_RESPONSE', function() { 13 | let action = actionCreator.loginResponse(); 14 | expect(action).to.deep.equal({"type": "LOGIN_RESPONSE"}) 15 | }); 16 | }); 17 | 18 | describe('(REDUX) LoginAction loginError(string)', function() { 19 | it('should return object with type LOGIN_ERROR and error message content', function() { 20 | let errorMessage = "Oh no error message!"; 21 | let action = actionCreator.loginError(errorMessage); 22 | 23 | expect(action).to.deep.equal({"type": "LOGIN_ERROR", "content": { "message": errorMessage}}) 24 | }); 25 | }); 26 | 27 | describe('(REDUX) LoginAction loginUser(Object)', function() { 28 | it('(Function) loginUser() should successfully send login to backend', function() { 29 | // let values = {"username": "123", "password":"123"}; 30 | // let action = actionCreator.loginUser(values); 31 | // 32 | // //returns [Function] -> [Promise]? 33 | // 34 | // //reduxForm.handleSubmit(values)?? 35 | // //returns function(dispatch) -> dispatch(something) 36 | // //how to test dispatches -> Jasmine 37 | // 38 | // expect(action).to.be.equal(""); 39 | }) 40 | }); 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/Header.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import HeaderContainer from './presentational/HeaderContainer' 5 | import DropDownContainer from './presentational/DropDownContainer' 6 | import DropDownItem from './presentational/DropDownItem' 7 | import DropDownDivider from './presentational/DropDownDivider' 8 | import HeaderButton from './presentational/HeaderButton' 9 | import { connect } from 'react-redux'; 10 | import * as Actions from '../actions/JoyrideAction'; 11 | 12 | class Header extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.triggerPageChange = this.triggerPageChange.bind(this); 16 | this.resetJoyride = this.resetJoyride.bind(this); 17 | } 18 | 19 | triggerPageChange(page) { 20 | this.props.callbackParent(page.target.className); 21 | } 22 | 23 | resetJoyride() { 24 | this.props.joyrideCallback(); 25 | } 26 | 27 | componentDidMount() {} 28 | 29 | render() { 30 | return ( 31 | 32 | 34 | 38 | {/**/} 39 | {/**/} 43 | {/**/} 44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | function mapStateToProps(state) { 52 | return { 53 | joyride: state.joyride 54 | } 55 | } 56 | 57 | export default connect(mapStateToProps, Actions)(Header); -------------------------------------------------------------------------------- /ui/app/src/scripts/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // import $ from 'jquery'; 4 | // window.jQuery = window.$ = $; 5 | import React from 'react'; 6 | import {connect} from 'react-redux'; 7 | import {Field, reduxForm} from 'redux-form/immutable'; 8 | import * as Actions from '../actions/LoginAction'; 9 | import LoginBox from './presentational/LoginBox'; 10 | import LoginField from './presentational/LoginField'; 11 | import LoginError from './presentational/LoginError'; 12 | 13 | class LoginForm extends React.Component { 14 | 15 | handleFormSubmit = (values) => { 16 | this.props.loginUser(values, this.props.successMethod); 17 | }; 18 | 19 | buildError = () => { 20 | return this.props.loginState.error ? 21 | 22 | : "" 23 | }; 24 | 25 | render() { 26 | let fields = [ 27 | , 35 | ]; 42 | return ( 43 | 46 | { fields } 47 | 48 | ); 49 | } 50 | } 51 | 52 | function mapStateToProps(state) { 53 | return { 54 | loginState: state.login 55 | } 56 | } 57 | 58 | export default connect(mapStateToProps, Actions)(reduxForm({ 59 | form: 'login' 60 | })(LoginForm)); -------------------------------------------------------------------------------- /ui/test/util/DefinedPathUtilsTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getDefinedOrElse, callFunctionWithParamIfDefined } from '../../app/src/scripts/util/DefinedPathUtils'; 3 | 4 | const testObject = { 5 | 'test1': { 6 | 'test2': { 7 | 'test3': "FOO" 8 | }, 9 | 'badTest': undefined 10 | }}; 11 | 12 | describe('(Function) getDefinedOrElse', function () { 13 | it('should return value from given path if defined', function () { 14 | let result = getDefinedOrElse(testObject, "test1.test2.test3", "BAR"); 15 | 16 | expect(result).to.equal("FOO"); 17 | }); 18 | 19 | it('should return alternate value if given path is not defined', function () { 20 | let result = getDefinedOrElse(testObject, "test1.mango.test3", "BAR"); 21 | 22 | expect(result).to.equal("BAR"); 23 | }); 24 | 25 | it('should return alternate value if given path is defined but value at path is undefined', function () { 26 | let result = getDefinedOrElse(testObject, "test1.badTest", "BAR"); 27 | 28 | expect(result).to.equal("BAR"); 29 | }); 30 | }); 31 | 32 | describe('(Function) callFunctionWithParamIfDefined', function () { 33 | it('should call given function if given path is defined', function () { 34 | var result = ""; 35 | 36 | callFunctionWithParamIfDefined(testObject, "test1.test2.test3", (obj => result = obj)); 37 | 38 | expect(result).to.equal("FOO"); 39 | }); 40 | 41 | it('should not call given function if given path is undefined', function () { 42 | var result = ""; 43 | 44 | callFunctionWithParamIfDefined(testObject, "test1.mango.test3", (obj => result = obj)); 45 | 46 | expect(result).to.equal(""); 47 | }); 48 | 49 | it('should not call given function if given path is defined but value at path is undefined', function () { 50 | var result = ""; 51 | 52 | callFunctionWithParamIfDefined(testObject, "test1.badtest", (obj => result = obj)); 53 | 54 | expect(result).to.equal(""); 55 | }); 56 | }); -------------------------------------------------------------------------------- /ui/test/actions/ControlActionTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as actionCreator from '../../app/src/scripts/actions/ControlAction' 3 | 4 | describe('(REDUX) ControlAction dataReducer(string)', function() { 5 | it('should return object with type \'TOGGLE_FILTER_CRITERIA\' and message content \'COMPLETED\' when passed COMPLETED as parameter.', function() { 6 | let action = actionCreator.toggleFilterCriteria(actionCreator.COMPLETED) 7 | 8 | expect(action).to.deep.equal({ 9 | "type": "TOGGLE_FILTER_CRITERIA", 10 | "content":{ 11 | "criteria":actionCreator.COMPLETED 12 | } 13 | }) 14 | }); 15 | 16 | it('should return object with type \'TOGGLE_FILTER_CRITERIA\' and message content \'OUTSTANDING\' when passed OUTSTANDING AS parameter', function() { 17 | let action = actionCreator.toggleFilterCriteria(actionCreator.OUTSTANDING) 18 | 19 | expect(action).to.deep.equal({ 20 | "type": "TOGGLE_FILTER_CRITERIA", 21 | "content":{ 22 | "criteria":actionCreator.OUTSTANDING 23 | } 24 | }) 25 | }); 26 | }); 27 | 28 | describe('(REDUX) ControlAction setPageType(string)', function() { 29 | it('should return object with type \'SET_PAGE_TYPE\' and pageType content \'index\'', function () { 30 | let action = actionCreator.setPageType("index"); 31 | 32 | expect(action).to.deep.equal({ 33 | "type": "SET_PAGE_TYPE", 34 | "content":{ 35 | "pageType":"index" 36 | } 37 | }) 38 | }); 39 | }); 40 | 41 | describe('(REDUX) ControlAction setSearchText(string)', function() { 42 | it('should return object with type \'SET_SEARCH_TEXT\' and searchText content \'Mango\'', function () { 43 | let action = actionCreator.setSearchText("Mango"); 44 | 45 | expect(action).to.deep.equal({ 46 | "type": "SET_SEARCH_TEXT", 47 | "content":{ 48 | "searchText":"Mango" 49 | } 50 | }) 51 | }); 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /ui/app/src/scripts/actions/LoginAction.js: -------------------------------------------------------------------------------- 1 | // import fetch from 'isomorphic-fetch'; 2 | import { change } from 'redux-form'; 3 | 4 | export const LOGIN_REQUEST = 'LOGIN_REQUEST'; 5 | 6 | export function loginRequest() { 7 | return { 8 | type: LOGIN_REQUEST 9 | } 10 | } 11 | 12 | export const LOGIN_RESPONSE = 'LOGIN_RESPONSE'; 13 | 14 | export function loginResponse() { 15 | return { 16 | type: LOGIN_RESPONSE 17 | } 18 | } 19 | 20 | export const LOGIN_ERROR = 'LOGIN_ERROR'; 21 | 22 | export function loginError(error) { 23 | return { 24 | type: LOGIN_ERROR, 25 | content: { 26 | message: error 27 | } 28 | } 29 | } 30 | 31 | export function loginUser(values, successMethod = null) { 32 | return function (dispatch) { 33 | dispatch(loginRequest()); 34 | 35 | let credentials = { 36 | username: values.get('username').toLowerCase().trim(), 37 | password: values.get('password').trim() 38 | }; 39 | 40 | var headers = new Headers(); 41 | headers.append("Content-Type", "application/json"); 42 | 43 | let fetchParams = { 44 | method: 'POST', 45 | headers: headers, 46 | body: JSON.stringify(credentials), 47 | credentials: 'same-origin' 48 | }; 49 | 50 | return fetch('/login', fetchParams) 51 | .then(response => response.json().then(json => { 52 | dispatch(change('login', 'password', '')); 53 | if (json.success) { 54 | dispatch(loginResponse()); 55 | if (successMethod && typeof(successMethod) === 'function') { 56 | successMethod(); 57 | } else { 58 | window.location.replace(json.redirect); 59 | } 60 | } 61 | else { 62 | dispatch(loginError(json.error)); 63 | } 64 | })) 65 | .catch(error => dispatch(loginError("Login failed, please try again later."))); 66 | } 67 | } -------------------------------------------------------------------------------- /conf/logback-prod.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${application.home:-.}/logs/application.log 8 | 9 | [${ENVIRONMENT_NAME:-default}] %date [%level] from %logger in %thread - %message%n%xException 10 | 11 | 12 | 13 | 14 | 15 | %date [${ENVIRONMENT_NAME:-default}]%coloredLevel %logger{15} - %message%n%xException{10} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/PortInErrorMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Port In Errors", 3 | "name": "PortInErrorMapping", 4 | "filterTypes": ["outstanding"], 5 | "mappings": [ 6 | { 7 | "displayName": "Port Date", 8 | "path": "$.entityId.portDate", 9 | "type" : "DATE", 10 | "searchable": true, 11 | "columnWidth": 120 12 | }, 13 | { 14 | "displayName": "PAC", 15 | "path": "$.entityId.pac", 16 | "searchable": true, 17 | "columnWidth": 100 18 | }, 19 | { 20 | "displayName": "Porting MSISDN", 21 | "path": "$.entityId.portingMsisdn", 22 | "searchable": true, 23 | "columnWidth": 120 24 | }, 25 | { 26 | "displayName": "Sky Allocated MSISDN", 27 | "path": "$.skyMsisdn", 28 | "searchable": true, 29 | "columnWidth": 120 30 | }, 31 | { 32 | "displayName": "Service ID", 33 | "path": "$.serviceId", 34 | "searchable": true, 35 | "columnWidth": 80 36 | }, 37 | { 38 | "displayName": "Operator ID", 39 | "path": "$.operatorId", 40 | "searchable": true, 41 | "columnWidth": 80 42 | }, 43 | { 44 | "displayName": "DSP", 45 | "path": "$.portInSecuredDetails.dspSyniverseCode", 46 | "searchable": true, 47 | "columnWidth": 40 48 | }, 49 | { 50 | "displayName": "DNO", 51 | "path": "$.portInSecuredDetails.dnoSyniverseCode", 52 | "searchable": true, 53 | "columnWidth": 40 54 | }, 55 | { 56 | "displayName": "Failure Code", 57 | "path": "$.failures.failureCode", 58 | "searchable": true, 59 | "columnWidth": 240, 60 | "flexGrow": 1 61 | }, 62 | { 63 | "displayName": "Failure Description", 64 | "path": "$.failures.failureDescription", 65 | "columnWidth": 260, 66 | "flexGrow": 1 67 | }, 68 | { 69 | "displayName": "Time of Failure", 70 | "path": "$.failures.failedAt", 71 | "searchable": true, 72 | "columnWidth": 300 73 | }, 74 | { 75 | "displayName": "Retry Button", 76 | "path": "", 77 | "columnWidth": 120 78 | } 79 | ] 80 | } -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${application.home:-.}/logs/application-${ENVIRONMENT_NAME:-default}.log 7 | 8 | %date [%level] from %logger in %thread - %message%n%xException 9 | 10 | 11 | 12 | 13 | 14 | %date [${ENVIRONMENT_NAME:-default}] %coloredLevel %logger{15} - %message%n%xException{10} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ui/test/reducers/LoginReducerTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import loginReducer from '../../app/src/scripts/reducers/loginReducer' 3 | 4 | describe('(REDUX) LoginReducer', function() { 5 | it('should return state with no modifications if action is not recognised', function () { 6 | expect(loginReducer(initialState(), ACTION_EMPTY)).to.deep.equal(initialState()); 7 | }); 8 | 9 | it('should return initialState if state is undefined', function () { 10 | expect(loginReducer(undefined, ACTION_EMPTY)).to.deep.equal(initialState()); 11 | }); 12 | }); 13 | 14 | describe('(REDUX) LoginReducer (ACTION) LOGIN_RESPONSE', function() { 15 | it('should return original state with empty error', function() { 16 | expect(loginReducer(initialState(), ACTION_LOGIN_RESPONSE)).to.deep.equal(initialState()); 17 | expect(loginReducer(initialState(), ACTION_LOGIN_RESPONSE).error).to.have.lengthOf(0); 18 | }); 19 | }); 20 | 21 | describe('(REDUX) LoginReducer (ACTION) LOGIN_ERROR', function() { 22 | it('should return original state with populated error', function() { 23 | let errorMessage = "Oh no! There is an error. Boo!"; 24 | let mutatedInitialState = initialState(); 25 | mutatedInitialState.error = errorMessage; 26 | 27 | let stateResult = loginReducer(initialState(), ACTION_LOGIN_ERROR(errorMessage)); 28 | 29 | expect(stateResult).to.deep.equal(mutatedInitialState); 30 | expect(stateResult.error).to.deep.equal(errorMessage); 31 | }); 32 | }); 33 | 34 | describe('(REDUX) LoginReducer (ACTION) LOGIN_REQUEST', function() { 35 | it('should return original state', function() { 36 | expect(loginReducer(initialState(), ACTION_LOGIN_REQUEST)).to.deep.equal(initialState()); 37 | }); 38 | }); 39 | 40 | const ACTION_EMPTY = {}; 41 | const ACTION_LOGIN_REQUEST = {"type": "LOGIN_REQUEST"}; 42 | const ACTION_LOGIN_RESPONSE = {"type": "LOGIN_RESPONSE"}; 43 | 44 | function ACTION_LOGIN_ERROR(message) { 45 | return { 46 | "type": "LOGIN_ERROR", 47 | "content": { 48 | "message": message 49 | } 50 | }; 51 | } 52 | 53 | function initialState() { 54 | return { 55 | error: '' 56 | }; 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile Link App 2 | 3 | This application handles the backend services for the mobile link app used for authentication within the WiFi spots. 4 | 5 | ## Authentication 6 | POST /signup 7 | JSON : 8 | ```json 9 | { 10 | "email":"id", 11 | "deviceId":"deviceId", 12 | "deviceOS":"deviceOS", 13 | "deviceModel":"model", 14 | "latitude":"lat", 15 | "longitude":"lon", 16 | "adID":"adid", 17 | "adIDType":"as" 18 | } 19 | ``` 20 | 21 | A new credential will be created and stored for each email, deviceId pair. 22 | DeviceId is not the real DeviceId, is an unique identifier for the device derived within the device itself. 23 | 24 | ``` 25 | { 26 | "username": "SSDSFGD", 27 | "password": "ASDwdw123ADFD" 28 | } 29 | ``` 30 | 31 | If those id and deviceId already exists, will return the original credentials (Idempotency) 32 | 33 | 34 | Otherwise: 35 | 404 Bad Request 36 | ``` 37 | { 38 | "reason": "some error from stacktrace" 39 | } 40 | ``` 41 | 42 | ## Get Links list 43 | URL: https://link-mobile-observations-dev.us-east-1.elasticbeanstalk.com/dwh 44 | returns: 45 | ``` 46 | [ 47 | { 48 | "id":"mn-09-120436", 49 | "latitude":"40.827117", 50 | "longitude":"-73.949738", 51 | "address":"3560 BROADWAY", 52 | "status":"Link Active!" 53 | }, 54 | { 55 | "id":"bx-05-119597", 56 | "latitude":"40.85187831", 57 | "longitude":"-73.90897566", 58 | "address":"1966 JEROME AVENUE", 59 | "status":"Link Active!" 60 | } 61 | ] 62 | ``` 63 | 64 | ## Healthcheck 65 | URL : /healthcheck 66 | Return 204 (NoContent) if it is a healthy status. 67 | 68 | ## Status 69 | URL : /status 70 | Checks the status of: database 71 | Returns 200 if everything is connectionAlive 72 | ``` 73 | { 74 | "name":"dynamoDB","connectionAlive":true,"message":"" 75 | } 76 | ``` 77 | 78 | Otherwise: 79 | Returns 400 (bad request) if everything is not connectionAlive 80 | ``` 81 | { 82 | "name":"dynamoDB","connectionAlive":false,"message":"some message" 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /app/actors/UserEventActor.scala: -------------------------------------------------------------------------------- 1 | package actors 2 | 3 | import javax.inject._ 4 | 5 | import akka.actor.Status.Status 6 | import akka.actor._ 7 | import akka.event.LoggingReceive 8 | import com.google.inject.assistedinject.Assisted 9 | import models.{Observation, ObservationHolder, Ping, WatchMessageEvents} 10 | import play.api.Configuration 11 | import play.api.libs.concurrent.InjectedActorSupport 12 | import play.api.libs.json.Json 13 | 14 | class UserEventActor @Inject()(@Assisted out: ActorRef, 15 | @Assisted uri: String, 16 | @Named("proxyActor") proxyActor: ActorRef, 17 | //@Named("dataSpammerActor") dataSpammerActor: ActorRef, 18 | configuration: Configuration) extends Actor with ActorLogging { 19 | 20 | 21 | override def preStart(): Unit = { 22 | super.preStart() 23 | configureDefaultEventSource() 24 | } 25 | 26 | def configureDefaultEventSource() = { 27 | proxyActor ! WatchMessageEvents(uri) 28 | //dataSpammerActor ! WatchEvents() 29 | } 30 | 31 | import models.implicits._ 32 | override def receive: Receive = LoggingReceive { 33 | // case MessageUpdate(message) => out ! message 34 | case _ : Status => //Akka sending status success. We need to handle receiving it, but don't need to do anything with it. 35 | case observation: ObservationHolder => out ! Json.toJson(observation) 36 | case observation: Observation => out ! Json.toJson(observation) 37 | case p: Ping => out ! Json.toJson(p) 38 | case default => log.error(s"Invalid message ${default.getClass}") 39 | } 40 | } 41 | 42 | class UserEventFactoryActor @Inject()(childFactory: UserEventActor.Factory) extends Actor with InjectedActorSupport with ActorLogging { 43 | import UserEventFactoryActor._ 44 | 45 | override def receive: Receive = LoggingReceive { 46 | case Create(id, out, uri) => 47 | val child: ActorRef = injectedChild(childFactory(out, uri), s"userActor-$id") //add in uri~? 48 | sender() ! child 49 | } 50 | } 51 | 52 | object UserEventFactoryActor { 53 | case class Create(id: String, out: ActorRef, uri: String) 54 | } 55 | 56 | object UserEventActor { 57 | trait Factory { 58 | // Corresponds to the @Assisted parameters defined in the constructor 59 | def apply(out: ActorRef, uri: String): Actor 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/models/model.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | 6 | import org.apache.commons.lang3.StringUtils 7 | import play.api.libs.functional.syntax.{unlift, _} 8 | import play.api.libs.json.{Format, JsObject, JsPath, Json} 9 | 10 | import scala.util.{Failure, Success, Try} 11 | 12 | object implicits { 13 | 14 | lazy implicit val typeIdFormat = Json.format[TypeId] 15 | lazy implicit val locationFormat = Json.format[Location] 16 | lazy implicit val observationFormat = Json.format[Observation] 17 | lazy implicit val observationWFormat = Json.format[ObservationWrapper] 18 | lazy implicit val observationBodyFormat = Json.format[ObservationHolder] 19 | lazy implicit val pingFormat = Json.format[Ping] 20 | lazy implicit val responseFormat = Json.format[Response] 21 | 22 | def parse(input: String) = { 23 | Json.parse(input).as[List[ObservationHolder]] 24 | } 25 | 26 | } 27 | 28 | case class TypeId(`type`: String, value: String) 29 | 30 | case class Location(lon: Double, lat: Double, horizontal_accuracy: Float) 31 | 32 | case class ObservationWrapper(provider_id: String, observation: Observation) 33 | 34 | case class Observation(ts: Long, id: TypeId, location: Location) { 35 | 36 | def str() = s"${id.`type`}-${id.value}" 37 | 38 | def t() = id.`type` 39 | 40 | def dist(p: Observation): Double = { 41 | (location.lon - p.location.lon) * (location.lon - p.location.lon) + 42 | (location.lat - p.location.lat) * (location.lat - p.location.lat) 43 | } 44 | } 45 | 46 | object Observation{ 47 | val sdf = new SimpleDateFormat("dd/MM/yy HH:mm:ss") 48 | def toDate(observation: Observation) = sdf.format(new Date(observation.ts * 1000)) 49 | } 50 | 51 | case class ObservationHolder(ts: Long, body: ObservationWrapper) 52 | 53 | case class Response(observations: List[Observation], means: List[(Observation, Int)], total: Long, idAds: Map[String, Int], idAdType: Map[String, Int] = Map()) 54 | 55 | object Response { 56 | def empty = Response(List(), List(), 0, Map()) 57 | 58 | def build(list: List[Observation], means: List[(Observation, Int)]) = { 59 | val l = Try{ 60 | list.sortWith(_.ts > _.ts ) 61 | } match { 62 | case Success(filtered) => filtered 63 | case Failure(_) => 64 | println(list.take(1)) 65 | list 66 | } 67 | 68 | Response(l.take(200), means, list.size, list.groupBy(_.id.value).mapValues(_.size), list.groupBy(_.id.`type`).mapValues(_.size)) 69 | } 70 | } -------------------------------------------------------------------------------- /app/controllers/IndexController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import cats.data.OptionT 4 | import cats.instances.future._ 5 | //import cats.implicits._ 6 | 7 | import javax.inject.Inject 8 | import models.{Observation, Response} 9 | import models.implicits._ 10 | import play.api.Logger 11 | import play.api.cache.AsyncCacheApi 12 | import play.api.libs.json.Json 13 | import play.api.mvc.{InjectedController, Result} 14 | 15 | import scala.concurrent.{ExecutionContext, Future} 16 | 17 | 18 | class IndexController @Inject()(cache: AsyncCacheApi)(implicit ec: ExecutionContext) extends InjectedController { 19 | 20 | val observations = "observations" 21 | val logger = Logger(getClass) 22 | 23 | def index = Action.async { _ => 24 | Future { 25 | Ok(views.html.index()) 26 | } 27 | 28 | } 29 | 30 | 31 | def data(): OptionT[Future, Result] = { 32 | for { 33 | obs2 <- OptionT(cache.get[List[Observation]](observations)) 34 | means2 <- OptionT(cache.get[List[(Observation, Int)]]("means")) 35 | res <- OptionT.some(Ok(Json.toJson(Response.build(obs2, means2)))) 36 | } yield { 37 | res 38 | } 39 | } 40 | 41 | 42 | def json = Action.async { 43 | data().getOrElseF(Future { 44 | Ok("no data") 45 | }) 46 | } 47 | 48 | 49 | def list = Action.async { 50 | val result: OptionT[Future, Result] = for { 51 | obs2 <- OptionT(cache.get[List[Observation]](observations)) 52 | means2 <- OptionT(cache.get[List[(Observation, Int)]]("means")) 53 | res <- OptionT.some(Response.build(obs2, means2)) 54 | } yield Ok(views.html.list(res)) 55 | 56 | 57 | result.getOrElse(Ok("No data")) 58 | } 59 | 60 | def filter(id: String) = Action.async { 61 | val result = for { 62 | obs2 <- OptionT(cache.get[List[Observation]](observations)) 63 | means2 <- OptionT(cache.get[List[(Observation, Int)]]("means")) 64 | res <- OptionT.some(Response.build(obs2.filter(_.id.value.contains(id)), means2)) 65 | } yield Ok(views.html.list(res)) 66 | 67 | result.getOrElse(Ok("No data")) 68 | } 69 | 70 | def filterByType(typeId: String) = Action.async { 71 | val result = for { 72 | obs2 <- OptionT(cache.get[List[Observation]](observations)) 73 | means2 <- OptionT(cache.get[List[(Observation, Int)]]("means")) 74 | res <- OptionT.some(Response.build(obs2.filter(_.id.`type`.contains(typeId)), means2)) 75 | } yield Ok(views.html.list(res)) 76 | 77 | result.getOrElse(Ok("No data")) 78 | } 79 | 80 | 81 | } 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /ui/app/src/scripts/util/Equal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript deep equals for objects, found at: https://stamat.wordpress.com/2013/06/22/javascript-object-comparison/. 3 | */ 4 | 5 | //Returns the object's class, Array, Date, RegExp, Object are of interest to us 6 | var getClass = function(val) { 7 | return Object.prototype.toString.call(val) 8 | .match(/^\[object\s(.*)\]$/)[1]; 9 | }; 10 | 11 | //Defines the type of the value, extended typeof 12 | var whatis = function(val) { 13 | 14 | if (val === undefined) 15 | return 'undefined'; 16 | if (val === null) 17 | return 'null'; 18 | 19 | var type = typeof val; 20 | 21 | if (type === 'object') 22 | type = getClass(val).toLowerCase(); 23 | 24 | if (type === 'number') { 25 | if (val.toString().indexOf('.') > 0) 26 | return 'float'; 27 | else 28 | return 'integer'; 29 | } 30 | 31 | return type; 32 | }; 33 | 34 | var compareObjects = function(a, b) { 35 | if (a === b) 36 | return true; 37 | for (var i in a) { 38 | if (b.hasOwnProperty(i)) { 39 | if (!equal(a[i],b[i])) return false; 40 | } else { 41 | return false; 42 | } 43 | } 44 | 45 | for (var i in b) { 46 | if (!a.hasOwnProperty(i)) { 47 | return false; 48 | } 49 | } 50 | return true; 51 | }; 52 | 53 | var compareArrays = function(a, b) { 54 | if (a === b) 55 | return true; 56 | if (a.length !== b.length) 57 | return false; 58 | for (var i = 0; i < a.length; i++){ 59 | if(!equal(a[i], b[i])) return false; 60 | }; 61 | return true; 62 | }; 63 | 64 | var _equal = {}; 65 | _equal.array = compareArrays; 66 | _equal.object = compareObjects; 67 | _equal.date = function(a, b) { 68 | return a.getTime() === b.getTime(); 69 | }; 70 | _equal.regexp = function(a, b) { 71 | return a.toString() === b.toString(); 72 | }; 73 | // uncoment to support function as string compare 74 | // _equal.fucntion = _equal.regexp; 75 | 76 | 77 | 78 | /* 79 | * Are two values equal, deep compare for objects and arrays. 80 | * @param a {any} 81 | * @param b {any} 82 | * @return {boolean} Are equal? 83 | */ 84 | export function equal(a, b) { 85 | if (a !== b) { 86 | var atype = whatis(a), btype = whatis(b); 87 | 88 | if (atype === btype) 89 | return _equal.hasOwnProperty(atype) ? _equal[atype](a, b) : a==b; 90 | 91 | return false; 92 | } 93 | 94 | return true; 95 | } 96 | -------------------------------------------------------------------------------- /ui/test/reducers/JoyrideReducerTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import joyrideReducer from '../../app/src/scripts/reducers/joyrideReducer' 3 | 4 | import * as JoyrideMappings from "../../app/src/resources/mappings/joyride"; 5 | import joyrideStep from "../resources/JoyrideStep.json"; 6 | 7 | describe('(REDUX) JoyrideReducer', function() { 8 | it('should return state with no modifications if action is not recognised', function () { 9 | expect(joyrideReducer(initialState(), ACTION_EMPTY)).to.deep.equal(initialState()); 10 | }); 11 | 12 | it('should return initialState if state is undefined', function () { 13 | expect(joyrideReducer(undefined, ACTION_EMPTY)).to.deep.equal(initialState()); 14 | }); 15 | }); 16 | 17 | describe('(REDUX) JoyrideReducer (ACTION) SET_STEP_INDEX', function() { 18 | it('should return state with modified stepIndex', function() { 19 | let stepIndex = 10052017; 20 | 21 | let mutatedInitialState = initialState(); 22 | mutatedInitialState.stepIndex = stepIndex; 23 | 24 | expect(joyrideReducer(initialState(), ACTION_SET_STEP_INDEX(stepIndex))).to.deep.equals(mutatedInitialState) 25 | }); 26 | }); 27 | 28 | describe('(REDUX) JoyrideReducer (ACTION) ADD_STEP', function() { 29 | it('should return state with a joyride step', function() { 30 | let mutatedInitialState = initialState(); 31 | mutatedInitialState.steps = [joyrideStep]; 32 | 33 | expect(joyrideReducer(initialState(), ACTION_ADD_STEP).steps).to.have.lengthOf(1); 34 | expect(joyrideReducer(initialState(), ACTION_ADD_STEP)).to.deep.equals(mutatedInitialState); 35 | }); 36 | }); 37 | 38 | describe('(REDUX) JoyrideReducer (ACTION) CLEAR_STEPS', function() { 39 | it('should populate state with a joyride step and then remove it', function() { 40 | let mutatedState = joyrideReducer(initialState(), ACTION_ADD_STEP); 41 | expect(mutatedState.steps).to.have.lengthOf(1); 42 | 43 | mutatedState = joyrideReducer(mutatedState, ACTION_CLEAR_STEP); 44 | expect(mutatedState.steps).to.have.lengthOf(0); 45 | }); 46 | }); 47 | 48 | const ACTION_EMPTY = { }; 49 | 50 | const ACTION_CLEAR_STEP = { "type": "CLEAR_STEPS" } 51 | 52 | const ACTION_ADD_STEP = { 53 | "type": "ADD_STEP", 54 | "content": { 55 | "step": joyrideStep 56 | } 57 | }; 58 | 59 | function ACTION_SET_STEP_INDEX(stepIndex){ 60 | return { 61 | "type": "SET_STEP_INDEX", 62 | "content": { 63 | "stepIndex": stepIndex 64 | } 65 | }; 66 | } 67 | 68 | function initialState() { 69 | return { 70 | steps: [], 71 | stepIndex: 0, 72 | stepMapping: JoyrideMappings 73 | } 74 | } -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/SubPortDashJRMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "sub-port-dash-req-record-received-circle": { 3 | "data": { 4 | "id": "SPD-001", 5 | "title": "REQ Record Summary Wheel", 6 | "text": "The summary wheel indicates the number of sub porting MSISDNs that have received REQ Records indicating they are porting today.", 7 | "selector": "#sub-port-dash-req-record-received-circle", 8 | "position": "right", 9 | "allowClicksThruHole": true, 10 | "style": { 11 | "mainColor": "#0d47a1", 12 | "beacon": { 13 | "inner": "#0d47a1", 14 | "outer": "#1565c0" 15 | } 16 | } 17 | } 18 | }, 19 | "sub-port-dash-routing-updated-circle": { 20 | "data": { 21 | "id": "SPD-002", 22 | "title": "Routing Updated Summary Wheel", 23 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have been updated on the switch. Any MSISDNs not in the Routing Updated state should be investigated as the necessary update may not have been successfully made to the switch.", 24 | "selector": "#sub-port-dash-routing-updated-circle", 25 | "position": "right", 26 | "allowClicksThruHole": true, 27 | "style": { 28 | "mainColor": "#0d47a1", 29 | "beacon": { 30 | "inner": "#0d47a1", 31 | "outer": "#1565c0" 32 | } 33 | } 34 | } 35 | }, 36 | "sub-port-dash-file-processed-circle": { 37 | "data": { 38 | "id": "SPD-003", 39 | "title": "File Summary Wheel", 40 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have completed all file based activities (generated and/or received). Please note that this information pertains to files being prepared for sending as opposed to being actually sent.", 41 | "selector": "#sub-port-dash-file-processed-circle", 42 | "position": "right", 43 | "allowClicksThruHole": true, 44 | "style": { 45 | "mainColor": "#0d47a1", 46 | "beacon": { 47 | "inner": "#0d47a1", 48 | "outer": "#1565c0" 49 | } 50 | } 51 | } 52 | }, 53 | "sub-port-dash-completed-circle": { 54 | "data": { 55 | "id": "SPD-004", 56 | "title": "Completed Summary Wheel", 57 | "text": "The summary wheel indicates that all activities have been completed for the Sub Port order.", 58 | "selector": "#sub-port-dash-completed-circle", 59 | "position": "right", 60 | "allowClicksThruHole": true, 61 | "style": { 62 | "mainColor": "#0d47a1", 63 | "beacon": { 64 | "inner": "#0d47a1", 65 | "outer": "#1565c0" 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/views/list.scala.html: -------------------------------------------------------------------------------- 1 | 2 | @( response: Response) 3 | 4 | @import models.Observation 5 | @import models.Response 6 | 7 | 8 | 9 | 10 | 11 | 12 | Paddington 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | @for(p <- response.means) { 25 | 26 | 27 | 28 | 29 | 30 | } 31 | 32 |
    Home
    Kmeans @p._1.location.lat , @p._1.location.lon #@p._2
    33 | 34 | 35 | @for(p <- response.idAds) { 36 | 37 | 38 | 41 | 42 | 43 | } 44 | 45 | 46 |
    ids 39 | @p._1 -> @p._2 40 |
    47 | 48 | 49 | @for(p <- response.idAdType) { 50 | 51 | 52 | 55 | 56 | } 57 | 58 | 59 |
    idType 53 | @p._1 -> @p._2 54 |
    60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | @for(p <- response.observations) { 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | } 83 | 84 | 85 |
    ID ID type TSlonglat...
    @p.id.value@p.t@Observation.toDate(p)@p.location.lon@p.location.lat
    86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/PortOutDashJRMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "port-out-dash-secured-circle": { 3 | "data": { 4 | "id": "POD-001", 5 | "title": "Secured Summary Wheel", 6 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have been confirmed by Syniverse to port out today. Any MSISDNs not in the Secured state should be investigated as it could mean that a MSISDN is no longer due to port out today as expected.", 7 | "selector": "#port-out-dash-secured-circle", 8 | "position": "right", 9 | "allowClicksThruHole": true, 10 | "style": { 11 | "mainColor": "#0d47a1", 12 | "beacon": { 13 | "inner": "#0d47a1", 14 | "outer": "#1565c0" 15 | } 16 | } 17 | } 18 | }, 19 | "port-out-dash-switch-updated-circle": { 20 | "data": { 21 | "id": "POD-002", 22 | "title": "Updated Summary Wheel", 23 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have been updated on the switch. Any MSISDNs not in the Updated state should be investigated as the necessary update may not have been successfully made to the switch.", 24 | "selector": "#port-out-dash-switch-updated-circle", 25 | "position": "right", 26 | "allowClicksThruHole": true, 27 | "style": { 28 | "mainColor": "#0d47a1", 29 | "beacon": { 30 | "inner": "#0d47a1", 31 | "outer": "#1565c0" 32 | } 33 | } 34 | } 35 | }, 36 | "port-out-dash-file-processed-circle": { 37 | "data": { 38 | "id": "POD-003", 39 | "title": "File Summary Wheel", 40 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have completed all file based activities (generated and/or received). Please note that this information pertains to files being prepared for sending as opposed to being actually sent.", 41 | "selector": "#port-out-dash-file-processed-circle", 42 | "position": "left", 43 | "allowClicksThruHole": true, 44 | "style": { 45 | "mainColor": "#0d47a1", 46 | "beacon": { 47 | "inner": "#0d47a1", 48 | "outer": "#1565c0" 49 | } 50 | } 51 | } 52 | }, 53 | "port-out-dash-completed-circle": { 54 | "data": { 55 | "id": "POD-004", 56 | "title": "Completed Summary Wheel", 57 | "text": "The summary wheel indicates that all activities have been completed for the Port Out order. The Customer will no longer have an active service on the network.", 58 | "selector": "#port-out-dash-completed-circle", 59 | "position": "left", 60 | "allowClicksThruHole": true, 61 | "style": { 62 | "mainColor": "#0d47a1", 63 | "beacon": { 64 | "inner": "#0d47a1", 65 | "outer": "#1565c0" 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /ui/test/resources/PortInOrders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "entityId": { 4 | "pac": "XDB118290", 5 | "portingMsisdn": "447515422375", 6 | "portDate": "2017-05-24" 7 | }, 8 | "eventCount": 2, 9 | "status": "SECURED", 10 | "lastEventAt": "2016-11-03T14:54:23.809Z[Europe/London]", 11 | "failures": [], 12 | "expectedPortDate": "2017-05-24", 13 | "operatorId": "sky", 14 | "operatorOrderId": "352647", 15 | "orderCreatedAt": "2016-11-03T14:54:23.809Z[Europe/London]", 16 | "portInSecuredDetails": { 17 | "portInMsisdnListRetrievedEventExternalId": 1, 18 | "pac": "XDB118290", 19 | "portingMsisdn": "447515422375", 20 | "dspSyniverseCode": "XDB", 21 | "dnoSyniverseCode": "NN", 22 | "rspSyniverseCode": "XDB", 23 | "rnoSyniverseCode": "SS", 24 | "portDate": "2017-05-24", 25 | "securedAt": "2016-11-03T14:54:23.809Z[Europe/London]", 26 | "isAutolocked": true 27 | }, 28 | "portInRspRecordPreparedDetails": {}, 29 | "requestId": "7894231-413-4123", 30 | "portInRspRecordReceivedDetails": {}, 31 | "portInReqRecordReceivedDetails": {}, 32 | "serviceId": "3075", 33 | "serviceState": "ACTIVE", 34 | "skyMsisdn": "447488222042" 35 | }, 36 | { 37 | "entityId": { 38 | "pac": "XDB118289", 39 | "portingMsisdn": "447515422375", 40 | "portDate": "2017-05-24" 41 | }, 42 | "eventCount": 1, 43 | "status": "SECURED", 44 | "lastEventAt": "2016-11-03T14:54:23.809Z[Europe/London]", 45 | "failures": [], 46 | "portInSecuredDetails": { 47 | "portInMsisdnListRetrievedEventExternalId": 1, 48 | "pac": "XDB118289", 49 | "portingMsisdn": "447515422375", 50 | "dspSyniverseCode": "XDB", 51 | "dnoSyniverseCode": "NN", 52 | "rspSyniverseCode": "XDB", 53 | "rnoSyniverseCode": "SS", 54 | "portDate": "2017-05-24", 55 | "securedAt": "2016-11-03T14:54:23.809Z[Europe/London]", 56 | "isAutolocked": true 57 | }, 58 | "portInRspRecordPreparedDetails": {}, 59 | "portInRspRecordReceivedDetails": {}, 60 | "portInReqRecordReceivedDetails": {} 61 | }, 62 | { 63 | "entityId": { 64 | "pac": "XDB118291", 65 | "portingMsisdn": "447515422375", 66 | "portDate": "2017-05-24" 67 | }, 68 | "eventCount": 1, 69 | "status": "ACCEPTED", 70 | "lastEventAt": "2016-11-03T14:54:23.809Z[Europe/London]", 71 | "failures": [], 72 | "expectedPortDate": "2017-05-24", 73 | "operatorId": "sky", 74 | "operatorOrderId": "352647", 75 | "orderCreatedAt": "2016-11-03T14:54:23.809Z[Europe/London]", 76 | "portInSecuredDetails": {}, 77 | "portInRspRecordPreparedDetails": {}, 78 | "requestId": "7894231-413-4123", 79 | "portInRspRecordReceivedDetails": {}, 80 | "portInReqRecordReceivedDetails": {}, 81 | "serviceId": "3075", 82 | "serviceState": "ACTIVE", 83 | "skyMsisdn": "447488222042" 84 | } 85 | ] -------------------------------------------------------------------------------- /app/actors/ProxyActor.scala: -------------------------------------------------------------------------------- 1 | package actors 2 | 3 | import javax.inject.Inject 4 | import akka.actor.{Actor, ActorLogging, ActorRef} 5 | import akka.event.LoggingReceive 6 | import com.google.inject.Singleton 7 | import models._ 8 | import play.api.Logger 9 | import play.api.cache.AsyncCacheApi 10 | 11 | import scala.collection.immutable.HashSet 12 | import scala.concurrent.{ExecutionContext, Future} 13 | import scala.concurrent.duration._ 14 | 15 | @Singleton 16 | class ProxyActor @Inject()(cache: AsyncCacheApi)(implicit ec: ExecutionContext) extends Actor with ActorLogging { 17 | 18 | protected[this] var watchers: Map[String, HashSet[ActorRef]] = Map[String, HashSet[ActorRef]]() 19 | 20 | val observations = "observations" 21 | val logger = Logger(getClass) 22 | 23 | def twoDaysAgo() = (System.currentTimeMillis() - 2.days.toMillis)/1000 24 | 25 | def receive = LoggingReceive { 26 | 27 | case Ping(_) => 28 | watchers.foreach{ w => 29 | w._2.foreach{ actor => 30 | actor ! Ping(1) 31 | } 32 | } 33 | case UpdateMessageList(obs) => 34 | 35 | cache.get[List[Observation]](observations).flatMap{ 36 | case None => 37 | log.info(s"Adding observations from scratch: ${obs.size}") 38 | cache.set(observations, obs) 39 | case Some(list) => 40 | log.info(s"Adding observation: ${obs.size}") 41 | cache.set(observations, (obs ::: list).filter(_.ts > (twoDaysAgo()))) 42 | } 43 | 44 | case UpdateMessage(observation) => 45 | 46 | cache.get[List[Observation]](observations).flatMap{ 47 | case None => 48 | log.info(s"Adding observation: ${observation}") 49 | cache.set(observations,List[Observation](observation)) 50 | case Some(list) => 51 | log.info(s"Adding observation: ${observation}") 52 | cache.set(observations, observation :: list) 53 | } 54 | 55 | 56 | 57 | val key = s"${observation.ts}-${observation.id.value}" 58 | cache.set(key, observation) 59 | 60 | watchers.foreach{ 61 | w => println(s"UpdateMessage ${observation.ts}...") 62 | w._2.foreach{ actor => 63 | log.info(s"Actor ${actor}") 64 | actor ! observation 65 | } 66 | } 67 | 68 | 69 | 70 | case WatchMessageEvents(eventType) => 71 | // send the event history to the user 72 | // sendMessage(Observation(2210, TypeId("1", "UUID"), Location(40, 75, 10)), sender) 73 | 74 | // add the watcher to the list 75 | val hashmap = watchers.getOrElse(eventType, HashSet.empty) 76 | watchers = watchers + (eventType -> (hashmap + sender)) 77 | log.info(s"Adding new user to watches ${watchers.size} ") 78 | 79 | case UnwatchMessageEvents() => 80 | watchers.foreach { case (_, v) => v - sender } 81 | case _ => 82 | } 83 | 84 | 85 | def sendMessage(observation: Observation, v: ActorRef) = { 86 | v ! observation 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/PortInExpectedMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Port In List", 3 | "name": "PortInExpectedMapping", 4 | "filterTypes": ["outstanding", "completed"], 5 | "actions": ["export"], 6 | "mappings": [ 7 | { 8 | "displayName": "Created Date", 9 | "path": "$.orderCreatedAt", 10 | "type" : "DATE", 11 | "downloadable": true, 12 | "columnWidth": 120, 13 | "flexGrow": 1 14 | }, 15 | { 16 | "displayName": "Port Date", 17 | "path": "$.entityId.portDate", 18 | "type" : "DATE", 19 | "searchable": true, 20 | "downloadable": true, 21 | "columnWidth": 120, 22 | "flexGrow": 1 23 | }, 24 | { 25 | "displayName": "PAC", 26 | "path": "$.entityId.pac", 27 | "searchable": true, 28 | "downloadable": true, 29 | "columnWidth": 120, 30 | "flexGrow": 1 31 | }, 32 | { 33 | "displayName": "Porting MSISDN", 34 | "path": "$.entityId.portingMsisdn", 35 | "searchable": true, 36 | "downloadable": true, 37 | "columnWidth": 120, 38 | "flexGrow": 1 39 | }, 40 | { 41 | "displayName": "Sky Allocated MSISDN", 42 | "path": "$.skyMsisdn", 43 | "searchable": true, 44 | "downloadable": true, 45 | "columnWidth": 120, 46 | "flexGrow": 1 47 | }, 48 | { 49 | "displayName": "Service ID", 50 | "path": "$.serviceId", 51 | "searchable": true, 52 | "downloadable": true, 53 | "columnWidth": 120, 54 | "flexGrow": 1 55 | }, 56 | { 57 | "displayName": "Operator ID", 58 | "path": "$.operatorId", 59 | "searchable": true, 60 | "downloadable": true, 61 | "columnWidth": 120, 62 | "flexGrow": 1 63 | }, 64 | { 65 | "displayName": "DSP", 66 | "path": "$.portInSecuredDetails.dspSyniverseCode", 67 | "searchable": true, 68 | "downloadable": true, 69 | "columnWidth": 60 70 | }, 71 | { 72 | "displayName": "DNO", 73 | "path": "$.portInSecuredDetails.dnoSyniverseCode", 74 | "searchable": true, 75 | "downloadable": true, 76 | "columnWidth": 60 77 | }, 78 | { 79 | "displayName": "Expected", 80 | "type": "CALCULATED", 81 | "downloadable": true, 82 | "path": "", 83 | "columnWidth": 60 84 | }, 85 | { 86 | "displayName": "Confirmed", 87 | "type": "CALCULATED", 88 | "downloadable": true, 89 | "path": "", 90 | "columnWidth": 60 91 | }, 92 | { 93 | "displayName": "Error", 94 | "type": "CALCULATED", 95 | "downloadable": true, 96 | "path": "", 97 | "columnWidth": 60 98 | }, 99 | { 100 | "displayName": "Status", 101 | "path": "$.status", 102 | "searchable": true, 103 | "downloadable": true, 104 | "columnWidth": 180, 105 | "flexGrow": 1 106 | } 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /ui/test/util/EqualTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { equal, compareArrays } from '../../app/src/scripts/util/Equal'; 3 | 4 | describe("(FUNCTION) equals", function() { 5 | it("should match json objects identical values", function() { 6 | let jsonObj = {"testField1":"value1"}; 7 | 8 | expect(equal(jsonObj,jsonObj)).to.equal(true) 9 | }); 10 | 11 | it("should not match json objects with different fields", function() { 12 | let jsonObj1 = {"testField2":"value1"}; 13 | let jsonObj2 = {"testField1":"VALUE2"}; 14 | 15 | expect(equal(jsonObj1,jsonObj2)).to.equal(false) 16 | }); 17 | 18 | it("should not match json objects with different amounts of fields ", function() { 19 | let jsonObj1 = {"testField1":"value1"}; 20 | let jsonObj2 = {"testField1":"value1", "testField2":"value1"}; 21 | 22 | expect(equal(jsonObj1,jsonObj2)).to.equal(false) 23 | }); 24 | 25 | it("should not match two arrays with different types of data", function() { 26 | let arrayValue1 = [1,2,3,4,5]; 27 | let arrayValue2 = ["1","2","3","4","5"]; 28 | 29 | expect(equal(arrayValue1, arrayValue2)).to.equal(false) 30 | }); 31 | 32 | it("should not match two arrays with different lengths", function() { 33 | let arrayValue1 = ["1","2","3","4"]; 34 | let arrayValue2 = ["1","2","3","4","5"]; 35 | 36 | expect(equal(arrayValue1, arrayValue2)).to.equal(false) 37 | }); 38 | 39 | it("should not match undefined and null value", function() { 40 | let undefinedValue; 41 | let nullValue = null; 42 | 43 | expect(equal(undefinedValue,nullValue)).to.equal(false) 44 | }); 45 | 46 | it("should not match float and integer", function() { 47 | let floatValue = 10.51; 48 | let integerValue = 3; 49 | 50 | expect(equal(floatValue,integerValue)).to.equal(false) 51 | }); 52 | 53 | it("should match identical objects", function() { 54 | let object = { 55 | "0" : "apple", 56 | "1" : "pear", 57 | "2" : "orange" 58 | }; 59 | 60 | expect(equal(object,object)).to.equal(true) 61 | }); 62 | 63 | it("should not match objects with different content", function() { 64 | let object1 = { 65 | "0" : "apple", 66 | "1" : "pear", 67 | "2" : "orange" 68 | }; 69 | 70 | let object2 = { 71 | "a" : "apple", 72 | "b" : "pear", 73 | "c" : "orange" 74 | }; 75 | 76 | expect(equal(object1,object2)).to.equal(false) 77 | }); 78 | 79 | it("should not match different date values", function() { 80 | let dateValue1 = new Date("May 17, 2017 11:13:00"); 81 | let dateValue2 = new Date("December 25, 1999 11:13:00"); 82 | 83 | expect(equal(dateValue1, dateValue2)).to.equal(false) 84 | }); 85 | 86 | it("should not match different regex expresssions", function() { 87 | let regexValue1 = /[1-9]/i; 88 | let regexValue2 = /[A-Z]/i; 89 | 90 | expect(equal(regexValue1, regexValue2)).to.equal(false) 91 | }); 92 | }); -------------------------------------------------------------------------------- /ui/test/reducers/ControlReducerTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import controlReducer from '../../app/src/scripts/reducers/controlReducer' 3 | import * as actionType from '../../app/src/scripts/actions/ControlAction' 4 | 5 | describe('(REDUX) controlReducer', function() { 6 | it('should return state with no modifications if action is not recognised', function () { 7 | expect(controlReducer(initialState(), ACTION_EMPTY)).to.deep.equal(initialState()); 8 | }); 9 | 10 | it('should return initialState if state is undefined', function () { 11 | expect(controlReducer(undefined, ACTION_EMPTY)).to.deep.equal(initialState()); 12 | }); 13 | }); 14 | 15 | describe('(REDUX) controlReducer (ACTION) dataReducer', function() { 16 | 17 | /* 18 | add a test to show coping of multiple toggles simultaneously ? 19 | */ 20 | 21 | it('should return initialState if toggle criteria is not recognised ', function() { 22 | expect(controlReducer(initialState(), ACTION_TOGGLE_FILTER_CRITERIA("BLAH%$!£!£$"))).to.deep.equal(initialState()); 23 | }); 24 | 25 | it('should return initialState with shouldShowCompleted toggled (false to true)', function() { 26 | let mutatedInitialState = initialState(); 27 | mutatedInitialState.shouldShowCompleted = !mutatedInitialState.shouldShowCompleted 28 | 29 | expect(controlReducer(initialState(), ACTION_TOGGLE_FILTER_CRITERIA(actionType.COMPLETED))).to.deep.equal(mutatedInitialState); 30 | }); 31 | 32 | it('should return initialState with shouldShowOutstanding toggled (true to false)', function() { 33 | let mutatedInitialState = initialState(); 34 | mutatedInitialState.shouldShowOutstanding = !mutatedInitialState.shouldShowOutstanding 35 | 36 | expect(controlReducer(initialState(), ACTION_TOGGLE_FILTER_CRITERIA(actionType.OUTSTANDING))).to.deep.equal(mutatedInitialState); 37 | }); 38 | }); 39 | 40 | describe('(REDUX) controlReducer (ACTION) togglePageType', function() { 41 | it('should return initialState with mutated PageType equalling PortIn ', function() { 42 | let mutatedInitialState = initialState(); 43 | mutatedInitialState.pageType = "PortIn"; 44 | 45 | expect(controlReducer(initialState(), ACTION_SET_PAGE_TYPE("PortIn"))).to.deep.equal(mutatedInitialState); 46 | }); 47 | }); 48 | 49 | describe('(REDUX) controlReducer (ACTION) setSearchText', function() { 50 | it('should return state with updated Search Text equalling Mango ', function() { 51 | let mutatedInitialState = initialState(); 52 | mutatedInitialState.searchText = "Mango"; 53 | 54 | expect(controlReducer(initialState(), actionType.setSearchText("Mango"))).to.deep.equal(mutatedInitialState); 55 | }); 56 | }); 57 | 58 | const ACTION_EMPTY = {}; 59 | 60 | function ACTION_SET_PAGE_TYPE(pageType) { 61 | return { 62 | "type": "SET_PAGE_TYPE", 63 | "content": { 64 | "pageType": pageType 65 | } 66 | } 67 | } 68 | 69 | function ACTION_TOGGLE_FILTER_CRITERIA(criteria) { 70 | return { 71 | "type": "TOGGLE_FILTER_CRITERIA", 72 | "content":{ 73 | "criteria": criteria 74 | } 75 | } 76 | } 77 | 78 | function initialState() { 79 | return { 80 | pageType: '', 81 | searchText: "", 82 | shouldShowCompleted: false, 83 | shouldShowOutstanding: true 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ui/app/src/scripts/components/DataTable.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import $ from 'jquery'; 4 | window.jQuery = window.$ = $; 5 | /*For some mad reason, this has to be require instead of 6 | import here and in Index, otherwise things break.*/ 7 | require("materialize-css"); 8 | import React from "react"; 9 | import {connect} from 'react-redux'; 10 | import * as Actions from '../actions'; 11 | import {callFunctionWithParamIfDefined} from '../util/DefinedPathUtils'; 12 | import {orderSelector} from '../util/FilterUtils'; 13 | import {mapCell} from '../util/RowMappings'; 14 | import Measure from 'react-measure'; 15 | 16 | const {Table, Column, Cell} = require('fixed-data-table-2'); 17 | let mapping; 18 | 19 | class DataTable extends React.Component { 20 | 21 | constructor(props) { 22 | super(props); 23 | mapping = this.props.mappings[this.props.type]; 24 | 25 | this.state = { 26 | tableRecords: [], 27 | tableDimensions: { 28 | width: -1, 29 | height: -1 30 | } 31 | }; 32 | 33 | this.retrieveOrderFromRow = this.retrieveOrderFromRow.bind(this); 34 | } 35 | 36 | componentDidMount() { 37 | callFunctionWithParamIfDefined(this.props.joyride, "stepMapping." + this.props.id + "-export-button.data", this.props.addStep); 38 | } 39 | 40 | retrieveOrderFromRow(orderId) { 41 | this.props.orders.forEach(row => { 42 | if (equal(row.ts, orderId)) { 43 | return row; 44 | } 45 | }); 46 | 47 | return null; 48 | } 49 | 50 | createColumns(mappingType, localMapping, orders) { 51 | return localMapping.mappings.map(col => 52 | {col.displayName}} 55 | cell={data => ( 56 | mapCell(this, orders[data.rowIndex], col, data, data.rowIndex, localMapping.filterTypes, mappingType) 57 | )} 58 | fixed={col.fixed} 59 | flexGrow={ col.flexGrow ? col.flexGrow : 0 } 60 | width={col.columnWidth} 61 | /> 62 | ); 63 | }; 64 | 65 | render() { 66 | return ( 67 | this.setState({tableDimensions: dimensions}) } 69 | whitelist={['height', 'width']}> 70 |
    71 | 78 | 79 | {this.createColumns(this.props.type, mapping, this.props.orders)} 80 |
    81 |
    82 |
    83 | ); 84 | } 85 | } 86 | 87 | function mapStateToProps(state) { 88 | return { 89 | mappings: state.data.get("mappings").toJS(), 90 | orders: orderSelector(state, state.control.searchText), 91 | joyride: state.joyride 92 | } 93 | } 94 | 95 | export default connect(mapStateToProps, Actions)(DataTable); -------------------------------------------------------------------------------- /ui/app/src/scripts/components/ModalDataTable.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import $ from 'jquery'; 4 | window.jQuery = window.$ = $; 5 | /*For some mad reason, this has to be require instead of 6 | import here and in Index, otherwise things break.*/ 7 | require("materialize-css"); 8 | import React from "react"; 9 | import {connect} from 'react-redux'; 10 | import * as Actions from '../actions'; 11 | import {callFunctionWithParamIfDefined} from '../util/DefinedPathUtils'; 12 | import {orderRecordsSelector} from '../util/FilterUtils'; 13 | import {mapCell} from '../util/RowMappings'; 14 | import Measure from 'react-measure'; 15 | 16 | const {Table, Column, Cell} = require('fixed-data-table-2'); 17 | let mapping = null; 18 | let data = null; 19 | 20 | class ModalDataTable extends React.Component { 21 | 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | tableRecords: [], 26 | tableDimensions: { 27 | width: -1, 28 | height: -1 29 | } 30 | }; 31 | 32 | this.retrieveOrderFromRow = this.retrieveOrderFromRow.bind(this); 33 | } 34 | 35 | componentDidMount() { 36 | callFunctionWithParamIfDefined(this.props.joyride, "stepMapping." + this.props.id + "-table", this.props.addStep); 37 | } 38 | 39 | retrieveOrderFromRow(orderId) { 40 | this.props.orders.forEach(row => { 41 | if (equal(row.entityId, orderId)) { 42 | return row; 43 | } 44 | }); 45 | 46 | return null; 47 | } 48 | 49 | createColumns(mappingType, localMapping, orders) { 50 | let a = localMapping.mappings.map(col => 51 | {col.displayName}} 54 | cell={ 55 | data => (mapCell(this, orders[data.rowIndex], col, data, data.rowIndex, localMapping.filterTypes, mappingType)) 56 | } 57 | fixed={col.fixed} 58 | flexGrow={ col.flexGrow ? col.flexGrow : 0 } 59 | width={col.columnWidth} 60 | />); 61 | return a; 62 | }; 63 | 64 | render() { 65 | mapping = this.props.mappings[this.props.type]; 66 | 67 | return ( 68 | { 70 | this.setState({tableDimensions: dimensions}) 71 | } } 72 | whitelist={['height', 'width']}> 73 |
    74 | 81 | 82 | {this.createColumns(this.props.type, mapping, this.props.orders)} 83 |
    84 |
    85 |
    86 | ); 87 | } 88 | } 89 | 90 | function mapStateToProps(state, ownProps) { 91 | return { 92 | mappings: state.data.get("mappings").toJS(), 93 | orders: orderRecordsSelector(state, ownProps.modalType, ownProps.orderId, ownProps.type, ownProps.searchText), 94 | joyride: state.joyride 95 | } 96 | } 97 | 98 | export default connect(mapStateToProps, Actions)(ModalDataTable); -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # https://www.playframework.com/documentation/latest/ConfigFile 3 | play.modules.enabled += "modules.AwsClientsModule" 4 | play.modules.enabled += "modules.KinesisModule" 5 | play.modules.enabled += "play.modules.swagger.SwaggerModule" 6 | play.modules.disabled += "play.core.ObjectMapperModule" 7 | play.filters.enabled += "play.filters.cors.CORSFilter" 8 | 9 | play.http.secret.key = "QCY?tAnfk?aZ?iwrNwnxIlR6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n" 10 | 11 | play.filters { 12 | ## CORS filter configuration 13 | # https://www.playframework.com/documentation/latest/CorsFilter 14 | # ~~~~~ 15 | # CORS is a protocol that allows web applications to make requests from the browser 16 | # across different domains. 17 | # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has 18 | # dependencies on CORS settings. 19 | cors { 20 | # Filter paths by a whitelist of path prefixes 21 | #pathPrefixes = ["/some/path", ...] 22 | 23 | # The allowed origins. If null, all origins are allowed. 24 | #allowedOrigins = ["http://www.example.com"] 25 | 26 | # The allowed HTTP methods. If null, all methods are allowed 27 | #allowedHttpMethods = ["GET", "POST"] 28 | } 29 | 30 | ## CSRF Filter 31 | # https://www.playframework.com/documentation/latest/ScalaCsrf#Applying-a-global-CSRF-filter 32 | # https://www.playframework.com/documentation/latest/JavaCsrf#Applying-a-global-CSRF-filter 33 | # ~~~~~ 34 | # Play supports multiple methods for verifying that a request is not a CSRF request. 35 | # The primary mechanism is a CSRF token. This token gets placed either in the query string 36 | # or body of every form submitted, and also gets placed in the users session. 37 | # Play then verifies that both tokens are present and match. 38 | csrf { 39 | # Sets the cookie to be sent only over HTTPS 40 | #cookie.secure = true 41 | 42 | # Defaults to CSRFErrorHandler in the root package. 43 | #errorHandler = MyCSRFErrorHandler 44 | } 45 | 46 | ## Security headers filter configuration 47 | # https://www.playframework.com/documentation/latest/SecurityHeaders 48 | # ~~~~~ 49 | # Defines security headers that prevent XSS attacks. 50 | # If enabled, then all options are set to the below configuration by default: 51 | headers { 52 | # The X-Frame-Options header. If null, the header is not set. 53 | #frameOptions = "DENY" 54 | 55 | # The X-XSS-Protection header. If null, the header is not set. 56 | #xssProtection = "1; mode=block" 57 | 58 | # The X-Content-Type-Options header. If null, the header is not set. 59 | #contentTypeOptions = "nosniff" 60 | 61 | # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. 62 | #permittedCrossDomainPolicies = "master-only" 63 | 64 | # The Content-Security-Policy header. If null, the header is not set. 65 | #contentSecurityPolicy = "default-src 'self'" 66 | } 67 | 68 | ## Allowed hosts filter configuration 69 | # https://www.playframework.com/documentation/latest/AllowedHostsFilter 70 | # ~~~~~ 71 | # Play provides a filter that lets you configure which hosts can access your application. 72 | # This is useful to prevent cache poisoning attacks. 73 | hosts { 74 | # Allow requests to example.com, its subdomains, and localhost:9000. 75 | #allowed = [".example.com", "localhost:9000"] 76 | allowed = ["."] 77 | } 78 | } 79 | environment = "local" 80 | environment = ${?ENVIRONMENT_NAME} 81 | salt.password = "Hola" 82 | salt.password = ${?SALT_PASSWORD} 83 | retries = 5 84 | retries = ${?APP_RETRIES} 85 | retry.policy = [0m, 1m, 1m, 2m, 3m, 5m, 8m] 86 | 87 | healthresponse = "This application is basically functional." 88 | 89 | gulp.devDirs = ["ui/app/dist"] 90 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-mobile-app", 3 | "version": "0.0.1", 4 | "description": "Link Mobile App", 5 | "main": "paddington.js", 6 | "author": "Link Mobile App - IxN", 7 | "license": "ISC", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "babel": { 12 | "presets": [ 13 | "react", 14 | "es2015", 15 | "stage-0" 16 | ], 17 | "env": { 18 | "test": { 19 | "plugins": [ 20 | "istanbul" 21 | ] 22 | } 23 | } 24 | }, 25 | "nyc": { 26 | "check-coverage": true, 27 | "statements": 6.34, 28 | "branches": 1.97, 29 | "functions": 10.67, 30 | "lines": 6.56, 31 | "require": [ 32 | "babel-register" 33 | ], 34 | "extension": [ 35 | ".jsx", 36 | ".js" 37 | ], 38 | "include": [ 39 | "**/app/src/scripts/**" 40 | ], 41 | "reporter": [ 42 | "text", 43 | "text-summary", 44 | "html", 45 | "teamcity" 46 | ], 47 | "all": true, 48 | "instrument": false, 49 | "sourceMap": false, 50 | "cache": true, 51 | "report-dir": "./coverage" 52 | }, 53 | "scripts": { 54 | "test": "cross-env NODE_ENV=test nyc mocha", 55 | "coverage": "nyc report" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git@github.com:LinkNYC/link-mobile-observations.git" 60 | }, 61 | "browser": { 62 | "jsdom": false 63 | }, 64 | "devDependencies": { 65 | "babel-core": "^6.26.3", 66 | "babel-plugin-istanbul": "^4.1.6", 67 | "babel-preset-es2015": "^6.24.1", 68 | "babel-preset-react": "^6.24.1", 69 | "babel-preset-stage-0": "^6.24.1", 70 | "babel-register": "^6.26.0", 71 | "babelify": "^8.0.0", 72 | "browserify": "^16.2.0", 73 | "chai": "^4.1.2", 74 | "chai-enzyme": "^0.8.0", 75 | "concat-stream": "^1.6.2", 76 | "cross-env": "^5.1.4", 77 | "del": "^3.0.0", 78 | "enzyme": "^3.3.0", 79 | "factor-bundle": "^2.5.0", 80 | "gulp": "^3.9.1", 81 | "gulp-exec": "^3.0.1", 82 | "gulp-file": "^0.4.0", 83 | "gulp-if": "^2.0.2", 84 | "gulp-livereload": "^3.8.1", 85 | "gulp-logger": "0.0.2", 86 | "gulp-notify": "^3.2.0", 87 | "gulp-sass": "^4.0.1", 88 | "gulp-shell": "^0.6.5", 89 | "gulp-sourcemaps": "^2.6.4", 90 | "gulp-uglify": "^3.0.0", 91 | "istanbul": "^0.4.5", 92 | "jquery": "^3.3.1", 93 | "lodash": "^4.17.10", 94 | "merge-stream": "^1.0.1", 95 | "mkdirp": "^0.5.1", 96 | "mocha": "^5.1.1", 97 | "nyc": "^11.7.1", 98 | "path": "^0.12.7", 99 | "react": "^16.3.2", 100 | "react-addons-test-utils": "^15.6.2", 101 | "react-dom": "^16.3.2", 102 | "redux-devtools": "^3.4.1", 103 | "resolve": "^1.7.1", 104 | "sinon": "^5.0.2", 105 | "sinon-chai": "^3.0.0", 106 | "testdom": "^2.0.0", 107 | "vinyl-buffer": "^1.0.1", 108 | "vinyl-source-stream": "^2.0.0", 109 | "watchify": "^3.11.0" 110 | }, 111 | "dependencies": { 112 | "babel-polyfill": "^6.26.0", 113 | "chart.js": "=2.7.2", 114 | "dateformat": "^3.0.3", 115 | "fixed-data-table-2": "^0.8.12", 116 | "immutable": "^3.8.2", 117 | "isomorphic-fetch": "^2.2.1", 118 | "jquery": "^3.3.1", 119 | "js-cookie": "^2.2.0", 120 | "jsonpath": "^1.0.0", 121 | "materialize-css": "^0.100.2", 122 | "npm-check-updates": "^2.14.2", 123 | "prop-types": "^15.6.1", 124 | "react": "^16.3.2", 125 | "react-chartjs-2": "=2.7.2", 126 | "react-csv": "^1.0.14", 127 | "react-dom": "^16.3.2", 128 | "react-joyride": "^1.11.4", 129 | "react-measure": "^2.0.2", 130 | "react-redux": "^5.0.7", 131 | "redux": "^4.0.0", 132 | "redux-devtools-extension": "^2.13.2", 133 | "redux-form": "^7.3.0", 134 | "redux-immutable": "^4.0.0", 135 | "redux-thunk": "^2.2.0" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ui/app/src/resources/mappings/joyride/PortInDashJRMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "port-in-dash-accepted-circle": { 3 | "data": { 4 | "id": "PID-001", 5 | "title": "Accepted Summary Wheel", 6 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have been scheduled by Syniverse to port today. Any MSISDNs not in the Accepted state should be investigated as it means Sky were not originally expecting these MSISDNs to port today.

    You can hover the cursor over a segment of the wheel in order to see the exact number of orders that are 'Done' or 'Not Done'.", 7 | "selector": "#port-in-dash-accepted-circle", 8 | "position": "right", 9 | "allowClicksThruHole": true, 10 | "style": { 11 | "mainColor": "#0d47a1", 12 | "beacon": { 13 | "inner": "#0d47a1", 14 | "outer": "#1565c0" 15 | } 16 | } 17 | }, 18 | "counts":{ 19 | "notDone": 5, 20 | "done": 10 21 | } 22 | }, 23 | "port-in-dash-secured-circle": { 24 | "data": { 25 | "id": "PID-002", 26 | "title": "Secured Summary Wheel", 27 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have been confirmed by Syniverse to port today. Any MSISDNs not in the Secured state should be investigated as it could mean that a MSISDN is no longer due to port today as expected.

    If there have been no orders so far today, 'No Data' will be displayed instead of a percentage.", 28 | "selector": "#port-in-dash-secured-circle", 29 | "position": "right", 30 | "allowClicksThruHole": true, 31 | "style": { 32 | "mainColor": "#0d47a1", 33 | "beacon": { 34 | "inner": "#0d47a1", 35 | "outer": "#1565c0" 36 | } 37 | } 38 | }, 39 | "counts": { 40 | "notDone": 0, 41 | "done": 0 42 | } 43 | }, 44 | "port-in-dash-activated-circle": { 45 | "data": { 46 | "id": "PID-003", 47 | "title": "Activated Summary Wheel", 48 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have been updated on the switch. Any MSISDNs not in the Activated state should be investigated as the necessary update may not have been successfully made to the switch.", 49 | "selector": "#port-in-dash-activated-circle", 50 | "position": "top", 51 | "allowClicksThruHole": true, 52 | "style": { 53 | "mainColor": "#0d47a1", 54 | "beacon": { 55 | "inner": "#0d47a1", 56 | "outer": "#1565c0" 57 | } 58 | } 59 | } 60 | }, 61 | "port-in-dash-file-processed-circle": { 62 | "data": { 63 | "id": "PID-004", 64 | "title": "File Summary Wheel", 65 | "text": "The summary wheel indicates the number of porting MSISDNs that Sky know to have completed all file based activities (generated and/or received). Please note that this information pertains to files being prepared for sending as opposed to being actually sent.", 66 | "selector": "#port-in-dash-file-processed-circle", 67 | "position": "left", 68 | "allowClicksThruHole": true, 69 | "style": { 70 | "mainColor": "#0d47a1", 71 | "beacon": { 72 | "inner": "#0d47a1", 73 | "outer": "#1565c0" 74 | } 75 | } 76 | } 77 | }, 78 | "port-in-dash-completed-circle": { 79 | "data": { 80 | "id": "PID-005", 81 | "title": "Completed Summary Wheel", 82 | "text": "The summary wheel indicates that all activities have been completed for the Port In order. The customer should now have a fully functioning service!", 83 | "selector": "#port-in-dash-completed-circle", 84 | "position": "left", 85 | "allowClicksThruHole": true, 86 | "style": { 87 | "mainColor": "#0d47a1", 88 | "beacon": { 89 | "inner": "#0d47a1", 90 | "outer": "#1565c0" 91 | } 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /app/actors/KmeansActor.scala: -------------------------------------------------------------------------------- 1 | package actors 2 | 3 | import akka.actor.{Actor, ActorLogging} 4 | import javax.inject.Inject 5 | import models.{Location, Observation, TypeId} 6 | import play.api.Logger 7 | import play.api.cache.AsyncCacheApi 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.util.{Failure, Success, Try} 11 | import com.google.inject.Singleton 12 | 13 | import scala.collection.immutable 14 | 15 | 16 | case class CalculateKMeans() 17 | 18 | @Singleton 19 | class KmeansActor @Inject()(cache: AsyncCacheApi)(implicit ec: ExecutionContext) extends Actor with ActorLogging { 20 | 21 | override def receive: Receive = { 22 | case CalculateKMeans() => 23 | cache.get[List[Observation]]("observations") map { 24 | case Some(obs) => 25 | // val k = KMeans.process(obs) 26 | // log.info(s"k ${k}") 27 | // cache.set("means", k) 28 | case None => log.warning("No observations to calculate") 29 | } 30 | 31 | case m => log.error(s"Unexpected message ${m}") 32 | 33 | } 34 | } 35 | 36 | 37 | object KMeans { 38 | 39 | val logger = Logger(getClass) 40 | def process(obs: List[Observation]): List[(Observation, Int)] = { 41 | val means: List[(Observation, Int)] = Try { 42 | val k = 2 43 | val clusters: Map[Int, List[Observation]] = 44 | obs.zipWithIndex.groupBy( 45 | x => x._2 % k) transform ( 46 | (i: Int, p: List[(Observation, Int)]) => for (x <- p) yield x._1) 47 | 48 | iterate(clusters, obs) 49 | 50 | } match { 51 | case Failure(ex) => ex.printStackTrace(); List[(Observation, Int)]() 52 | case Success(m) => m 53 | } 54 | means 55 | } 56 | 57 | def iterate(clusters: Map[Int, List[Observation]], points: List[Observation]): List[(Observation, Int)] = { 58 | val unzippedClusters = (clusters: Iterator[(Observation, Int)]) => clusters.map(cluster => cluster._1) 59 | 60 | // find cluster means 61 | val means = 62 | (clusters: Map[Int, List[Observation]]) => 63 | for (clusterIndex <- clusters.keys) 64 | yield clusterMean(clusters(clusterIndex)) 65 | 66 | // find the closest index 67 | def closest(p: Observation, means: Iterable[Observation]): Int = { 68 | val distances = for (center <- means) yield p.dist(center) 69 | return distances.zipWithIndex.min._2 70 | } 71 | 72 | // assignment step 73 | val newClusters: Map[Int, List[Observation]] = 74 | points.groupBy( 75 | (p: Observation) => closest(p, means(clusters))) 76 | 77 | render(newClusters) 78 | 79 | newClusters.mapValues(list => (clusterMean(list), list.size)).values.toList 80 | } 81 | 82 | def clusterMean(points: List[Observation]): Observation = { 83 | val cumulative = points.reduceLeft((a: Observation, b: Observation) => 84 | new Observation(ts = 0, id = TypeId("", ""), location = Location(lon = a.location.lon + b.location.lon, lat = a.location.lat + b.location.lat, horizontal_accuracy = 0))) 85 | 86 | return new Observation(ts = 0, id = TypeId("", ""), location = Location(lon = cumulative.location.lon / points.length, lat = cumulative.location.lat / points.length, horizontal_accuracy = 0)) 87 | } 88 | 89 | def render(points: Map[Int, List[Observation]]) { 90 | for (clusterNumber <- points.keys.toSeq.sorted) { 91 | logger.info(" Cluster " + clusterNumber) 92 | 93 | val meanPoint = clusterMean(points(clusterNumber)) 94 | logger.info(" Mean: " + meanPoint) 95 | } 96 | } 97 | } 98 | 99 | 100 | object test extends App { 101 | val k: Int = 2 102 | val obs = List(Observation(10, TypeId("", ""), Location(0, 0, 0)), Observation(11, TypeId("", ""), Location(10, 10, 0))) 103 | val clusters: Map[Int, List[Observation]] = 104 | obs.zipWithIndex.groupBy( 105 | x => x._2 % k) transform ( 106 | (i: Int, p: List[(Observation, Int)]) => for (x <- p) yield x._1) 107 | 108 | KMeans.iterate(clusters, obs) 109 | } -------------------------------------------------------------------------------- /app/modules/KinesisModule.scala: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import java.util.Date 4 | 5 | import actors.CalculateKMeans 6 | import akka.actor.{ActorRef, ActorSystem} 7 | import com.amazonaws.auth.DefaultAWSCredentialsProviderChain 8 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.{IRecordProcessor, IRecordProcessorFactory} 9 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.{KinesisClientLibConfiguration, Worker} 10 | import com.google.inject.AbstractModule 11 | import javax.inject.{Inject, Named} 12 | import kinesis._ 13 | import models.{Observation, Ping} 14 | import play.api.cache.AsyncCacheApi 15 | import play.api.libs.concurrent.AkkaGuiceSupport 16 | import play.api.{Configuration, Environment, Logger} 17 | 18 | import scala.language.postfixOps 19 | import scala.concurrent.{Await, ExecutionContext} 20 | import scala.concurrent.duration._ 21 | 22 | class KinesisModule(environment: Environment, configuration: Configuration) extends AbstractModule with AkkaGuiceSupport { 23 | 24 | override def configure() = { 25 | bind(classOf[Test]).asEagerSingleton 26 | } 27 | } 28 | 29 | 30 | class ProcessorFactory(proxyActor: ActorRef) extends IRecordProcessorFactory { 31 | 32 | override def createProcessor(): IRecordProcessor = { 33 | println("init ProcessorFactory ") 34 | new Processor(proxyActor) 35 | } 36 | } 37 | 38 | class Test @Inject()(system: ActorSystem, 39 | @Named("proxyActor") proxyActor: ActorRef, 40 | // @Named("kActor") kActor: ActorRef, 41 | cache: AsyncCacheApi, 42 | configuration: Configuration)(implicit ec: ExecutionContext) { 43 | 44 | Await.result(cache.set("means", List[(Observation,Int)]()), 10 seconds ) 45 | Await.result(cache.set("observations", List[Observation]()), 10 seconds) 46 | 47 | val env = configuration.get[String]("environment") 48 | // val hostname: String = InetAddress.getLocalHost.getHostName 49 | val logger = Logger(getClass) 50 | 51 | system.scheduler.schedule(90.milli, 30.seconds) { 52 | Logger("ping").info("pinging") 53 | proxyActor ! Ping(System.currentTimeMillis()) 54 | } 55 | 56 | // system.scheduler.schedule( 10 minutes, 30 minutes) { 57 | // Logger("means").info("kmeans") 58 | // kActor ! CalculateKMeans() 59 | // } 60 | 61 | 62 | import java.net.InetAddress 63 | import java.util.UUID 64 | 65 | val kinesisStream = env match { 66 | case "local" => "test" 67 | case "qa" => "test" 68 | case x: String => x 69 | } 70 | 71 | 72 | val app_table = env match { 73 | case "local" => s"link-programmatic-uda-observations-${InetAddress.getLocalHost.getCanonicalHostName}" 74 | case default => s"link-programmatic-uda-observations-${env}" 75 | } 76 | 77 | try { 78 | val cp = new DefaultAWSCredentialsProviderChain 79 | val workerId: String = InetAddress.getLocalHost.getCanonicalHostName + ":" + UUID.randomUUID 80 | val kinesisClientLibConfiguration: KinesisClientLibConfiguration = new KinesisClientLibConfiguration( 81 | app_table, 82 | s"programmatic-uda-${kinesisStream}-observations", 83 | cp, 84 | workerId) 85 | 86 | val since = new Date(System.currentTimeMillis() - 2.days.toMillis) 87 | logger.info(s"since $since") 88 | kinesisClientLibConfiguration.withTimestampAtInitialPositionInStream( since ) 89 | 90 | val recordProcessorFactory = new ProcessorFactory(proxyActor) 91 | val worker = new Worker(recordProcessorFactory, kinesisClientLibConfiguration) 92 | 93 | logger.info(s"Running ${workerId} to process stream.") 94 | 95 | var exitCode: Int = 0 96 | 97 | new Thread(worker).start() 98 | 99 | logger.info(s"Running system in $env ") 100 | logger.info(s"Kinesis stream 'programmatic-uda-${kinesisStream}-observations' ") 101 | logger.info(s"app table: ${app_table} ") 102 | 103 | } catch { 104 | case t: Throwable => 105 | System.err.println("Caught throwable while processing data.") 106 | t.printStackTrace() 107 | logger.error("Ex with KCL", t) 108 | // exitCode = 1 109 | } 110 | 111 | 112 | } -------------------------------------------------------------------------------- /ui/test/actions/DataActionTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as actionCreator from '../../app/src/scripts/actions/DataAction' 3 | 4 | let orders = ["order1", "order2", "order3"]; 5 | 6 | describe('(REDUX) DataAction populateOrders(Array)', function() { 7 | it('should return object with type POPULATE_ORDERS and orders content', function() { 8 | let action = actionCreator.populateOrders(orders); 9 | expect(action).to.deep.equal({ 10 | "type": "POPULATE_ORDERS", 11 | "content": { 12 | "orders": orders 13 | } 14 | }) 15 | }); 16 | 17 | it('should return object with type POPULATE_ORDERS and empty orders content', function() { 18 | let action = actionCreator.populateOrders([]); 19 | expect(action).to.deep.equal({ 20 | "type": "POPULATE_ORDERS", 21 | "content": { 22 | "orders": [] 23 | } 24 | }) 25 | }); 26 | }); 27 | 28 | describe('(REDUX) DataAction updateOrders(Array)', function() { 29 | it('should return object with type UPDATE_ORDERS and orders content', function() { 30 | let action = actionCreator.updateOrders(orders); 31 | expect(action).to.deep.equal({ 32 | "type": "UPDATE_ORDERS", 33 | "content": { 34 | "orders": orders 35 | } 36 | }) 37 | }); 38 | 39 | it('should return object with type UPDATE_ORDERS and empty orders content', function() { 40 | let action = actionCreator.updateOrders([]); 41 | expect(action).to.deep.equal({ 42 | "type": "UPDATE_ORDERS", 43 | "content": { 44 | "orders": [] 45 | } 46 | }) 47 | }); 48 | }); 49 | 50 | describe('(REDUX) DataAction removeOrders(Array)', function() { 51 | it('should return object with type REMOVE_ORDERS and orders content', function() { 52 | let action = actionCreator.removeOrders(orders); 53 | expect(action).to.deep.equal({ 54 | "type": "REMOVE_ORDERS", 55 | "content": { 56 | "orders": orders 57 | } 58 | }) 59 | }); 60 | 61 | it('should return object with type REMOVE_ORDERS and empty orders content', function() { 62 | let action = actionCreator.removeOrders([]); 63 | expect(action).to.deep.equal({ 64 | "type": "REMOVE_ORDERS", 65 | "content": { 66 | "orders": [] 67 | } 68 | }) 69 | }); 70 | }); 71 | 72 | describe('(REDUX) DataAction clearOrders()', function() { 73 | it('should return object with type CLEAR_ORDERS', function() { 74 | let action = actionCreator.clearOrders(); 75 | expect(action).to.deep.equal({ 76 | "type": "CLEAR_ORDERS" 77 | }) 78 | }) 79 | }); 80 | 81 | let summaries = ["summary1", "summary2", "summary3"]; 82 | 83 | describe('(REDUX) DataAction populateSummaries(Array)', function() { 84 | it('should return object with type POPULATE_SUMMARIES and summaries content', function() { 85 | let action = actionCreator.populateSummaries(summaries); 86 | expect(action).to.deep.equal({ 87 | "type": "POPULATE_SUMMARIES", 88 | "content": { 89 | "summaries": summaries 90 | } 91 | }) 92 | }) 93 | }); 94 | 95 | describe('(REDUX) DataAction updateSummaries(Array)', function() { 96 | it('should return object with type UPDATE_SUMMARIES and summaries content', function() { 97 | let action = actionCreator.updateSummaries(summaries); 98 | expect(action).to.deep.equal({ 99 | "type": "UPDATE_SUMMARIES", 100 | "content": { 101 | "summaries": summaries 102 | } 103 | }) 104 | }) 105 | }); 106 | 107 | describe('(REDUX) DataAction clearSummaries()', function() { 108 | it('should return object with type CLEAR_SUMMARIES', function() { 109 | let action = actionCreator.clearSummaries() 110 | expect(action).to.deep.equal({ 111 | "type": "CLEAR_SUMMARIES" 112 | }) 113 | }) 114 | }); -------------------------------------------------------------------------------- /ui/test/resources/WrittenFileState.js: -------------------------------------------------------------------------------- 1 | import { Map, OrderedMap, fromJS } from 'immutable'; 2 | 3 | export default { 4 | "data": Map({ 5 | "mappings": fromJS({ 6 | "writtenFiles": { 7 | "name": "WrittenFileMapping", 8 | "filterTypes": [], 9 | "mappings": [ 10 | { 11 | "displayName": "Filename", 12 | "path": "$.entityId.absoluteFilePath", 13 | "searchable": true, 14 | "columnWidth": 300, 15 | "flexGrow": 2, 16 | "isKey": true 17 | }, 18 | { 19 | "displayName": "File Type", 20 | "path": "$.fileType", 21 | "searchable": true, 22 | "columnWidth": 60 23 | }, 24 | { 25 | "displayName": "Date Generated", 26 | "path": "$.writtenAt", 27 | "type": "DATE", 28 | "searchable": true, 29 | "columnWidth": 120 30 | }, 31 | { 32 | "displayName": "No. of Records", 33 | "path": "$.records.length", 34 | "columnWidth": 120 35 | }, 36 | { 37 | "displayName": "No. of Pending Records", 38 | "path": "$.pendingRecordsCount", 39 | "columnWidth": 120 40 | }, 41 | { 42 | "displayName": "View Records", 43 | "path": "", 44 | "columnWidth": 140 45 | } 46 | ] 47 | }, 48 | "writtenFileRecords": { 49 | "name": "WrittenFileRecordMapping", 50 | "filterTypes": [], 51 | "mappings": [ 52 | { 53 | "displayName": "MSISDN", 54 | "path": "$.msisdn", 55 | "searchable": true, 56 | "columnWidth": 120 57 | }, 58 | { 59 | "displayName": "DNO", 60 | "path": "$.dno", 61 | "searchable": true, 62 | "columnWidth": 40, 63 | "flexGrow": 2 64 | }, 65 | { 66 | "displayName": "RNO", 67 | "path": "$.rno", 68 | "searchable": true, 69 | "columnWidth": 40, 70 | "flexGrow": 2 71 | }, 72 | { 73 | "displayName": "ONO", 74 | "path": "$.ono", 75 | "searchable": true, 76 | "columnWidth": 40, 77 | "flexGrow": 2 78 | } 79 | ] 80 | } 81 | }), 82 | "orders": OrderedMap({ 83 | "/tmp/nfs/vfuk/vfukmnp01/SK201701121108VF005.REQ,EE": { 84 | "entityId": { 85 | "absoluteFilePath": "/tmp/nfs/vfuk/vfukmnp01/SK201701121108VF005.REQ", 86 | "targetNetworkOperatorCode": "EE" 87 | }, 88 | "eventCount": 1, 89 | "status": "COMPLETED", 90 | "lastEventAt": "2017-05-31T12:22:31.32+01:00[Europe/London]", 91 | "writtenAt": "2017-05-31T12:22:31.32+01:00[Europe/London]", 92 | "fileType": "REQ", 93 | "pendingRecordsCount": 1, 94 | "records": [ 95 | { 96 | "transactionNumber": "TRANS1" 97 | } 98 | ] 99 | } 100 | }), 101 | "summaries": Map({}) 102 | }), 103 | "control": { 104 | "pageType": "receivedFiles", 105 | "shouldShowCompleted": false, 106 | "shouldShowOutstanding": true 107 | } 108 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/util/FilterUtils.js: -------------------------------------------------------------------------------- 1 | import JsonPath from 'jsonpath'; 2 | import { getDefinedOrElse } from './DefinedPathUtils'; 3 | 4 | export function isCompleted(order, mappingName) { 5 | return nonReceivedFilesOrderIsCompleted(order, mappingName) || receivedFileRecordsAllProcessedSuccessfully(order); 6 | } 7 | 8 | function nonReceivedFilesOrderIsCompleted(order, mappingName) { 9 | return (orderStatusIsCompleted(order) && mappingTypeIsNot("receivedFiles", mappingName)); 10 | } 11 | 12 | export function receivedFileRecordsAllProcessedSuccessfully(order) { 13 | return allRecordsAreSuccessful(order) && noFailedRecords(order); 14 | } 15 | 16 | function allRecordsAreSuccessful(order) { 17 | return JsonPath.query(order, "$.numberOfRecords")[0] === JsonPath.query(order, "$.successfulRecords.length")[0]; 18 | } 19 | 20 | function noFailedRecords(order) { 21 | return JsonPath.query(order, "$.failedRecords.length")[0] === 0; 22 | } 23 | 24 | export function orderStatusIsCompleted(order) { 25 | return JsonPath.query(order, "$.status").toString().includes("COMPLETED"); 26 | } 27 | 28 | function mappingTypeIsNot(page, mappingName) { 29 | return mappingName !== page; 30 | } 31 | 32 | function filterCompleted(shouldShowCompleted, filterTypes, mappingName, order) { 33 | return (filterTypes.includes("completed") ? shouldShowCompleted : true) 34 | || !isCompleted(order, mappingName); 35 | } 36 | 37 | function filterOutstanding(shouldShowOutstanding, filterTypes, order) { 38 | return (filterTypes.includes("outstanding") ? shouldShowOutstanding : true) 39 | || !isOutstanding(order); 40 | } 41 | 42 | export function isOutstanding(order) { 43 | let startOfToday = new Date().setHours(0, 0, 0, 0); 44 | 45 | return (Date.parse(JsonPath.query(order, "$.entityId.portDate")) < startOfToday || 46 | Date.parse(JsonPath.query(order, "$.expectedPortDate")) < startOfToday || 47 | Date.parse(JsonPath.query(order, "$.recordReceivedAt").toString().replace("[Europe/London]", "")) < startOfToday); 48 | } 49 | 50 | function searchFilter(order, tableColumnToOrderMapping, searchString) { 51 | return tableColumnToOrderMapping.find(mapping => 52 | lowerCasePathValue(order, mapping).includes(searchString.toLowerCase()) 53 | ) !== undefined; 54 | } 55 | 56 | function lowerCasePathValue(order, mapping) { 57 | return getDefinedOrElse(order, mapping.get("path").replace(/\$\./, ""), "").toString().toLowerCase(); 58 | } 59 | 60 | export function orderSelector(state, searchString = "") { 61 | let mappings = getDataMappingProperty("mappings").from(state).filter(mapping => mapping.get("searchable")); 62 | let filterTypes = getDataMappingProperty("filterTypes").from(state); 63 | 64 | let filteredOrders = state.data.get("orders").toArray() 65 | .filter(order => filterCompleted(state.control.shouldShowCompleted, filterTypes, state.control.pageType, order)) 66 | .filter(order => filterOutstanding(state.control.shouldShowOutstanding, filterTypes, order)); 67 | 68 | if (searchString && searchString.trim().length && mappings && mappings.size) { 69 | return filteredOrders.filter(order => searchFilter(order, mappings, searchString.trim())); 70 | } else { 71 | return filteredOrders; 72 | } 73 | } 74 | 75 | function getDataMappingProperty(property, type) { 76 | return { 77 | from: function(state) { 78 | return state.data.getIn(["mappings", type ? type : state.control.pageType, property], []); 79 | } 80 | } 81 | } 82 | 83 | function getDataFromOrderAsArray(state, type, orderId) { 84 | let recordData = getDefinedOrElse(state.data.get("orders").get(orderId), type, []); 85 | return Array.isArray(recordData) ? recordData : [recordData]; 86 | } 87 | 88 | function filterIncompleteOrderRecords(data) { 89 | return data.filter(order => order.msisdn); 90 | } 91 | 92 | export function orderRecordsSelector(state, type, orderId, mappingType, searchString = "") { 93 | let mappings = getDataMappingProperty("mappings", mappingType).from(state).filter(mapping => mapping.get("searchable")); 94 | 95 | let data = getDataFromOrderAsArray(state, type, orderId); 96 | 97 | if (mappingType === "writtenFileRecords") data = filterIncompleteOrderRecords(data); 98 | 99 | if (searchString && searchString.trim().length && mappings && mappings.size) { 100 | return data.filter(record => searchFilter(record, mappings, searchString.trim())); 101 | } else { 102 | return data; 103 | } 104 | } -------------------------------------------------------------------------------- /ui/app/src/scripts/reducers/dataReducer.js: -------------------------------------------------------------------------------- 1 | import { Map, OrderedMap, fromJS } from 'immutable'; 2 | import { POPULATE_ORDERS, POPULATE_SUMMARIES, UPDATE_ORDERS, UPDATE_SUMMARIES, CLEAR_ORDERS, CLEAR_SUMMARIES, REMOVE_ORDERS } from '../actions/DataAction'; 3 | import PortInDashMapping from "../../resources/mappings/PortInDashMapping.json"; 4 | import PortInExpectedMapping from "../../resources/mappings/PortInExpectedMapping.json"; 5 | import PortInErrorMapping from "../../resources/mappings/PortInErrorMapping.json"; 6 | import PortOutExpectedMapping from "../../resources/mappings/PortOutExpectedMapping.json"; 7 | import PortOutErrorMapping from "../../resources/mappings/PortOutErrorMapping.json"; 8 | import SubPortExpectedMapping from "../../resources/mappings/SubPortExpectedMapping.json"; 9 | import SubPortErrorMapping from "../../resources/mappings/SubPortErrorMapping.json"; 10 | import ReceivedFileMapping from "../../resources/mappings/ReceivedFileMapping.json"; 11 | import ReceivedFileRecordMapping from "../../resources/mappings/ReceivedFileRecordMapping.json"; 12 | import WrittenFileMapping from "../../resources/mappings/WrittenFileMapping.json"; 13 | import WrittenFileRecordMapping from "../../resources/mappings/WrittenFileRecordMapping.json"; 14 | import ReceivedFileFailureMapping from "../../resources/mappings/ReceivedFileFailureMapping.json"; 15 | import {equal} from "../util/Equal"; 16 | 17 | /* 18 | Mapping Keys equate to pageTypes received from Back End. 19 | */ 20 | const initialState = Map({ 21 | mappings: fromJS({ 22 | portInDash: PortInDashMapping, 23 | portInExpected: PortInExpectedMapping, 24 | portInErrors: PortInErrorMapping, 25 | portOutExpected: PortOutExpectedMapping, 26 | portOutErrors: PortOutErrorMapping, 27 | subPortExpected: SubPortExpectedMapping, 28 | subPortErrors: SubPortErrorMapping, 29 | receivedFiles: ReceivedFileMapping, 30 | receivedFileRecords: ReceivedFileRecordMapping, 31 | receivedFileFailure: ReceivedFileFailureMapping, 32 | writtenFiles: WrittenFileMapping, 33 | writtenFileRecords: WrittenFileRecordMapping 34 | }), 35 | orders: OrderedMap({}), 36 | summaries: Map({}) 37 | }); 38 | 39 | export default function(state = initialState, action) { 40 | switch (action.type) { 41 | case POPULATE_ORDERS: 42 | 43 | // console.log("----") 44 | // var old = state.get('orders') 45 | // console.log("orders "+ old + " " + old.size) 46 | // 47 | // console.log("oders " + action.content.orders) 48 | // action.content.orders.forEach(function(a) { 49 | // console.log("a" + a) 50 | // }) 51 | // var newOrders = OrderedMap(action.content.orders.map( 52 | // o => [o.ts, o] 53 | // )) 54 | // console.log("orders "+ newOrders + " " + newOrders.size) 55 | // 56 | // var merged = old.mergeDeep(newOrders) 57 | // 58 | // console.log(" new orders " + merged + " " + merged.size) 59 | // console.log("----") 60 | 61 | return state.set('orders', state.get('orders').mergeDeep(OrderedMap(action.content.orders.map( 62 | o => [o.ts+o.id.value, o] 63 | ) 64 | ))); 65 | case POPULATE_SUMMARIES: 66 | return state.set('summaries', 67 | Map(action.content.summaries.map(o => 68 | [o.summaryStage, o]))); 69 | case UPDATE_ORDERS: 70 | return state.set('orders', state.get('orders').mergeDeep(OrderedMap(action.content.orders.map( 71 | o => [Object.values(o.entityId).toString(), o] 72 | ) 73 | ))); 74 | case REMOVE_ORDERS: 75 | return state.set('orders', state.get('orders').filter(stateOrder => notPresent(stateOrder, action.content.orders))); 76 | case UPDATE_SUMMARIES: 77 | 78 | return state.set('summaries', 79 | state.get('summaries').mergeDeep( 80 | Map(action.content.summaries.map(o => 81 | [o.summaryStage, o])))); 82 | case CLEAR_ORDERS: 83 | return state.set('orders', 84 | initialState.get('orders')); 85 | case CLEAR_SUMMARIES: 86 | return state.set('summaries', 87 | initialState.get('summaries')); 88 | default: 89 | return state; 90 | } 91 | } 92 | 93 | function notPresent(stateOrder, actionOrders) { 94 | return !actionOrders.filter(actionOrder => equal(actionOrder.entityId, stateOrder.entityId)).length; 95 | } -------------------------------------------------------------------------------- /app/kinesis/Processor.scala: -------------------------------------------------------------------------------- 1 | package kinesis 2 | 3 | import java.nio.charset.Charset 4 | import java.util 5 | import java.util.zip.Inflater 6 | 7 | import javax.inject.{Inject, Named} 8 | import akka.actor.ActorRef 9 | import com.amazonaws.services.kinesis.clientlibrary.exceptions.{InvalidStateException, ShutdownException, ThrottlingException} 10 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.{IRecordProcessor, IRecordProcessorCheckpointer} 11 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShutdownReason 12 | import com.amazonaws.services.kinesis.model.Record 13 | import models.{ObservationHolder, UpdateMessage, UpdateMessageList} 14 | import play.api.Logger 15 | 16 | import scala.collection.JavaConverters._ 17 | import scala.concurrent.duration._ 18 | 19 | class Processor(proxyActor: ActorRef) extends IRecordProcessor { 20 | 21 | println("initing Processor...") 22 | val logger = Logger(getClass) 23 | var kinesisShardId = "" 24 | 25 | // Backoff and retry settings 26 | val BACKOFF_TIME_IN_MILLIS = 10.seconds.toMillis 27 | val NUM_RETRIES = 20 28 | 29 | // Checkpoint about once a minute 30 | val CHECKPOINT_INTERVAL_MILLIS = 2.minutes.toMillis 31 | var nextCheckpointTimeInMillis = 0L 32 | 33 | val decoder = Charset.forName("UTF-8").newDecoder 34 | 35 | 36 | def decompress(inData: Array[Byte]): Array[Byte] = { 37 | val inflater = new Inflater() 38 | inflater.setInput(inData) 39 | val decompressedData = new Array[Byte](inData.size * 2) 40 | var count = inflater.inflate(decompressedData) 41 | var finalData = decompressedData.take(count) 42 | while (count > 0) { 43 | count = inflater.inflate(decompressedData) 44 | finalData = finalData ++ decompressedData.take(count) 45 | } 46 | inflater.end() 47 | 48 | val value = new String(finalData, "UTF-8") 49 | val observations: List[ObservationHolder] = models.implicits.parse(value) 50 | // println(s" -> #: ${observations.size} ${proxyActor}") 51 | // observations.foreach { obs => 52 | // proxyActor ! UpdateMessage(obs.body.observation) 53 | // } 54 | proxyActor ! UpdateMessageList(observations.map(_.body.observation)) 55 | return finalData 56 | } 57 | 58 | 59 | override def processRecords(records: util.List[Record], checkpointer: IRecordProcessorCheckpointer): Unit = { 60 | logger.info("Processing " + records.size + " records from " + kinesisShardId) 61 | 62 | // Process records and perform all exception handling. 63 | records.asScala.foreach { record => 64 | 65 | decompress(record.getData.array()) 66 | 67 | 68 | } 69 | 70 | // Checkpoint once every checkpoint interval. 71 | if (System.currentTimeMillis > nextCheckpointTimeInMillis) { 72 | checkpoint(checkpointer) 73 | nextCheckpointTimeInMillis = System.currentTimeMillis + CHECKPOINT_INTERVAL_MILLIS 74 | } 75 | } 76 | 77 | override def initialize(shardId: String): Unit = { 78 | logger.info("Initializing record processor for shard: " + shardId) 79 | println("Initializing record processor for shard: " + shardId) 80 | kinesisShardId = shardId 81 | } 82 | 83 | override def shutdown(checkpointer: IRecordProcessorCheckpointer, reason: ShutdownReason): Unit = { 84 | 85 | } 86 | 87 | 88 | private def checkpoint(checkpointer: IRecordProcessorCheckpointer): Unit = { 89 | logger.info("Checkpointing shard " + kinesisShardId) 90 | var i = 0 91 | while (i < NUM_RETRIES) { 92 | try { 93 | checkpointer.checkpoint() 94 | } catch { 95 | case se: ShutdownException => 96 | // Ignore checkpoint if the processor instance has been shutdown (fail over). 97 | logger.info("Caught shutdown exception, skipping checkpoint.", se) 98 | case e: ThrottlingException => 99 | // Backoff and re-attempt checkpoint upon transient failures 100 | if (i >= (NUM_RETRIES - 1)) { 101 | logger.error("Checkpoint failed after " + (i + 1) + "attempts.", e) 102 | } 103 | else logger.info("Transient issue when checkpointing - attempt " + (i + 1) + " of " + NUM_RETRIES, e) 104 | case e: InvalidStateException => 105 | // This indicates an issue with the DynamoDB table (check for table, provisioned IOPS). 106 | logger.error("Cannot save checkpoint to the DynamoDB table used by the Amazon Kinesis Client Library.", e) 107 | } 108 | try 109 | Thread.sleep(BACKOFF_TIME_IN_MILLIS) 110 | catch { 111 | case e: InterruptedException => 112 | logger.debug("Interrupted sleep", e) 113 | } 114 | 115 | i += 1 116 | 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ui/test/reducers/DataReducerTest.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import dataReducer from '../../app/src/scripts/reducers/dataReducer' 3 | 4 | import PortOutOrder from '../resources/PortOutOrder.json' 5 | import SummariesOrder from '../resources/SummariesOrder.json' 6 | 7 | import { Map, OrderedMap, fromJS } from 'immutable'; 8 | 9 | import PortInExpectedMapping from "../../app/src/resources/mappings/PortInExpectedMapping.json"; 10 | import PortInErrorMapping from "../../app/src/resources/mappings/PortInErrorMapping.json"; 11 | import PortOutExpectedMapping from "../../app/src/resources/mappings/PortOutExpectedMapping.json"; 12 | import PortOutErrorMapping from "../../app/src/resources/mappings/PortOutErrorMapping.json"; 13 | import SubPortExpectedMapping from "../../app/src/resources/mappings/SubPortExpectedMapping.json"; 14 | import SubPortErrorMapping from "../../app/src/resources/mappings/SubPortErrorMapping.json"; 15 | import ReceivedFileMapping from "../../app/src/resources/mappings/ReceivedFileMapping.json"; 16 | import ReceivedFileRecordMapping from "../../app/src/resources/mappings/ReceivedFileRecordMapping.json"; 17 | import WrittenFileMapping from "../../app/src/resources/mappings/WrittenFileMapping.json"; 18 | import WrittenFileRecordMapping from "../../app/src/resources/mappings/WrittenFileRecordMapping.json"; 19 | import ReceivedFileFailureMapping from "../../app/src/resources/mappings/ReceivedFileFailureMapping.json"; 20 | 21 | describe('(REDUX) DataReducer', function() { 22 | it('should return state with no modifications if action is not recognised', function() { 23 | expect(dataReducer(initialState(), ACTION_EMPTY)).to.deep.equal(initialState()); 24 | }); 25 | 26 | it('should return initialState if state is undefined', function() { 27 | expect(dataReducer(undefined, ACTION_EMPTY)).to.deep.equal(initialState()); 28 | }); 29 | }); 30 | 31 | describe('(REDUX) DataReducer (ACTION) POPULATE_ORDERS', function() { 32 | it('should return state with some order', function() { 33 | // let initialState = setupInitialState(); 34 | // let action = { 35 | // "type": "POPULATE_ORDERS", 36 | // "content": { 37 | // "orders": PortOutOrder 38 | // } 39 | // }; 40 | // 41 | // let mutatedInitialState = setupInitialState(); 42 | // mutatedInitialState.orders = PortOutOrder; 43 | // 44 | // let result = dataReducer(initialState, action); 45 | // 46 | // expect(result).to.deep.equal([]); 47 | }) 48 | 49 | 50 | }); 51 | 52 | describe('(REDUX) DataReducer (ACTION) POPULATE_SUMMARIES', function() { 53 | it('should return state with some order', function() { 54 | // let mutatedInitialState = initialState(); 55 | // mutatedInitialState._root.entries[2][1] = formatSummaries(SummariesOrder); //ownerId =/= Undefined?? 56 | // expect(result).to.deep.equal(mutatedInitialState); 57 | 58 | //entries[2][1] ==> Summaries in store 59 | let result = dataReducer(initialState(), ACTION_POPULATE_SUMMARIES); 60 | expect(result._root.entries[2][1]).to.deep.equal(formatSummaries(SummariesOrder)); 61 | 62 | 63 | }) 64 | }); 65 | 66 | describe('(REDUX) DataReducer (ACTION) UPDATE_ORDERS', function() { 67 | 68 | }); 69 | 70 | describe('(REDUX) DataReducer (ACTION) REMOVE_ORDERS', function() { 71 | 72 | }); 73 | 74 | describe('(REDUX) DataReducer (ACTION) UPDATE_SUMMARIES', function() { 75 | 76 | }); 77 | 78 | describe('(REDUX) DataReducer (ACTION) CLEAR_ORDERS', function() { 79 | 80 | }); 81 | 82 | describe('(REDUX) DataReducer (ACTION) CLEAR_SUMMARIES', function() { 83 | 84 | }); 85 | 86 | function formatSummaries(summaries) { 87 | return Map(summaries.map(o => [o.summaryStage, o])); 88 | } 89 | 90 | const ACTION_EMPTY = {}; 91 | 92 | const ACTION_POPULATE_SUMMARIES = { 93 | "type": "POPULATE_SUMMARIES", 94 | "content": { 95 | "summaries": SummariesOrder 96 | } 97 | }; 98 | 99 | function initialState() { 100 | return Map({ 101 | mappings: fromJS({ 102 | portInExpected: PortInExpectedMapping, 103 | portInErrors: PortInErrorMapping, 104 | portOutExpected: PortOutExpectedMapping, 105 | portOutErrors: PortOutErrorMapping, 106 | subPortExpected: SubPortExpectedMapping, 107 | subPortErrors: SubPortErrorMapping, 108 | receivedFiles: ReceivedFileMapping, 109 | receivedFileRecords: ReceivedFileRecordMapping, 110 | receivedFileFailure: ReceivedFileFailureMapping, 111 | writtenFiles: WrittenFileMapping, 112 | writtenFileRecords: WrittenFileRecordMapping 113 | }), 114 | orders: OrderedMap({}), 115 | summaries: Map({}) 116 | }) 117 | } --------------------------------------------------------------------------------