From 159b6d6208dddeb64459027ac72987c57b7ca6e2 Mon Sep 17 00:00:00 2001 From: David Mang Date: Tue, 20 Aug 2024 19:59:52 +0200 Subject: [PATCH 01/25] change repo name to lowercase in image name in compose file --- .github/workflows/deploy.yml | 2 +- compose.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ad7489b..a859cac 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy on: push: branches: - - main # or the branch you want to deploy from + - develop # or the branch you want to deploy from jobs: build: diff --git a/compose.yml b/compose.yml index 235162c..6d9002e 100644 --- a/compose.yml +++ b/compose.yml @@ -25,7 +25,7 @@ services: backend: container_name: server hostname: server - image: ghcr.io/ls1intum/Thaii/server:latest + image: ghcr.io/ls1intum/thaii/server:latest build: context: . dockerfile: docker/server/Dockerfile @@ -62,7 +62,7 @@ services: client: container_name: client hostname: client - image: ghcr.io/ls1intum/Thaii/client:latest + image: ghcr.io/ls1intum/thaii/client:latest build: context: . dockerfile: docker/client/Dockerfile From a84892644309c1e4e1dbd8cf28d3b3411986b956 Mon Sep 17 00:00:00 2001 From: David Mang Date: Tue, 20 Aug 2024 20:07:20 +0200 Subject: [PATCH 02/25] change image name to lower case --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a859cac..55f52d7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,8 +26,8 @@ jobs: - name: Push Docker Images to GHCR run: | - docker push ghcr.io/${{ github.repository }}/client:latest - docker push ghcr.io/${{ github.repository }}/server:latest + docker push ghcr.io/ls1intum/thaii/client:latest + docker push ghcr.io/ls1intum/thaii/server:latest deploy: name: Deploy Application From 215970ebda46f78b65bf85ebab60b9cfafacaa54 Mon Sep 17 00:00:00 2001 From: David Mang Date: Tue, 20 Aug 2024 20:13:29 +0200 Subject: [PATCH 03/25] Change ssh agent version in deploy script --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 55f52d7..d46e676 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -42,7 +42,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y openssh-client - name: Add SSH Key - uses: webfactory/ssh-agent@v0.8.1 + uses: webfactory/ssh-agent@v0.5.3 with: ssh-private-key: ${{ secrets.SSH_KEY }} From a2a726f001bbee3a10dc9ca8c8c05b2a0d334cbd Mon Sep 17 00:00:00 2001 From: David Mang Date: Fri, 23 Aug 2024 18:46:10 +0200 Subject: [PATCH 04/25] Change openssh to appleboy runner --- .github/workflows/deploy.yml | 88 +++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d46e676..17a1014 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,46 +35,62 @@ jobs: needs: build steps: + - name: SSH to VM and Execute Docker-Compose Down + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + docker compose -f compose.yml --env-file=.env down --remove-orphans --rmi all + - name: Checkout Code uses: actions/checkout@v3 - - name: Install SSH Client - run: sudo apt-get update && sudo apt-get install -y openssh-client - - - name: Add SSH Key - uses: webfactory/ssh-agent@v0.5.3 - with: - ssh-private-key: ${{ secrets.SSH_KEY }} - - name: Copy Files to Server - run: | - scp -o StrictHostKeyChecking=no ./compose.yml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/compose.yml - scp -o StrictHostKeyChecking=no -r ./letsencrypt ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/letsencrypt + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + scp -o StrictHostKeyChecking=no ./compose.yml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/compose.yml + scp -o StrictHostKeyChecking=no -r ./letsencrypt ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/letsencrypt - name: Set Up Environment Variables - run: | - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} << 'EOF' - echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env - echo "DEBUG=${{ secrets.DEBUG }}" >> .env - echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env - echo "POSTGRES_DB=${{ secrets.POSTGRES_DB }}" >> .env - echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env - echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env - echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env - echo "EMAIL_USE_TLS=${{ secrets.EMAIL_USE_TLS }}" >> .env - echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env - echo "EMAIL_HOST_USER=${{ secrets.EMAIL_HOST_USER }}" >> .env - echo "EMAIL_HOST_PASSWORD=${{ secrets.EMAIL_HOST_PASSWORD }}" >> .env - echo "DEFAULT_FROM_EMAIL=${{ secrets.DEFAULT_FROM_EMAIL }}" >> .env - echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env - echo "DJANGO_SUPERUSER_USERNAME=${{ secrets.DJANGO_SUPERUSER_USERNAME }}" >> .env - echo "DJANGO_SUPERUSER_PASSWORD=${{ secrets.DJANGO_SUPERUSER_PASSWORD }}" >> .env - echo "DJANGO_SUPERUSER_EMAIL=${{ secrets.DJANGO_SUPERUSER_EMAIL }}" >> .env - EOF + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} << 'EOF' + touch .env + echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env + echo "DEBUG=${{ secrets.DEBUG }}" >> .env + echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env + echo "POSTGRES_DB=${{ secrets.POSTGRES_DB }}" >> .env + echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env + echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env + echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env + echo "EMAIL_USE_TLS=${{ secrets.EMAIL_USE_TLS }}" >> .env + echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env + echo "EMAIL_HOST_USER=${{ secrets.EMAIL_HOST_USER }}" >> .env + echo "EMAIL_HOST_PASSWORD=${{ secrets.EMAIL_HOST_PASSWORD }}" >> .env + echo "DEFAULT_FROM_EMAIL=${{ secrets.DEFAULT_FROM_EMAIL }}" >> .env + echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env + echo "DJANGO_SUPERUSER_USERNAME=${{ secrets.DJANGO_SUPERUSER_USERNAME }}" >> .env + echo "DJANGO_SUPERUSER_PASSWORD=${{ secrets.DJANGO_SUPERUSER_PASSWORD }}" >> .env + echo "DJANGO_SUPERUSER_EMAIL=${{ secrets.DJANGO_SUPERUSER_EMAIL }}" >> .env + EOF - - name: Deploy on Server - run: | - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "mkdir -p ~/" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "touch ~/letsencrypt/acme.json && chmod 600 ~/letsencrypt/acme.json" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< ${{ secrets.GITHUB_TOKEN }}" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker compose pull && docker compose up -d && docker compose logs" + - name: SSH to VM and Execute Docker-Compose Up + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "mkdir -p ~/" + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "touch ~/letsencrypt/acme.json && chmod 600 ~/letsencrypt/acme.json" + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< ${{ secrets.GITHUB_TOKEN }}" + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker compose pull && docker compose up -d && docker compose logs" From d84c0af07f731da2985b60e91591a55e1eb41d4e Mon Sep 17 00:00:00 2001 From: David Mang Date: Fri, 23 Aug 2024 19:10:33 +0200 Subject: [PATCH 05/25] Refactor deployment process and split it into build and deploy --- .github/workflows/build_docker.yml | 102 ++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 3 +- .github/workflows/deploy_docker.yml | 79 +++++++++++++++++++++ .github/workflows/prod.yml | 19 ++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build_docker.yml create mode 100644 .github/workflows/deploy_docker.yml create mode 100644 .github/workflows/prod.yml diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml new file mode 100644 index 0000000..d74f8a6 --- /dev/null +++ b/.github/workflows/build_docker.yml @@ -0,0 +1,102 @@ +name: Build Docker Image + +on: + workflow_call: + outputs: + server_image_tag: + description: "The tag of the server image that was built" + value: ${{ jobs.build.outputs.server_image_tag }} + client_image_tag: + description: "The tag of the client image that was built" + value: ${{ jobs.build.outputs.client_image_tag }} + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dockerfile: .docker/client/Dockerfile + image: ghcr.io/ls1intum/thaii/client + context: ./client + path: client + - dockerfile: .docker/server/Dockerfile + image: ghcr.io/ls1intum/thaii/server + context: ./server + path: server + outputs: + server_image_tag: "${{ steps.output-tag-server.outputs.server_image_tag }}" + client_image_tag: "${{ steps.output-tag-client.outputs.client_image_tag }}" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Get changed files in the client folder + id: changed-files-client-folder + uses: tj-actions/changed-files@v44 + with: + files: client/** + + - name: Get changed files in the server folder + id: changed-files-server-folder + uses: tj-actions/changed-files@v44 + with: + files: server/** + + - name: Log in to the Container registry + if: ${{ (steps.changed-files-client-folder.outputs.any_changed == 'true') || (steps.changed-files-server-folder.outputs.any_changed == 'true') }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + if: ${{ (steps.changed-files-client-folder.outputs.any_changed == 'true') || (steps.changed-files-server-folder.outputs.any_changed == 'true') }} + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Install Docker Buildx + if: ${{ (steps.changed-files-client-folder.outputs.any_changed == 'true') || (steps.changed-files-server-folder.outputs.any_changed == 'true') }} + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ matrix.image }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + + - name: Build and push Docker Image + uses: docker/build-push-action@v5 + if: ${{ (steps.changed-files-client-folder.outputs.any_changed == 'true' && matrix.path == 'client') || (steps.changed-files-server-folder.outputs.any_changed == 'true' && matrix.path == 'server') }} + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + + - id: output-tag-client + run: | + if [[ "${{ matrix.path }}" == "client" ]] && [[ "${{ steps.changed-files-client-folder.outputs.any_changed }}" == "true" ]]; then + echo "client_image_tag=${{ steps.meta.outputs.version }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ matrix.path }}" == "client" ]]; then + echo "client_image_tag=latest" >> "$GITHUB_OUTPUT" + fi + + - id: output-tag-server + run: | + if [[ "${{ matrix.path }}" == "server" ]] && [[ "${{ steps.changed-files-server-folder.outputs.any_changed }}" == "true" ]]; then + echo "server_image_tag=${{ steps.meta.outputs.version }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ matrix.path }}" == "server" ]]; then + echo "server_image_tag=latest" >> "$GITHUB_OUTPUT" + fi \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 17a1014..15f0707 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy on: push: branches: - - develop # or the branch you want to deploy from + - main # or the branch you want to deploy from jobs: build: @@ -58,6 +58,7 @@ jobs: scp -o StrictHostKeyChecking=no -r ./letsencrypt ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/letsencrypt - name: Set Up Environment Variables + uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.SERVER_DOMAIN }} username: ${{ secrets.SERVER_USER }} diff --git a/.github/workflows/deploy_docker.yml b/.github/workflows/deploy_docker.yml new file mode 100644 index 0000000..a4c78fe --- /dev/null +++ b/.github/workflows/deploy_docker.yml @@ -0,0 +1,79 @@ +name: Deploy Docker Image + +on: + workflow_call: + inputs: + environment: + required: true + type: string + server_image_tag: + default: "latest" + type: string + client_image_tag: + default: "latest" + type: string + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: SSH to VM and Execute Docker-Compose Down + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + docker compose -f compose.yml --env-file=.env down --remove-orphans --rmi all + + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Copy Files to Server + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + scp -o StrictHostKeyChecking=no ./compose.yml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/compose.yml + scp -o StrictHostKeyChecking=no -r ./letsencrypt ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/letsencrypt + + - name: Set Up Environment Variables + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} << 'EOF' + touch .env + echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env + echo "DEBUG=${{ secrets.DEBUG }}" >> .env + echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env + echo "POSTGRES_DB=${{ secrets.POSTGRES_DB }}" >> .env + echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env + echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env + echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env + echo "EMAIL_USE_TLS=${{ secrets.EMAIL_USE_TLS }}" >> .env + echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env + echo "EMAIL_HOST_USER=${{ secrets.EMAIL_HOST_USER }}" >> .env + echo "EMAIL_HOST_PASSWORD=${{ secrets.EMAIL_HOST_PASSWORD }}" >> .env + echo "DEFAULT_FROM_EMAIL=${{ secrets.DEFAULT_FROM_EMAIL }}" >> .env + echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env + echo "DJANGO_SUPERUSER_USERNAME=${{ secrets.DJANGO_SUPERUSER_USERNAME }}" >> .env + echo "DJANGO_SUPERUSER_PASSWORD=${{ secrets.DJANGO_SUPERUSER_PASSWORD }}" >> .env + echo "DJANGO_SUPERUSER_EMAIL=${{ secrets.DJANGO_SUPERUSER_EMAIL }}" >> .env + EOF + + - name: SSH to VM and Execute Docker-Compose Up + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "mkdir -p ~/" + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "touch ~/letsencrypt/acme.json && chmod 600 ~/letsencrypt/acme.json" + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< ${{ secrets.GITHUB_TOKEN }}" + ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker compose pull && docker compose up -d && docker compose logs" diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml new file mode 100644 index 0000000..b3e462c --- /dev/null +++ b/.github/workflows/prod.yml @@ -0,0 +1,19 @@ +name: Build and Deploy to Prod + +on: + push: + branches: [develop] + +jobs: + build-prod-container: + uses: ./.github/workflows/build_docker.yml + secrets: inherit + deploy-prod-container: + needs: build-prod-container + uses: ./.github/workflows/deploy_docker.yml + secrets: inherit + with: + environment: Production + server_image_tag: "latest" + client_image_tag: "latest" + \ No newline at end of file From 3b61201c02ed3fed52c942f530775c57604b60fd Mon Sep 17 00:00:00 2001 From: David Mang Date: Thu, 29 Aug 2024 14:31:32 +0200 Subject: [PATCH 06/25] Add proxy variables to deploy docker --- .github/workflows/deploy_docker.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/deploy_docker.yml b/.github/workflows/deploy_docker.yml index a4c78fe..926d082 100644 --- a/.github/workflows/deploy_docker.yml +++ b/.github/workflows/deploy_docker.yml @@ -23,6 +23,10 @@ jobs: host: ${{ secrets.SERVER_DOMAIN }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | docker compose -f compose.yml --env-file=.env down --remove-orphans --rmi all @@ -35,6 +39,10 @@ jobs: host: ${{ secrets.SERVER_DOMAIN }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | scp -o StrictHostKeyChecking=no ./compose.yml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/compose.yml scp -o StrictHostKeyChecking=no -r ./letsencrypt ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/letsencrypt @@ -45,6 +53,10 @@ jobs: host: ${{ secrets.SERVER_DOMAIN }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} << 'EOF' touch .env @@ -72,6 +84,10 @@ jobs: host: ${{ secrets.SERVER_DOMAIN }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "mkdir -p ~/" ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "touch ~/letsencrypt/acme.json && chmod 600 ~/letsencrypt/acme.json" From d40adfcfcbb2da41a438d5d722b7d1272a5ffbfd Mon Sep 17 00:00:00 2001 From: David Mang Date: Thu, 29 Aug 2024 14:41:30 +0200 Subject: [PATCH 07/25] Copy letsencrypt and docker compose file to vm in deploy docker script --- .github/workflows/deploy_docker.yml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy_docker.yml b/.github/workflows/deploy_docker.yml index 926d082..06cec4e 100644 --- a/.github/workflows/deploy_docker.yml +++ b/.github/workflows/deploy_docker.yml @@ -33,19 +33,31 @@ jobs: - name: Checkout Code uses: actions/checkout@v3 - - name: Copy Files to Server - uses: appleboy/ssh-action@v1.0.3 + - name: Copy Docker Compose File From Repo to VM Host + uses: appleboy/scp-action@v0.1.7 with: - host: ${{ secrets.SERVER_DOMAIN }} - username: ${{ secrets.SERVER_USER }} - key: ${{ secrets.SSH_KEY }} + host: ${{ vars.VM_HOST }} + username: ${{ vars.VM_USERNAME }} + key: ${{ secrets.VM_SSH_PRIVATE_KEY }} proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} - script: | - scp -o StrictHostKeyChecking=no ./compose.yml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/compose.yml - scp -o StrictHostKeyChecking=no -r ./letsencrypt ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }}:~/letsencrypt + source: "./compose.yml" + target: /home/${{ secrets.SERVER_USER }} + + - name: Copy Letsencrypt File From Repo to VM Host + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ vars.VM_HOST }} + username: ${{ vars.VM_USERNAME }} + key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} + source: "./letsencrypt" + target: /home/${{ secrets.SERVER_USER }} - name: Set Up Environment Variables uses: appleboy/ssh-action@v1.0.3 From 0bbbcb15377f47feb47a083dfa3b57b2c7932e51 Mon Sep 17 00:00:00 2001 From: David Mang Date: Thu, 29 Aug 2024 14:45:08 +0200 Subject: [PATCH 08/25] Fix pipeline variables in deploy docker script --- .github/workflows/deploy_docker.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy_docker.yml b/.github/workflows/deploy_docker.yml index 06cec4e..961c379 100644 --- a/.github/workflows/deploy_docker.yml +++ b/.github/workflows/deploy_docker.yml @@ -36,9 +36,9 @@ jobs: - name: Copy Docker Compose File From Repo to VM Host uses: appleboy/scp-action@v0.1.7 with: - host: ${{ vars.VM_HOST }} - username: ${{ vars.VM_USERNAME }} - key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} @@ -49,9 +49,9 @@ jobs: - name: Copy Letsencrypt File From Repo to VM Host uses: appleboy/scp-action@v0.1.7 with: - host: ${{ vars.VM_HOST }} - username: ${{ vars.VM_USERNAME }} - key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + host: ${{ secrets.SERVER_DOMAIN }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_KEY }} proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} From 30bfd06a8c2846fa5bce484239f855a872600fc6 Mon Sep 17 00:00:00 2001 From: David Mang Date: Thu, 29 Aug 2024 14:53:41 +0200 Subject: [PATCH 09/25] Change env variables copying in deploy docker script --- .github/workflows/deploy_docker.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy_docker.yml b/.github/workflows/deploy_docker.yml index 961c379..1896ec4 100644 --- a/.github/workflows/deploy_docker.yml +++ b/.github/workflows/deploy_docker.yml @@ -70,7 +70,6 @@ jobs: proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} << 'EOF' touch .env echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env echo "DEBUG=${{ secrets.DEBUG }}" >> .env @@ -88,7 +87,6 @@ jobs: echo "DJANGO_SUPERUSER_USERNAME=${{ secrets.DJANGO_SUPERUSER_USERNAME }}" >> .env echo "DJANGO_SUPERUSER_PASSWORD=${{ secrets.DJANGO_SUPERUSER_PASSWORD }}" >> .env echo "DJANGO_SUPERUSER_EMAIL=${{ secrets.DJANGO_SUPERUSER_EMAIL }}" >> .env - EOF - name: SSH to VM and Execute Docker-Compose Up uses: appleboy/ssh-action@v1.0.3 From 47e0f7aae32cb67ee2808a8e53a78b959e07e086 Mon Sep 17 00:00:00 2001 From: David Mang Date: Thu, 29 Aug 2024 15:02:07 +0200 Subject: [PATCH 10/25] Change excecute docker compose up script on VM --- .github/workflows/deploy_docker.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy_docker.yml b/.github/workflows/deploy_docker.yml index 1896ec4..039841d 100644 --- a/.github/workflows/deploy_docker.yml +++ b/.github/workflows/deploy_docker.yml @@ -99,7 +99,7 @@ jobs: proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "mkdir -p ~/" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "touch ~/letsencrypt/acme.json && chmod 600 ~/letsencrypt/acme.json" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< ${{ secrets.GITHUB_TOKEN }}" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_DOMAIN }} "docker compose pull && docker compose up -d && docker compose logs" + mkdir -p ~/ + touch ~/letsencrypt/acme.json && chmod 600 ~/letsencrypt/acme.json + docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< ${{ secrets.GITHUB_TOKEN }} + docker compose pull && docker compose up -d && docker compose logs From a303da3feb84da043253db9af3e322d834ca9e94 Mon Sep 17 00:00:00 2001 From: David Mang Date: Thu, 29 Aug 2024 15:16:44 +0200 Subject: [PATCH 11/25] Change the origin of templates to show admin panels with css files --- server/server/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server/settings.py b/server/server/settings.py index 2a1d7aa..2ca9879 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -47,7 +47,7 @@ } STATICFILES_STORAGE = ('whitenoise.storage.CompressedManifestStaticFilesStorage') -STATIC_URL = '/static/' +STATIC_URL = '/api/static/' STATIC_ROOT = os.path.join(BASE_DIR, "templates") SIMPLE_JWT = { From 41ea4da61a97109d0253e9b5313668a7a1e9c1b4 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 14:46:30 +0200 Subject: [PATCH 12/25] Change context of dockerfile in build docker pipeline --- .github/workflows/build_docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index d74f8a6..074e9f5 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -19,11 +19,11 @@ jobs: include: - dockerfile: .docker/client/Dockerfile image: ghcr.io/ls1intum/thaii/client - context: ./client + context: . path: client - dockerfile: .docker/server/Dockerfile image: ghcr.io/ls1intum/thaii/server - context: ./server + context: . path: server outputs: server_image_tag: "${{ steps.output-tag-server.outputs.server_image_tag }}" From 06bd13a959138862c5325dadfb09e3f6b735f9d7 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 14:52:28 +0200 Subject: [PATCH 13/25] Change path to dockerfile in build docker script --- .github/workflows/build_docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index 074e9f5..2b4ad43 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -17,11 +17,11 @@ jobs: fail-fast: false matrix: include: - - dockerfile: .docker/client/Dockerfile + - dockerfile: ./docker/client/Dockerfile image: ghcr.io/ls1intum/thaii/client context: . path: client - - dockerfile: .docker/server/Dockerfile + - dockerfile: ./docker/server/Dockerfile image: ghcr.io/ls1intum/thaii/server context: . path: server From c4044f954d9fe7efa84ccdafd4e9abb0e19b6b32 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 14:57:17 +0200 Subject: [PATCH 14/25] Change context of dockerfiles in matrix in build docker script --- .github/workflows/build_docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index 2b4ad43..d7159e7 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -19,11 +19,11 @@ jobs: include: - dockerfile: ./docker/client/Dockerfile image: ghcr.io/ls1intum/thaii/client - context: . + context: ./docker/client path: client - dockerfile: ./docker/server/Dockerfile image: ghcr.io/ls1intum/thaii/server - context: . + context: ./docker/server path: server outputs: server_image_tag: "${{ steps.output-tag-server.outputs.server_image_tag }}" From f8a440215c606bb5babf4e66e687575220f6787e Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:01:24 +0200 Subject: [PATCH 15/25] Add comments to test pipelines --- client/src/api/chat.api.ts | 2 ++ server/chat/views.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/api/chat.api.ts b/client/src/api/chat.api.ts index 935473b..edd1a95 100644 --- a/client/src/api/chat.api.ts +++ b/client/src/api/chat.api.ts @@ -1,6 +1,8 @@ import { ChatBody } from "../types/chatbot/chatbot.types"; import api from "./interceptor.api"; +// Fetch all chats for a user +// @limit: Number of loaded chats. Default number is 5. export const fetchChats = async (limit: number) => { try { if (!limit) { diff --git a/server/chat/views.py b/server/chat/views.py index ef3cc45..4219c75 100644 --- a/server/chat/views.py +++ b/server/chat/views.py @@ -9,7 +9,6 @@ from rest_framework.response import Response from rest_framework import status -# Create your views here. class CreateUserView(generics.CreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer From a810d1738fe3c48f8aa9e83b47561816fc84b20a Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:06:49 +0200 Subject: [PATCH 16/25] Change context in build docker file --- .github/workflows/build_docker.yml | 4 ++-- client/src/api/chat.api.ts | 1 + server/pages/views.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index d7159e7..c9336d1 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -19,11 +19,11 @@ jobs: include: - dockerfile: ./docker/client/Dockerfile image: ghcr.io/ls1intum/thaii/client - context: ./docker/client + context: ./client path: client - dockerfile: ./docker/server/Dockerfile image: ghcr.io/ls1intum/thaii/server - context: ./docker/server + context: ./server path: server outputs: server_image_tag: "${{ steps.output-tag-server.outputs.server_image_tag }}" diff --git a/client/src/api/chat.api.ts b/client/src/api/chat.api.ts index edd1a95..ee8d2bf 100644 --- a/client/src/api/chat.api.ts +++ b/client/src/api/chat.api.ts @@ -15,6 +15,7 @@ export const fetchChats = async (limit: number) => { } }; +// Fetch the count of all chats for a user export const fetchChatsCount = async () => { try { const response = await api.get(`/api/v1/chats/count/`); diff --git a/server/pages/views.py b/server/pages/views.py index ae42c46..5d1ed1f 100644 --- a/server/pages/views.py +++ b/server/pages/views.py @@ -8,7 +8,6 @@ from chat.models import Chat from rest_framework.permissions import IsAuthenticated -# Create your views here. class PageListView(APIView): permission_classes = [IsAuthenticated] From 03a85bd0130ac02509d7268e91d37c902c9025d2 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:23:02 +0200 Subject: [PATCH 17/25] Change context if dockerfile in build docker script --- .github/workflows/build_docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index c9336d1..d7159e7 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -19,11 +19,11 @@ jobs: include: - dockerfile: ./docker/client/Dockerfile image: ghcr.io/ls1intum/thaii/client - context: ./client + context: ./docker/client path: client - dockerfile: ./docker/server/Dockerfile image: ghcr.io/ls1intum/thaii/server - context: ./server + context: ./docker/server path: server outputs: server_image_tag: "${{ steps.output-tag-server.outputs.server_image_tag }}" From 341c082519aec791f40839d535e3dd2afb935d58 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:27:32 +0200 Subject: [PATCH 18/25] Add comments to text pipeline --- client/src/api/chat.api.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/src/api/chat.api.ts b/client/src/api/chat.api.ts index ee8d2bf..8a5567e 100644 --- a/client/src/api/chat.api.ts +++ b/client/src/api/chat.api.ts @@ -1,7 +1,7 @@ import { ChatBody } from "../types/chatbot/chatbot.types"; import api from "./interceptor.api"; -// Fetch all chats for a user +// Fetch all chats for an user // @limit: Number of loaded chats. Default number is 5. export const fetchChats = async (limit: number) => { try { @@ -15,7 +15,7 @@ export const fetchChats = async (limit: number) => { } }; -// Fetch the count of all chats for a user +// Fetch the count of all chats for an user export const fetchChatsCount = async () => { try { const response = await api.get(`/api/v1/chats/count/`); @@ -26,6 +26,7 @@ export const fetchChatsCount = async () => { } }; +// Fetch chat by chat ID for an user export const fetchChatById = async (chatId: number) => { try { const response = await api.get(`/api/v1/chats/${chatId}/`); @@ -35,6 +36,8 @@ export const fetchChatById = async (chatId: number) => { } }; +// Fetch chats assigned to a page for a user +// @pageId: Id of a existing page export const fetchChatByPageId = async (pageId: number) => { try { const response = await api.get(`/api/v1/chats/page/${pageId}/`); @@ -45,6 +48,8 @@ export const fetchChatByPageId = async (pageId: number) => { } }; +// Create new chat +// @chat: title: string, page: string, labels: string[] export const createChat = async (chat: ChatBody) => { try { const response = await api.post("/api/v1/chats/", chat); @@ -55,6 +60,9 @@ export const createChat = async (chat: ChatBody) => { } }; +// Change existing chat of user +// @chat: title: string, page: string, labels: string[] +// @id: id of chat which should be changed export const changeChat = async (id: number, chat: ChatBody) => { try { const response = await api.put(`/api/v1/chats/${id}/`, chat); @@ -65,6 +73,8 @@ export const changeChat = async (id: number, chat: ChatBody) => { } }; +// Delete existing chat of user +// @id: id of chat to delete export const deleteChat = async (id: number) => { try { const response = await api.delete(`/api/v1/chats/${id}/`); From 739f64edf1cf5f403afbe2a47d2200bee5158083 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:35:05 +0200 Subject: [PATCH 19/25] Change dockerfile path to client --- .github/workflows/build_docker.yml | 4 ++-- client/src/api/insights.api.ts | 8 ++++++++ docker/client/Dockerfile | 4 ++-- docker/server/Dockerfile | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index d7159e7..2b4ad43 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -19,11 +19,11 @@ jobs: include: - dockerfile: ./docker/client/Dockerfile image: ghcr.io/ls1intum/thaii/client - context: ./docker/client + context: . path: client - dockerfile: ./docker/server/Dockerfile image: ghcr.io/ls1intum/thaii/server - context: ./docker/server + context: . path: server outputs: server_image_tag: "${{ steps.output-tag-server.outputs.server_image_tag }}" diff --git a/client/src/api/insights.api.ts b/client/src/api/insights.api.ts index 739600d..07ad278 100644 --- a/client/src/api/insights.api.ts +++ b/client/src/api/insights.api.ts @@ -1,6 +1,13 @@ import { FilterBody } from "../types/statistics/statistics.types"; import api from "./interceptor.api"; +//@filter: +// - dateRange: Dates included in analysis +// - page: Pages included in analysis +// - labels: Labels included in analysis +// - tags: Tags included in analysis + +// Fetch number of total chats export const fetchTotalChats = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-chats/`, filter); @@ -10,6 +17,7 @@ export const fetchTotalChats = async (filter: FilterBody) => { } }; +// Fetch number of total messages export const fetchTotalMessages = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-messages/`, filter); diff --git a/docker/client/Dockerfile b/docker/client/Dockerfile index d76e21c..24d8c22 100644 --- a/docker/client/Dockerfile +++ b/docker/client/Dockerfile @@ -6,11 +6,11 @@ ENV VITE_API_URL=$VITE_API_URL WORKDIR /client -COPY ./client/package*.json ./ +COPY ../../client/package*.json ./ RUN npm install -COPY ./client . +COPY ../../client . RUN npm run build diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index d5e3784..7485c71 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.12.4-slim-bookworm WORKDIR /server -COPY ./server . +COPY ../../server . RUN python3 -m venv /opt/venv && \ /opt/venv/bin/pip install --upgrade pip && \ From ed9ff70fe2844059009e2be26706abf466d805a9 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:48:09 +0200 Subject: [PATCH 20/25] Change server dockerfile to adjust to folder structure --- .github/workflows/build_docker.yml | 3 +++ docker/client/Dockerfile | 2 ++ docker/server/Dockerfile | 19 +++++++++++++++---- server/insights/views.py | 1 + 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index 2b4ad43..cfcf943 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -84,6 +84,9 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} + build-args: | + "VITE_API_URL=${{ var.VITE_API_URL }}" + "VITE_ENABLE_TRACKING"=${{ var.VITE_ENABLE_TRACKING }}" - id: output-tag-client run: | diff --git a/docker/client/Dockerfile b/docker/client/Dockerfile index 24d8c22..e4e1452 100644 --- a/docker/client/Dockerfile +++ b/docker/client/Dockerfile @@ -1,8 +1,10 @@ FROM node:alpine ARG VITE_API_URL +ARG VITE_ENABLE_TRACKING ENV VITE_API_URL=$VITE_API_URL +ENV VITE_ENABLE_TRACKING=$VITE_ENABLE_TRACKING WORKDIR /client diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index 7485c71..5d3ebfe 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -1,14 +1,25 @@ +# Use Python slim image FROM python:3.12.4-slim-bookworm +# Set the working directory inside the Docker image WORKDIR /server -COPY ../../server . +# Copy only the requirements first to leverage caching +COPY ../../server/requirements.txt . +# Create and activate the virtual environment, then install dependencies RUN python3 -m venv /opt/venv && \ /opt/venv/bin/pip install --upgrade pip && \ - /opt/venv/bin/pip install -r requirements.txt --no-cache-dir && \ - chmod +x /server/entrypoint.sh + /opt/venv/bin/pip install -r requirements.txt --no-cache-dir + +# Copy the rest of the server code into the Docker image +COPY ../../server . + +# Ensure entrypoint script has execute permissions +RUN chmod +x /server/entrypoint.sh +# Expose the application port EXPOSE 8000 -CMD [ "/server/entrypoint.sh" ] \ No newline at end of file +# Run the entrypoint script +CMD [ "/server/entrypoint.sh" ] diff --git a/server/insights/views.py b/server/insights/views.py index d6fcfe6..82db368 100644 --- a/server/insights/views.py +++ b/server/insights/views.py @@ -19,6 +19,7 @@ import datetime from django.contrib.postgres.aggregates import StringAgg +# Get current date and time now_ = now() class TotalChatsView(APIView): From 9dff50f8f5bec1a4d9cfc98618bce05aed0d0b67 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 15:51:28 +0200 Subject: [PATCH 21/25] Fix naming error in build docker script --- .github/workflows/build_docker.yml | 4 ++-- server/insights/services/commonWordAnalyzer.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index cfcf943..c8e0da7 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -85,8 +85,8 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} build-args: | - "VITE_API_URL=${{ var.VITE_API_URL }}" - "VITE_ENABLE_TRACKING"=${{ var.VITE_ENABLE_TRACKING }}" + "VITE_API_URL=${{ vars.VITE_API_URL }}" + "VITE_ENABLE_TRACKING"=${{ vars.VITE_ENABLE_TRACKING }}" - id: output-tag-client run: | diff --git a/server/insights/services/commonWordAnalyzer.py b/server/insights/services/commonWordAnalyzer.py index 6c31b07..a1b8803 100644 --- a/server/insights/services/commonWordAnalyzer.py +++ b/server/insights/services/commonWordAnalyzer.py @@ -11,6 +11,7 @@ cachedStopWords = stopwords.words("english") +# Handler to find common words in messages of users def handle_array_common_word(array, variant): textList = [] formattedTextList = [] From 0b2c0396162d657c9d524dbb49c764ac909b9909 Mon Sep 17 00:00:00 2001 From: David Mang <117346875+mangdavid@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:20:21 +0200 Subject: [PATCH 22/25] Feature/create interaction log (#5) * Add view to create and download eventlogs * Add download button to sidebar to download data as spreadsheet --------- Co-authored-by: David Mang --- client/src/api/interaction.api.ts | 23 +++++++++ .../download-button.component.tsx | 40 +++++++++++++++ .../components/sidebar/sidebar.component.tsx | 26 ++++++++++ .../word-cloud/word-cloud.component.tsx | 29 +++++++++++ .../statistics/statistics.component.tsx | 10 +++- client/src/services/interactions.service.ts | 20 ++++++++ .../types/interaction/interaction.types.ts | 3 ++ server/chat/admin.py | 50 +++++++++++++++++-- server/chat/views.py | 8 +++ server/interactions/__init__.py | 0 server/interactions/admin.py | 37 ++++++++++++++ server/interactions/apps.py | 6 +++ .../interactions/migrations/0001_initial.py | 27 ++++++++++ .../0002_alter_eventlog_created_at.py | 18 +++++++ server/interactions/migrations/__init__.py | 0 server/interactions/models.py | 11 ++++ server/interactions/serializers.py | 9 ++++ server/interactions/tests.py | 3 ++ server/interactions/urls.py | 6 +++ server/interactions/views.py | 48 ++++++++++++++++++ server/pages/admin.py | 43 +++++++++++++++- server/pages/views.py | 10 +++- server/requirements.txt | 4 +- server/server/settings.py | 1 + server/server/urls.py | 1 + 25 files changed, 425 insertions(+), 8 deletions(-) create mode 100644 client/src/api/interaction.api.ts create mode 100644 client/src/components/sidebar/download-button/download-button.component.tsx create mode 100644 client/src/components/statistics/dashboard/word-cloud/word-cloud.component.tsx create mode 100644 client/src/services/interactions.service.ts create mode 100644 client/src/types/interaction/interaction.types.ts create mode 100644 server/interactions/__init__.py create mode 100644 server/interactions/admin.py create mode 100644 server/interactions/apps.py create mode 100644 server/interactions/migrations/0001_initial.py create mode 100644 server/interactions/migrations/0002_alter_eventlog_created_at.py create mode 100644 server/interactions/migrations/__init__.py create mode 100644 server/interactions/models.py create mode 100644 server/interactions/serializers.py create mode 100644 server/interactions/tests.py create mode 100644 server/interactions/urls.py create mode 100644 server/interactions/views.py diff --git a/client/src/api/interaction.api.ts b/client/src/api/interaction.api.ts new file mode 100644 index 0000000..e7eed90 --- /dev/null +++ b/client/src/api/interaction.api.ts @@ -0,0 +1,23 @@ +import { EventLogBody } from "../types/interaction/interaction.types"; +import api from "./interceptor.api"; + +export const createEventLog = async (eventlog: EventLogBody) => { + try { + const response = await api.post("/api/v1/event-logs/", eventlog); + return response; + } catch (error) { + console.error("Error creating event log:", error); + throw error; + } +}; + +export const fetchEventLogs = async () => { + try { + const response = await api.get(`/api/v1/event-logs/`, { + responseType: "blob", + }); + return response; + } catch (error) { + throw error; + } +}; diff --git a/client/src/components/sidebar/download-button/download-button.component.tsx b/client/src/components/sidebar/download-button/download-button.component.tsx new file mode 100644 index 0000000..3ee9547 --- /dev/null +++ b/client/src/components/sidebar/download-button/download-button.component.tsx @@ -0,0 +1,40 @@ +import { Button, Typography, useTheme } from "@mui/material"; +import { getEventLogs } from "../../../services/interactions.service"; +import { Download } from "react-feather"; + +const DownloadButton = () => { + const theme = useTheme(); + + const handleDownload = async () => { + try { + const response = await getEventLogs(); + + // Create a link element, set its href to the blob URL, and trigger a click to download the file + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", "user_interaction_data.xlsx"); // The file name you want to save as + document.body.appendChild(link); + link.click(); + + // Clean up and remove the link + link.remove(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("Error downloading file:", error); + } + }; + + return ( + + ); +}; + +export default DownloadButton; diff --git a/client/src/components/sidebar/sidebar.component.tsx b/client/src/components/sidebar/sidebar.component.tsx index a10bfab..72fb5d3 100644 --- a/client/src/components/sidebar/sidebar.component.tsx +++ b/client/src/components/sidebar/sidebar.component.tsx @@ -23,6 +23,8 @@ import { import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import { getUserPermissions } from "../../services/user.service"; +import { addEventLog } from "../../services/interactions.service"; +import DownloadButton from "./download-button/download-button.component"; function Sidebar({ open, setOpen }: SidebarParams) { const navigate = useNavigate(); @@ -112,6 +114,17 @@ function Sidebar({ open, setOpen }: SidebarParams) { }} onClick={() => { navigate(item.link); + if ( + (import.meta.env.VITE_ENABLE_TRACKING as string) == "true" + ) { + if (item.name == "Insights") { + addEventLog({ + location: item.name + " - Behavioral Indicators", + }); + } else { + addEventLog({ location: item.name }); + } + } }} > ); })} + + + + + + + + {title} + + + + + + + + + + + ); +} + +export default WordCloudGraph; diff --git a/client/src/components/statistics/statistics.component.tsx b/client/src/components/statistics/statistics.component.tsx index 48bb220..aec3d52 100644 --- a/client/src/components/statistics/statistics.component.tsx +++ b/client/src/components/statistics/statistics.component.tsx @@ -11,6 +11,7 @@ import { getPagesForInsights } from "../../services/pages.service"; import { getLabels } from "../../services/label.service"; import { getTags } from "../../services/tags.service"; import { useToolStore } from "../../states/global.store"; +import { addEventLog } from "../../services/interactions.service"; const Filter = lazy(() => import("./filter/filter.component")); const BehavioralDashboard = lazy( @@ -120,7 +121,14 @@ function Statistics({ open }: SidebarParams) { key={tabItem.name} elevation={0} className="main tabs" - onClick={() => setTab(tabItem.tab)} + onClick={() => { + setTab(tabItem.tab); + if ((import.meta.env.VITE_ENABLE_TRACKING as string) == "true") { + addEventLog({ + location: "Insights - " + tabItem.name, + }); + } + }} sx={{ border: tabItem.tab === tab ? "solid 2px #7f7f7f" : 0, }} diff --git a/client/src/services/interactions.service.ts b/client/src/services/interactions.service.ts new file mode 100644 index 0000000..803bd0f --- /dev/null +++ b/client/src/services/interactions.service.ts @@ -0,0 +1,20 @@ +import { createEventLog, fetchEventLogs } from "../api/interaction.api"; +import { EventLogBody } from "../types/interaction/interaction.types"; + +export const addEventLog = async (event: EventLogBody) => { + try { + const response = await createEventLog(event); + return response.data; + } catch (error: any) { + throw error; + } +}; + +export const getEventLogs = async () => { + try { + const response = await fetchEventLogs(); + return response; + } catch (error: any) { + throw error; + } +}; diff --git a/client/src/types/interaction/interaction.types.ts b/client/src/types/interaction/interaction.types.ts new file mode 100644 index 0000000..a0b753e --- /dev/null +++ b/client/src/types/interaction/interaction.types.ts @@ -0,0 +1,3 @@ +export type EventLogBody = { + location: string; +}; diff --git a/server/chat/admin.py b/server/chat/admin.py index 32a7c9d..562f774 100644 --- a/server/chat/admin.py +++ b/server/chat/admin.py @@ -1,7 +1,51 @@ from django.contrib import admin from .models import Chat, Message, Label +from django.contrib.auth.models import User +import csv +from django.http import HttpResponse +from django.contrib import admin + +def export_as_csv(modeladmin, request, queryset): + meta = modeladmin.model._meta + field_names = [field.name for field in meta.fields] + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) + writer = csv.writer(response) + + writer.writerow(field_names) + for obj in queryset: + row = [] + for field in field_names: + value = getattr(obj, field) + if isinstance(value, User): # Check if the field is a User instance + value = value.id # Replace the value with the user's ID + row.append(value) + writer.writerow(row) + + return response + +export_as_csv.short_description = "Export Selected as CSV" + +class ModelsChatActions(admin.ModelAdmin): + list_display = [field.name for field in Chat._meta.fields] + actions = [export_as_csv] + search_fields = [field.name for field in Chat._meta.fields if field.name != 'id'] + list_filter = [field.name for field in Chat._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')] + +class ModelsMessagesActions(admin.ModelAdmin): + list_display = [field.name for field in Message._meta.fields] + actions = [export_as_csv] + search_fields = [field.name for field in Message._meta.fields if field.name != 'id'] + list_filter = [field.name for field in Message._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')] + +class ModelsLabelsActions(admin.ModelAdmin): + list_display = [field.name for field in Label._meta.fields] + actions = [export_as_csv] + search_fields = [field.name for field in Label._meta.fields if field.name != 'id'] + list_filter = [field.name for field in Label._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')] # Register your models here. -admin.site.register(Chat) -admin.site.register(Message) -admin.site.register(Label) +admin.site.register(Chat, ModelsChatActions) +admin.site.register(Message, ModelsMessagesActions) +admin.site.register(Label, ModelsLabelsActions) diff --git a/server/chat/views.py b/server/chat/views.py index 4219c75..5c0a51f 100644 --- a/server/chat/views.py +++ b/server/chat/views.py @@ -127,6 +127,14 @@ def post(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, pk, format=None): + try: + label = Label.objects.get(id=pk) + except Label.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + label.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class ChatByPageDetailView(APIView): permission_classes = [IsAuthenticated] diff --git a/server/interactions/__init__.py b/server/interactions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/interactions/admin.py b/server/interactions/admin.py new file mode 100644 index 0000000..9ea8ee7 --- /dev/null +++ b/server/interactions/admin.py @@ -0,0 +1,37 @@ +from django.contrib import admin +from .models import EventLog +from django.contrib.auth.models import User +import csv +from django.http import HttpResponse +from django.contrib import admin + +def export_as_csv(modeladmin, request, queryset): + meta = modeladmin.model._meta + field_names = [field.name for field in meta.fields] + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) + writer = csv.writer(response) + + writer.writerow(field_names) + for obj in queryset: + row = [] + for field in field_names: + value = getattr(obj, field) + if isinstance(value, User): # Check if the field is a User instance + value = value.id # Replace the value with the user's ID + row.append(value) + writer.writerow(row) + + return response + +export_as_csv.short_description = "Export Selected as CSV" + +class ModelsEventLogActions(admin.ModelAdmin): + list_display = [field.name for field in EventLog._meta.fields] + actions = [export_as_csv] + search_fields = [field.name for field in EventLog._meta.fields if field.name != 'id'] + list_filter = [field.name for field in EventLog._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')] + +# Register your models here. +admin.site.register(EventLog, ModelsEventLogActions) \ No newline at end of file diff --git a/server/interactions/apps.py b/server/interactions/apps.py new file mode 100644 index 0000000..67bf651 --- /dev/null +++ b/server/interactions/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InteractionsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'interactions' diff --git a/server/interactions/migrations/0001_initial.py b/server/interactions/migrations/0001_initial.py new file mode 100644 index 0000000..7cf12fc --- /dev/null +++ b/server/interactions/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.4 on 2024-08-26 10:30 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='EventLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('location', models.CharField(max_length=100)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='eventlogs', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/server/interactions/migrations/0002_alter_eventlog_created_at.py b/server/interactions/migrations/0002_alter_eventlog_created_at.py new file mode 100644 index 0000000..1c02f1a --- /dev/null +++ b/server/interactions/migrations/0002_alter_eventlog_created_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-08-26 13:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('interactions', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='eventlog', + name='created_at', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/server/interactions/migrations/__init__.py b/server/interactions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/interactions/models.py b/server/interactions/models.py new file mode 100644 index 0000000..5e96699 --- /dev/null +++ b/server/interactions/models.py @@ -0,0 +1,11 @@ +from django.db import models +from django.contrib.auth.models import User +from django.utils import timezone + +class EventLog(models.Model): + location = models.CharField(max_length=100) + created_at = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="eventlogs") + + def __str__(self): + return self.location \ No newline at end of file diff --git a/server/interactions/serializers.py b/server/interactions/serializers.py new file mode 100644 index 0000000..308f28d --- /dev/null +++ b/server/interactions/serializers.py @@ -0,0 +1,9 @@ +from django.contrib.auth.models import User +from rest_framework import serializers +from .models import EventLog + +class EventLogSerializer(serializers.ModelSerializer): + class Meta: + model = EventLog + fields = ["id", "location", "created_at"] + extra_kwargs = {"user": {"read_only": True}} \ No newline at end of file diff --git a/server/interactions/tests.py b/server/interactions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/interactions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/interactions/urls.py b/server/interactions/urls.py new file mode 100644 index 0000000..3832f6d --- /dev/null +++ b/server/interactions/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('event-logs/', views.EventLogView.as_view(), name='event_log'), +] diff --git a/server/interactions/views.py b/server/interactions/views.py new file mode 100644 index 0000000..89d7b0a --- /dev/null +++ b/server/interactions/views.py @@ -0,0 +1,48 @@ +import pandas as pd +from django.shortcuts import render +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from chat.models import Message, Chat +from .models import EventLog +from .serializers import EventLogSerializer +from rest_framework.response import Response +from django.http import HttpResponse +from rest_framework import status + +class EventLogView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + user = self.request.user + interaction_data = EventLog.objects.filter(user=user).values() + chat_data = Message.objects.filter(user=user).values('id', 'request', 'response', 'chat__title', 'chat__page__label', 'created_at') + + # Convert the data to a pandas DataFrame + interaction_df = pd.DataFrame(list(interaction_data)) + interaction_df['created_at'] = interaction_df['created_at'].astype(str) + + chat_df = pd.DataFrame(list(chat_data)) + chat_df['created_at'] = chat_df['created_at'].astype(str) + + # Create an Excel file in memory + response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + response['Content-Disposition'] = f'attachment; filename={user.username}_data.xlsx' + + with pd.ExcelWriter(response, engine='openpyxl') as writer: + chat_df.to_excel(writer, index=False, sheet_name='ChatData') + interaction_df.to_excel(writer, index=False, sheet_name='InteractionData') + + return response + + def post(self, request, *args, **kwargs): + try: + data = { + 'location': request.data.get('location'), + } + except Exception: + return Response(status=status.HTTP_400_BAD_REQUEST) + serializer = EventLogSerializer(data=data) + if serializer.is_valid(): + serializer.save(user=self.request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/server/pages/admin.py b/server/pages/admin.py index 7363d51..e1f28db 100644 --- a/server/pages/admin.py +++ b/server/pages/admin.py @@ -1,6 +1,45 @@ from django.contrib import admin from .models import Tag, Page +from django.contrib.auth.models import User +import csv +from django.http import HttpResponse +from django.contrib import admin + +def export_as_csv(modeladmin, request, queryset): + meta = modeladmin.model._meta + field_names = [field.name for field in meta.fields] + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) + writer = csv.writer(response) + + writer.writerow(field_names) + for obj in queryset: + row = [] + for field in field_names: + value = getattr(obj, field) + if isinstance(value, User): # Check if the field is a User instance + value = value.id # Replace the value with the user's ID + row.append(value) + writer.writerow(row) + + return response + +export_as_csv.short_description = "Export Selected as CSV" + +class ModelsTagActions(admin.ModelAdmin): + list_display = [field.name for field in Tag._meta.fields] + actions = [export_as_csv] + search_fields = [field.name for field in Tag._meta.fields if field.name != 'id'] + list_filter = [field.name for field in Tag._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')] + +class ModelsPageActions(admin.ModelAdmin): + list_display = [field.name for field in Page._meta.fields] + actions = [export_as_csv] + search_fields = [field.name for field in Page._meta.fields if field.name != 'id'] + list_filter = [field.name for field in Page._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')] + # Register your models here. -admin.site.register(Tag) -admin.site.register(Page) \ No newline at end of file +admin.site.register(Tag, ModelsTagActions) +admin.site.register(Page, ModelsPageActions) \ No newline at end of file diff --git a/server/pages/views.py b/server/pages/views.py index 5d1ed1f..c152ca0 100644 --- a/server/pages/views.py +++ b/server/pages/views.py @@ -111,4 +111,12 @@ def post(self, request, *args, **kwargs): if serializer.is_valid(): serializer.save(user=self.request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, pk, format=None): + try: + tag = Tag.objects.get(id=pk) + except Tag.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + tag.delete() + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt index 19a98aa..287dfcd 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -14,4 +14,6 @@ gunicorn whitenoise exchangelib nltk -scikit-learn \ No newline at end of file +scikit-learn +pandas +openpyxl \ No newline at end of file diff --git a/server/server/settings.py b/server/server/settings.py index 2ca9879..6b516a5 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -69,6 +69,7 @@ 'pages', 'insights', 'deploy_management', + 'interactions', 'rest_framework', 'corsheaders' ] diff --git a/server/server/urls.py b/server/server/urls.py index eb2865b..dcabc43 100644 --- a/server/server/urls.py +++ b/server/server/urls.py @@ -14,4 +14,5 @@ path("api/v1/", include("pages.urls")), path("api/v1/", include("users.urls")), path("api/v1/", include("insights.urls")), + path("api/v1/", include("interactions.urls")), ] From 878254a893f07f35571f3fd17132769d97a3e3c9 Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 16:27:53 +0200 Subject: [PATCH 23/25] Delete word cloud component --- .../word-cloud/word-cloud.component.tsx | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 client/src/components/statistics/dashboard/word-cloud/word-cloud.component.tsx diff --git a/client/src/components/statistics/dashboard/word-cloud/word-cloud.component.tsx b/client/src/components/statistics/dashboard/word-cloud/word-cloud.component.tsx deleted file mode 100644 index 3fe8ec9..0000000 --- a/client/src/components/statistics/dashboard/word-cloud/word-cloud.component.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Grid, Paper, Stack, Typography, useTheme } from "@mui/material"; -import { SingleBarGraphParams } from "../../../../types/statistics/statistics.types"; -import ReactWordcloud from "react-wordcloud"; -import { ResponsiveContainer } from "recharts"; - -function WordCloudGraph({ value, title }: SingleBarGraphParams) { - const theme = useTheme(); - console.log(value); - return ( - - - - - - {title} - - - - - - - - - - - ); -} - -export default WordCloudGraph; From 125bb7e7001ccde442533153ce4ed52cfa16e97a Mon Sep 17 00:00:00 2001 From: David Mang Date: Sun, 1 Sep 2024 16:32:11 +0200 Subject: [PATCH 24/25] Test client pipeline --- client/src/constants/routes.constant.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/constants/routes.constant.ts b/client/src/constants/routes.constant.ts index 159ac6e..9c46be6 100644 --- a/client/src/constants/routes.constant.ts +++ b/client/src/constants/routes.constant.ts @@ -1,2 +1,3 @@ export const LOGIN = "api/v1/token/" +export const REGISTER = "api/v1/token/" From 12b18d23d428fbefe8b12429d5394daa6549980f Mon Sep 17 00:00:00 2001 From: David Mang <117346875+mangdavid@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:59:40 +0200 Subject: [PATCH 25/25] Feature/refactor codebase (#7) * Move all types to the types folder * Comment the api files in the client * Fix download button alignment when sidebar is collapsed * Enhance deleting tags option --------- Co-authored-by: David Mang --- client/index.html | 4 +- client/public/robot.png | Bin 0 -> 13653 bytes client/src/App.tsx | 6 +- client/src/api/auth.api.ts | 11 + client/src/api/insights.api.ts | 17 +- client/src/api/interaction.api.ts | 5 + client/src/api/interceptor.api.ts | 7 +- client/src/api/label.api.ts | 20 +- client/src/api/message.api.ts | 7 + client/src/api/page.api.ts | 20 +- client/src/api/tag.api.ts | 21 +- client/src/api/user.api.ts | 17 ++ .../admin-table/admin-table.component.tsx | 2 +- .../components/chatbot/chatbot.component.tsx | 4 +- .../empty-chat/empty-chat.component.tsx | 0 .../documentation/documentation.component.tsx | 2 +- .../{general => }/footer/footer.component.tsx | 0 .../footer/styles/footer.styles.css | 0 .../anonymous-route.component.tsx | 9 - .../auth-provider/auth-provider.component.tsx | 0 .../creation-dialog.component.tsx | 6 +- .../tag-label-auto-complete.component.tsx | 226 +++++++++++++----- .../color-selector.component.tsx | 2 +- .../tag-label-dialog.component.tsx | 2 +- .../general/snackbar/snackbar.component.tsx | 2 +- .../general/snackbar/types/snackbar.types.ts | 6 - .../layout/page-layout.component.tsx | 4 +- .../page-tree-view.component.tsx | 2 +- .../src/components/pages/pages.component.tsx | 6 +- .../src/components/pages/types/pages.types.ts | 82 ------- .../download-button.component.tsx | 37 ++- .../components/sidebar/sidebar.component.tsx | 8 +- .../statistics/statistics.component.tsx | 4 +- .../admin-route/admin-route.component.tsx | 4 +- .../protected-route/protected-route.route.tsx | 6 +- client/src/services/label.service.ts | 12 +- client/src/services/tags.service.ts | 13 +- client/src/types/chatbot/chatbot.types.ts | 2 +- .../create-dialog}/creation-dialog.types.ts | 6 +- client/src/types/general/general.types.ts | 10 +- client/src/types/page/page.types.ts | 82 +++++++ .../types => types/sidebar}/sidebar.types.ts | 3 + server/chat/urls.py | 3 +- server/chat/views.py | 1 + server/insights/services/arrayMerge.py | 1 + server/pages/urls.py | 3 +- server/pages/views.py | 1 + 47 files changed, 473 insertions(+), 213 deletions(-) create mode 100644 client/public/robot.png rename client/src/components/{general => chatbot}/empty-chat/empty-chat.component.tsx (100%) rename client/src/components/{general => }/footer/footer.component.tsx (100%) rename client/src/components/{general => }/footer/styles/footer.styles.css (100%) delete mode 100644 client/src/components/general/anonymous-route/anonymous-route.component.tsx delete mode 100644 client/src/components/general/auth-provider/auth-provider.component.tsx delete mode 100644 client/src/components/general/snackbar/types/snackbar.types.ts delete mode 100644 client/src/components/pages/types/pages.types.ts rename client/src/{components/general => routes}/admin-route/admin-route.component.tsx (79%) rename client/src/{components/general => routes}/protected-route/protected-route.route.tsx (88%) rename client/src/{components/general/create-dialog/types => types/create-dialog}/creation-dialog.types.ts (87%) rename client/src/{components/sidebar/types => types/sidebar}/sidebar.types.ts (80%) diff --git a/client/index.html b/client/index.html index 76db3ab..d2d0931 100644 --- a/client/index.html +++ b/client/index.html @@ -8,9 +8,9 @@ rel="stylesheet" /> - + - MA Mang + Thaii
diff --git a/client/public/robot.png b/client/public/robot.png new file mode 100644 index 0000000000000000000000000000000000000000..5071dbc5358d5633ba65c06c52d2bf08e4c976d0 GIT binary patch literal 13653 zcmeIZ_gm9J(=VJ5ARxUcNEZT#G?CsB5C~mBgLFhdq)QhFK_gXqldeKQlq$W)h@eyn zAfbmYJ@gK5^nT9$yuY0P;N(iKZ?ZeHGyB;#J2QzkHq@rM!gd7!0MI~nG)({i65>}9 z00lYmu@^XeLVS?=s6)*th_4WeCsD-TlwLZId;kF2j=vvZnlLRpvGBU@J#$}EPbc31 z`{#~;fPero7Y|n-2YW9^G0*4D*_%pi000jFs(IHeFlS>b;LPz}C}Dd*W>u`R2DOSM zb%_ypMtYCzs&v$qDD%o)b2X&-4#bFAFQl|1T{EuYdlc~-i5a=)#^1KhAj8WOZp6Ck|aZ*|L^^OL%>93NEe*= zLkYivPC>LHtP$=A1Mp?2gqN%r6TuZPh$chlHiFV1wJ{souj@z+0b*pG+By!%mm$0O zW(f4$?(}{MCt?V3@6AK66x2O*7h*$lNh3KmV?!I|L?Q^J(t&xo7JWyTpvi;2K%m{| zgM!Dw0M&xVmzI*}%s6GeehAdy%lStf1NwYHxFygJY(M`ry{bfc@o33E8b$YhF;~^t|Wi*>E<@TLm zN$~e+mMoK*yNE^AKNKx|aK2>r3ek`fYd6(fhqT=+*c0r}xJ8>}saq2XF&k>*Bbiyc zREP~W1#oiV>=^uw-HR2#!)+O^@sRg@OMv23TJ_{TRU&U4zu(@p09M`3gU?w#S&Dih zfuVMh60KKKa42IhLrBKn)k$_!eHL(iU*S9+n?jL-_p==Mj59UeTRKEmMx?I^x zyb&LC)8)$ccfLjQ-I81+7bJ$(2Kd;&2D^&t6QzYwWL~xsviPl`5Yj2g*1?WVGDY zlM4sO;AWc%9s3^+;^hw7lDgUy=Wxk|a&_Ekv7Gu6#2-Gq39XR-DPtHXT;Y+4aCxml zwILUlJPfPjmIebv;?K_d8S+Cl7pfc_G9YpN2Yl-63V^DuyLnf)$DFmfPWyih=p}2h zOu##8fOXuIIU19}smVif{cN)h$%RsGs=PI`l<38z{26fIJVm}@cmu;bdhTY3i?7gl zf@~Z?ej-6LPs2+5SmE3BBa*FXycH*`0uDftNcKs8_5Q zXUhMS{y{Vj55_iwa39|L@Icw}HnhRR1hK9F+2mmrB9z(WIY!6o7M2Rdjiq|Yyg0Zo z`T)&+)7IRz$TB>at+yT`6i;N~0Em1Dd2dko9i1`L8(!xBvt9;)rzGuyVY5?Llw>q6Akea@p|kzAGG`XK>>Q3U`xLFZWjUX}MecFK?fG+O-+qXrso$34yHRC1 zzkU%rmC7!jjVtT7brc!cE@*xq%wp1k^-Sa%Q!+=pTKM``zLJ$6%)GD=qHr_LGqnmT z*mlDZ#c8oMsV-XefC4o~}$?Cv>?GTYH-o@W*2L-Em@C$3iCk!om8m)q! z2fK*h;!+w$OoZptVx^Jy$C>zQQAK-A=N}c0!fjaISvU(2Sn)h~HN2e_^Aoc}PzHzv zq{>#ve~-^(^nb}>CP)Hrz5g0^hv1BC?9}z7^dj@3d~fP?<9!NG+^5kE?UAo^drg3} zDQXPcOe8JV)Me{kOSvWG8Ht>8q8Kl=W@^#@kLr5}ULIYID6T#kR;cYdM+c!Fp)-+= zwkW?9ztj7E1W>D(nzR!PqcW|{#I+HeJG=}-%OZv-Us0i?_y4PU@_6JgJhuibs$#Pt z`WM3yxjpq)^D(bs##Rs2PqgrNqhueSo`Na>@nrrY9I-nMuPYtbUgP=C%l^VUF zz4*A^$E<#XGKBt!Y31Nj#!qpIE+n1sLTBn)aM{P(RK~932!iXS8%DetOA}Kv?MvpR zR4K9g0k>B0%sMcH!IHvS4t&L*+z?=6fD?e}E{fr8!^0rdyk@fhi#?+9;9`Pe=&-3N zW%^S(AOH{m3WKyvn@djhY4j|JExo_Gv&nhZ=tkJO@h0J|d7sxNet?Gjk_&^;o&Z>u zCD++3ZcWhF2~FR;e=+VRZ%_WU%ZvOj%YOyf4RDgSFa>f=-RfFIb5kfTOp#AEXfs)PO3L2~X`(p1+r#dyLmW2dtpy@p-HNXQz4S7%2T zG@J4&`$IE&0SABsKBcNL&qRNr>t?*|0F^6ef>Xlv*x|!OLSrdU zN5dY|Up_1`(vR%Bv0f8+%l>%!^rmgzw?cLbNw5<{MO=4)xVc@klQj?&88~(AhW#;x z#!|$MScVJ!O86Sp*h`W}k)t0t)An{}Q8{f-7_;_?E#GPHHBp)fdNYW2b~|w8N=WF+ zC~^=#Kq}+5p#Ss^J42n*1$Z^^jxRr9U8^7Yn0T`EldQ*gF<#pevbL`f6(wn-X#u{S z=@t4#;SZdq2#GY-LRF;HqN*uCJR@AheX<&`R?la@GIBMwqHt87j*)AF8{F%$N_sOh z4)HBK40O%p;^dKJ8EIQQ=a0I|^N%@X<`wd2Fa>)>YWdqyx4m{K>npng+lnz{Cf^LN zNS@hAlZCV>`E)vOe?ke*Mc0tXfda`YAH6z$cs-jbZ!QOVbk*kMMYc{D zAl~ivjD_q(LiZzt-&n3OWkz|RAhrxSMY1gW*15Ka7I_uo`QdC|HmRSlH2q@>(N1s% z{K#aQ2EbqkcTcf7klyxX~z3wy(g2o~wwL9Xh*=-|YBgib$)QNG9etEHKY z#SuK(78pBh!F4VQ14;S3M^mPu0bAA+?3#M%#AN-=(41&+ZAle!zF zH|{^?@5x38%I1IX|EG<4rxYHdu8~tcXQ}n{D8Nl67Mr@R8F(cvI4*V(Dahaz^52Bg%#Z03-=t-Vx8 znn!Y$+H&|~3K?wrjc6_AkiG4a!knzgFKf<_bI|qvNK_RpA)(keoePrN$oL!{H{kz>MTu(m?}oCfrR}ghj{S z-3UM22kb8%i&;>&vni*BDpx5J__A692HuC6`ndmHWQKiC$ir=ehdlXYPK(`%!;+Ge zTkF{^kztzc*QkltiyX2Bcs5A2s8M(RE~X8boE0Ea8MU;z+_N&^@$z3MN8PFLf>N+Y zXV-AzmL3qGQbgsvt?A+ZRdo8;b5o)XM%>m}JYOm=yu9VubznoB&_WB-i4 zz?)?Bk@xe5Dp!#N!aGBx-VbbO3+~kdxQ!&&!t=E+{Yg`rh?Di^#qj=j5?F=fZ5Iv6m1|GkPkzIF4fC&fDG-#gGcDHEi+b2^?j%-flB&E;3q4wlq0dwmN{q;k z?6EU786-Z>ozzXdvw;~&Z7M-w{rZq!#Rml`CngB(WO#Lg@XCRqZSrpqD!g-HnQ142 zWw}?gu&stWcO;S|gnC8$%vRg{rlv`lJJnfCjbD$tU3*Su)UU{cf}tiGge6Ee>43bI z0Vm-s0IB`mUpS+Pf3X_agxfzT*ln^xNJ85YUCPAUO5w=P}TOYEo-F?`X!cFTBvAHL}KUfU_n5?aeZgV6$QYG!#77wYEhmo(AYW z`z?jnD=d4}&p+($K2&>95YY4x5vDiJzC~g+2g7q<=xE(kpS15E@rKOzN!J{@`;wx>Vl0U5rREoJKO4dOJX7r0)RW=Q$))(sCaDz3_8} zN1?S|LICRgPgyxLei<5&5;q?e26bt9Q{y+}yc^MOXzil<;xiT5ksH>qRbJHdC|O~1 zsBnW+N!Ihob>#Iy!RRw_bQxj-8sI=&5Q);@PQ>MH>3Z%D z(7d?fb1tQ2Z1Kl$oe1Xzn#-_Z9Q^6k+N<~o zv?uSJwPY)V*h5nEVBzPkax$52b`{MISMWRjXTs=1?at^VsHhZg> zu2*m(u6-;49FnF(qJnZN3zwXTJt!P;qn*^uQg9a{{ArnmjliEW)-vOzu%d-kB*kaY zXOXWEX@w;d{XHDPq8pLFKctg{Kwp{*V!Q5iibB_RDX}A-+D>np(+@OU?`r@(s3&uH zes6z0?Coc+hZuYpK+_bK%zHp)5NNjXgihkMPR*z#PRew@Gkg>j%NPRdbE5BG&O_V0 zQe9@6*7i`)@d1Z5iNAa-!siwlyoDdQhbB?iKC{5Q$#V^#Vc>doI08abq7NBEM#a!S z(U%4U>(z$3L%ct)8Jp0HPTO93Pxd~6b*_W^_$yeNLa9s>6hC?qtxYld%r+m3%DP%f zk5kcTsq+3*S;u1ciG7Y1v-35yN|wNd*^Cex<&w>OUbu*KJyc%c9<#yzMlOb$=6n-( zijtMly83&GB!qG0;hAF5toei;J6;rNYl0H?N)d8wqLxeYiVK+Pf%^16Bng}LDy@?E zR4GFg;1X6W%i-(-2F(bxc#2xz&(=36azxitG_U%NWfscY8W>T%i8Fd#TMNif;i>b+ z%&|7hvN}V-z-8k$6PZliLT+Fr({GBEtSKGYhs(dn=ikH`ysoVR6f=;2Ml~EY%P}61 z6)LI z$%|y^ZQZPHmAaE1b(2cOF>z7Jcz<0!(8qM!poAZ$loa`b`r1zW9mxrj*qjuY6p>p0 zR;dA*T&z^w6swor9PW8G6n7#wB5*^%bOx)74@GL#IDu1^LadBFI+(`9L0{LF0BqBE z^1SggOkHUcN_e+!*gFAJ7VK+u{P4zFp*)MY5eLjo=~k&InH=ZQgO`En$xbr}lRTQq zd&jy%PgYHuQV0rV51W&nauYs775R8%xUT+ACsDd{S7aVt%+#4W;oaa?@pIVB;x4Nh z?DIhKl2T#Lo$KteS6uagMdob>cQH$Mr4^!md$_Xoi+&EV+G`oa`3v;vztdytWd2L0 z0aOvb%Q263iJtrE!bY&~hP@Fm&BqFs_@w_T)=9Z67K{;;iT~thPuY!A*YT$92j^Wo z=8UE6ah|@;X`nKM5I6Q{ec2qIzx%6geUzbp=F@4txG-~elRe1J)Q)(S3_=$WU8u_8 zpquqy2Qm;aQp}!?o2aDt4N>SblJIRAgxOEP-QOh1;VB8Qd5<9X8*c0Xw%)i zf?<7C_GLkzZE6YmwbD7oQ@>ZSdO;+&ES#)k`81aDG12_EY4Vd~GXA!^xJ<;MR%g(jR9mLwq&1PjeigPX{tEd%jzWv#%g#wm34`CH)V%h@O3chC8$2vd~t?Qeham4B~x z@&7afLUv`8jIDSiy!WMtBHa(xi0c$kv*mEZF?@fSu#(JenPDFRwEwlp@M=*b-wkEZF zA4CK?uK9gGIBR0l^jS6wWBIYbCW`8#^Al)2bIT4ucd;f!#d_ChytQX^sBN)610{4J=jy6V&@+WDWmVo zOWvr(nNm=}P%T~Mg7B(bD=~hRc5d!%98pR}GeDN7RE;Hgl22RCpK^=O{ktza;ccmW z;JWWh*ys_LP+d4dx^ul{sJAv$m4qSS8JZi~P!FX=~%*335n>x4@#ihU=O7_P!Q$!~`1`&;euCStqIFQw83OgIRFG;1ZuToKdBR~e4+p=Lrwq-_pA9L9<8 z^pmv6eBI7dBoZsUk-9n{XV1h9{igWZ_s~A3%qROGqhom&romM`%83x`{TeTdEXjKX zLge4$T02B8s=>72MniC&EZdD4Ug}<#)3`fyfeHi&MrEs+Ugr9kFJXp-$q7D5dd7A{ zY5%=e(Fgnq>Q{SK!~nm_oriCR>jZxKKDu0brkaZL#Wa`UxF%yM zdM3+_Vx~S%bEeHJ(F*j5$Ik$LB)e4Jh=wz z;u@xYN;S4+*v71M^?Y<#+pdlpSaTRX>UTjlo&P4CX|3A>V_V7x+3G-TcYVm}olb!E zDw{~=giG5bLWtC&C~-J|u?okLFv8O0Xo3dFPzmie5 z$2$j$c&4Wyv)$;T9y3^<|C4Kfyfywdw%K4Ve2!Lt28%;XgIbq22}xl^&Sa$LjPhLCt*vtz=G=i0^x8S_~#@*BZ9lTPU~;~Rz{ zU4J6S3b>}73a{_}im0Iov>G28vQ|ulTlOZethpE&@h#D-o?O11yj$^oGd0M$rL6{0 z+5F*m-;QNFH)BSIMug$Nl(HAJTeeCwl0sCly zQ&h#R>1Bzi!U!yVX1FEqsD&e4d3ClGx#O63CpT6YTt4KTDqfiC!mwsz^-)ws-AaY~ z9cMMiwBhj`TU&+3s>#uTo$M^ps4%6A_v`oRJZ0W{2w5))4fQ1Fy{8>Zu9OK_A<@lj z%iW`v7W7|k-&&wMw)cNGcJHwXiVW!|TBFe8Dd$X|k-%02QV&s?NZ;h~#ap~LaAj== z;p%+pqjMiAAcWom*V&Ibmr)cD?8HY0p?-O!_4Gf^Wi2?|cKpF*g#nGVs9@)kp)=(2N15iw9d?_T#h(*(kC&hL7l#BD z8wQZD7b!stp6SsMSXa#pUa+>4dx&v%Ft1-fK5h>GY(mO7iyGcu^TxPg0D@%cGXyx) zyi7`Ky*2;azEaI6TbLUahqVI5QzJ0`2sz^?O?ZgmD<6ARBW_RVm!?dpR+$+HEM=`q zLl024jwi?K9a$3>K&r&3*kWTtgz-ab@pcJJD@#EHEnuNc&&OKzQg}Atfx9GDbwW^0X&k{ySF@bCc5Lh?YC=-0QRAEU$n`n(RM8>F}Q{=p=_IHj$pqUOxqXV zp_K0xKFs5PaZwBXz&e$}7@}RWCU|eXR?nOEBF0$2bR=yjw)Jql(!a}0iNXqpKg?)? zo79AkC`LBe3_mD6t?C=^)4k zTrk16vr8n!;hNDq>{FN@1EYJiC)zZjoo(Sqr)d|d#Owj)&cC?;QET@$IK9_H z`4W^LS?HS&wOk8>h_zcT5@D{|&S-&e#>@G`%;5zSSxxiYoe@&iigrKOmZzf}0y>}1 z89we6$H^yk<%!hVKWMSM;;YHJe) zE(w{7Tt1c4s2>ipgTS)5McF1?aE0&Lx1FvBkNp8yHW1uOA?n7+G%zYO;hm% zi_SjbD*t0JPuV#9T;oit4I5n0B^L38j?l@V5eq}vmRvTFXvv)*c} z9_Q!g`j@903w(oLBJykktklkeztT{weaQcDA`1%Y!C%I4epNJ#YTXj!X*+Xbc(`b3 zd)!BA_(5@zC3tHphc~KM*HgK?Xx+vm4qIFMS>5@_6#FT@^MfOmA1!n}xq^Za6MQHVF=8Ck>8VJI#es^|GP>7pJD(7%WQ-Sa6=A+G6u=PwGCYbV$ z<#P$*!8DbQ{Q8EU?d5$e$=~4o2SehvLCd4Uz3T(c{Sg3#rB~^4tHwA0uqA$8-F+S6 zlqY+n-W?6#Bdb}~i0y&Ye2r-VB1^>e8ih=7_)IZHA%d;T!bMc~@SVv%B*BngPY6bz zaX7Rad3Jk(sYp8Q=$@Q@6ZhNEU-BO_7iDuKqk6g=k2MUJ%&}?CrP!{7IVObfvvJE{ zgJo^_g)s)7lk5SWwv>w~31sndUg5F$v6J1@7ag|xaH7cZ^A4q2Wa{Q!)Sr7|jx{4c z#(DjD@N)L|>ab2gRn%m~x018fn5nZ*JtFNT^2B3fSOGwf@ndX`UDu3xaB?&v97ZuZ@3*ioH7wt$>H-e;_(+>aeFlCzo!)nT{Ko;xaNdSbtUTgZvf_a z6%_Aspse*L-UQTR9w1c98 zH>Y0d4#u8klX|BfJe?c(W-@nZZnPrTeUWShY-!uJ^3PbTZ_1@SGEI%RUjr zrUv~Fpsc}XV4?QwhPpGneR2Dk$#A1VC5#P02$pL}5yFa-aZow=l&~1U?6Py*M`V4f z;F1^B@jv=jbyfJWplgF}x!lU0F@=9i^~I=SF#|HJ&?AzN&ZZPBy8Y z6}%&q>EJouH#`Gp9*M9muM)f=Ls!++&Jn1-5Kz;1udqa<%^;A`_)Oh4!JR_yvr6=B z^%pQq0apIiWm{t9{%Rb$f*2gM1x)5D8XJ82yeLa^L`%q^Y+K{S<4I?R1Ym%KgtK_; zhBo^+{sVC>JWjZ5C6%b@voAdUEXkz~>AW`q* zOyZhsbPu_dbqy5X9xXNsn!t)K%2q!A)pk)Av3+5#fQV=jA3StZ3w+aP`(cad8xF&) zRX>Vu6CY`vl(8-guC4;nSA1rf4M0dTg^pfBmW1yzh2{@GPz25JyB%j^9=+OH`=FXe z`_A7VFz0v7s}xP3P<_owkuA_?v=?eJ+6(UI^CdAcCuxx%45+;i3kz{{0Q1;nh+}3L zm$Zw&DB6$zc3^@R50jnJ`>*IV7A!>~)|lY76TZh?FwayjO@`nz4aSaNPV6ArM5o(W z&8CdXX#^yYZ8lFQD8ZV}EmeY!Un%8;=z=9KLWhKzT4{iQ&&|r@nybTi@D*NMr6;az z1Q~4HLB5V~xHHkEE@brK;-KJ=xP{^7RR!)+O9!gD2$w|rD%jUbmKe!^FJhfGOAd*I z(1e2j5^m-4M6mVpl;-``e7z6G|8SE}>Q;#H?Kv&a>=&H;99l`Y|JN6KLfXj{R!=H1 z{Fuv2vCqtE9^HsUS)s(n<0_v&H9Z{2MUKgYR)POZt2&aJYoWwsPK;pSUZDk7W`Z6B zY*2^9NHvd^iHZuo8CgluHV!)-3`QB*t#3~>y?&Zv)Av-dk|rgcB%g1i=B7}wG9v7T zRgb~6umf6{7+Nw|y8mB#(=yzHY+{56RrjpvW#!Gm-;bUP#CiDO`D_L4atJT*)OcphV|B47C~Of3OkY%!ceg>yXPT*4I{*9caU>D( z597DNiU*L?>46{t`)PM>15leWsW0{xWhcd`D5BEfjx-BTkJYAk0=Se6o3}j&>XU{WLNKE&pOVS}$%FCY5Zj$6_1j5FafypLsyfJp|8sQUH zy0BWmTa(rS9lN|ale(DZZfB5eD$Tu5{WQpkOUPVtlV6&jCxoaQ*WcC^GYGnPWn}Fy z{<5j(n40m>@LggH{{N>1t9CG@5$J%dUxYno*kT!Y3JQzMT`a+&d;zu9AbQVHEYNI$ z9N&m$Krg;_NE)BJgZIMIV`9>?vW`(8;})!|P#4!_C#l5V$X)cxg0K>2vQ99t zi@r!*nc?k&+hQ)}J(O`5^5{{*6#SXdO-?nI26nqF8(bJrTkwd>z{6BM;qp5AD>4bo zelX8q7U(&SP{(4As$W7A9?d+nc=rOmf6$OZleZH8z9&Ls^ z=5^>>MybfkfKv{PB4(`NQOcYM=o;`4NCghFrI;kO(mQet<0L3f=89*ypgGZX(QU$7 zD+ixnpl03|zUZoly)=hF*XmSPRyT-!=Kk&nzy=wv03an`T47E#7!Ih@MT>w)574Ud zx(8kwpK$7a>bl(%zJUm449$nQ`1w$7U7E&i5S@R_?yo{g68l9kjP=QdP8_d6!6{Xs z#NQGqP+}o%b-ck37{EpQaXF8hv9~K`D;4t95Z#AKas%C&}I06hGv9l?Qd~xk7;Xh z1>e~XUzR1b5|aV7f9Z!>e>snGKmAbq9^~b@u$=$kPxaakj3%@KvOhS%b~2s3q3t#? zO6kma$h!DUD_C3#)F7~fThldSyCDF@uF@7ZzG!-K>fK4vwULv+WE&;;#%nT#kt4u* zC77=R0=1ZWBxMQ9ms8g#CI$`>?}$A5#BeHdkebS5=Y_sDYd*kTh~)3;L~b7e`VCpYpmHxV3R37H209JdvwBp zctf|Qsf`)BX?xoMbrlUjQJykudUHF?c&(D1m`zCaxx^Pd>lXq zi~|wq+UT}m>Og~-6bLl!h5QQq&v|+!;uq4{_yCH&g5MW1G|A^Q%#;Au-#(r^Uxp|} zBt$^=AFRS!BMqLv>0(&jncV0KHLn+}X7`dmzOt2#ez+F3yhfQ&%NjtFIxG%mTx~Pk z{}Wa|Z*B>iF$^r-NA-B2ZJ$e0W=n~@Ykb+9Mi{pbtrOUY8c_wFQith6N)%4%S6fo| z51QbWKz)$Sy=qCCP4IM#={cJ;-4TTgjHV^_Hrn%m-kU>f{BBIWouHaY{f5T4_KpL( zIzsNk4fF0>GgD_J{n72p4P$5Z`%5IAB=Fv+G~0LZ9O@Nbd|JCJ4Drq7dx9aHJSEw* zkPY!mZ+r@0=NATh`h!Mfon2~t7eAW4B+wz>1qIT1!j3U(6=aY}#-$P7!fD^X+vFxX z89!5grpZ%23XqVw9x`f^G*kTvjBr3~37i>1Z>@M?f=>(>;mZ{WRleTygeo$xn}k3+ zRXaM&wasj%->#1+&KMdPj-&}p#BI-%bbm6hFdtP(5@_A8uVbPS?@(hqwnog6ZuN0e zx5#Emh7W#_z%-D;s8h`ft|JdO42I*hpY%r7*boER_f(SlzqNuOBaZC&Qz=uyyex5c z?65w#|6L~4g+J!g!{lNANcz=MfmO`VcF9MJXT$5*qhCb6jPD&0G@e=QwVxC6^m1r+@Cql~Rp5g_v%H}tL2Low0giu`! z*Ak!ZkY5Ljk6{DL1XNk$5YPr1x`(}v2C^|18duX2=ydL(pp>G3U7xRny*F=hk4}t4u5pHye%r~~HO!UICeG`I)zu&YJy}o~czrnJ0OI$< z5Zz4Sr_-;nCX2i~Y_jZjFVHrBH-b&Dw?~;OP_)vKn7wOOP(~4)m}B7Mb3B^XUoe-u zUs!?z95EhymPgmTsU%yPpqyud?2ld`o9u?&y-iVJ{KIi3%Lbr}Yv#~Kb`Rbu$}?H7 zEA|3kmxvkgCi0f_T7-O(kd%;DDWho*0bx^5ifa9dx&Oa>B1vq4OKG%EXxjPKgF}aS z&clr#ow+1(@k};Q>5<)B3F`$qF{bm*JWE^xDuSA+vD0WiU9yI2^&oLUr4ty?AJ9E< z?dT4n6dSH%BCre+C)(L&QS3db^{hv9nEA?AUaNV9n-+J?JJ0ryNUP-X0X01i6tC>L zU<&k5-St~|Y4lYT=suna;oq3@3mAyFZSL$e<^>TNe~vFeS0T4GpHUKdMpe+xUU6a2 z{gWF6ay$$L8vJ(L*4SEpLy0U<#Y9OA;-4z{7>coP@qdQAF`wi+P|EM{EV(KTV8 zq6(onlB=xo60x9D1o(|zk-Hu!=}!u&?P^6ABa8rt%<=b3iGeS}YRa_}q6_rBmq_x- zng>3gqWx$~m?JiVSm%)qs1Tt3GFJb$ZeAC|vnWnNmrir9;0x#%X974LY@-{f&I1(^ z1sZ}?z&WOJ*ZA$g?}BB(@xXZUya-oH9X6tY^`-TaEQH3~ScU^dY;$6XBoY)xsl2Gt>-~ zKj6gG0UwP9B7w+LraJ1W!Y9VbZ2JIv`#fPv*WF8whE&35TV)5Z+UG3b`jvH{8{N9* zTc-w4C1>Szd;PCYuPy_qS|SRQJ1K{@h#6$H;@oFJf?g9mQ|dVXLj}B+U8zpt|Lvb6 j|F8Tn0xvc$K@5%SQSBPy--v&K0zmH>YL=_pz5M?G1Epy9 literal 0 HcmV?d00001 diff --git a/client/src/App.tsx b/client/src/App.tsx index 6b9b9ac..8205c64 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,22 +4,22 @@ import PageLayout from "./components/layout/page-layout.component"; import Chatbot from "./components/chatbot/chatbot.component"; import Pages from "./components/pages/pages.component"; import Login from "./components/login/login.component"; -import ProtectedRoute from "./components/general/protected-route/protected-route.route"; +import ProtectedRoute from "./routes/protected-route/protected-route.route"; import Register from "./components/register/register.component"; import Activation from "./components/register/activation/activation.component"; import AdminTable from "./components/admin-table/admin-table.component"; import Statistics from "./components/statistics/statistics.component"; import NotFound from "./components/not-found/not-found.component"; import InactivityLogout from "./components/general/inactivity-logout/inactivity-logout.component"; -import AdminRoute from "./components/general/admin-route/admin-route.component"; import Documentation from "./components/documentation/documentation.component"; import { useAuthStore, useToolStore } from "./states/global.store"; import { getPagesForInsights } from "./services/pages.service"; import { getLabels } from "./services/label.service"; import { getTags } from "./services/tags.service"; -import { PageDTO, TagDTO } from "./components/pages/types/pages.types"; import { LabelDTO } from "./types/chatbot/chatbot.types"; import { QueryClient, QueryClientProvider } from "react-query"; +import AdminRoute from "./routes/admin-route/admin-route.component"; +import { PageDTO, TagDTO } from "./types/page/page.types"; function App() { const queryClient = new QueryClient() diff --git a/client/src/api/auth.api.ts b/client/src/api/auth.api.ts index c754f79..4119a02 100644 --- a/client/src/api/auth.api.ts +++ b/client/src/api/auth.api.ts @@ -5,6 +5,9 @@ const apiRefresh = axios.create({ baseURL: import.meta.env.VITE_API_URL as string, }); +// Authenticate a registered user into the system +// @user: username: string, password: string +// @response: token and refresh token export const loginUser = async (user: UserBody) => { try { const response = await apiRefresh.post("api/v1/token/", user); @@ -14,6 +17,9 @@ export const loginUser = async (user: UserBody) => { } }; +// Refresh expired token with the provided refresh token +// @refresh: JWT to refresh token +// @response: JWT to authenticate user export const refreshToken = async (refresh: string) => { try { const response = await apiRefresh.post("api/v1/token/refresh/", { @@ -25,6 +31,9 @@ export const refreshToken = async (refresh: string) => { } }; +// Register a new user with a username and a password. The username (email) must be whitelisted before registering +// @user: username: string, password: string +// @response: status 201 if created and an email is sent to the email export const registerUser = async (user: UserBody) => { try { const response = await apiRefresh.post("api/v1/user/register/", user); @@ -34,6 +43,8 @@ export const registerUser = async (user: UserBody) => { } }; +// Activate account by by clicking on a link +// @activation: uique query parameters to activate account of users export const activateUser = async (activation: ActivationBody) => { try { const response = await apiRefresh.post("api/v1/user/activate/", activation); diff --git a/client/src/api/insights.api.ts b/client/src/api/insights.api.ts index 07ad278..7108710 100644 --- a/client/src/api/insights.api.ts +++ b/client/src/api/insights.api.ts @@ -7,7 +7,7 @@ import api from "./interceptor.api"; // - labels: Labels included in analysis // - tags: Tags included in analysis -// Fetch number of total chats +// Fetches the total number of chats based on the provided filter criteria export const fetchTotalChats = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-chats/`, filter); @@ -17,7 +17,7 @@ export const fetchTotalChats = async (filter: FilterBody) => { } }; -// Fetch number of total messages +// Fetches the total number of messages based on the provided filter criteria export const fetchTotalMessages = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-messages/`, filter); @@ -27,6 +27,7 @@ export const fetchTotalMessages = async (filter: FilterBody) => { } }; +// Fetches chats and messages statistics by a specific time unit export const fetchChatsMessagesByTime = async ( filter: FilterBody, item: number @@ -42,6 +43,7 @@ export const fetchChatsMessagesByTime = async ( } }; +// Fetches chats and messages statistics by a specific item export const fetchChatsMessagesByItem = async ( filter: FilterBody, item: number @@ -57,6 +59,7 @@ export const fetchChatsMessagesByItem = async ( } }; +// Fetches the duration of conversations based on the provided filter criteria export const fetchConversationDuration = async (filter: FilterBody) => { try { const response = await api.post( @@ -69,6 +72,7 @@ export const fetchConversationDuration = async (filter: FilterBody) => { } }; +// Fetches conversation duration by specific items export const fetchConversationDurationByItem = async ( filter: FilterBody, item: number @@ -84,6 +88,7 @@ export const fetchConversationDurationByItem = async ( } }; +// Fetches the total emission statistics based on the filter criteria export const fetchTotalEmission = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-emission/`, filter); @@ -93,6 +98,7 @@ export const fetchTotalEmission = async (filter: FilterBody) => { } }; +// Fetches total water usage statistics based on the filter criteria export const fetchTotalWater = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-water/`, filter); @@ -102,6 +108,7 @@ export const fetchTotalWater = async (filter: FilterBody) => { } }; +// Fetches the total cost statistics based on the filter criteria export const fetchTotalCost = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-cost/`, filter); @@ -111,6 +118,7 @@ export const fetchTotalCost = async (filter: FilterBody) => { } }; +// Fetches tradeoff indicators by a time unit export const fetchTradeoffIndicatorsByTime = async ( filter: FilterBody, item: number @@ -126,6 +134,7 @@ export const fetchTradeoffIndicatorsByTime = async ( } }; +// Fetches tradeoff indicators by a specific item export const fetchTradeoffIndicatorsByItem = async ( filter: FilterBody, item: number @@ -141,6 +150,7 @@ export const fetchTradeoffIndicatorsByItem = async ( } }; +// Fetches keywords based on the provided filter criteria export const fetchKeywords = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/keywords/`, filter); @@ -150,6 +160,7 @@ export const fetchKeywords = async (filter: FilterBody) => { } }; +// Fetches common nouns from the API based on the filter criteria export const fetchCommonNouns = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/common-nouns/`, filter); @@ -159,6 +170,7 @@ export const fetchCommonNouns = async (filter: FilterBody) => { } }; +// Fetches common verbs from the API based on the filter criteria export const fetchCommonVerbs = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/common-verbs/`, filter); @@ -168,6 +180,7 @@ export const fetchCommonVerbs = async (filter: FilterBody) => { } }; +// Fetches common adjectives from the API based on the filter criteria export const fetchCommonAdjectives = async (filter: FilterBody) => { try { const response = await api.post( diff --git a/client/src/api/interaction.api.ts b/client/src/api/interaction.api.ts index e7eed90..69e4c71 100644 --- a/client/src/api/interaction.api.ts +++ b/client/src/api/interaction.api.ts @@ -1,6 +1,9 @@ import { EventLogBody } from "../types/interaction/interaction.types"; import api from "./interceptor.api"; +// Creates a new event log by sending a POST request with the event log data +// @param eventlog - An object of type EventLogBody containing event log details +// @returns The response from the API if the event log is created successfully export const createEventLog = async (eventlog: EventLogBody) => { try { const response = await api.post("/api/v1/event-logs/", eventlog); @@ -11,6 +14,8 @@ export const createEventLog = async (eventlog: EventLogBody) => { } }; +// Fetches all event logs from the API as a blob (binary large object) +// @returns The response from the API containing the event logs in binary format export const fetchEventLogs = async () => { try { const response = await api.get(`/api/v1/event-logs/`, { diff --git a/client/src/api/interceptor.api.ts b/client/src/api/interceptor.api.ts index 0ebf928..51c1107 100644 --- a/client/src/api/interceptor.api.ts +++ b/client/src/api/interceptor.api.ts @@ -3,18 +3,23 @@ import { ACCESS_TOKEN } from "../helpers/auth.helpers"; import { isTokenExpired } from "../helpers/token.helpers"; import { refreshToken } from "./auth.api"; +// Creates an axios instance with the base URL set to the environment variable const api = axios.create({ baseURL: import.meta.env.VITE_API_URL as string, }); +// Interceptor for handling requests, particularly for token management api.interceptors.request.use( async (config) => { + // Retrieve the access token from local storage const token = localStorage.getItem(ACCESS_TOKEN); + // If a token exists, attach it to the request's Authorization header if (token) { config.headers.Authorization = `Bearer ${token}`; - + // Check if the token is expired if (isTokenExpired(token)) { const newToken = await refreshToken(token); + // Save the new token to local storage and update the Authorization header if (newToken) { localStorage.setItem(ACCESS_TOKEN, String(newToken)); config.headers.Authorization = `Bearer ${newToken}`; diff --git a/client/src/api/label.api.ts b/client/src/api/label.api.ts index af70b4a..3912cb2 100644 --- a/client/src/api/label.api.ts +++ b/client/src/api/label.api.ts @@ -1,6 +1,8 @@ import { LabelBody } from "../types/chatbot/chatbot.types"; import api from "./interceptor.api"; +// Fetches all labels from the API +// @returns The response from the API containing the list of labels export const fetchLabels = async () => { try { const response = await api.get(`/api/v1/labels/`); @@ -11,12 +13,28 @@ export const fetchLabels = async () => { } }; +// Creates a new label by sending a POST request with the label data +// @param label - An object of type LabelBody containing label details +// @returns The response from the API if the label is created successfully export const createLabel = async (label: LabelBody) => { try { const response = await api.post("/api/v1/labels/", label); return response; } catch (error) { - console.error("Error creating user:", error); + console.error("Error creating label:", error); throw error; } }; + +// Delete existing label of user +// @id: id of label to delete +export const deleteLabel = async (id: number) => { + try { + const response = await api.delete(`/api/v1/labels/${id}/`); + return response; + } catch (error) { + console.error("Error deleting label:", error); + throw error; + } +}; + diff --git a/client/src/api/message.api.ts b/client/src/api/message.api.ts index d2ed7b7..6c44f52 100644 --- a/client/src/api/message.api.ts +++ b/client/src/api/message.api.ts @@ -1,6 +1,9 @@ import { MessageBody } from "../types/chatbot/chatbot.types"; import api from "./interceptor.api"; +// Fetches all messages associated with a specific chat by its ID +// @param chatId - The unique identifier of the chat for which messages are to be fetched +// @returns The response from the API containing the list of messages for the specified chat export const fetchMessagesByChatId = async (chatId: number) => { try { const response = await api.get(`/api/v1/messages/${chatId}/`); @@ -10,6 +13,10 @@ export const fetchMessagesByChatId = async (chatId: number) => { } }; +// Creates a new message in a specific chat by sending a POST request with the message data +// @param chatId - The unique identifier of the chat where the message will be created +// @param message - An object of type MessageBody containing message details (e.g., content, sender) +// @returns The response from the API if the message is created successfully export const createMessage = async (chatId: number, message: MessageBody) => { try { const response = await api.post(`/api/v1/messages/${chatId}/`, message); diff --git a/client/src/api/page.api.ts b/client/src/api/page.api.ts index 4be7476..3351f57 100644 --- a/client/src/api/page.api.ts +++ b/client/src/api/page.api.ts @@ -1,6 +1,8 @@ -import { PageBody } from "../components/pages/types/pages.types"; +import { PageBody } from "../types/page/page.types"; import api from "./interceptor.api"; +// Fetches all pages from the API +// @returns The response from the API containing the list of all pages export const fetchPages = async () => { try { const response = await api.get(`/api/v1/pages/`); @@ -10,6 +12,8 @@ export const fetchPages = async () => { } }; +// Fetches insight-related pages from the API +// @returns The response from the API containing the list of pages with insights export const fetchInsightPages = async () => { try { const response = await api.get(`/api/v1/pages/insights/`); @@ -19,6 +23,9 @@ export const fetchInsightPages = async () => { } }; +// Fetches a specific page by its ID from the API +// @param pageId - The unique identifier of the page to be fetched +// @returns The response from the API containing the details of the requested page export const fetchPageById = async (pageId: number) => { try { const response = await api.get(`/api/v1/pages/${pageId}/`); @@ -28,6 +35,9 @@ export const fetchPageById = async (pageId: number) => { } }; +// Creates a new page by sending a POST request with the page data +// @param page - An object of type PageBody containing page details +// @returns The response from the API if the page is created successfully export const createPage = async (page: PageBody) => { try { const response = await api.post("/api/v1/pages/", page); @@ -37,7 +47,10 @@ export const createPage = async (page: PageBody) => { } }; - +// Updates an existing page by sending a PUT request with the updated page data +// @param id - The unique identifier of the page to be updated +// @param page - An object of type PageBody containing updated page details +// @returns The response from the API if the page is updated successfully export const changePage = async (id: number, page: PageBody) => { try { const response = await api.put(`/api/v1/pages/${id}/`, page); @@ -47,6 +60,9 @@ export const changePage = async (id: number, page: PageBody) => { } }; +// Deletes a specific page by its ID from the API +// @param id - The unique identifier of the page to be deleted +// @returns The response from the API if the page is deleted successfully export const deletePage = async (id: number) => { try { const response = await api.delete(`/api/v1/pages/${id}/`); diff --git a/client/src/api/tag.api.ts b/client/src/api/tag.api.ts index 5fbb471..b886750 100644 --- a/client/src/api/tag.api.ts +++ b/client/src/api/tag.api.ts @@ -1,6 +1,8 @@ -import { TagBody } from "../components/pages/types/pages.types"; +import { TagBody } from "../types/page/page.types"; import api from "./interceptor.api"; +// Fetches all tags from the API +// @returns The response from the API containing the list of all tags export const fetchTags = async () => { try { const response = await api.get(`/api/v1/tags/`); @@ -11,12 +13,27 @@ export const fetchTags = async () => { } }; +// Creates a new tag by sending a POST request with the tag data +// @param tag - An object of type TagBody containing tag details +// @returns The response from the API if the tag is created successfully export const createTag = async (tag: TagBody) => { try { const response = await api.post("/api/v1/tags/", tag); return response; } catch (error) { - console.error("Error creating user:", error); + console.error("Error creating tag:", error); + throw error; + } +}; + +// Delete existing tag of user +// @id: id of tag to delete +export const deleteTag = async (id: number) => { + try { + const response = await api.delete(`/api/v1/tags/${id}/`); + return response; + } catch (error) { + console.error("Error deleting tag:", error); throw error; } }; diff --git a/client/src/api/user.api.ts b/client/src/api/user.api.ts index 6b97920..4b6faf9 100644 --- a/client/src/api/user.api.ts +++ b/client/src/api/user.api.ts @@ -1,5 +1,7 @@ import api from "./interceptor.api"; +// Fetches all users from the API +// @returns The response from the API containing the list of all users export const fetchUsers = async () => { try { const response = await api.get(`/api/v1/users/`); @@ -9,6 +11,11 @@ export const fetchUsers = async () => { } }; +// Updates a user's status and role (active and staff) by sending a PUT request +// @param id - The unique identifier of the user to be updated +// @param is_active - A boolean indicating whether the user account is activated +// @param is_staff - A boolean indicating whether the user is a staff member +// @returns The response from the API if the user is updated successfully export const changeUser = async (id: number, is_active: boolean, is_staff: boolean) => { try { const response = await api.put(`/api/v1/users/${id}/`, {is_active, is_staff}); @@ -18,6 +25,8 @@ export const changeUser = async (id: number, is_active: boolean, is_staff: boole } } +// Fetches the whitelist of emails from the API +// @returns The response from the API containing the whitelist of emails export const fetchWhitelist = async () => { try { const response = await api.get(`/api/v1/users/whitelist/`); @@ -27,6 +36,9 @@ export const fetchWhitelist = async () => { } }; +// Adds a new email to the whitelist by sending a POST request +// @param email - The email address to be added to the whitelist +// @returns The response from the API if the email is added successfully export const createWhitelistEmail = async (email: string) => { try { const response = await api.post(`/api/v1/users/whitelist/`, {email}); @@ -36,6 +48,9 @@ export const createWhitelistEmail = async (email: string) => { } }; +// Deletes an email from the whitelist by its ID +// @param id - The unique identifier of the email to be deleted from the whitelist +// @returns The response from the API if the email is deleted successfully export const deleteWhitelist = async (id: number) => { try { const response = await api.delete(`/api/v1/users/whitelist/${id}/`); @@ -45,6 +60,8 @@ export const deleteWhitelist = async (id: number) => { } }; +// Fetches the permissions of the current user from the API +// @returns The response from the API containing the user's permissions export const fetchUserPermission = async () => { try { const response = await api.get(`/api/v1/users/permissions/`); diff --git a/client/src/components/admin-table/admin-table.component.tsx b/client/src/components/admin-table/admin-table.component.tsx index f0e0116..8052c99 100644 --- a/client/src/components/admin-table/admin-table.component.tsx +++ b/client/src/components/admin-table/admin-table.component.tsx @@ -7,7 +7,7 @@ import { Typography, useTheme, } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; import AdminTitle from "./admin-title/admin-title.component"; import { getUsers, getWhitelist } from "../../services/user.service"; import { UserDTO, WhitelistDTO } from "../../types/register/register.types"; diff --git a/client/src/components/chatbot/chatbot.component.tsx b/client/src/components/chatbot/chatbot.component.tsx index caf9d03..8067f8a 100644 --- a/client/src/components/chatbot/chatbot.component.tsx +++ b/client/src/components/chatbot/chatbot.component.tsx @@ -9,7 +9,7 @@ import { IconButton, useTheme, } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; import "../layout/styles/layout.styles.css"; import { ArrowUpCircle, Edit } from "react-feather"; import { lazy, Suspense, useEffect, useState } from "react"; @@ -21,7 +21,7 @@ import LoadingComponent from "../general/loading-component/loading.component"; import { useToolStore } from "../../states/global.store"; import { getPages } from "../../services/pages.service"; import "./styles/chatbot.styles.css"; -import EmptyChat from "../general/empty-chat/empty-chat.component"; +import EmptyChat from "./empty-chat/empty-chat.component"; import { ErrorBoundary } from "react-error-boundary"; import ErrorBoundaryFallback from "../general/error-boundary/error-boundary.component"; import CreationDialog from "../general/create-dialog/creation-dialog.component"; diff --git a/client/src/components/general/empty-chat/empty-chat.component.tsx b/client/src/components/chatbot/empty-chat/empty-chat.component.tsx similarity index 100% rename from client/src/components/general/empty-chat/empty-chat.component.tsx rename to client/src/components/chatbot/empty-chat/empty-chat.component.tsx diff --git a/client/src/components/documentation/documentation.component.tsx b/client/src/components/documentation/documentation.component.tsx index 675d435..4a1b9b4 100644 --- a/client/src/components/documentation/documentation.component.tsx +++ b/client/src/components/documentation/documentation.component.tsx @@ -1,5 +1,5 @@ import { Box, Typography } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; function Documentation({ open }: SidebarParams) { return ( diff --git a/client/src/components/general/footer/footer.component.tsx b/client/src/components/footer/footer.component.tsx similarity index 100% rename from client/src/components/general/footer/footer.component.tsx rename to client/src/components/footer/footer.component.tsx diff --git a/client/src/components/general/footer/styles/footer.styles.css b/client/src/components/footer/styles/footer.styles.css similarity index 100% rename from client/src/components/general/footer/styles/footer.styles.css rename to client/src/components/footer/styles/footer.styles.css diff --git a/client/src/components/general/anonymous-route/anonymous-route.component.tsx b/client/src/components/general/anonymous-route/anonymous-route.component.tsx deleted file mode 100644 index df49849..0000000 --- a/client/src/components/general/anonymous-route/anonymous-route.component.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Navigate, Outlet } from "react-router-dom"; -import { ACCESS_TOKEN } from "../../../helpers/auth.helpers"; - -function AnonymousRoute() { - const token = localStorage.getItem(ACCESS_TOKEN); - return token ? : ; -} - -export default AnonymousRoute; \ No newline at end of file diff --git a/client/src/components/general/auth-provider/auth-provider.component.tsx b/client/src/components/general/auth-provider/auth-provider.component.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/components/general/create-dialog/creation-dialog.component.tsx b/client/src/components/general/create-dialog/creation-dialog.component.tsx index e083633..61bf35d 100644 --- a/client/src/components/general/create-dialog/creation-dialog.component.tsx +++ b/client/src/components/general/create-dialog/creation-dialog.component.tsx @@ -15,10 +15,9 @@ import { Typography, useTheme, } from "@mui/material"; -import { CreationDialogParams } from "./types/creation-dialog.types"; +import { CreationDialogParams } from "../../../types/create-dialog/creation-dialog.types"; import { PlusCircle, X } from "react-feather"; import React, { lazy, Suspense, useEffect, useState } from "react"; -import { TagDTO } from "../../pages/types/pages.types"; import { getTags } from "../../../services/tags.service"; import { useToolStore } from "../../../states/global.store"; import { LabelDTO } from "../../../types/chatbot/chatbot.types"; @@ -28,6 +27,7 @@ import { getLabels } from "../../../services/label.service"; import LoadingComponent from "../loading-component/loading.component"; import { ErrorBoundary } from "react-error-boundary"; import ErrorBoundaryFallback from "../error-boundary/error-boundary.component"; +import { TagDTO } from "../../../types/page/page.types"; const PageTreeView = lazy( () => import("../../pages/page-tree-view/page-tree-view.component") ); @@ -323,6 +323,8 @@ function CreationDialog({ setCurrentElements={setCurrentElements} selectedTagsOrLabels={selectedTagOrLabel} setSelectedTagsOrLabels={setSelectedTagOrLabel} + fetchTagOrLabelData={fetchTagOrLabelData} + source={source} /> diff --git a/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx b/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx index b21ccec..31f538f 100644 --- a/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx +++ b/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx @@ -2,14 +2,20 @@ import { Autocomplete, Checkbox, Chip, + Grid, + IconButton, TextField, useTheme, } from "@mui/material"; -import { CheckSquare, Square } from "react-feather"; -import { SyntheticEvent } from "react"; -import { TagDTO } from "../../../pages/types/pages.types"; -import { TagLabelListParams } from "../types/creation-dialog.types"; +import { CheckSquare, Square, Trash } from "react-feather"; +import { SyntheticEvent, useState } from "react"; +import { TagLabelListParams } from "../../../../types/create-dialog/creation-dialog.types"; import { LabelDTO } from "../../../../types/chatbot/chatbot.types"; +import { TagDTO } from "../../../../types/page/page.types"; +import CustomSnackbar from "../../snackbar/snackbar.component"; +import LoadingComponent from "../../loading-component/loading.component"; +import { removeTag } from "../../../../services/tags.service"; +import { removeLabel } from "../../../../services/label.service"; function TagLabelAutoComplete({ elements, @@ -17,8 +23,31 @@ function TagLabelAutoComplete({ setCurrentElements, selectedTagsOrLabels, setSelectedTagsOrLabels, + fetchTagOrLabelData, + source, }: TagLabelListParams) { const theme = useTheme(); + const [snackbar, setSnackbar] = useState({ + message: "", + type: "", + open: false, + }); + const [loading, setLoading] = useState(false); + + const handleClose = ( + _event: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === "clickaway") { + return; + } + + setSnackbar({ + message: snackbar.message, + type: snackbar.type, + open: false, + }); + }; const handleChange = (_event: SyntheticEvent, value: any) => { setSelectedTagsOrLabels( @@ -27,61 +56,144 @@ function TagLabelAutoComplete({ setCurrentElements(value); }; - return ( - option.label} - renderTags={(value, getTagProps) => - value.map((option, index) => { - const { key, ...tagProps } = getTagProps({ index }); - return ( - - ); + const handleDeleteElement = async (id: number) => { + setLoading(true); + if (source == "chat") { + removeLabel(id) + .then(() => { + setSnackbar({ + message: "Label successfully deleted.", + type: "success", + open: true, + }); + }) + .then(() => {}) + .catch(() => { + setSnackbar({ + message: "Error: Label could not be deleted.", + type: "error", + open: true, + }); + }) + .finally(() => { + fetchTagOrLabelData(); + setLoading(false); + }); + } else { + removeTag(id) + .then(() => { + setSnackbar({ + message: "Tag successfully deleted.", + type: "success", + open: true, + }); }) - } - renderOption={(props, option, { selected }) => ( -
  • - } - checkedIcon={} - style={{ marginRight: 8 }} - checked={selected || selectedTagsOrLabels.includes(option.id)} + .then(() => {}) + .catch(() => { + setSnackbar({ + message: "Error: Tag could not be deleted.", + type: "error", + open: true, + }); + }) + .finally(() => { + fetchTagOrLabelData(); + setLoading(false); + }); + } + }; + + if (loading) { + return ; + } + + return ( + <> + option.label} + renderTags={(value, getTagProps) => + value.map((option, index) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderOption={(props, option, { selected }) => ( +
  • + + + } + checkedIcon={} + style={{ marginRight: 8 }} + checked={selected || selectedTagsOrLabels.includes(option.id)} + /> + {option.label} + + + handleDeleteElement(option.id)} + > + + + + +
  • + )} + renderInput={(params) => ( + - {option.label} - - )} - renderInput={(params) => ( - - )} - /> + )} + /> + + ); } diff --git a/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx b/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx index f6fe8ee..0f747b9 100644 --- a/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx +++ b/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx @@ -1,5 +1,5 @@ import { Avatar, Stack } from "@mui/material"; -import { ColorSelectorParams } from "../../types/creation-dialog.types"; +import { ColorSelectorParams } from "../../../../../types/create-dialog/creation-dialog.types"; import { Check, Circle } from "react-feather"; const colors = [ diff --git a/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx b/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx index 6ff3811..1b7e2d6 100644 --- a/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx +++ b/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { TagLabelDialogParams } from "../types/creation-dialog.types"; +import { TagLabelDialogParams } from "../../../../types/create-dialog/creation-dialog.types"; import { addTag } from "../../../../services/tags.service"; import { TagBody } from "../../../pages/types/pages.types"; import { diff --git a/client/src/components/general/snackbar/snackbar.component.tsx b/client/src/components/general/snackbar/snackbar.component.tsx index 3c45a4b..af85bd3 100644 --- a/client/src/components/general/snackbar/snackbar.component.tsx +++ b/client/src/components/general/snackbar/snackbar.component.tsx @@ -1,5 +1,5 @@ import { Alert, Snackbar } from "@mui/material"; -import { SnackbarParams } from "./types/snackbar.types"; +import { SnackbarParams } from "../../../types/general/general.types"; function CustomSnackbar({ message, type, open, handleClose }: SnackbarParams) { diff --git a/client/src/components/general/snackbar/types/snackbar.types.ts b/client/src/components/general/snackbar/types/snackbar.types.ts deleted file mode 100644 index 0b3609d..0000000 --- a/client/src/components/general/snackbar/types/snackbar.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type SnackbarParams = { - message: string; - type: string; - open: boolean; - handleClose: (event: React.SyntheticEvent | Event, reason?: string) => void; -}; diff --git a/client/src/components/layout/page-layout.component.tsx b/client/src/components/layout/page-layout.component.tsx index eaf1339..a61421a 100644 --- a/client/src/components/layout/page-layout.component.tsx +++ b/client/src/components/layout/page-layout.component.tsx @@ -1,8 +1,8 @@ import { Grid } from "@mui/material"; import Sidebar from "../sidebar/sidebar.component"; import { Outlet } from "react-router-dom"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; -import Footer from "../general/footer/footer.component"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; +import Footer from "../footer/footer.component"; function PageLayout({ open, setOpen }: SidebarParams) { return ( diff --git a/client/src/components/pages/page-tree-view/page-tree-view.component.tsx b/client/src/components/pages/page-tree-view/page-tree-view.component.tsx index d1b51c4..084defc 100644 --- a/client/src/components/pages/page-tree-view/page-tree-view.component.tsx +++ b/client/src/components/pages/page-tree-view/page-tree-view.component.tsx @@ -8,8 +8,8 @@ import { useTreeViewApiRef } from "@mui/x-tree-view/hooks"; import { SyntheticEvent, useEffect, useState } from "react"; import { useToolStore } from "../../../states/global.store"; import { Alert, Typography } from "@mui/material"; -import { PageDTO } from "../types/pages.types"; import { getPageById } from "../../../services/pages.service"; +import { PageDTO } from "../../../types/page/page.types"; function PageTreeView({ currentPageId, diff --git a/client/src/components/pages/pages.component.tsx b/client/src/components/pages/pages.component.tsx index 97453ed..678338a 100644 --- a/client/src/components/pages/pages.component.tsx +++ b/client/src/components/pages/pages.component.tsx @@ -1,17 +1,17 @@ import { Button, Grid, Typography } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; import { Edit } from "react-feather"; import { lazy, Suspense, useEffect, useState } from "react"; import CustomSnackbar from "../general/snackbar/snackbar.component"; import { useToolStore } from "../../states/global.store"; -import { PageDTO } from "./types/pages.types"; import { addPage, getPageById, getPages } from "../../services/pages.service"; import CreationDialog from "../general/create-dialog/creation-dialog.component"; import LoadingComponent from "../general/loading-component/loading.component"; import { getChatsByPageId } from "../../services/chats.service"; import { ChatDTO } from "../../types/chatbot/chatbot.types"; import { useQuery } from "react-query"; -import EmptyChat from "../general/empty-chat/empty-chat.component"; +import EmptyChat from "../chatbot/empty-chat/empty-chat.component"; +import { PageDTO } from "../../types/page/page.types"; const PageTitle = lazy(() => import("./page-title/page-title.component")); const ChatTable = lazy(() => import("./chat-table/chat-table.component")); const PageTreeView = lazy( diff --git a/client/src/components/pages/types/pages.types.ts b/client/src/components/pages/types/pages.types.ts deleted file mode 100644 index 444392c..0000000 --- a/client/src/components/pages/types/pages.types.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Dispatch, SetStateAction } from "react"; - -// DTOs -export type PageDTO = { - id: string; - label: string; - children: string[]; - parent_page: number | null; - level: number; - read_only: boolean; - tags: TagDTO[]; -}; - -export type TagDTO = { - id: string; - label: string; - color: string; -}; - -// Request Bodies -export type PageBody = { - label: string; - parent_page_id: number | null; - tags: string[]; -}; - -export type TagBody = { - label: string; - color: string; -}; - -// List Elements -export type TagList = { - tags: TagDTO[]; - selectedTags: TagDTO[]; - setSelectedTags: Dispatch>; -}; - -// Function Parameter Definitions -export type PageDialogParams = { - open: boolean; - setOpen: (open: boolean) => void; - label: string; - setLabel: (title: string) => void; - parentId: number; - setParentId: (parentId: number) => void; - createPage?: (e: any) => void; - editPage?: (page: PageDTO) => void; - deletePage?: (id: number) => void; - pages: any; - type: string; -}; - -export type PopperParams = { - open: boolean; - setOpen: (open: boolean) => void; - anchor: HTMLButtonElement | null; - fetchTags: () => Promise; - hasValue: (tag_name: string) => boolean; -}; - -export type ColorSelectorParams = { - color: string; - setColor: (color: string) => void; -}; - -export type TreeViewParams = { - currentPageId: string; - setCurrentPageId: (currentPageId: string) => void; - isNewPage: boolean; -}; - -export type PageTitleParams = { - currentPage: PageDTO; - fetchPagesData: () => void; - fetchPageById: (id: number) => void; - setSnackbar: (snackbar: { - message: string; - type: string; - open: boolean; - }) => void; -}; diff --git a/client/src/components/sidebar/download-button/download-button.component.tsx b/client/src/components/sidebar/download-button/download-button.component.tsx index 3ee9547..8ba5b28 100644 --- a/client/src/components/sidebar/download-button/download-button.component.tsx +++ b/client/src/components/sidebar/download-button/download-button.component.tsx @@ -1,8 +1,15 @@ -import { Button, Typography, useTheme } from "@mui/material"; +import { + Button, + IconButton, + Tooltip, + Typography, + useTheme, +} from "@mui/material"; import { getEventLogs } from "../../../services/interactions.service"; import { Download } from "react-feather"; +import { DownloadButtonParams } from "../../../types/sidebar/sidebar.types"; -const DownloadButton = () => { +const DownloadButton = ({ open }: DownloadButtonParams) => { const theme = useTheme(); const handleDownload = async () => { @@ -25,15 +32,25 @@ const DownloadButton = () => { } }; + if (open) { + return ( + + ); + } + return ( - + + + + + ); }; diff --git a/client/src/components/sidebar/sidebar.component.tsx b/client/src/components/sidebar/sidebar.component.tsx index 72fb5d3..f274c62 100644 --- a/client/src/components/sidebar/sidebar.component.tsx +++ b/client/src/components/sidebar/sidebar.component.tsx @@ -9,7 +9,7 @@ import { } from "@mui/material"; import WavingHandIcon from "@mui/icons-material/WavingHand"; import { menu_items } from "./helpers/sidebar.helpers"; -import { MenuItem, SidebarParams } from "./types/sidebar.types"; +import { MenuItem, SidebarParams } from "../../types/sidebar/sidebar.types"; import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; import { Sidebar as SidebarIcon, @@ -160,13 +160,11 @@ function Sidebar({ open, setOpen }: SidebarParams) { xs={12} sx={{ display: "flex", - justifyContent: "flex-start", - ml: "1rem", - mr: "1rem", + justifyContent: "center", mt: "3vh", }} > - +
    import("./filter/filter.component")); const BehavioralDashboard = lazy( diff --git a/client/src/components/general/admin-route/admin-route.component.tsx b/client/src/routes/admin-route/admin-route.component.tsx similarity index 79% rename from client/src/components/general/admin-route/admin-route.component.tsx rename to client/src/routes/admin-route/admin-route.component.tsx index 16f2bbf..01e2320 100644 --- a/client/src/components/general/admin-route/admin-route.component.tsx +++ b/client/src/routes/admin-route/admin-route.component.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { getUserPermissions } from "../../../services/user.service"; -import { ACCESS_TOKEN } from "../../../helpers/auth.helpers"; import { Navigate, Outlet } from "react-router-dom"; +import { ACCESS_TOKEN } from "../../helpers/auth.helpers"; +import { getUserPermissions } from "../../services/user.service"; function AdminRoute() { const [isAdmin, setIsAdmin] = useState(null); diff --git a/client/src/components/general/protected-route/protected-route.route.tsx b/client/src/routes/protected-route/protected-route.route.tsx similarity index 88% rename from client/src/components/general/protected-route/protected-route.route.tsx rename to client/src/routes/protected-route/protected-route.route.tsx index 2b2991f..f00e397 100644 --- a/client/src/components/general/protected-route/protected-route.route.tsx +++ b/client/src/routes/protected-route/protected-route.route.tsx @@ -1,9 +1,9 @@ import { Navigate, Outlet } from "react-router-dom"; import { jwtDecode } from "jwt-decode"; -import { REFRESH_TOKEN, ACCESS_TOKEN } from "../../../helpers/auth.helpers"; +import { REFRESH_TOKEN, ACCESS_TOKEN } from "../../helpers/auth.helpers"; import { useState, useEffect } from "react"; -import { refreshToken } from "../../../api/auth.api"; -import { useAuthStore } from "../../../states/global.store"; +import { refreshToken } from "../../api/auth.api"; +import { useAuthStore } from "../../states/global.store"; function ProtectedRoute() { const [loading, setLoading] = useState(true); diff --git a/client/src/services/label.service.ts b/client/src/services/label.service.ts index 6896a89..411698a 100644 --- a/client/src/services/label.service.ts +++ b/client/src/services/label.service.ts @@ -1,4 +1,4 @@ -import { createLabel, fetchLabels } from "../api/label.api"; +import { createLabel, deleteLabel, fetchLabels } from "../api/label.api"; import { LabelBody } from "../types/chatbot/chatbot.types"; export const getLabels = async () => { @@ -18,3 +18,13 @@ export const addLabel = async (label: LabelBody) => { throw error; } }; + + +export const removeLabel = async (id: number) => { + try { + const response = await deleteLabel(id) + return response.data; + } catch (error: any) { + throw error; + } +}; diff --git a/client/src/services/tags.service.ts b/client/src/services/tags.service.ts index 7764021..6ed63db 100644 --- a/client/src/services/tags.service.ts +++ b/client/src/services/tags.service.ts @@ -1,5 +1,5 @@ -import { createTag, fetchTags } from "../api/tag.api"; -import { TagBody } from "../components/pages/types/pages.types"; +import { createTag, deleteTag, fetchTags } from "../api/tag.api"; +import { TagBody } from "../types/page/page.types"; export const getTags = async () => { try { @@ -18,3 +18,12 @@ export const addTag = async (tag: TagBody) => { throw error; } }; + +export const removeTag = async (id: number) => { + try { + const response = await deleteTag(id) + return response.data; + } catch (error: any) { + throw error; + } +}; diff --git a/client/src/types/chatbot/chatbot.types.ts b/client/src/types/chatbot/chatbot.types.ts index 974dc46..244641a 100644 --- a/client/src/types/chatbot/chatbot.types.ts +++ b/client/src/types/chatbot/chatbot.types.ts @@ -1,4 +1,4 @@ -import { PageDTO } from "../../components/pages/types/pages.types"; +import { PageDTO } from "../page/page.types"; export type LabelDTO = { id: string; diff --git a/client/src/components/general/create-dialog/types/creation-dialog.types.ts b/client/src/types/create-dialog/creation-dialog.types.ts similarity index 87% rename from client/src/components/general/create-dialog/types/creation-dialog.types.ts rename to client/src/types/create-dialog/creation-dialog.types.ts index fdaea5f..6d393e6 100644 --- a/client/src/components/general/create-dialog/types/creation-dialog.types.ts +++ b/client/src/types/create-dialog/creation-dialog.types.ts @@ -1,5 +1,5 @@ -import { LabelDTO } from "../../../../types/chatbot/chatbot.types"; -import { TagDTO } from "../../../pages/types/pages.types"; +import { LabelDTO } from "../chatbot/chatbot.types"; +import { TagDTO } from "../page/page.types"; export type CreationDialogParams = { open: boolean; @@ -25,6 +25,8 @@ export type TagLabelListParams = { setCurrentElements: (currentElement: TagDTO[] | LabelDTO[]) => void; selectedTagsOrLabels: string[]; setSelectedTagsOrLabels: (selectedTagOrLabel: string[]) => void; + fetchTagOrLabelData: () => void; + source: string; }; export type TagLabelDialogParams = { diff --git a/client/src/types/general/general.types.ts b/client/src/types/general/general.types.ts index 64ff67e..2d6f5af 100644 --- a/client/src/types/general/general.types.ts +++ b/client/src/types/general/general.types.ts @@ -1,4 +1,12 @@ export type InputError = { error: boolean; errorMessage: string; -} \ No newline at end of file +} + +export type SnackbarParams = { + message: string; + type: string; + open: boolean; + handleClose: (event: React.SyntheticEvent | Event, reason?: string) => void; + }; + \ No newline at end of file diff --git a/client/src/types/page/page.types.ts b/client/src/types/page/page.types.ts index e69de29..444392c 100644 --- a/client/src/types/page/page.types.ts +++ b/client/src/types/page/page.types.ts @@ -0,0 +1,82 @@ +import { Dispatch, SetStateAction } from "react"; + +// DTOs +export type PageDTO = { + id: string; + label: string; + children: string[]; + parent_page: number | null; + level: number; + read_only: boolean; + tags: TagDTO[]; +}; + +export type TagDTO = { + id: string; + label: string; + color: string; +}; + +// Request Bodies +export type PageBody = { + label: string; + parent_page_id: number | null; + tags: string[]; +}; + +export type TagBody = { + label: string; + color: string; +}; + +// List Elements +export type TagList = { + tags: TagDTO[]; + selectedTags: TagDTO[]; + setSelectedTags: Dispatch>; +}; + +// Function Parameter Definitions +export type PageDialogParams = { + open: boolean; + setOpen: (open: boolean) => void; + label: string; + setLabel: (title: string) => void; + parentId: number; + setParentId: (parentId: number) => void; + createPage?: (e: any) => void; + editPage?: (page: PageDTO) => void; + deletePage?: (id: number) => void; + pages: any; + type: string; +}; + +export type PopperParams = { + open: boolean; + setOpen: (open: boolean) => void; + anchor: HTMLButtonElement | null; + fetchTags: () => Promise; + hasValue: (tag_name: string) => boolean; +}; + +export type ColorSelectorParams = { + color: string; + setColor: (color: string) => void; +}; + +export type TreeViewParams = { + currentPageId: string; + setCurrentPageId: (currentPageId: string) => void; + isNewPage: boolean; +}; + +export type PageTitleParams = { + currentPage: PageDTO; + fetchPagesData: () => void; + fetchPageById: (id: number) => void; + setSnackbar: (snackbar: { + message: string; + type: string; + open: boolean; + }) => void; +}; diff --git a/client/src/components/sidebar/types/sidebar.types.ts b/client/src/types/sidebar/sidebar.types.ts similarity index 80% rename from client/src/components/sidebar/types/sidebar.types.ts rename to client/src/types/sidebar/sidebar.types.ts index 37832ec..341c6d4 100644 --- a/client/src/components/sidebar/types/sidebar.types.ts +++ b/client/src/types/sidebar/sidebar.types.ts @@ -14,3 +14,6 @@ export type StatisticParams = { messages: any } +export type DownloadButtonParams = { + open: boolean; +} diff --git a/server/chat/urls.py b/server/chat/urls.py index 51fa9f4..0937c92 100644 --- a/server/chat/urls.py +++ b/server/chat/urls.py @@ -15,5 +15,6 @@ name="chats", ), path("messages//", views.MessageDetailView.as_view(), name="message"), - path("labels/", views.LabelApiView.as_view(), name="label-create") + path("labels/", views.LabelApiView.as_view(), name="label-create"), + path("labels//", views.LabelApiView.as_view(), name="label-delete") ] \ No newline at end of file diff --git a/server/chat/views.py b/server/chat/views.py index 5c0a51f..3c830c0 100644 --- a/server/chat/views.py +++ b/server/chat/views.py @@ -131,6 +131,7 @@ def post(self, request, *args, **kwargs): def delete(self, request, pk, format=None): try: label = Label.objects.get(id=pk) + label.chats.clear() except Label.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) label.delete() diff --git a/server/insights/services/arrayMerge.py b/server/insights/services/arrayMerge.py index 49ccb8d..0430d5a 100644 --- a/server/insights/services/arrayMerge.py +++ b/server/insights/services/arrayMerge.py @@ -1,3 +1,4 @@ + def mergeByDate(array1, array2, key): combined_dict = {} for obj in array1: diff --git a/server/pages/urls.py b/server/pages/urls.py index 326fecf..de0edee 100644 --- a/server/pages/urls.py +++ b/server/pages/urls.py @@ -9,5 +9,6 @@ ), path("pages/", views.PageListView.as_view(), name="page-list"), path("pages/insights/", views.PageListFilterView.as_view(), name="page-list"), - path("tags/", views.TagApiView.as_view(), name="tag-create") + path("tags/", views.TagApiView.as_view(), name="tag-create"), + path("tags//", views.TagApiView.as_view(), name="tag-delete") ] \ No newline at end of file diff --git a/server/pages/views.py b/server/pages/views.py index c152ca0..6920420 100644 --- a/server/pages/views.py +++ b/server/pages/views.py @@ -116,6 +116,7 @@ def post(self, request, *args, **kwargs): def delete(self, request, pk, format=None): try: tag = Tag.objects.get(id=pk) + tag.pages.clear() except Tag.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) tag.delete()