diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..557f156b4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.github +dist +coverage +Makefile +.gitignore +*keys.ts +*.keys.json diff --git a/.gitignore b/.gitignore index 93e5cb260..6cf093c1c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ Thumbs.db # stryker temp files .stryker-tmp + +#ENV VARIABLES +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..24f6ebb5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM node:14 AS development + +ENV USERNAME timetracker +ENV HOME /home/${USERNAME} + +RUN useradd -ms /bin/bash ${USERNAME} + +WORKDIR ${HOME}/time-tracker-ui +COPY . . +RUN rm -f .env +RUN chown ${USERNAME}:${USERNAME} -R ${HOME}/time-tracker-ui + +USER ${USERNAME} +RUN npm cache clean --force && npm install +EXPOSE 4200 +EXPOSE 9876 +CMD npm run config && ${HOME}/time-tracker-ui/node_modules/.bin/ng serve --host 0.0.0.0 --disableHostCheck + + + +FROM development as build +COPY .env . +RUN npm run config && npm run build + + + +FROM nginx:1.21 AS production + +ENV USERNAME app +RUN useradd -ms /bin/bash ${USERNAME} + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /home/timetracker/time-tracker-ui/dist/time-tracker /usr/share/nginx/html + +RUN chown -R ${USERNAME}:${USERNAME} /var/cache/nginx && \ + chown -R ${USERNAME}:${USERNAME} /var/log/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} + +EXPOSE 4200 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a436cf461 --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +override SHELL := /bin/bash + +.PHONY: help +help: ## Show this help message. + @echo 'Usage:' + @echo ' make [target] ...' + @echo + @echo 'Targets:' + @grep --no-filename -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: build +build: ## Create docker image with dependencies needed for development. + docker-compose build + +.PHONY: cleanup +cleanup: ## Delete image timetracker_ui + docker rmi timetracker_ui + +.PHONY: run +run: ## Execute timetracker_ui docker containe. + docker-compose --env-file ./.env up -d + +.PHONY: logs +logs: ## Show logs of timetracker_ui. + docker logs -f timetracker_ui + +.PHONY: stop +stop: ## Stop container timetracker_ui. + docker-compose stop + +.PHONY: restart +restart: ## Restart container timetracker_ui. + docker-compose stop + docker-compose up -d + +.PHONY: remove +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" + +.PHONY: publish +publish: ## Publish the container image timetracker_ui. + docker tag timetracker_ui:latest $(registry_url)/timetracker_ui:latest + docker push $(registry_url)/timetracker_ui:latest + +.PHONY: build_prod +build_prod: ## Create docker image with dependencies needed for production. + docker build --target production -t timetracker_ui_prod -f Dockerfile . + +.PHONY: run_prod +run_prod: ## Execute timetracker_ui_prod docker container. + docker run -d -p 4200:4200 --name timetracker_ui_prod timetracker_ui_prod + +.PHONY: stop_prod +stop_prod: ## Stop container timetracker_ui_prod. + docker stop timetracker_ui_prod + +.PHONY: remove_prod +remove_prod: ## Delete container timetracker_ui_prod. + docker stop timetracker_ui_prod + docker rm timetracker_ui_prod + +.PHONY: publish_prod +publish_prod: ## Publish the container image timetracker_ui_prod. + docker tag timetracker_ui_prod:latest $(registry_url)/timetracker_ui_prod:latest + docker push $(registry_url)/timetracker_ui_prod:latest + +.PHONY: login +login: ## Login in respository of docker images. + az acr login --name $(container_registry) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..d7516c1a1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3.9' +services: + time-tracker-ui: + container_name: timetracker_ui + image: timetracker_ui + build: + target: development + context: . + dockerfile: ./Dockerfile + ports: + - 4200:4200 + - 9876:9876 + environment: + AUTHORITY: ${AUTHORITY} + CLIENT_ID: ${CLIENT_ID} + SCOPES: ${SCOPES} + STACK_EXCHANGE_ID: ${STACK_EXCHANGE_ID} + STACK_EXCHANGE_ACCESS_TOKEN: ${STACK_EXCHANGE_ACCESS_TOKEN} + AZURE_APP_CONFIGURATION_CONNECTION_STRING: ${AZURE_APP_CONFIGURATION_CONNECTION_STRING} + 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/nginx.conf b/nginx.conf new file mode 100644 index 000000000..824374fdc --- /dev/null +++ b/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 4200; + + root /usr/share/nginx/html; + index index.html; + + server_name _; + + location / { + try_files $uri /index.html; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7963a9727..8543ef7ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.60.6", + "version": "1.65.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6472,15 +6472,6 @@ "p-try": "^2.0.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==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -7877,6 +7868,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-14.2.0.tgz", + "integrity": "sha512-05POuPJyPpO6jqzTNweQFfAyMSD4qa4lvsMOWyTRTdpHKy6nnnN+IYWaXF+lHivhBH/ufDKlR4IWCAN3oPnHuw==" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -21621,6 +21617,15 @@ } } }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -23454,9 +23459,9 @@ } }, "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==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -23638,9 +23643,9 @@ "dev": true }, "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz", + "integrity": "sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg==", "dev": true }, "ts-node": { @@ -24690,9 +24695,9 @@ }, "dependencies": { "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==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" diff --git a/package.json b/package.json index 1601ff24b..8cd1f98f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "time-tracker", - "version": "1.60.6", + "version": "1.65.0", "scripts": { + "config": "ts-node ./scripts/setenv.ts", "preinstall": "npx npm-force-resolutions", "ng": "ng", "start": "ng serve", @@ -44,6 +45,7 @@ "datatables.net-responsive": "2.2.6", "datatables.net-responsive-dt": "2.2.6", "date-fns": "2.22.1", + "dotenv": "^14.2.0", "jquery": "3.5.1", "jszip": "3.7.0", "minimist": "1.2.5", diff --git a/scripts/setenv.ts b/scripts/setenv.ts new file mode 100644 index 000000000..0c07a1b30 --- /dev/null +++ b/scripts/setenv.ts @@ -0,0 +1,34 @@ +const { writeFile } = require('fs'); +require('dotenv').config(); + +const pathJs = `./src/environments/keys.ts` +const contentKeys = +`export const AUTHORITY = '${process.env.AUTHORITY}'; +export const CLIENT_ID = '${process.env.CLIENT_ID}'; +export const SCOPES = ['${process.env.SCOPES}']; +export const STACK_EXCHANGE_ID = '${process.env.STACK_EXCHANGE_ID}'; +export const STACK_EXCHANGE_ACCESS_TOKEN = '${process.env.STACK_EXCHANGE_ACCESS_TOKEN}'; +export const AZURE_APP_CONFIGURATION_CONNECTION_STRING = '${process.env.AZURE_APP_CONFIGURATION_CONNECTION_STRING}'; +`; + +writeFile(pathJs, contentKeys, function (err) { + if (err) { + console.log(err); + } + console.log(`Wrote variables to ${pathJs}`); +}); + +const pathJson = `./src/environments/.keys.json` +const contentKeysJson = +`{ + "authority": "${process.env.AUTHORITY_JSON}", + "client_id": "${process.env.CLIENT_ID_JSON}", + "scopes": ["${process.env.SCOPES_JSON}"] +}`; + +writeFile(pathJson, contentKeysJson, function (err) { + if (err) { + console.log(err); + } + console.log(`Wrote variables to ${pathJson}`); +}); 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 5658a972e..9e6dcb415 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 @@ -50,7 +50,8 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn text: 'CSV', filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` }, - ] + ], + columnDefs: [{ type: 'date', targets: 2 }] }; dtTrigger: Subject = new Subject(); @ViewChild(DataTableDirective, { static: false }) diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.html b/src/app/modules/shared/components/details-fields/details-fields.component.html index 3b737aab9..f89af7b41 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.html +++ b/src/app/modules/shared/components/details-fields/details-fields.component.html @@ -72,7 +72,7 @@
- +
- -
- - + +
+ + {{ this.getTimeDifference() }} +
@@ -143,15 +128,6 @@
-
- -
- - {{ this.getTimeDifference() }} - -
-
- { expect(component.projectSelected.emit).toHaveBeenCalledWith(data); }); - it('on selected start_date should change end_date', () => { - const expectedStartDate = '2020-02-05'; - - component.onStartDateChange(expectedStartDate); - fixture.detectChanges(); - const endDateInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_date'); - - expect(endDateInput.value).toEqual(expectedStartDate); - }); - - it('on selected end_date should not change start_date', () => { - const expectedStartDate = '2020-02-05'; - const expectedEndDate = '2020-02-06'; - - component.ngOnInit(); - fixture.detectChanges(); - const startDateInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#start_date'); - const endDateInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_date'); - startDateInput.value = expectedStartDate; - endDateInput.value = expectedEndDate; - endDateInput.dispatchEvent(new Event('#end_date')); - - expect(endDateInput.value).not.toEqual(startDateInput.value); - expect(startDateInput.value).toEqual(expectedStartDate); - }); - it('on get current date should return expected date', () => { const expectedDate = moment(new Date()).format(DATE_FORMAT_YEAR); @@ -560,15 +534,6 @@ describe('DetailsFieldsComponent', () => { expect(startDateInput.max).toEqual(expectedDate); }); - it('on the input with id #end_date we could get the current Date ', () => { - fixture.detectChanges(); - const expectedDate = moment(new Date()).format(DATE_FORMAT_YEAR); - const endDateInput = fixture.debugElement.nativeElement.querySelector('[id=end_date]'); - - expect(endDateInput.id).toEqual('end_date'); - expect(endDateInput.max).toEqual(expectedDate); - }); - const diffParams = [ { case: 'positive should return correctly diff', diff --git a/src/app/modules/shared/components/sidebar/sidebar.component.html b/src/app/modules/shared/components/sidebar/sidebar.component.html index 164bfc1ec..4afd966c6 100644 --- a/src/app/modules/shared/components/sidebar/sidebar.component.html +++ b/src/app/modules/shared/components/sidebar/sidebar.component.html @@ -2,7 +2,7 @@