├── 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 |
9 | info_outline
10 |
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 |
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 | {label}
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 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
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 |
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 |
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 |
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 | Home
23 |
24 | @for(p <- response.means) {
25 |
26 | Kmeans
27 | @p._1.location.lat , @p._1.location.lon #@p._2
28 |
29 |
30 | }
31 |
32 |
33 |
34 |
35 | @for(p <- response.idAds) {
36 |
37 | ids
38 |
39 | @p._1 -> @p._2
40 |
41 |
42 |
43 | }
44 |
45 |
46 |
47 |
48 |
49 | @for(p <- response.idAdType) {
50 |
51 | idType
52 |
53 | @p._1 -> @p._2
54 |
55 |
56 | }
57 |
58 |
59 |
60 |
61 |
62 |
63 | ID
64 | ID type
65 | TS
66 | long
67 | lat
68 | ...
69 |
70 |
71 |
72 | @for(p <- response.observations) {
73 |
74 | @p.id.value
75 | @p.t
76 | @Observation.toDate(p)
77 | @p.location.lon
78 | @p.location.lat
79 |
80 |
81 |
82 | }
83 |
84 |
85 |
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 | }
--------------------------------------------------------------------------------