Merge branch 'master' of https://dev.sp-tarkov.com/SPT-AKI/Website
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ereshkigal 2021-12-15 18:15:11 +01:00
commit 7b6c75d0e0
34 changed files with 909 additions and 570 deletions

View File

@ -1,2 +1,11 @@
[host] all:
{{ DEPLOYMENT_USER }}@{{ DEPLOY_HOSTNAME }} ansible_connection=ssh ansible_user={{ DEPLOYMENT_USER }} children:
sptarkov:
hosts:
sp-tarkov:
ansible_host: {{ DEPLOY_HOSTNAME }}
ansible_user: {{ DEPLOY_USER }}
ansible_connection: paramiko_ssh
ansible_ssh_pass: {{ DEPLOY_SSH_KEY_PASSPHRASE }}
ansible_ssh_user: {{ DEPLOY_USER }}
ansible_ssh_private_key_file: /drone/src/private.key # Hardcoded because created by the Drone plugin

View File

@ -1,6 +1,6 @@
--- ---
- name: install and configure PHP8 and composer - name: install and configure PHP8 and composer
hosts: host hosts: sptarkov
tasks: tasks:
- name: Delete spt-items-api before adding everything again - name: Delete spt-items-api before adding everything again
@ -36,9 +36,9 @@
- name: Reset files permissions - name: Reset files permissions
file: file:
path: "{{ lookup('env', 'SPT_ITEMS_PATH') }}" path: "{{ lookup('env', 'SPT_ITEMS_PATH') }}"
owner: "{{ lookup('env', 'DEPLOYMENT_USER') }}" owner: "{{ lookup('env', 'DEPLOY_USER') }}"
group: "{{ lookup('env', 'DEPLOYMENT_USER_GROUP') }}" group: "{{ lookup('env', 'DEPLOY_USER_GROUP') }}"
mode: 0774 mode: 0775
recurse: yes recurse: yes
- name: Initialize database - name: Initialize database

View File

@ -8,66 +8,96 @@ concurrency:
trigger: trigger:
event: event:
- push - push
branch:
- master
- main
- development
steps: steps:
- name: replace hosts and user variables - name: replace hosts and user variables
image: ubuntu:impish image: ubuntu:impish
environment: environment:
DEPLOY_HOSTNAME: SPT_ITEMS_HOSTNAME:
from_secret: deploy_hostname from_secret: spt_items_hostname
SPT_ITEMS_HOSTNAME: DEPLOY_HOSTNAME:
from_secret: spt_items_hostname from_secret: deploy_hostname
DEPLOYMENT_USER: DEPLOY_USER:
from_secret: deploy_username from_secret: deploy_username
commands: DEPLOY_PRIVATE_KEY:
- sed -i 's/{{ SPT_ITEMS_HOSTNAME }}/'"$SPT_ITEMS_HOSTNAME"'/g' ./items/frontend/.env.example from_secret: deploy_ssh_key
- mv ./items/frontend/.env.example ./items/frontend/.env DEPLOY_SSH_KEY_PASSPHRASE:
- sed -i 's/{{ DEPLOY_HOSTNAME }}/'"$DEPLOY_HOSTNAME"'/g' ./.ansible-items/inventory from_secret: deploy_ssh_key_passphrase
- sed -i 's/{{ DEPLOYMENT_USER }}/'"$DEPLOYMENT_USER"'/g' ./.ansible-items/inventory commands:
- sed -i 's/{{ SPT_ITEMS_HOSTNAME }}/'"$SPT_ITEMS_HOSTNAME"'/g' ./items/frontend/.env.example
- mv ./items/frontend/.env.example ./items/frontend/.env
- echo "$DEPLOY_PRIVATE_KEY" > private.key && chmod 600 private.key
- sed -i 's/{{ DEPLOY_HOSTNAME }}/'"$DEPLOY_HOSTNAME"'/g' ./.ansible-items/inventory
- sed -i 's/{{ DEPLOY_SSH_KEY_PASSPHRASE }}/'"$DEPLOY_SSH_KEY_PASSPHRASE"'/g' ./.ansible-items/inventory
- sed -i 's/{{ DEPLOY_USER }}/'"$DEPLOY_USER"'/g' ./.ansible-items/inventory
- name: build frontend - name: install dependencies and build frontend
image: node:lts-alpine3.14 image: node:lts-alpine3.14
commands: commands:
- node -v - node -v
- npm -v - npm -v
- yarn --version - yarn --version
- yarn --cwd ./items/frontend install - yarn --cwd ./items/frontend install
- yarn --cwd ./items/frontend build --pure-lockfile - yarn --cwd ./items/frontend build --pure-lockfile
- rm -rf ./items/api/public/static/* - rm -rf ./items/api/public/static/*
- mv ./items/frontend/build/* ./items/api/public - cp -r ./items/frontend/build/* ./items/api/public
- rm ./items/api/public/index.html - rm ./items/api/public/index.html
depends_on:
- replace hosts and user variables
- name: check ansible syntax - name: frontend
image: plugins/ansible:3 image: nginx:1.21.4-alpine
settings: commands:
playbook: ./.ansible-items/playbook.yml - cp -r ./items/frontend/build/* /usr/share/nginx/html
inventory: ./.ansible-items/inventory - cp ./items/frontend/src/cypress/nginx_config/default.conf /etc/nginx/conf.d/default.conf
galaxy: ./.ansible-items/requirements.yml - nginx -g "daemon off;"
syntax_check: true detach: true
- name: apply ansible playbook depends_on:
image: plugins/ansible:3 - install dependencies and build frontend
settings:
playbook: ./.ansible-items/playbook.yml - name: test frontend
inventory: ./.ansible-items/inventory image: cypress/browsers:node16.5.0-chrome94-ff93
galaxy: ./.ansible-items/requirements.yml commands:
private_key: - node -v
from_secret: deploy_ssh_key - npm -v
environment: - yarn --version
DEPLOY_HOSTNAME: - yarn --cwd ./items/frontend cy:ci
from_secret: deploy_hostname depends_on:
SPT_ITEMS_HOSTNAME: - install dependencies and build frontend
from_secret: spt_items_hostname
DEPLOYMENT_USER: - name: check ansible syntax
from_secret: deploy_username image: plugins/ansible:3
DEPLOYMENT_USER_GROUP: settings:
from_secret: deploy_user_group playbook: ./.ansible-items/playbook.yml
SPT_ITEMS_PATH: inventory: ./.ansible-items/inventory
from_secret: deploy_path galaxy: ./.ansible-items/requirements.yml
when: syntax_check: true
branch: when:
- master branch:
- main - development
- name: apply ansible playbook
image: plugins/ansible:3
settings:
playbook: ./.ansible-items/playbook.yml
inventory: ./.ansible-items/inventory
galaxy: ./.ansible-items/requirements.yml
timeout: 60
verbose: 2
environment:
SPT_ITEMS_HOSTNAME:
from_secret: spt_items_hostname
DEPLOY_HOSTNAME:
from_secret: deploy_hostname
DEPLOY_USER:
from_secret: deploy_username
DEPLOY_USER_GROUP:
from_secret: deploy_user_group
SPT_ITEMS_PATH:
from_secret: deploy_path
depends_on:
- test frontend
when:
branch:
- master
- main

View File

@ -8,66 +8,96 @@ concurrency:
trigger: trigger:
event: event:
- push - push
branch:
- master
- main
- development
steps: steps:
- name: replace hosts and user variables - name: replace hosts and user variables
image: ubuntu:impish image: ubuntu:impish
environment: environment:
DEPLOY_HOSTNAME: SPT_ITEMS_HOSTNAME:
from_secret: deploy_hostname from_secret: spt_items_hostname
SPT_ITEMS_HOSTNAME: DEPLOY_HOSTNAME:
from_secret: spt_items_hostname from_secret: deploy_hostname
DEPLOYMENT_USER: DEPLOY_USER:
from_secret: deploy_username from_secret: deploy_username
commands: DEPLOY_PRIVATE_KEY:
- sed -i 's/{{ SPT_ITEMS_HOSTNAME }}/'"$SPT_ITEMS_HOSTNAME"'/g' ./items/frontend/.env.example from_secret: deploy_ssh_key
- mv ./items/frontend/.env.example ./items/frontend/.env DEPLOY_SSH_KEY_PASSPHRASE:
- sed -i 's/{{ DEPLOY_HOSTNAME }}/'"$DEPLOY_HOSTNAME"'/g' ./.ansible-items/inventory from_secret: deploy_ssh_key_passphrase
- sed -i 's/{{ DEPLOYMENT_USER }}/'"$DEPLOYMENT_USER"'/g' ./.ansible-items/inventory commands:
- sed -i 's/{{ SPT_ITEMS_HOSTNAME }}/'"$SPT_ITEMS_HOSTNAME"'/g' ./items/frontend/.env.example
- mv ./items/frontend/.env.example ./items/frontend/.env
- echo "$DEPLOY_PRIVATE_KEY" > private.key && chmod 600 private.key
- sed -i 's/{{ DEPLOY_HOSTNAME }}/'"$DEPLOY_HOSTNAME"'/g' ./.ansible-items/inventory
- sed -i 's/{{ DEPLOY_SSH_KEY_PASSPHRASE }}/'"$DEPLOY_SSH_KEY_PASSPHRASE"'/g' ./.ansible-items/inventory
- sed -i 's/{{ DEPLOY_USER }}/'"$DEPLOY_USER"'/g' ./.ansible-items/inventory
- name: build frontend - name: install dependencies and build frontend
image: node:lts-alpine3.14 image: node:lts-alpine3.14
commands: commands:
- node -v - node -v
- npm -v - npm -v
- yarn --version - yarn --version
- yarn --cwd ./items/frontend install - yarn --cwd ./items/frontend install
- yarn --cwd ./items/frontend build --pure-lockfile - yarn --cwd ./items/frontend build --pure-lockfile
- rm -rf ./items/api/public/static/* - rm -rf ./items/api/public/static/*
- mv ./items/frontend/build/* ./items/api/public - cp -r ./items/frontend/build/* ./items/api/public
- rm ./items/api/public/index.html - rm ./items/api/public/index.html
depends_on:
- replace hosts and user variables
- name: check ansible syntax - name: frontend
image: plugins/ansible:3 image: nginx:1.21.4-alpine
settings: commands:
playbook: ./.ansible-items/playbook.yml - cp -r ./items/frontend/build/* /usr/share/nginx/html
inventory: ./.ansible-items/inventory - cp ./items/frontend/src/cypress/nginx_config/default.conf /etc/nginx/conf.d/default.conf
galaxy: ./.ansible-items/requirements.yml - nginx -g "daemon off;"
syntax_check: true detach: true
- name: apply ansible playbook depends_on:
image: plugins/ansible:3 - install dependencies and build frontend
settings:
playbook: ./.ansible-items/playbook.yml - name: test frontend
inventory: ./.ansible-items/inventory image: cypress/browsers:node16.5.0-chrome94-ff93
galaxy: ./.ansible-items/requirements.yml commands:
private_key: - node -v
from_secret: deploy_ssh_key - npm -v
environment: - yarn --version
DEPLOY_HOSTNAME: - yarn --cwd ./items/frontend cy:ci
from_secret: deploy_hostname depends_on:
SPT_ITEMS_HOSTNAME: - install dependencies and build frontend
from_secret: spt_items_hostname
DEPLOYMENT_USER: - name: check ansible syntax
from_secret: deploy_username image: plugins/ansible:3
DEPLOYMENT_USER_GROUP: settings:
from_secret: deploy_user_group playbook: ./.ansible-items/playbook.yml
SPT_ITEMS_PATH: inventory: ./.ansible-items/inventory
from_secret: deploy_path galaxy: ./.ansible-items/requirements.yml
when: syntax_check: true
branch: when:
- master branch:
- main - development
- name: apply ansible playbook
image: plugins/ansible:3
settings:
playbook: ./.ansible-items/playbook.yml
inventory: ./.ansible-items/inventory
galaxy: ./.ansible-items/requirements.yml
timeout: 60
verbose: 2
environment:
SPT_ITEMS_HOSTNAME:
from_secret: spt_items_hostname
DEPLOY_HOSTNAME:
from_secret: deploy_hostname
DEPLOY_USER:
from_secret: deploy_username
DEPLOY_USER_GROUP:
from_secret: deploy_user_group
SPT_ITEMS_PATH:
from_secret: deploy_path
depends_on:
- test frontend
when:
branch:
- master
- main

View File

@ -14,6 +14,7 @@
| deploy_username | The default username to use on the remote server <br> used by Ansible SSH | www-data | | deploy_username | The default username to use on the remote server <br> used by Ansible SSH | www-data |
| deploy_user_group | The default user group to use on the remote server <br> used to set permission on the website folder | www-data | | deploy_user_group | The default user group to use on the remote server <br> used to set permission on the website folder | www-data |
| deploy_ssh_key | The **content** of the ssh private key used to connect to the remote server <br> The key needs to be in RSA in "RSA PRIVATE KEY" format <br> The ssh publick key needs to already be in the user used in the remote server ~/.ssh/authorized_keys | -----BEGIN RSA PRIVATE KEY----- <br> The key <br> -----END RSA PRIVATE KEY----- | | deploy_ssh_key | The **content** of the ssh private key used to connect to the remote server <br> The key needs to be in RSA in "RSA PRIVATE KEY" format <br> The ssh publick key needs to already be in the user used in the remote server ~/.ssh/authorized_keys | -----BEGIN RSA PRIVATE KEY----- <br> The key <br> -----END RSA PRIVATE KEY----- |
| deploy_ssh_key_passphrase | The passphrase to decrypt the SSH private key | test |
## ⚠ Important notes for the deployment ⚠ ## ⚠ Important notes for the deployment ⚠
* Add all required secrets in Drone * Add all required secrets in Drone
@ -31,9 +32,12 @@
## The pipeline summary ## The pipeline summary
1. Each push will: 1. Each push will:
1. Builds the frontend 1. Test the frontend
1. Move the build frontend in the backend `public` folder 2. IF on `development` or `master`/`main` branch
1. IF Promoted to production, deploys to the server 1. Builds the frontend
3. IF pushed from `master` or `main` main branch
1. Move the build frontend in the backend `public` folder
2. Deploys to the server
## The pipeline walkthrough ## The pipeline walkthrough
see [Walkthrough.md](./docs/Walkthrough.md) see [Walkthrough.md](./docs/Walkthrough.md)

View File

@ -4,12 +4,12 @@
* [Overview](#overview) * [Overview](#overview)
* [Pipeline definition](#pipeline-definition) * [Pipeline definition](#pipeline-definition)
* [Pipeline concurrency](#pipeline-concurrency) * [Pipeline concurrency](#pipeline-concurrency)
* [Environment variables](#environment-variables)
* [Triggers](#triggers) * [Triggers](#triggers)
* [Steps](#steps) * [Steps](#steps)
* [Fetch and update submodules](#Fetch-and-update-submodules)
* [Replace hosts and user variables](#replace-hosts-and-user-variables) * [Replace hosts and user variables](#replace-hosts-and-user-variables)
* [Build frontend](#build-frontend) * [Install frontend dependencies and build it](#install-frontend-dependencies-and-build-it)
* [Run the frontend](#run-the-frontend)
* [Test frontend](#test-frontend)
* [Check ansible syntax](#check-ansible-syntax) * [Check ansible syntax](#check-ansible-syntax)
* [Apply ansible playbook](#apply-ansible-playbook) * [Apply ansible playbook](#apply-ansible-playbook)
* [Playbook definition](#playbook-definition) * [Playbook definition](#playbook-definition)
@ -17,8 +17,8 @@
* [Copy the project](#copy-the-project) * [Copy the project](#copy-the-project)
* [Copy PHP env file](#copy-php-env-file) * [Copy PHP env file](#copy-php-env-file)
* [Get JavaScript chunks name](#get-javascript-chunks-name) * [Get JavaScript chunks name](#get-javascript-chunks-name)
* [Get file names from find output](#get-file-from-find-output) * [Get file names from find output](#get-file-names-from-find-output)
* [Copy app.blade.php file](#copy-app-blade-php-file) * [Copy app.blade.php file](#copy-appbladephp-file)
* [Download and install composer dependencies](#download-and-install-composer-dependencies) * [Download and install composer dependencies](#download-and-install-composer-dependencies)
## Overview ## Overview
@ -26,10 +26,10 @@
* the backend is a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) located in [api](../api) that points towards [https://dev.sp-tarkov.com/Rev/spt-items-api.git](https://dev.sp-tarkov.com/Rev/spt-items-api.git) * the backend is a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) located in [api](../api) that points towards [https://dev.sp-tarkov.com/Rev/spt-items-api.git](https://dev.sp-tarkov.com/Rev/spt-items-api.git)
* the frontend is a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) located in [frontend](../frontend) that points towards [https://dev.sp-tarkov.com/shirito/item-finder-website-frontend.git](https://dev.sp-tarkov.com/shirito/item-finder-website-frontend.git) * the frontend is a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) located in [frontend](../frontend) that points towards [https://dev.sp-tarkov.com/shirito/item-finder-website-frontend.git](https://dev.sp-tarkov.com/shirito/item-finder-website-frontend.git)
* There are two Ansible pipelines * There are two Ansible pipelines
* A docker pipeline [drone-docker.yml](../drone-docker.yml) * A docker pipeline [drone-docker.yml](../../.drone-docker.yml)
* A kubernetes pipeline [drone-kubernetes.yml](../drone-kubernetes.yml) * A kubernetes pipeline [drone-kubernetes.yml](../../.drone-kubernetes.yml)
* All ansible playbook files are located in [.ansible](../.ansible) * All ansible playbook files are located in [.ansible](../../.ansible-items)
* The documentation is located in [documentation](../documentation) * The documentation is located in [documentation](../docs)
## Pipeline definition ## Pipeline definition
```yml ```yml
@ -46,54 +46,50 @@ concurrency:
``` ```
The pipeline is set to only one build at a time (every subsequent build with be pending). The pipeline is set to only one build at a time (every subsequent build with be pending).
## Environment variables
```yml
environment:
SPT_ITEMS_PATH: /var/www/html/aki/spt-items-api
```
Here are the environment variables. They are automatically injected in every step.
## Triggers ## Triggers
```yml ```yml
trigger: trigger:
event: event:
- push - push
branch:
- master
- main
- development
``` ```
The pipeline is run on every push only on branches `master`, `main` and `development`. We want to check that every development on `development` branch is correct and deploy automatically when merged in `master`/`main`. The pipeline is run on every push We want to check that every development on `development` branch is correct and deploy automatically when merged in `master`/`main`.
## Steps ## Steps
### Replace hosts and user variables ### Replace hosts and user variables
```yml ```yml
- name: replace hosts and user variables - name: replace hosts and user variables
image: ubuntu:impish image: ubuntu:impish
environment: environment:
DEPLOY_HOSTNAME:
from_secret: deploy_hostname
SPT_ITEMS_HOSTNAME: SPT_ITEMS_HOSTNAME:
from_secret: spt_items_hostname from_secret: spt_items_hostname
DEPLOYMENT_USER: DEPLOY_HOSTNAME:
from_secret: deploy_hostname
DEPLOY_USER:
from_secret: deploy_username from_secret: deploy_username
DEPLOY_PRIVATE_KEY:
from_secret: deploy_ssh_key
DEPLOY_SSH_KEY_PASSPHRASE:
from_secret: deploy_ssh_key_passphrase
commands: commands:
- sed -i 's/{{ SPT_ITEMS_HOSTNAME }}/'"$SPT_ITEMS_HOSTNAME"'/g' ./items/frontend/.env.example - sed -i 's/{{ SPT_ITEMS_HOSTNAME }}/'"$SPT_ITEMS_HOSTNAME"'/g' ./items/frontend/.env.example
- mv ./items/frontend/.env.example ./items/frontend/.env - mv ./items/frontend/.env.example ./items/frontend/.env
- sed -i 's/{{ DEPLOY_HOSTNAME }}/'"$DEPLOY_HOSTNAME"'/g' ./.ansible-items/inventory - echo "$DEPLOY_PRIVATE_KEY" > private.key && chmod 600 private.key
- sed -i 's/{{ DEPLOYMENT_USER }}/'"$DEPLOYMENT_USER"'/g' ./.ansible-items/inventory - sed -i 's/{{ DEPLOY_HOSTNAME }}/'"$DEPLOY_HOSTNAME"'/g' ./.ansible-items/inventory
- sed -i 's/{{ DEPLOY_SSH_KEY_PASSPHRASE }}/'"$DEPLOY_SSH_KEY_PASSPHRASE"'/g' ./.ansible-items/inventory
- sed -i 's/{{ DEPLOY_USER }}/'"$DEPLOY_USER"'/g' ./.ansible-items/inventory
``` ```
Executed on every push. \ Executed on every push. \
The following environment variables are injected using Drone secrets: The following environment variables are injected using Drone secrets:
* `SPT_ITEMS_HOSTNAME` is used by the frontend to call the backend. * `SPT_ITEMS_HOSTNAME` is used by the frontend to call the backend.
* `DEPLOY_HOSTNAME` is used by Ansible to connect to the remote server via SSH. * `DEPLOY_HOSTNAME` is used by Ansible to connect to the remote server via SSH.
* `DEPLOYMENT_USER` is used by Ansible to connect to the remote server via SSH. * `DEPLOY_USER` is used by Ansible to connect to the remote server via SSH.
* all environment variables at the pipeline level (see [Environment variables](#environment-variables)) * `DEPLOY_PRIVATE_KEY` is the SSH key used to connect to the remote server via SSH
* `DEPLOY_SSH_KEY_PASSPHRASE` is the SSH key passphrase
Using `sed` makes temporary changes in the container/pod instead of commiting secrets in the repo in plain text. \ Using `sed` makes temporary changes in the container/pod instead of commiting secrets in the repo in plain text. \
The changes are never pushed and are discarded when the container/pod is terminated. The changes are never pushed and are discarded when the container/pod is terminated.
### Build frontend ### Install frontend dependencies and build it
```yml ```yml
- name: build frontend - name: install dependencies and build frontend
image: node:lts-alpine3.14 image: node:lts-alpine3.14
commands: commands:
- node -v - node -v
@ -102,15 +98,46 @@ The changes are never pushed and are discarded when the container/pod is termina
- yarn --cwd ./items/frontend install - yarn --cwd ./items/frontend install
- yarn --cwd ./items/frontend build --pure-lockfile - yarn --cwd ./items/frontend build --pure-lockfile
- rm -rf ./items/api/public/static/* - rm -rf ./items/api/public/static/*
- mv ./items/frontend/build/* ./items/api/public - cp -r ./items/frontend/build/* ./items/api/public
- rm ./items/api/public/index.html - rm ./items/api/public/index.html
depends_on:
- replace hosts and user variables
``` ```
Executed on every push. \ Executed on every push. \
Since the PHP backend serves the ReactJS frontend, the former is built and moved in the latter. Since the PHP backend serves the ReactJS frontend, the former is built and moved in the latter.
Notes: Notes:
* `yarn --cwd <path> <command>` executes the command in the specified file * `yarn --cwd <path> <command>` executes the command in the specified file
* `rm -rf ./api/public/static/*` deletes the static files to make sure no old JavaScript files remain * `rm -rf ./api/public/static/*` deletes the static files to make sure no old JavaScript files remain
* `rm ./api/public/index.html` ReactJS is bundled with a `index.html`. It is discarded to use [](https://dev.sp-tarkov.com/Rev/spt-items-api/raw/branch/master/resources/views/app.blade.php) instead. * `rm ./api/public/invdex.html` ReactJS is bundled with a `index.html`. It is discarded to use [](https://dev.sp-tarkov.com/Rev/spt-items-api/raw/branch/master/resources/views/app.blade.php) instead.
### Run the frontend
```yaml
- name: frontend
image: nginx:1.21.4-alpine
commands:
- cp -r ./items/frontend/build/* /usr/share/nginx/html
- cp ./items/frontend/src/cypress/nginx_config/default.conf /etc/nginx/conf.d/default.conf
- nginx -g "daemon off;"
detach: true
depends_on:
- install dependencies and build frontend
```
Copies the frontend and the nginx conf in the container to be able to test it.
The frontend is run and `detach` is specified so the End-to-End tests (using cypress) can run on it
### Test frontend
```yaml
- name: test frontend
image: cypress/browsers:node16.5.0-chrome94-ff93
commands:
- node -v
- npm -v
- yarn --version
- yarn --cwd ./items/frontend cy:ci
depends_on:
- install dependencies and build frontend
```
Run frontend tests using Cypress
### Check ansible syntax ### Check ansible syntax
```yml ```yml
@ -121,9 +148,12 @@ Notes:
inventory: ./.ansible-items/inventory inventory: ./.ansible-items/inventory
galaxy: ./.ansible-items/requirements.yml galaxy: ./.ansible-items/requirements.yml
syntax_check: true syntax_check: true
when:
branch:
- development
``` ```
Executed on every push. \ Executed on every push. \
Check the Ansible syntax in [playbook.yml](../.ansible/playbook.yml), [inventory](../.ansible/inventory) and [requirements.yml](../.ansible/requirements.yml). The check is executed on every push since we want to detect any error before validating the build using the promotion. Check the Ansible syntax in [playbook.yml](../../.ansible-items/playbook.yml), [inventory](../../.ansible-items/inventory) and [requirements.yml](../../.ansible-items/requirements.yml). The check is executed on every push since we want to detect any error before validating the build using the promotion.
### Apply ansible playbook ### Apply ansible playbook
```yml ```yml
@ -133,15 +163,21 @@ Check the Ansible syntax in [playbook.yml](../.ansible/playbook.yml), [inventory
playbook: ./.ansible-items/playbook.yml playbook: ./.ansible-items/playbook.yml
inventory: ./.ansible-items/inventory inventory: ./.ansible-items/inventory
galaxy: ./.ansible-items/requirements.yml galaxy: ./.ansible-items/requirements.yml
private_key: timeout: 60
from_secret: deploy_ssh_key verbose: 2
environment: environment:
DEPLOY_HOSTNAME:
from_secret: deploy_hostname
SPT_ITEMS_HOSTNAME: SPT_ITEMS_HOSTNAME:
from_secret: spt_items_hostname from_secret: spt_items_hostname
DEPLOYMENT_USER: DEPLOY_HOSTNAME:
from_secret: deploy_hostname
DEPLOY_USER:
from_secret: deploy_username from_secret: deploy_username
DEPLOY_USER_GROUP:
from_secret: deploy_user_group
SPT_ITEMS_PATH:
from_secret: deploy_path
depends_on:
- test frontend
when: when:
branch: branch:
- master - master
@ -153,14 +189,15 @@ This step is [idempotent](https://en.wikipedia.org/wiki/Idempotence). \
The following environment variables are injected using Drone secrets: The following environment variables are injected using Drone secrets:
* `SPT_ITEMS_HOSTNAME` is used by the PHP env file. * `SPT_ITEMS_HOSTNAME` is used by the PHP env file.
* `DEPLOY_HOSTNAME` is used to connect to the remote server via SSH. * `DEPLOY_HOSTNAME` is used to connect to the remote server via SSH.
* `DEPLOYMENT_USER` is used to connect to the remote server via SSH. * `DEPLOY_USER` is used to connect to the remote server via SSH.
* all environment variables at the pipeline level (see [Environment variables](#environment-variables)) * `DEPLOY_USER_GROUP` is the user group, used to give read/write/execute permissions to the whole group. It must be the same as Nginx's user.
* `SPT_ITEMS_PATH` is the path on the remote server where the files will be copyed to.
#### Playbook definition #### Playbook definition
```yml ```yml
hosts: host hosts: sptarkov
``` ```
Uses the host defined in [inventory](../.ansible/inventory). Remember, the step [Replace hosts and user variables](#replace-hosts-and-user-variables) already replaced the variables at this point. The playbook will be executed as `root` user using `sudo`. Uses the host defined in [inventory](../../.ansible-items/inventory). Remember, the step [Replace hosts and user variables](#replace-hosts-and-user-variables) already replaced the variables at this point.
#### Delete old spt-items-api #### Delete old spt-items-api
```yml ```yml
@ -170,7 +207,7 @@ Uses the host defined in [inventory](../.ansible/inventory). Remember, the step
path: "{{ lookup('env', 'SPT_ITEMS_PATH') }}" path: "{{ lookup('env', 'SPT_ITEMS_PATH') }}"
``` ```
Since the copy does not override the folder, this step takes care of it. \ Since the copy does not override the folder, this step takes care of it. \
`SPT_ITEMS_PATH` is injected thanks to the pipeline level environment variables (see [Environment variables](#environment-variables)) `SPT_ITEMS_PATH` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook))
#### Copy the project #### Copy the project
```yml ```yml
@ -188,8 +225,8 @@ Copies the whole project (frontend and backend) from the [api](../api) folder in
src: ./templates/.php-env.j2 src: ./templates/.php-env.j2
dest: "{{ lookup('env', 'SPT_ITEMS_PATH') }}/.env" dest: "{{ lookup('env', 'SPT_ITEMS_PATH') }}/.env"
``` ```
Uses [Jinja2](https://jinja2docs.readthedocs.io/en/stable/) to resolve the [template for the PHP .env file](../.ansible/templates/.php_env.j2). \ Uses [Jinja2](https://jinja2docs.readthedocs.io/en/stable/) to resolve the [template for the PHP .env file](../../.ansible-items/templates/.php-env.j2). \
`SPT_ITEMS_PATH` is injected thanks to the pipeline level environment variables (see [Environment variables](#environment-variables)). \ `SPT_ITEMS_PATH` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook)) \
`SPT_ITEMS_HOSTNAME` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook)) `SPT_ITEMS_HOSTNAME` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook))
#### Get JavaScript chunks name #### Get JavaScript chunks name
@ -199,8 +236,8 @@ Uses [Jinja2](https://jinja2docs.readthedocs.io/en/stable/) to resolve the [temp
cmd: find "{{ lookup('env', 'SPT_ITEMS_PATH') }}" -type f -name "*chunk.js" -printf "%f\n" cmd: find "{{ lookup('env', 'SPT_ITEMS_PATH') }}" -type f -name "*chunk.js" -printf "%f\n"
register: find_output register: find_output
``` ```
Prepare a find of all JavaScript chunk files for the [app.blade.php.j2](../.ansible/templates/app.blade.php.j2) template. \ Prepare a find of all JavaScript chunk files for the [app.blade.php.j2](../../.ansible-items/templates/app.blade.php.j2) template. \
`SPT_ITEMS_PATH` is injected thanks to the pipeline level environment variables (see [Environment variables](#environment-variables)). `SPT_ITEMS_PATH` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook))
#### Get file names from find output #### Get file names from find output
```yml ```yml
@ -208,7 +245,7 @@ Prepare a find of all JavaScript chunk files for the [app.blade.php.j2](../.ansi
set_fact: set_fact:
chunk_list: "{{ find_output['stdout'].split('\n') }}" chunk_list: "{{ find_output['stdout'].split('\n') }}"
``` ```
Splits the string containing the list of all JavaScript chunk files for the [app.blade.php.j2](../.ansible/templates/app.blade.php.j2) template. Splits the string containing the list of all JavaScript chunk files for the [app.blade.php.j2](../../.ansible-items/templates/app.blade.php.j2) template.
#### Copy app.blade.php file #### Copy app.blade.php file
```yml ```yml
@ -217,8 +254,8 @@ Splits the string containing the list of all JavaScript chunk files for the [app
src: ./templates/app.blade.php.j2 src: ./templates/app.blade.php.j2
dest: "{{ lookup('env', 'SPT_ITEMS_PATH') }}/resources/views/app.blade.php" dest: "{{ lookup('env', 'SPT_ITEMS_PATH') }}/resources/views/app.blade.php"
``` ```
Uses [Jinja2](https://jinja2docs.readthedocs.io/en/stable/) to resolve the [template for the PHP app.blade.php file](../.ansible/templates/app.blade.php.j2). \ Uses [Jinja2](https://jinja2docs.readthedocs.io/en/stable/) to resolve the [template for the PHP app.blade.php file](../../.ansible-items/templates/app.blade.php.j2). \
`SPT_ITEMS_PATH` is injected thanks to the pipeline level environment variables (see [Environment variables](#environment-variables)). `SPT_ITEMS_PATH` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook)).
#### Download and install composer dependencies #### Download and install composer dependencies
```yml ```yml
@ -233,17 +270,16 @@ Uses [Jinja2](https://jinja2docs.readthedocs.io/en/stable/) to resolve the [temp
- name: Reset files permissions - name: Reset files permissions
file: file:
path: "{{ lookup('env', 'SPT_ITEMS_PATH') }}" path: "{{ lookup('env', 'SPT_ITEMS_PATH') }}"
owner: "{{ lookup('env', 'DEPLOYMENT_USER') }}" owner: "{{ lookup('env', 'DEPLOY_USER') }}"
group: www-data group: "{{ lookup('env', 'DEPLOY_USER_GROUP') }}"
mode: 0774 mode: 0774
recurse: yes recurse: yes
``` ```
Permissions 0644: Permissions 0644:
* user: read/write/execute * user: read/write/execute
* group: read * group: read/write/execute (for Nginx to execute the php as well as write in the Laravel logs)
* other: read * other: read
`www-data` is hardcoded here but it should be the standard user for Apache and Nginx. \ `SPT_ITEMS_PATH` is injected in the environments properties (see [Apply ansible playbook](#apply-ansible-playbook)).
`SPT_ITEMS_PATH` is injected thanks to the pipeline level environment variables (see [Environment variables](#environment-variables)).
#### Initialize database #### Initialize database
```yml ```yml

View File

@ -1,5 +1,6 @@
{ {
"baseUrl": "http://localhost:3000", "baseUrl": "http://localhost:3000",
"video": false,
"integrationFolder": "src/cypress/integration", "integrationFolder": "src/cypress/integration",
"fixtureFolder": "src/cypress/fixtures", "fixtureFolder": "src/cypress/fixtures",
"supportFile": "src/cypress/support/index.js", "supportFile": "src/cypress/support/index.js",

View File

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -13,8 +13,10 @@
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^8.6.0", "cypress": "9.0.0",
"start-server-and-test": "^1.14.0" "start-server-and-test": "^1.14.0",
"typescript": "^4.1.2",
"wait-on": "^6.0.0"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
@ -22,11 +24,12 @@
"@mui/icons-material": "^5.0.3", "@mui/icons-material": "^5.0.3",
"@mui/material": "^5.0.3", "@mui/material": "^5.0.3",
"@mui/styles": "^5.0.1", "@mui/styles": "^5.0.1",
"history": "5",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-router-dom": "6",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1", "web-vitals": "^1.0.1",
"zustand": "^3.5.13" "zustand": "^3.5.13"
}, },
@ -36,8 +39,11 @@
"test:jest": "react-scripts test", "test:jest": "react-scripts test",
"cy:start:app": "cross-env BROWSER=none react-scripts -r @cypress/instrument-cra start", "cy:start:app": "cross-env BROWSER=none react-scripts -r @cypress/instrument-cra start",
"cy:start:wait": "start-server-and-test cy:start:app http://localhost:3000", "cy:start:wait": "start-server-and-test cy:start:app http://localhost:3000",
"cy:install": "cypress install",
"cy:verify": "cypress verify",
"cy:run:ci": "cross-env CYPRESS_BASE_URL=http://frontend cypress run --browser chrome",
"cy:open": "yarn run cy:start:wait -- \"cypress open\"", "cy:open": "yarn run cy:start:wait -- \"cypress open\"",
"cy:run": "yarn run cy:start:wait -- \"cypress run\"", "cy:ci": "yarn run wait-on http-get://frontend && yarn run cy:install && yarn run cy:verify && yarn run cy:run:ci",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
@ -59,8 +65,13 @@
] ]
}, },
"nyc": { "nyc": {
"include":["src/**/*.ts", "src/**/*.tsx"], "include": [
"exclude": ["src/reportWebVitals.ts"], "src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"src/reportWebVitals.ts"
],
"excludeAfterRemap": true "excludeAfterRemap": true
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,109 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="Senko.svg"
inkscape:export-filename="G:\Official-SPT-Website\items\frontend\public\Senko.png"
inkscape:export-xdpi="6.5542703"
inkscape:export-ydpi="6.5542703"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
units="px"
width="512px"
inkscape:zoom="2"
inkscape:cx="237.25"
inkscape:cy="308.25"
inkscape:window-width="1918"
inkscape:window-height="2071"
inkscape:window-x="1912"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer2" />
<defs
id="defs2" />
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
sodipodi:insensitive="true"
style="display:none;opacity:0.560261">
<image
width="135.46666"
height="135.46666"
preserveAspectRatio="none"
style="image-rendering:optimizeQuality"
xlink:href="file:///C:/Users/mangi/Downloads/cbimage.png"
id="image851"
x="2.7755576e-17"
y="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Vectors">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 57.52693,14.843495 C 57.410877,12.661731 51.397926,11.268153 43.831419,8.2809548 45.338713,11.093505 47.068813,13.447161 44.596296,15.191748 38.636156,19.397174 29.955524,24.939364 15.868351,32.519676 26.725142,29.585137 55.838776,18.042907 57.52693,14.843495 Z"
id="path973"
sodipodi:nodetypes="ccscc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 6.7171919,59.288951 c 0,0 9.7889021,3.914164 12.6354121,3.564442 2.84651,-0.349722 24.019083,-6.879054 38.584354,-13.733298 0,0 -1.537979,-1.906423 -7.295931,-1.874468 -5.757953,0.03195 -20.211551,6.328008 -43.9238351,12.043324 z"
id="path973-69"
sodipodi:nodetypes="czczc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 48.122176,37.54381 c -1.232593,-1.803961 -6.907354,-3.051501 -14.924557,-1.673159 2.74906,1.620197 6.959357,3.657244 7.330694,6.817329 0.883551,7.519041 -3.70442,22.015324 -35.7508463,67.70939 C 30.583823,83.728004 50.961041,55.466892 48.122176,37.54381 Z"
id="path973-6"
sodipodi:nodetypes="ccscc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 46.036413,46.48147 c -1.232593,-1.803961 1.979358,4.244957 -6.037845,5.623299 0.223363,6.765135 0.503644,57.527511 -6.420323,69.679121 6.172239,6.35186 7.240267,5.73861 11.114865,8.58937 0.736474,-6.18316 3.15318,-65.968708 1.343303,-83.89179 z"
id="path973-6-0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 115.53919,30.415369 C 107.38986,24.808662 103.16969,52.222323 96.943881,65.21414 103.95471,56.4204 124.82782,36.805909 115.53919,30.415369 Z"
id="path973-9"
sodipodi:nodetypes="scs" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 107.65215,125.52123 c 9.30104,0.3094 49.17635,3.11209 12.29815,-8.69842 -16.53053,-5.29402 -29.508741,-34.427468 -37.036766,-44.827756 0.75933,13.845435 18.534656,45.862106 24.738616,53.526176 z"
id="path973-9-6"
sodipodi:nodetypes="cscc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.23757px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 68.35759,73.019974 c 4.512042,-0.335257 8.147729,-24.553068 -3.641892,-33.568807 2.793719,8.335678 1.719792,16.407471 0.731829,19.226592 -0.845868,2.413656 -4.878742,14.920943 2.910063,14.342215 z"
id="path973-9-3"
sodipodi:nodetypes="scss" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.23757px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 59.276543,85.32281 C 56.530494,88.91866 43.057416,72.918966 40.497215,69.39512 c 10.391021,2.965585 12.107326,5.273931 14.449451,6.853352 2.595558,1.750324 7.834568,4.485074 4.329877,9.074338 z"
id="path973-9-3-1"
sodipodi:nodetypes="scss" />
<path
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m 72.892707,6.7468749 c 7.980077,3.7070851 9.522752,3.9595281 10.517188,8.4997391 1.962552,8.960262 -2.931732,59.541875 -4.564063,65.782031 C 77.622774,85.704215 61.697785,111.71986 51.461458,126.33854 71.486723,113.82936 81.693839,86.666049 82.909268,83.971968 86.747627,75.463979 91.272126,13.970573 90.683182,11.835806 89.929245,9.1029796 81.563522,6.5477024 72.892707,6.7468749 Z"
id="path3332"
sodipodi:nodetypes="csscssc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<!-- <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> --> <!-- <link rel="icon" href="%PUBLIC_URL%/favico.ico" /> -->
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favico.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="item finder" /> <meta name="description" content="item finder" />
@ -19,7 +19,7 @@
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML. Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will Unlike "/favico.ico" or "favico.ico", "%PUBLIC_URL%/favico.ico" will
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->

View File

@ -3,12 +3,12 @@
"name": "Items Website for sp-tarkov", "name": "Items Website for sp-tarkov",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favico.ico",
"sizes": "32x32", "sizes": "32x32",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "favicon.svg", "src": "favico.svg",
"type": "image/svg+xml", "type": "image/svg+xml",
"sizes": "64x64 96x96 128x128 160x160 192x192 512x512" "sizes": "64x64 96x96 128x128 160x160 192x192 512x512"
} }

View File

@ -1,23 +1,23 @@
import { Box, Theme, Typography } from '@mui/material' import {Box, Typography} from '@mui/material'
import { makeStyles } from '@mui/styles' import {makeStyles} from '@mui/styles'
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles(() => ({
footerHolder: { footerHolder: {
display: 'flex', display: 'flex',
flex: '0 1 3vh', flex: '0 1 3vh',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '0 10vw 0 10vw' padding: '0 10vw 0 10vw'
} }
})) }))
export const Footer = () => { export const Footer = () => {
const classes = useStyles() const classes = useStyles()
return ( return (
<Box className={classes.footerHolder}> <Box className={classes.footerHolder}>
<Typography>SPT-Aki ©2021 Created by Rev and Shirito</Typography> <Typography id={"footer"}>SPT-Aki ©2021 Created by Rev and Shirito</Typography>
</Box> </Box>
) )
} }

View File

@ -0,0 +1,84 @@
{
"item": {
"_id": "5734773724597737fd047c14",
"_name": "condensed_milk",
"_parent": "5448e8d04bdc2ddf718b4569",
"_props": {
"AllowSpawnOnLocations": [],
"AnimationVariantsNumber": 0,
"BackgroundColor": "orange",
"CanRequireOnRagfair": true,
"CanSellOnRagfair": true,
"ChangePriceCoef": 1,
"ConflictingItems": [],
"CreditsPrice": 24943,
"Description": "Condensed milk",
"DiscardingBlock": false,
"ExamineExperience": 20,
"ExamineTime": 1,
"ExaminedByDefault": false,
"ExtraSizeDown": 0,
"ExtraSizeForceAdd": false,
"ExtraSizeLeft": 0,
"ExtraSizeRight": 0,
"ExtraSizeUp": 0,
"FixedPrice": false,
"Height": 1,
"HideEntrails": false,
"IsAlwaysAvailableForInsurance": false,
"IsLockedafterEquip": false,
"IsUnbuyable": false,
"IsUndiscardable": false,
"IsUngivable": false,
"IsUnsaleable": false,
"ItemSound": "food_tin_can",
"LootExperience": 50,
"MaxResource": 1,
"MergesWithChildren": false,
"Name": "Condensed milk",
"NotShownInSlot": false,
"Prefab": {
"path": "assets/content/weapons/usable_items/item_food_condensed_milk/item_food_condensed_milk_loot.bundle",
"rcid": ""
},
"QuestItem": false,
"RagFairCommissionModifier": 1,
"Rarity": "Rare",
"RepairCost": 0,
"RepairSpeed": 0,
"SendToClient": false,
"ShortName": "Condensed milk",
"SpawnChance": 9,
"StackMaxSize": 1,
"StackObjectsCount": 1,
"StimulatorBuffs": "Buffs_food_condensed_milk",
"Unlootable": false,
"UnlootableFromSide": [],
"UnlootableFromSlot": "FirstPrimaryWeapon",
"UsePrefab": {
"path": "assets/content/weapons/usable_items/item_food_condensed_milk/item_food_condensed_milk_container.bundle",
"rcid": ""
},
"Weight": 0.4,
"Width": 1,
"effects_damage": [],
"effects_health": {
"Energy": {
"value": 75
},
"Hydration": {
"value": -65
}
},
"foodEffectType": "afterUse",
"foodUseTime": 4
},
"_proto": "5734770f24597738025ee254",
"_type": "Item"
},
"locale": {
"Description": "Condensed milk, also called \"Sguschyonka\" in Russia, once was a part of field ration for the Union soldiers in Civil War, but later reached unprecedented popularity in post-Soviet countries, becoming almost a staple product. Canned, it can be stored for decades and remain just as sweet, tasty and nutritious.",
"Name": "Condensed milk",
"ShortName": "Cond. milk"
}
}

View File

@ -4,7 +4,7 @@ describe('Dark Mode Toggle', () => {
beforeEach(() => { beforeEach(() => {
cy.window() cy.window()
.its("sessionStorage") .its("sessionStorage")
.invoke("removeItem", "items.sp-tarkov.com-locales"); .invoke("removeItem", "db.sp-tarkov.com-locales");
cy.intercept( cy.intercept(
'**/api/locales', '**/api/locales',
@ -35,7 +35,7 @@ describe('Dark Mode Toggle', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.removeItem('items.sp-tarkov.com-prefered-color-scheme') window.localStorage.removeItem('db.sp-tarkov.com-prefered-color-scheme')
} }
}) })
}) })
@ -55,7 +55,7 @@ describe('Dark Mode Toggle', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.setItem('items.sp-tarkov.com-prefered-color-scheme', 'light') window.localStorage.setItem('db.sp-tarkov.com-prefered-color-scheme', 'light')
} }
}) })
}) })
@ -74,7 +74,7 @@ describe('Dark Mode Toggle', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.setItem('items.sp-tarkov.com-prefered-color-scheme', 'dark') window.localStorage.setItem('db.sp-tarkov.com-prefered-color-scheme', 'dark')
} }
}) })
}) })
@ -94,7 +94,7 @@ describe('Dark Mode Toggle', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.setItem('items.sp-tarkov.com-prefered-color-scheme', 'light') window.localStorage.setItem('db.sp-tarkov.com-prefered-color-scheme', 'light')
} }
}) })
}) })
@ -123,7 +123,7 @@ describe('Dark Mode Toggle', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.setItem('items.sp-tarkov.com-prefered-color-scheme', 'dark') window.localStorage.setItem('db.sp-tarkov.com-prefered-color-scheme', 'dark')
} }
}) })
}) })

View File

@ -4,7 +4,7 @@ describe('Footer', () => {
it('footer should be visible', () => { it('footer should be visible', () => {
cy.visit('/'); cy.visit('/');
cy.get('.makeStyles-footerHolder-63 > .MuiTypography-root').should('be.visible') cy.get('#footer').should('be.visible')
cy.get('.makeStyles-footerHolder-63 > .MuiTypography-root').should('have.text', 'SPT-Aki ©2021 Created by Rev and Shirito'); cy.get('#footer').should('have.text', 'SPT-Aki ©2021 Created by Rev and Shirito');
}) })
}) })

View File

@ -4,7 +4,7 @@ describe('Header', () => {
beforeEach(() => { beforeEach(() => {
cy.window() cy.window()
.its("sessionStorage") .its("sessionStorage")
.invoke("removeItem", "items.sp-tarkov.com-locales"); .invoke("removeItem", "db.sp-tarkov.com-locales");
cy.intercept( cy.intercept(
'**/api/locales', '**/api/locales',

View File

@ -4,7 +4,7 @@ describe('Json Theme', () => {
beforeEach(() => { beforeEach(() => {
cy.window() cy.window()
.its("sessionStorage") .its("sessionStorage")
.invoke("removeItem", "items.sp-tarkov.com-locales"); .invoke("removeItem", "db.sp-tarkov.com-locales");
cy.intercept( cy.intercept(
'**/api/locales', '**/api/locales',
@ -35,7 +35,7 @@ describe('Json Theme', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.removeItem('items.sp-tarkov.com-prefered-json-theme') window.localStorage.removeItem('db.sp-tarkov.com-prefered-json-theme')
} }
}); });
}) })
@ -54,7 +54,7 @@ describe('Json Theme', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.setItem('items.sp-tarkov.com-prefered-json-theme', 'eighties') window.localStorage.setItem('db.sp-tarkov.com-prefered-json-theme', 'eighties')
} }
}); });
}) })

View File

@ -4,7 +4,7 @@ describe('Locale select', () => {
beforeEach(() => { beforeEach(() => {
cy.window() cy.window()
.its("sessionStorage") .its("sessionStorage")
.invoke("removeItem", "items.sp-tarkov.com-locales"); .invoke("removeItem", "db.sp-tarkov.com-locales");
cy.intercept( cy.intercept(
'**/api/locales', '**/api/locales',
@ -31,7 +31,7 @@ describe('Locale select', () => {
[]).as('getLocalesWithoutData'); []).as('getLocalesWithoutData');
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.removeItem('items.sp-tarkov.com-prefered-locale') window.localStorage.removeItem('db.sp-tarkov.com-prefered-locale')
} }
}) })
}) })
@ -60,7 +60,7 @@ describe('Locale select', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.removeItem('items.sp-tarkov.com-prefered-locale') window.localStorage.removeItem('db.sp-tarkov.com-prefered-locale')
} }
}) })
}) })
@ -77,7 +77,7 @@ describe('Locale select', () => {
before(() => { before(() => {
cy.visit('/', { cy.visit('/', {
onBeforeLoad: function (window) { onBeforeLoad: function (window) {
window.localStorage.setItem('items.sp-tarkov.com-prefered-locale', 'locale2') window.localStorage.setItem('db.sp-tarkov.com-prefered-locale', 'locale2')
} }
}) })
}) })

View File

@ -1,11 +1,40 @@
import condensedMilk from "../fixtures/condensed_milk.json";
export {}; export {};
describe('Search area', ()=>{ describe('Search area', ()=>{
beforeEach(() => { beforeEach(() => {
cy.intercept('GET','**/api/locales', { cy.intercept({
method: 'GET',
url: '**/api/locales'
}, {
statusCode: 200, statusCode: 200,
body: ['locale1', 'locale2'] body: ['locale1', 'locale2']
}); }).as('getLocalesWithoutData');
cy.intercept({
method: 'GET',
url: '**/api/item?*'
}, {
statusCode: 200,
body: condensedMilk
})
cy.intercept({
method: 'POST',
url: '**/api/search*'
}, {
statusCode: 200,
body: []
})
cy.intercept({
method: 'GET',
url: '**/api/item/hierarchy*'
}, {
statusCode: 200,
body: condensedMilk
})
cy.visit('/'); cy.visit('/');
}) })
@ -31,4 +60,19 @@ describe('Search area', ()=>{
.should('be.visible') .should('be.visible')
.contains('p', 'No data to display'); .contains('p', 'No data to display');
}) })
describe('Searching', () => {
it('Search using ID displays the json with locale', () => {
cy.get('#search-autocomplete')
.type(condensedMilk.item._id)
.type('{ENTER}')
.should('have.value', condensedMilk.locale.Name)
cy.get(`.react-json-view .object-key-val > .pushed-content > .object-content > .variable-row > .variable-value > div > .string-value:contains("${condensedMilk.item._id}")`)
.should('have.length', 1)
.invoke('text')
.should('eq', `"${condensedMilk.item._id}"`);
cy.get(`.react-json-view .object-key-val > .pushed-content > .object-content > .variable-row > .variable-value > div > .string-value:contains("${condensedMilk.locale.Name}")`)
.should('have.length.above', 0);
})
})
}) })

View File

@ -0,0 +1,82 @@
import condensedMilk from '../fixtures/condensed_milk.json'
export {};
describe('Url check', () => {
beforeEach(() => {
cy.window()
.its("sessionStorage")
.invoke("removeItem", "db.sp-tarkov.com-locales");
cy.intercept({
method: 'GET',
url: '**/api/locales'
}, {
statusCode: 200,
body: []
}).as('getLocalesWithoutData');
cy.intercept({
method: 'GET',
url: '**/api/item?*'
}, {
statusCode: 200,
body: condensedMilk
})
cy.intercept({
method: 'POST',
url: '**/api/search*'
}, {
statusCode: 200,
body: []
})
cy.intercept({
method: 'GET',
url: '**/api/item/hierarchy*'
}, {
statusCode: 200,
body: condensedMilk
})
})
afterEach(() => {
cy.clearLocalStorage();
})
describe('Check page not found', () => {
it('Invalid url should redirect to page not found', () => {
cy.visit('/ABC')
cy.get('#not-found-message').contains("This page does not exist !");
})
})
describe('Check root redirection', () => {
it('Root should redirect to search', () => {
cy.visit('/')
cy.url().should("include", "/search");
})
})
describe('Check url changes with search input', () => {
it('ID in url applies search', () => {
cy.visit(`/search/${condensedMilk.item._id}`);
cy.get('#search-autocomplete').should('have.value', condensedMilk.locale.Name)
})
it('Search reflects in url', () => {
cy.visit(`/`);
cy.get('#search-autocomplete')
.type(condensedMilk.item._id)
.type('{ENTER}')
.should('have.value', condensedMilk.locale.Name);
cy.url().should("include", `/search/${condensedMilk.item._id}`);
cy.get(`.react-json-view .object-key-val > .pushed-content > .object-content > .variable-row > .variable-value > div > .string-value:contains("${condensedMilk.item._id}")`)
.should('have.length', 1)
.invoke('text')
.should('eq', `"${condensedMilk.item._id}"`);
})
})
})

View File

@ -0,0 +1,15 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -4,12 +4,16 @@ import { ItemData } from '../dto/ItemData';
import { useGlobalState } from '../state/GlobalState'; import { useGlobalState } from '../state/GlobalState';
const handleFetch = async (url: string, init?: RequestInit | undefined): Promise<any | null> => { const handleFetch = async (url: string, init?: RequestInit | undefined): Promise<any | null> => {
const resp = await fetch(url, { ...init, mode: 'cors' }) try {
if (resp.status === 200) { const resp = await fetch(url, { ...init, mode: 'cors' })
const jsonResponse = await resp.json(); if (resp.status === 200) {
return jsonResponse !== undefined ? jsonResponse : null; const jsonResponse = await resp.json();
} else if (resp.status >= 400) { return jsonResponse !== undefined ? jsonResponse : null;
console.error(resp) } else if (resp.status >= 400) {
console.warn(resp)
}
}catch (e) {
console.warn(e)
} }
return null; return null;
} }

View File

@ -1,10 +1,10 @@
export enum LocalStorageKeys { export enum LocalStorageKeys {
PREFERED_COLOR_SCHEME = 'items.sp-tarkov.com-prefered-color-scheme', PREFERED_COLOR_SCHEME = 'db.sp-tarkov.com-prefered-color-scheme',
PREFERED_JSON_THEME = 'items.sp-tarkov.com-prefered-json-theme', PREFERED_JSON_THEME = 'db.sp-tarkov.com-prefered-json-theme',
PREFERED_LOCALE = 'items.sp-tarkov.com-prefered-locale' PREFERED_LOCALE = 'db.sp-tarkov.com-prefered-locale'
} }
export enum SessionStorageKeys { export enum SessionStorageKeys {
LOCALES = 'items.sp-tarkov.com-locales', LOCALES = 'db.sp-tarkov.com-locales',
ITEMS_HIERARCHY = 'items.sp-tarkov.com-items-hierarchy', ITEMS_HIERARCHY = 'db.sp-tarkov.com-items-hierarchy',
} }

View File

@ -4,10 +4,10 @@ import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App/>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -0,0 +1,26 @@
import {Box} from '@mui/material'
import {NavigationBreadcrumb} from './mainPageComponents/NavigationBreadcrumb'
import {SearchArea} from './mainPageComponents/SearchArea'
import {makeStyles} from "@mui/styles";
import React from "react";
const useStyles = makeStyles(() => ({
searchContainer: {
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
padding: '2vh 2vw 1vh 2vw'
}
}))
export const InteractiveArea = () => {
const classes = useStyles();
return (
<>
<NavigationBreadcrumb/>
<Box className={classes.searchContainer}>
<SearchArea/>
</Box>
</>
)
}

View File

@ -1,11 +1,13 @@
import {Box, Theme} from '@mui/material' import {Box} from '@mui/material'
import {Footer} from '../components/Footer' import {Footer} from '../components/Footer'
import {Header} from '../components/Header' import {Header} from '../components/Header'
import {NavigationBreadcrumb} from './mainPageComponents/NavigationBreadcrumb'
import {SearchArea} from './mainPageComponents/SearchArea'
import {makeStyles} from "@mui/styles"; import {makeStyles} from "@mui/styles";
import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
import React from "react";
import {InteractiveArea} from "./InteractiveArea";
import {PageNotFound} from "./PageNotFound";
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles(() => ({
container: { container: {
background: 'background.default', background: 'background.default',
display: 'flex', display: 'flex',
@ -13,12 +15,6 @@ const useStyles = makeStyles((theme: Theme) => ({
flexGrow: 1, flexGrow: 1,
height: '100vh', height: '100vh',
maxheight: '100vh', maxheight: '100vh',
},
searchContainer: {
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
padding: '2vh 2vw 1vh 2vw'
} }
})) }))
@ -28,10 +24,15 @@ export const MainPage = () => {
<> <>
<Box className={classes.container}> <Box className={classes.container}>
<Header/> <Header/>
<NavigationBreadcrumb/> <BrowserRouter>
<Box className={classes.searchContainer}> <Routes>
<SearchArea/> <Route path="/search" element={<InteractiveArea/>}/>
</Box> <Route path="/search/:id" element={<InteractiveArea/>}/>
<Route path="/404" element={<PageNotFound />}/>
<Route path="/" element={<Navigate replace to="/search"/>}/>
<Route path="*" element={<Navigate replace to="/404" />} />
</Routes>
</BrowserRouter>
<Footer/> <Footer/>
</Box> </Box>
</> </>

View File

@ -0,0 +1,44 @@
import {Box, Theme, Typography} from '@mui/material'
import {NavigationBreadcrumb} from './mainPageComponents/NavigationBreadcrumb'
import {makeStyles} from "@mui/styles";
import React from "react";
const useStyles = makeStyles((theme: Theme) => ({
searchContainer: {
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
padding: '2vh 2vw 1vh 2vw'
},
notFoundAreaHolder: {
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
background: theme.palette.background.paper,
padding: '2vh 2vw 2vh 2vw',
},
notFoundContainer: {
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
width: "100%",
alignItems: "center",
paddingTop: "10vh"
},
}))
export const PageNotFound = () => {
const classes = useStyles();
return (
<>
<NavigationBreadcrumb/>
<Box className={classes.searchContainer}>
<Box className={classes.notFoundAreaHolder}>
<Box className={classes.notFoundContainer}>
<Typography id={'not-found-message'} variant={"h3"}>This page does not exist !</Typography>
</Box>
</Box>
</Box>
</>
)
}

View File

@ -1,197 +1,202 @@
import { SyntheticEvent, useCallback, useEffect, useState } from 'react' import {SyntheticEvent, useCallback, useEffect, useState} from 'react'
import { import {Autocomplete, Box, CircularProgress, Theme, Typography,} from '@mui/material'
Autocomplete, import {makeStyles} from '@mui/styles'
Box,
CircularProgress,
Theme,
Typography,
} from '@mui/material'
import { makeStyles } from '@mui/styles'
import TextField from '@mui/material/TextField' import TextField from '@mui/material/TextField'
import ReactJson from 'react-json-view' import ReactJson from 'react-json-view'
import { import {getItem, getItemHierarchy, searchItem,} from '../../dataaccess/ItemBackend'
getItem, import {ItemOption} from '../../dto/ItemOption'
getItemHierarchy, import {useGlobalState} from '../../state/GlobalState'
searchItem, import {useNavigate, useParams} from "react-router-dom";
} from '../../dataaccess/ItemBackend'
import { ItemOption } from '../../dto/ItemOption'
import { useGlobalState } from '../../state/GlobalState'
interface IItemOption { interface IItemOption {
id?: string id?: string
name?: string name?: string
shortName?: string shortName?: string
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
searchAreaHolder: { searchAreaHolder: {
display: 'flex', display: 'flex',
flexGrow: 1, flexGrow: 1,
flexDirection: 'column', flexDirection: 'column',
background: theme.palette.background.paper, background: theme.palette.background.paper,
padding: '2vh 2vw 2vh 2vw', padding: '2vh 2vw 2vh 2vw',
}, },
jsonHolder: { jsonHolder: {
display: 'flex', display: 'flex',
flexGrow: 1, flexGrow: 1,
alignItems: 'center', alignItems: 'center',
flexDirection: 'column', flexDirection: 'column',
background: theme.palette.background.paper, background: theme.palette.background.paper,
maxHeight: '80vh', maxHeight: '80vh',
}, },
autocomplete: {}, autocomplete: {},
})) }))
export const SearchArea = () => { export const SearchArea = () => {
const classes = useStyles() const classes = useStyles()
const preferedLocale = useGlobalState((state) => state.preferedLocale) const params = useParams();
const preferedJsonViewerTheme = useGlobalState( const navigate = useNavigate();
useCallback((state) => state.preferedJsonViewerTheme, []), const preferedLocale = useGlobalState((state) => state.preferedLocale)
) const preferedJsonViewerTheme = useGlobalState(
const [searchInputState, setSearchInput] = useGlobalState((state) => [ useCallback((state) => state.preferedJsonViewerTheme, []),
state.searchInput,
state.setSearchInput,
])
const [setHierarchy, initHierarchy] = useGlobalState((state) => [state.setHierarchy, state.initHierarchy])
const [selectedItem, setSelectedItem] = useGlobalState((state) => [
state.selectedItem,
state.setSelectedItem,
])
const [selectOptions, setSelecteOptions] = useState<ItemOption[]>([])
const [isbusy, setIsBusy] = useState<boolean>(false)
const searchThreshold = 3
const handleNameInput = async (input: string) => {
const searchResults = await searchItem(input, preferedLocale)
console.log(searchResults)
const options = searchResults?.map((res) => ({
id: res.item._id,
name: res.locale.Name ? res.locale.Name : res.item._name,
shortName: res.locale.ShortName
}))
setSelecteOptions(options ? options : [])
}
const handleIDInput = useCallback(async (input: string) => {
const itemJson = await getItem(input, preferedLocale)
if (!itemJson) {
setSelectedItem(undefined)
setSearchInput('')
return;
}
setSelectedItem(itemJson)
const itemObj = {
id: itemJson.item._id,
name: itemJson.locale.Name ? itemJson.locale.Name : itemJson.item._name,
shortName: itemJson.locale.ShortName
}
setSelecteOptions([itemObj])
setSearchInput(itemObj.name)
// Update hierachy
const itemHierarchy = await getItemHierarchy(
itemJson.item,
preferedLocale,
) )
setHierarchy(itemHierarchy ? itemHierarchy : {}) const [searchInputState, setSearchInput] = useGlobalState((state) => [
// eslint-disable-next-line state.searchInput,
}, []) // Need to only be created on startup state.setSearchInput,
])
const [setHierarchy, initHierarchy] = useGlobalState((state) => [state.setHierarchy, state.initHierarchy])
const [selectedItem, setSelectedItem] = useGlobalState((state) => [
state.selectedItem,
state.setSelectedItem,
])
const [selectOptions, setSelecteOptions] = useState<ItemOption[]>([])
const [isbusy, setIsBusy] = useState<boolean>(false)
const searchThreshold = 3
useEffect(() => initHierarchy(), [initHierarchy]) const handleNameInput = useCallback(async (input: string) => {
const searchResults = await searchItem(input, preferedLocale)
const options = searchResults?.map((res) => ({
id: res.item._id,
name: res.locale.Name ? res.locale.Name : res.item._name,
shortName: JSON.stringify(res.locale.ShortName)
}))
setSelecteOptions(options ? options : [])
}, [preferedLocale])
useEffect(() => { const handleIDInput = useCallback(async (input: string) => {
if (searchInputState && searchInputState.match(/([a-z0-9]{24})/)) { const itemJson = await getItem(input, preferedLocale)
handleIDInput(searchInputState) if (!itemJson) {
} setSelectedItem(undefined)
}, [handleIDInput, searchInputState]) setSearchInput('')
return;
}
const handleInput = async (input: string) => { setSelectedItem(itemJson)
if (!input || input.length < searchThreshold || isbusy) { const itemObj = {
setSelectedItem(undefined) id: itemJson.item._id,
setSelecteOptions([]) name: itemJson.locale.Name ? itemJson.locale.Name : itemJson.item._name,
setIsBusy(false) shortName: itemJson.locale.ShortName
return }
} setSelecteOptions([itemObj])
setIsBusy(true) setSearchInput(itemObj.name)
if (input.match(/([a-z0-9]{24})/)) await handleIDInput(input) // Update hierachy
else await handleNameInput(input) const itemHierarchy = await getItemHierarchy(
itemJson.item,
preferedLocale,
)
setHierarchy(itemHierarchy ? itemHierarchy : {})
// eslint-disable-next-line
}, []) // Need to only be created on startup
setIsBusy(false) useEffect(() => initHierarchy(), [initHierarchy])
}
const formatDisplayItems = () => { useEffect(()=>{
// If loading if (selectedItem){
if (isbusy) return <CircularProgress size={100} /> navigate(`/search/${selectedItem.item._id}`)
}
},[selectedItem, navigate])
// If finished loading useEffect(() => {
if (selectedItem){ if (searchInputState && searchInputState.match(/([a-z0-9]{24})/)) {
return ( handleIDInput(searchInputState)
<ReactJson }
src={selectedItem} }, [handleIDInput, searchInputState])
theme={preferedJsonViewerTheme}
style={{
marginTop: '2vh',
width: '100%',
overflowY: 'auto',
display: 'flex',
}}
/>
)
}
else return <Typography id='search-no-data'>No data to display</Typography>
}
const findOptionValue = (option: ItemOption, value: ItemOption): boolean => { const handleInput = useCallback(async (input: string) => {
return option.name?.toLocaleLowerCase() === value.name?.toLocaleLowerCase() if (!input || input.length < searchThreshold || isbusy) {
|| option.id?.toLocaleLowerCase() === value.id?.toLocaleLowerCase() setSelectedItem(undefined)
|| option.shortName?.toLocaleLowerCase() === value.shortName?.toLocaleLowerCase() setSelecteOptions([])
} setIsBusy(false)
return
}
setIsBusy(true)
return ( if (input.match(/([a-z0-9]{24})/)) await handleIDInput(input)
<Box className={classes.searchAreaHolder}> else await handleNameInput(input)
<Autocomplete
id='search-autocomplete' setIsBusy(false)
options={selectOptions.map((elt) => ({name: elt.name, shortName: elt.shortName, id: elt.id}))} }, [handleIDInput, handleNameInput, isbusy, setSelectedItem])
getOptionLabel={(option)=> option.name ? option.name : option.id}
isOptionEqualToValue={(option, value) => findOptionValue(option, value)} useEffect(() => {
open={!isbusy && searchInputState.length >= searchThreshold && (searchInputState !== selectedItem?.locale.Name && searchInputState !== selectedItem?.item._name)} if (!searchInputState && params.id) {
className={classes.autocomplete} const newId = params.id.trim();
inputValue={searchInputState ? searchInputState : ''} console.log(newId);
onInputChange={async (evt: SyntheticEvent, newValue: string) => { setSearchInput(newId);
if (!evt) return (async () => await handleInput(newId))();
setSelectedItem(undefined) }
setSearchInput(newValue) }, [params, searchInputState, setSearchInput, handleInput, navigate])
await handleInput(newValue.trim())
}} const formatDisplayItems = () => {
value={(()=>{ // If loading
const selectedOption = selectOptions.find(elt => elt.id === searchInputState || elt.name === searchInputState); if (isbusy) return <CircularProgress size={100}/>
return selectedOption ? selectedOption : null;
})()} // If finished loading
onChange={async (event: SyntheticEvent, newValue: IItemOption | null) => { if (selectedItem) {
if (newValue) { return (
const selectedOption = selectOptions.find( <ReactJson
(elt) => elt.name === newValue.name, src={selectedItem}
theme={preferedJsonViewerTheme}
style={{
marginTop: '2vh',
width: '100%',
overflowY: 'auto',
display: 'flex',
}}
/>
) )
if (selectedOption) await handleIDInput(selectedOption.id) } else return <Typography id='search-no-data'>No data to display</Typography>
} }
}}
renderInput={(params) =>( const findOptionValue = (option: ItemOption, value: ItemOption): boolean => {
<TextField {...params} label="Search by name or ID" /> return option.name?.toLocaleLowerCase() === value.name?.toLocaleLowerCase()
)} || option.id?.toLocaleLowerCase() === value.id?.toLocaleLowerCase()
renderOption={(props, option, state) => ( || option.shortName?.toLocaleLowerCase() === value.shortName?.toLocaleLowerCase()
<li {...props} key={option.id}><Typography>{option.name}</Typography></li> }
)}
filterOptions={(options, state) => options.filter(elt =>{ return (
console.log(elt.shortName?.toLocaleLowerCase(), state.inputValue.toLocaleLowerCase(), elt.shortName?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())) <Box className={classes.searchAreaHolder}>
return (elt.name?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase()) <Autocomplete
|| elt.id?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase()) id='search-autocomplete'
|| elt.shortName?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())) options={selectOptions.map((elt) => ({name: elt.name, shortName: elt.shortName, id: elt.id}))}
})} getOptionLabel={(option) => option.name ? option.name : option.id}
filterSelectedOptions isOptionEqualToValue={(option, value) => findOptionValue(option, value)}
/> open={!isbusy && searchInputState.length >= searchThreshold && (searchInputState !== selectedItem?.locale.Name && searchInputState !== selectedItem?.item._name)}
<Box className={classes.jsonHolder}>{formatDisplayItems()}</Box> className={classes.autocomplete}
</Box> inputValue={searchInputState ? searchInputState : ''}
) onInputChange={async (evt: SyntheticEvent, newValue: string) => {
if (!evt) return
setSelectedItem(undefined)
setSearchInput(newValue)
await handleInput(newValue.trim())
}}
value={(() => {
const selectedOption = selectOptions.find(elt => elt.id === searchInputState || elt.name === searchInputState);
return selectedOption ? selectedOption : null;
})()}
onChange={async (event: SyntheticEvent, newValue: IItemOption | null) => {
if (newValue) {
const selectedOption = selectOptions.find(
(elt) => elt.name === newValue.name,
)
if (selectedOption) await handleIDInput(selectedOption.id)
}
}}
renderInput={(params) => (
<TextField {...params} label="Search by name or ID"/>
)}
renderOption={(props, option ) => (
<li {...props} key={option.id}><Typography>{option.name}</Typography></li>
)}
filterOptions={(options, state) => options.filter(elt => {
return (elt.name?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())
|| elt.id?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())
|| elt.shortName?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase()))
})}
filterSelectedOptions
/>
<Box className={classes.jsonHolder}>{formatDisplayItems()}</Box>
</Box>
)
} }

View File

@ -57,8 +57,7 @@ export const useGlobalState = create<GlobalState>((set) => ({
localesList: [], localesList: [],
refreshLocalesList: async () => { refreshLocalesList: async () => {
const locales = sessionStorage.getItem(SessionStorageKeys.LOCALES); const locales = sessionStorage.getItem(SessionStorageKeys.LOCALES);
const localesList = locales !== null && locales !== undefined && locales !== 'undefined' ? JSON.parse(locales) : await getLocaleList() const localesList = locales !== null && locales !== undefined && locales !== 'undefined' && locales !== 'null' ? JSON.parse(locales) : await getLocaleList()
console.log(localesList)
if (!locales) sessionStorage.setItem(SessionStorageKeys.LOCALES, JSON.stringify(localesList ? localesList : null)) if (!locales) sessionStorage.setItem(SessionStorageKeys.LOCALES, JSON.stringify(localesList ? localesList : null))
set((_state) => ({ localesList: localesList ? localesList : [] })) set((_state) => ({ localesList: localesList ? localesList : [] }))
}, },

View File

@ -1282,6 +1282,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.7.6":
version "7.16.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.15.4", "@babel/template@^7.3.3", "@babel/template@^7.4.4": "@babel/template@^7.10.4", "@babel/template@^7.15.4", "@babel/template@^7.3.3", "@babel/template@^7.4.4":
version "7.15.4" version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194"
@ -1385,10 +1392,10 @@
debug "4.2.0" debug "4.2.0"
find-yarn-workspace-root "^2.0.0" find-yarn-workspace-root "^2.0.0"
"@cypress/request@^2.88.6": "@cypress/request@^2.88.7":
version "2.88.6" version "2.88.7"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.7.tgz#386d960ab845a96953723348088525d5a75aaac4"
integrity sha512-z0UxBE/+qaESAHY9p9sM2h8Y4XqtsbDCt0/DPOrqA/RZgKi4PkxdpXyK4wCCnSk1xHqWHZZAE+gV6aDAR6+caQ== integrity sha512-FTULIP2rnDJvZDT9t6B4nSfYR40ue19tVmv3wUcY05R9/FPCoMl1nAPJkzWzBCo7ltVn5ThQTbxiMoGBN7k0ig==
dependencies: dependencies:
aws-sign2 "~0.7.0" aws-sign2 "~0.7.0"
aws4 "^1.8.0" aws4 "^1.8.0"
@ -4876,12 +4883,12 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@^8.6.0: cypress@9.0.0:
version "8.6.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.6.0.tgz#8d02fa58878b37cfc45bbfce393aa974fa8a8e22" resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.0.0.tgz#8c496f7f350e611604cc2f77b663fb81d0c235d2"
integrity sha512-F7qEK/6Go5FsqTueR+0wEw2vOVKNgk5847Mys8vsWkzPoEKdxs+7N9Y1dit+zhaZCLtMPyrMwjfA53ZFy+lSww== integrity sha512-/93SWBZTw7BjFZ+I9S8SqkFYZx7VhedDjTtRBmXO0VzTeDbmxgK/snMJm/VFjrqk/caWbI+XY4Qr80myDMQvYg==
dependencies: dependencies:
"@cypress/request" "^2.88.6" "@cypress/request" "^2.88.7"
"@cypress/xvfb" "^1.2.4" "@cypress/xvfb" "^1.2.4"
"@types/node" "^14.14.31" "@types/node" "^14.14.31"
"@types/sinonjs__fake-timers" "^6.0.2" "@types/sinonjs__fake-timers" "^6.0.2"
@ -4916,7 +4923,6 @@ cypress@^8.6.0:
ospath "^1.2.2" ospath "^1.2.2"
pretty-bytes "^5.6.0" pretty-bytes "^5.6.0"
proxy-from-env "1.0.0" proxy-from-env "1.0.0"
ramda "~0.27.1"
request-progress "^3.0.0" request-progress "^3.0.0"
supports-color "^8.1.1" supports-color "^8.1.1"
tmp "~0.2.1" tmp "~0.2.1"
@ -6802,6 +6808,13 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
history@5, history@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.1.0.tgz#2e93c09c064194d38d52ed62afd0afc9d9b01ece"
integrity sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==
dependencies:
"@babel/runtime" "^7.7.6"
hmac-drbg@^1.0.1: hmac-drbg@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -10838,11 +10851,6 @@ raf@^3.4.1:
dependencies: dependencies:
performance-now "^2.1.0" performance-now "^2.1.0"
ramda@~0.27.1:
version "0.27.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -10969,6 +10977,21 @@ react-refresh@^0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-router-dom@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.0.2.tgz#860cefa697b9d4965eced3f91e82cdbc5995f3ad"
integrity sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==
dependencies:
history "^5.1.0"
react-router "6.0.2"
react-router@6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.0.2.tgz#bd2b0fa84fd1d152671e9f654d9c0b1f5a7c86da"
integrity sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==
dependencies:
history "^5.1.0"
react-scripts@4.0.3: react-scripts@4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.3.tgz#b1cafed7c3fa603e7628ba0f187787964cb5d345" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.3.tgz#b1cafed7c3fa603e7628ba0f187787964cb5d345"
@ -13172,7 +13195,7 @@ w3c-xmlserializer@^2.0.0:
dependencies: dependencies:
xml-name-validator "^3.0.0" xml-name-validator "^3.0.0"
wait-on@6.0.0: wait-on@6.0.0, wait-on@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.0.tgz#7e9bf8e3d7fe2daecbb7a570ac8ca41e9311c7e7" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.0.tgz#7e9bf8e3d7fe2daecbb7a570ac8ca41e9311c7e7"
integrity sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw== integrity sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==