diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4452f2286..94e3ffe66 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -300,6 +300,14 @@ jobs: repository: ${{ github.repository }} client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' + - name: Repository Dispatch stage.ocelot.social + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ github.token }} + event-type: trigger-build-success + repository: 'Ocelot-Social-Community/stage.ocelot.social' + client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' + # ############################################################################## # # JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ###################################### # ############################################################################## diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46d80241f..1740c09fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -329,19 +329,16 @@ jobs: - name: backend | docker-compose run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend - name: cypress | Fullstack tests + id: e2e-tests run: | yarn install yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} ) ########################################################################## - # UPLOAD SCREENSHOTS & VIDEO ############################################# + # UPLOAD SCREENSHOTS - IF TESTS FAIL ##################################### ########################################################################## - - name: Upload Artifact + - name: Full stack tests | if any test failed, upload screenshots + if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} uses: actions/upload-artifact@v3 with: name: cypress-screenshots path: cypress/screenshots/ - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: cypress-videos - path: cypress/videos/ diff --git a/README.md b/README.md index 8ddf97fe5..06e8d4929 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ Prepare database once before you start by running the following command in a sec ```bash # in main folder while docker-compose is up -$ docker-compose exec backend yarn run db:migrate init +$ docker compose exec backend yarn run db:migrate init +$ docker compose exec backend yarn run db:migrate up ``` Then clear and seed database by running the following command as well in the second terminal: diff --git a/backend/README.md b/backend/README.md index 03724ce54..98b0e7218 100644 --- a/backend/README.md +++ b/backend/README.md @@ -81,8 +81,7 @@ More details about our GraphQL playground and how to use it with ocelot.social c ### Database Indexes and Constraints -Database indexes and constraints need to be created when the database and the -backend is running: +Database indexes and constraints need to be created and upgraded when the database and the backend are running: {% tabs %} {% tab title="Docker" %} @@ -98,6 +97,11 @@ $ docker compose exec backend yarn prod:migrate init $ docker compose exec backend /bin/sh -c "yarn prod:migrate init" ``` +```bash +# in main folder with docker compose running +$ docker exec backend yarn run db:migrate up +``` + {% endtab %} {% tab title="Without Docker" %} @@ -107,6 +111,11 @@ $ docker compose exec backend /bin/sh -c "yarn prod:migrate init" yarn run db:migrate init ``` +```bash +# in backend/ with database running (In docker or local) +yarn run db:migrate up +``` + {% endtab %} {% endtabs %} @@ -134,6 +143,8 @@ $ docker exec backend yarn run db:reset $ docker-compose down -v # if container is not running, run this command to set up your database indexes and constraints $ docker exec backend yarn run db:migrate init +# And then upgrade the indexes and const +$ docker exec backend yarn run db:migrate up ``` {% endtab %} diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index ecc792736..44af5bbea 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -23,6 +23,7 @@ export const cleanDatabase = async (options = {}) => { return transaction.run( ` MATCH (everything) + WHERE NOT 'Migration' IN labels(everything) DETACH DELETE everything `, ) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 4a13dcc88..5ec1700b9 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -16,6 +16,7 @@ export default { Group: async (_object, params, context, _resolveInfo) => { const { isMember, id, slug, first, offset } = params let pagination = '' + const orderBy = 'ORDER BY group.createdAt DESC' if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}` const matchParams = { id, slug } removeUndefinedNullValuesFromObject(matchParams) @@ -29,6 +30,7 @@ export default { WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} + ${orderBy} ${pagination} ` } else { @@ -39,6 +41,7 @@ export default { WITH group WHERE group.groupType IN ['public', 'closed'] RETURN group {.*, myRole: NULL} + ${orderBy} ${pagination} ` } else { @@ -48,6 +51,7 @@ export default { WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} + ${orderBy} ${pagination} ` } diff --git a/cypress/cypress.json b/cypress/cypress.json index dbe8691fa..de323f736 100644 --- a/cypress/cypress.json +++ b/cypress/cypress.json @@ -4,6 +4,7 @@ "ignoreTestFiles": "*.js", "chromeWebSecurity": false, "baseUrl": "http://localhost:3000", + "video":false, "retries": { "runMode": 2, "openMode": 0 diff --git a/deployment/DOCKER_MORE_CLOSELY.md b/deployment/DOCKER_MORE_CLOSELY.md new file mode 100644 index 000000000..67488fe81 --- /dev/null +++ b/deployment/DOCKER_MORE_CLOSELY.md @@ -0,0 +1,33 @@ +# Docker + +## Apple M1 Platform + +***Attention:** For using Docker commands in Apple M1 environments!* + +```bash +# set env variable for your shell +$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 +``` + +For even more informations, see [Docker More Closely](#docker-more-closely) + +### Docker Compose Override File For Apple M1 Platform + +For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: + +```bash +# in main folder + +# for production +$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up + +# for production testing Docker images from DockerHub +$ docker compose -f docker-compose.ocelotsocial-branded.yml -f docker-compose.apple-m1.override.yml up + +# only once: init admin user and create indexes and constraints in Neo4j database +$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" +``` + +## Docker More Closely In Main Code + +To get more informations about the Apple M1 platform and to analyze the Docker builds etc. you find our documentation in our main code, [here](https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/DOCKER_MORE_CLOSELY.md). diff --git a/deployment/src/kubernetes/dns.values.template.yaml b/deployment/configurations/example/kubernetes/dns.values.template.yaml similarity index 100% rename from deployment/src/kubernetes/dns.values.template.yaml rename to deployment/configurations/example/kubernetes/dns.values.template.yaml diff --git a/deployment/scripts/cluster.reseed.sh b/deployment/scripts/cluster.reseed.sh new file mode 100755 index 000000000..705e1880a --- /dev/null +++ b/deployment/scripts/cluster.reseed.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# base setup +SCRIPT_PATH=$(realpath $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) + +# configuration +CONFIGURATION=${CONFIGURATION:-"example"} +KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml} + +# clean & seed +kubectl --kubeconfig=${KUBECONFIG} -n default exec -it $(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js" \ No newline at end of file diff --git a/deployment/scripts/cluster.upgrade.sh b/deployment/scripts/cluster.upgrade.sh index 2502a778a..1aec2cf18 100755 --- a/deployment/scripts/cluster.upgrade.sh +++ b/deployment/scripts/cluster.upgrade.sh @@ -7,7 +7,13 @@ SCRIPT_DIR=$(dirname $SCRIPT_PATH) # configuration CONFIGURATION=${CONFIGURATION:-"example"} KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml} -VALUES=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/values.yaml +VALUES=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/values. +DOCKERHUB_OCELOT_TAG=${DOCKERHUB_OCELOT_TAG:-"latest"} # upgrade with helm -helm --kubeconfig=${KUBECONFIG} upgrade ocelot --values ${VALUES} ${SCRIPT_DIR}/../src/kubernetes/ --debug --timeout 10m \ No newline at end of file +helm --kubeconfig=${KUBECONFIG} upgrade ocelot \ + --values ${VALUES} \ + --set appVersion="${DOCKERHUB_OCELOT_TAG}" + ${SCRIPT_DIR}/../src/kubernetes/ \ + --debug \ + --timeout 10m \ No newline at end of file diff --git a/deployment/scripts/secret.generate.sh b/deployment/scripts/secret.generate.sh new file mode 100755 index 000000000..dba958c34 --- /dev/null +++ b/deployment/scripts/secret.generate.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# generate a secret and store it in the SECRET file. +# Note that this overwrites the existing file + +# base setup +SCRIPT_PATH=$(realpath $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) + +# configuration +CONFIGURATION=${CONFIGURATION:-"example"} +SECRET_FILE=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/SECRET + +openssl rand -base64 32 > ${SECRET_FILE} \ No newline at end of file diff --git a/deployment/scripts/secrets.decrypt.sh b/deployment/scripts/secrets.decrypt.sh new file mode 100755 index 000000000..283768ad0 --- /dev/null +++ b/deployment/scripts/secrets.decrypt.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# decrypt secrets in the selected configuration +# Note that existing decrypted files will be overwritten + +# base setup +SCRIPT_PATH=$(realpath $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) + +# configuration +CONFIGURATION=${CONFIGURATION:-"example"} +SECRET=${SECRET} +SECRET_FILE=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/SECRET +FILES=(\ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/.env" \ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml" \ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/values.yaml" \ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/dns.values.yaml" \ + ) + +# Load SECRET from file if it is not set explicitly +if [ -z ${SECRET} ] && [ -f "${SECRET_FILE}" ]; then + SECRET=$(<${SECRET_FILE}) +fi + +# exit when there is no SECRET set +if [ -z ${SECRET} ]; then + echo "No SECRET provided and no SECRET-File found." + exit 1 +fi + +# decrypt +for file in "${FILES[@]}" +do + if [ -f "${file}.enc" ]; then + #gpg --symmetric --batch --passphrase="${SECRET}" --cipher-algo AES256 --output ${file}.enc ${file} + gpg --quiet --batch --yes --decrypt --passphrase="${SECRET}" --output ${file} ${file}.enc + echo "Decrypted ${file}" + fi +done + +echo "DONE" +# gpg --quiet --batch --yes --decrypt --passphrase="${SECRET}" \ +# --output $HOME/secrets/my_secret.json my_secret.json.gpg diff --git a/deployment/scripts/secrets.encrypt.sh b/deployment/scripts/secrets.encrypt.sh new file mode 100755 index 000000000..ef6c87e85 --- /dev/null +++ b/deployment/scripts/secrets.encrypt.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# encrypt secrets in the selected configuration +# Note that existing encrypted files will be overwritten + +# base setup +SCRIPT_PATH=$(realpath $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) + +# configuration +CONFIGURATION=${CONFIGURATION:-"example"} +SECRET=${SECRET} +SECRET_FILE=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/SECRET +FILES=(\ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/.env" \ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml" \ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/values.yaml" \ + "${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/dns.values.yaml" \ + ) + +# Load SECRET from file if it is not set explicitly +if [ -z ${SECRET} ] && [ -f "${SECRET_FILE}" ]; then + SECRET=$(<${SECRET_FILE}) +fi + +# exit when there is no SECRET set +if [ -z ${SECRET} ]; then + echo "No SECRET provided and no SECRET-File found." + exit 1 +fi + +# encrypt +for file in "${FILES[@]}" +do + if [ -f "${file}" ]; then + gpg --symmetric --batch --yes --passphrase="${SECRET}" --cipher-algo AES256 --output ${file}.enc ${file} + echo "Encrypted ${file}" + fi +done + +echo "DONE" diff --git a/deployment/src/kubernetes/.gitignore b/deployment/src/kubernetes/.gitignore deleted file mode 100644 index e0473b0fd..000000000 --- a/deployment/src/kubernetes/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/dns.values.yaml -/nginx.values.yaml -/values.yaml diff --git a/deployment/src/kubernetes/nginx.values.template.yaml b/deployment/src/kubernetes/nginx.values.yaml similarity index 100% rename from deployment/src/kubernetes/nginx.values.template.yaml rename to deployment/src/kubernetes/nginx.values.yaml diff --git a/neo4j/README.md b/neo4j/README.md index df3b5fde6..1ea625d89 100644 --- a/neo4j/README.md +++ b/neo4j/README.md @@ -44,7 +44,7 @@ for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/), -on Archlinux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/) +on Arch linux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/) or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). Just be sure to update the Neo4j connection string and credentials accordingly in `backend/.env`. diff --git a/webapp/components/Button/JoinLeaveButton.vue b/webapp/components/Button/JoinLeaveButton.vue index 9bcb5042a..152039eb0 100644 --- a/webapp/components/Button/JoinLeaveButton.vue +++ b/webapp/components/Button/JoinLeaveButton.vue @@ -6,6 +6,7 @@ :icon="icon" :filled="isMember && !hovered" :danger="isMember && hovered" + v-tooltip="tooltip" @mouseenter.native="onHover" @mouseleave.native="hovered = false" @click.prevent="toggle" @@ -24,6 +25,7 @@ export default { group: { type: Object, required: true }, userId: { type: String, required: true }, isMember: { type: Boolean, required: true }, + isNonePendingMember: { type: Boolean, required: true }, disabled: { type: Boolean, default: false }, loading: { type: Boolean, default: false }, }, @@ -35,17 +37,33 @@ export default { }, computed: { icon() { - if (this.isMember && this.hovered) { - return 'close' - } else { - return this.isMember ? 'check' : 'plus' + if (this.isMember) { + if (this.isNonePendingMember) { + return this.hovered ? 'close' : 'check' + } else { + return this.hovered ? 'close' : 'question-circle' + } } + return 'plus' }, label() { if (this.isMember) { - return this.$t('group.joinLeaveButton.iAmMember') - } else { - return this.$t('group.joinLeaveButton.join') + if (this.isNonePendingMember) { + return this.hovered + ? this.$t('group.joinLeaveButton.leave') + : this.$t('group.joinLeaveButton.iAmMember') + } else { + return this.$t('group.joinLeaveButton.pendingMember') + } + } + return this.$t('group.joinLeaveButton.join') + }, + tooltip() { + return { + content: this.$t('group.joinLeaveButton.tooltip'), + placement: 'right', + show: this.isMember && !this.isNonePendingMember && this.hovered, + trigger: this.isMember && !this.isNonePendingMember ? 'hover' : 'manual', } }, }, diff --git a/webapp/components/Category/index.vue b/webapp/components/Category/index.vue index 8e4014086..ebbaae584 100644 --- a/webapp/components/Category/index.vue +++ b/webapp/components/Category/index.vue @@ -30,6 +30,7 @@ export default { } } .filterActive { - background-color: $color-success-active; + color: $color-primary-inverse; + background-color: $color-primary-active; } diff --git a/webapp/components/FilterMenu/HeaderButton.vue b/webapp/components/FilterMenu/HeaderButton.vue new file mode 100644 index 000000000..780e29ade --- /dev/null +++ b/webapp/components/FilterMenu/HeaderButton.vue @@ -0,0 +1,57 @@ + + + diff --git a/webapp/components/Group/GroupList.vue b/webapp/components/Group/GroupList.vue index 7618e5b57..dbde61ac6 100644 --- a/webapp/components/Group/GroupList.vue +++ b/webapp/components/Group/GroupList.vue @@ -1,9 +1,14 @@ + diff --git a/webapp/components/MasonryGrid/MasonryGrid.vue b/webapp/components/MasonryGrid/MasonryGrid.vue index 00afa31af..f95c0e16e 100644 --- a/webapp/components/MasonryGrid/MasonryGrid.vue +++ b/webapp/components/MasonryGrid/MasonryGrid.vue @@ -30,7 +30,7 @@ export default { /* dirty fix to override broken styleguide inline-styles */ .ds-grid { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)) !important; - gap: 16px !important; + gap: 32px 16px !important; grid-auto-rows: 20px; } diff --git a/webapp/components/NotificationsTable/NotificationsTable.spec.js b/webapp/components/NotificationsTable/NotificationsTable.spec.js index e48610034..0d3560787 100644 --- a/webapp/components/NotificationsTable/NotificationsTable.spec.js +++ b/webapp/components/NotificationsTable/NotificationsTable.spec.js @@ -59,8 +59,8 @@ describe('NotificationsTable.vue', () => { wrapper = Wrapper() }) - it('renders a table', () => { - expect(wrapper.find('.ds-table').exists()).toBe(true) + it('renders a grid table', () => { + expect(wrapper.find('.notification-grid').exists()).toBe(true) }) describe('renders 4 columns', () => { @@ -84,7 +84,7 @@ describe('NotificationsTable.vue', () => { describe('Post', () => { let firstRowNotification beforeEach(() => { - firstRowNotification = wrapper.findAll('tbody tr').at(0) + firstRowNotification = wrapper.findAll('.notification-grid-row').at(0) }) it('renders the author', () => { @@ -117,7 +117,7 @@ describe('NotificationsTable.vue', () => { describe('Comment', () => { let secondRowNotification beforeEach(() => { - secondRowNotification = wrapper.findAll('tbody tr').at(1) + secondRowNotification = wrapper.findAll('.notification-grid-row').at(1) }) it('renders the author', () => { diff --git a/webapp/components/NotificationsTable/NotificationsTable.vue b/webapp/components/NotificationsTable/NotificationsTable.vue index 07db4f9a2..7065b2624 100644 --- a/webapp/components/NotificationsTable/NotificationsTable.vue +++ b/webapp/components/NotificationsTable/NotificationsTable.vue @@ -1,62 +1,108 @@