diff --git a/.github/workflows/time-tracker-ui-cd-prod.yml b/.github/workflows/time-tracker-ui-cd-prod.yml new file mode 100644 index 000000000..19bbda245 --- /dev/null +++ b/.github/workflows/time-tracker-ui-cd-prod.yml @@ -0,0 +1,54 @@ +name: time-tracker-ui-cd-prod + +on: + release: + types: + - published + +jobs: + cd: + runs-on: ubuntu-latest + env: + TF_WORKSPACE: prod + WORKING_DIR: infrastructure/ + ARM_CLIENT_ID: ${{secrets.TF_ARM_CLIENT_ID}} + ARM_CLIENT_SECRET: ${{secrets.TF_ARM_CLIENT_SECRET}} + ARM_SUBSCRIPTION_ID: ${{secrets.TF_ARM_SUBSCRIPTION_ID}} + ARM_TENANT_ID: ${{secrets.TF_ARM_TENANT_ID}} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get the release_version + run: | + echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + echo $RELEASE_VERSION + + - name: Login to azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Build the docker image + run: make build + + - name: Publish docker image to prod azure container registry + run: | + make login publish acr=timetrackerserviceprodregistry image_tag=$RELEASE_VERSION + + - name: Setup terraform + uses: hashicorp/setup-terraform@v1 + + - name: Authenticate with the TF modules repository + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.INFRA_TERRAFORM_MODULES_SSH_PRIV_KEY }} + + - name: Terraform Init + working-directory: ${{ env.WORKING_DIR }} + run: terraform init + + - name: Terraform Apply + working-directory: ${{ env.WORKING_DIR }} + run: terraform apply -lock=false -var-file="${{ env.TF_WORKSPACE }}.tfvars" -var "image_tag=$RELEASE_VERSION" -auto-approve diff --git a/.github/workflows/time-tracker-ui-cd-stage.yml b/.github/workflows/time-tracker-ui-cd-stage.yml new file mode 100644 index 000000000..f5462c2a9 --- /dev/null +++ b/.github/workflows/time-tracker-ui-cd-stage.yml @@ -0,0 +1,53 @@ +name: time-tracker-ui-cd-stage + +on: + push: + tags: + - 'v*.*.*' + +jobs: + cd: + runs-on: ubuntu-latest + env: + TF_WORKSPACE: stage + WORKING_DIR: infrastructure/ + ARM_CLIENT_ID: ${{secrets.TF_ARM_CLIENT_ID}} + ARM_CLIENT_SECRET: ${{secrets.TF_ARM_CLIENT_SECRET}} + ARM_SUBSCRIPTION_ID: ${{secrets.TF_ARM_SUBSCRIPTION_ID}} + ARM_TENANT_ID: ${{secrets.TF_ARM_TENANT_ID}} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get the release_version + run: | + echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + echo $RELEASE_VERSION + + - name: Login to azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Build the docker image + run: make build + + - name: Publish docker image to stage azure container registry + run: | + make login publish acr=timetrackerservicestageregistry image_tag=$RELEASE_VERSION + + - name: Setup terraform + uses: hashicorp/setup-terraform@v1 + + - name: Authenticate with the TF modules repository + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.INFRA_TERRAFORM_MODULES_SSH_PRIV_KEY }} + + - name: Terraform Init + working-directory: ${{ env.WORKING_DIR }} + run: terraform init + + - name: Terraform Apply + working-directory: ${{ env.WORKING_DIR }} + run: terraform apply -lock=false -var-file="${{ env.TF_WORKSPACE }}.tfvars" -var "image_tag=$RELEASE_VERSION" -auto-approve diff --git a/.github/workflows/time-tracker-ui-ci.yml b/.github/workflows/time-tracker-ui-ci.yml new file mode 100644 index 000000000..4085c1dcf --- /dev/null +++ b/.github/workflows/time-tracker-ui-ci.yml @@ -0,0 +1,137 @@ +name: time-tracker-ui-ci + +on: + push: + branches: + - "**" + + pull_request: + branches: + - "**" + +jobs: + ci: + runs-on: ubuntu-latest + env: + WORKING_DIR: infrastructure/ + DB_CONNECTION: ${{ secrets.DB_CONNECTION }} + ARM_CLIENT_ID: ${{secrets.TF_ARM_CLIENT_ID}} + ARM_CLIENT_SECRET: ${{secrets.TF_ARM_CLIENT_SECRET}} + ARM_SUBSCRIPTION_ID: ${{secrets.TF_ARM_SUBSCRIPTION_ID}} + ARM_TENANT_ID: ${{secrets.TF_ARM_TENANT_ID}} + strategy: + max-parallel: 5 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Authenticate with the TF modules repository + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.INFRA_TERRAFORM_MODULES_SSH_PRIV_KEY }} + + - name: build docker + run: make build + + - name: Inject Secrets + env: + SCOPES: ${{ secrets.SCOPES }} + CLIENT_ID: ${{ secrets.CLIENT_ID }} + AUTHORITY: ${{ secrets.AUTHORITY }} + STACK_EXCHANGE_ID: ${{ secrets.STACK_EXCHANGE_ID }} + STACK_EXCHANGE_ACCESS_TOKEN: ${{ secrets.STACK_EXCHANGE_ACCESS_TOKEN }} + AZURE_APP_CONFIGURATION_CONNECTION_STRING: ${{ secrets.AZURE_APP_CONFIGURATION_CONNECTION_STRING }} + run: | + chmod +x ./scripts/populate-keys.sh + sh ./scripts/populate-keys.sh + + - name: Running tests + run: | + chmod -R 777 ./$home + make test + - name: Generate coverage report + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: bash <(curl -s https://codecov.io/bash) + + - name: Setup terraform + uses: hashicorp/setup-terraform@v1 + + - name: 'Terraform Init' + id: init + working-directory: ./${{ env.WORKING_DIR }} + run: terraform init + + - name: 'Terraform validate' + id: validate + working-directory: ./${{ env.WORKING_DIR }} + run: terraform validate + + - name: Terraform Plan Stage + id: plan-stage + run: terraform plan -var-file=${{ env.TF_WORKSPACE }}.tfvars -var image_tag=latest -no-color + continue-on-error: true + working-directory: ./${{ env.WORKING_DIR }} + env: + TF_WORKSPACE: stage + + - name: Terraform Plan Prod + id: plan-prod + run: terraform plan -var-file=${{ env.TF_WORKSPACE }}.tfvars -var image_tag=latest -no-color + continue-on-error: true + working-directory: ./${{ env.WORKING_DIR }} + env: + TF_WORKSPACE: prod + + - name: Update Pull Request with Stage Plan + uses: actions/github-script@0.9.0 + if: github.event_name == 'pull_request' + env: + PLAN: "terraform\n${{ steps.plan-stage.outputs.stdout }}" + TF_WORKSPACE: stage + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const output = `#### [${{ env.WORKING_DIR }}][${{ env.TF_WORKSPACE }}] Terraform Plan 📖 \`${{ steps.plan-stage.outcome }}\` +
Show Plan + \`\`\`\n + ${process.env.PLAN} + \`\`\` +
+ *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`; + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - name: Update Pull Request with Prod Plan + uses: actions/github-script@0.9.0 + if: github.event_name == 'pull_request' + env: + PLAN: "terraform\n${{ steps.plan-prod.outputs.stdout }}" + TF_WORKSPACE: prod + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const output = `#### [${{ env.WORKING_DIR }}][${{ env.TF_WORKSPACE }}] Terraform Plan 📖 \`${{ steps.plan-prod.outcome }}\` +
Show Plan + \`\`\`\n + ${process.env.PLAN} + \`\`\` +
+ *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`; + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + - name: Terraform Plan Stage Status + if: steps.plan-stage.outcome == 'failure' + run: exit 1 + + - name: Terraform Plan Prod Status + if: steps.plan-prod.outcome == 'failure' + run: exit 1 diff --git a/.gitignore b/.gitignore index 6cf093c1c..3ac4d2e46 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ Thumbs.db #ENV VARIABLES .env + +# Terraform files +**/.terraform** diff --git a/Dockerfile b/Dockerfile index 24f6ebb5b..e1e08bc62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,33 @@ FROM node:14 AS development ENV USERNAME timetracker ENV HOME /home/${USERNAME} +ENV CHROME_BIN /opt/google/chrome/google-chrome +#Essential tools and xvfb +RUN apt-get update && apt-get install -y \ + software-properties-common \ + unzip \ + curl \ + wget \ + xvfb + +#Chrome browser to run the tests +ARG CHROME_VERSION=65.0.3325.181 +RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add \ + && wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ + && dpkg -i google-chrome-stable_current_amd64.deb || true +RUN apt-get install -y -f \ + && rm -rf /var/lib/apt/lists/* + +#Disable the SUID sandbox so that chrome can launch without being in a privileged container +RUN dpkg-divert --add --rename --divert /opt/google/chrome/google-chrome.real /opt/google/chrome/google-chrome \ + && echo "#! /bin/bash\nexec /opt/google/chrome/google-chrome.real --no-sandbox --disable-setuid-sandbox \"\$@\"" > /opt/google/chrome/google-chrome \ + && chmod 755 /opt/google/chrome/google-chrome + +#Chrome Driver +ARG CHROME_DRIVER_VERSION=2.37 +RUN mkdir -p /opt/selenium \ + && curl http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -o /opt/selenium/chromedriver_linux64.zip \ + && cd /opt/selenium; unzip /opt/selenium/chromedriver_linux64.zip; rm -rf chromedriver_linux64.zip; ln -fs /opt/selenium/chromedriver /usr/local/bin/chromedriver; RUN useradd -ms /bin/bash ${USERNAME} @@ -9,6 +36,8 @@ WORKDIR ${HOME}/time-tracker-ui COPY . . RUN rm -f .env RUN chown ${USERNAME}:${USERNAME} -R ${HOME}/time-tracker-ui +RUN chmod -R 777 ${HOME}/time-tracker-ui + USER ${USERNAME} RUN npm cache clean --force && npm install @@ -37,6 +66,12 @@ RUN chown -R ${USERNAME}:${USERNAME} /var/cache/nginx && \ chown -R ${USERNAME}:${USERNAME} /etc/nginx/conf.d RUN touch /var/run/nginx.pid && chown -R ${USERNAME}:${USERNAME} /var/run/nginx.pid -USER ${USERNAME} +# FIXME: Actually if we can deploy to azure in port 80 we need a root user +# Maybe we can refactor this dockerfile to use root user directly this is not a good approach y +# security terms. It's a good practice to have rootless in containers so for this +# we can to refactor this dockerfile and the terraform module to deploy in other ports because +# Ports below 1024 needs root permisions. + +# USER ${USERNAME} -EXPOSE 4200 \ No newline at end of file +EXPOSE 80 diff --git a/Makefile b/Makefile index c0eb69456..f465cfed1 100644 --- a/Makefile +++ b/Makefile @@ -39,9 +39,14 @@ remove: ## Delete container timetracker_ui. docker-compose down --volumes --remove-orphans --rmi local .PHONY: test -test: ## Run all tests on docker container timetracker_ui. - docker-compose --env-file ./.env up -d - docker exec -it timetracker_ui bash -c "npm run test" +test: ## Run all tests on docker container timetracker_ui at the CLI. + docker-compose -f docker-compose.yml --env-file ./.env up -d + docker exec timetracker_ui bash -c "npm run ci-test" + +.PHONY: testdev +testdev: ## Run all tests on docker container timetracker_ui at the Dev + docker-compose -f docker-compose.yml -f docker-compose.dev.yml --env-file ./.env up -d + docker exec timetracker_ui bash -c "npm run ci-test" .PHONY: publish publish: require-acr-arg require-image_tag-arg ## Upload a docker image to the stage azure container registry acr= image_tag= @@ -66,13 +71,13 @@ remove_prod: ## Delete container timetracker_ui_prod. docker rm timetracker_ui_prod .PHONY: publish_prod -publish_prod: require-acr-arg require-image_tag-arg ## Upload a docker image to the prod azure container registry acr= image_tag= +publish_prod: ## Upload a docker image to the prod azure container registry acr= image_tag= docker tag timetracker_ui_prod:latest $(acr).azurecr.io/timetracker_ui:$(image_tag) docker push $(acr).azurecr.io/timetracker_ui:$(image_tag) .PHONY: login login: ## Login in respository of docker images. - az acr login --name $(container_registry) + az acr login --name $(acr) .PHONY: release release: require-VERSION-arg require-COMMENT-arg ## Creates an pushes a new tag. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..18535960c --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,13 @@ +version: '3.9' +services: + time-tracker-ui: + user: root + volumes: + - ./src:/home/timetracker/time-tracker-ui/src/ + - ./scripts:/home/timetracker/time-tracker-ui/scripts/ + - ./e2e:/home/timetracker/time-tracker-ui/e2e/ + - ./coverage:/home/timetracker/time-tracker-ui/coverage + - ./angular.json:/home/timetracker/time-tracker-ui/angular.json + - ./karma.conf.js:/home/timetracker/time-tracker-ui/karma.conf.js + - ./package.json:/home/timetracker/time-tracker-ui/package.json + - ./webpack.config.js:/home/timetracker/time-tracker-ui/webpack.config.js diff --git a/docker-compose.yml b/docker-compose.yml index d7516c1a1..61bd1ca44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - 4200:4200 - 9876:9876 environment: + CHROME_BIN: /opt/google/chrome/google-chrome AUTHORITY: ${AUTHORITY} CLIENT_ID: ${CLIENT_ID} SCOPES: ${SCOPES} @@ -20,12 +21,4 @@ services: AUTHORITY_JSON: ${AUTHORITY_JSON} CLIENT_ID_JSON: ${CLIENT_ID_JSON} SCOPES_JSON: ${SCOPES_JSON} - volumes: - - ./src:/home/timetracker/time-tracker-ui/src/ - - ./scripts:/home/timetracker/time-tracker-ui/scripts/ - - ./e2e:/home/timetracker/time-tracker-ui/e2e/ - - ./coverage:/home/timetracker/time-tracker-ui/coverage - - ./angular.json:/home/timetracker/time-tracker-ui/angular.json - - ./karma.conf.js:/home/timetracker/time-tracker-ui/karma.conf.js - - ./package.json:/home/timetracker/time-tracker-ui/package.json - - ./webpack.config.js:/home/timetracker/time-tracker-ui/webpack.config.js + diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js index 7c798cfff..17f6b4754 100644 --- a/e2e/protractor.conf.js +++ b/e2e/protractor.conf.js @@ -13,7 +13,14 @@ exports.config = { './src/**/*.e2e-spec.ts' ], capabilities: { - browserName: 'chrome' + browserName: 'chrome', + 'chromeOptions': { + 'args': [ + '--no-sandbox', + '--headless', + '--window-size=1024,768' + ] + } }, directConnect: true, baseUrl: 'http://localhost:4200/', @@ -29,4 +36,4 @@ exports.config = { }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } -}; \ No newline at end of file +}; diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 000000000..d7b433273 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,59 @@ +terraform { + required_version = "~> 1.1.2" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 2.90" + } + } + + backend "azurerm" { + resource_group_name = "ioet-infra-tf-state" + storage_account_name = "timetrackertfstate" + container_name = "time-tracker-tf-state" + key = "time-tracker-ui.tfstate" + } + +} + +provider "azurerm" { + features {} + skip_provider_registration = true +} + +data "terraform_remote_state" "service" { + backend = "azurerm" + workspace = terraform.workspace + config = { + resource_group_name = "ioet-infra-tf-state" + storage_account_name = "timetrackertfstate" + container_name = "time-tracker-tf-state" + key = "this.tfstate" + } +} + +locals { + common_name = "time-tracker-ui" + environment = terraform.workspace + service_name = "${local.common_name}-${local.environment}" + create_app_service_plan = true + service_plan_kind = "Linux" + image_name = "timetracker_ui" +} + +module "ui" { + source = "git@github.com:ioet/infra-terraform-modules.git//azure-app-service?ref=tags/v0.0.5" + app_service_name = local.service_name + create_app_service_plan = local.create_app_service_plan + docker_image_name = "${local.image_name}:${var.image_tag}" + docker_image_namespace = data.terraform_remote_state.service.outputs.container_registry_login_server + docker_registry_password = data.terraform_remote_state.service.outputs.container_registry_admin_password + docker_registry_url = data.terraform_remote_state.service.outputs.container_registry_login_server + docker_registry_username = data.terraform_remote_state.service.outputs.container_registry_admin_username + location = data.terraform_remote_state.service.outputs.container_registry_location + resource_group_name = data.terraform_remote_state.service.outputs.resource_group_name + service_plan_kind = local.service_plan_kind + service_plan_name = local.service_name + service_plan_size = var.service_plan_size + service_plan_tier = var.service_plan_tier +} diff --git a/infrastructure/prod.tfvars b/infrastructure/prod.tfvars new file mode 100644 index 000000000..d93ddc3de --- /dev/null +++ b/infrastructure/prod.tfvars @@ -0,0 +1,2 @@ +service_plan_size = "S1" +service_plan_tier = "Standard" diff --git a/infrastructure/stage.tfvars b/infrastructure/stage.tfvars new file mode 100644 index 000000000..d93ddc3de --- /dev/null +++ b/infrastructure/stage.tfvars @@ -0,0 +1,2 @@ +service_plan_size = "S1" +service_plan_tier = "Standard" diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf new file mode 100644 index 000000000..6a035a126 --- /dev/null +++ b/infrastructure/variables.tf @@ -0,0 +1,17 @@ +variable "image_tag" { + type = string + description = "Specifies the docker image tag that is stored in a private container registry like ACR (Azure Container Registry)." + sensitive = true +} + +variable "service_plan_size" { + default = "S1" + type = string + description = "Specifies the size of the service plan. This variable format is: Tier (letter) + Size (number). Size could be: 1 = Small (1 Core 1.75GB RAM), 2 = Medium (2 Core 3.5 GB RAM), 3 = Large (4 Core 7GB RAM)" +} + +variable "service_plan_tier" { + default = "Standard" + type = string + description = "Specifies the tier of the service plan. Tier is the pricing plan of the service plan resource." +} diff --git a/karma.conf.js b/karma.conf.js index c51657f20..afb638d5f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,6 +1,7 @@ // 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: '', @@ -21,6 +22,12 @@ module.exports = function (config) { seed: '90967', }, // leave Jasmine Spec Runner output visible in browser }, + // Karma Typescript compiler options + karmaTypescriptConfig: { + coverageOptions : { + instrumentation: false + } + }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/time-tracker'), reports: ['html', 'lcovonly', 'text-summary'], diff --git a/nginx.conf b/nginx.conf index 824374fdc..bc375802f 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,5 +1,5 @@ server { - listen 4200; + listen 80; root /usr/share/nginx/html; index index.html; @@ -9,4 +9,4 @@ server { location / { try_files $uri /index.html; } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 1eaf616db..3d2ad4cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.69.1", + "version": "1.72.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -20539,6 +20539,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "requires": { "safe-buffer": "^5.1.0" } @@ -24795,14 +24796,6 @@ "ajv-keywords": "^3.1.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "requires": { - "randombytes": "^2.1.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 05ea62f7f..396d1b3fe 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "time-tracker", - "version": "1.69.1", + "version": "1.72.1", "scripts": { "config": "ts-node ./scripts/setenv.ts", "preinstall": "npx npm-force-resolutions", "ng": "ng", "start": "ng serve", "build": "ng build --prod", - "test": "ng test", + "test": "ng test --browsers ChromeHeadless", "test-headless": "ng test --browsers ChromeHeadless", "ci-test": "ng test --no-watch --no-progress --browsers ChromeHeadless", "lint": "ng lint", diff --git a/scripts/populate-keys.sh b/scripts/populate-keys.sh index f395689af..6ab4a5cd4 100644 --- a/scripts/populate-keys.sh +++ b/scripts/populate-keys.sh @@ -1,10 +1,10 @@ #!/bin/bash -> src/environments/keys.ts -echo 'export const AUTHORITY = "'$AUTHORITY'";' >> src/environments/keys.ts -echo 'export const CLIENT_ID = "'$CLIENT_ID'";' >> src/environments/keys.ts -echo 'export const SCOPES = ["'$SCOPES'"];' >> src/environments/keys.ts -echo 'export const STACK_EXCHANGE_ID = "'$STACK_EXCHANGE_ID'";' >> src/environments/keys.ts -echo 'export const STACK_EXCHANGE_ACCESS_TOKEN = "'$STACK_EXCHANGE_ACCESS_TOKEN'";' >> src/environments/keys.ts -echo 'export const AZURE_APP_CONFIGURATION_CONNECTION_STRING = "'$AZURE_APP_CONFIGURATION_CONNECTION_STRING'";' >> src/environments/keys.ts -cat src/environments/keys.ts +> .env +echo 'AUTHORITY = '$AUTHORITY'' >> .env +echo 'CLIENT_ID = '$CLIENT_ID'' >> .env +echo 'SCOPES = '$SCOPES'' >> .env +echo 'STACK_EXCHANGE_ID = '$STACK_EXCHANGE_ID'' >> .env +echo 'STACK_EXCHANGE_ACCESS_TOKEN = '$STACK_EXCHANGE_ACCESS_TOKEN'' >> .env +echo 'AZURE_APP_CONFIGURATION_CONNECTION_STRING = '$AZURE_APP_CONFIGURATION_CONNECTION_STRING'' >> .env +cat .env diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 98a6d5ea9..e226c5626 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -89,6 +89,7 @@ import { NgSelectModule } from '@ng-select/ng-select'; import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mode.component'; import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login'; import { GoogleLoginProvider } from 'angularx-social-login'; +import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component'; const maskConfig: Partial = { validation: false, @@ -128,6 +129,7 @@ const maskConfig: Partial = { EntryFieldsComponent, SubstractDatePipe, TechnologiesComponent, + SearchUserComponent, TimeEntriesSummaryComponent, TimeDetailsPipe, InputLabelComponent, diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html index fee2f96b6..8a9f6e06b 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html @@ -1,4 +1,5 @@
+ - +
{{ entry.description }}{{ entry.technologies }} + +
+ {{ technology }} +
+
+
diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss index 34034aa2b..0708ffecd 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss @@ -83,3 +83,15 @@ table.dataTable thead .sorting_asc, table.dataTable thead .sorting_desc { background-image: none; } + +.badge { + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 0.5em; + color: #FFFFFF; + font-weight: bold; + text-transform: capitalize; + font-style: italic; + cursor: pointer; +} diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts index 9e6dcb415..d7003a2ac 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts @@ -1,13 +1,16 @@ import { formatDate } from '@angular/common'; -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; -import { select, Store } from '@ngrx/store'; +import { AfterViewInit, Component, EventEmitter, OnDestroy, Output, OnInit, ViewChild } from '@angular/core'; +import { select, Store, ActionsSubject } from '@ngrx/store'; import { DataTableDirective } from 'angular-datatables'; import * as moment from 'moment'; import { Observable, Subject, Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { Entry } from 'src/app/modules/shared/models'; import { DataSource } from 'src/app/modules/shared/models/data-source.model'; import { EntryState } from '../../../time-clock/store/entry.reducer'; import { getReportDataSource } from '../../../time-clock/store/entry.selectors'; +import { User } from 'src/app/modules/users/models/users'; +import { LoadUsers, UserActionTypes } from 'src/app/modules/users/store/user.actions'; @Component({ selector: 'app-time-entries-table', @@ -15,8 +18,11 @@ import { getReportDataSource } from '../../../time-clock/store/entry.selectors'; styleUrls: ['./time-entries-table.component.scss'], }) export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit { + @Output() selectedUserId = new EventEmitter(); + selectOptionValues = [15, 30, 50, 100, -1]; selectOptionNames = [15, 30, 50, 100, 'All']; + users: User[] = []; dtOptions: any = { scrollY: '590px', dom: '<"d-flex justify-content-between"B<"d-flex"<"mr-5"l>f>>rtip', @@ -24,6 +30,7 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn lengthMenu: [this.selectOptionValues, this.selectOptionNames], buttons: [ { + text: 'Column Visibility' + ' â–¼', extend: 'colvis', columns: ':not(.hidden-col)' }, @@ -60,14 +67,24 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn reportDataSource$: Observable>; rerenderTableSubscription: Subscription; - constructor(private store: Store) { + constructor(private store: Store, private actionsSubject$: ActionsSubject, private storeUser: Store ) { this.reportDataSource$ = this.store.pipe(select(getReportDataSource)); } + uploadUsers(): void { + this.storeUser.dispatch(new LoadUsers()); + this.actionsSubject$ + .pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS)) + .subscribe((action) => { + this.users = action.payload; + }); + } + ngOnInit(): void { this.rerenderTableSubscription = this.reportDataSource$.subscribe((ds) => { this.rerenderDataTable(); }); + this.uploadUsers(); } ngAfterViewInit(): void { @@ -82,11 +99,11 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn private rerenderDataTable(): void { if (this.dtElement && this.dtElement.dtInstance) { this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => { - dtInstance.destroy(); - this.dtTrigger.next(); + dtInstance.destroy(); + this.dtTrigger.next(); }); } else { - this.dtTrigger.next(); + this.dtTrigger.next(); } } @@ -99,10 +116,15 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn return regex.test(uri); } - bodyExportOptions(data, row, column, node){ + bodyExportOptions(data, row, column, node) { const dataFormated = data.toString().replace(/<((.|\n){0,200}?)>/gi, ''); const durationColumnIndex = 3; return column === durationColumnIndex ? moment.duration(dataFormated).asHours().toFixed(2) : dataFormated; } + + user(userId: string){ + this.selectedUserId.emit(userId); + } + } diff --git a/src/app/modules/reports/components/time-range-form/time-range-form.component.ts b/src/app/modules/reports/components/time-range-form/time-range-form.component.ts index 0c9c01e28..eea7f3a65 100644 --- a/src/app/modules/reports/components/time-range-form/time-range-form.component.ts +++ b/src/app/modules/reports/components/time-range-form/time-range-form.component.ts @@ -1,6 +1,6 @@ import { ToastrService } from 'ngx-toastr'; import { formatDate } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; +import { OnChanges, SimpleChanges, Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { DATE_FORMAT } from 'src/environments/environment'; import * as entryActions from '../../../time-clock/store/entry.actions'; @@ -12,7 +12,10 @@ import * as moment from 'moment'; selector: 'app-time-range-form', templateUrl: './time-range-form.component.html', }) -export class TimeRangeFormComponent implements OnInit { +export class TimeRangeFormComponent implements OnInit, OnChanges { + + @Input() userId: string; + public reportForm: FormGroup; private startDate = new FormControl(''); private endDate = new FormControl(''); @@ -27,6 +30,12 @@ export class TimeRangeFormComponent implements OnInit { this.setInitialDataOnScreen(); } + ngOnChanges(changes: SimpleChanges){ + if (!changes.userId.firstChange){ + this.onSubmit(); + } + } + setInitialDataOnScreen() { this.reportForm.setValue({ startDate: formatDate(moment().startOf('week').format('l'), DATE_FORMAT, 'en'), @@ -43,7 +52,7 @@ export class TimeRangeFormComponent implements OnInit { this.store.dispatch(new entryActions.LoadEntriesByTimeRange({ start_date: moment(this.startDate.value).startOf('day'), end_date: moment(this.endDate.value).endOf('day'), - })); + }, this.userId)); } } } diff --git a/src/app/modules/reports/pages/reports.component.html b/src/app/modules/reports/pages/reports.component.html index 661823546..1b8fde066 100644 --- a/src/app/modules/reports/pages/reports.component.html +++ b/src/app/modules/reports/pages/reports.component.html @@ -1,3 +1,2 @@ - - - + + \ No newline at end of file diff --git a/src/app/modules/reports/pages/reports.component.ts b/src/app/modules/reports/pages/reports.component.ts index dbdc3e14c..6b02adef8 100644 --- a/src/app/modules/reports/pages/reports.component.ts +++ b/src/app/modules/reports/pages/reports.component.ts @@ -6,4 +6,10 @@ import { Component } from '@angular/core'; styleUrls: ['./reports.component.scss'] }) export class ReportsComponent { + + userId: string; + + user(userId: string){ + this.userId = userId; + } } diff --git a/src/app/modules/shared/components/search-user/search-user.component.html b/src/app/modules/shared/components/search-user/search-user.component.html new file mode 100644 index 000000000..05a214947 --- /dev/null +++ b/src/app/modules/shared/components/search-user/search-user.component.html @@ -0,0 +1,8 @@ +
+ + + + 👤{{user.name}}📨{{ user.email}} + + +
\ No newline at end of file diff --git a/src/app/modules/shared/components/search-user/search-user.component.scss b/src/app/modules/shared/components/search-user/search-user.component.scss new file mode 100644 index 000000000..34890f2e0 --- /dev/null +++ b/src/app/modules/shared/components/search-user/search-user.component.scss @@ -0,0 +1,9 @@ +label { + width: 225px; +} +.selectUser { + display: inline-block; + width: 350px; + padding: 0 12px 20px 12px; +} + diff --git a/src/app/modules/shared/components/search-user/search-user.component.ts b/src/app/modules/shared/components/search-user/search-user.component.ts new file mode 100644 index 000000000..4ed732f3c --- /dev/null +++ b/src/app/modules/shared/components/search-user/search-user.component.ts @@ -0,0 +1,22 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-search-user', + templateUrl: './search-user.component.html', + styleUrls: ['./search-user.component.scss'], +}) + +export class SearchUserComponent { + + readonly ALLOW_SELECT_MULTIPLE = true; + selectedUser: string; + + @Input() users: string[] = []; + + @Output() selectedUserId = new EventEmitter(); + + updateUser() { + this.selectedUserId.emit(this.selectedUser || '*'); + } +} + diff --git a/src/app/modules/shared/components/sidebar/sidebar.component.html b/src/app/modules/shared/components/sidebar/sidebar.component.html index 4afd966c6..a7ebe74b9 100644 --- a/src/app/modules/shared/components/sidebar/sidebar.component.html +++ b/src/app/modules/shared/components/sidebar/sidebar.component.html @@ -2,8 +2,7 @@