Sets up basic Angular app
|
@ -0,0 +1,13 @@
|
||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
|
@ -1,88 +1,49 @@
|
||||||
# Logs
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# compiled output
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
# Runtime data
|
# dependencies
|
||||||
pids
|
/node_modules
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# profiling files
|
||||||
lib-cov
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# IDEs and editors
|
||||||
coverage
|
/.idea
|
||||||
*.lcov
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
# nyc test coverage
|
# IDE - VSCode
|
||||||
.nyc_output
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
# misc
|
||||||
.grunt
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
# System Files
|
||||||
bower_components
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
# node-waf configuration
|
# SonarCloud
|
||||||
.lock-wscript
|
/.scannerwork/
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
sonar.organization=sartography
|
||||||
|
sonar.projectKey=sartography_cr-connect-frontend
|
||||||
|
sonar.host.url=https://sonarcloud.io
|
||||||
|
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||||
|
sonar.typescript.tsconfigPath=tsconfig.json
|
||||||
|
sonar.coverage.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
|
||||||
|
sonar.cpd.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
|
||||||
|
sonar.exclusions=src/assets/**
|
|
@ -0,0 +1,55 @@
|
||||||
|
sudo: required
|
||||||
|
|
||||||
|
dist: bionic
|
||||||
|
|
||||||
|
language: node_js
|
||||||
|
|
||||||
|
node_js:
|
||||||
|
- 12
|
||||||
|
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
- xvfb
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- |
|
||||||
|
if [[ $TRAVIS_BRANCH =~ ^(feature\/.*)$ ]]; then
|
||||||
|
export E2E_TAG="dev"
|
||||||
|
elif [[ $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|rrt\/.*)$ ]]; then
|
||||||
|
export E2E_TAG="${TRAVIS_BRANCH//\//_}"
|
||||||
|
else
|
||||||
|
export E2E_TAG="latest"
|
||||||
|
fi
|
||||||
|
echo "E2E_TAG = $E2E_TAG"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
addons:
|
||||||
|
chrome: stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- API_URL=http://localhost:5000/v1.0
|
||||||
|
- BASE_HREF=/
|
||||||
|
- DEPLOY_URL=/
|
||||||
|
- HOME_ROUTE=home
|
||||||
|
- IRB_URL=http://localhost:5001/
|
||||||
|
- PORT0=4200
|
||||||
|
- PRODUCTION=false
|
||||||
|
script:
|
||||||
|
- npm run ci
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: script
|
||||||
|
script: bash ./deploy.sh sartography/cr-connect-frontend
|
||||||
|
on:
|
||||||
|
all_branches: true
|
||||||
|
condition: $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|master|rrt\/.*)$
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
on_success: change
|
||||||
|
on_failure: always
|
||||||
|
recipients:
|
||||||
|
- dan@sartography.com
|
|
@ -0,0 +1,31 @@
|
||||||
|
### STAGE 1: Build ###
|
||||||
|
FROM sartography/cr-connect-angular-base AS builder
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
ARG build_config=prod
|
||||||
|
RUN npm install && \
|
||||||
|
npm run build:$build_config
|
||||||
|
|
||||||
|
### STAGE 2: Run ###
|
||||||
|
FROM nginx:alpine
|
||||||
|
RUN set -x && apk add --update --no-cache bash libintl gettext curl
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist/* /etc/nginx/html/
|
||||||
|
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Script for substituting environment variables
|
||||||
|
COPY ./docker/substitute-env-variables.sh ./entrypoint.sh
|
||||||
|
RUN chmod +x ./entrypoint.sh
|
||||||
|
|
||||||
|
# Fix for Angular routing
|
||||||
|
RUN echo "pushstate: enabled" > /etc/nginx/html/Staticfile
|
||||||
|
|
||||||
|
# The entrypoint.sh script will run after the container finishes starting.
|
||||||
|
# Substitutes environment variables in nginx configuration and index.html,
|
||||||
|
# then starts/reloads nginx.
|
||||||
|
ENTRYPOINT ["./entrypoint.sh", \
|
||||||
|
"/etc/nginx/html/index.html,/etc/nginx/conf.d/default.conf", \
|
||||||
|
"PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,DEPLOY_URL,PORT0,GOOGLE_ANALYTICS_KEY,SENTRY_KEY,TITLE", \
|
||||||
|
"/etc/nginx/html", \
|
||||||
|
"true"]
|
|
@ -1,7 +1,6 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
The MIT License (MIT)
|
Copyright (c) 2019 Sartography
|
||||||
|
|
||||||
Copyright (c) 2020 Aaron Louie
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
45
README.md
|
@ -1 +1,44 @@
|
||||||
covid19-testing-kiosk
|
# sartography/cr-connect-frontend
|
||||||
|
[![Build Status](https://travis-ci.com/sartography/cr-connect-frontend.svg?branch=master)](https://travis-ci.com/sartography/cr-connect-frontend)
|
||||||
|
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=sartography_cr-connect-frontend&metric=coverage)](https://sonarcloud.io/dashboard?id=sartography_cr-connect-frontend)
|
||||||
|
|
||||||
|
# CR Connect Frontend
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.15.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Local Development with Sartogprahy Libraries
|
||||||
|
If you are making changes to the Sartography Libraries dependency,
|
||||||
|
you can replace that line in the package.json file with something akin to this, where you supply the full path to the dist folder, remember
|
||||||
|
to run 'npm build' in that directory for your local changes to take effect, and be sure to change this back to the proper value before
|
||||||
|
committing your code
|
||||||
|
```
|
||||||
|
"sartography-workflow-lib": "/home/dan/code/workflow/sartography-libraries/dist/sartography-workflow-lib",
|
||||||
|
```
|
||||||
|
Also note that you need to add
|
||||||
|
```json
|
||||||
|
"preserveSymlinks": true
|
||||||
|
```
|
||||||
|
to your angular.json file in build/options.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"cr-connect-frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/cr-connect-frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"aot": true,
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"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": "cr-connect-frontend:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "cr-connect-frontend:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "cr-connect-frontend:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/marked/lib/marked.js"
|
||||||
|
],
|
||||||
|
"codeCoverageExclude": [
|
||||||
|
"src/polyfills.ts",
|
||||||
|
"src/test.ts",
|
||||||
|
"docker/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"tsconfig.app.json",
|
||||||
|
"tsconfig.spec.json",
|
||||||
|
"e2e/tsconfig.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "cr-connect-frontend:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "cr-connect-frontend:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "cr-connect-frontend",
|
||||||
|
"cli": {
|
||||||
|
"analytics": "39bf12a6-4921-4c8d-bd17-8df6300cf98a"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
> 0.5%
|
||||||
|
last 2 versions
|
||||||
|
Firefox ESR
|
||||||
|
not dead
|
||||||
|
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#########################################################################
|
||||||
|
# Builds the Docker image for the current git branch on Travis CI and
|
||||||
|
# publishes it to Docker Hub.
|
||||||
|
#
|
||||||
|
# Parameters:
|
||||||
|
# $1: Docker Hub repository to publish to
|
||||||
|
#
|
||||||
|
# Required environment variables (place in Settings menu on Travis CI):
|
||||||
|
# $DOCKER_USERNAME: Docker Hub username
|
||||||
|
# $DOCKER_TOKEN: Docker Hub access token
|
||||||
|
#########################################################################
|
||||||
|
|
||||||
|
echo 'Building Docker image...'
|
||||||
|
DOCKER_REPO="$1"
|
||||||
|
|
||||||
|
function branch_to_tag () {
|
||||||
|
if [ "$1" == "master" ]; then echo "latest"; else echo "$1" ; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function branch_to_deploy_group() {
|
||||||
|
if [[ $1 =~ ^(rrt\/.*)$ ]]; then echo "rrt"; else echo "crconnect" ; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
DOCKER_TAG=$(branch_to_tag "$TRAVIS_BRANCH")
|
||||||
|
|
||||||
|
DEPLOY_GROUP=$(branch_to_deploy_group "$TRAVIS_BRANCH")
|
||||||
|
|
||||||
|
if [ "$DEPLOY_GROUP" == "rrt" ]; then
|
||||||
|
IFS='/' read -ra ARR <<< "$TRAVIS_BRANCH" # Split branch on '/' character
|
||||||
|
DOCKER_TAG=$(branch_to_tag "rrt_${ARR[1]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DOCKER_REPO = $DOCKER_REPO"
|
||||||
|
echo "DOCKER_TAG = $DOCKER_TAG"
|
||||||
|
|
||||||
|
echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin || exit 1
|
||||||
|
docker build -f Dockerfile -t "$DOCKER_REPO:$DOCKER_TAG" . || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
# Push Docker image to Docker Hub
|
||||||
|
echo "Publishing to Docker Hub..."
|
||||||
|
docker push "$DOCKER_REPO" || exit 1
|
||||||
|
echo "Done."
|
|
@ -0,0 +1,95 @@
|
||||||
|
version: "3.3"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: db
|
||||||
|
image: sartography/cr-connect-db:$E2E_TAG
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=crc_user
|
||||||
|
- POSTGRES_PASSWORD=crc_pass
|
||||||
|
- POSTGRES_MULTIPLE_DATABASES=crc_test,pb_test
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
pb:
|
||||||
|
container_name: pb
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
image: sartography/protocol-builder-mock:$E2E_TAG
|
||||||
|
environment:
|
||||||
|
- APPLICATION_ROOT=/
|
||||||
|
- CORS_ALLOW_ORIGINS=localhost:5000,backend:5000,localhost:5002,bpmn:5002,localhost:4200,frontend:4200
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_NAME=pb_test
|
||||||
|
- DB_PASSWORD=crc_pass
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_USER=crc_user
|
||||||
|
- PORT0=5001
|
||||||
|
- UPGRADE_DB=true
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
command: ./wait-for-it.sh db:5432 -t 0 -- ./docker_run.sh
|
||||||
|
|
||||||
|
backend:
|
||||||
|
container_name: backend
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- pb
|
||||||
|
image: sartography/cr-connect-workflow:$E2E_TAG
|
||||||
|
environment:
|
||||||
|
- APPLICATION_ROOT=/
|
||||||
|
- CORS_ALLOW_ORIGINS=localhost:5002,bpmn:5002,localhost:4200,frontend:4200
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_NAME=crc_test
|
||||||
|
- DB_PASSWORD=crc_pass
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_USER=crc_user
|
||||||
|
- DEVELOPMENT=true
|
||||||
|
- LDAP_URL=mock
|
||||||
|
- PB_BASE_URL=http://pb:5001/v2.0/
|
||||||
|
- PB_ENABLED=true
|
||||||
|
- PORT0=5000
|
||||||
|
- PRODUCTION=false
|
||||||
|
- RESET_DB=true
|
||||||
|
- TESTING=false
|
||||||
|
- UPGRADE_DB=true
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
command: ./wait-for-it.sh pb:5001 -t 0 -- ./docker_run.sh
|
||||||
|
|
||||||
|
|
||||||
|
# bpmn:
|
||||||
|
# container_name: bpmn
|
||||||
|
# depends_on:
|
||||||
|
# - db
|
||||||
|
# - backend
|
||||||
|
# image: sartography/cr-connect-bpmn:dev
|
||||||
|
# environment:
|
||||||
|
# - API_URL=http://localhost:5000/api/v1.0
|
||||||
|
# - BASE_HREF=/bpmn/
|
||||||
|
# - DEPLOY_URL=/bpmn/
|
||||||
|
# - HOME_ROUTE=home
|
||||||
|
# - PORT0=5002
|
||||||
|
# - PRODUCTION=false
|
||||||
|
# ports:
|
||||||
|
# - "5002:5002"
|
||||||
|
#
|
||||||
|
# frontend:
|
||||||
|
# container_name: frontend
|
||||||
|
# depends_on:
|
||||||
|
# - db
|
||||||
|
# - backend
|
||||||
|
# image: sartography/cr-connect-frontend:dev
|
||||||
|
# environment:
|
||||||
|
# - API_URL=http://localhost:5000/api/v1.0
|
||||||
|
# - BASE_HREF=/app/
|
||||||
|
# - DEPLOY_URL=/app/
|
||||||
|
# - HOME_ROUTE=home
|
||||||
|
# - PORT0=4200
|
||||||
|
# - PRODUCTION=false
|
||||||
|
# ports:
|
||||||
|
# - "4200:4200"
|
|
@ -0,0 +1,123 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Substitutes the given environment variables in the given files.
|
||||||
|
# Parameters:
|
||||||
|
# $1: Comma-delimited list of file paths
|
||||||
|
# $2: Comma-delimited list of environment variables
|
||||||
|
# $3: Absolute path to nginx html directory (optional)
|
||||||
|
# $4: Should restart nginx (optional)
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
echo 'Substituting environment variables...'
|
||||||
|
num_args=0
|
||||||
|
|
||||||
|
# The first parameter is a comma-delimited list of paths to files which should be substituted
|
||||||
|
if [[ -z $1 ]]; then
|
||||||
|
echo 'ERROR: No target files given.'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
num_args=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The second parameter is a comma-delimited list of environment variable names
|
||||||
|
if [[ -z $2 ]]; then
|
||||||
|
echo 'ERROR: No environment variables given.'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
num_args=2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The third parameter is the absolute path to the nginx html directory
|
||||||
|
if [[ -z $3 ]]; then
|
||||||
|
echo '' # It's optional. Don't print anything.
|
||||||
|
else
|
||||||
|
num_args=3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The fourth parameter, if 'true', is whether we should reload nginx
|
||||||
|
if [[ -z $4 ]]; then
|
||||||
|
echo '' # It's optional. Don't print anything.
|
||||||
|
else
|
||||||
|
num_args=4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find & replace BASE_HREF in all files in the nginx html directory
|
||||||
|
if [[ "$2" == *"BASE_HREF"* ]] && [[ "$2" == *"DEPLOY_URL"* ]]; then
|
||||||
|
# Add trailing slash to $BASE_HREF if needed
|
||||||
|
length=${#BASE_HREF}
|
||||||
|
last_char=${BASE_HREF:length-1:1}
|
||||||
|
[[ $last_char != "/" ]] && BASE_HREF="$BASE_HREF/"; :
|
||||||
|
|
||||||
|
# Add trailing slash to $DEPLOY_URL if needed
|
||||||
|
length=${#DEPLOY_URL}
|
||||||
|
last_char=${DEPLOY_URL:length-1:1}
|
||||||
|
[[ $last_char != "/" ]] && DEPLOY_URL="$DEPLOY_URL/"; :
|
||||||
|
|
||||||
|
# The third parameter is the absolute path to the nginx html directory
|
||||||
|
if [[ $num_args -ge 3 ]]; then
|
||||||
|
# Replace all instances of __REPLACE_ME_WITH_BASE_HREF__ with $BASE_HREF
|
||||||
|
find "$3" \( -type d -name .git -prune \) -o -type f -print0 | \
|
||||||
|
xargs -0 sed -i 's@__REPLACE_ME_WITH_BASE_HREF__@'"$BASE_HREF"'@g'
|
||||||
|
|
||||||
|
echo 'Replacing base href...'
|
||||||
|
# Wait a few seconds in case find | sed needs more time
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Replace all instances of __REPLACE_ME_WITH_DEPLOY_URL__ with $DEPLOY_URL
|
||||||
|
find "$3" \( -type d -name .git -prune \) -o -type f -print0 | \
|
||||||
|
xargs -0 sed -i 's@__REPLACE_ME_WITH_DEPLOY_URL__@'"$DEPLOY_URL"'@g'
|
||||||
|
|
||||||
|
echo 'Replacing deploy url...'
|
||||||
|
# Wait a few seconds in case find | sed needs more time
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert "VAR1,VAR2,VAR3,..." to "\$VAR1 \$VAR2 \$VAR3 ..."
|
||||||
|
env_list="\\\$${2//,/ \\\$}" # "\" and "$" are escaped as "\\" and "\$"
|
||||||
|
for file_path in ${1//,/ }
|
||||||
|
do
|
||||||
|
echo "replacing environment variables in $file_path"
|
||||||
|
|
||||||
|
# Replace strings in the given file(s) in env_list
|
||||||
|
envsubst "$env_list" < "$file_path" > "$file_path".tmp && mv "$file_path".tmp "$file_path"
|
||||||
|
|
||||||
|
echo '...'
|
||||||
|
# Wait a second in case envsubst needs more time
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# If this is the nginx default.conf file, replace double slashes with single slashes
|
||||||
|
if [[ $file_path == *"/default.conf"* ]]; then
|
||||||
|
sed -i -e 's@//@/@g' "$file_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo 'Finished substituting environment variables.'
|
||||||
|
for env_var in ${2//,/ }
|
||||||
|
do
|
||||||
|
echo "$env_var = ${!env_var}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Reload nginx
|
||||||
|
if [ $num_args -ge 4 ] && [ "$4" == "true" ]; then
|
||||||
|
# Check to see if nginx command is available
|
||||||
|
if hash nginx 2> /dev/null; then
|
||||||
|
# Check to see if nginx is already running
|
||||||
|
if [ -e /var/run/nginx.pid ]; then
|
||||||
|
echo "nginx is currently running. Reloading nginx..."
|
||||||
|
exec nginx -s reload
|
||||||
|
echo "nginx reloaded."
|
||||||
|
else
|
||||||
|
echo "nginx is not yet running. Starting nginx..."
|
||||||
|
exec nginx -g 'daemon off;'
|
||||||
|
echo "nginx started."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "nginx command not found on this system."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute all other commands with parameters
|
||||||
|
num_args=$((num_args + 1))
|
||||||
|
exec "${@:num_args}"
|
|
@ -0,0 +1,32 @@
|
||||||
|
// @ts-check
|
||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { import("protractor").Config }
|
||||||
|
*/
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 60000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.e2e-spec.ts'
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
browserName: 'chrome'
|
||||||
|
},
|
||||||
|
directConnect: true,
|
||||||
|
baseUrl: 'http://localhost:4200/',
|
||||||
|
framework: 'jasmine',
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
showColors: true,
|
||||||
|
defaultTimeoutInterval: 60000,
|
||||||
|
print: function() {}
|
||||||
|
},
|
||||||
|
onPrepare() {
|
||||||
|
require('ts-node').register({
|
||||||
|
project: require('path').join(__dirname, './tsconfig.json')
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,125 @@
|
||||||
|
import {HttpClient} from 'protractor-http-client';
|
||||||
|
import {AppPage} from './app.po';
|
||||||
|
|
||||||
|
describe('Clinical Research Coordinator App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
let http: HttpClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
http = new HttpClient('http://localhost:5001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically sign-in and redirect to home screen', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getRoute()).toEqual('/home');
|
||||||
|
expect(page.getElements('#cta_protocol_builder').count()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to help screen', () => {
|
||||||
|
page.clickAndExpectRoute('#nav_help', '/help');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to profile', () => {
|
||||||
|
page.clickElement('#nav_account');
|
||||||
|
page.clickAndExpectRoute('#nav_profile', '/profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to notifications', () => {
|
||||||
|
page.clickElement('#nav_account');
|
||||||
|
page.clickAndExpectRoute('#nav_notifications', '/notifications');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate back to home screen', () => {
|
||||||
|
page.setLocalStorageVar('numstudy', '1');
|
||||||
|
expect(page.getLocalStorageVar('numstudy')).toEqual('1');
|
||||||
|
page.clickAndExpectRoute('#nav_home', '/home');
|
||||||
|
expect(page.getElements('#cta_protocol_builder').count()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open Protocol Builder in new window', async () => {
|
||||||
|
expect(page.getElements('#cta_protocol_builder').count()).toEqual(1);
|
||||||
|
expect(page.getElements('#cta_reload_studies').count()).toEqual(1);
|
||||||
|
|
||||||
|
// Open Protocol Builder in new tab.
|
||||||
|
const numTabsBefore = await page.getNumTabs();
|
||||||
|
page.clickElement('#cta_protocol_builder');
|
||||||
|
const numTabsAfter = await page.getNumTabs();
|
||||||
|
expect(numTabsAfter).toBeGreaterThan(numTabsBefore);
|
||||||
|
|
||||||
|
// Close Protocol Builder tab.
|
||||||
|
await page.switchFocusToTab(1);
|
||||||
|
await page.closeTab();
|
||||||
|
await page.switchFocusToTab(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load new study from Protocol Builder', async () => {
|
||||||
|
const numStudiesBefore = await page.getElements('.study-row').count();
|
||||||
|
|
||||||
|
// Add a new study to Protocol Builder.
|
||||||
|
http.post('/new_study', '' +
|
||||||
|
`STUDYID=${Math.floor(Math.random() * 100000)}&` +
|
||||||
|
`TITLE=${encodeURIComponent('New study title')}&` +
|
||||||
|
`NETBADGEID=dhf8r&` +
|
||||||
|
`DATE_MODIFIED=${encodeURIComponent(new Date().toISOString())}&` +
|
||||||
|
`requirements=9&` +
|
||||||
|
`requirements=21&` +
|
||||||
|
`requirements=40&` +
|
||||||
|
`requirements=44&` +
|
||||||
|
`requirements=52&` +
|
||||||
|
`requirements=53&` +
|
||||||
|
`Q_COMPLETE=y`,
|
||||||
|
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
).catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
http.failOnHttpError = false;
|
||||||
|
|
||||||
|
// Reload the list of studies.
|
||||||
|
await page.clickElement('#cta_reload_studies');
|
||||||
|
await page.waitForNotVisible('.loading');
|
||||||
|
await page.waitForClickable('.study-row');
|
||||||
|
|
||||||
|
const numStudiesAfter = await page.getElements('.study-row').count();
|
||||||
|
expect(numStudiesAfter).toBeGreaterThan(numStudiesBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to a study', async () => {
|
||||||
|
const studyRow = page.getElement('.study-row');
|
||||||
|
const studyId = await studyRow.getAttribute('data-study-id');
|
||||||
|
await expect(studyId).not.toBeUndefined();
|
||||||
|
await expect(studyId).not.toBeNull();
|
||||||
|
page.clickAndExpectRoute('.study-row', '/study/' + studyId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display workflow spec categories in tiles', async () => {
|
||||||
|
const numTiles = await page.getElements('.workflow-list-item').count();
|
||||||
|
expect(numTiles).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to a workflow', async () => {
|
||||||
|
const wfSelector = '.workflow-list-item .workflow-action';
|
||||||
|
expect(page.getElements(wfSelector).count()).toBeGreaterThan(0);
|
||||||
|
const workflow = await page.getElement(wfSelector);
|
||||||
|
const studyId = await workflow.getAttribute('data-study-id');
|
||||||
|
const catId = await workflow.getAttribute('data-category-id');
|
||||||
|
const workflowId = await workflow.getAttribute('data-workflow-id');
|
||||||
|
|
||||||
|
console.log('studyId', studyId);
|
||||||
|
console.log('catId', catId);
|
||||||
|
console.log('workflowId', workflowId);
|
||||||
|
const expectedRoute = `/study/${studyId}?category=${catId}&workflow=${workflowId}`;
|
||||||
|
await page.clickElement(wfSelector);
|
||||||
|
const newRoute = await page.getRoute();
|
||||||
|
expect(newRoute.slice(0, expectedRoute.length)).toEqual(expectedRoute);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: CATCH 401/403 ERRORS AND VERIFY THAT THEY REDIRECT TO LOGIN
|
||||||
|
// afterEach(async () => {
|
||||||
|
// // Assert that there are no errors emitted from the browser
|
||||||
|
// const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||||
|
// expect(logs).not.toContain(jasmine.objectContaining({
|
||||||
|
// level: logging.Level.SEVERE,
|
||||||
|
// } as logging.Entry));
|
||||||
|
// });
|
||||||
|
});
|
|
@ -0,0 +1,103 @@
|
||||||
|
import {browser, by, element, ElementArrayFinder, ElementFinder, ExpectedConditions} from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
navigateTo() {
|
||||||
|
return browser.get(browser.baseUrl) as Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickAndExpectRoute(clickSelector: string, expectedRoute: string | RegExp) {
|
||||||
|
this.waitForClickable(clickSelector);
|
||||||
|
this.clickElement(clickSelector);
|
||||||
|
if (typeof expectedRoute === 'string') {
|
||||||
|
expect(this.getRoute()).toEqual(expectedRoute);
|
||||||
|
} else {
|
||||||
|
expect(this.getRoute()).toMatch(expectedRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clickElement(selector: string) {
|
||||||
|
this.waitForClickable(selector);
|
||||||
|
this.scrollTo(selector);
|
||||||
|
this.focus(selector);
|
||||||
|
return this.getElement(selector).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTab() {
|
||||||
|
return browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
focus(selector: string) {
|
||||||
|
return browser.controlFlow().execute(() => {
|
||||||
|
return browser.executeScript('arguments[0].focus()', this.getElement(selector).getWebElement());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement(selector: string): ElementFinder {
|
||||||
|
return element.all(by.css(selector)).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
getElements(selector: string): ElementArrayFinder {
|
||||||
|
return element.all(by.css(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalStorageVar(name: string) {
|
||||||
|
return browser.executeScript(`return window.localStorage.getItem('${name}');`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNumTabs() {
|
||||||
|
return browser.getAllWindowHandles().then(wh => {
|
||||||
|
return wh.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoute() {
|
||||||
|
const url = await this.getUrl();
|
||||||
|
return '/' + url.split(browser.baseUrl)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(selector: string) {
|
||||||
|
return element(by.css(selector)).getText() as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl() {
|
||||||
|
return browser.getCurrentUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTo(selector: string) {
|
||||||
|
browser.controlFlow().execute(() => {
|
||||||
|
browser.executeScript('arguments[0].scrollIntoView(false)', this.getElement(selector).getWebElement());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalStorageVar(name: string, value: string) {
|
||||||
|
return browser.executeScript(`return window.localStorage.setItem('${name}','${value}');`);
|
||||||
|
}
|
||||||
|
|
||||||
|
switchFocusToTab(tabIndex: number) {
|
||||||
|
return browser.getAllWindowHandles().then(wh => {
|
||||||
|
return wh.forEach((h, i) => {
|
||||||
|
if (i === tabIndex) { return browser.switchTo().window(h); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
waitFor(t: number) {
|
||||||
|
return browser.sleep(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForClickable(selector: string) {
|
||||||
|
const e = this.getElement(selector);
|
||||||
|
return browser.wait(ExpectedConditions.elementToBeClickable(e), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotVisible(selector: string) {
|
||||||
|
const e = this.getElement(selector);
|
||||||
|
return browser.wait(ExpectedConditions.invisibilityOf(e), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForVisible(selector: string) {
|
||||||
|
const e = this.getElement(selector);
|
||||||
|
return browser.wait(ExpectedConditions.visibilityOf(e), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
|
||||||
|
if (config.browsers.indexOf('ChromeHeadlessCI') !== -1) {
|
||||||
|
process.env.CHROME_BIN = require('puppeteer').executablePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set({
|
||||||
|
files: ['karma.globals.js'],
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage-istanbul-reporter'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
coverageIstanbulReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage'),
|
||||||
|
reports: ['lcovonly'],
|
||||||
|
fixWebpackSourcePaths: true
|
||||||
|
},
|
||||||
|
customLaunchers: {
|
||||||
|
ChromeHeadlessCI: {
|
||||||
|
base: 'ChromeHeadless',
|
||||||
|
flags: ['--no-sandbox', '--disable-gpu' ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
var ENV = {
|
||||||
|
production: 'false',
|
||||||
|
api: 'apiRoot',
|
||||||
|
irbUrl: 'irbUrl',
|
||||||
|
homeRoute: 'home',
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
server {
|
||||||
|
listen $PORT0;
|
||||||
|
|
||||||
|
port_in_redirect off;
|
||||||
|
|
||||||
|
location $BASE_HREF/ {
|
||||||
|
alias /etc/nginx/html/;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri$args $uri $BASE_HREF/index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"name": "cr-connect-frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"build:prod": "ng build --configuration=production --prod --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
|
||||||
|
"build:staging": "ng build --configuration=staging --prod --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
|
||||||
|
"build:test": "ng build --configuration=test",
|
||||||
|
"test": "ng test",
|
||||||
|
"test:coverage": "ng test --codeCoverage=true --watch=false --browsers=ChromeHeadless",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "./node_modules/protractor/bin/webdriver-manager update && ng e2e",
|
||||||
|
"e2e:with-backend": "npm run backend && ng e2e && npm run backend:stop",
|
||||||
|
"backend:stop": "cd docker && docker-compose down && cd ..",
|
||||||
|
"backend:build": "cd docker && docker-compose pull && docker-compose build && cd ..",
|
||||||
|
"backend:start": "cd docker && docker-compose up -d --force-recreate && cd ..",
|
||||||
|
"backend": "npm run backend:stop && npm run backend:build && npm run backend:start",
|
||||||
|
"env": "chmod +x ./docker/substitute-env-variables.sh && ./docker/substitute-env-variables.sh src/index.html PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,DEPLOY_URL,PORT0,GOOGLE_ANALYTICS_KEY,SENTRY_KEY,TITLE",
|
||||||
|
"ci": "npm run lint && npm run test:coverage && sonar-scanner && npm run env && npm run backend && npm run e2e"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "~9.1.11",
|
||||||
|
"@angular/cdk": "^9.2.4",
|
||||||
|
"@angular/common": "~9.1.11",
|
||||||
|
"@angular/compiler": "~9.1.11",
|
||||||
|
"@angular/core": "~9.1.11",
|
||||||
|
"@angular/flex-layout": "^9.0.0-beta.31",
|
||||||
|
"@angular/forms": "~9.1.11",
|
||||||
|
"@angular/material": "^9.2.4",
|
||||||
|
"@angular/platform-browser": "~9.1.11",
|
||||||
|
"@angular/platform-browser-dynamic": "~9.1.11",
|
||||||
|
"@angular/router": "~9.1.11",
|
||||||
|
"@ngx-formly/core": "^5.8.0",
|
||||||
|
"@ngx-formly/material": "^5.8.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"rfdc": "^1.1.4",
|
||||||
|
"rxjs": "~6.5.4",
|
||||||
|
"tslib": "^1.13.0",
|
||||||
|
"zone.js": "~0.10.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^0.901.10",
|
||||||
|
"@angular/cli": "^9.1.10",
|
||||||
|
"@angular/compiler-cli": "~9.1.11",
|
||||||
|
"@angular/language-service": "~9.1.11",
|
||||||
|
"@types/jasmine": "^3.5.11",
|
||||||
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"@types/node": "^12.12.47",
|
||||||
|
"codelyzer": "^5.1.2",
|
||||||
|
"jasmine-core": "~3.5.0",
|
||||||
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
|
"karma": "^5.1.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~2.1.1",
|
||||||
|
"karma-jasmine": "~3.1.1",
|
||||||
|
"karma-jasmine-html-reporter": "^1.5.4",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.set": "^4.3.2",
|
||||||
|
"protractor": "^7.0.0",
|
||||||
|
"protractor-http-client": "^1.0.4",
|
||||||
|
"sonar-scanner": "^3.1.0",
|
||||||
|
"ts-node": "~8.6.2",
|
||||||
|
"tslint": "~6.0.0",
|
||||||
|
"typescript": "~3.7.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
sonar.organization=sartography
|
||||||
|
sonar.projectKey=sartography_cr-connect-frontend
|
||||||
|
sonar.host.url=https://sonarcloud.io
|
||||||
|
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||||
|
sonar.typescript.tsconfigPath=tsconfig.json
|
||||||
|
sonar.coverage.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
|
||||||
|
sonar.cpd.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
|
||||||
|
sonar.exclusions=src/assets/**
|
|
@ -0,0 +1,65 @@
|
||||||
|
// GLOBAL SCSS VARIABLES
|
||||||
|
$body-font-family: 'franklin-gothic-urw', sans-serif;
|
||||||
|
$heading-font-family: 'franklin-gothic-urw-cond', sans-serif;
|
||||||
|
|
||||||
|
// COLOR PALETTE
|
||||||
|
|
||||||
|
// gray
|
||||||
|
$brand-gray: #4e4e4e;
|
||||||
|
$brand-gray-tint-1: #666666;
|
||||||
|
$brand-gray-tint-2: #DADADA;
|
||||||
|
$brand-gray-tint-3: #F1F1EF;
|
||||||
|
$brand-gray-tint-4: scale-color($brand-gray, $lightness: 90%);
|
||||||
|
$body-color: $brand-gray;
|
||||||
|
$brand-gray-shade-1: scale-color($brand-gray, $lightness: -20%);
|
||||||
|
$brand-gray-shade-2: scale-color($brand-gray, $lightness: -40%);
|
||||||
|
$brand-gray-shade-3: scale-color($brand-gray, $lightness: -60%);
|
||||||
|
$brand-gray-shade-4: scale-color($brand-gray, $lightness: -80%);
|
||||||
|
$brand-gray-shade-5: scale-color($brand-gray, $lightness: -100%);
|
||||||
|
$body-color-muted: $brand-gray-tint-1;
|
||||||
|
$body-color-light: $brand-gray-tint-4;
|
||||||
|
$brand-gray-muted: $brand-gray-tint-1;
|
||||||
|
$brand-gray-light: $brand-gray-tint-4;
|
||||||
|
|
||||||
|
// primary (UVA "Jefferson Blue")
|
||||||
|
$brand-primary: #232D4B;
|
||||||
|
$brand-primary-tint-1: #394E79;
|
||||||
|
$brand-primary-tint-2: #6C799C;
|
||||||
|
$brand-primary-tint-3: #A9AFC7;
|
||||||
|
$brand-primary-tint-4: scale-color($brand-primary, $lightness: 90%);
|
||||||
|
$brand-primary-shade-1: #092255;
|
||||||
|
$brand-primary-shade-2: #041D4F;
|
||||||
|
$brand-primary-shade-3: #02194A;
|
||||||
|
$brand-primary-shade-4: #021745;
|
||||||
|
$brand-primary-shade-5: #03143E;
|
||||||
|
$brand-primary-muted: $brand-primary-tint-1;
|
||||||
|
$brand-primary-light: $brand-primary-tint-4;
|
||||||
|
|
||||||
|
// accent (UVA "Rotunda Orange")
|
||||||
|
$brand-accent: #E57200;
|
||||||
|
$brand-accent-tint-1: #F69350;
|
||||||
|
$brand-accent-tint-2: #FAB584;
|
||||||
|
$brand-accent-tint-3: #FDD8BB;
|
||||||
|
$brand-accent-tint-4: scale-color($brand-accent, $lightness: 90%);
|
||||||
|
$brand-accent-shade-1: #E76E25;
|
||||||
|
$brand-accent-shade-2: #DD6923;
|
||||||
|
$brand-accent-shade-3: #D36421;
|
||||||
|
$brand-accent-shade-4: #C8601F;
|
||||||
|
$brand-accent-shade-5: #C05B1D;
|
||||||
|
$brand-accent-muted: $brand-accent-tint-1;
|
||||||
|
$brand-accent-light: $brand-accent-tint-4;
|
||||||
|
|
||||||
|
// warning (UVA "Emergency Red")
|
||||||
|
$brand-warning: #DF1E43;
|
||||||
|
$brand-warning-muted: desaturate(scale-color($brand-warning, $lightness: 30%), 20%);
|
||||||
|
$brand-warning-light: scale-color($brand-warning, $lightness: 90%);
|
||||||
|
|
||||||
|
// Green
|
||||||
|
$brand-green: #64B343;
|
||||||
|
$brand-green-muted: #8EC774;
|
||||||
|
$brand-green-light: #B5D9A3;
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
$easeDuration: 300ms;
|
||||||
|
$animationDuration: 500ms;
|
||||||
|
$header-height: 84px;
|
|
@ -0,0 +1,92 @@
|
||||||
|
@import 'config';
|
||||||
|
@import '../node_modules/@angular/material/theming';
|
||||||
|
|
||||||
|
// Define a custom typography config that overrides the font-family
|
||||||
|
$custom-typography: mat-typography-config(
|
||||||
|
$font-family: $body-font-family,
|
||||||
|
$display-4: mat-typography-level(3.75rem, 1.0925, 700, $body-font-family),
|
||||||
|
$display-3: mat-typography-level(2.4917rem, 1.1037, 700, $body-font-family),
|
||||||
|
$display-2: mat-typography-level(2.2148rem, 1.1287, 700, $body-font-family),
|
||||||
|
$display-1: mat-typography-level(1.9688rem, 1.1429, 700, $body-font-family),
|
||||||
|
$headline: mat-typography-level(1.7500rem, 1.1429, 700, $body-font-family), // h1
|
||||||
|
$title: mat-typography-level(1.5625rem, 1.1200, 500, $body-font-family), // h2
|
||||||
|
$subheading-2: mat-typography-level(1.3750rem, 1.0000, 700, $heading-font-family), // h3
|
||||||
|
$subheading-1: mat-typography-level(1.2500rem, 1.1000, 700, $body-font-family), // h4
|
||||||
|
$body-2: mat-typography-level(1.1250rem, 1.1111, 500, $body-font-family), // h5
|
||||||
|
$body-1: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family), // p
|
||||||
|
$caption: mat-typography-level(0.8750rem, 0.7143, 500, $body-font-family), // small
|
||||||
|
$button: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family),
|
||||||
|
$input: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mat-blue: (
|
||||||
|
50: #f1f5f7,
|
||||||
|
100: #b3c1d3,
|
||||||
|
200: $brand-primary-tint-3,
|
||||||
|
300: $brand-primary-tint-2,
|
||||||
|
400: $brand-primary-tint-1,
|
||||||
|
500: $brand-primary,
|
||||||
|
600: $brand-primary-shade-1,
|
||||||
|
700: $brand-primary-shade-2,
|
||||||
|
800: $brand-primary-shade-3,
|
||||||
|
900: $brand-primary-shade-4,
|
||||||
|
A100: #b3c1d3,
|
||||||
|
A200: $brand-primary-tint-3,
|
||||||
|
A400: $brand-primary-tint-2,
|
||||||
|
A700: $brand-primary-tint-1,
|
||||||
|
contrast: (
|
||||||
|
50: $body-color,
|
||||||
|
100: $body-color,
|
||||||
|
200: $body-color,
|
||||||
|
300: #ffffff,
|
||||||
|
400: #ffffff,
|
||||||
|
500: #ffffff,
|
||||||
|
600: #ffffff,
|
||||||
|
700: #ffffff,
|
||||||
|
800: #ffffff,
|
||||||
|
900: #ffffff,
|
||||||
|
A100: $body-color,
|
||||||
|
A200: #ffffff,
|
||||||
|
A400: #ffffff,
|
||||||
|
A700: #ffffff,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mat-orange: (
|
||||||
|
50: #fceee0,
|
||||||
|
100: $brand-accent-tint-3,
|
||||||
|
200: $brand-accent-tint-2,
|
||||||
|
300: $brand-accent-tint-1,
|
||||||
|
400: $brand-accent,
|
||||||
|
500: $brand-accent-shade-1,
|
||||||
|
600: $brand-accent-shade-2,
|
||||||
|
700: $brand-accent-shade-3,
|
||||||
|
800: $brand-accent-shade-4,
|
||||||
|
900: $brand-accent-shade-5,
|
||||||
|
A100: #fceee0,
|
||||||
|
A200: $brand-accent-tint-3,
|
||||||
|
A400: $brand-accent-tint-2,
|
||||||
|
A700: $brand-accent-tint-1,
|
||||||
|
contrast: (
|
||||||
|
50: $body-color,
|
||||||
|
100: $body-color,
|
||||||
|
200: $body-color,
|
||||||
|
300: $body-color,
|
||||||
|
400: $body-color,
|
||||||
|
500: #ffffff,
|
||||||
|
600: #ffffff,
|
||||||
|
700: #ffffff,
|
||||||
|
800: #ffffff,
|
||||||
|
900: #ffffff,
|
||||||
|
A100: $body-color,
|
||||||
|
A200: $body-color,
|
||||||
|
A400: $body-color,
|
||||||
|
A700: $body-color,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$cr-connect-primary: mat-palette($mat-blue);
|
||||||
|
$cr-connect-accent: mat-palette($mat-orange);
|
||||||
|
$cr-connect-warn: mat-palette($mat-red);
|
||||||
|
$cr-connect-theme: mat-light-theme($cr-connect-primary, $cr-connect-accent, $cr-connect-warn);
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import {APP_BASE_HREF, Location} from '@angular/common';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatCardModule} from '@angular/material/card';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatListModule} from '@angular/material/list';
|
||||||
|
import {MatMenuModule} from '@angular/material/menu';
|
||||||
|
import {MatProgressBarModule} from '@angular/material/progress-bar';
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {MatSidenavModule} from '@angular/material/sidenav';
|
||||||
|
import {MatToolbarModule} from '@angular/material/toolbar';
|
||||||
|
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {FormlyModule} from '@ngx-formly/core';
|
||||||
|
import {FormlyMaterialModule} from '@ngx-formly/material';
|
||||||
|
import {ChartsModule} from 'ng2-charts';
|
||||||
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
|
import {ApiService, MockEnvironment, SessionRedirectComponent, ToFormlyPipe} from 'sartography-workflow-lib';
|
||||||
|
import {routes} from './app-routing.module';
|
||||||
|
import {AppComponent} from './app.component';
|
||||||
|
import {DashboardComponent} from './dashboard/dashboard.component';
|
||||||
|
import {FooterComponent} from './footer/footer.component';
|
||||||
|
import {HelpComponent} from './help/help.component';
|
||||||
|
import {HomeComponent} from './home/home.component';
|
||||||
|
import {InboxComponent} from './inbox/inbox.component';
|
||||||
|
import {NavbarComponent} from './navbar/navbar.component';
|
||||||
|
import {NotificationsComponent} from './notifications/notifications.component';
|
||||||
|
import {ProfileComponent} from './profile/profile.component';
|
||||||
|
import {StudiesComponent} from './studies/studies.component';
|
||||||
|
import {StudyComponent} from './study/study.component';
|
||||||
|
import {WorkflowFilesComponent} from './workflow-files/workflow-files.component';
|
||||||
|
import {WorkflowFormComponent} from './workflow-form/workflow-form.component';
|
||||||
|
import {WorkflowStepsMenuListComponent} from './workflow-steps-menu-list/workflow-steps-menu-list.component';
|
||||||
|
import {WorkflowComponent} from './workflow/workflow.component';
|
||||||
|
|
||||||
|
|
||||||
|
describe('Router: App', () => {
|
||||||
|
let location: Location;
|
||||||
|
let router: Router;
|
||||||
|
let fixture;
|
||||||
|
const mockEnvironment = new MockEnvironment();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
FooterComponent,
|
||||||
|
HelpComponent,
|
||||||
|
HomeComponent,
|
||||||
|
InboxComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
NotificationsComponent,
|
||||||
|
ProfileComponent,
|
||||||
|
SessionRedirectComponent,
|
||||||
|
StudiesComponent,
|
||||||
|
StudyComponent,
|
||||||
|
ToFormlyPipe,
|
||||||
|
WorkflowComponent,
|
||||||
|
WorkflowFilesComponent,
|
||||||
|
WorkflowFormComponent,
|
||||||
|
WorkflowStepsMenuListComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
ChartsModule,
|
||||||
|
FormlyMaterialModule,
|
||||||
|
FormlyModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
|
MarkdownModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatListModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
|
RouterTestingModule.withRoutes(routes),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
HttpClient,
|
||||||
|
ApiService,
|
||||||
|
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
|
||||||
|
{provide: APP_BASE_HREF, useValue: '/'},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
router = TestBed.inject(Router);
|
||||||
|
location = TestBed.inject(Location);
|
||||||
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.ngZone.run(() => router.initialNavigation());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigate to "" redirects you to /', async () => {
|
||||||
|
console.log('mockEnvironment', mockEnvironment);
|
||||||
|
const success = await fixture.ngZone.run(() => router.navigate(['']));
|
||||||
|
expect(success).toBeTruthy();
|
||||||
|
expect(location.path()).toBe('/home');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
|
import {ThisEnvironment} from '../environments/environment.injectable';
|
||||||
|
import {HomeComponent} from './home/home.component';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: HomeComponent
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot(routes, {
|
||||||
|
scrollPositionRestoration: 'enabled',
|
||||||
|
anchorScrolling: 'enabled',
|
||||||
|
scrollOffset: [0, 84],
|
||||||
|
})
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: [
|
||||||
|
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule {
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<div class="mat-typography">
|
||||||
|
<app-navbar (userChanged)="reload()"></app-navbar>
|
||||||
|
<router-outlet *ngIf="!loading; else loadingMessage"></router-outlet>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #loadingMessage>
|
||||||
|
<app-loading></app-loading>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,58 @@
|
||||||
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {FakeMatIconRegistry} from '@angular/material/icon/testing';
|
||||||
|
import {MatMenuModule} from '@angular/material/menu';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {AppComponent} from './app.component';
|
||||||
|
import {FooterComponent} from './footer/footer.component';
|
||||||
|
import {NavbarComponent} from './navbar/navbar.component';
|
||||||
|
import {ApiService} from './services/api.service';
|
||||||
|
import {MockEnvironment} from './testing/environment.mock';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
let component: AppComponent;
|
||||||
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
|
const mockEnvironment = new MockEnvironment();
|
||||||
|
const mockTitle = `'Once,' said the Mock Title at last, with a deep sigh, 'I was a real Title.'`;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
FooterComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatMenuModule,
|
||||||
|
RouterTestingModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
HttpClient,
|
||||||
|
FakeMatIconRegistry,
|
||||||
|
ApiService,
|
||||||
|
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
|
||||||
|
{provide: APP_BASE_HREF, useValue: '/'},
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockEnvironment.title = mockTitle;
|
||||||
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should set the page title to match environment variable`, () => {
|
||||||
|
expect((component as any).titleService.getTitle()).toEqual(mockTitle);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {Component, Inject} from '@angular/core';
|
||||||
|
import {MatIconRegistry} from '@angular/material/icon';
|
||||||
|
import {DomSanitizer, Title} from '@angular/platform-browser';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {AppEnvironment} from './interfaces/appEnvironment.interface';
|
||||||
|
import {GoogleAnalyticsService} from './services/google-analytics.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
|
||||||
|
private titleService: Title,
|
||||||
|
) {
|
||||||
|
this.titleService.setTitle(this.environment.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
this.loading = true;
|
||||||
|
setTimeout(() => this.loading = false, 300);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {APP_BASE_HREF, PlatformLocation} from '@angular/common';
|
||||||
|
import {HttpClientModule} from '@angular/common/http';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {FormlyModule} from '@ngx-formly/core';
|
||||||
|
import {ThisEnvironment} from '../environments/environment.injectable';
|
||||||
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
|
import {AppComponent} from './app.component';
|
||||||
|
import {FooterComponent} from './footer/footer.component';
|
||||||
|
import {HomeComponent} from './home/home.component';
|
||||||
|
import {LoadingComponent} from './loading/loading.component';
|
||||||
|
import {NavbarComponent} from './navbar/navbar.component';
|
||||||
|
import {ApiService} from './services/api.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used internal to get a string instance of the `<base href="" />` value from `index.html`.
|
||||||
|
* This is an exported function, instead of a private function or inline lambda, to prevent this error:
|
||||||
|
*
|
||||||
|
* `Error encountered resolving symbol values statically.`
|
||||||
|
* `Function calls are not supported.`
|
||||||
|
* `Consider replacing the function or lambda with a reference to an exported function.`
|
||||||
|
*
|
||||||
|
* @param platformLocation an Angular service used to interact with a browser's URL
|
||||||
|
* @return a string instance of the `<base href="" />` value from `index.html`
|
||||||
|
*/
|
||||||
|
export function getBaseHref(platformLocation: PlatformLocation): string {
|
||||||
|
return platformLocation.getBaseHrefFromDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
LoadingComponent,
|
||||||
|
FooterComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
HomeComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
FormlyModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
AppRoutingModule // <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},
|
||||||
|
ApiService,
|
||||||
|
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
|
||||||
|
{provide: APP_BASE_HREF, useFactory: getBaseHref, deps: [PlatformLocation]},
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
entryComponents: []
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<p>footer works!</p>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FooterComponent } from './footer.component';
|
||||||
|
|
||||||
|
describe('FooterComponent', () => {
|
||||||
|
let component: FooterComponent;
|
||||||
|
let fixture: ComponentFixture<FooterComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ FooterComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FooterComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-footer',
|
||||||
|
templateUrl: './footer.component.html',
|
||||||
|
styleUrls: ['./footer.component.scss']
|
||||||
|
})
|
||||||
|
export class FooterComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<p>home works!</p>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
|
||||||
|
describe('HomeComponent', () => {
|
||||||
|
let component: HomeComponent;
|
||||||
|
let fixture: ComponentFixture<HomeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ HomeComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HomeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.component.html',
|
||||||
|
styleUrls: ['./home.component.scss']
|
||||||
|
})
|
||||||
|
export class HomeComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ApiError {
|
||||||
|
status_code: number;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface AppEnvironment {
|
||||||
|
production: boolean;
|
||||||
|
api: string;
|
||||||
|
title: string;
|
||||||
|
googleAnalyticsKey: string;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface Sample {
|
||||||
|
id: string;
|
||||||
|
barCodeId: string;
|
||||||
|
locationId: string;
|
||||||
|
createdAd: string;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div *ngIf="showSpinner" class="loading" fxLayoutAlign="center center">
|
||||||
|
{{message || ''}}
|
||||||
|
<mat-spinner [diameter]="diameter"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
<span *ngIf="!showSpinner">{{message || '...'}}</span>
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
|
import { LoadingComponent } from './loading.component';
|
||||||
|
|
||||||
|
describe('LoadingComponent', () => {
|
||||||
|
let component: LoadingComponent;
|
||||||
|
let fixture: ComponentFixture<LoadingComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ LoadingComponent ],
|
||||||
|
imports: [
|
||||||
|
MatProgressSpinnerModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoadingComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-loading',
|
||||||
|
templateUrl: './loading.component.html',
|
||||||
|
styleUrls: ['./loading.component.scss']
|
||||||
|
})
|
||||||
|
export class LoadingComponent implements OnInit {
|
||||||
|
@Input() showSpinner = true;
|
||||||
|
@Input() message: string;
|
||||||
|
@Input() size = 'lg';
|
||||||
|
@Input() baseSize = 24;
|
||||||
|
|
||||||
|
get diameter(): number {
|
||||||
|
switch (this.size) {
|
||||||
|
case 'xl':
|
||||||
|
return this.baseSize * 4;
|
||||||
|
case 'lg':
|
||||||
|
return this.baseSize * 3;
|
||||||
|
case 'med':
|
||||||
|
return this.baseSize * 2;
|
||||||
|
case 'sm':
|
||||||
|
return this.baseSize;
|
||||||
|
default:
|
||||||
|
return this.baseSize * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<p>navbar works!</p>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NavbarComponent } from './navbar.component';
|
||||||
|
|
||||||
|
describe('NavbarComponent', () => {
|
||||||
|
let component: NavbarComponent;
|
||||||
|
let fixture: ComponentFixture<NavbarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ NavbarComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NavbarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-navbar',
|
||||||
|
templateUrl: './navbar.component.html',
|
||||||
|
styleUrls: ['./navbar.component.scss']
|
||||||
|
})
|
||||||
|
export class NavbarComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
|
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {MatBottomSheetModule} from '@angular/material/bottom-sheet';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {MockEnvironment} from '../testing/environment.mock';
|
||||||
|
import {ApiService} from './api.service';
|
||||||
|
|
||||||
|
describe('ApiService', () => {
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
let location: Location;
|
||||||
|
let service: ApiService;
|
||||||
|
const mockEnvironment = new MockEnvironment();
|
||||||
|
const mockRouter = {
|
||||||
|
createUrlTree: jasmine.createSpy('createUrlTree'),
|
||||||
|
navigate: jasmine.createSpy('navigate')
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
MatBottomSheetModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ApiService,
|
||||||
|
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
|
||||||
|
{provide: APP_BASE_HREF, useValue: '/'},
|
||||||
|
{provide: Router, useValue: mockRouter},
|
||||||
|
{provide: Location, useValue: location},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
service = TestBed.inject(ApiService);
|
||||||
|
location = TestBed.inject(Location);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Inject, Injectable} from '@angular/core';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {Observable, throwError} from 'rxjs';
|
||||||
|
import {catchError} from 'rxjs/operators';
|
||||||
|
import {ApiError} from '../interfaces/apiError.interface';
|
||||||
|
import {AppEnvironment} from '../interfaces/appEnvironment.interface';
|
||||||
|
import {Sample} from '../interfaces/sample.interface';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
apiRoot: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
|
||||||
|
@Inject(APP_BASE_HREF) public baseHref: string,
|
||||||
|
private httpClient: HttpClient,
|
||||||
|
private router: Router,
|
||||||
|
private location: Location,
|
||||||
|
) {
|
||||||
|
this.apiRoot = environment.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the string value from a given URL */
|
||||||
|
getStringFromUrl(url: string): Observable<string> {
|
||||||
|
return this.httpClient
|
||||||
|
.get(url, {responseType: 'text'})
|
||||||
|
.pipe(catchError(err => this._handleError(err)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add new sample */
|
||||||
|
addSample(sample: Sample): Observable<Sample> {
|
||||||
|
const url = this.apiRoot;
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.put<Sample>(url, sample)
|
||||||
|
.pipe(catchError(err => this._handleError(err)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleError(error: ApiError): Observable<never> {
|
||||||
|
return throwError(error.message || 'Could not complete your request; please try again later.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {SessionRedirectComponent} from '../components/session-redirect/session-redirect.component';
|
||||||
|
import {MockEnvironment} from '../testing/mocks/environment.mocks';
|
||||||
|
import {GoogleAnalyticsService} from './google-analytics.service';
|
||||||
|
|
||||||
|
describe('GoogleAnalyticsService', () => {
|
||||||
|
let service: GoogleAnalyticsService;
|
||||||
|
const mockEnvironment = new MockEnvironment();
|
||||||
|
const mockRouter = {
|
||||||
|
createUrlTree: jasmine.createSpy('createUrlTree'),
|
||||||
|
navigate: jasmine.createSpy('navigate'),
|
||||||
|
events: jasmine.createSpyObj('events', ['subscribe']),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
declarations: [SessionRedirectComponent],
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
RouterTestingModule.withRoutes([
|
||||||
|
{
|
||||||
|
path: 'session/:token',
|
||||||
|
component: SessionRedirectComponent
|
||||||
|
}
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
GoogleAnalyticsService,
|
||||||
|
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
|
||||||
|
{provide: APP_BASE_HREF, useValue: '/'},
|
||||||
|
{provide: Router, useValue: mockRouter},
|
||||||
|
{provide: Location, useValue: location},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = TestBed.inject(GoogleAnalyticsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
expect(service.analyticsKey).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set a new analytics key', () => {
|
||||||
|
service.init('new_key');
|
||||||
|
expect(service.analyticsKey).toEqual('new_key');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,82 @@
|
||||||
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
|
import {HttpRequest} from '@angular/common/http';
|
||||||
|
import {Inject, Injectable} from '@angular/core';
|
||||||
|
import {NavigationEnd, Router} from '@angular/router';
|
||||||
|
import {ApiError} from '../interfaces/apiError.interface';
|
||||||
|
import {AppEnvironment} from '../interfaces/appEnvironment.interface';
|
||||||
|
|
||||||
|
declare var gtag;
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class GoogleAnalyticsService {
|
||||||
|
analyticsKey: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
|
||||||
|
@Inject(APP_BASE_HREF) public baseHref: string,
|
||||||
|
private router: Router,
|
||||||
|
) {
|
||||||
|
this.analyticsKey = this.environment.googleAnalyticsKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public authEvent(req: HttpRequest<any>) {
|
||||||
|
this.event('login', 'authentication', req.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
public errorEvent(error: ApiError) {
|
||||||
|
this.event(error.code, 'error_messages', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUser(uid) {
|
||||||
|
if (gtag) {
|
||||||
|
gtag('set', {user_id: uid}); // Set the user ID using signed-in user_id.
|
||||||
|
this.event('user-id available', 'authentication', uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(analyticsKey) {
|
||||||
|
this.analyticsKey = analyticsKey || this.analyticsKey;
|
||||||
|
this.listenForRouteChanges();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const script1 = document.createElement('script');
|
||||||
|
script1.async = true;
|
||||||
|
script1.src = 'https://www.googletagmanager.com/gtag/js?id=' + this.analyticsKey;
|
||||||
|
document.head.appendChild(script1);
|
||||||
|
|
||||||
|
const script2 = document.createElement('script');
|
||||||
|
script2.innerHTML = `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', '` + this.analyticsKey + `', {'send_page_view': false});
|
||||||
|
`;
|
||||||
|
document.head.appendChild(script2);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('Error appending google analytics');
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private event(action: string, category: string, label: string) {
|
||||||
|
if (gtag) {
|
||||||
|
gtag('event', action, {
|
||||||
|
event_category: category,
|
||||||
|
event_label: label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenForRouteChanges() {
|
||||||
|
const analyticsKey = this.environment.googleAnalyticsKey;
|
||||||
|
this.router.events.subscribe(event => {
|
||||||
|
if (gtag && event instanceof NavigationEnd) {
|
||||||
|
gtag('config', analyticsKey, {
|
||||||
|
page_path: event.urlAfterRedirects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {AppEnvironment} from '../interfaces/appEnvironment.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MockEnvironment implements AppEnvironment {
|
||||||
|
production = false;
|
||||||
|
api = 'apiRoot';
|
||||||
|
title = 'Mock Title';
|
||||||
|
googleAnalyticsKey = '';
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>
|
After Width: | Height: | Size: 480 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>
|
After Width: | Height: | Size: 427 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M17,11H13V13H14L12,14.67L10,13H11V11H7V13H8L11,15.5L8,18H7V20H11V18H10L12,16.33L14,18H13V20H17V18H16L13,15.5L16,13H17V11Z" /></svg>
|
After Width: | Height: | Size: 501 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>
|
After Width: | Height: | Size: 754 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M13,3.5L18.5,9H13V3.5M12,11A3,3 0 0,1 15,14C15,15.88 12.75,16.06 12.75,17.75H11.25C11.25,15.31 13.5,15.5 13.5,14A1.5,1.5 0 0,0 12,12.5A1.5,1.5 0 0,0 10.5,14H9A3,3 0 0,1 12,11M11.25,18.5H12.75V20H11.25V18.5Z" /></svg>
|
After Width: | Height: | Size: 569 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>
|
After Width: | Height: | Size: 754 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>
|
After Width: | Height: | Size: 427 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>
|
After Width: | Height: | Size: 480 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>
|
After Width: | Height: | Size: 427 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>
|
After Width: | Height: | Size: 427 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>
|
After Width: | Height: | Size: 754 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>
|
After Width: | Height: | Size: 480 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M14,9H19.5L14,3.5V9M7,2H15L21,8V20A2,2 0 0,1 19,22H7C5.89,22 5,21.1 5,20V4A2,2 0 0,1 7,2M11.93,12.44C12.34,13.34 12.86,14.08 13.46,14.59L13.87,14.91C13,15.07 11.8,15.35 10.53,15.84V15.84L10.42,15.88L10.92,14.84C11.37,13.97 11.7,13.18 11.93,12.44M18.41,16.25C18.59,16.07 18.68,15.84 18.69,15.59C18.72,15.39 18.67,15.2 18.57,15.04C18.28,14.57 17.53,14.35 16.29,14.35L15,14.42L14.13,13.84C13.5,13.32 12.93,12.41 12.53,11.28L12.57,11.14C12.9,9.81 13.21,8.2 12.55,7.54C12.39,7.38 12.17,7.3 11.94,7.3H11.7C11.33,7.3 11,7.69 10.91,8.07C10.54,9.4 10.76,10.13 11.13,11.34V11.35C10.88,12.23 10.56,13.25 10.05,14.28L9.09,16.08L8.2,16.57C7,17.32 6.43,18.16 6.32,18.69C6.28,18.88 6.3,19.05 6.37,19.23L6.4,19.28L6.88,19.59L7.32,19.7C8.13,19.7 9.05,18.75 10.29,16.63L10.47,16.56C11.5,16.23 12.78,16 14.5,15.81C15.53,16.32 16.74,16.55 17.5,16.55C17.94,16.55 18.24,16.44 18.41,16.25M18,15.54L18.09,15.65C18.08,15.75 18.05,15.76 18,15.78H17.96L17.77,15.8C17.31,15.8 16.6,15.61 15.87,15.29C15.96,15.19 16,15.19 16.1,15.19C17.5,15.19 17.9,15.44 18,15.54M8.83,17C8.18,18.19 7.59,18.85 7.14,19C7.19,18.62 7.64,17.96 8.35,17.31L8.83,17M11.85,10.09C11.62,9.19 11.61,8.46 11.78,8.04L11.85,7.92L12,7.97C12.17,8.21 12.19,8.53 12.09,9.07L12.06,9.23L11.9,10.05L11.85,10.09Z" /></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>
|
After Width: | Height: | Size: 754 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M8,11V13H9V19H8V20H12V19H11V17H13A3,3 0 0,0 16,14A3,3 0 0,0 13,11H8M13,13A1,1 0 0,1 14,14A1,1 0 0,1 13,15H11V13H13Z" /></svg>
|
After Width: | Height: | Size: 495 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M8,11V13H9V19H8V20H12V19H11V17H13A3,3 0 0,0 16,14A3,3 0 0,0 13,11H8M13,13A1,1 0 0,1 14,14A1,1 0 0,1 13,15H11V13H13Z" /></svg>
|
After Width: | Height: | Size: 495 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>
|
After Width: | Height: | Size: 545 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>
|
After Width: | Height: | Size: 754 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M13,3.5L18.5,9H13V3.5M12,11A3,3 0 0,1 15,14C15,15.88 12.75,16.06 12.75,17.75H11.25C11.25,15.31 13.5,15.5 13.5,14A1.5,1.5 0 0,0 12,12.5A1.5,1.5 0 0,0 10.5,14H9A3,3 0 0,1 12,11M11.25,18.5H12.75V20H11.25V18.5Z" /></svg>
|
After Width: | Height: | Size: 569 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>
|
After Width: | Height: | Size: 480 B |