diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml new file mode 100644 index 0000000..7ed62ec --- /dev/null +++ b/.github/workflows/deploy-dev.yaml @@ -0,0 +1,52 @@ +name: Deploy on dev +on: + workflow_dispatch: + inputs: + version: + description: 'App version to deploy' + required: true + default: '1.0.0' + +env: + ENVIRONMENT: dev + REGION: eu-central-1 + TEAM: Platform-Engineering + +jobs: + release: + name: Deploy workoutrecorder-frontend + runs-on: [self-hosted, dev] + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Pull SSH key for Ansible connection + run: | + cd ansible + aws s3 cp \ + s3://red-devops-workout-recorder/post-provisioning/app_${{ env.ENVIRONMENT }}_key.pem \ + app_${{ env.ENVIRONMENT }}_key.pem + chmod 600 app_${{ env.ENVIRONMENT }}_key.pem + + - name: Download artifact + run: | + cd ansible + aws s3 cp \ + s3://red-devops-workout-recorder/release/workoutrecorder-frontend-${{ github.event.inputs.version }}.zip \ + workoutrecorder-frontend-${{ github.event.inputs.version }}.zip + + - name: Ansible Playbook + run: | + cd ansible + ansible-playbook \ + deploy.yaml \ + -i ./inventories/${{ env.ENVIRONMENT }}/inventory_aws_ec2.yaml \ + -extra-vars="app_version=${{ github.event.inputs.version }}" \ + --private-key app_${{ env.ENVIRONMENT }}_key.pem + + - name: Remove SSH key + if: always() + run: | + cd ansible + rm app_${{ env.ENVIRONMENT }}_key.pem -f + continue-on-error: true diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml new file mode 100644 index 0000000..7f3bf3e --- /dev/null +++ b/.github/workflows/deploy-prod.yaml @@ -0,0 +1,52 @@ +name: Deploy on prod +on: + workflow_dispatch: + inputs: + version: + description: 'App version to deploy' + required: true + default: '1.0.0' + +env: + ENVIRONMENT: prod + REGION: eu-central-1 + TEAM: Platform-Engineering + +jobs: + release: + name: Deploy workoutrecorder-frontend + runs-on: [self-hosted, prod] + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Pull SSH key for Ansible connection + run: | + cd ansible + aws s3 cp \ + s3://red-devops-workout-recorder/post-provisioning/app_${{ env.ENVIRONMENT }}_key.pem \ + app_${{ env.ENVIRONMENT }}_key.pem + chmod 600 app_${{ env.ENVIRONMENT }}_key.pem + + - name: Download artifact + run: | + cd ansible + aws s3 cp \ + s3://red-devops-workout-recorder/release/workoutrecorder-frontend-${{ github.event.inputs.version }}.zip \ + workoutrecorder-frontend-${{ github.event.inputs.version }}.zip + + - name: Ansible Playbook + run: | + cd ansible + ansible-playbook \ + deploy.yaml \ + -i ./inventories/${{ env.ENVIRONMENT }}/inventory_aws_ec2.yaml \ + -extra-vars="app_version=${{ github.event.inputs.version }}" \ + --private-key app_${{ env.ENVIRONMENT }}_key.pem + + - name: Remove SSH key + if: always() + run: | + cd ansible + rm app_${{ env.ENVIRONMENT }}_key.pem -f + continue-on-error: true diff --git a/.github/workflows/deploy-uat.yaml b/.github/workflows/deploy-uat.yaml new file mode 100644 index 0000000..05a742b --- /dev/null +++ b/.github/workflows/deploy-uat.yaml @@ -0,0 +1,52 @@ +name: Deploy on uat +on: + workflow_dispatch: + inputs: + version: + description: 'App version to deploy' + required: true + default: '1.0.0' + +env: + ENVIRONMENT: uat + REGION: eu-central-1 + TEAM: Platform-Engineering + +jobs: + release: + name: Deploy workoutrecorder-frontend + runs-on: [self-hosted, uat] + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Pull SSH key for Ansible connection + run: | + cd ansible + aws s3 cp \ + s3://red-devops-workout-recorder/post-provisioning/app_${{ env.ENVIRONMENT }}_key.pem \ + app_${{ env.ENVIRONMENT }}_key.pem + chmod 600 app_${{ env.ENVIRONMENT }}_key.pem + + - name: Download artifact + run: | + cd ansible + aws s3 cp \ + s3://red-devops-workout-recorder/release/workoutrecorder-frontend-${{ github.event.inputs.version }}.zip \ + workoutrecorder-frontend-${{ github.event.inputs.version }}.zip + + - name: Ansible Playbook + run: | + cd ansible + ansible-playbook \ + deploy.yaml \ + -i ./inventories/${{ env.ENVIRONMENT }}/inventory_aws_ec2.yaml \ + -extra-vars="app_version=${{ github.event.inputs.version }}" \ + --private-key app_${{ env.ENVIRONMENT }}_key.pem + + - name: Remove SSH key + if: always() + run: | + cd ansible + rm app_${{ env.ENVIRONMENT }}_key.pem -f + continue-on-error: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..32f2a6d --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,52 @@ +name: Release workoutrecorder-frontend +on: + workflow_dispatch: + inputs: + version: + description: 'App version to release' + required: true + default: '1.0.0' + fabioip: + description: 'Public fabio ip addres' + required: true + +jobs: + release: + name: Release workoutrecorder-frontend + runs-on: self-hosted + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v2 + + - name: Set app version + run: | + version=${{ github.event.inputs.version }} + sed -i "s//$version/g" frontend/package.json + + - name: Set fabio public ip + run: | + fabioip=${{ github.event.inputs.fabioip }} + sed -i "s//$fabioip/g" frontend/src/environments/environment.prod.ts + + - name: Install dependency + run: | + mkdir app + cp frontend/package.json app + cd app + npm install + + - name: Build and Package + run: | + cp frontend/* app -r + cd app + npm run build --prod + zip -jr workoutrecorder-frontend-${{ github.event.inputs.version }}.zip dist/workoutrecorder-front/* + + - name: Upload Release + run: | + aws s3 cp \ + app/workoutrecorder-frontend-${{ github.event.inputs.version }}.zip \ + s3://red-devops-workout-recorder/release/workoutrecorder-frontend-${{ github.event.inputs.version }}.zip diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..50792e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Red-DevOps + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d129a88 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Workoutrecorder - Frontend App + +The content of this project is part of the post on blog https://red-devops.pl/
+The repository consists of two parts. The first part contained in the frontend folder contains the source code of the Angular application. The second part of the application, contained in the ansibl folder, contains the Ansible playbook that deploys applications to remote servers. This repository is part of a larger workout-recorder project which can be found here https://github.com/red-devops/Workout-recorder-consul. The application should be deployed on hoset after executing the HashiCorp Terraform from folder 100-app-infra/180-instances from the main repo. \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..4b075b3 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,4 @@ +[defaults] +remote_user = ubuntu +host_key_checking = False +enable_plugins = host_list, virtualbox, yaml, constructed, aws_ec2, script, ini, auto, toml \ No newline at end of file diff --git a/ansible/deploy.yaml b/ansible/deploy.yaml new file mode 100644 index 0000000..a4329bb --- /dev/null +++ b/ansible/deploy.yaml @@ -0,0 +1,17 @@ +--- +- name: Deploy workoutrecorder backend + hosts: all + become: true + roles: + + - role: prerequisites + tags: + - prerequisites + + - role: consul + tags: + - consul + + - role: frontend + tags: + - frontend diff --git a/ansible/group_vars/all.yaml b/ansible/group_vars/all.yaml new file mode 100644 index 0000000..c6c72cd --- /dev/null +++ b/ansible/group_vars/all.yaml @@ -0,0 +1,19 @@ +#APP +app: "frontend" +user: frontend +group: frontend +mode: u+rwx,g+r,o+r +app_port: 80 +app_home: "/home/{{ user }}" +app_data: "{{ app_home }}/data" +app_config_home: "{{ app_home }}/config" +app_bin_home: "{{ app_home }}/bin" +app_log_home: "{{ app_home }}/log" +app_version: 1.0.0 +sites_available_dir: /etc/nginx/sites-available + +#AWS +region: "eu-central-1" + +#Consul +consul_bootstrap_secret_name: Consul-Global-Managemen-Token-{{ env }} \ No newline at end of file diff --git a/ansible/inventories/dev/group_vars/all.yaml b/ansible/inventories/dev/group_vars/all.yaml new file mode 100644 index 0000000..b2def8c --- /dev/null +++ b/ansible/inventories/dev/group_vars/all.yaml @@ -0,0 +1,2 @@ +# Dev environment +env: dev diff --git a/ansible/inventories/dev/inventory b/ansible/inventories/dev/inventory new file mode 100644 index 0000000..8bd0203 --- /dev/null +++ b/ansible/inventories/dev/inventory @@ -0,0 +1,2 @@ +[all] +localhost ansible_connection=local \ No newline at end of file diff --git a/ansible/inventories/dev/inventory_aws_ec2.yaml b/ansible/inventories/dev/inventory_aws_ec2.yaml new file mode 100644 index 0000000..449ad26 --- /dev/null +++ b/ansible/inventories/dev/inventory_aws_ec2.yaml @@ -0,0 +1,13 @@ +--- +plugin: aws_ec2 +regions: + - "eu-central-1" +filters: + instance-state-name: running + tag:Name: EC2-dev-frontend-ASG + tag:Environment: dev + tag:ostype: linux +compose: + private_ip_address: private_ip_address +hostnames: + - private-ip-address \ No newline at end of file diff --git a/ansible/inventories/prod/group_vars/all.yaml b/ansible/inventories/prod/group_vars/all.yaml new file mode 100644 index 0000000..133afa5 --- /dev/null +++ b/ansible/inventories/prod/group_vars/all.yaml @@ -0,0 +1,2 @@ +# Prod environment +env: prod \ No newline at end of file diff --git a/ansible/inventories/prod/inventory b/ansible/inventories/prod/inventory new file mode 100644 index 0000000..8bd0203 --- /dev/null +++ b/ansible/inventories/prod/inventory @@ -0,0 +1,2 @@ +[all] +localhost ansible_connection=local \ No newline at end of file diff --git a/ansible/inventories/prod/inventory_aws_ec2.yaml b/ansible/inventories/prod/inventory_aws_ec2.yaml new file mode 100644 index 0000000..e3db213 --- /dev/null +++ b/ansible/inventories/prod/inventory_aws_ec2.yaml @@ -0,0 +1,13 @@ +--- +plugin: aws_ec2 +regions: + - "eu-central-1" +filters: + instance-state-name: running + tag:Name: EC2-prod-backend-ASG + tag:Environment: prod + tag:ostype: linux +compose: + private_ip_address: private_ip_address +hostnames: + - private-ip-address \ No newline at end of file diff --git a/ansible/inventories/uat/group_vars/all.yaml b/ansible/inventories/uat/group_vars/all.yaml new file mode 100644 index 0000000..4af678b --- /dev/null +++ b/ansible/inventories/uat/group_vars/all.yaml @@ -0,0 +1,2 @@ +# Uat environment +env: uat \ No newline at end of file diff --git a/ansible/inventories/uat/inventory b/ansible/inventories/uat/inventory new file mode 100644 index 0000000..8bd0203 --- /dev/null +++ b/ansible/inventories/uat/inventory @@ -0,0 +1,2 @@ +[all] +localhost ansible_connection=local \ No newline at end of file diff --git a/ansible/inventories/uat/inventory_aws_ec2.yaml b/ansible/inventories/uat/inventory_aws_ec2.yaml new file mode 100644 index 0000000..0a5656f --- /dev/null +++ b/ansible/inventories/uat/inventory_aws_ec2.yaml @@ -0,0 +1,13 @@ +--- +plugin: aws_ec2 +regions: + - "eu-central-1" +filters: + instance-state-name: running + tag:Name: EC2-uat-backend-ASG + tag:Environment: uat + tag:ostype: linux +compose: + private_ip_address: private_ip_address +hostnames: + - private-ip-address \ No newline at end of file diff --git a/ansible/roles/consul/defaults/main.yaml b/ansible/roles/consul/defaults/main.yaml new file mode 100644 index 0000000..1d52777 --- /dev/null +++ b/ansible/roles/consul/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +# Defaults for consul agent diff --git a/ansible/roles/consul/tasks/main.yaml b/ansible/roles/consul/tasks/main.yaml new file mode 100644 index 0000000..a031a27 --- /dev/null +++ b/ansible/roles/consul/tasks/main.yaml @@ -0,0 +1,62 @@ +--- +# Task file for consul +- name: Render script + template: + src: "{{ item }}" + dest: "{{ item }}" + mode: "{{ mode }}" + delegate_to: localhost + with_items: + - "get_keygen.sh" + - "get_service_token.sh" + +- name: Get keygen form vault + command: "./get_keygen.sh" + register: keygen_output + delegate_to: localhost + +- name: Set keygen variable + set_fact: + keygen: "{{ keygen_output.stdout | from_json | json_query('data.data.key') }}" + +- name: Get service token + command: "./get_service_token.sh" + register: service_token_output + delegate_to: localhost + +- name: Set service token + set_fact: + service_token: "{{ service_token_output.stdout }}" + +- name: Install consul + unarchive: + src: "{{ item }}" + dest: "{{ app_bin_home }}" + remote_src: yes + owner: "{{ user }}" + group: "{{ group }}" + with_items: + - "https://releases.hashicorp.com/consul/1.16.0/consul_1.16.0_linux_amd64.zip" + +- name: Copy configuration + template: + src: "config.hcl" + dest: "{{ app_config_home }}/consul_config.hcl" + owner: "{{ user }}" + group: "{{ group }}" + mode: "{{ mode }}" + +- name: Copy systemd consul-agent.service + template: + src: "consul-agent.service" + dest: "/usr/lib/systemd/system/consul-agent.service" + owner: "{{ user }}" + group: "{{ group }}" + mode: "{{ mode }}" + +- name: Start Consul agent + systemd: + name: consul-agent + state: restarted + daemon_reload: yes + enabled: yes \ No newline at end of file diff --git a/ansible/roles/consul/templates/config.hcl b/ansible/roles/consul/templates/config.hcl new file mode 100644 index 0000000..c6bf7f2 --- /dev/null +++ b/ansible/roles/consul/templates/config.hcl @@ -0,0 +1,63 @@ +# Main +server = false +datacenter = "{{ env }}-{{ region }}" +domain = "consul" +node_name = "{{ app }}-{{ private_ip_address }}" +rejoin_after_leave = true +leave_on_terminate = true + +# Logging +log_level = "INFO" +enable_syslog = true #jak bedzie dzialac zmienic na false + +# Data persistence +data_dir = "{{ app_data }}" + +# Networkig +client_addr = "127.0.0.1" +bind_addr = "{{ private_ip_address }}" + +# Join other Consul agents +retry_join = [ "provider=aws tag_key=function tag_value=consul-server region={{ region }}" ] + +# Ports +ports { + grpc = 8502 + http = 8500 + dns = 8600 +} + +## ACL configuration +acl = { + enabled = true + default_policy = "deny" + enable_token_persistence = true + tokens { + agent = "{{ service_token }}" + default = "{{ service_token }}" + } +} + +service { + name = "{{ app }}" + id = "{{ app }}-{{ private_ip_address }}" + tags = [ + "urlprefix-/" + ] + port = {{ app_port }} + address = "{{ private_ip_address }}" + token = "{{ service_token }}" + + check + { + id = "{{ app }}", + name = "{{ app }} status check", + service_id = "{{ app }}-{{ private_ip_address }}", + tcp = "localhost:{{ app_port }}", + interval = "15s", + timeout = "5s" + } +} + +# Gossip encryption +encrypt = "{{ keygen }}" diff --git a/ansible/roles/consul/templates/consul-agent.service b/ansible/roles/consul/templates/consul-agent.service new file mode 100644 index 0000000..0983565 --- /dev/null +++ b/ansible/roles/consul/templates/consul-agent.service @@ -0,0 +1,17 @@ +[Unit] +Description="HashiCorp Consul Agent" +Documentation=https://www.consul.io/ +Requires=network-online.target +After=network-online.target + +[Service] +Restart=on-failure +PermissionsStartOnly=true +ExecStart=/bin/sh -c 'exec {{ app_bin_home }}/consul agent -config-file={{ app_config_home }}/consul_config.hcl >> {{ app_log_home }}/consul.log 2>&1' +ExecReload=/bin/kill -HUP $MAINPID +KillSignal=SIGTERM +User={{ user }} +Group={{ group }} + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/ansible/roles/consul/templates/get_keygen.sh b/ansible/roles/consul/templates/get_keygen.sh new file mode 100644 index 0000000..d6a295f --- /dev/null +++ b/ansible/roles/consul/templates/get_keygen.sh @@ -0,0 +1,5 @@ +#!/bin/bash +export VAULT_ADDR=http://$(aws ec2 describe-instances --region {{ region }} --filters "Name=tag:Name,Values=vault-{{ env }}" --query 'Reservations[].Instances[].PrivateIpAddress' --output text):8200 +export VAULT_TOKEN=$(aws secretsmanager get-secret-value --secret-id cicd-vault-{{ env }}-token --region {{ region }} --query 'SecretString' --output text | awk -F'"' '{print $4}') + +/home/ubuntu/bin/vault kv get -format=json kv/consul/keygen \ No newline at end of file diff --git a/ansible/roles/consul/templates/get_service_token.sh b/ansible/roles/consul/templates/get_service_token.sh new file mode 100644 index 0000000..462e6b3 --- /dev/null +++ b/ansible/roles/consul/templates/get_service_token.sh @@ -0,0 +1,5 @@ +#!/bin/bash +export CONSUL_HTTP_ADDR=http://$(aws ec2 describe-instances --region {{ region }} --filters "Name=tag:Name,Values=consul-{{ env }}" --query 'Reservations[].Instances[].PrivateIpAddress' --output text | awk '{print $1}'):8500 +export CONSUL_HTTP_TOKEN=$(aws secretsmanager get-secret-value --secret-id {{ consul_bootstrap_secret_name }} --region {{ region }} | jq -r '.["SecretString"] | fromjson | ."acl-bootsrap-token"') + +/home/ubuntu/bin/consul acl token list -format=json | jq -r '.[] | select(.Policies[].Name == "{{ app }}") | .SecretID' diff --git a/ansible/roles/frontend/defaults/main.yaml b/ansible/roles/frontend/defaults/main.yaml new file mode 100644 index 0000000..7c2d6ba --- /dev/null +++ b/ansible/roles/frontend/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +# Defaults for workoutrecorder frontend \ No newline at end of file diff --git a/ansible/roles/frontend/tasks/main.yaml b/ansible/roles/frontend/tasks/main.yaml new file mode 100644 index 0000000..cabaf72 --- /dev/null +++ b/ansible/roles/frontend/tasks/main.yaml @@ -0,0 +1,47 @@ +--- +# Task file for workoutrecorder frontend +- name: Download artifact + copy: + src: "{{ playbook_dir }}/workoutrecorder-{{ app }}-{{ app_version }}.zip" + dest: "{{ app_home }}/dest/workoutrecorder-{{ app }}-{{ app_version }}.zip" + owner: "{{ user }}" + group: "{{ group }}" + mode: "{{ mode }}" + +- name: Unzip artifact + unarchive: + src: "{{ app_home }}/dest/workoutrecorder-{{ app }}-{{ app_version }}.zip" + dest: "{{ app_home }}/dest" + owner: "{{ user }}" + group: "{{ group }}" + mode: "u+rwx,g+r,o+rx" + remote_src: yes + +- name: Copy nginx configuration + template: + src: "frontend-app" + dest: "{{ sites_available_dir }}/frontend-app" + owner: "{{ user }}" + group: "{{ group }}" + mode: "u+rwx,g+r,o+rx" + +- name: Create a symbolic link to the configuration file + file: + src: "{{ sites_available_dir }}/frontend-app" + dest: /etc/nginx/sites-enabled/frontend-app + state: link + +- name: Set default page to frontend application + lineinfile: + path: /etc/nginx/nginx.conf + search_string: "include /etc/nginx/sites-enabled/*;" + line: "\tinclude /etc/nginx/sites-enabled/frontend-app*;" + owner: root + group: root + +- name: Restart Nginx + systemd: + name: nginx + state: restarted + daemon_reload: yes + enabled: yes \ No newline at end of file diff --git a/ansible/roles/frontend/templates/frontend-app b/ansible/roles/frontend/templates/frontend-app new file mode 100644 index 0000000..fd9aa74 --- /dev/null +++ b/ansible/roles/frontend/templates/frontend-app @@ -0,0 +1,10 @@ +server { + listen 80; + server_name localhost; + + location / { + root /home/{{ app }}/dest; + index index.html; + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/ansible/roles/prerequisites/defaults/main.yaml b/ansible/roles/prerequisites/defaults/main.yaml new file mode 100644 index 0000000..5022376 --- /dev/null +++ b/ansible/roles/prerequisites/defaults/main.yaml @@ -0,0 +1,7 @@ +--- +# Defaults for prerequisites +packages: + - jq + - awscli + - unzip + - nginx \ No newline at end of file diff --git a/ansible/roles/prerequisites/tasks/main.yaml b/ansible/roles/prerequisites/tasks/main.yaml new file mode 100644 index 0000000..ad8def4 --- /dev/null +++ b/ansible/roles/prerequisites/tasks/main.yaml @@ -0,0 +1,48 @@ +--- +# Task file for prerequisites +- name: Update APT + apt: + upgrade: dist + update_cache: yes + +- name: Install prerequisites + apt: + name: "{{ item }}" + update_cache: yes + loop: "{{ packages }}" + +- name: Start nginx + systemd: + name: nginx + state: started + daemon_reload: yes + enabled: yes + +- name: Create group {{ group }} + group: + name: "{{ group }}" + state: present + +- name: Create user {{ user }} + user: + name: "{{ user }}" + group: "{{ group }}" + state: present + createhome: yes + home: /home/{{ user }} + shell: /bin/bash + +- name: Crete app directories + file: + path: "{{ item }}" + state: directory + owner: "{{ user }}" + group: "{{ group }}" + mode: "{{ mode }}" + with_items: + - "{{ app_home }}" + - "{{ app_data }}" + - "{{ app_config_home }}" + - "{{ app_bin_home }}" + - "{{ app_log_home }}" + - "{{ app_home }}/dest" diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 0000000..427441d --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1,17 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR +not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,16 @@ +# 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 + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..de51f68 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,45 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events*.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..3137cff --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,106 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "workoutrecorder-front": { + "projectType": "application", + "schematics": { + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/workoutrecorder-front", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "workoutrecorder-front:build:production" + }, + "development": { + "browserTarget": "workoutrecorder-front:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "workoutrecorder-front: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.css" + ], + "scripts": [] + } + } + } + } + }, + "defaultProject": "workoutrecorder-front" +} diff --git a/frontend/default b/frontend/default new file mode 100644 index 0000000..532f47b --- /dev/null +++ b/frontend/default @@ -0,0 +1,91 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# https://www.nginx.com/resources/wiki/start/ +# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/ +# https://wiki.debian.org/Nginx/DirectoryStructure +# +# In most cases, administrators will remove this file from sites-enabled/ and +# leave it as reference inside of sites-available where it will continue to be +# updated by the nginx packaging team. +# +# This file will automatically load configuration files provided by other +# applications, such as Drupal or Wordpress. These applications will be made +# available underneath a path with that package name, such as /drupal8. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +server { + listen 80; + listen [::]:80; + + # SSL configuration + # + # listen 443 ssl default_server; + # listen [::]:443 ssl default_server; + # + # Note: You should disable gzip for SSL traffic. + # See: https://bugs.debian.org/773332 + # + # Read up on ssl_ciphers to ensure a secure configuration. + # See: https://bugs.debian.org/765782 + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + # include snippets/snakeoil.conf; + + root /usr/share/nginx/html; + + # Add index.php to the list if you are using PHP + index index.html index.htm ; + + server_name www.workoutrecorder.com workoutrecorder.com; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + } + + # pass PHP scripts to FastCGI server + # + #location ~ \.php$ { + # include snippets/fastcgi-php.conf; + # + # # With php-fpm (or other unix sockets): + # fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + # # With php-cgi (or other tcp sockets): + # fastcgi_pass 127.0.0.1:9000; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} \ No newline at end of file diff --git a/frontend/karma.conf.js b/frontend/karma.conf.js new file mode 100644 index 0000000..b326d84 --- /dev/null +++ b/frontend/karma.conf.js @@ -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) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/workoutrecorder-front'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b559636 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "workoutrecorder-front", + "version": "", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "~12.2.0", + "@angular/common": "~12.2.0", + "@angular/compiler": "~12.2.0", + "@angular/core": "~12.2.0", + "@angular/forms": "~12.2.0", + "@angular/platform-browser": "~12.2.0", + "@angular/platform-browser-dynamic": "~12.2.0", + "@angular/router": "~12.2.0", + "rxjs": "~6.6.0", + "tslib": "^2.3.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~12.2.12", + "@angular/cli": "~12.2.12", + "@angular/compiler-cli": "~12.2.0", + "@types/jasmine": "~3.8.0", + "@types/node": "^12.11.1", + "jasmine-core": "~3.8.0", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "~1.7.0", + "typescript": "~4.3.5" + } +} diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 0000000..b08e047 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1,126 @@ +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
DataDystansCzasSpalone kalorieUwagi
{{workout.date}}{{workout.distance}}{{workout.time}}{{workout.calories}}{{workout.comments}} + + + +
+
+ + + + + + + diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..5dac2da --- /dev/null +++ b/frontend/src/app/app.component.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'workoutrecorder-front'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('workoutrecorder-front'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain('workoutrecorder-front app is running!'); + }); +}); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 0000000..b9a6802 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,87 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { NgForm } from '@angular/forms'; +import { Workout } from './workout'; +import { WorkoutService } from './workout.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent implements OnInit{ + public workouts!: Workout[]; + public updateWorkout!: Workout | null; + + constructor(private workoutService: WorkoutService){} + + ngOnInit() { + this.getWorkouts(); + } + + public getWorkouts(): void { + this.workoutService.getWorkouts().subscribe( + (response: Workout[]) => { + this.workouts = response; + }, + (error: HttpErrorResponse) => { + alert(error.message); + } + ) + } + + public onAddWorkout(addForm: NgForm): void { + document.getElementById("add-workout-form")?.click(); + this.workoutService.addWorkout(addForm.value).subscribe( + (respose: Workout) => { + this.getWorkouts(); + addForm.reset(); + }, + (error: HttpErrorResponse) => { + alert(error.message); + addForm.reset(); + } + ); + } + + public onUpdateWorkout(workout: Workout): void { + document.getElementById("update-workout-form")?.click(); + this.workoutService.updateWorkout(workout).subscribe( + (respose: Workout) => { + this.getWorkouts(); + }, + (error: HttpErrorResponse) => { + alert(error.message); + } + ); + } + + public onDeleteWorkout(workoutId: number): void { + this.workoutService.deleteWorkout(workoutId).subscribe( + (respose: void) => { + this.getWorkouts(); + }, + (error: HttpErrorResponse) => { + alert(error.message); + } + ); + } + + public onOpenModal(workout: Workout | null, mode: string): void{ + const container = document.getElementById('main-container')!; + const button = document.createElement('button'); + button.type = 'button' + button.style.display = 'none'; + button.setAttribute('data-toggle', 'modal'); + if (mode === 'add'){ + button.setAttribute('data-target', '#addWorkoutModal'); + } + else { + this.updateWorkout = workout; + button.setAttribute('data-target', '#updateWorkoutModal'); + } + container.appendChild(button); + button.click(); + } + +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts new file mode 100644 index 0000000..0b75ed1 --- /dev/null +++ b/frontend/src/app/app.module.ts @@ -0,0 +1,20 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + HttpClientModule, + FormsModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/frontend/src/app/workout.service.ts b/frontend/src/app/workout.service.ts new file mode 100644 index 0000000..daca477 --- /dev/null +++ b/frontend/src/app/workout.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import {HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Workout } from './workout'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class WorkoutService { + private apiServerUrl: string | undefined; + + constructor(private http: HttpClient) { + this.apiServerUrl = environment.apiServerUrl; + } + + public getWorkouts(): Observable { + return this.http.get(this.apiServerUrl + '/workout/all'); + } + + public addWorkout(workout: Workout): Observable { + return this.http.post(this.apiServerUrl + '/workout/add', workout); + } + + public updateWorkout(workout: Workout): Observable { + return this.http.put(this.apiServerUrl + '/workout/update', workout); + } + + public deleteWorkout(workoutId: number): Observable { + return this.http.delete(this.apiServerUrl + '/workout/delete/' + workoutId); + } +} diff --git a/frontend/src/app/workout.ts b/frontend/src/app/workout.ts new file mode 100644 index 0000000..54511c3 --- /dev/null +++ b/frontend/src/app/workout.ts @@ -0,0 +1,10 @@ +import { Time } from "@angular/common"; + +export interface Workout { + id: number; + date: Date; + time: Time; + distance: number; + calories: number; + comments: String; +} \ No newline at end of file diff --git a/frontend/src/assets/.gitkeep b/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts new file mode 100644 index 0000000..98be22d --- /dev/null +++ b/frontend/src/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiServerUrl: 'http://:9999' +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 0000000..f56ff47 --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/frontend/src/favicon.ico b/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/frontend/src/favicon.ico differ diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..478cde1 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,17 @@ + + + + + WorkoutrecorderFront + + + + + + + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..c7b673c --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/frontend/src/polyfills.ts b/frontend/src/polyfills.ts new file mode 100644 index 0000000..373f538 --- /dev/null +++ b/frontend/src/polyfills.ts @@ -0,0 +1,65 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..bf43e70 --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,3 @@ +@import 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css'; + +body{margin-top:20px;} \ No newline at end of file diff --git a/frontend/src/test.ts b/frontend/src/test.ts new file mode 100644 index 0000000..b4dd603 --- /dev/null +++ b/frontend/src/test.ts @@ -0,0 +1,27 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { teardown: { destroyAfterEach: true }}, +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..82d91dc --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..6df8283 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,30 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2017", + "module": "es2020", + "lib": [ + "es2018", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json new file mode 100644 index 0000000..092345b --- /dev/null +++ b/frontend/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +}