├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── apps ├── .gitkeep ├── admin │ ├── README.md │ ├── e2e │ │ ├── app.e2e-spec.ts │ │ ├── app.po.ts │ │ └── tsconfig.e2e.json │ └── src │ │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.module.ts │ │ ├── assets │ │ ├── .gitkeep │ │ └── nx-logo.png │ │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ └── tsconfig.app.json ├── agent │ ├── README.md │ ├── e2e │ │ ├── app.e2e-spec.ts │ │ ├── app.po.ts │ │ └── tsconfig.e2e.json │ └── src │ │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.module.ts │ │ ├── assets │ │ ├── .gitkeep │ │ └── nx-logo.png │ │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ └── tsconfig.app.json └── portal │ ├── README.md │ ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json │ └── src │ ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.module.ts │ ├── assets │ ├── .gitkeep │ └── nx-logo.png │ ├── environments │ ├── environment.prod.ts │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ └── tsconfig.app.json ├── build.sh ├── karma.conf.js ├── libs ├── .gitkeep ├── age-range │ ├── index.ts │ └── src │ │ ├── age-range.module.spec.ts │ │ ├── age-range.module.ts │ │ ├── age-ranges.ts │ │ └── enum-to-range.pipe.ts ├── app-schema │ ├── README.md │ ├── index.ts │ └── src │ │ ├── employee-listing.ts │ │ └── employee.ts ├── employee-api │ ├── index.ts │ └── src │ │ ├── employee-api.module.spec.ts │ │ ├── employee-api.module.ts │ │ └── employee-api.service.ts ├── employee-display │ ├── index.ts │ └── src │ │ ├── employee-detail-view │ │ ├── employee-detail-view.component.html │ │ └── employee-detail-view.component.ts │ │ ├── employee-display.module.spec.ts │ │ ├── employee-display.module.ts │ │ └── employee-list-table-view │ │ ├── employee-list-table-view.component.html │ │ └── employee-list-table-view.component.ts ├── employee-list │ ├── index.ts │ └── src │ │ ├── employee-list.module.ts │ │ └── employee-list │ │ ├── employee-list.component.html │ │ └── employee-list.component.ts ├── employee-management │ ├── index.ts │ └── src │ │ ├── add-employee │ │ ├── add-employee.component.html │ │ └── add-employee.component.ts │ │ ├── edit-employee │ │ ├── edit-employee.component.html │ │ └── edit-employee.component.ts │ │ ├── employee-fields │ │ ├── employee-fields.component.html │ │ └── employee-fields.component.ts │ │ ├── employee-management.module.spec.ts │ │ ├── employee-management.module.ts │ │ ├── employee-management.routes.ts │ │ ├── employee-navigation.service.ts │ │ └── management-screen │ │ ├── management-screen.component.html │ │ └── management-screen.component.ts ├── employee-search │ ├── index.ts │ └── src │ │ ├── employee-list │ │ ├── employee-list.component.html │ │ └── employee-list.component.ts │ │ └── employee-search.module.ts ├── fruit-basket │ ├── index.ts │ └── src │ │ ├── basket-ui │ │ ├── basket-ui.component.html │ │ └── basket-ui.component.ts │ │ ├── counter-display │ │ ├── counter-display.component.html │ │ └── counter-display.component.ts │ │ ├── fruit-basket.module.ts │ │ └── state │ │ └── state.ts ├── retry-loader │ ├── index.ts │ └── src │ │ ├── faulty.ts │ │ ├── load-with-retry.ts │ │ ├── retry-loader.module.spec.ts │ │ └── retry-loader.module.ts └── video-stat-dashboard │ ├── index.ts │ └── src │ ├── +state │ ├── video-stats.actions.ts │ ├── video-stats.effects.ts │ ├── video-stats.init.ts │ ├── video-stats.interfaces.ts │ └── video-stats.reducer.ts │ ├── dashboard.component.css │ ├── dashboard.component.html │ ├── dashboard.component.ts │ ├── services │ ├── app.service.ts │ ├── dashboard.service.ts │ ├── mock-video-list.ts │ ├── top-list.service.ts │ ├── views-breakdown.service.ts │ └── views-filter.service.ts │ ├── top-list │ ├── top-list-display.component.css │ ├── top-list-display.component.html │ ├── top-list-display.component.ts │ ├── top-list.component.html │ └── top-list.component.ts │ ├── video-container │ ├── video-container-display.component.html │ ├── video-container-display.component.ts │ ├── video-container.component.html │ └── video-container.component.ts │ ├── video-stat-dashboard.module.ts │ ├── views-breakdown │ ├── filter-state-display.component.html │ ├── filter-state-display.component.ts │ ├── views-breakdown-display.component.html │ ├── views-breakdown-display.component.ts │ ├── views-breakdown.component.html │ └── views-breakdown.component.ts │ └── views-filter │ ├── views-filter-display.component.html │ ├── views-filter-display.component.ts │ ├── views-filter.component.html │ └── views-filter.component.ts ├── package.json ├── protractor.conf.js ├── proxy.conf.json ├── servers ├── README.md └── node │ ├── .gitignore │ ├── README.md │ ├── db.json │ ├── package.json │ ├── src │ ├── graphql │ │ └── graphql.ts │ ├── main.ts │ ├── rest │ │ └── rest.ts │ └── sse │ │ ├── channels.ts │ │ ├── fx.ts │ │ └── sse.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── test.js ├── tsconfig.compodoc.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@nrwl/schematics/src/schema.json", 3 | "project": { 4 | "name": "enterprise-example", 5 | "npmScope": "enterprise-example", 6 | "latestMigration": "20180227-cleanup-scripts" 7 | }, 8 | "e2e": { 9 | "protractor": { 10 | "config": "./protractor.conf.js" 11 | } 12 | }, 13 | "lint": [ 14 | { 15 | "project": "./tsconfig.spec.json", 16 | "exclude": "**/node_modules/**" 17 | }, 18 | { 19 | "project": "apps/admin/src/tsconfig.app.json", 20 | "exclude": "**/node_modules/**" 21 | }, 22 | { 23 | "project": "apps/admin/e2e/tsconfig.e2e.json", 24 | "exclude": "**/node_modules/**" 25 | }, 26 | { 27 | "project": "apps/agent/src/tsconfig.app.json", 28 | "exclude": "**/node_modules/**" 29 | }, 30 | { 31 | "project": "apps/agent/e2e/tsconfig.e2e.json", 32 | "exclude": "**/node_modules/**" 33 | }, 34 | { 35 | "project": "apps/portal/src/tsconfig.app.json", 36 | "exclude": "**/node_modules/**" 37 | }, 38 | { 39 | "project": "apps/portal/e2e/tsconfig.e2e.json", 40 | "exclude": "**/node_modules/**" 41 | } 42 | ], 43 | "test": { 44 | "karma": { 45 | "config": "./karma.conf.js" 46 | } 47 | }, 48 | "apps": [ 49 | { 50 | "name": "admin", 51 | "root": "apps/admin/src", 52 | "outDir": "dist/apps/admin", 53 | "assets": [ 54 | "assets", 55 | "favicon.ico" 56 | ], 57 | "index": "index.html", 58 | "main": "main.ts", 59 | "polyfills": "polyfills.ts", 60 | "test": "../../../test.js", 61 | "tsconfig": "tsconfig.app.json", 62 | "testTsconfig": "../../../tsconfig.spec.json", 63 | "prefix": "app", 64 | "styles": [ 65 | "../../../node_modules/materialize-css/dist/css/materialize.css", 66 | "styles.css" 67 | ], 68 | "scripts": [], 69 | "environmentSource": "environments/environment.ts", 70 | "environments": { 71 | "dev": "environments/environment.ts", 72 | "prod": "environments/environment.prod.ts" 73 | } 74 | }, 75 | { 76 | "name": "agent", 77 | "root": "apps/agent/src", 78 | "outDir": "dist/apps/agent", 79 | "assets": [ 80 | "assets", 81 | "favicon.ico" 82 | ], 83 | "index": "index.html", 84 | "main": "main.ts", 85 | "polyfills": "polyfills.ts", 86 | "test": "../../../test.js", 87 | "tsconfig": "tsconfig.app.json", 88 | "testTsconfig": "../../../tsconfig.spec.json", 89 | "prefix": "app", 90 | "styles": [ 91 | "../../../node_modules/materialize-css/dist/css/materialize.css", 92 | "styles.css" 93 | ], 94 | "scripts": [], 95 | "environmentSource": "environments/environment.ts", 96 | "environments": { 97 | "dev": "environments/environment.ts", 98 | "prod": "environments/environment.prod.ts" 99 | } 100 | }, 101 | { 102 | "name": "portal", 103 | "root": "apps/portal/src", 104 | "outDir": "dist/apps/portal", 105 | "assets": [ 106 | "assets", 107 | "favicon.ico" 108 | ], 109 | "index": "index.html", 110 | "main": "main.ts", 111 | "polyfills": "polyfills.ts", 112 | "test": "../../../test.js", 113 | "tsconfig": "tsconfig.app.json", 114 | "testTsconfig": "../../../tsconfig.spec.json", 115 | "prefix": "app", 116 | "styles": [ 117 | "../../../node_modules/materialize-css/dist/css/materialize.css", 118 | "styles.css" 119 | ], 120 | "scripts": [], 121 | "environmentSource": "environments/environment.ts", 122 | "environments": { 123 | "dev": "environments/environment.ts", 124 | "prod": "environments/environment.prod.ts" 125 | } 126 | }, 127 | { 128 | "name": "age-range", 129 | "root": "libs/age-range/src", 130 | "test": "../../../test.js", 131 | "appRoot": "" 132 | }, 133 | { 134 | "name": "app-schema", 135 | "root": "libs/app-schema/src", 136 | "test": "../../../test.js", 137 | "appRoot": "" 138 | }, 139 | { 140 | "name": "employee-api", 141 | "root": "libs/employee-api/src", 142 | "test": "../../../test.js", 143 | "appRoot": "" 144 | }, 145 | { 146 | "name": "employee-display", 147 | "root": "libs/employee-display/src", 148 | "test": "../../../test.js", 149 | "appRoot": "" 150 | }, 151 | { 152 | "name": "employee-list", 153 | "root": "libs/employee-list/src", 154 | "test": "../../../test.js", 155 | "appRoot": "" 156 | }, 157 | { 158 | "name": "employee-management", 159 | "root": "libs/employee-management/src", 160 | "test": "../../../test.js", 161 | "appRoot": "" 162 | }, 163 | { 164 | "name": "employee-search", 165 | "root": "libs/employee-search/src", 166 | "test": "../../../test.js", 167 | "appRoot": "" 168 | }, 169 | { 170 | "name": "fruit-basket", 171 | "root": "libs/fruit-basket/src", 172 | "test": "../../../test.js", 173 | "appRoot": "" 174 | }, 175 | { 176 | "name": "retry-loader", 177 | "root": "libs/retry-loader/src", 178 | "test": "../../../test.js", 179 | "appRoot": "" 180 | }, 181 | { 182 | "name": "video-stat-dashboard", 183 | "root": "libs/video-stat-dashboard/src", 184 | "test": "../../../test.js", 185 | "appRoot": "" 186 | }, 187 | { 188 | "name": "$workspaceRoot", 189 | "root": ".", 190 | "appRoot": "" 191 | } 192 | ], 193 | "defaults": { 194 | "schematics": { 195 | "collection": "@nrwl/schematics" 196 | }, 197 | "styleExt": "css", 198 | "component": {} 199 | }, 200 | "warnings": { 201 | "typescriptMismatch": false 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | NOTES.md 45 | *.log 46 | documentation 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | cache: 5 | yarn: true 6 | script: ./build.sh 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "./node_modules/typescript/lib", 4 | "editor.detectIndentation": false, 5 | "editor.tabSize": 2, 6 | "vsicons.presets.angular": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | { 10 | "version": "0.1.0", 11 | "command": "node_modules/.bin/tsc", 12 | "isShellCommand": true, 13 | // Show the output window only if unrecognized errors occur. 14 | "showOutput": "silent", 15 | // Use this tsconfig 16 | "args": ["-p", ".", "--noEmit" ], 17 | "problemMatcher": "$tsc" 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kyle Cordes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Enterprise Example 2 | 3 | [![Build Status](https://travis-ci.org/OasisDigital/scalable-enterprise-angular.svg?branch=master)](https://travis-ci.org/OasisDigital/scalable-enterprise-angular) 4 | 5 | This is a **work in progress**. It is likely to have many changes over time, 6 | particularly as all of the tools improve. 7 | 8 | Compodoc documentation is available at: 9 | 10 | https://oasisdigital.github.io/angular-enterprise-example/ 11 | 12 | ## Goals 13 | 14 | 1. Show an example of a sprawling set of related Angular applications, divided 15 | into various libraries. 16 | 2. Manage complexity, size, and scale. 17 | 3. Provide a way to "bloat up" with numerous randomly generated additional 18 | modules/features/components, up to the size of the largest Angular 19 | applications. 20 | 4. Initially use Angular CLI and Nx. 21 | 5. Later, Bazel. 22 | 23 | ## Contents so far 24 | 25 | * 3 applications, using overlapping sets of... 26 | * 10 libraries 27 | * dependencies between the libraries 28 | * A Node server, which serves REST/JSON, SSE, and GraphQL 29 | 30 | ## Technologies used 31 | 32 | * Angular 5 33 | * Angular CLI 34 | * Nx 35 | * NgRx/Store, Store addons 36 | * RxJS 37 | * Lodash, Moment 38 | * REST 39 | * SSE (Server Sent Events) 40 | * GraphQL 41 | 42 | ### Example application(s) 43 | 44 | This set of example applications/features use Nx to wire up inter-project 45 | dependencies during development. Following the Nx convention, they are divided 46 | into "apps" and "libs". 47 | 48 | There is a many-to-many relationship between applications and modules, and 49 | modules can use other modules. 50 | 51 | In addition, there is a "servers" directory intended to contain one or more 52 | server-side example code bases that support the Angular example. These are not 53 | managed using Nx, which is Angular specific. However, in a sprawling set of 54 | related servers and libraries, Lerna could be used too much the same effect. 55 | 56 | The example applications are not very complex - certainly not complex enough to 57 | warrant the amount of complexity used to build it. Real application of this 58 | modest complexity could easily be written as a single project (each). 59 | 60 | Still, the example applications reuse blocks of functionality, so they show the 61 | value of this multi-package approach. 62 | 63 | There are three application to run: 64 | 65 | * Admin - bundles 5 feature modules 66 | * Agent - bundles 2 feature module 67 | * Portal - bundles 1 feature module 68 | 69 | To understand how they are cross wired, look at the tsconfig.json file for each. 70 | 71 | Two of the modules use ngrx/store for state management, With appropriate lazy 72 | loading of feature modules. 73 | 74 | ### Running 75 | 76 | In one window: 77 | 78 | ``` 79 | yarn 80 | yarn start 81 | # add --app=agent or --app=portal if desired 82 | ``` 83 | 84 | In another window: 85 | 86 | ``` 87 | cd servers/node 88 | yarn 89 | yarn start 90 | ``` 91 | 92 | ## Contact us 93 | 94 | Main author: [Kyle Cordes](http://kylecordes.com/) 95 | 96 | Much help from the team at: [Oasis Digital](https://oasisdigital.com/) 97 | 98 | ... who teach [Angular Boot Camp](https://angularbootcamp.com/) 99 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/admin/README.md: -------------------------------------------------------------------------------- 1 | # Admin application 2 | 3 | This admin application has access to "all" of the features of this example set. 4 | It is intended to be, as the name says, like a Admin application. 5 | 6 | Although in a sense matters like load time may not matter in an admin 7 | application (they are particularly used by a smaller number of people, on faster 8 | hardware, for longer sessions, compared to other kinds of software), nonetheless 9 | this one has lazy loading of all features. 10 | 11 | Because essentially all of the features are delegated to libraries that are reused across this family related applications, there is very little code here. 12 | -------------------------------------------------------------------------------- /apps/admin/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('admin App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.text()).toContain('Welcome'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/admin/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | text() { 9 | return browser.findElement(by.css('body')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/admin/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc/e2e/admin", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | }, 13 | "include": [ 14 | "../**/*.ts" 15 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 16 | 17 | , "../../../libs/employee-list/index.ts" 18 | 19 | , "../../../libs/employee-management/index.ts" 20 | 21 | , "../../../libs/employee-search/index.ts" 22 | 23 | , "../../../libs/fruit-basket/index.ts" 24 | 25 | , "../../../libs/video-stat-dashboard/index.ts" 26 | ], 27 | "exclude": [ 28 | "**/*.spec.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /apps/admin/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/admin/src/app/app.component.css -------------------------------------------------------------------------------- /apps/admin/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /apps/admin/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('AppComponent', () => { 7 | let component: AppComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach( 11 | async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [RouterTestingModule], 14 | declarations: [AppComponent] 15 | }).compileComponents(); 16 | }) 17 | ); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(AppComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/admin/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /apps/admin/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AppComponent } from './app.component'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NxModule } from '@nrwl/nx'; 5 | import { RouterModule } from '@angular/router'; 6 | import { StoreModule } from '@ngrx/store'; 7 | import { EffectsModule } from '@ngrx/effects'; 8 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 9 | import { environment } from '../environments/environment'; 10 | // import { StoreRouterConnectingModule } from '@ngrx/router-store'; 11 | 12 | const ROUTES = [ 13 | { 14 | path: '', 15 | pathMatch: 'full', 16 | redirectTo: 'video-stat-dashboard' 17 | }, 18 | { 19 | path: 'employee-list', 20 | loadChildren: '@enterprise-example/employee-list#EmployeeListModule' 21 | }, 22 | { 23 | path: 'employee-management', 24 | loadChildren: '@enterprise-example/employee-management#EmployeeManagementModule' 25 | }, 26 | { 27 | path: 'employee-search', 28 | loadChildren: '@enterprise-example/employee-search#EmployeeSearchModule' 29 | }, 30 | { 31 | path: 'fruit-basket', 32 | loadChildren: '@enterprise-example/fruit-basket#FruitBasketModule' 33 | }, 34 | { 35 | path: 'video-stat-dashboard', 36 | loadChildren: '@enterprise-example/video-stat-dashboard#VideoStatDashboardModule' 37 | } 38 | ]; 39 | 40 | @NgModule({ 41 | imports: [ 42 | BrowserModule, 43 | NxModule.forRoot(), 44 | RouterModule.forRoot(ROUTES, { initialNavigation: 'enabled' }), 45 | StoreModule.forRoot({}), 46 | EffectsModule.forRoot([]), 47 | !environment.production ? StoreDevtoolsModule.instrument() : [], 48 | // StoreRouterConnectingModule 49 | ], 50 | declarations: [AppComponent], 51 | bootstrap: [AppComponent] 52 | }) 53 | export class AppModule { } 54 | -------------------------------------------------------------------------------- /apps/admin/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/admin/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/admin/src/assets/nx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/admin/src/assets/nx-logo.png -------------------------------------------------------------------------------- /apps/admin/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /apps/admin/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /apps/admin/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/admin/src/favicon.ico -------------------------------------------------------------------------------- /apps/admin/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin Application 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/admin/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /apps/admin/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/weak-map'; 35 | import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | import 'core-js/es6/reflect'; 42 | 43 | /** Evergreen browsers require these. **/ 44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 45 | import 'core-js/es7/reflect'; 46 | 47 | /** 48 | * Required to support Web Animations `@angular/platform-browser/animations`. 49 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 50 | **/ 51 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | /*************************************************************************************************** 59 | * APPLICATION IMPORTS 60 | */ 61 | 62 | /** 63 | * Date, currency, decimal and percent pipes. 64 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 65 | */ 66 | // import 'intl'; // Run `npm install --save intl`. 67 | /** 68 | * Need to import at least one locale-data with intl. 69 | */ 70 | // import 'intl/locale-data/jsonp/en'; 71 | -------------------------------------------------------------------------------- /apps/admin/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | select { 4 | display: inherit; 5 | } 6 | -------------------------------------------------------------------------------- /apps/admin/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc/apps/admin", 5 | "module": "es2015" 6 | }, 7 | "include": [ 8 | "**/*.ts" 9 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 10 | 11 | , "../../../libs/employee-list/index.ts" 12 | 13 | , "../../../libs/employee-management/index.ts" 14 | 15 | , "../../../libs/employee-search/index.ts" 16 | 17 | , "../../../libs/fruit-basket/index.ts" 18 | 19 | , "../../../libs/video-stat-dashboard/index.ts" 20 | ], 21 | "exclude": [ 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /apps/agent/README.md: -------------------------------------------------------------------------------- 1 | # Agent application 2 | 3 | This is the Agent application. It is intended to be used by a medium size number 4 | of people, for example all "agents" at a company. It has access to a partial 5 | subset of all the features of the system. 6 | 7 | Because essentially all of the features are delegated to libraries that are reused across this family related applications, there is very little code here. 8 | -------------------------------------------------------------------------------- /apps/agent/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('agent App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.text()).toContain('Welcome'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/agent/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | text() { 9 | return browser.findElement(by.css('body')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/agent/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc/e2e/agent", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | }, 13 | "include": [ 14 | "../**/*.ts" 15 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 16 | ], 17 | "exclude": [ 18 | "**/*.spec.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/agent/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/agent/src/app/app.component.css -------------------------------------------------------------------------------- /apps/agent/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /apps/agent/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('AppComponent', () => { 7 | let component: AppComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach( 11 | async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [RouterTestingModule], 14 | declarations: [AppComponent] 15 | }).compileComponents(); 16 | }) 17 | ); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(AppComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/agent/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /apps/agent/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AppComponent } from './app.component'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NxModule } from '@nrwl/nx'; 5 | import { RouterModule } from '@angular/router'; 6 | import { StoreModule } from '@ngrx/store'; 7 | import { EffectsModule } from '@ngrx/effects'; 8 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 9 | import { environment } from '../environments/environment'; 10 | // import { StoreRouterConnectingModule } from '@ngrx/router-store'; 11 | 12 | const ROUTES = [ 13 | { 14 | path: '', 15 | pathMatch: 'full', 16 | redirectTo: 'employee-search' 17 | }, 18 | { 19 | path: 'employee-list', 20 | loadChildren: '@enterprise-example/employee-list#EmployeeListModule' 21 | }, 22 | { 23 | path: 'employee-search', 24 | loadChildren: '@enterprise-example/employee-search#EmployeeSearchModule' 25 | } 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [BrowserModule, 30 | NxModule.forRoot(), 31 | RouterModule.forRoot(ROUTES, { initialNavigation: 'enabled' }), 32 | StoreModule.forRoot({}), 33 | EffectsModule.forRoot([]), 34 | !environment.production ? StoreDevtoolsModule.instrument() : [], 35 | // StoreRouterConnectingModule 36 | ], 37 | declarations: [AppComponent], 38 | bootstrap: [AppComponent] 39 | }) 40 | export class AppModule { } 41 | -------------------------------------------------------------------------------- /apps/agent/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/agent/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/agent/src/assets/nx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/agent/src/assets/nx-logo.png -------------------------------------------------------------------------------- /apps/agent/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /apps/agent/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /apps/agent/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/agent/src/favicon.ico -------------------------------------------------------------------------------- /apps/agent/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Agent 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/agent/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /apps/agent/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/weak-map'; 35 | import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | import 'core-js/es6/reflect'; 42 | 43 | /** Evergreen browsers require these. **/ 44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 45 | import 'core-js/es7/reflect'; 46 | 47 | /** 48 | * Required to support Web Animations `@angular/platform-browser/animations`. 49 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 50 | **/ 51 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | /*************************************************************************************************** 59 | * APPLICATION IMPORTS 60 | */ 61 | 62 | /** 63 | * Date, currency, decimal and percent pipes. 64 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 65 | */ 66 | // import 'intl'; // Run `npm install --save intl`. 67 | /** 68 | * Need to import at least one locale-data with intl. 69 | */ 70 | // import 'intl/locale-data/jsonp/en'; 71 | -------------------------------------------------------------------------------- /apps/agent/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/agent/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc/apps/agent", 5 | "module": "es2015" 6 | }, 7 | "include": [ 8 | "**/*.ts" 9 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 10 | 11 | , "../../../libs/employee-list/index.ts" 12 | 13 | , "../../../libs/employee-search/index.ts" 14 | ], 15 | "exclude": [ 16 | "**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/portal/README.md: -------------------------------------------------------------------------------- 1 | # Portal application 2 | 3 | This is the portal application. It is intended to be an example of the smallest 4 | subset of functionality, likely to be used by the largest number of people, 5 | among the family of related applications. 6 | 7 | Because essentially all of the features are delegated to libraries that are reused across this family related applications, there is very little code here. 8 | 9 | Although this small example is very similar to the agent and admin applications, 10 | it would be more realistic if this were offered with extensive mobile support, 11 | additional attention to load time and size, etc. 12 | -------------------------------------------------------------------------------- /apps/portal/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('portal App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.text()).toContain('Welcome'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/portal/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | text() { 9 | return browser.findElement(by.css('body')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/portal/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc/e2e/portal", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | }, 13 | "include": [ 14 | "../**/*.ts" 15 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 16 | ], 17 | "exclude": [ 18 | "**/*.spec.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/portal/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/portal/src/app/app.component.css -------------------------------------------------------------------------------- /apps/portal/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /apps/portal/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('AppComponent', () => { 7 | let component: AppComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach( 11 | async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [RouterTestingModule], 14 | declarations: [AppComponent] 15 | }).compileComponents(); 16 | }) 17 | ); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(AppComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/portal/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /apps/portal/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AppComponent } from './app.component'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NxModule } from '@nrwl/nx'; 5 | import { RouterModule, Route } from '@angular/router'; 6 | import { StoreModule } from '@ngrx/store'; 7 | import { EffectsModule } from '@ngrx/effects'; 8 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 9 | // import { StoreRouterConnectingModule } from '@ngrx/router-store'; 10 | 11 | import { environment } from '../environments/environment'; 12 | 13 | const ROUTES: Route[] = [ 14 | { 15 | path: '', 16 | pathMatch: 'full', 17 | redirectTo: 'emp-search' 18 | }, 19 | { 20 | path: 'emp-search', 21 | loadChildren: '@enterprise-example/employee-search#EmployeeSearchModule' 22 | } 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [ 27 | BrowserModule, 28 | NxModule.forRoot(), 29 | RouterModule.forRoot(ROUTES, 30 | { initialNavigation: 'enabled' }), 31 | StoreModule.forRoot({}), 32 | EffectsModule.forRoot([]), 33 | !environment.production ? StoreDevtoolsModule.instrument() : [], 34 | // StoreRouterConnectingModule 35 | ], 36 | declarations: [AppComponent], 37 | bootstrap: [AppComponent] 38 | }) 39 | export class AppModule { } 40 | -------------------------------------------------------------------------------- /apps/portal/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/portal/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/portal/src/assets/nx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/portal/src/assets/nx-logo.png -------------------------------------------------------------------------------- /apps/portal/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /apps/portal/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /apps/portal/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/apps/portal/src/favicon.ico -------------------------------------------------------------------------------- /apps/portal/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Employee Portal 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/portal/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /apps/portal/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/weak-map'; 35 | import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | import 'core-js/es6/reflect'; 42 | 43 | /** Evergreen browsers require these. **/ 44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 45 | import 'core-js/es7/reflect'; 46 | 47 | /** 48 | * Required to support Web Animations `@angular/platform-browser/animations`. 49 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 50 | **/ 51 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | /*************************************************************************************************** 59 | * APPLICATION IMPORTS 60 | */ 61 | 62 | /** 63 | * Date, currency, decimal and percent pipes. 64 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 65 | */ 66 | // import 'intl'; // Run `npm install --save intl`. 67 | /** 68 | * Need to import at least one locale-data with intl. 69 | */ 70 | // import 'intl/locale-data/jsonp/en'; 71 | -------------------------------------------------------------------------------- /apps/portal/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/portal/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc/apps/portal", 5 | "module": "es2015" 6 | }, 7 | "include": [ 8 | "**/*.ts" 9 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 10 | 11 | , "../../../libs/employee-search/index.ts" 12 | ], 13 | "exclude": [ 14 | "**/*.spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | yarn run ng build --prod -a portal 5 | yarn run ng build --prod -a agent 6 | yarn run ng build --prod -a admin 7 | 8 | yarn run lint 9 | 10 | yarn run compodoc 11 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | const { makeSureNoAppIsSelected } = require('@nrwl/schematics/src/utils/cli-config-utils'); 5 | // Nx only supports running unit tests for all apps and libs. 6 | makeSureNoAppIsSelected(); 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | basePath: '', 11 | frameworks: ['jasmine', '@angular/cli'], 12 | plugins: [ 13 | require('karma-jasmine'), 14 | require('karma-chrome-launcher'), 15 | require('karma-jasmine-html-reporter'), 16 | require('karma-coverage-istanbul-reporter'), 17 | require('@angular/cli/plugins/karma') 18 | ], 19 | client:{ 20 | clearContext: false // leave Jasmine Spec Runner output visible in browser 21 | }, 22 | coverageIstanbulReporter: { 23 | reports: [ 'html', 'lcovonly' ], 24 | fixWebpackSourcePaths: true 25 | }, 26 | angularCli: { 27 | environment: 'dev' 28 | }, 29 | reporters: ['progress', 'kjhtml'], 30 | port: 9876, 31 | colors: true, 32 | logLevel: config.LOG_INFO, 33 | autoWatch: true, 34 | browsers: ['Chrome'], 35 | singleRun: false 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OasisDigital/angular-enterprise-example/b64e6552f0d8f0e648c6b6fdcf6b8e98d7706aa4/libs/.gitkeep -------------------------------------------------------------------------------- /libs/age-range/index.ts: -------------------------------------------------------------------------------- 1 | export { AgeRangeModule } from './src/age-range.module'; 2 | export { ageRanges } from './src/age-ranges'; 3 | -------------------------------------------------------------------------------- /libs/age-range/src/age-range.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { AgeRangeModule } from './age-range.module'; 2 | 3 | describe('AgeRangeModule', () => { 4 | it('should work', () => { 5 | expect(new AgeRangeModule()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/age-range/src/age-range.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { EnumToRangePipe } from './enum-to-range.pipe'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | EnumToRangePipe 8 | ], 9 | exports: [ 10 | EnumToRangePipe 11 | ] 12 | }) 13 | export class AgeRangeModule { } 14 | -------------------------------------------------------------------------------- /libs/age-range/src/age-ranges.ts: -------------------------------------------------------------------------------- 1 | // Named like a variable because it might become configurable in the future. 2 | // Data is better than code. 3 | 4 | export const ageRanges = [ 5 | { label: 'Under 18', lower: 0, upper: 18 }, 6 | { label: '18 - 39', lower: 18, upper: 40 }, 7 | { label: '40 - 59', lower: 40, upper: 60 }, 8 | { label: '60 and up', lower: 60, upper: 999 } 9 | ]; 10 | -------------------------------------------------------------------------------- /libs/age-range/src/enum-to-range.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | import { ageRanges } from './age-ranges'; 4 | 5 | @Pipe({ 6 | name: 'enumToRange' 7 | }) 8 | export class EnumToRangePipe implements PipeTransform { 9 | 10 | transform(value: any, _args?: any): any { 11 | try { 12 | const index = parseInt(value, 10); 13 | return ageRanges[index].label; 14 | } catch (error) { 15 | return null; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/app-schema/README.md: -------------------------------------------------------------------------------- 1 | # The schema of the application (example) 2 | 3 | This reusable library is intended to express the idea that a family of related 4 | applications typically shares a certain amount of "schema". Within Angular at 5 | least, this usually takes the form of a set of typescript types, although it may 6 | also take the form of something like a GraphQL schema. 7 | 8 | This is the smallest of the example libraries, and has the greatest number of 9 | other libraries that depend on it; that is typical of a somewhat reasonable 10 | dependency graph. 11 | -------------------------------------------------------------------------------- /libs/app-schema/index.ts: -------------------------------------------------------------------------------- 1 | export { IEmployeeListing } from './src/employee-listing'; 2 | export { IEmployee } from './src/employee'; 3 | -------------------------------------------------------------------------------- /libs/app-schema/src/employee-listing.ts: -------------------------------------------------------------------------------- 1 | export interface IEmployeeListing { 2 | id: number; 3 | first_name: string; 4 | last_name: string; 5 | } 6 | -------------------------------------------------------------------------------- /libs/app-schema/src/employee.ts: -------------------------------------------------------------------------------- 1 | import { IEmployeeListing } from './employee-listing'; 2 | 3 | export interface IEmployee extends IEmployeeListing { 4 | email: string; 5 | hours_worked: number; 6 | hourly_wage: number; 7 | } 8 | -------------------------------------------------------------------------------- /libs/employee-api/index.ts: -------------------------------------------------------------------------------- 1 | export { EmployeeApiModule } from './src/employee-api.module'; 2 | export { EmployeeApi } from './src/employee-api.service'; 3 | -------------------------------------------------------------------------------- /libs/employee-api/src/employee-api.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeApiModule } from './employee-api.module'; 2 | 3 | describe('EmployeeApiModule', () => { 4 | it('should work', () => { 5 | expect(new EmployeeApiModule()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/employee-api/src/employee-api.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | 5 | import { EmployeeApi } from './employee-api.service'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | HttpClientModule 11 | ], 12 | providers: [EmployeeApi] 13 | }) 14 | export class EmployeeApiModule { } 15 | -------------------------------------------------------------------------------- /libs/employee-api/src/employee-api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { delayWhen } from 'rxjs/operators'; 5 | import { of } from 'rxjs/observable/of'; 6 | 7 | import { IEmployee, IEmployeeListing } from '@enterprise-example/app-schema'; 8 | 9 | const API_URL = '/api'; 10 | const EMP_URL = API_URL + '/employees'; 11 | 12 | // Configure the amount of latency and jitter to simulate 13 | const API_LATENCY = 50; 14 | 15 | // Set to 3000 to see that out-of-order replies don't cause any problem: 16 | const API_JITTER = 250; 17 | 18 | // randomDelay() can be piped into an observable chain to add a (not 19 | // surprisingly) random delay to each value flowing through the observable. Note 20 | // that the delay must be calculated randomly for each value; otherwise the 21 | // notion of "jitter" would not work. 22 | 23 | function randomDelay() { 24 | return delayWhen(_x => of(Math.round(API_LATENCY + Math.random() * API_JITTER))); 25 | } 26 | 27 | @Injectable() 28 | export class EmployeeApi { 29 | constructor(private http: HttpClient) { } 30 | 31 | listAll(): Observable { 32 | return this.http.get(EMP_URL) 33 | .pipe(randomDelay()); 34 | } 35 | 36 | listFiltered(searchText: string): Observable { 37 | const params = new HttpParams() 38 | .set('q', searchText) 39 | .set('_limit', '20'); 40 | return this.http.get(EMP_URL, { params }) 41 | .pipe(randomDelay()); 42 | } 43 | 44 | loadOne(employeeId: number): Observable { 45 | return this.http.get(`${EMP_URL}/${employeeId}`) 46 | .pipe(randomDelay()); 47 | } 48 | 49 | save(employee: IEmployee) { 50 | return this.http.put(`${EMP_URL}/${employee.id}`, employee) 51 | .pipe(randomDelay()); 52 | } 53 | 54 | saveNew(employee: IEmployee) { 55 | return this.http.post(EMP_URL, employee) 56 | .pipe(randomDelay()); 57 | } 58 | 59 | delete(id: number) { 60 | return this.http.delete(`${EMP_URL}/${id}`) 61 | .pipe(randomDelay()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /libs/employee-display/index.ts: -------------------------------------------------------------------------------- 1 | export { EmployeeDisplayModule } from './src/employee-display.module'; 2 | -------------------------------------------------------------------------------- /libs/employee-display/src/employee-detail-view/employee-detail-view.component.html: -------------------------------------------------------------------------------- 1 | {{employee?.first_name}} {{employee?.last_name}} 2 |

Email: {{employee?.email}}

3 |

Hours Worked: {{employee?.hours_worked}}

4 |

Hourly Wage: {{employee?.hourly_wage}}

5 | -------------------------------------------------------------------------------- /libs/employee-display/src/employee-detail-view/employee-detail-view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { IEmployee } from '@enterprise-example/app-schema'; 4 | 5 | @Component({ 6 | selector: 'employee-detail-view', 7 | templateUrl: './employee-detail-view.component.html' 8 | }) 9 | export class EmployeeDetailViewComponent { 10 | @Input() employee: IEmployee; 11 | } 12 | -------------------------------------------------------------------------------- /libs/employee-display/src/employee-display.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeDisplayModule } from './employee-display.module'; 2 | 3 | describe('EmployeeDisplayModule', () => { 4 | it('should work', () => { 5 | expect(new EmployeeDisplayModule()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/employee-display/src/employee-display.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { EmployeeDetailViewComponent } from './employee-detail-view/employee-detail-view.component'; 6 | import { EmployeeListTableViewComponent } from './employee-list-table-view/employee-list-table-view.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | EmployeeDetailViewComponent, 11 | EmployeeListTableViewComponent 12 | ], 13 | imports: [ 14 | CommonModule, 15 | ReactiveFormsModule 16 | ], 17 | exports: [ 18 | EmployeeDetailViewComponent, 19 | EmployeeListTableViewComponent 20 | ] 21 | }) 22 | export class EmployeeDisplayModule { } 23 | -------------------------------------------------------------------------------- /libs/employee-display/src/employee-list-table-view/employee-list-table-view.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 15 |
6 | {{employee.first_name}} 7 | 9 | {{employee.last_name}} 10 | 12 | {{employee.email}} 13 |
16 | 17 | 20 | -------------------------------------------------------------------------------- /libs/employee-display/src/employee-list-table-view/employee-list-table-view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | import { IEmployeeListing } from '@enterprise-example/app-schema'; 4 | 5 | @Component({ 6 | selector: 'employee-list-table-view', 7 | templateUrl: './employee-list-table-view.component.html' 8 | }) 9 | export class EmployeeListTableViewComponent { 10 | @Input() list: IEmployeeListing[]; 11 | @Input() selectedId: number; 12 | @Output() selectId = new EventEmitter(); 13 | 14 | noop(e: MouseEvent) { 15 | e.preventDefault(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/employee-list/index.ts: -------------------------------------------------------------------------------- 1 | export { EmployeeListModule } from './src/employee-list.module'; 2 | -------------------------------------------------------------------------------- /libs/employee-list/src/employee-list.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule, Route } from '@angular/router'; 4 | 5 | import { EmployeeDisplayModule } from '@enterprise-example/employee-display'; 6 | import { EmployeeApiModule } from '@enterprise-example/employee-api'; 7 | 8 | import { EmployeeListComponent } from './employee-list/employee-list.component'; 9 | 10 | const ROUTES: Route[] = [ 11 | { path: '', component: EmployeeListComponent } 12 | ]; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | EmployeeListComponent 17 | ], 18 | imports: [ 19 | EmployeeDisplayModule, 20 | EmployeeApiModule, 21 | CommonModule, 22 | RouterModule.forChild(ROUTES) 23 | ] 24 | }) 25 | export class EmployeeListModule { } 26 | -------------------------------------------------------------------------------- /libs/employee-list/src/employee-list/employee-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
Employees
6 | 7 | 10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |
Employee detail
19 |

Loading status: {{ status | async }}

20 | 23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /libs/employee-list/src/employee-list/employee-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Subject } from 'rxjs/Subject'; 4 | import { filter, map, share } from 'rxjs/operators'; 5 | 6 | import { EmployeeApi } from '@enterprise-example/employee-api'; 7 | import { IEmployeeListing } from '@enterprise-example/app-schema'; 8 | import { StatusStrings, LoadResultStatus, loadWithRetry, faulty } from '@enterprise-example/retry-loader'; 9 | 10 | @Component({ 11 | selector: 'employee-list', 12 | templateUrl: './employee-list.component.html' 13 | }) 14 | export class EmployeeListComponent { 15 | selectedEmployee: Observable; 16 | status: Observable; 17 | selectedEmployeeId = new Subject(); 18 | employees: Observable; 19 | showEmployeeDetails: Observable; 20 | 21 | constructor(api: EmployeeApi) { 22 | this.employees = api.listAll(); 23 | 24 | const loadResults = loadWithRetry( 25 | this.selectedEmployeeId, 26 | id => api.loadOne(id) 27 | .pipe(faulty()) // simulate bad connection 28 | ).pipe(share()); 29 | 30 | this.status = loadResults 31 | .pipe(map(result => StatusStrings[result.status])); 32 | 33 | this.showEmployeeDetails = loadResults 34 | .pipe(map(result => result.status === LoadResultStatus.Success)); 35 | 36 | this.selectedEmployee = loadResults 37 | .pipe(filter(result => result.status === LoadResultStatus.Success), 38 | map(result => result.data)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs/employee-management/index.ts: -------------------------------------------------------------------------------- 1 | export { EmployeeManagementModule } from './src/employee-management.module'; 2 | -------------------------------------------------------------------------------- /libs/employee-management/src/add-employee/add-employee.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Add employee
4 | 5 |
6 | 7 | 8 | 10 | 12 |
13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /libs/employee-management/src/add-employee/add-employee.component.ts: -------------------------------------------------------------------------------- 1 | import { Component} from '@angular/core'; 2 | import { FormGroup, FormBuilder } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | 5 | import { EmployeeApi } from '@enterprise-example/employee-api'; 6 | 7 | import { EmployeeNavigation } from '../employee-navigation.service'; 8 | 9 | @Component({ 10 | selector: 'add-employee', 11 | templateUrl: './add-employee.component.html' 12 | }) 13 | export class AddEmployeeComponent { 14 | containerFormGroup: FormGroup; 15 | 16 | constructor(private api: EmployeeApi, private nav: EmployeeNavigation, 17 | fb: FormBuilder, route: ActivatedRoute) { 18 | this.nav.calculateModuleBaseRoute(route); 19 | this.containerFormGroup = fb.group({}); 20 | } 21 | 22 | cancelClicked() { 23 | this.nav.list(); 24 | } 25 | 26 | saveClicked() { 27 | this.api.saveNew({ 28 | ...this.containerFormGroup.value.employee 29 | }).subscribe(_x => this.nav.list()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/employee-management/src/edit-employee/edit-employee.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Edit employee
4 | 5 |
6 | 7 | 8 | 10 | 12 | 14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /libs/employee-management/src/edit-employee/edit-employee.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | import { FormGroup, FormBuilder } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Subscription } from 'rxjs/Subscription'; 5 | import { switchMap, share } from 'rxjs/operators'; 6 | 7 | import { EmployeeApi } from '@enterprise-example/employee-api'; 8 | 9 | import { EmployeeNavigation } from '../employee-navigation.service'; 10 | 11 | @Component({ 12 | selector: 'edit-employee', 13 | templateUrl: './edit-employee.component.html' 14 | }) 15 | export class EditEmployeeComponent implements OnDestroy { 16 | containerFormGroup: FormGroup; 17 | sub: Subscription; 18 | id: number; 19 | 20 | constructor(private nav: EmployeeNavigation, 21 | private api: EmployeeApi, fb: FormBuilder, route: ActivatedRoute) { 22 | this.nav.calculateModuleBaseRoute(route); 23 | this.containerFormGroup = fb.group({}); 24 | 25 | const employee$ = nav.employeeId(route) 26 | .pipe(switchMap(id => api.loadOne(id)), 27 | share()); 28 | 29 | this.sub = employee$.subscribe(e => { 30 | this.id = e.id; 31 | const { first_name, last_name, email, hourly_wage } = e; 32 | const hours_worked = e.hours_worked || 0; 33 | this.containerFormGroup.setValue({ 34 | employee: { first_name, last_name, email, hourly_wage, hours_worked } 35 | }); 36 | }); 37 | } 38 | 39 | ngOnDestroy() { 40 | this.sub.unsubscribe(); 41 | } 42 | 43 | cancelClicked() { 44 | this.nav.list(); 45 | } 46 | 47 | saveClicked() { 48 | this.api.save({ 49 | ...this.containerFormGroup.value.employee, 50 | id: this.id 51 | }).subscribe(_x => this.nav.list()); 52 | } 53 | 54 | deleteClicked() { 55 | this.api.delete(this.id).subscribe(_x => this.nav.list()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /libs/employee-management/src/employee-fields/employee-fields.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 | 12 |
Please enter a name
13 |
14 | 15 |
16 | 17 | 21 |
Please enter a name
22 |
23 | 24 |
25 | 26 | 30 |
Please enter email
31 |
32 | 33 |
34 | 35 | 39 |
Please enter wage
40 |
41 | 42 |
43 | 44 | 48 |
Please enter hours
49 |
50 |
51 | -------------------------------------------------------------------------------- /libs/employee-management/src/employee-fields/employee-fields.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'employee-fields', 6 | templateUrl: './employee-fields.component.html' 7 | }) 8 | export class EmployeeFieldsComponent implements OnInit { 9 | @Input() parentFormGroup: FormGroup; 10 | 11 | fg: FormGroup; 12 | 13 | constructor(fb: FormBuilder) { 14 | this.fg = fb.group({ 15 | 'first_name': ['', Validators.required], 16 | 'last_name': ['', [Validators.required, Validators.minLength(3)]], 17 | 'email': [''], 18 | 'hours_worked': ['', [Validators.required, Validators.pattern('[0-9]+')]], 19 | 'hourly_wage': ['', [Validators.required, Validators.pattern('[0-9]+')]] 20 | }); 21 | } 22 | 23 | ngOnInit() { 24 | this.parentFormGroup.addControl('employee', this.fg); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/employee-management/src/employee-management.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeManagementModule } from './employee-management.module'; 2 | 3 | describe('EmployeeManagementModule', () => { 4 | it('should work', () => { 5 | expect(new EmployeeManagementModule()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/employee-management/src/employee-management.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { EmployeeDisplayModule } from '@enterprise-example/employee-display'; 6 | import { EmployeeApiModule } from '@enterprise-example/employee-api'; 7 | 8 | import { ROUTER_MODULE, ROUTED_COMPONENTS } from './employee-management.routes'; 9 | import { EmployeeNavigation } from './employee-navigation.service'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | ...ROUTED_COMPONENTS 14 | ], 15 | imports: [ 16 | EmployeeDisplayModule, 17 | CommonModule, 18 | ReactiveFormsModule, 19 | ROUTER_MODULE, 20 | EmployeeApiModule 21 | ], 22 | providers: [ 23 | EmployeeNavigation 24 | ] 25 | }) 26 | export class EmployeeManagementModule { } 27 | -------------------------------------------------------------------------------- /libs/employee-management/src/employee-management.routes.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule, Route } from '@angular/router'; 2 | 3 | import { AddEmployeeComponent } from './add-employee/add-employee.component'; 4 | import { EditEmployeeComponent } from './edit-employee/edit-employee.component'; 5 | import { EmployeeFieldsComponent } from './employee-fields/employee-fields.component'; 6 | import { ManagementScreenComponent } from './management-screen/management-screen.component'; 7 | 8 | const ROUTES: Route[] = [ 9 | { path: 'add', component: AddEmployeeComponent }, 10 | { path: ':id', component: EditEmployeeComponent }, 11 | { path: '', component: ManagementScreenComponent } 12 | ]; 13 | 14 | export const ROUTER_MODULE = RouterModule.forChild(ROUTES); 15 | 16 | export const ROUTED_COMPONENTS = [ 17 | ManagementScreenComponent, 18 | EmployeeFieldsComponent, 19 | AddEmployeeComponent, 20 | EditEmployeeComponent 21 | ]; 22 | -------------------------------------------------------------------------------- /libs/employee-management/src/employee-navigation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class EmployeeNavigation { 8 | private baseRoute: ActivatedRoute; 9 | 10 | constructor(private router: Router) { } 11 | 12 | calculateModuleBaseRoute(route: ActivatedRoute) { 13 | this.baseRoute = route.pathFromRoot[1]; 14 | } 15 | 16 | employeeId(componentRoute: ActivatedRoute): Observable { 17 | return componentRoute.params 18 | .pipe(map(params => params['id'])); 19 | } 20 | 21 | list() { 22 | this.router.navigate(['.'], { relativeTo: this.baseRoute }); 23 | } 24 | 25 | add() { 26 | this.router.navigate(['add'], { relativeTo: this.baseRoute }); 27 | } 28 | 29 | edit(id: number) { 30 | this.router.navigate([id], { relativeTo: this.baseRoute }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/employee-management/src/management-screen/management-screen.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
Employee management
6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /libs/employee-management/src/management-screen/management-screen.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { sortBy } from 'lodash'; 6 | import { combineLatest } from 'rxjs/observable/combineLatest' 7 | import { startWith, debounceTime, switchMap } from 'rxjs/operators' 8 | 9 | import { EmployeeApi } from '@enterprise-example/employee-api'; 10 | import { IEmployee } from '@enterprise-example/app-schema'; 11 | 12 | import { EmployeeNavigation } from '../employee-navigation.service'; 13 | 14 | @Component({ 15 | selector: 'management-screen', 16 | templateUrl: './management-screen.component.html' 17 | }) 18 | export class ManagementScreenComponent { 19 | nameFilter = new FormControl(''); 20 | sort = new FormControl('last_name'); 21 | filteredList: Observable; 22 | 23 | constructor(api: EmployeeApi, private nav: EmployeeNavigation, route: ActivatedRoute) { 24 | this.nav.calculateModuleBaseRoute(route); 25 | 26 | const nameFilter$ = this.nameFilter.valueChanges.pipe( 27 | startWith(this.nameFilter.value), 28 | debounceTime(250)); 29 | const sort$ = this.sort.valueChanges.pipe( 30 | startWith(this.sort.value)); 31 | 32 | // List reacts to filter and sort changes 33 | this.filteredList = combineLatest( 34 | nameFilter$.pipe(switchMap(x => api.listFiltered(x))), 35 | sort$, 36 | (list, sort) => sortBy(list, sort) 37 | ); 38 | } 39 | 40 | addClicked() { 41 | this.nav.add(); 42 | } 43 | 44 | select(id: number) { 45 | this.nav.edit(id); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/employee-search/index.ts: -------------------------------------------------------------------------------- 1 | export { EmployeeSearchModule } from './src/employee-search.module'; 2 | -------------------------------------------------------------------------------- /libs/employee-search/src/employee-list/employee-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
Employees
6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 18 |
19 |
20 | 21 | 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
Employee detail
33 | 34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /libs/employee-search/src/employee-list/employee-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Subject } from 'rxjs/Subject'; 4 | import { FormControl } from '@angular/forms'; 5 | import { sortBy } from 'lodash'; 6 | import { combineLatest } from 'rxjs/observable/combineLatest' 7 | import { debounceTime, publishReplay, startWith, switchMap, refCount } from 'rxjs/operators' 8 | 9 | import { EmployeeApi } from '@enterprise-example/employee-api'; 10 | import { IEmployeeListing } from '@enterprise-example/app-schema'; 11 | 12 | @Component({ 13 | selector: 'employee-list', 14 | templateUrl: './employee-list.component.html' 15 | }) 16 | export class EmployeeListComponent { 17 | nameFilter = new FormControl(''); 18 | sort = new FormControl('last_name'); 19 | 20 | filteredList: Observable; 21 | selectedId = new Subject(); 22 | selectedEmployee: Observable; 23 | 24 | constructor(api: EmployeeApi) { 25 | // .valueChanges is missing the initial value; add it: 26 | const nameFilter$ = this.nameFilter.valueChanges 27 | .pipe(startWith(this.nameFilter.value)); 28 | const sort$ = this.sort.valueChanges 29 | .pipe(startWith(this.sort.value)); 30 | 31 | // List reacts to filter and sort changes 32 | this.filteredList = combineLatest( 33 | nameFilter$.pipe( 34 | debounceTime(250), 35 | switchMap(x => api.listFiltered(x)) 36 | ), 37 | sort$, 38 | (list, sort) => sortBy(list, sort)); 39 | 40 | // Detail reacts to selected employee changes 41 | this.selectedEmployee = this.selectedId 42 | .pipe(switchMap(id => api.loadOne(id)), 43 | publishReplay(1), 44 | refCount()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /libs/employee-search/src/employee-search.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { RouterModule, Route } from '@angular/router'; 5 | 6 | import { EmployeeDisplayModule } from '@enterprise-example/employee-display'; 7 | import { EmployeeApiModule } from '@enterprise-example/employee-api'; 8 | 9 | import { EmployeeListComponent } from './employee-list/employee-list.component'; 10 | 11 | const ROUTES: Route[] = [ 12 | { path: '', component: EmployeeListComponent } 13 | ]; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | EmployeeListComponent 18 | ], 19 | imports: [ 20 | EmployeeDisplayModule, 21 | CommonModule, 22 | ReactiveFormsModule, 23 | EmployeeApiModule, 24 | RouterModule.forChild(ROUTES) 25 | ], 26 | providers: [] 27 | }) 28 | export class EmployeeSearchModule { } 29 | -------------------------------------------------------------------------------- /libs/fruit-basket/index.ts: -------------------------------------------------------------------------------- 1 | export { FruitBasketModule } from './src/fruit-basket.module'; 2 | -------------------------------------------------------------------------------- /libs/fruit-basket/src/basket-ui/basket-ui.component.html: -------------------------------------------------------------------------------- 1 |

Fruit Basket

2 | 3 | 4 |

Total Fruit: {{ total | async }}

5 | 6 |
7 | 12 | 13 | 14 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /libs/fruit-basket/src/basket-ui/basket-ui.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { PickBerryAction, AppState, PickApplesAction, EmptyCartAction } from '../state/state'; 6 | 7 | @Component({ 8 | selector: 'basket-ui', 9 | templateUrl: './basket-ui.component.html' 10 | }) 11 | export class BasketUiComponent { 12 | berry: Observable; 13 | apple: Observable; 14 | total: Observable; 15 | 16 | constructor(public store: Store) { 17 | this.berry = store.select(state => state.fruit.berryCounter); 18 | this.apple = store.select(state => state.fruit.appleCounter); 19 | this.total = store.select(state => state.fruit.berryCounter + state.fruit.appleCounter); 20 | } 21 | 22 | pickBerry() { 23 | this.store.dispatch(new PickBerryAction()); 24 | } 25 | 26 | pickApple(count: number) { 27 | this.store.dispatch(new PickApplesAction(count)); 28 | } 29 | 30 | empty() { 31 | this.store.dispatch(new EmptyCartAction()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /libs/fruit-basket/src/counter-display/counter-display.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
{{label}}
4 | Current total: {{ counter }} 5 |
6 |
7 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /libs/fruit-basket/src/counter-display/counter-display.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'counter-display', 5 | templateUrl: './counter-display.component.html' 6 | }) 7 | export class CounterDisplayComponent { 8 | @Input() label: string; 9 | @Input() counter: number; 10 | @Output() pick = new EventEmitter(); 11 | } 12 | -------------------------------------------------------------------------------- /libs/fruit-basket/src/fruit-basket.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, InjectionToken } from '@angular/core'; 2 | import { RouterModule, Route } from '@angular/router'; 3 | import { CommonModule } from '@angular/common'; 4 | import { StoreModule, ActionReducerMap } from '@ngrx/store'; 5 | 6 | import { BasketUiComponent } from './basket-ui/basket-ui.component'; 7 | import { CounterDisplayComponent } from './counter-display/counter-display.component'; 8 | import { fruitReducerMap, FruitState } from './state/state'; 9 | 10 | // The verbosity that replaced combineReducers is documented here: 11 | // https://github.com/ngrx/platform/blob/master/docs/store/api.md#injecting-reducers 12 | 13 | export const FEATURE_REDUCER_TOKEN = 14 | new InjectionToken>('Feature Reducers'); 15 | 16 | export function getReducers(): ActionReducerMap { 17 | return fruitReducerMap; 18 | } 19 | 20 | const ROUTES: Route[] = [ 21 | { path: '', pathMatch: 'full', component: BasketUiComponent } 22 | ]; 23 | 24 | @NgModule({ 25 | declarations: [ 26 | BasketUiComponent, 27 | CounterDisplayComponent 28 | ], 29 | imports: [ 30 | CommonModule, 31 | StoreModule.forFeature('fruit', FEATURE_REDUCER_TOKEN), 32 | RouterModule.forChild(ROUTES) 33 | ], 34 | providers: [ 35 | { 36 | provide: FEATURE_REDUCER_TOKEN, 37 | useFactory: getReducers 38 | } 39 | ] 40 | }) 41 | export class FruitBasketModule { } 42 | -------------------------------------------------------------------------------- /libs/fruit-basket/src/state/state.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | // This example shows an older approach, which uses classes that are nonetheless 4 | // acceptable in the sense of being serializable and de-serializable, because 5 | // their class type/prototype does not actually matter. Just the data inside. 6 | 7 | export interface AppState { 8 | fruit: FruitState; 9 | } 10 | 11 | export interface FruitState { 12 | berryCounter: number; 13 | appleCounter: number; 14 | } 15 | 16 | const PICK_BERRY = 'PICK_BERRY'; 17 | export class PickBerryAction implements Action { 18 | type = PICK_BERRY; 19 | } 20 | 21 | const PICK_APPLES = 'PICK_APPLES'; 22 | export class PickApplesAction implements Action { 23 | type = PICK_APPLES; 24 | constructor(public payload: number) { } 25 | } 26 | 27 | const EMPTY_CART = 'EMPTY_CART'; 28 | export class EmptyCartAction implements Action { 29 | type = EMPTY_CART; 30 | } 31 | 32 | function berryCounterReducer 33 | (value: number = 0, action: Action): number { 34 | switch (action.type) { 35 | case PICK_BERRY: 36 | return value + 1; 37 | 38 | case EMPTY_CART: 39 | return 0; 40 | 41 | default: 42 | return value; 43 | } 44 | } 45 | 46 | function appleCounterReducer(value: number = 0, action: Action): number { 47 | switch (action.type) { 48 | // If you have too many apples, they spill and you lose them all. 49 | case PICK_APPLES: 50 | const apples = value + (action as PickApplesAction).payload; 51 | return apples > 10 ? 0 : apples; 52 | 53 | case EMPTY_CART: 54 | return 0; 55 | 56 | default: 57 | return value; 58 | } 59 | } 60 | 61 | export const fruitReducerMap = { 62 | berryCounter: berryCounterReducer, 63 | appleCounter: appleCounterReducer 64 | }; 65 | -------------------------------------------------------------------------------- /libs/retry-loader/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/load-with-retry'; 2 | export * from './src/faulty'; 3 | -------------------------------------------------------------------------------- /libs/retry-loader/src/faulty.ts: -------------------------------------------------------------------------------- 1 | // This observable transformation can be used to simulate a faulty 2 | // network or backend service; it adds random delays and random 3 | // failure probability. 4 | 5 | // Wire it in to an observable stream with .let(). 6 | 7 | import { Observable } from 'rxjs/Observable'; 8 | import { defer } from 'rxjs/observable/defer'; 9 | import { _throw } from 'rxjs/observable/throw'; 10 | import { timer } from 'rxjs/observable/timer'; 11 | import { flatMap } from 'rxjs/operators'; 12 | 13 | export interface IFaultyOptions { 14 | errorProbability?: number; 15 | maxDelayMs?: number; 16 | } 17 | 18 | const DEFAULT_OPTIONS = { 19 | errorProbability: 0.3, 20 | maxDelayMs: 1000 21 | }; 22 | 23 | export function faulty(options?: IFaultyOptions): (source: Observable) => Observable { 24 | options = Object.assign({}, DEFAULT_OPTIONS, options); 25 | return (source) => defer(() => { 26 | return timer(Math.random() * options.maxDelayMs) 27 | .pipe(flatMap(_value => 28 | (Math.random() < options.errorProbability) ? 29 | _throw(new Error('Failed in faulty')) : 30 | source)); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /libs/retry-loader/src/load-with-retry.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import { Subject } from 'rxjs/Subject'; 3 | import { merge } from 'rxjs/observable/merge'; 4 | import { of } from 'rxjs/observable/of'; 5 | import { defer } from 'rxjs/observable/defer'; 6 | import { timer } from 'rxjs/observable/timer'; 7 | import { map, retryWhen, switchMap, tap, filter, delayWhen } from 'rxjs/operators'; 8 | 9 | export enum LoadResultStatus { 10 | InProgress, 11 | Retrying, 12 | Waiting, 13 | Success, 14 | Error 15 | } 16 | 17 | export const StatusStrings = [ 18 | 'In Progress', 19 | 'Retrying', 20 | 'Waiting to Retry', 21 | 'Success', 22 | 'Error' 23 | ]; 24 | 25 | export interface LoadResult { 26 | status: LoadResultStatus; 27 | data?: T; 28 | error?: any; 29 | willRetry?: boolean; 30 | } 31 | 32 | export interface LoadWithRetryOptions { 33 | // To retry once after failure, use attempts=2 34 | attempts?: number; 35 | retryDelayMs?: number; 36 | retryBackoffCoefficient?: number; 37 | retryMaxDelayMs?: number; 38 | } 39 | 40 | const DEFAULT_OPTIONS: LoadWithRetryOptions = { 41 | attempts: 3, 42 | retryDelayMs: 2000, 43 | retryBackoffCoefficient: 1.5, 44 | retryMaxDelayMs: 30000 45 | }; 46 | 47 | export function loadWithRetry( 48 | source: Observable, 49 | producer: (key: S) => Observable, 50 | options?: LoadWithRetryOptions 51 | ): Observable> { 52 | options = Object.assign({}, DEFAULT_OPTIONS, options); 53 | 54 | return source.pipe(switchMap(key => { 55 | const statusUpdates = new Subject>(); 56 | let attempt = 0; 57 | return merge( 58 | of({ status: LoadResultStatus.InProgress }), 59 | statusUpdates, 60 | defer(() => { 61 | attempt++; 62 | return producer(key); 63 | }) 64 | .pipe(retryWhen(errors => errors.pipe( 65 | tap(error => 66 | statusUpdates.next({ 67 | status: LoadResultStatus.Error, 68 | error, 69 | willRetry: attempt < options.attempts 70 | })), 71 | filter(_ => attempt < options.attempts), 72 | tap(_ => statusUpdates.next({ status: LoadResultStatus.Waiting })), 73 | delayWhen(() => retryDelay(options, attempt)), 74 | tap(_ => statusUpdates.next({ status: LoadResultStatus.Retrying })) 75 | )), 76 | map((data: T) => ({ status: LoadResultStatus.Success, data }))) 77 | ); 78 | })); 79 | } 80 | 81 | function retryDelay(options: LoadWithRetryOptions, attempt: number): Observable { 82 | const jitter = (Math.random() - .5) * options.retryDelayMs * .5; 83 | let delay = options.retryDelayMs * Math.pow(options.retryBackoffCoefficient, attempt - 1) + jitter; 84 | delay = Math.min(delay, options.retryMaxDelayMs); 85 | return timer(delay); 86 | } 87 | -------------------------------------------------------------------------------- /libs/retry-loader/src/retry-loader.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { RetryLoaderModule } from './retry-loader.module'; 2 | 3 | describe('RetryLoaderModule', () => { 4 | it('should work', () => { 5 | expect(new RetryLoaderModule()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/retry-loader/src/retry-loader.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule] 6 | }) 7 | export class RetryLoaderModule {} 8 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export { VideoStatDashboardModule } from './src/video-stat-dashboard.module'; 2 | 3 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/+state/video-stats.actions.ts: -------------------------------------------------------------------------------- 1 | import { Video } from './video-stats.interfaces'; 2 | 3 | export interface AddNewGraphAxis { 4 | type: 'ADD_NEW_GRAPH_AXIS'; 5 | payload: string; 6 | } 7 | 8 | export interface ChangeRegion { 9 | type: 'CHANGE_REGION'; 10 | payload: string; 11 | } 12 | 13 | export interface ChangeDateFrom { 14 | type: 'CHANGE_DATE_FROM'; 15 | payload: number; 16 | } 17 | 18 | export interface ChangeDateTo { 19 | type: 'CHANGE_DATE_TO'; 20 | payload: number; 21 | } 22 | 23 | export interface ToggleAge { 24 | type: 'TOGGLE_AGE'; 25 | payload: number; 26 | } 27 | 28 | export interface VideosArrived { 29 | type: 'VIDEOS_ARRIVED'; 30 | payload: Video[]; 31 | } 32 | 33 | export interface VideosSelected { 34 | type: 'VIDEO_SELECTED'; 35 | payload: string; 36 | } 37 | 38 | export type VideoStatsAction = AddNewGraphAxis | ChangeRegion | ChangeDateFrom | ChangeDateTo | ToggleAge | VideosArrived | VideosSelected; 39 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/+state/video-stats.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | // import { Actions } from '@ngrx/effects'; 3 | 4 | @Injectable() 5 | export class VideoStatsEffects { 6 | // I would prefer to do something roughly like this, but there is not yet an 7 | // automatic feature level initialization action provided by NgRx. 8 | // @Effect() startup = this.actions.ofType('@ngrx/store/init') 9 | // .pipe(map(_ => ({ type: 'VIDEOS_ARRIVED', payload: VIDEO_LIST }))) 10 | 11 | // constructor(private actions: Actions) { } 12 | } 13 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/+state/video-stats.init.ts: -------------------------------------------------------------------------------- 1 | import { ViewsFilterState, ViewsBreakdownState } from './video-stats.interfaces'; 2 | import * as moment from 'moment'; 3 | 4 | export const viewsBreakdownStateInitial: ViewsBreakdownState = { 5 | selectedAxis: ['age'] 6 | }; 7 | 8 | export const viewsFilterInitialState: ViewsFilterState = { 9 | region: 'All', 10 | dateTo: moment().startOf('day').valueOf(), 11 | dateFrom: moment('1995-01-01').valueOf(), 12 | ageRanges: [true, true, true, true] 13 | }; 14 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/+state/video-stats.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ViewsBreakdownState { 2 | selectedAxis: string[]; 3 | } 4 | 5 | export interface View { 6 | age: number; 7 | region: string; 8 | date: string; 9 | } 10 | 11 | export interface Video { 12 | title: string; 13 | author: string; 14 | id: string; 15 | viewDetails: View[]; 16 | } 17 | 18 | export interface ViewsFilterState { 19 | region: string; 20 | dateTo: number; 21 | dateFrom: number; 22 | ageRanges: boolean[]; // bit for each bracket 23 | } 24 | 25 | export interface VideoStats { 26 | videoList: Video[]; 27 | viewsFilter: ViewsFilterState; 28 | currentVideo: string; 29 | viewsBreakdown: ViewsBreakdownState; 30 | topList: string[]; 31 | } 32 | 33 | export interface VideoStatsState { 34 | readonly videoStats: VideoStats; 35 | } 36 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/+state/video-stats.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ViewsFilterState, ViewsBreakdownState, Video } from './video-stats.interfaces'; 2 | import { VideoStatsAction } from './video-stats.actions'; 3 | import { viewsFilterInitialState, viewsBreakdownStateInitial } from './video-stats.init'; 4 | 5 | const viewsBreakdown = 6 | (prevState: ViewsBreakdownState = viewsBreakdownStateInitial, action: VideoStatsAction): ViewsBreakdownState => { 7 | switch (action.type) { 8 | case 'ADD_NEW_GRAPH_AXIS': 9 | const selectedAxis = [...prevState.selectedAxis, action.payload]; 10 | return Object.assign({}, prevState, { selectedAxis }); 11 | default: 12 | return prevState; 13 | } 14 | }; 15 | 16 | const viewsFilter = (previousState: ViewsFilterState = viewsFilterInitialState, 17 | action: VideoStatsAction): ViewsFilterState => { 18 | switch (action.type) { 19 | case 'CHANGE_REGION': 20 | return Object.assign({}, previousState, { region: action.payload }); 21 | case 'CHANGE_DATE_FROM': 22 | return Object.assign({}, previousState, { dateFrom: action.payload }); 23 | case 'CHANGE_DATE_TO': 24 | return Object.assign({}, previousState, { dateTo: action.payload }); 25 | case 'TOGGLE_AGE': 26 | const ageRanges = previousState.ageRanges.slice(); // clone 27 | ageRanges[action.payload] = !ageRanges[action.payload]; 28 | return Object.assign({}, previousState, { ageRanges }); 29 | default: 30 | return previousState; 31 | } 32 | }; 33 | 34 | const videoList = (state: Video[] = [], action: VideoStatsAction) => { 35 | switch (action.type) { 36 | case 'VIDEOS_ARRIVED': 37 | return action.payload; 38 | default: 39 | return state; 40 | } 41 | }; 42 | 43 | const currentVideo = (prevState: string = undefined, action: VideoStatsAction): string => { 44 | switch (action.type) { 45 | case 'VIDEO_SELECTED': 46 | return action.payload; 47 | default: 48 | return prevState; 49 | } 50 | }; 51 | 52 | const topList = (prevState: string[] = [], action: VideoStatsAction): string[] => { 53 | switch (action.type) { 54 | default: 55 | return prevState; 56 | } 57 | }; 58 | 59 | export const videoStatsReducerMap = { 60 | videoList, 61 | viewsFilter, 62 | currentVideo, 63 | viewsBreakdown, 64 | topList 65 | }; 66 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/dashboard.component.css: -------------------------------------------------------------------------------- 1 | img { 2 | max-height: 106px; 3 | } 4 | 5 | /* Override Materialize */ 6 | label { 7 | padding-left: 25px !important; 8 | } 9 | 10 | svg { 11 | display: block; 12 | margin: auto; 13 | } 14 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AppService } from './services/app.service'; 4 | 5 | @Component({ 6 | selector: 'app-dashboard', 7 | templateUrl: './dashboard.component.html', 8 | styleUrls: ['./dashboard.component.css'] 9 | }) 10 | export class DashboardComponent { 11 | 12 | constructor(_appService: AppService) { 13 | // Have it injected for the side effect of triggering the data population. 14 | // This is an anti-pattern, right? 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/services/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | 4 | import { VideoStatsState } from '../+state/video-stats.interfaces'; 5 | import { VIDEO_LIST } from './mock-video-list'; 6 | 7 | @Injectable() 8 | export class AppService { 9 | 10 | constructor(store: Store) { 11 | // Mock: Fetch list of videos and make them available. 12 | store.dispatch({ type: 'VIDEOS_ARRIVED', payload: VIDEO_LIST }); 13 | // this logic should arguably be something automatic upon loading of the 14 | // "future", but that capability is not yet provided by NgRx. 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/video-stat-dashboard/src/services/dashboard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { filter } from 'rxjs/operators'; 5 | import { combineLatest } from 'rxjs/observable/combineLatest' 6 | 7 | import { VideoStatsState } from '../+state/video-stats.interfaces'; 8 | import { Video } from '../+state/video-stats.interfaces'; 9 | 10 | @Injectable() 11 | export class DashboardService { 12 | 13 | currentVideo: Observable