diff --git a/apps/ues-recycling-angular-e2e/cypress.json b/apps/ues-recycling-angular-e2e/cypress.json new file mode 100644 index 0000000000..2db95f9914 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/cypress.json @@ -0,0 +1,12 @@ +{ + "fileServerFolder": ".", + "fixturesFolder": "./src/fixtures", + "integrationFolder": "./src/integration", + "modifyObstructiveCode": false, + "pluginsFile": "./src/plugins/index", + "supportFile": "./src/support/index.ts", + "video": true, + "videosFolder": "../../dist/cypress/apps/ues-recycling-angular-e2e/videos", + "screenshotsFolder": "../../dist/cypress/apps/ues-recycling-angular-e2e/screenshots", + "chromeWebSecurity": false +} diff --git a/apps/ues-recycling-angular-e2e/src/fixtures/example.json b/apps/ues-recycling-angular-e2e/src/fixtures/example.json new file mode 100644 index 0000000000..294cbed6ce --- /dev/null +++ b/apps/ues-recycling-angular-e2e/src/fixtures/example.json @@ -0,0 +1,4 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io" +} diff --git a/apps/ues-recycling-angular-e2e/src/integration/app.spec.ts b/apps/ues-recycling-angular-e2e/src/integration/app.spec.ts new file mode 100644 index 0000000000..bd213e6c75 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/src/integration/app.spec.ts @@ -0,0 +1,13 @@ +import { getGreeting } from '../support/app.po'; + +describe('ues-recycling-angular', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + // Custom command example, see `../support/commands.ts` file + cy.login('my-email@something.com', 'myPassword'); + + // Function helper example, see `../support/app.po.ts` file + getGreeting().contains('Welcome to ues-recycling-angular!'); + }); +}); diff --git a/apps/ues-recycling-angular-e2e/src/plugins/index.js b/apps/ues-recycling-angular-e2e/src/plugins/index.js new file mode 100644 index 0000000000..9067e75a25 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/src/plugins/index.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + + // Preprocess Typescript file using Nx helper + on('file:preprocessor', preprocessTypescript(config)); +}; diff --git a/apps/ues-recycling-angular-e2e/src/support/app.po.ts b/apps/ues-recycling-angular-e2e/src/support/app.po.ts new file mode 100644 index 0000000000..3293424696 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/src/support/app.po.ts @@ -0,0 +1 @@ +export const getGreeting = () => cy.get('h1'); diff --git a/apps/ues-recycling-angular-e2e/src/support/commands.ts b/apps/ues-recycling-angular-e2e/src/support/commands.ts new file mode 100644 index 0000000000..36c834059c --- /dev/null +++ b/apps/ues-recycling-angular-e2e/src/support/commands.ts @@ -0,0 +1,31 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +declare namespace Cypress { + interface Chainable { + login(email: string, password: string): void; + } +} +// +// -- This is a parent command -- +Cypress.Commands.add('login', (email, password) => { + console.log('Custom command example: Login', email, password); +}); +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/apps/ues-recycling-angular-e2e/src/support/index.ts b/apps/ues-recycling-angular-e2e/src/support/index.ts new file mode 100644 index 0000000000..3d469a6b6c --- /dev/null +++ b/apps/ues-recycling-angular-e2e/src/support/index.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; diff --git a/apps/ues-recycling-angular-e2e/tsconfig.e2e.json b/apps/ues-recycling-angular-e2e/tsconfig.e2e.json new file mode 100644 index 0000000000..9dc3660a79 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/tsconfig.e2e.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "outDir": "../../dist/out-tsc", + "allowJs": true, + "types": ["cypress", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.js"] +} diff --git a/apps/ues-recycling-angular-e2e/tsconfig.json b/apps/ues-recycling-angular-e2e/tsconfig.json new file mode 100644 index 0000000000..08841a7f56 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.e2e.json" + } + ] +} diff --git a/apps/ues-recycling-angular-e2e/tslint.json b/apps/ues-recycling-angular-e2e/tslint.json new file mode 100644 index 0000000000..36892e47e3 --- /dev/null +++ b/apps/ues-recycling-angular-e2e/tslint.json @@ -0,0 +1 @@ +{ "extends": "../../tslint.json", "linterOptions": { "exclude": ["!**/*"] }, "rules": {} } diff --git a/apps/ues-recycling-angular/.browserslistrc b/apps/ues-recycling-angular/.browserslistrc new file mode 100644 index 0000000000..427441dc93 --- /dev/null +++ b/apps/ues-recycling-angular/.browserslistrc @@ -0,0 +1,17 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR +not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/apps/ues-recycling-angular/jest.config.js b/apps/ues-recycling-angular/jest.config.js new file mode 100644 index 0000000000..f7e518a865 --- /dev/null +++ b/apps/ues-recycling-angular/jest.config.js @@ -0,0 +1,20 @@ +module.exports = { + displayName: 'ues-recycling-angular', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: ['jest-preset-angular/build/InlineFilesTransformer', 'jest-preset-angular/build/StripStylesTransformer'] + } + } + }, + coverageDirectory: '../../coverage/apps/ues-recycling-angular', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/apps/ues-recycling-angular/src/app/app.component.html b/apps/ues-recycling-angular/src/app/app.component.html new file mode 100644 index 0000000000..0680b43f9c --- /dev/null +++ b/apps/ues-recycling-angular/src/app/app.component.html @@ -0,0 +1 @@ + diff --git a/apps/ues-recycling-angular/src/app/app.component.scss b/apps/ues-recycling-angular/src/app/app.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/ues-recycling-angular/src/app/app.component.spec.ts b/apps/ues-recycling-angular/src/app/app.component.spec.ts new file mode 100644 index 0000000000..1887f73787 --- /dev/null +++ b/apps/ues-recycling-angular/src/app/app.component.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [AppComponent] + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'ues-recycling-angular'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('ues-recycling-angular'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to ues-recycling-angular!'); + }); +}); diff --git a/apps/ues-recycling-angular/src/app/app.component.ts b/apps/ues-recycling-angular/src/app/app.component.ts new file mode 100644 index 0000000000..f2cf89f541 --- /dev/null +++ b/apps/ues-recycling-angular/src/app/app.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'tamu-gisc-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent {} diff --git a/apps/ues-recycling-angular/src/app/app.module.ts b/apps/ues-recycling-angular/src/app/app.module.ts new file mode 100644 index 0000000000..aea05bf6f5 --- /dev/null +++ b/apps/ues-recycling-angular/src/app/app.module.ts @@ -0,0 +1,53 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; + +import * as WebFont from 'webfontloader'; +import { env, EnvironmentService } from '@tamu-gisc/common/ngx/environment'; +import { AuthGuard, AuthProvider } from '@tamu-gisc/common/ngx/auth'; + +import * as environment from '../environments/environment'; +import { AppComponent } from './app.component'; + +WebFont.load({ + google: { + families: ['Material Icons'] + }, + custom: { + families: ['Moriston', 'Tungsten'], + urls: ['assets/fonts/moriston_pro/moriston_pro.css', 'assets/fonts/tungsten/tungsten.css'] + } +}); + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + HttpClientModule, + RouterModule.forRoot( + [ + { + path: 'data', + loadChildren: () => import('@tamu-gisc/ues/recycling/ngx').then((m) => m.DataModule), + canActivate: [AuthGuard] + }, + { + path: '', + loadChildren: () => import('@tamu-gisc/ues/recycling/ngx').then((m) => m.MapModule) + } + ], + { initialNavigation: 'enabled' } + ) + ], + providers: [ + EnvironmentService, + { + provide: env, + useValue: environment + }, + AuthProvider + ], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/apps/ues-recycling-angular/src/assets/.gitkeep b/apps/ues-recycling-angular/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/ues-recycling-angular/src/environments/definitions.ts b/apps/ues-recycling-angular/src/environments/definitions.ts new file mode 100644 index 0000000000..ae8f57f328 --- /dev/null +++ b/apps/ues-recycling-angular/src/environments/definitions.ts @@ -0,0 +1,100 @@ +import { SearchSource, SearchSourceQueryParamsProperties } from '@tamu-gisc/search'; +import { LayerSource, LegendItem } from '@tamu-gisc/common/types'; + +import esri = __esri; + +export const Connections = { + basemapUrl: 'https://gis.tamu.edu/arcgis/rest/services/FCOR/TAMU_BaseMap/MapServer', + inforUrl: 'https://gis.tamu.edu/arcgis/rest/services/FCOR/MapInfo_20190529/MapServer', + accessibleUrl: 'https://fc-gis.tamu.edu/arcgis/rest/services/FCOR/ADA_120717/MapServer/0', + constructionUrl: 'https://gis.tamu.edu/arcgis/rest/services/FCOR/Construction_2018/MapServer', + recyclingPointsUrl: 'https://ues-arc.tamu.edu/arcgis/rest/services/Recycling/Utilities_Recycilng_WebMap/MapServer/0' +}; + +export const Definitions = { + BUILDINGS: { + id: 'buildings', + layerId: 'buildings-layer', + name: 'Buildings', + url: `${Connections.basemapUrl}/1` + }, + RECYCLING: { + id: 'recycling', + layerId: 'recycling-layer', + name: 'Recycling Centers', + url: `${Connections.recyclingPointsUrl}` + } +}; + +const commonLayerProps = { + outFields: ['*'], + minScale: 100000, + maxScale: 0, + elevationInfo: { mode: 'relative-to-ground', offset: 1 } as esri.FeatureLayerElevationInfo, + popupEnabled: false +}; + +// Persistent layer definitions that will be processed by a factory and added to the map. +export const LayerSources: LayerSource[] = [ + { + type: 'feature', + id: Definitions.RECYCLING.layerId, + title: Definitions.RECYCLING.name, + url: Definitions.RECYCLING.url, + listMode: 'hide', + visible: true, + layerIndex: 2, + native: { + ...commonLayerProps, + definitionExpression: "public_view LIKE 'Yes'" + } + }, + { + type: 'graphic', + id: 'selection-layer', + title: 'Selected Buildings', + category: 'Infrastructure', + listMode: 'hide', + visible: true + } +]; + +const commonQueryParams: Partial = { + f: 'json', + resultRecordCount: 5, + outFields: '*', + outSR: 4326, + returnGeometry: true, + spatialRel: 'esriSpatialRelIntersects' +}; + +// Search sources used for querying features. +export const SearchSources: SearchSource[] = []; + +export const SelectionSymbols = { + polygon: { + type: 'simple-fill', + style: 'solid', + color: [252, 227, 0, 0.55], + outline: { + color: [252, 227, 0, 0.8], + width: '2px' + } + }, + point: { + type: 'simple-marker', + style: 'circle', + size: 8, + outline: { + width: 2 + } + }, + multipoint: { + type: 'simple-marker', + style: 'circle', + size: 8, + outline: { + width: 2 + } + } +}; diff --git a/apps/ues-recycling-angular/src/environments/environment.prod.ts b/apps/ues-recycling-angular/src/environments/environment.prod.ts new file mode 100644 index 0000000000..8f79378636 --- /dev/null +++ b/apps/ues-recycling-angular/src/environments/environment.prod.ts @@ -0,0 +1,13 @@ +import { AuthOptions } from '@tamu-gisc/oidc/client'; + +export const environment = { + production: true +}; + +export * from './definitions'; + +export const apiUrl = 'https://nodes.geoservices.tamu.edu/api/ues/recycling/'; +export const auth_options: AuthOptions = { + url: `https://nodes.geoservices.tamu.edu/api/ues/recycling`, + attach_href: true +}; diff --git a/apps/ues-recycling-angular/src/environments/environment.ts b/apps/ues-recycling-angular/src/environments/environment.ts new file mode 100644 index 0000000000..467899a836 --- /dev/null +++ b/apps/ues-recycling-angular/src/environments/environment.ts @@ -0,0 +1,21 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +import 'zone.js/dist/zone-error'; // Included with Angular CLI. + +export * from './definitions'; + +export const apiUrl = 'http://localhost:3333/api/'; +export const auth_url = 'http://localhost:3333/api'; diff --git a/apps/ues-recycling-angular/src/favicon.ico b/apps/ues-recycling-angular/src/favicon.ico new file mode 100644 index 0000000000..b61687193b Binary files /dev/null and b/apps/ues-recycling-angular/src/favicon.ico differ diff --git a/apps/ues-recycling-angular/src/index.html b/apps/ues-recycling-angular/src/index.html new file mode 100644 index 0000000000..ed88f26648 --- /dev/null +++ b/apps/ues-recycling-angular/src/index.html @@ -0,0 +1,23 @@ + + + + + UES Recycling Trends + + + + + + + + + + + + diff --git a/apps/ues-recycling-angular/src/main.ts b/apps/ues-recycling-angular/src/main.ts new file mode 100644 index 0000000000..d9a2e7e4a5 --- /dev/null +++ b/apps/ues-recycling-angular/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/apps/ues-recycling-angular/src/polyfills.ts b/apps/ues-recycling-angular/src/polyfills.ts new file mode 100644 index 0000000000..c5f27446d3 --- /dev/null +++ b/apps/ues-recycling-angular/src/polyfills.ts @@ -0,0 +1,62 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/apps/ues-recycling-angular/src/styles.scss b/apps/ues-recycling-angular/src/styles.scss new file mode 100644 index 0000000000..b6a13602a3 --- /dev/null +++ b/apps/ues-recycling-angular/src/styles.scss @@ -0,0 +1,980 @@ +/* ========================================================================== + main.scss - Contains all front-end styles for the web application + Application: Texas A&M Aggie Map + Author: Edgar Hernandez - TAMU GeoInnovation Service Center © 2020 + ========================================================================== */ + +// +// +// Note: This project requires a SASS compiler. Changes made here will be compiled into the file utilized by the browser. +// For more information on LESS visit: https://sass-lang.com/ +// +// As it stands, this project is built with Angular and SASS compiled and bundled by Webpack. +// +// + +// ========================================================================== +// GRID +// ========================================================================== + +@import 'libs/sass/1140'; + +// ========================================================================== +// VARIABLES +// ========================================================================== + +@import 'libs/sass/variables'; + +// ========================================================================== +// SASS Mixins +// ========================================================================== + +@import 'libs/sass/mixins'; + +// ========================================================================== +// QUICKFLEXBOX CLASSES +// ========================================================================== + +@import 'libs/sass/dom_flexbox'; + +// ========================================================================== +// Spacing Classes +// ========================================================================== + +@import 'libs/sass/spacing'; + +// ========================================================================== +// UI Elements +// ========================================================================== + +$active-color: hsl(223, 75%, 67%); +$state-transition-color: #536dfe; +$inactive-color: hsl(223, 15%, 60%); +$lightened-inactive-color: lighten($inactive-color, 25%); + +@import 'libs/sass/modules/loaders'; +@import 'libs/sass/modules/tabs'; + +// ========================================================================== +// DEFAULTS +// ========================================================================== + +html, +body { + font-size: 16px; + font-family: $moriston-fStack; + margin: 0; + padding: 0; + top: 0px; + height: 100%; + width: 100%; +} + +body { + //Disable pull to refresh + overscroll-behavior-y: contain; +} + +body.scroll { + -webkit-overflow-scrolling: touch; //Preserve mobile momentum scrolling + overflow-y: scroll; +} + +body p { + padding-bottom: 0.5rem; + color: inherit; + position: inherit; + font-weight: inherit; + + &.large { + font-size: 1.2rem; + font-weight: 400; + color: #616161; + } +} + +ul { + display: block; + margin-bottom: 1rem !important; + + &.bullet { + list-style: disc; + list-style-position: outside; + margin-left: 2rem; + } +} + +li { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + color: inherit; + position: relative; +} + +p, +li, +a { + line-height: 1.7; +} + +a { + color: #500000; + text-decoration: underline; + font-size: inherit; + font-weight: 500; + + &.no-decor { + text-decoration: none !important; + } +} + +a:link { + color: #500000; + text-decoration: underline; +} + +a:visited { + color: #500000; + text-decoration: underline; +} + +a:hover, +a:active, +a:focus { + color: #500000; +} + +a:visited { + text-decoration: none; + text-decoration: underline; +} + +select { + width: 100%; + padding: 0.5rem; +} + +h1 { + font-size: 3rem; + font-weight: 300; +} + +h2 { + font-size: 2.1875rem; + font-weight: 400; + margin-top: 0.4rem; + margin-bottom: 0.2rem; +} + +h3 { + font-size: 1.875rem; + font-weight: 300; + margin-top: 0.4rem; + margin-bottom: 0.2rem; +} + +h4 { + font-size: 1.875rem; + font-weight: 300; + margin-top: 0.4rem; + margin-bottom: 0.2rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: $tungsten-fStack; + line-height: 1.2; + letter-spacing: 1.25px; + text-transform: uppercase; + + &.short-border { + display: inline-block; + border-bottom: 1px solid #eeeeee; + padding-bottom: 0.5rem; + } + + &.long-border { + display: block; + border-bottom: 1px solid #eeeeee; + padding-bottom: 0.5rem; + } +} + +.table-overflow-scroll { + overflow: auto; +} + +table { + // All tables, except tables inside code elements. + &:not([class^='hljs']) { + width: 100%; + margin: 1rem 0; + font-size: 0.95rem; + + border: 1pt solid $lightened-inactive-color; + border-collapse: collapse; + + td, + th { + padding: 0.75rem; + } + + td { + border: 1pt solid $lightened-inactive-color; + border-collapse: collapse; + } + + thead { + tr { + background: #fafafa; + } + + th { + text-align: left; + } + } + + tbody { + tr { + &:nth-child(2n) { + background: #fafafa; + } + + // Targets the div in the "Description" column of each row + td:nth-child(2) { + & > div:first-child { + @include flexbox(); + @include flex-direction(row); + @include justify-content(space-between); + + & > div:nth-child(2) { + white-space: nowrap; + margin-left: 1rem; + } + } + } + } + } + } + + // Add small padding after line column in code blocks + &[class^='hljs'] { + td:first-child { + padding-right: 0.5rem; + } + } +} + +.accessible-viewfinder, +.accessible-viewfinder-features { + z-index: 999; +} + +.accessible-viewfinder { + position: fixed; + height: 15rem; + width: 15rem; + left: 50%; + top: 50%; + border: 2px solid #2979ff; + @include transform(translateX(-50%) translateY(-50%)); +} + +.accessible-viewfinder-features { + position: fixed; + background: #f5f5f5; + width: 40rem; + height: 8.5rem; + overflow: hidden; + padding: 0 1rem; + box-sizing: border-box; + left: 50%; + top: calc(50% + 15rem); + border-radius: 3px; + @include transform(translateX(-50%) translateY(-50%)); + @include flexbox(); + @include flex-direction(row); + @include justify-content(center); + + .feature-item-container { + @include flexbox(); + @include flex-direction(row); + @include justify-content(flex-start); + @include flex-wrap(wrap); + } + + .feature-item { + @include flexbox(); + @include flex-direction(row); + @include justify-content(center); + @include align-items(center); + font-size: 0.75rem; + padding: 0.5rem; + + p { + padding-bottom: 0; + } + + .feature-index { + margin-right: 0.5rem; + + .wrapper { + width: 1.5rem; + height: 1.5rem; + border: 1px solid #bdbdbd; + border-radius: 5px; + @include flexbox(); + @include flex-direction(row); + @include justify-content(center); + @include align-items(center); + } + + p { + font-weight: 500; + } + } + } +} + +input[type='text'], +input[type='email'], +textarea { + font-family: $moriston-fStack; + background-color: $form-grey; + border: 1px solid transparent; + color: #323232; + outline: none; + box-sizing: border-box; + font-size: 0.9rem; + @include transition(border-color 0.25s); + + &:focus { + border-color: #6150508e; + } +} + +input[type='text'] { + padding: 0.5rem 0.75rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +textarea, +input[type='email'] { + width: 100%; + padding: 0.75rem; + border-radius: 0.5rem; +} + +.feedback-form { + width: 50%; + + input, + textarea { + width: 100%; + padding: 0.75rem; + + &:focus { + & + label { + color: black; + } + } + + &[type='submit'] { + display: inline-block; + width: auto; + padding: 0.75rem 1rem; + background: #500000; + color: #f5f5f5; + cursor: pointer; + + &:focus { + background: lighten(#500000, 5%); + } + } + } + + textarea { + min-height: 10rem; + } + + label { + font-weight: 500; + color: #616161; + font-size: 0.9rem; + margin-left: 0.5rem; + } +} + +.input-item { + margin: 2.5rem 0; + position: relative; + + label { + position: absolute; + top: -1.6rem; + left: 0; + } + + &.last { + margin-bottom: 1.5rem; + } +} + +tamu-gisc-line-chart h3 { + font-family: $moriston-fStack; + font-size: 0.9rem; +} + +.stat-card { + padding: 1rem; + border: 1pt solid #bdbdbd; + border-radius: 7px; + margin: 1.5rem 0.5rem; + @include flexbox(); + @include flex-direction(column); + @include justify-content(center); + @include align-items(center); + + .stat-metric { + font-size: 2.75rem; + padding: 1rem; + } + + .stat-title { + font-size: 0.9rem; + color: #9e9e9e; + text-transform: uppercase; + font-weight: 600; + letter-spacing: 1px; + margin-bottom: 1rem; + } + + .stat-list { + @include flexbox(); + @include flex-direction(row); + @include flex-wrap(wrap); + @include justify-content(center); + + .stat-list-item { + background: transparent; + border: 1.1pt solid #1e88e5; + padding: 0.4rem 1rem; + margin: 0.5rem.25rem; + border-radius: 1rem; + font-weight: 500; + cursor: pointer; + + &:hover { + background: #1e88e5; + color: #ffffff; + } + } + } + + .stat-chart { + width: 100%; + } +} + +.checkbox-accordion { + position: relative; + + .checkbox-instructions { + text-align: center; + + p { + font-weight: 600; + text-decoration: underline; + color: #500000; + } + + & > p.checkbox-true { + display: none; + } + } + + .accordion-content { + display: none; + } + + input[type='checkbox'] { + position: absolute; + width: 100%; + opacity: 0; + padding: 0.5rem; + cursor: pointer; + + &:checked { + & ~ .checkbox-instructions { + p.checkbox-true { + display: block; + } + + p.checkbox-false { + display: none; + } + } + + & ~ .accordion-content { + @include flexbox(); + } + } + } +} + +.metric-safe { + color: rgb(102, 187, 106); +} + +.metric-warning { + color: rgb(255, 152, 0); +} + +.metric-alert { + color: rgb(239, 83, 80); +} + +.semi-bold { + font-weight: 500; +} + +.bold { + font-weight: 600; +} + +.hidden { + display: none; +} + +.loader { + position: fixed; + height: 100%; + width: 100%; + top: 0; + left: 0; + background: #500000; + z-index: 9999; + opacity: 1; + text-align: center; + color: #ffffff; + @include transition(opacity 0.3s); + + .content { + width: 20rem; + font-family: $primary-fStack; + } + + .progress-bar { + margin-top: 2rem; + background: #ffffff; + height: 0.3rem; + width: 100%; + border-radius: 99rem; + @include transition(all 0.3s); + @include transform(translateZ(0)); + + &.anim { + @include animation(do-progress 15s forwards); + } + } + + p { + margin-top: 1.5rem; + font-family: $moriston-fStack; + font-size: 0.8rem; + font-weight: 300; + font-style: italic; + color: #eeeeee; + } + + &.fade-out { + opacity: 0; + } +} + +@keyframes do-progress { + 0% { + max-width: 0%; + } + + 100% { + max-width: 90%; + } +} + +.handle { + height: 0.25rem; + width: 2rem; + background: #bdbdbd; + border-radius: 99rem; + position: absolute; + left: 50%; + top: 0.65rem; + @include transform(translateX(-50%)); + margin-bottom: 0; + box-sizing: border-box; +} + +// ========================================================================== +// ESRI SPECIFIC +// ========================================================================== +.card { + background: #fafafa; + padding: 1rem 1.5rem; + border-radius: 7px; + margin-bottom: 1.5rem; + min-width: 25rem; + max-width: 25rem; + box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.15); + @include flexbox(); + @include flex-direction(column); + + p:last-child { + padding-bottom: 0; + } + + &.metric { + @include justify-content(center); + @include align-items(center); + + .toolbar { + position: absolute; + right: 1rem; + top: 0.5rem; + font-size: 1.3rem; + + span.material-icons { + font-size: inherit; + } + } + } + + .large-metric { + font-size: 2.25rem; + font-weight: 600; + padding-bottom: 0.5rem; + } + + .card-title { + text-transform: uppercase; + letter-spacing: 1.4px; + font-weight: 600; + color: $inactive-color; + font-size: 0.9rem; + } +} +// ========================================================================== +// ESRI SPECIFIC +// ========================================================================== +body.mapping { + @include flexbox(); + @include flex-direction(column); + + app-root { + @include flex(1); + } +} + +.esri-zoom .esri-widget--button:last-child { + border-top: solid 1px rgba(50, 50, 50, 0.25); +} + +.esri-component.esri-attribution.esri-widget { + display: none; +} + +.copy-val { + position: absolute; + height: 0; + width: 0; + opacity: 0; + display: block; + z-index: -999999; +} + +.copy-link { + overflow: hidden !important; + white-space: nowrap; + margin-right: 0.15rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + max-width: 80%; +} + +.component-name { + font-weight: 600; + border-bottom: #e0e0e0 1px solid; + padding-bottom: 0.5rem; +} + +.toggle-dots { + display: none; + @include flex-direction(row); + @include justify-content(center); + position: absolute; + bottom: 0.25rem; + left: 0; + width: 100%; + + & > div { + height: 0.5rem; + width: 0.5rem; + border-radius: 99rem; + background: #bdbdbd; + margin: 0.5rem 0.3rem; + + &.active { + background: #616161; + } + } +} + +.esri-component.esri-track.esri-widget--button.esri-widget { + margin-left: 0; + border-radius: 99rem; + padding: 1.25rem; + + span { + font-size: 1.25rem; + } +} + +// ========================================================================== +// APPLICATION STATUS +// ========================================================================== +.error { + color: #f44336; + font-weight: 600; +} + +.success { + color: #43a047; + font-weight: 600; +} + +.description i { + font-size: 1rem; + vertical-align: middle; +} + +// ========================================================================== +// Action feedback +// ========================================================================== + +.copy-button { + color: $main-blue; + padding: 0.8rem 0.75rem; + font-weight: 600; + width: initial; + position: relative; + + &:focus { + outline: none; + } + + &::before { + content: attr(copy-label); + font-size: 0.6rem; + left: 50%; + top: 50%; + @include transform(translateX(-50%) translateY(-50%)); + opacity: 0; + position: absolute; + color: lighten($main-blue, 5%); + } + + &.copying { + &::before { + @include animation(float-text 0.7s); + } + } +} + +@include keyframes(float-text) { + 0% { + opacity: 0; + } + + 50% { + opacity: 1; + @include transform(translateX(-50%) translateY(-175%)); + } + + 100% { + opacity: 0; + @include transform(translateX(-50%) translateY(-250%)); + } +} + +// Changelog Markdown styles +// As to not remove view encapsulation for those component styles +.markdown { + line-height: 1.4; + + h2, + h3 { + font-size: 1rem; + font-family: $moriston-fStack; + text-transform: none; + letter-spacing: initial; + font-weight: 600; + color: #424242; + } + + h2 { + margin-top: 0.5rem; + margin-bottom: 1.25rem; + } + + h3 { + font-size: 0.95rem; + margin-left: 0.75rem; + margin-bottom: 0.5rem; + + & ~ p { + margin-left: 0.75rem; + display: block; + } + + & ~ ul { + margin-left: 1.25rem; + } + } + + p { + padding-bottom: 0.5rem; + } + + code { + font-family: 'Consolas'; + font-size: 0.95rem; + display: inline-block; + padding: 0.1rem 0.5rem; + line-height: 1; + border-radius: 5px; + background: #eeeeee; + color: #ef5350; + } + + ul { + // margin-bottom: .25rem !important; + font-size: 0.95rem; + list-style-position: inside; + + li { + padding: 0; + + ul { + margin-left: 1.25rem; + margin-bottom: 0 !important; + } + } + } + + .changelong-event-body-media { + @include flexbox(); + @include flex-direction(row); + @include justify-content(center); + margin-bottom: 1rem; + margin-top: 1rem; + + & > a { + @include flexbox(); + @include flex-direction(row); + @include align-items(center); + text-decoration: none; + font-size: 0.8rem; + text-align: center; + color: #616161; + font-weight: 400; + max-width: 47%; + margin: 0 1.5rem; + } + + figure { + margin: 0; + font-style: italic; + @include flexbox(); + @include flex-direction(column); + @include align-items(center); + } + + figcaption { + font-size: 0.8rem; + text-align: center; + color: #616161; + } + + img { + display: block; + max-width: 100%; + max-height: 25vh; + height: auto; + width: auto; + margin-bottom: 0.5rem; + } + } + + .material-icons { + font-size: 0.8rem; + } +} + +// ========================================================================== +// MEDIA QUERIES MEDIUM +// ========================================================================== + +@media only screen and (max-width: 960px) { +} + +@media only screen and (max-width: 768px) { + .container12, + .container16 { + width: 87% !important; + } + + .column1, + .column2, + .column3, + .column4, + .column5, + .column6, + .column7, + .column8, + .column9, + .column10, + .column11, + .column12 { + width: 100% !important; + } + + .feedback-form { + width: 65%; + } + + // Hide main header and footer for mobile view + body app-header { + display: none; + } + + body app-footer { + display: none; + } +} + +@media only screen and (max-width: 478px) { +} + +@media only screen and (max-width: 350px) { + .loader { + .content { + width: 75%; + } + } +} diff --git a/apps/ues-recycling-angular/src/test-setup.ts b/apps/ues-recycling-angular/src/test-setup.ts new file mode 100644 index 0000000000..8d88704e8f --- /dev/null +++ b/apps/ues-recycling-angular/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/apps/ues-recycling-angular/tsconfig.app.json b/apps/ues-recycling-angular/tsconfig.app.json new file mode 100644 index 0000000000..305f09ff0f --- /dev/null +++ b/apps/ues-recycling-angular/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["arcgis-js-api"] + }, + "files": ["src/main.ts", "src/polyfills.ts"] +} diff --git a/apps/ues-recycling-angular/tsconfig.editor.json b/apps/ues-recycling-angular/tsconfig.editor.json new file mode 100644 index 0000000000..2664fcb0d9 --- /dev/null +++ b/apps/ues-recycling-angular/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*.ts"], + "compilerOptions": { + "types": ["jest", "node", "arcgis-js-api"] + } +} diff --git a/apps/ues-recycling-angular/tsconfig.json b/apps/ues-recycling-angular/tsconfig.json new file mode 100644 index 0000000000..81cb95e9dc --- /dev/null +++ b/apps/ues-recycling-angular/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.editor.json" + } + ] +} diff --git a/apps/ues-recycling-angular/tsconfig.spec.json b/apps/ues-recycling-angular/tsconfig.spec.json new file mode 100644 index 0000000000..612163f639 --- /dev/null +++ b/apps/ues-recycling-angular/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "arcgis-js-api"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/apps/ues-recycling-angular/tslint.json b/apps/ues-recycling-angular/tslint.json new file mode 100644 index 0000000000..fbdbabc698 --- /dev/null +++ b/apps/ues-recycling-angular/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [true, "attribute", "tamuGisc", "camelCase"], + "component-selector": [true, "element", "tamu-gisc", "kebab-case"] + }, + "linterOptions": { + "exclude": ["!**/*"] + } +} diff --git a/apps/ues-recycling-data-api-nest/.eslintrc.json b/apps/ues-recycling-data-api-nest/.eslintrc.json new file mode 100644 index 0000000000..54ac39535a --- /dev/null +++ b/apps/ues-recycling-data-api-nest/.eslintrc.json @@ -0,0 +1 @@ +{ "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } diff --git a/apps/ues-recycling-data-api-nest/jest.config.js b/apps/ues-recycling-data-api-nest/jest.config.js new file mode 100644 index 0000000000..9ab1a4216d --- /dev/null +++ b/apps/ues-recycling-data-api-nest/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'ues-recycling-data-api-nest', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json' + } + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/apps/ues-recycling-data-api-nest' +}; diff --git a/apps/ues-recycling-data-api-nest/package.json b/apps/ues-recycling-data-api-nest/package.json new file mode 100644 index 0000000000..0c66995da7 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/package.json @@ -0,0 +1,23 @@ +{ + "name": "ues-recycling-data-api-deploy", + "version": "0.0.0", + "license": "MIT", + "private": true, + "dependencies": { + "@nestjs/common": "7.0.0", + "@nestjs/core": "7.0.0", + "@nestjs/passport": "^6.1.1", + "@nestjs/platform-express": "7.0.0", + "@nestjs/typeorm": "^6.2.0", + "class-validator": "^0.11.0", + "express-session": "^1.17.0", + "mssql": "^6.0.1", + "openid-client": "^3.15.3", + "papaparse": "^5.3.0", + "passport": "^0.4.1", + "reflect-metadata": "^0.1.13", + "rxjs": "6.5.5", + "typeorm": "^0.2.20" +} +} + \ No newline at end of file diff --git a/apps/ues-recycling-data-api-nest/src/app/.gitkeep b/apps/ues-recycling-data-api-nest/src/app/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/ues-recycling-data-api-nest/src/app/app.controller.spec.ts b/apps/ues-recycling-data-api-nest/src/app/app.controller.spec.ts new file mode 100644 index 0000000000..6b78656e3c --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/app/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let app: TestingModule; + + beforeAll(async () => { + app = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService] + }).compile(); + }); + + describe('getData', () => { + it('should return "Welcome to ues-recycling-data-api-nest!"', () => { + const appController = app.get(AppController); + expect(appController.getData()).toEqual({ message: 'Welcome to ues-recycling-data-api-nest!' }); + }); + }); +}); diff --git a/apps/ues-recycling-data-api-nest/src/app/app.controller.ts b/apps/ues-recycling-data-api-nest/src/app/app.controller.ts new file mode 100644 index 0000000000..869e37d237 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/app/app.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; + +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + public getData() { + return this.appService.getData(); + } +} diff --git a/apps/ues-recycling-data-api-nest/src/app/app.module.ts b/apps/ues-recycling-data-api-nest/src/app/app.module.ts new file mode 100644 index 0000000000..8c83e192e1 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/app/app.module.ts @@ -0,0 +1,39 @@ +import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { Location, Result } from '@tamu-gisc/ues/recycling/common/entities'; +import { LocationsModule, ResultsModule } from '@tamu-gisc/ues/recycling/data-api'; +import { OidcClientModule, OidcClientController, ClaimsMiddleware } from '@tamu-gisc/oidc/client'; + +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +import { dbConfig, idpConfig } from '../environments/environment'; + +@Module({ + imports: [ + OidcClientModule.forRoot({ + host: idpConfig.issuer_url + }), + TypeOrmModule.forRoot({ + ...dbConfig, + entities: [Location, Result] + }), + LocationsModule, + ResultsModule + ], + controllers: [AppController], + providers: [AppService] +}) +export class AppModule implements NestModule { + public configure(consumer: MiddlewareConsumer) { + consumer + .apply(ClaimsMiddleware) + .exclude( + { path: 'oidc/login', method: RequestMethod.GET }, + { path: 'oidc/logout', method: RequestMethod.GET }, + { path: 'oidc/auth/callback', method: RequestMethod.GET } + ) + .forRoutes(OidcClientController); + } +} diff --git a/apps/ues-recycling-data-api-nest/src/app/app.service.spec.ts b/apps/ues-recycling-data-api-nest/src/app/app.service.spec.ts new file mode 100644 index 0000000000..baeae2c7a1 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/app/app.service.spec.ts @@ -0,0 +1,21 @@ +import { Test } from '@nestjs/testing'; + +import { AppService } from './app.service'; + +describe('AppService', () => { + let service: AppService; + + beforeAll(async () => { + const app = await Test.createTestingModule({ + providers: [AppService] + }).compile(); + + service = app.get(AppService); + }); + + describe('getData', () => { + it('should return "Welcome to ues-recycling-data-api-nest!"', () => { + expect(service.getData()).toEqual({ message: 'Welcome to ues-recycling-data-api-nest!' }); + }); + }); +}); diff --git a/apps/ues-recycling-data-api-nest/src/app/app.service.ts b/apps/ues-recycling-data-api-nest/src/app/app.service.ts new file mode 100644 index 0000000000..038b5a7ab8 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/app/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + public getData(): { message: string } { + return { message: 'Welcome to ues-recycling-data-api-nest!' }; + } +} diff --git a/apps/ues-recycling-data-api-nest/src/assets/.gitkeep b/apps/ues-recycling-data-api-nest/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/ues-recycling-data-api-nest/src/environments/environment.prod.ts b/apps/ues-recycling-data-api-nest/src/environments/environment.prod.ts new file mode 100644 index 0000000000..987fddbd25 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/environments/environment.prod.ts @@ -0,0 +1,9 @@ +export const environment = { + production: true, + port: 28052, + globalPrefix: '', + allowedOrigins: ['https://dev.aggiemap.tamu.edu', 'https://ues-dev.geoservices.tamu.edu', 'https://maps.apogee.tamu.edu'] +}; + +export { productionDbConfig as dbConfig } from './ormconfig'; +export { productionClientConfig as idpConfig } from './oidc-client-config'; diff --git a/apps/ues-recycling-data-api-nest/src/environments/environment.ts b/apps/ues-recycling-data-api-nest/src/environments/environment.ts new file mode 100644 index 0000000000..53dc3e31cd --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/environments/environment.ts @@ -0,0 +1,9 @@ +export const environment = { + production: false, + port: 3333, + globalPrefix: 'api', + allowedOrigins: ['http://localhost:4200'] +}; + +export { localDbConfig as dbConfig } from './ormconfig'; +export { localClientConfig as idpConfig } from './oidc-client-config'; diff --git a/apps/ues-recycling-data-api-nest/src/main.ts b/apps/ues-recycling-data-api-nest/src/main.ts new file mode 100644 index 0000000000..6c2964b46c --- /dev/null +++ b/apps/ues-recycling-data-api-nest/src/main.ts @@ -0,0 +1,57 @@ +import { NestFactory } from '@nestjs/core'; + +import passport from 'passport'; +import session from 'express-session'; +import * as sqliteStore from 'connect-sqlite3'; + +import { OpenIdClient } from '@tamu-gisc/oidc/client'; + +import { AppModule } from './app/app.module'; +import { environment, idpConfig } from './environments/environment'; + +const SQLiteStore = sqliteStore(session); + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + cors: { + credentials: true, + origin: environment.allowedOrigins + } + }); + + const sqlStore = new SQLiteStore({ + db: 'sessions.db', + concurrentDB: true, + table: 'sessions', + dir: __dirname + }); + + app.use( + session({ + name: 'ues-recycling', + resave: false, + saveUninitialized: false, + secret: 'GEOINNOVATIONSERVICECENTER', + store: sqlStore, + cookie: { + maxAge: 14 * 24 * 60 * 60 * 1000 // 2 weeks + } + }) + ); + + app.use(passport.initialize()); + app.use(passport.session()); + + app.setGlobalPrefix(environment.globalPrefix); + const port = environment.port || process.env.port || 3333; + + await app.listen(port, () => { + console.log('Listening at http://localhost:' + port + '/' + environment.globalPrefix); + }); +} + +OpenIdClient.build(idpConfig.metadata, idpConfig.parameters, idpConfig.issuer_url) + .then(() => bootstrap()) + .catch((err) => { + console.warn(err); + }); diff --git a/apps/ues-recycling-data-api-nest/tsconfig.app.json b/apps/ues-recycling-data-api-nest/tsconfig.app.json new file mode 100644 index 0000000000..fa5d932492 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/tsconfig.app.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"], + "emitDecoratorMetadata": true, + "target": "es2015" + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/apps/ues-recycling-data-api-nest/tsconfig.json b/apps/ues-recycling-data-api-nest/tsconfig.json new file mode 100644 index 0000000000..63dbe35fb2 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/apps/ues-recycling-data-api-nest/tsconfig.spec.json b/apps/ues-recycling-data-api-nest/tsconfig.spec.json new file mode 100644 index 0000000000..29efa430b2 --- /dev/null +++ b/apps/ues-recycling-data-api-nest/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7a3d15385e..7c974e4020 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,80 +1,40 @@ -# Node.js with Angular -# Build a Node.js project that uses Angular. -# Add steps that analyze code, save build artifacts, deploy, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript +# pool: +# vmImage: 'windows-latest' pool: - vmImage: 'windows-latest' + name: 'default' -resources: - containers: - - container: mssql - image: mcr.microsoft.com/mssql/server:2019-latest - ports: - - 1433:1433 - env: - ACCEPT_EULA: Y - SA_PASSWORD: yourStrong(!)Password - MSSQL_PID: Developer - options: --name mssql +# variables: +# npm_config_cache: $(Pipeline.Workspace)/.npm jobs: - - job: ci - timeoutInMinutes: 120 - services: - mssql: mssql - pool: - vmImage: 'ubuntu-latest' - variables: - IS_PR: $[ eq(variables['Build.Reason'], 'PullRequest') ] + - job: UESRecyclingCI steps: - - task: InstallSSHKey@0 - inputs: - knownHostsEntry: 'github.com,140.82.112.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' - sshKeySecureFile: 'id_rsa' + - checkout: self + clean: false + fetchDepth: 1 # Fetch only one commit + - task: NodeTool@0 inputs: - versionSpec: '12.16.2' + versionSpec: '12.18' displayName: 'Install Node.js' - - task: PowerShell@2 - displayName: 'delay 10' - inputs: - targetType: 'inline' - script: | - # Write your PowerShell commands here. - start-sleep -s 10 - - task: CmdLine@2 - displayName: 'docker logs' - inputs: - script: 'docker logs mssql' - - script: - npm install - displayName: 'npm install' - - task: CmdLine@2 - displayName: 'npm install mssql' - inputs: - script: 'npm install mssql' - workingDirectory: '$(build.sourcesdirectory)' - - task: CmdLine@2 - displayName: 'docker list' - inputs: - script: 'docker ps -all' - - task: CmdLine@2 - displayName: 'Create test database' - inputs: - script: 'sqlcmd -S localhost -d master -U sa -P "yourStrong(!)Password" -Q "CREATE DATABASE test"' - - task: DownloadSecureFile@1 - name: ormConfig - displayName: 'Download ormconfig.ts' - inputs: - secureFile: 'ormconfig.ts' + + - script: npm i + displayName: 'Clean dependencies' + + - script: | + npx nx run ues-recycling-angular:build --prod --baseHref /recycling/ --skip-nx-cache + displayName: 'Build ues-recycling-angular' + - task: CopyFiles@2 - displayName: 'Copy ormconfig' inputs: - SourceFolder: $(Agent.TempDirectory) - Contents: ormconfig.ts - TargetFolder: $(System.DefaultWorkingDirectory)/libs/covid/data-api/src/lib - - - script: npx nx affected --target=test --base=origin/development --parallel - - script: npx nx affected --target=lint --base=origin/development --parallel - - script: npx nx affected --target=build --base=origin/development --prod --parallel \ No newline at end of file + Contents: 'dist/**' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + displayName: 'Copy Distribution Files' + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory) # dist or build files + ArtifactName: 'js-monorepo' # output artifact named www + displayName: 'Publish Artifacts' + diff --git a/jest.config.js b/jest.config.js index 430b619757..7d9337c0a6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -106,6 +106,11 @@ module.exports = { '/libs/two/dashboard', '/libs/two/data-access', '/libs/common/ngx/auth', - '/apps/ues-operations-nest' + '/apps/ues-operations-nest', + '/apps/ues-recycling-data-api-nest', + '/apps/ues-recycling-angular', + '/libs/ues/recycling/common/entities', + '/libs/ues/recycling/data-api', + '/libs/ues/recycling/ngx' ] }; diff --git a/libs/aggiemap/src/lib/popups/components/tent-zone/tent-zone.component.spec.ts b/libs/aggiemap/src/lib/popups/components/tent-zone/tent-zone.component.spec.ts index c576a5088f..61ce8f2369 100644 --- a/libs/aggiemap/src/lib/popups/components/tent-zone/tent-zone.component.spec.ts +++ b/libs/aggiemap/src/lib/popups/components/tent-zone/tent-zone.component.spec.ts @@ -1,24 +1,62 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { env, EnvironmentService } from '@tamu-gisc/common/ngx/environment'; +import { AppStorage } from '@tamu-gisc/common/ngx/local-store'; +import { Angulartics2Module } from 'angulartics2'; + +import { LOCAL_STORAGE, StorageServiceModule } from 'ngx-webstorage-service'; import { TentZonePopupComponent } from './tent-zone.component'; +import esri = __esri; + describe('TentZonePopupComponent', () => { let component: TentZonePopupComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TentZonePopupComponent] + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([]), + HttpClientTestingModule, + Angulartics2Module.forRoot(), + StorageServiceModule + ], + declarations: [TentZonePopupComponent], + providers: [ + EnvironmentService, + { + provide: env, + useValue: { + SearchSources: [], + NotificationEvents: [], + LayerSources: [] + } + }, + { + provide: AppStorage, + useExisting: LOCAL_STORAGE + } + ] }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(TentZonePopupComponent); component = fixture.componentInstance; - fixture.detectChanges(); + // fixture.detectChanges(); }); it('should create', () => { + component.data = ({ + attributes: { + Number: 1111 + } + } as unknown) as esri.Graphic; + + fixture.detectChanges(); + expect(component).toBeTruthy(); }); }); diff --git a/libs/assets/powershell/iis_global_rewrite.ps1 b/libs/assets/powershell/iis_global_rewrite.ps1 new file mode 100644 index 0000000000..0339ec03b3 --- /dev/null +++ b/libs/assets/powershell/iis_global_rewrite.ps1 @@ -0,0 +1,53 @@ +param( + [parameter(Position=0, Mandatory=$true, HelpMessage="Name to be assigned to the redirect rule that is created.")] + [string]$RuleName, + [parameter(Position=1, Mandatory=$true, HelpMessage="URL pattern to match.")] + [string]$UrlPattern, + [parameter(Position=2, Mandatory=$true, HelpMessage="Rewrite type, 'url' or 'farm'")] + [string]$RewriteType, + [parameter(Position=3, Mandatory=$true, HelpMessage="URL or farm to redirect matching requests.")] + [string]$RedirectUrl +) + +if($RewriteType -eq 'farm') { + if((Get-WebConfigurationProperty –pspath MACHINE/WEBROOT/APPHOST –Filter "/webFarms/webFarm[@name='$RedirectUrl']" -Name ".").length -eq 0){ + throw "Provided farm does not exist. Please check the farm by provided name exists: $RedirectUrl"; + } +} + +$path = "MACHINE/WEBROOT/APPHOST"; +$baseRulesFilter= "/system.webServer/rewrite/globalRules"; +$baseRuleFilter= "$baseRulesFilter/rule"; +$filter = "$baseRuleFilter[@name='$RuleName']"; + +write-host ""; +write-host("Path: $path"); +write-host("Base Rules Filter: $baseRulesFilter"); +write-host("Base Rule Filter: $baseRuleFilter"); +write-host("Filter: $filter"); +write-host ""; + + +$ExistingRule = Get-WebConfigurationProperty –pspath $path –Filter $filter -Name "."; + +# Check if rule exists +if(($ExistingRule).length -eq 0) { + Add-WebConfigurationProperty –pspath $path –Filter $baseRulesFilter –Name "." -Force –Value @{name=$RuleName;patternSyntax='Wildcard';stopProcessing='False';}; + Write-Host "$RuleName has been created for the site host. Applying match rule, conditions, and action..." –BackgroundColor DarkGreen –ForegroundColor Gray; +} else { + Write-Host "$RuleName rule already exists. Removing rule conditions to avoid duplicates."; + Remove-WebConfigurationProperty –pspath $path –Filter $filter -Name "conditions"; +} + +Set-WebConfigurationProperty -pspath $path -Filter "$filter/match" -Name "url" -Value $UrlPattern; +Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "type" –Value "Rewrite"; + +if($RewriteType -eq "url"){ + Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "url" –Value $RedirectUrl; +} elseif ($RewriteType -eq "farm"){ + Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "url" –Value "http://$RedirectUrl/{R:1}"; +} + +Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "appendQueryString" –Value "true"; + +Write-Host "$RuleName for $SiteBinding has been updated successfully." –BackgroundColor DarkGreen –ForegroundColor Gray; diff --git a/libs/assets/powershell/iis_global_rewrite_clear_all.ps1 b/libs/assets/powershell/iis_global_rewrite_clear_all.ps1 new file mode 100644 index 0000000000..ee0e8b7422 --- /dev/null +++ b/libs/assets/powershell/iis_global_rewrite_clear_all.ps1 @@ -0,0 +1,15 @@ +$path = "MACHINE/WEBROOT/APPHOST"; +$baseRulesFilter= "/system.webServer/rewrite/globalRules"; +$baseRuleFilter= "$baseRulesFilter/rule"; +$filter = "$baseRuleFilter"; + +write-host ""; +write-host("Path: $path"); +write-host("Base Rules Filter: $baseRulesFilter"); +write-host("Base Rule Filter: $baseRuleFilter"); +write-host("Filter: $filter"); +write-host ""; + +Clear-WebConfiguration -PSPath $path -Filter $filter; + +Write-Host("All global rewrite rules have been cleared."); diff --git a/libs/assets/powershell/iis_site_rewrite.ps1 b/libs/assets/powershell/iis_site_rewrite.ps1 new file mode 100644 index 0000000000..00fa3f960d --- /dev/null +++ b/libs/assets/powershell/iis_site_rewrite.ps1 @@ -0,0 +1,44 @@ +param( + [parameter(Position=0, Mandatory=$true, HelpMessage="Value of the target sites IIS binding.")] + [string]$SiteBinding, + [parameter(Position=1, Mandatory=$true, HelpMessage="Name to be assigned to the redirect rule that is created.")] + [string]$RuleName, + [parameter(Position=2, Mandatory=$true, HelpMessage="URL pattern to match.")] + [string]$UrlPattern, + [parameter(Position=3, Mandatory=$true, HelpMessage="URL to redirect matching requests.")] + [string]$RedirectUrl +) + +$path = "MACHINE/WEBROOT/APPHOST/$SiteBinding"; +$baseRulesFilter= "/system.webServer/rewrite/rules"; +$baseRuleFilter= "$baseRulesFilter/rule"; +$filter = "$baseRuleFilter[@name='$RuleName']"; + +write-host ""; +write-host("Path: $path"); +write-host("Base Rules Filter: $baseRulesFilter"); +write-host("Base Rule Filter: $baseRuleFilter"); +write-host("Filter: $filter"); +write-host ""; + + +$ExistingRule = Get-WebConfigurationProperty –pspath $path –Filter $filter -Name "."; + +# Check if rule exists +if(($ExistingRule).length -eq 0) { + Add-WebConfigurationProperty –pspath $path –Filter $baseRulesFilter –Name "." -Force –Value @{name=$RuleName;patternSyntax='Wildcard';stopProcessing='False';}; + Write-Host "$RuleName has been created for $SiteBinding. Applying match rule, conditions, and action..." –BackgroundColor DarkGreen –ForegroundColor Gray; +} else { + Write-Host "$RuleName rule already exists. Removing rule conditions to avoid duplicates."; + Remove-WebConfigurationProperty –pspath $path –Filter $filter -Name "conditions"; +} + +Set-WebConfigurationProperty -pspath $path -Filter "$filter/match" -Name "url" -Value $UrlPattern; +Set-WebConfigurationProperty –pspath $path –Filter "$filter/conditions" -Name "logicalGrouping" -Value "MatchAll"; +Add-WebConfigurationProperty –pspath $path –Filter "$filter/conditions" –Name "." –Value @{input="{REQUEST_FILENAME}";matchType="IsFile";negate="true";}; +Add-WebConfigurationProperty –pspath $path –Filter "$filter/conditions" –Name "." –Value @{input="{REQUEST_FILENAME}";matchType="IsDirectory";negate="true";}; +Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "type" –Value "Rewrite"; +Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "url" –Value $RedirectUrl; +Set-WebConfigurationProperty –pspath $path –Filter "$filter/action" –Name "appendQueryString" –Value "true"; + +Write-Host "$RuleName for $SiteBinding has been updated successfully." –BackgroundColor DarkGreen –ForegroundColor Gray; diff --git a/libs/assets/powershell/iis_site_rewrite_clear_all.ps1 b/libs/assets/powershell/iis_site_rewrite_clear_all.ps1 new file mode 100644 index 0000000000..3b77f35ada --- /dev/null +++ b/libs/assets/powershell/iis_site_rewrite_clear_all.ps1 @@ -0,0 +1,20 @@ +param( + [parameter(Position=0, Mandatory=$true, HelpMessage="Value of the target sites IIS binding.")] + [string]$SiteBinding +) + +$path = "MACHINE/WEBROOT/APPHOST/$SiteBinding"; +$baseRulesFilter= "/system.webServer/rewrite/rules"; +$baseRuleFilter= "$baseRulesFilter/rule"; +$filter = "$baseRuleFilter"; + +write-host ""; +write-host("Path: $path"); +write-host("Base Rules Filter: $baseRulesFilter"); +write-host("Base Rule Filter: $baseRuleFilter"); +write-host("Filter: $filter"); +write-host ""; + +Clear-WebConfiguration -PSPath $path -Filter $filter; + +Write-Host("All rewrite rules have been cleared for $path"); diff --git a/libs/assets/powershell/iis_web_farm.ps1 b/libs/assets/powershell/iis_web_farm.ps1 new file mode 100644 index 0000000000..57cedb92be --- /dev/null +++ b/libs/assets/powershell/iis_web_farm.ps1 @@ -0,0 +1,43 @@ +param( + [parameter(Position=0, Mandatory=$true, HelpMessage="Server farm name")] + [string]$FarmName, + [parameter(Position=1, Mandatory=$true, HelpMessage="Address")] + [string]$Address, + [parameter(Position=2, Mandatory=$true, HelpMessage="HTTP Port")] + [int]$HttpPort +) + + +$path = "MACHINE/WEBROOT/APPHOST"; +$baseRulesFilter= "/webFarms"; +$baseRuleFilter= "$baseRulesFilter/webFarm"; +$filter = "$baseRuleFilter[@name='$FarmName ($HttpPort)']"; + +write-host ""; +write-host("Path: $path"); +write-host("Base Rules Filter: $baseRulesFilter"); +write-host("Base Rule Filter: $baseRuleFilter"); +write-host("Filter: $filter"); +write-host ""; + + +$ExistingRule = Get-WebConfigurationProperty –pspath $path –Filter $filter -Name "."; + +# Check if rule exists and remove it if it does. +if(($ExistingRule).length -gt 0) { + Write-Host "$FarmName web farm already exists. Removing farm servers to avoid duplicates."; + Remove-WebConfigurationProperty –pspath $path –Filter $filter -Name "."; +} else { + Add-WebConfigurationProperty –pspath $path –Filter $baseRulesFilter –Name "." -Force –Value @{name="$FarmName ($HttpPort)";patternSyntax='Wildcard';stopProcessing='False';}; + Write-Host "$FarmName web farm has been created for the site host." –BackgroundColor DarkGreen –ForegroundColor Gray; +} + +# Add a farm server entry +Write-Host "Adding server farm entry..."; +Add-WebConfiguration –pspath $path –Filter $filter –Value (@{address=$Address; enabled=$true}); + +# Set the farm server port +Write-Host "Setting port on server entry..."; +Set-WebConfigurationProperty -pspath $path -Filter "$filter/server[@address='$Address']" -Name "applicationRequestRouting" -Value @{httpPort=$HttpPort}; + +Write-Host "Server farm named $FarmName with address $Address and port $HttpPort have been created successfully." –BackgroundColor DarkGreen –ForegroundColor Gray; diff --git a/libs/common/ngx/auth/src/lib/services/auth.service.ts b/libs/common/ngx/auth/src/lib/services/auth.service.ts index bc3767e816..15931acf1d 100644 --- a/libs/common/ngx/auth/src/lib/services/auth.service.ts +++ b/libs/common/ngx/auth/src/lib/services/auth.service.ts @@ -10,7 +10,11 @@ import { EnvironmentService } from '@tamu-gisc/common/ngx/environment'; providedIn: 'root' }) export class AuthService { - public authOptions: AuthOptions; + // Default auth options so they can be overwritten. + public authOptions: AuthOptions = { + url: undefined, + attach_href: undefined + }; constructor(private http: HttpClient, private env: EnvironmentService) { if (this.env.value('auth_url', true)) { diff --git a/libs/maps/esri/src/lib/services/map.service.ts b/libs/maps/esri/src/lib/services/map.service.ts index e79cb04d7e..2992a8d954 100644 --- a/libs/maps/esri/src/lib/services/map.service.ts +++ b/libs/maps/esri/src/lib/services/map.service.ts @@ -688,7 +688,7 @@ export class EsriMapService { export interface MapConfig { basemap: MapProperties; - view: MapViewProperties; + view: ViewProperties; } interface MapProperties extends esri.MapProperties { diff --git a/libs/maps/esri/src/lib/services/module-provider.service.ts b/libs/maps/esri/src/lib/services/module-provider.service.ts index 3f9dd65665..1198033359 100644 --- a/libs/maps/esri/src/lib/services/module-provider.service.ts +++ b/libs/maps/esri/src/lib/services/module-provider.service.ts @@ -191,6 +191,10 @@ export class EsriModuleProviderService { { class: 'esri/layers/GroupLayer', name: 'GroupLayer' + }, + { + class: 'esri/geometry/Circle', + name: 'Circle' } ]; diff --git a/libs/sass/modules/_esri_map.scss b/libs/sass/modules/_esri_map.scss index 14f35b6c0e..7e54378364 100644 --- a/libs/sass/modules/_esri_map.scss +++ b/libs/sass/modules/_esri_map.scss @@ -16,6 +16,10 @@ } } + &.horizontal { + @include flex-direction(row); + } + &.bottom { bottom: 0.75rem; } diff --git a/libs/ues/recycling/common/entities/.eslintrc.json b/libs/ues/recycling/common/entities/.eslintrc.json new file mode 100644 index 0000000000..c448193096 --- /dev/null +++ b/libs/ues/recycling/common/entities/.eslintrc.json @@ -0,0 +1 @@ +{ "extends": "../../../../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } diff --git a/libs/ues/recycling/common/entities/README.md b/libs/ues/recycling/common/entities/README.md new file mode 100644 index 0000000000..055ca8ce8a --- /dev/null +++ b/libs/ues/recycling/common/entities/README.md @@ -0,0 +1,7 @@ +# ues-recycling-common-entities + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ues-recycling-common-entities` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/ues/recycling/common/entities/jest.config.js b/libs/ues/recycling/common/entities/jest.config.js new file mode 100644 index 0000000000..67fa5f51ab --- /dev/null +++ b/libs/ues/recycling/common/entities/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'ues-recycling-common-entities', + preset: '../../../../../jest.preset.js', + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json' + } + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../../../coverage/libs/ues/recycling/common/entities' +}; diff --git a/libs/ues/recycling/common/entities/src/index.ts b/libs/ues/recycling/common/entities/src/index.ts new file mode 100644 index 0000000000..40e4cdd020 --- /dev/null +++ b/libs/ues/recycling/common/entities/src/index.ts @@ -0,0 +1 @@ +export * from './lib/ues-recycling-common-entities'; diff --git a/libs/ues/recycling/common/entities/src/lib/base/all.entity.ts b/libs/ues/recycling/common/entities/src/lib/base/all.entity.ts new file mode 100644 index 0000000000..7296524be5 --- /dev/null +++ b/libs/ues/recycling/common/entities/src/lib/base/all.entity.ts @@ -0,0 +1,58 @@ +import { + Entity, + BaseEntity, + PrimaryColumn, + UpdateDateColumn, + CreateDateColumn, + BeforeUpdate, + BeforeInsert, + Column, + OneToMany, + ManyToOne +} from 'typeorm'; + +import { v4 as guid } from 'uuid'; + +@Entity() +export class TimeStampEntity extends BaseEntity { + @UpdateDateColumn() + public updated: Date; + + @CreateDateColumn() + public created: Date; +} + +@Entity() +export class GuidIdentity extends TimeStampEntity { + @PrimaryColumn() + public guid: string; + + @BeforeUpdate() + @BeforeInsert() + private generateGuid(): void { + if (this.guid === undefined) { + this.guid = guid(); + } + } +} + +@Entity({ name: 'location' }) +export class Location extends GuidIdentity { + @Column() + public id: string; + + @OneToMany((type) => Result, (result) => result.location) + public results: Result[]; +} + +@Entity({ name: 'results' }) +export class Result extends GuidIdentity { + @Column({ type: 'float' }) + public value: number; + + @Column({ nullable: false }) + public date: Date; + + @ManyToOne((type) => Location, { cascade: true }) + public location: Location; +} diff --git a/libs/ues/recycling/common/entities/src/lib/ues-recycling-common-entities.ts b/libs/ues/recycling/common/entities/src/lib/ues-recycling-common-entities.ts new file mode 100644 index 0000000000..da73c63607 --- /dev/null +++ b/libs/ues/recycling/common/entities/src/lib/ues-recycling-common-entities.ts @@ -0,0 +1 @@ +export * from './base/all.entity'; diff --git a/libs/ues/recycling/common/entities/tsconfig.json b/libs/ues/recycling/common/entities/tsconfig.json new file mode 100644 index 0000000000..acb2f2f3a1 --- /dev/null +++ b/libs/ues/recycling/common/entities/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/ues/recycling/common/entities/tsconfig.lib.json b/libs/ues/recycling/common/entities/tsconfig.lib.json new file mode 100644 index 0000000000..c3e41d19ab --- /dev/null +++ b/libs/ues/recycling/common/entities/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "types": [] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/ues/recycling/common/entities/tsconfig.spec.json b/libs/ues/recycling/common/entities/tsconfig.spec.json new file mode 100644 index 0000000000..49abaa3925 --- /dev/null +++ b/libs/ues/recycling/common/entities/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js", "**/*.spec.jsx", "**/*.d.ts"] +} diff --git a/libs/ues/recycling/data-api/.eslintrc.json b/libs/ues/recycling/data-api/.eslintrc.json new file mode 100644 index 0000000000..4f2c1729fa --- /dev/null +++ b/libs/ues/recycling/data-api/.eslintrc.json @@ -0,0 +1 @@ +{ "extends": "../../../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } diff --git a/libs/ues/recycling/data-api/README.md b/libs/ues/recycling/data-api/README.md new file mode 100644 index 0000000000..60287b13db --- /dev/null +++ b/libs/ues/recycling/data-api/README.md @@ -0,0 +1,7 @@ +# ues-recycling-data-api + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ues-recycling-data-api` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/ues/recycling/data-api/jest.config.js b/libs/ues/recycling/data-api/jest.config.js new file mode 100644 index 0000000000..9a3e0acebf --- /dev/null +++ b/libs/ues/recycling/data-api/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'ues-recycling-data-api', + preset: '../../../../jest.preset.js', + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json' + } + }, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../../coverage/libs/ues/recycling/data-api' +}; diff --git a/libs/ues/recycling/data-api/src/index.ts b/libs/ues/recycling/data-api/src/index.ts new file mode 100644 index 0000000000..63265969f0 --- /dev/null +++ b/libs/ues/recycling/data-api/src/index.ts @@ -0,0 +1 @@ +export * from './lib/ues-recycling-data-api.module'; diff --git a/libs/ues/recycling/data-api/src/lib/modules/base/base.controller.spec.ts b/libs/ues/recycling/data-api/src/lib/modules/base/base.controller.spec.ts new file mode 100644 index 0000000000..9e48378145 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/base/base.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BaseController } from './base.controller'; + +describe('Base Controller', () => { + let controller: BaseController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [BaseController] + }).compile(); + + controller = module.get(BaseController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/libs/ues/recycling/data-api/src/lib/modules/base/base.controller.ts b/libs/ues/recycling/data-api/src/lib/modules/base/base.controller.ts new file mode 100644 index 0000000000..88bc445844 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/base/base.controller.ts @@ -0,0 +1,48 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { BaseEntity, DeepPartial } from 'typeorm'; + +import { BaseService } from './base.service'; + +@Controller('base') +export class BaseController { + constructor(private s: BaseService) {} + + @Get('') + public getAll() { + return this.s.getAll(); + } + + @Get(':id') + public getOne(@Param() params) { + return this.s.getOne({ + where: { + guid: params.id + } + }); + } + + @Post('') + public insert(@Body() body: DeepPartial) { + if (body) { + return this.s.createOne(body); + } + } + + @Patch(':id') + public update(@Param() params, @Body() body: DeepPartial) { + if (params && body) { + return this.s.updateOne({ where: { guid: params.id } }, body); + } else { + throw new Error('Input parameter missing'); + } + } + + @Delete(':id') + public delete(@Param() params) { + if (params) { + return this.s.deleteOne({ where: { guid: params.id } }); + } else { + throw new Error('Input parameter missing'); + } + } +} diff --git a/libs/ues/recycling/data-api/src/lib/modules/base/base.service.spec.ts b/libs/ues/recycling/data-api/src/lib/modules/base/base.service.spec.ts new file mode 100644 index 0000000000..32a2cdcaa2 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/base/base.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BaseService } from './base.service'; + +describe('BaseService', () => { + let service: BaseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [BaseService] + }).compile(); + + service = module.get(BaseService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/ues/recycling/data-api/src/lib/modules/base/base.service.ts b/libs/ues/recycling/data-api/src/lib/modules/base/base.service.ts new file mode 100644 index 0000000000..44b23729ca --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/base/base.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { BaseEntity, DeepPartial, FindManyOptions, FindOneOptions, Repository } from 'typeorm'; + +@Injectable() +export class BaseService { + constructor(public readonly repository: Repository) {} + + public async createOne(entity: DeepPartial) { + return await this.repository.create(entity).save(); + } + + public async getOne(options: FindOneOptions) { + return await this.repository.findOne(options); + } + + public async getMany(options: FindManyOptions) { + return await this.repository.find(options); + } + + public async getAll() { + return await this.repository.find(); + } + + public async updateOne(find: FindManyOptions, updates: DeepPartial) { + const record = await this.repository.findOne(find); + + Object.assign(record, updates); + + return await record.save(); + } + + public async deleteOne(options: FindManyOptions) { + const record = await this.repository.findOne(options); + + return await record.remove(); + } +} diff --git a/libs/ues/recycling/data-api/src/lib/modules/locations/locations.controller.spec.ts b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.controller.spec.ts new file mode 100644 index 0000000000..202cbbefa8 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LocationsController } from './locations.controller'; + +describe('Locations Controller', () => { + let controller: LocationsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [LocationsController] + }).compile(); + + controller = module.get(LocationsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/libs/ues/recycling/data-api/src/lib/modules/locations/locations.controller.ts b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.controller.ts new file mode 100644 index 0000000000..1aed1a2c0f --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; + +import { AzureIdpGuard } from '@tamu-gisc/oidc/client'; +import { Location } from '@tamu-gisc/ues/recycling/common/entities'; + +import { BaseController } from '../base/base.controller'; +import { LocationsService } from './locations.service'; + +@Controller('locations') +export class LocationsController extends BaseController { + constructor(private service: LocationsService) { + super(service); + } + + @UseGuards(AzureIdpGuard) + @Get('') + public getLocations() { + return this.service.getLocations(); + } + + @UseGuards(AzureIdpGuard) + @Get('results') + public getResultsPerLocation() { + return this.service.getLocationAndResults(); + } +} diff --git a/libs/ues/recycling/data-api/src/lib/modules/locations/locations.module.ts b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.module.ts new file mode 100644 index 0000000000..4b4e44cdc5 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; + +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { Location } from '@tamu-gisc/ues/recycling/common/entities'; + +import { LocationsService } from './locations.service'; +import { LocationsController } from './locations.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Location])], + controllers: [LocationsController], + providers: [LocationsService] +}) +export class LocationsModule {} diff --git a/libs/ues/recycling/data-api/src/lib/modules/locations/locations.service.spec.ts b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.service.spec.ts new file mode 100644 index 0000000000..c74c27c868 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LocationsService } from './locations.service'; + +describe('LocationsService', () => { + let service: LocationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LocationsService] + }).compile(); + + service = module.get(LocationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/ues/recycling/data-api/src/lib/modules/locations/locations.service.ts b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.service.ts new file mode 100644 index 0000000000..f76f7e5fb7 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/locations/locations.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { Location } from '@tamu-gisc/ues/recycling/common/entities'; + +import { BaseService } from '../base/base.service'; + +@Injectable() +export class LocationsService extends BaseService { + constructor(@InjectRepository(Location) private repo: Repository) { + super(repo); + } + + public getLocations() { + return this.repo.find({ + order: { + id: 'ASC' + } + }); + } + + public getLocationAndResults() { + return this.repo.find({ + order: { + id: 'ASC' + }, + relations: ['results'] + }); + } +} diff --git a/libs/ues/recycling/data-api/src/lib/modules/results/results.controller.spec.ts b/libs/ues/recycling/data-api/src/lib/modules/results/results.controller.spec.ts new file mode 100644 index 0000000000..86fff78f04 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/results/results.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ResultsController } from './results.controller'; + +describe('Results Controller', () => { + let controller: ResultsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ResultsController] + }).compile(); + + controller = module.get(ResultsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/libs/ues/recycling/data-api/src/lib/modules/results/results.controller.ts b/libs/ues/recycling/data-api/src/lib/modules/results/results.controller.ts new file mode 100644 index 0000000000..7fc69054c5 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/results/results.controller.ts @@ -0,0 +1,66 @@ +import { + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + UploadedFile, + UseInterceptors, + UseGuards +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; + +import { Result } from '@tamu-gisc/ues/recycling/common/entities'; +import { AzureIdpGuard } from '@tamu-gisc/oidc/client'; + +import { BaseController } from '../base/base.controller'; +import { ResultsService } from './results.service'; + +@Controller('results') +export class ResultsController extends BaseController { + constructor(private service: ResultsService) { + super(service); + } + + @UseGuards(AzureIdpGuard) + @Get() + public getAllResults() { + return this.service.getResults({ options: { groupByDate: true } }); + } + + @UseGuards(AzureIdpGuard) + @Get('latest/:id/:days') + public getLatestForLocationForDays(@Param() params: { id: string; days: string }) { + return this.service.getLatestNValuesForLocation(params.id, params.days); + } + + @UseGuards(AzureIdpGuard) + @Get('latest/:id') + public getLatestForLocation(@Param() params: { id: string }) { + return this.service.getLatestNValuesForLocation(params.id, undefined); + } + + @UseGuards(AzureIdpGuard) + @Get('latest/average') + public getLatestAverage() { + return this.service.getLatestNValueAverageForLocation(undefined, 1); + } + + @UseGuards(AzureIdpGuard) + @Get('latest') + public getLatestValue() { + return this.service.getLatestNValuesForLocation(undefined, 1); + } + + @UseGuards(AzureIdpGuard) + @Post('csv') + @UseInterceptors(FileInterceptor('file', { dest: '../files' })) + public handleFileUpload(@UploadedFile() file) { + if (file) { + return this.service.handleFileUpload(file.filename); + } else { + throw new HttpException('Input parameter missing', HttpStatus.BAD_REQUEST); + } + } +} diff --git a/libs/ues/recycling/data-api/src/lib/modules/results/results.module.ts b/libs/ues/recycling/data-api/src/lib/modules/results/results.module.ts new file mode 100644 index 0000000000..426f1b4521 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/results/results.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { Result, Location } from '@tamu-gisc/ues/recycling/common/entities'; + +import { ResultsController } from './results.controller'; +import { ResultsService } from './results.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Result, Location])], + controllers: [ResultsController], + providers: [ResultsService] +}) +export class ResultsModule {} diff --git a/libs/ues/recycling/data-api/src/lib/modules/results/results.service.spec.ts b/libs/ues/recycling/data-api/src/lib/modules/results/results.service.spec.ts new file mode 100644 index 0000000000..26ada108a7 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/results/results.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ResultsService } from './results.service'; + +describe('ResultsService', () => { + let service: ResultsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ResultsService] + }).compile(); + + service = module.get(ResultsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/ues/recycling/data-api/src/lib/modules/results/results.service.ts b/libs/ues/recycling/data-api/src/lib/modules/results/results.service.ts new file mode 100644 index 0000000000..cca563daca --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/modules/results/results.service.ts @@ -0,0 +1,325 @@ +import * as fs from 'fs'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import * as Papa from 'papaparse'; + +import { Result, Location } from '@tamu-gisc/ues/recycling/common/entities'; +import { groupBy } from '@tamu-gisc/common/utils/collection'; + +import { BaseService } from '../base/base.service'; + +@Injectable() +export class ResultsService extends BaseService { + constructor( + @InjectRepository(Result) private repo: Repository, + @InjectRepository(Location) private locationRepo: Repository + ) { + super(repo); + } + + public async getResults(args: IResultsQueryArgs) { + const query = this.repo.createQueryBuilder('result').innerJoinAndSelect('result.location', 'location'); + + // Apply conditions + if (args && args.limiters) { + if (args.limiters.id) { + query.andWhere(`location.id = ${args.limiters.id}`); + } + } + + // Apply ordering + query.orderBy('location.id', 'ASC').addOrderBy('result.date', 'DESC'); + + const ret = await query.getMany(); + + if (args && args.options) { + if (args.options.groupByDate) { + return groupBy(ret, 'date', 'date'); + } + } + + return ret; + } + + /** + * For each location, returns the latest n recorded values. + */ + public async getLatestNValuesForLocation(id: string, days?: number | string) { + const locationsQuery = await this.locationRepo.createQueryBuilder('locations'); + + if (id !== undefined) { + locationsQuery.andWhere(`id = '${id}'`); + } + + const locations = await locationsQuery.orderBy('id', 'ASC').getMany(); + + const queries = locations.map((l) => { + const query = this.repo + .createQueryBuilder('result') + .innerJoinAndSelect('result.location', 'location') + .where(`location.id = '${l.id}'`) + .orderBy('date', 'DESC'); + + if (days !== undefined) { + query.limit(typeof days === 'string' ? parseInt(days, 10) : days); + } + + return query.getMany(); + }); + + const resolved = await Promise.all(queries); + + return resolved.reduce((acc, curr) => { + return [...acc, ...curr]; + }, []); + } + + public async getLatestNValueAverageForLocation(id?: string, days?: number | string): Promise { + const results = await this.getLatestNValuesForLocation(id, days); + + const average = + results.reduce((acc, curr) => { + return acc + curr.value; + }, 0) / results.length; + + return { + average, + results + }; + } + + /** + * Parses an input file and inserts/updates into database. + * + * Synchronizes locations based on the delimited file header. + * + * Updates values wherever an existing value for a given location exists. + */ + public handleFileUpload(filename: string): Promise { + try { + return new Promise((r, rj) => { + let rowIndex = 0; + + // Calling papaparse abort() doesn't seem to terminate the stream with the error callback, + // and instead calls complete() which should only be reached if everything completed successfully. + let isError = false; + + // Create a readable stream that papaparse can understand. + const readStream = fs.createReadStream(`../files/${filename}`); + + let locations: Array; + + // Do the parsing + Papa.parse(readStream, { + header: true, + encoding: 'utf8', + step: async (results, parser, c) => { + parser.pause(); + + if (rowIndex === 0) { + rowIndex++; + + try { + locations = await this.synchronizeLocations(results.meta.fields, parser); + } catch (err) { + isError = true; + rj(err); + parser.abort(); + } + } + + if (isError === false) { + try { + await this.processDataRow(results, parser, locations); + parser.resume(); + } catch (err) { + rj(err); + + parser.abort(); + } + } + }, + error: async (error, file) => { + rj(error); + }, + complete: async (results, file) => { + r({ status: HttpStatus.OK, message: 'File processed successfully' }); + } + }); + }); + } catch (err) { + throw new HttpException('is this returned?', HttpStatus.BAD_REQUEST); + } + } + + private async processDataRow(row, parser, locations: Array) { + try { + // There is a weird issue with the CSV parser that requires the keys to be + // lower-cased and trimmed for reliable access. + const trimmed = Object.entries(row.data).reduce((acc, [key, value]) => { + // Handle empty headers, which cause errors when assigning values. + if (key === '') { + return acc; + } + + const keyName = key.toLowerCase().trim(); + + acc[keyName] = value; + + return acc; + }, {}); + + // From the parsed row, pluck out the parsed date string value and convert it to a Date object. + const rowDate = new Date(trimmed['weeks']); + + const dataForRowDate = await this.getResultsForDate(rowDate); + + // If data exist for this date, attempt to update, otherwise all location values + // for the current date. + if (dataForRowDate.length > 0) { + return await this.updateValuesForDate(dataForRowDate, trimmed); + } else { + return await this.addValuesForDate(rowDate, trimmed, locations); + } + } catch (err) { + throw new HttpException('Could not update at least one row from file.', HttpStatus.BAD_REQUEST); + } + } + + private async synchronizeLocations(locations: Array, parser) { + try { + // Remove the first field. It is the date column. + // + // The remaining locations will be '-' concatenated arrays which need to be + // prepared by transforming into tier and zone objects . + const locationStrings = locations.slice(1, locations.length).filter((header) => { + // Filters out any empty headers + return header !== ''; + }); + + const mappedLocationStrings = locationStrings.map((location) => { + return { + id: location + }; + }); + + const existingLocations = await this.locationRepo.find({ + where: mappedLocationStrings + }); + + // Diff the list of entities that exist with the provided list. This list + // will represent the locations that are not yet added to the db. + // + // If they have not been added, yet, they will be added. + const nonExisting = locationStrings.filter((l) => { + return ( + existingLocations.find((dbLocation) => { + return dbLocation.id === l; + }) === undefined + ); + }); + + if (nonExisting.length === 0) { + return existingLocations; + } + + // Cast location objects to Location entities. + const entitiesFromNonExisting = nonExisting.map((nonExistingLocation) => { + const entity = new Location(); + + entity.id = nonExistingLocation; + + return entity; + }); + + await this.locationRepo.save(entitiesFromNonExisting); + + // Return a list of locations as they can be used by other parts of this service to + // avoid unnecessary lookups. + return this.locationRepo.find({ + where: mappedLocationStrings + }); + } catch (err) { + throw new HttpException('Could not synchronize locations', HttpStatus.BAD_REQUEST); + } + } + + private async getResultsForDate(date: Date) { + return this.repo.find({ + where: { + date: date + }, + relations: ['location'] + }); + } + + private async addValuesForDate(date: Date, results: IParsedResultRow, locations: Array) { + const resultEntities = Object.entries(results) + .map(([key, value], index, arr) => { + if (key === 'weeks') { + return undefined; + } + + const result = new Result(); + + result.date = date; + result.location = locations.find((l) => l.id.toLowerCase().trim() === key); + result.value = parseFloat(value); + + return result; + }) + .filter((r) => r); // Filter undefined values (i.e. Date row) + + return this.repo.save(resultEntities); + } + + private async updateValuesForDate(dbResults: Array, newResults: IParsedResultRow) { + // Filter and update the value if any matching new value exists. + const entitiesToUpdate = dbResults.reduce((acc, curr) => { + const matchingNewResultRowValue = newResults[`${curr.location.id.trim().toLowerCase()}`]; + + // Return early if there is no new value for the current db result entity. + if (matchingNewResultRowValue === undefined) { + return acc; + } + + // Return early if the float parse results in not a real number + if (isNaN(parseFloat(matchingNewResultRowValue))) { + return acc; + } + + const parsedMatchingValue = parseFloat(matchingNewResultRowValue); + + // Do not update the entity if the value is the same + if (parsedMatchingValue === curr.value) { + return acc; + } + + curr.value = parsedMatchingValue; + + return [...acc, curr]; + }, []); + + return this.repo.save(entitiesToUpdate); + } +} + +export interface IParsedResultRow { + [key: string]: string; +} + +export interface IResultsQueryArgs { + limiters?: { + id?: number | string; + }; + options?: { + groupByDate?: boolean; + }; +} + +export interface IAverageResponse { + average: number; + results: Result[]; +} diff --git a/libs/ues/recycling/data-api/src/lib/ues-recycling-data-api.module.ts b/libs/ues/recycling/data-api/src/lib/ues-recycling-data-api.module.ts new file mode 100644 index 0000000000..897baafb99 --- /dev/null +++ b/libs/ues/recycling/data-api/src/lib/ues-recycling-data-api.module.ts @@ -0,0 +1,4 @@ +export * from './modules/locations/locations.module'; +export * from './modules/results/results.module'; + +export * from './modules/results/results.service'; diff --git a/libs/ues/recycling/data-api/tsconfig.json b/libs/ues/recycling/data-api/tsconfig.json new file mode 100644 index 0000000000..26b7b4afd1 --- /dev/null +++ b/libs/ues/recycling/data-api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/ues/recycling/data-api/tsconfig.lib.json b/libs/ues/recycling/data-api/tsconfig.lib.json new file mode 100644 index 0000000000..e3364d0c42 --- /dev/null +++ b/libs/ues/recycling/data-api/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es6" + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/ues/recycling/data-api/tsconfig.spec.json b/libs/ues/recycling/data-api/tsconfig.spec.json new file mode 100644 index 0000000000..541aa925eb --- /dev/null +++ b/libs/ues/recycling/data-api/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js", "**/*.spec.jsx", "**/*.d.ts"] +} diff --git a/libs/ues/recycling/ngx/README.md b/libs/ues/recycling/ngx/README.md new file mode 100644 index 0000000000..3aecedf1ac --- /dev/null +++ b/libs/ues/recycling/ngx/README.md @@ -0,0 +1,7 @@ +# ues-recycling-ngx + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ues-recycling-ngx` to execute the unit tests. diff --git a/libs/ues/recycling/ngx/jest.config.js b/libs/ues/recycling/ngx/jest.config.js new file mode 100644 index 0000000000..b0fc955642 --- /dev/null +++ b/libs/ues/recycling/ngx/jest.config.js @@ -0,0 +1,20 @@ +module.exports = { + displayName: 'ues-recycling-ngx', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: ['jest-preset-angular/build/InlineFilesTransformer', 'jest-preset-angular/build/StripStylesTransformer'] + } + } + }, + coverageDirectory: '../../../../coverage/libs/ues/recycling/ngx', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/libs/ues/recycling/ngx/src/index.ts b/libs/ues/recycling/ngx/src/index.ts new file mode 100644 index 0000000000..51eec7cba3 --- /dev/null +++ b/libs/ues/recycling/ngx/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/modules/data-access/data-access.module'; +export * from './lib/modules/data/data.module'; +export * from './lib/modules/map/map.module'; + +export * from './lib/modules/data-access/locations/locations.service'; +export * from './lib/modules/data-access/results/results.service'; diff --git a/libs/ues/recycling/ngx/src/lib/modules/core/core.module.ts b/libs/ues/recycling/ngx/src/lib/modules/core/core.module.ts new file mode 100644 index 0000000000..8033df132c --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/core/core.module.ts @@ -0,0 +1,7 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +@NgModule({ + declarations: [], + imports: [CommonModule] +}) +export class CoreModule {} diff --git a/libs/ues/recycling/ngx/src/lib/modules/core/services/recycling.service.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/core/services/recycling.service.spec.ts new file mode 100644 index 0000000000..88e3126db3 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/core/services/recycling.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { RecyclingService } from './recycling.service'; + +describe('RecyclingService', () => { + let service: RecyclingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RecyclingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/core/services/recycling.service.ts b/libs/ues/recycling/ngx/src/lib/modules/core/services/recycling.service.ts new file mode 100644 index 0000000000..b815a18523 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/core/services/recycling.service.ts @@ -0,0 +1,182 @@ +import { Injectable } from '@angular/core'; + +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { distinctUntilChanged, filter, map, pluck, shareReplay, switchMap } from 'rxjs/operators'; + +import { EsriMapService } from '@tamu-gisc/maps/esri'; +import { Result } from '@tamu-gisc/ues/recycling/common/entities'; + +import { ResultsService } from '../../data-access/results/results.service'; + +import esri = __esri; + +@Injectable({ + providedIn: 'root' +}) +export class RecyclingService { + public selectedLocationGraphic: BehaviorSubject = new BehaviorSubject(undefined); + public selectedLocationMeta: Observable; + public selectedLocationResults: Observable; + public selectedLocationRecyclingStats: Observable; + + public allLocationResults: Observable[]>; + public allLocationRecyclingStats: Observable; + + public allOrSelectedRecyclingStats: Observable; + + constructor(private mapService: EsriMapService, private resultsService: ResultsService) { + this.mapService.hitTest + .pipe( + map((snapshot) => { + const [graphic] = snapshot.graphics; + return graphic; + }), + filter((g) => { + return g !== undefined; + }), + // Filter out multiple emissions by feature id. This will prevent many xhr requests and limit other + // unnecessary UI reactive changes. + distinctUntilChanged((oldGraphic, newGraphic) => { + return oldGraphic.attributes.OBJECTID === newGraphic.attributes.OBJECTID; + }), + shareReplay(1) + ) + .subscribe((graphic) => { + this.selectedLocationGraphic.next(graphic); + }); + + this.selectedLocationMeta = this.selectedLocationGraphic.pipe( + switchMap((g) => { + if (g === undefined) { + return of(undefined); + } else { + return of(g).pipe(pluck('attributes')); + } + }), + shareReplay(1) + ); + + this.selectedLocationResults = this.selectedLocationMeta.pipe( + switchMap((meta) => { + if (meta === undefined) { + return of(undefined); + } else { + return of(meta).pipe( + switchMap((m) => { + const resolvedId = m.bldNum !== null ? m.bldNum : m.Name; + + return this.resultsService.getResultsForLocation({ id: resolvedId }); + }) + ); + } + }), + shareReplay(1) + ); + + this.selectedLocationRecyclingStats = this.selectedLocationResults.pipe( + map((results) => { + if (results === undefined) { + return undefined; + } else { + const totalRecycled = results.reduce((acc, curr) => { + const isTonUnit = curr.value % 1 !== 0; + + if (isTonUnit) { + // Convert tons to pounds + return acc + parseInt((curr.value * 2000).toFixed(0), 10); + } + + return acc + curr.value; + }, 0); + + return { + records: results.length, + total: totalRecycled, + results + } as RecyclingResultsStatistics; + } + }) + ); + + this.allLocationResults = this.resultsService.getResults().pipe( + map((groups) => { + return groups.map((group) => { + return { + date: new Date(group.identity as string), + value: group.items.reduce((acc, curr) => { + const isTonUnit = curr.value % 1 !== 0; + + if (isTonUnit) { + // Convert tons to pounds + return acc + parseInt((curr.value * 2000).toFixed(0), 10); + } + + return acc + curr.value; + }, 0), + location: undefined + }; + }); + }), + shareReplay(1) + ); + + this.allLocationRecyclingStats = this.allLocationResults.pipe( + map((results) => { + if (results === undefined) { + return undefined; + } else { + const totalRecycled = results.reduce((acc, curr) => { + const isTonUnit = curr.value % 1 !== 0; + + if (isTonUnit) { + // Convert tons to pounds + return acc + parseInt((curr.value * 2000).toFixed(0), 10); + } + + return acc + curr.value; + }, 0); + + return { + records: results.length, + total: totalRecycled, + results + } as RecyclingResultsStatistics; + } + }) + ); + + this.allOrSelectedRecyclingStats = this.selectedLocationRecyclingStats.pipe( + switchMap((stats) => { + if (stats === undefined) { + return this.allLocationRecyclingStats; + } else { + return of(stats); + } + }) + ); + } + + public clearSelected() { + this.selectedLocationGraphic.next(undefined); + } + + public setLocation(location: esri.Graphic) { + this.selectedLocationGraphic.next(location); + } +} + +export interface RecyclingLocationMetadata { + OBJECTID: number; + ID: number; + Name: string; + bldNum: string; + camPub: string; + style: string; + public_view: 'Yes' | 'No'; +} + +export interface RecyclingResultsStatistics { + records: number; + total: number; + results: Result[]; +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data-access/data-access.module.ts b/libs/ues/recycling/ngx/src/lib/modules/data-access/data-access.module.ts new file mode 100644 index 0000000000..9155c9a5b8 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data-access/data-access.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { LocationsService } from './locations/locations.service'; +import { ResultsService } from './results/results.service'; + +@NgModule({ + imports: [CommonModule], + declarations: [], + providers: [ResultsService, LocationsService] +}) +export class DataAccessModule {} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data-access/locations/locations.service.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/data-access/locations/locations.service.spec.ts new file mode 100644 index 0000000000..28caa58caa --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data-access/locations/locations.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LocationsService } from './locations.service'; + +describe('LocationsService', () => { + let service: LocationsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LocationsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/data-access/locations/locations.service.ts b/libs/ues/recycling/ngx/src/lib/modules/data-access/locations/locations.service.ts new file mode 100644 index 0000000000..f41748b963 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data-access/locations/locations.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { EnvironmentService } from '@tamu-gisc/common/ngx/environment'; +import { Location } from '@tamu-gisc/ues/recycling/common/entities'; + +@Injectable({ + providedIn: 'root' +}) +export class LocationsService { + private apiUrl: string; + + constructor(private http: HttpClient, private env: EnvironmentService) { + this.apiUrl = this.env.value('apiUrl') + 'locations'; + } + + public getLocations(): Observable> { + return this.http.get>(this.apiUrl, { withCredentials: true }); + } + + public getLocationsResults(): Observable> { + return this.http.get>(this.apiUrl + '/results', { withCredentials: true }); + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data-access/results/results.service.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/data-access/results/results.service.spec.ts new file mode 100644 index 0000000000..3d8f69f7b6 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data-access/results/results.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ResultsService } from './results.service'; + +describe('ResultsService', () => { + let service: ResultsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ResultsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/data-access/results/results.service.ts b/libs/ues/recycling/ngx/src/lib/modules/data-access/results/results.service.ts new file mode 100644 index 0000000000..7aca489ce3 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data-access/results/results.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { EnvironmentService } from '@tamu-gisc/common/ngx/environment'; +import { Result, Location } from '@tamu-gisc/ues/recycling/common/entities'; +import { IAverageResponse } from '@tamu-gisc/ues/recycling/data-api'; +import { Group } from '@tamu-gisc/common/utils/collection'; + +@Injectable({ + providedIn: 'root' +}) +export class ResultsService { + private apiUrl: string; + + constructor(private http: HttpClient, private env: EnvironmentService) { + this.apiUrl = this.env.value('apiUrl') + 'results'; + } + + public getResults() { + return this.http.get>>(this.apiUrl, { withCredentials: true }); + } + + public getResultsForLocation(location: Partial) { + return this.http.get>(`${this.apiUrl}/latest/${location.id}`, { + withCredentials: true + }); + } + + public getResultsForLocationForDays(location: Partial, days: number) { + return this.http.get>(`${this.apiUrl}/latest/${location.id}/${days}`, { + withCredentials: true + }); + } + + public getLatestResults() { + return this.http.get>(`${this.apiUrl}/latest`, { withCredentials: true }); + } + + public getLatestResultsAverage() { + return this.http.get(`${this.apiUrl}/latest/average`, { withCredentials: true }); + } + + public uploadData(data: FormData) { + return this.http.post(`${this.apiUrl}/csv`, data, { + reportProgress: true, + withCredentials: true + }); + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/data.component.html b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.html new file mode 100644 index 0000000000..46c5a30c35 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.html @@ -0,0 +1,16 @@ +
+

Recycling Data

+ +
+
+
+
Latest
+
Upload
+
+ +
+ +
+
+
+
diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/data.component.scss b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.scss new file mode 100644 index 0000000000..a99a3ad1db --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.scss @@ -0,0 +1,9 @@ +:host { + display: block; + padding-top: 3rem; +} + +.tab-content { + margin-top: 2rem; + margin-bottom: 2rem; +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/data.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.spec.ts new file mode 100644 index 0000000000..9b2c0f6b1f --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataComponent } from './data.component'; + +describe('DataComponent', () => { + let component: DataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DataComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/data.component.ts b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.ts new file mode 100644 index 0000000000..7d324220db --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/data.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'tamu-gisc-data', + templateUrl: './data.component.html', + styleUrls: ['./data.component.scss'] +}) +export class DataComponent implements OnInit { + constructor() {} + + public ngOnInit(): void {} +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/data.module.ts b/libs/ues/recycling/ngx/src/lib/modules/data/data.module.ts new file mode 100644 index 0000000000..9b29247975 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/data.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +import { DataComponent } from './data.component'; + +const routes: Routes = [ + { + path: '', + component: DataComponent, + children: [ + { + path: 'latest', + loadChildren: () => import('./pages/existing/existing.module').then((m) => m.ExistingModule) + }, + { + path: 'upload', + loadChildren: () => import('./pages/upload/upload.module').then((m) => m.UploadModule) + }, + { + path: '', + pathMatch: 'full', + redirectTo: 'latest' + } + ] + } +]; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(routes)], + declarations: [DataComponent] +}) +export class DataModule {} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.html b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.html new file mode 100644 index 0000000000..ddb9cdea60 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.html @@ -0,0 +1,26 @@ +
+ + + +
+ + + + + + + + + + + + + +
Date{{header}}
{{day?.identity | date : 'shortDate'}}{{day.items | resultLookup : header}}
+
+
+ + +

No data.

+
+
diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.scss b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.spec.ts new file mode 100644 index 0000000000..11fb630edc --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExistingComponent } from './existing.component'; + +describe('ExistingComponent', () => { + let component: ExistingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExistingComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExistingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.ts new file mode 100644 index 0000000000..0190be748e --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; +import { forkJoin, Observable } from 'rxjs'; +import { map, shareReplay } from 'rxjs/operators'; + +import { Group } from '@tamu-gisc/common/utils/collection'; +import { Result } from '@tamu-gisc/ues/recycling/common/entities'; + +import { LocationsService } from '../../../data-access/locations/locations.service'; +import { ResultsService } from '../../../data-access/results/results.service'; +@Component({ + selector: 'tamu-gisc-existing', + templateUrl: './existing.component.html', + styleUrls: ['./existing.component.scss'] +}) +export class ExistingComponent implements OnInit { + public tableData: Observable; + + constructor(private locationsService: LocationsService, private resultsService: ResultsService) {} + + public ngOnInit(): void { + this.tableData = forkJoin([this.locationsService.getLocations(), this.resultsService.getResults()]).pipe( + map(([locations, results]) => { + return { + fields: locations.map((location) => { + return location.id; + }), + days: results + }; + }), + shareReplay(1) + ); + } +} + +export interface IExistingDataTableData { + fields: Array; + days: Array>; +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.module.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.module.ts new file mode 100644 index 0000000000..274e31adce --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/existing/existing.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { ExistingComponent } from './existing.component'; +import { ResultLookupPipe } from '../../pipes/result-lookup.pipe'; + +const routes: Routes = [ + { + path: '', + component: ExistingComponent + } +]; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(routes)], + declarations: [ExistingComponent, ResultLookupPipe] +}) +export class ExistingModule {} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.html b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.html new file mode 100644 index 0000000000..fd91a7ad7a --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.html @@ -0,0 +1,25 @@ +
+
+
+
+
+ +

Select File

+
+
+
+
+
+ +
+
+ +
+

File was uploaded and processed successfully.

+ Go back to data table. +
+ +

There was an error processing the uploaded file. Please ensure you have provided the correct format.

+
+
+
diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.scss b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.scss new file mode 100644 index 0000000000..633f4a3ae7 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.scss @@ -0,0 +1,12 @@ +ul { + list-style-position: inside; +} + +.file-upload-container { + max-width: 15rem; + margin: 1rem auto; +} + +form { + margin-bottom: 2rem; +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.spec.ts new file mode 100644 index 0000000000..4b9d03cc38 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UploadComponent } from './upload.component'; + +describe('UploadComponent', () => { + let component: UploadComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UploadComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UploadComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.ts new file mode 100644 index 0000000000..c6bcd0fb51 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; + +import { ResultsService } from '../../../data-access/results/results.service'; + +@Component({ + selector: 'tamu-gisc-upload', + templateUrl: './upload.component.html', + styleUrls: ['./upload.component.scss'] +}) +export class UploadComponent implements OnInit { + public form: FormGroup; + public success = false; + public error = false; + + constructor(private fb: FormBuilder, private resultsService: ResultsService) {} + + public ngOnInit() { + this.form = this.fb.group({ + file: ['', Validators.required] + }); + } + + public upload() { + const formValue = this.form.getRawValue(); + + const data: FormData = new FormData(); + + data.append('file', formValue.file); + + this.resultsService.uploadData(data).subscribe( + (res) => { + console.log('Updated data', res); + this.success = true; + }, + (err) => { + this.error = true; + } + ); + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.module.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.module.ts new file mode 100644 index 0000000000..a628befeda --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pages/upload/upload.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { UIFormsModule } from '@tamu-gisc/ui-kits/ngx/forms'; + +import { UploadComponent } from './upload.component'; + +const routes: Routes = [ + { + path: '', + component: UploadComponent + } +]; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(routes), ReactiveFormsModule, UIFormsModule], + declarations: [UploadComponent] +}) +export class UploadModule {} diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pipes/result-lookup.pipe.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pipes/result-lookup.pipe.spec.ts new file mode 100644 index 0000000000..7079dae750 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pipes/result-lookup.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ResultLookupPipe } from './result-lookup.pipe'; + +describe('ResultLookupPipe', () => { + it('create an instance', () => { + const pipe = new ResultLookupPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/data/pipes/result-lookup.pipe.ts b/libs/ues/recycling/ngx/src/lib/modules/data/pipes/result-lookup.pipe.ts new file mode 100644 index 0000000000..2663040e43 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/data/pipes/result-lookup.pipe.ts @@ -0,0 +1,26 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { Result } from '@tamu-gisc/ues/recycling/common/entities'; + +@Pipe({ + name: 'resultLookup' +}) +export class ResultLookupPipe implements PipeTransform { + /** + * Accepts an array of sample results and a string representation of a location id (typically a tamu building number). + * + * This pipe finds within the results collection the object with matching tier and sample values, and returns + * associated sampling value. + */ + public transform(results: Array, locationId: string): unknown { + const match = results.find((res) => { + return `${res.location.id}` === locationId; + }); + + if (match) { + return match.value; + } else { + return null; + } + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.html b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.html new file mode 100644 index 0000000000..b8d20dae75 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.html @@ -0,0 +1,16 @@ +
+
+
+ search + +
+
+ +
+
close
+
+
+ + diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.scss b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.scss new file mode 100644 index 0000000000..32b5c2faa3 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.scss @@ -0,0 +1,91 @@ +@import 'libs/sass/mixins'; + +:host { + &:focus-within { + .dropdown { + display: initial; + } + } +} + +.card, +.input-line, +.action-section { + @include align-items(center); +} + +.card { + @include flex-direction(row); + @include justify-content(space-between); + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.input-line { + @include flexbox(); + color: #7e7e7e; + + span { + margin-right: 0.3rem; + } +} + +.material-icons { + color: #7e7e7e; +} + +.action-section { + @include flexbox(); + + .action { + cursor: pointer; + margin-left: 0.5rem; + margin-right: 0.5rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } +} + +form { + width: 100%; +} + +input[type='text'] { + width: 100%; + background: none; + padding: 0; + font-size: 1rem; + border: none; +} + +.dropdown { + background: #fafafa; + padding: 0.5rem 0; + position: absolute; + top: calc(100% - 1.75rem); + z-index: -1; + max-height: 45vh; + overflow: auto; + box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.15); + border-radius: 7pt; + width: 100%; + display: none; + + p { + position: relative; + padding: 0.5rem 1.5rem; + box-sizing: border-box; + cursor: pointer; + width: 100%; + + &:hover { + background: #eeeeee; + } + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.spec.ts new file mode 100644 index 0000000000..5204b3a0a3 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OmnitoolbarComponent } from './omnitoolbar.component'; + +describe('OmnitoolbarComponent', () => { + let component: OmnitoolbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ OmnitoolbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OmnitoolbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.ts new file mode 100644 index 0000000000..2f5a968ab4 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/omnitoolbar/omnitoolbar.component.ts @@ -0,0 +1,120 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, Subject } from 'rxjs'; +import { debounceTime, map, shareReplay } from 'rxjs/operators'; + +import { getObjectPropertyValues } from '@tamu-gisc/common/utils/object'; + +import { RecyclingService, RecyclingLocationMetadata } from '../../../core/services/recycling.service'; + +import esri = __esri; + +@Component({ + selector: 'tamu-gisc-omnitoolbar', + templateUrl: './omnitoolbar.component.html', + styleUrls: ['./omnitoolbar.component.scss'] +}) +export class OmnitoolbarComponent implements OnInit, OnDestroy { + @Input() + public source: Array; + + /** + * Dot notation property string collection that will be resolved and used to match against + * the text input. + */ + @Input() + public searchPaths: Array; + + @Input() + public displayTemplate: string; + + @Output() + public selectedSuggestion: EventEmitter = new EventEmitter(); + + @Output() + public clearSuggestion: EventEmitter = new EventEmitter(); + + public form: FormGroup; + public selectedLocation: Observable; + public name: Observable; + public number: Observable; + public filteredResults: Observable>; + + private $destroy: Subject; + + constructor(private recyclingService: RecyclingService, private fb: FormBuilder) {} + + public ngOnInit(): void { + this.selectedLocation = this.recyclingService.selectedLocationMeta; + + this.name = this.selectedLocation.pipe( + map((meta) => { + return (meta && meta.Name) || undefined; + }) + ); + + this.number = this.selectedLocation.pipe( + map((meta) => { + return (meta && meta.bldNum) || undefined; + }) + ); + + this.form = this.fb.group({ + search: [''] + }); + + this.filteredResults = this.form.get('search').valueChanges.pipe( + debounceTime(300), + map((term) => { + // If source is not yet an expected array, return early; + if (this.source instanceof Array === false) { + return; + } + + return this.source.filter((sourceItem) => { + if (term === '') { + return sourceItem; + } else { + const resolvedProperties = getObjectPropertyValues(sourceItem, this.searchPaths).map((s) => + typeof s === 'string' ? s.trim().toLowerCase() : s + ) as string[]; + + return resolvedProperties.some((rp) => { + return typeof rp === 'string' ? rp.includes(term.trim().toLowerCase()) : false; + }); + } + }); + }), + shareReplay(1) + ); + + this.selectedLocation.subscribe((location) => { + if (location === undefined) { + this.form.reset(); + } else { + this.form.patchValue({ + search: this.getLocationDisplay(location) + }); + } + }); + } + + public ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + public clear(): void { + this.recyclingService.clearSelected(); + this.clearSuggestion.emit(); + } + + public select(selection: T): void { + this.selectedSuggestion.emit(selection); + this.recyclingService.setLocation(selection); + } + + private getLocationDisplay(location: RecyclingLocationMetadata): string { + return `${location.Name} ${location.bldNum ? '(' + location.bldNum + ')' : ''}`.trim(); + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.html b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.html new file mode 100644 index 0000000000..2a0455d008 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.html @@ -0,0 +1 @@ +

{{perspective.toUpperCase()}}

diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.scss b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.scss new file mode 100644 index 0000000000..ac1969f25d --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.scss @@ -0,0 +1,16 @@ +@import 'libs/sass/mixins'; + +p { + padding: 0.9rem 0.9rem; + font-weight: 600; + background: #fafafa; + margin-left: 1rem; + border-radius: 7pt; + box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.15); + cursor: pointer; + @include transition(color 0.3s); + + &.toggled { + color: #2196f3; + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.spec.ts new file mode 100644 index 0000000000..dc9a948e52 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PerspectiveToggleComponent } from './perspective-toggle.component'; + +describe('PerspectiveToggleComponent', () => { + let component: PerspectiveToggleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PerspectiveToggleComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PerspectiveToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.ts new file mode 100644 index 0000000000..63c0db8c92 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/perspective-toggle/perspective-toggle.component.ts @@ -0,0 +1,51 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; + +import { EsriMapService } from '@tamu-gisc/maps/esri'; + +import esri = __esri; + +@Component({ + selector: 'tamu-gisc-perspective-toggle', + templateUrl: './perspective-toggle.component.html', + styleUrls: ['./perspective-toggle.component.scss'] +}) +export class PerspectiveToggleComponent implements OnInit { + public perspective: '2d' | '3d' = '2d'; + + private view: esri.SceneView; + + @Output() + public toggledPerspective: EventEmitter<'2d' | '3d'> = new EventEmitter(); + + constructor(private mapService: EsriMapService) {} + + public ngOnInit(): void { + this.mapService.store.subscribe((instance) => { + this.view = instance.view as esri.SceneView; + }); + } + + public toggle() { + if (this.perspective === '2d') { + this.perspective = '3d'; + this.view.constraints.tilt.max = 179.5; + + this.setCameraTilt(75); + this.toggledPerspective.emit('3d'); + } else { + this.perspective = '2d'; + this.setCameraTilt(0); + this.toggledPerspective.emit('2d'); + + setTimeout(() => { + this.view.constraints.tilt.max = 0; + }, 250); + } + } + + public setCameraTilt(angle: number) { + this.view.goTo({ + tilt: angle + }); + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.html b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.html new file mode 100644 index 0000000000..d82f94a23a --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.html @@ -0,0 +1,9 @@ +
+

Showing historical data for all recycling locations. Select a location to show its respective data.

+ + +

Name: {{(selectedMetadata | async)?.Name}}

+

Number: {{(selectedMetadata | async)?.bldNum}}

+

Style: {{(selectedMetadata | async)?.style}}

+
+
diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.scss b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.spec.ts new file mode 100644 index 0000000000..b345af0c55 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RecycleLocationDetailsCardComponent } from './recycle-location-details-card.component'; + +describe('RecycleLocationDetailsCardComponent', () => { + let component: RecycleLocationDetailsCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RecycleLocationDetailsCardComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RecycleLocationDetailsCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.ts new file mode 100644 index 0000000000..0ba0d92e25 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycle-location-details-card/recycle-location-details-card.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { RecyclingLocationMetadata, RecyclingService } from '../../../core/services/recycling.service'; + +@Component({ + selector: 'tamu-gisc-recycle-location-details-card', + templateUrl: './recycle-location-details-card.component.html', + styleUrls: ['./recycle-location-details-card.component.scss'] +}) +export class RecycleLocationDetailsCardComponent implements OnInit { + public selectedMetadata: Observable = this.recyclingService.selectedLocationMeta; + + constructor(private recyclingService: RecyclingService) {} + + public ngOnInit(): void {} +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.html b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.html new file mode 100644 index 0000000000..ca290290ad --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.scss b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.scss new file mode 100644 index 0000000000..313b377759 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.scss @@ -0,0 +1,6 @@ +@import 'libs/sass/mixins'; + +tamu-gisc-bar-chart { + @include flex(1); + width: 100%; +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.spec.ts new file mode 100644 index 0000000000..1b6ec35cea --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RecycledTrendsCardComponent } from './recycled-trends-card.component'; + +describe('RecycledTrendsCardComponent', () => { + let component: RecycledTrendsCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RecycledTrendsCardComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RecycledTrendsCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.ts new file mode 100644 index 0000000000..c28bb14918 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/recycled-trends-card/recycled-trends-card.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { filter, map, switchMap } from 'rxjs/operators'; + +import { IChartConfiguration } from '@tamu-gisc/charts'; + +import { RecyclingResultsStatistics, RecyclingService } from '../../../core/services/recycling.service'; + +@Component({ + selector: 'tamu-gisc-recycled-trends-card', + templateUrl: './recycled-trends-card.component.html', + styleUrls: ['./recycled-trends-card.component.scss'] +}) +export class RecycledTrendsCardComponent implements OnInit { + public chartData: Observable; + public selectedLocation: Observable; + + public chartOptions: Partial = { + scales: { + xAxes: [ + { + type: 'time' + } + ], + yAxes: [ + { + scaleLabel: { + labelString: 'Weight (lbs)', + display: true + } + } + ] + }, + legend: { + position: 'bottom', + display: false + }, + plugins: { + colorschemes: { + scheme: 'brewer.Paired8' + } + } + }; + + constructor(private recyclingService: RecyclingService) {} + + public ngOnInit(): void { + this.selectedLocation = this.recyclingService.allOrSelectedRecyclingStats; + + this.chartData = this.selectedLocation.pipe( + map((locs) => { + const data = locs.results.map((loc, index) => { + return loc.value; + }); + + const labels = locs.results.map((l) => new Date(l.date)); + + return { + labels: [...labels], + datasets: [ + { + data + } + ] + }; + }) + ); + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.html b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.html new file mode 100644 index 0000000000..7a1cb155e1 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.html @@ -0,0 +1,6 @@ +
+
compare_arrows
+

{{totalWeight | async | number}} {{unit | async}}

+ +

Weight Collected

+
diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.scss b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.scss new file mode 100644 index 0000000000..0a013c73ec --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.scss @@ -0,0 +1,8 @@ +.material-icons { + color: #7e7e7e; + cursor: pointer; + + &:hover { + color: initial; + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.spec.ts new file mode 100644 index 0000000000..76980844e4 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TotalRecycledCardComponent } from './total-recycled-card.component'; + +describe('TotalRecycledCardComponent', () => { + let component: TotalRecycledCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TotalRecycledCardComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TotalRecycledCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.ts b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.ts new file mode 100644 index 0000000000..ab34965989 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/components/total-recycled-card/total-recycled-card.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { map, pluck, switchMap } from 'rxjs/operators'; + +import { RecyclingService, RecyclingResultsStatistics } from '../../../core/services/recycling.service'; +import { ResultsService } from '../../../data-access/results/results.service'; + +@Component({ + selector: 'tamu-gisc-total-recycled-card', + templateUrl: './total-recycled-card.component.html', + styleUrls: ['./total-recycled-card.component.scss'] +}) +export class TotalRecycledCardComponent implements OnInit { + public selectedLocation: Observable = this.recyclingService.allOrSelectedRecyclingStats; + + public totalWeight: Observable; + public unit: BehaviorSubject<'lbs' | 'tons'> = new BehaviorSubject('lbs'); + + constructor(private resultsService: ResultsService, private recyclingService: RecyclingService) {} + + public ngOnInit(): void { + this.totalWeight = this.unit.pipe( + switchMap((unit) => { + let factor = 1; + + if (unit === 'tons') { + factor = 1 / 2000; + } + + return this.selectedLocation.pipe( + pluck('total'), + map((total) => { + return total * factor; + }) + ); + }) + ); + } + + public toggleUnit() { + if (this.unit.getValue() === 'lbs') { + this.unit.next('tons'); + } else { + this.unit.next('lbs'); + } + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/map.component.html b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.html new file mode 100644 index 0000000000..a455ae9d8b --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.html @@ -0,0 +1,36 @@ +
+ + + + + +
+ +
+ + + +
+ +
+ + +
+ + + + + + + + + + + +
+
+ +
+

+
+
diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/map.component.scss b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.scss new file mode 100644 index 0000000000..832448cbb2 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.scss @@ -0,0 +1,2 @@ +@import 'libs/sass/mixins'; +@import 'libs/sass/modules/esri_map'; diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/map.component.spec.ts b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.spec.ts new file mode 100644 index 0000000000..f163147974 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MapComponent } from './map.component'; + +describe('MapComponent', () => { + let component: MapComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MapComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MapComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/map.component.ts b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.ts new file mode 100644 index 0000000000..f2f321c16b --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/map.component.ts @@ -0,0 +1,338 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { combineLatest, from, Observable, Subject } from 'rxjs'; +import { filter, map, pluck, shareReplay } from 'rxjs/operators'; + +import { loadModules } from 'esri-loader'; + +import { MapServiceInstance, MapConfig, EsriMapService, EsriModuleProviderService } from '@tamu-gisc/maps/esri'; +import { ResponsiveService } from '@tamu-gisc/dev-tools/responsive'; +import { EnvironmentService } from '@tamu-gisc/common/ngx/environment'; +import { FeatureSelectorService } from '@tamu-gisc/maps/feature/feature-selector'; +import { FeatureHighlightService } from '@tamu-gisc/maps/feature/feature-highlight'; +import { Location } from '@tamu-gisc/ues/recycling/common/entities'; + +import { LocationsService } from '../data-access/locations/locations.service'; + +import esri = __esri; + +@Component({ + selector: 'tamu-gisc-map', + templateUrl: './map.component.html', + styleUrls: ['./map.component.scss'], + providers: [FeatureSelectorService] +}) +export class MapComponent implements OnInit, OnDestroy { + public map: esri.Map; + public view: esri.MapView | esri.SceneView; + public isMobile: Observable; + public config: MapConfig; + + public searchSource: Observable; + + private _destroy$: Subject = new Subject(); + + constructor( + private responsiveService: ResponsiveService, + private environment: EnvironmentService, + private mapService: EsriMapService, + private highlightService: FeatureHighlightService, + private selectionService: FeatureSelectorService, + private locationsService: LocationsService, + private loader: EsriModuleProviderService + ) {} + + public ngOnInit(): void { + this.isMobile = this.responsiveService.isMobile.pipe(shareReplay()); + + const connections = this.environment.value('Connections'); + + this.config = { + basemap: { + basemap: { + baseLayers: [ + { + type: 'TileLayer', + url: connections.basemapUrl, + spatialReference: { + wkid: 102100 + }, + listMode: 'hide', + visible: true, + minScale: 100000, + maxScale: 0, + title: 'Base Map' + } + ], + id: 'aggie_basemap', + title: 'Aggie Basemap' + } + }, + view: { + mode: '3d', + properties: { + // container: this.mapViewEl.nativeElement, + map: undefined, // Reference to the map object created before the scene + center: [-96.344672, 30.61306], + spatialReference: { + wkid: 102100 + }, + // constraints: { + // minScale: 100000, // minZoom is the max you can zoom OUT into space + // maxScale: 0 // maxZoom is the max you can zoom INTO the ground + // }, + constraints: { + altitude: { + min: 0, + max: 10000 + }, + tilt: { + max: 0 + } + }, + zoom: 16, + ui: { + components: this.isMobile ? ['attribution'] : ['attribution', 'zoom'] + } + } + } + }; + + this.mapService.store.subscribe((instance) => { + this.map = instance.map; + this.view = instance.view; + + this.view.when(() => { + const l = this.map.findLayerById('recycling-layer') as esri.FeatureLayer; + + this.searchSource = from( + l.queryFeatures({ + outFields: ['*'], + where: '1=1', + returnGeometry: true + }) + ).pipe(pluck('features')); + }); + }); + + // Set loader phrases and display a random one. + const phrases = [ + 'An Aggie does not lie, cheat or steal or tolerate those who do.', + 'Home of the 12th Man', + 'Whoop!', + "Gig 'Em!", + 'Howdy Ags!' + ]; + (document.querySelector('.phrase')).innerText = phrases[Math.floor(Math.random() * phrases.length)]; + + this.selectionService.snapshot + .pipe( + map((features) => { + return features.filter((g) => g.layer.id === 'recycling-layer'); + }), + filter((features) => features !== undefined) + ) + .subscribe((res) => { + this.highlight(res, false); + }); + } + + public ngOnDestroy() { + this._destroy$.next(); + this._destroy$.complete(); + } + + public continue(instances: MapServiceInstance) { + loadModules(['esri/widgets/Track', 'esri/widgets/Compass']) + .then(([Track, Compass]) => { + instances.view.when(() => { + // + // Loader disable + // + document.querySelector('.loader .progress-bar').classList.remove('anim'); + + setTimeout(() => { + document.querySelector('.loader').classList.add('fade-out'); + + setTimeout(() => { + (document.querySelector('.loader')).style.display = 'none'; + }, 300); + }, 300); + }); + + const track: esri.Track = new Track({ + view: instances.view, + useHeadingEnabled: true, + goToLocationEnabled: false + }); + + const compass = new Compass({ + view: instances.view + }); + + if (this.isMobile) { + instances.view.ui.add(track, 'bottom-right'); + } else { + instances.view.ui.add(track, 'top-right'); + instances.view.ui.add(compass, 'top-right'); + } + }) + .catch((err) => { + throw new Error(err); + }); + } + + /** + * Toggles a class on a DOM Element. While the .toggle() method can be used in code, + * it cannot be used in HTML templates + * + * @param {*} event Event object + * @param {*} className Class to be toggled + */ + public toggleClass = (event: KeyboardEvent | MouseEvent, className: string) => { + if ((event.currentTarget).classList) { + if ((event.currentTarget).classList.contains(className)) { + (event.currentTarget).classList.remove(className); + } else { + (event.currentTarget).classList.add(className); + } + } else { + throw new Error('No event provided.'); + } + }; + + /** + * Highlights the provided features. + * + * Provides ability to zoom to features. + */ + public highlight(features: esri.Graphic | esri.Graphic[], focus: boolean) { + const feats = features instanceof Array ? features : [features]; + + this.highlightService.highlight({ + features: feats + }); + + if (focus) { + this.view.goTo(feats); + } + } + + public clearHighlight() { + this.highlightService.clearAll(); + } + + public toggle3dLayer(mode: '2d' | '3d') { + const layerName = 'recycling-layer-three'; + + const existingLayer = this.map.findLayerById(layerName); + + if (mode === '3d') { + if (!existingLayer) { + combineLatest([ + this.searchSource, + this.locationsService.getLocationsResults(), + this.loader.require(['FeatureLayer', 'Circle']) + ]).subscribe( + ([searchSource, locs, [FeatureLayer, Circle]]: [ + esri.Graphic[], + Location[], + [esri.FeatureLayerConstructor, esri.CircleConstructor] + ]) => { + const cloned = searchSource.reduce((collection, graphic) => { + const c = graphic.clone(); + const cg = new Circle({ + center: { + latitude: (c.geometry as esri.Point).latitude, + longitude: (c.geometry as esri.Point).longitude + }, + spatialReference: c.geometry.spatialReference, + radius: 10, + radiusUnit: 'meters' + }); + + c.geometry = cg; + + const matchedLocation = locs.find((location) => { + if (c.attributes.bldNum) { + return parseInt(c.attributes.bldNum, 10) === parseInt(location.id, 10); + } else { + return c.attributes.Name === location.id; + } + }); + + if (matchedLocation === undefined) { + return collection; + } + + c.attributes.total = matchedLocation.results.reduce((acc, curr) => { + const isDouble = curr.value % 1 !== 0; + if (isDouble) { + return acc + parseInt((curr.value * 2000).toFixed(0), 10); + } else { + return acc + curr.value; + } + }, 0); + + return [...collection, c]; + }, []); + + const layer = new FeatureLayer({ + id: layerName, + source: cloned, + objectIdField: 'OBJECTID', + fields: [ + { + name: 'OBJECTID', + type: 'oid' + }, + { + name: 'total', + type: 'integer' + } + ], + elevationInfo: { + mode: 'relative-to-scene' + }, + renderer: { + type: 'simple', + symbol: { + type: 'polygon-3d', + symbolLayers: [{ type: 'extrude', material: { color: '#71C96E' } }] + }, + label: '% population in poverty by county', + visualVariables: [ + { + type: 'size', + field: 'total', + stops: [ + { + value: 0, + size: 0 + }, + { + value: 100000, + size: 250 + }, + { + value: 1000000, + size: 500 + } + ] + } + ] + } as esri.RendererProperties + }); + + this.map.add(layer); + } + ); + } else { + existingLayer.visible = true; + } + } else { + if (existingLayer) { + existingLayer.visible = false; + } + } + } +} diff --git a/libs/ues/recycling/ngx/src/lib/modules/map/map.module.ts b/libs/ues/recycling/ngx/src/lib/modules/map/map.module.ts new file mode 100644 index 0000000000..efbe3066c1 --- /dev/null +++ b/libs/ues/recycling/ngx/src/lib/modules/map/map.module.ts @@ -0,0 +1,43 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { EsriMapModule } from '@tamu-gisc/maps/esri'; +import { UESCoreUIModule } from '@tamu-gisc/ues/common/ngx'; +import { MapsFeatureCoordinatesModule } from '@tamu-gisc/maps/feature/coordinates'; +import { MapsFeatureAccessibilityModule } from '@tamu-gisc/maps/feature/accessibility'; +import { LayerListModule } from '@tamu-gisc/maps/feature/layer-list'; +import { ChartsModule } from '@tamu-gisc/charts'; + +import { MapComponent } from './map.component'; +import { OmnitoolbarComponent } from './components/omnitoolbar/omnitoolbar.component'; +import { TotalRecycledCardComponent } from './components/total-recycled-card/total-recycled-card.component'; +import { RecyclingService } from '../core/services/recycling.service'; +import { RecycledTrendsCardComponent } from './components/recycled-trends-card/recycled-trends-card.component'; +import { RecycleLocationDetailsCardComponent } from './components/recycle-location-details-card/recycle-location-details-card.component'; +import { PerspectiveToggleComponent } from './components/perspective-toggle/perspective-toggle.component'; + +const routes: Routes = [ + { + path: '', + component: MapComponent + } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(routes), + EsriMapModule, + UESCoreUIModule, + MapsFeatureCoordinatesModule, + MapsFeatureAccessibilityModule, + LayerListModule, + ChartsModule, + ReactiveFormsModule + ], + declarations: [MapComponent, OmnitoolbarComponent, TotalRecycledCardComponent, RecycledTrendsCardComponent, RecycleLocationDetailsCardComponent, PerspectiveToggleComponent], + providers: [RecyclingService] +}) +export class MapModule {} diff --git a/libs/ues/recycling/ngx/src/test-setup.ts b/libs/ues/recycling/ngx/src/test-setup.ts new file mode 100644 index 0000000000..8d88704e8f --- /dev/null +++ b/libs/ues/recycling/ngx/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/ues/recycling/ngx/tsconfig.json b/libs/ues/recycling/ngx/tsconfig.json new file mode 100644 index 0000000000..26b7b4afd1 --- /dev/null +++ b/libs/ues/recycling/ngx/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/ues/recycling/ngx/tsconfig.lib.json b/libs/ues/recycling/ngx/tsconfig.lib.json new file mode 100644 index 0000000000..c5149765ee --- /dev/null +++ b/libs/ues/recycling/ngx/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": ["arcgis-js-api"], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/ues/recycling/ngx/tsconfig.spec.json b/libs/ues/recycling/ngx/tsconfig.spec.json new file mode 100644 index 0000000000..6ac0f91fdd --- /dev/null +++ b/libs/ues/recycling/ngx/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "arcgis-js-api"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/ues/recycling/ngx/tslint.json b/libs/ues/recycling/ngx/tslint.json new file mode 100644 index 0000000000..33d39cbe7f --- /dev/null +++ b/libs/ues/recycling/ngx/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../../tslint.json", + "rules": { + "directive-selector": [true, "attribute", "tamuGisc", "camelCase"], + "component-selector": [true, "element", "tamu-gisc", "kebab-case"] + }, + "linterOptions": { + "exclude": ["!**/*"] + } +} diff --git a/nx.json b/nx.json index 2b7a2d707d..9099e29ab3 100644 --- a/nx.json +++ b/nx.json @@ -412,6 +412,25 @@ }, "ues-operations-nest": { "tags": [] + }, + "ues-recycling-angular-e2e": { + "tags": [], + "implicitDependencies": ["ues-recycling-angular"] + }, + "ues-recycling-angular": { + "tags": [] + }, + "ues-recycling-data-api-nest": { + "tags": [] + }, + "ues-recycling-common-entities": { + "tags": [] + }, + "ues-recycling-data-api": { + "tags": [] + }, + "ues-recycling-ngx": { + "tags": [] } }, "tasksRunnerOptions": { diff --git a/package-lock.json b/package-lock.json index dd4f133055..ab778694ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17901,8 +17901,8 @@ "dev": true }, "geojson-utils": { - "version": "git+ssh://git@github.com/TamuGeoInnovation/Tamu.GeoInnovation.js.geojsonutils.git#31951584fca6053df39361d2c02217152db2a94f", - "from": "git+ssh://git@github.com/TamuGeoInnovation/Tamu.GeoInnovation.js.geojsonutils.git" + "version": "git+https://github.com/TamuGeoInnovation/Tamu.GeoInnovation.js.geojsonutils.git#31951584fca6053df39361d2c02217152db2a94f", + "from": "git+https://github.com/TamuGeoInnovation/Tamu.GeoInnovation.js.geojsonutils.git" }, "geojson-vt": { "version": "3.2.1", diff --git a/package.json b/package.json index ba7cf81aae..13b9097595 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "express-rate-limit": "^5.1.3", "express-session": "^1.17.0", "fs-extra": "^9.0.0", - "geojson-utils": "git+ssh://git@github.com/TamuGeoInnovation/Tamu.GeoInnovation.js.geojsonutils.git", + "geojson-utils": "https://github.com/TamuGeoInnovation/Tamu.GeoInnovation.js.geojsonutils.git", "got": "^11.3.0", "helmet": "^3.23.3", "interactjs": "^1.9.19", diff --git a/tsconfig.base.json b/tsconfig.base.json index 2d1ffae8e3..a30fce7ffc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -107,7 +107,10 @@ "@tamu-gisc/gisday/data-access": ["libs/gisday/data-access/src/index.ts"], "@tamu-gisc/gisday/data-api": ["libs/gisday/data-api/src/index.ts"], "@tamu-gisc/gisday/common": ["libs/gisday/common/src/index.ts"], - "@tamu-gisc/common/ngx/auth": ["libs/common/ngx/auth/src/index.ts"] + "@tamu-gisc/common/ngx/auth": ["libs/common/ngx/auth/src/index.ts"], + "@tamu-gisc/ues/recycling/common/entities": ["libs/ues/recycling/common/entities/src/index.ts"], + "@tamu-gisc/ues/recycling/data-api": ["libs/ues/recycling/data-api/src/index.ts"], + "@tamu-gisc/ues/recycling/ngx": ["libs/ues/recycling/ngx/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 64898c6396..dabe0d9df8 100644 --- a/workspace.json +++ b/workspace.json @@ -4382,6 +4382,250 @@ } } } + }, + "ues-recycling-angular": { + "projectType": "application", + "root": "apps/ues-recycling-angular", + "sourceRoot": "apps/ues-recycling-angular/src", + "prefix": "tamu-gisc", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/apps/ues-recycling-angular", + "index": "apps/ues-recycling-angular/src/index.html", + "main": "apps/ues-recycling-angular/src/main.ts", + "polyfills": "apps/ues-recycling-angular/src/polyfills.ts", + "tsConfig": "apps/ues-recycling-angular/tsconfig.app.json", + "aot": true, + "assets": [ + "apps/ues-recycling-angular/src/favicon.ico", + "apps/ues-recycling-angular/src/assets", + { + "glob": "**/*", + "input": "./libs/ues/assets/", + "output": "./assets" + }, + { + "glob": "**/*", + "input": "./libs/assets/", + "output": "./assets" + } + ], + "styles": ["apps/ues-recycling-angular/src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "apps/ues-recycling-angular/src/environments/environment.ts", + "with": "apps/ues-recycling-angular/src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ] + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "ues-recycling-angular:build" + }, + "configurations": { + "production": { + "browserTarget": "ues-recycling-angular:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "ues-recycling-angular:build" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "apps/ues-recycling-angular/tsconfig.app.json", + "apps/ues-recycling-angular/tsconfig.spec.json", + "apps/ues-recycling-angular/tsconfig.editor.json" + ], + "exclude": ["**/node_modules/**", "!apps/ues-recycling-angular/**/*"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/apps/ues-recycling-angular"], + "options": { + "jestConfig": "apps/ues-recycling-angular/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "ues-recycling-angular-e2e": { + "root": "apps/ues-recycling-angular-e2e", + "sourceRoot": "apps/ues-recycling-angular-e2e/src", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@nrwl/cypress:cypress", + "options": { + "cypressConfig": "apps/ues-recycling-angular-e2e/cypress.json", + "tsConfig": "apps/ues-recycling-angular-e2e/tsconfig.e2e.json", + "devServerTarget": "ues-recycling-angular:serve" + }, + "configurations": { + "production": { + "devServerTarget": "ues-recycling-angular:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": ["apps/ues-recycling-angular-e2e/tsconfig.e2e.json"], + "exclude": ["**/node_modules/**", "!apps/ues-recycling-angular-e2e/**/*"] + } + } + } + }, + "ues-recycling-data-api-nest": { + "root": "apps/ues-recycling-data-api-nest", + "sourceRoot": "apps/ues-recycling-data-api-nest/src", + "projectType": "application", + "prefix": "ues-recycling-data-api-nest", + "architect": { + "build": { + "builder": "@nrwl/node:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/ues-recycling-data-api-nest", + "main": "apps/ues-recycling-data-api-nest/src/main.ts", + "tsConfig": "apps/ues-recycling-data-api-nest/tsconfig.app.json", + "assets": ["apps/ues-recycling-data-api-nest/src/assets"] + }, + "configurations": { + "production": { + "optimization": true, + "extractLicenses": true, + "inspect": false, + "fileReplacements": [ + { + "replace": "apps/ues-recycling-data-api-nest/src/environments/environment.ts", + "with": "apps/ues-recycling-data-api-nest/src/environments/environment.prod.ts" + } + ] + } + } + }, + "serve": { + "builder": "@nrwl/node:execute", + "options": { + "buildTarget": "ues-recycling-data-api-nest:build", + "port": 7777 + } + }, + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["apps/ues-recycling-data-api-nest/**/*.ts"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/apps/ues-recycling-data-api-nest"], + "options": { + "jestConfig": "apps/ues-recycling-data-api-nest/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "ues-recycling-common-entities": { + "root": "libs/ues/recycling/common/entities", + "sourceRoot": "libs/ues/recycling/common/entities/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["libs/ues/recycling/common/entities/**/*.ts"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/ues/recycling/common/entities"], + "options": { + "jestConfig": "libs/ues/recycling/common/entities/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "ues-recycling-data-api": { + "root": "libs/ues/recycling/data-api", + "sourceRoot": "libs/ues/recycling/data-api/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["libs/ues/recycling/data-api/**/*.ts"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/ues/recycling/data-api"], + "options": { + "jestConfig": "libs/ues/recycling/data-api/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "ues-recycling-ngx": { + "projectType": "library", + "root": "libs/ues/recycling/ngx", + "sourceRoot": "libs/ues/recycling/ngx/src", + "prefix": "tamu-gisc", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": ["libs/ues/recycling/ngx/tsconfig.lib.json", "libs/ues/recycling/ngx/tsconfig.spec.json"], + "exclude": ["**/node_modules/**", "!libs/ues/recycling/ngx/**/*"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/ues/recycling/ngx"], + "options": { + "jestConfig": "libs/ues/recycling/ngx/jest.config.js", + "passWithNoTests": true + } + } + } } }, "cli": {