diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..30d5f8f6a9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +* +!build/* +!nginx.conf.template +!entrypoint.sh diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml new file mode 100644 index 0000000000..91ba9b24aa --- /dev/null +++ b/.github/workflows/ci-actions.yml @@ -0,0 +1,225 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + unit-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [10.x, 12.x, 14.x] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Build + run: | + yarn install + yarn build + - name: Test + run: yarn test --coverage --watchAll=false + - uses: codecov/codecov-action@v1 + with: + flags: unitests + + e2e: + needs: [unit-test] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + browser: [chrome, firefox] + services: + keycloak: + image: quay.io/keycloak/keycloak:12.0.2 + ports: + - 8180:8080 + env: + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + options: >- + --health-cmd "curl --fail http://localhost:8080/auth || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + controls-db: + image: postgres:13.1 + ports: + - 5432:5432 + env: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: controls_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Keycloak Admin CLI + uses: carlosthe19916/keycloak-action@0.4 + with: + server: http://keycloak:8080/auth + username: admin + password: admin + kcadm: create realms -f konveyor-realm.json + - name: Controls API + run: | + docker run -d --name controls --network ${{ job.services.controls-db.network }} --network-alias controls -p 8080:8080 \ + -e QUARKUS_HTTP_PORT=8080 \ + -e QUARKUS_DATASOURCE_USERNAME=user \ + -e QUARKUS_DATASOURCE_PASSWORD=password \ + -e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://controls-db:5432/controls_db \ + -e QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/auth/realms/konveyor \ + -e QUARKUS_OIDC_CLIENT_ID=controls-api \ + -e QUARKUS_OIDC_CREDENTIALS_SECRET=secret quay.io/mrizzi/poc-controls:latest-native + sleep 5s && docker logs controls + - name: Build + run: | + yarn install + yarn build:instrumentation + - name: Cypress run + uses: cypress-io/github-action@v2 + with: + record: false + start: yarn run ui:start + wait-on: "http://localhost:3000" + wait-on-timeout: 120 + config: pageLoadTimeout=100000 + browser: ${{ matrix.browser }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: e2e-screenshots-${{ matrix.os }}-${{ matrix.browser }} + path: cypress/screenshots + - uses: actions/upload-artifact@v1 + if: always() + with: + name: e2e-videos-${{ matrix.os }}-${{ matrix.browser }} + path: cypress/videos + - uses: codecov/codecov-action@v1 + with: + flags: e2etests + + container-images: + if: ${{ github.event_name != 'pull_request' && github.repository_owner == 'konveyor' }} + runs-on: ubuntu-latest + needs: [unit-test, e2e] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Build + run: | + yarn install + yarn build + - name: Push to Quay.io + uses: elgohr/Publish-Docker-Github-Action@3.02 + with: + registry: quay.io + name: konveyor/tackle-ui + username: ${{ secrets.QUAYIO_USERNAME }} + password: ${{ secrets.QUAYIO_PASSWORD }} + dockerfile: Dockerfile + snapshot: false + tags: "main" + + test-container-images: + needs: [container-images] + runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest] + browser: [chrome, firefox] + services: + keycloak: + image: quay.io/keycloak/keycloak:12.0.2 + ports: + - 8180:8080 + env: + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + options: >- + --health-cmd "curl --fail http://localhost:8080/auth || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + controls-db: + image: postgres:13.1 + ports: + - 5432:5432 + env: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: controls_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Keycloak Admin CLI + uses: carlosthe19916/keycloak-action@0.4 + with: + server: http://keycloak:8080/auth + username: admin + password: admin + kcadm: create realms -f konveyor-realm.json + - name: Controls API + run: | + docker run -d --name controls --network ${{ job.services.controls-db.network }} --network-alias controls -p 8080:8080 \ + -e QUARKUS_HTTP_PORT=8080 \ + -e QUARKUS_DATASOURCE_USERNAME=user \ + -e QUARKUS_DATASOURCE_PASSWORD=password \ + -e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://controls-db:5432/controls_db \ + -e QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/auth/realms/konveyor \ + -e QUARKUS_OIDC_CLIENT_ID=controls-api \ + -e QUARKUS_OIDC_CREDENTIALS_SECRET=secret \ + quay.io/mrizzi/poc-controls:latest-native + sleep 5s && docker logs controls + - name: Tackle UI + run: | + docker run -d --name tackle-ui --network ${{ job.services.controls-db.network }} --network-alias tackle-ui -p 3000:8080 \ + -e SSO_REALM=konveyor \ + -e SSO_CLIENT_ID=tackle-ui \ + -e SSO_SERVER_URL=http://keycloak:8080/auth \ + -e CONTROLS_API_URL=http://controls:8080/controls \ + quay.io/konveyor/tackle-ui:main + sleep 5s && docker logs tackle-ui + - name: Cypress run + uses: cypress-io/github-action@v2 + with: + record: false + wait-on: "http://localhost:3000" + wait-on-timeout: 120 + config: pageLoadTimeout=100000 + browser: ${{ matrix.browser }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_auth_base_url: http://localhost:3000/auth + CYPRESS_controls_base_url: http://localhost:8080/controls + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: container-screenshots-${{ matrix.os }}-${{ matrix.browser }} + path: cypress/screenshots + - uses: actions/upload-artifact@v1 + if: always() + with: + name: container-videos-${{ matrix.os }}-${{ matrix.browser }} + path: cypress/videos diff --git a/.github/workflows/test-containers.yml b/.github/workflows/test-containers.yml new file mode 100644 index 0000000000..168ae7e194 --- /dev/null +++ b/.github/workflows/test-containers.yml @@ -0,0 +1,32 @@ +name: Test containers + +on: [push] + +jobs: + container-images: + if: ${{ github.event_name != 'pull_request' && github.repository_owner != 'konveyor' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Build + run: | + yarn install + yarn build + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + - name: Push to GitHub Packages + uses: elgohr/Publish-Docker-Github-Action@3.02 + with: + registry: docker.pkg.github.com + name: ${{ github.repository_owner }}/${{ github.event.repository.name }}/tackle-ui + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + dockerfile: Dockerfile + snapshot: false + tags: "${{ steps.extract_branch.outputs.branch }}" diff --git a/.gitignore b/.gitignore index 67045665db..0e987a71cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,33 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript +# dependencies +/node_modules +/.pnp +.pnp.js -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# testing +/coverage -# Dependency directories -node_modules/ -jspm_packages/ +# production +/build -# TypeScript v1 declaration files -typings/ +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# Optional eslint cache .eslintcache -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ +# cypress +cypress/videos +cypress/screenshots +.nyc_output -# TernJS port file -.tern-port +# VSCode +.vscode/* \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 0000000000..cd7475934a --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,11 @@ +module.exports = { + "stories": [ + "../src/**/*.stories.mdx", + "../src/**/*.stories.@(js|jsx|ts|tsx)" + ], + "addons": [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/preset-create-react-app" + ] +} \ No newline at end of file diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 0000000000..df2876a8b6 --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,5 @@ + +import { addons } from '@storybook/addons'; +import theme from './theme'; +addons.setConfig({ theme }); + diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 0000000000..5cc66f1dd6 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,6 @@ + +import "../node_modules/@patternfly/patternfly/patternfly.css"; + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, +} \ No newline at end of file diff --git a/.storybook/theme.js b/.storybook/theme.js new file mode 100644 index 0000000000..4583d3227d --- /dev/null +++ b/.storybook/theme.js @@ -0,0 +1,9 @@ + +import { create } from '@storybook/theming/create'; +export default create({ + base: 'light', + brandTitle: 'Controls', + brandUrl: 'https://github.com/konveyor/tackle-ui', + brandImage: 'https://raw.githubusercontent.com/konveyor/tackle-ui/main/public/logo192.png', +}); + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..c0ed2a8238 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM registry.access.redhat.com/ubi8/nginx-118 + +# Add application sources to a directory that the assemble script expects them +# and set permissions so that the container runs without root access +USER 0 +ADD nginx.conf.template ./ +ADD build /tmp/src/ +RUN chown -R 1001:0 /tmp/src +USER 1001 + +# Let the assemble script to install the dependencies +RUN /usr/libexec/s2i/assemble + +# Replace ENV VARIABLES +COPY entrypoint.sh / +ENTRYPOINT ["/entrypoint.sh"] + +# Run script uses standard ways to run the application +CMD /usr/libexec/s2i/run diff --git a/README.md b/README.md index 9124f6b646..bd6a6295fa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,96 @@ -# Tackle Web UI +# Tackle UI -This repository contains the front end components for the services defined within the [tackle-controls](https://github.com/konveyor/tackle-controls), [tackle-application-inventory](https://github.com/konveyor/tackle-application-inventory) and [tackle-pathfinder](https://github.com/konveyor/tackle-pathfinder) repositories. +The UI web console for the tackle project. + +# Development + +Follow the set of instructions below to start the UI in development mode. + +# Clone repository + +```shell +git clone https://github.com/konveyor/tackle-ui +``` + +# Start dependencies + +This project depends on other resources: + +- Keycloak +- Controls + +## Start dependencines with docker-compose + +Start the dependencies using `docker-compose.yml`: + +```shell +docker-compose up +``` + +## Start dependencies with Docker + +### Create a docker network + +```shell +docker network create konveyor +``` + +### Start keycloak + +```shell +docker run -d \ +--network konveyor --network-alias keycloak \ +-p 8180:8080 \ +-e KEYCLOAK_USER=admin \ +-e KEYCLOAK_PASSWORD=admin \ +-e KEYCLOAK_IMPORT=/tmp/konveyor-realm.json \ +-e DB_VENDOR=h2 \ +-v $(pwd)/konveyor-realm.json:/tmp/konveyor-realm.json:z \ +quay.io/keycloak/keycloak:12.0.2 +``` + +### Start controls + +Start the controls' database: + +```shell +docker run -d \ +--network konveyor --network-alias controls-db \ +-p 5432:5432 \ +-e POSTGRES_USER=user \ +-e POSTGRES_PASSWORD=password \ +-e POSTGRES_DB=controls_db \ +postgres:13.1 +``` + +Start the controls: + +```shell +docker run -d \ +--network konveyor --network-alias controls \ +-p 8080:8080 \ +-e QUARKUS_HTTP_PORT=8080 \ +-e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://controls-db:5432/controls_db \ +-e QUARKUS_DATASOURCE_USERNAME=user \ +-e QUARKUS_DATASOURCE_PASSWORD=password \ +-e QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/auth/realms/konveyor \ +-e QUARKUS_OIDC_CLIENT_ID=controls-api \ +-e QUARKUS_OIDC_CREDENTIALS_SECRET=secret \ +quay.io/mrizzi/poc-controls:latest-native +``` + +# Start the UI + +Install the npm dependencies: + +```shell +yarn install +``` + +Start the UI: + +```shell +yarn start +``` + +You should be able to open http://localhost:3000 and start working on the UI. diff --git a/commands.sh b/commands.sh new file mode 100644 index 0000000000..0c0ebc2f20 --- /dev/null +++ b/commands.sh @@ -0,0 +1,56 @@ +yarn add react@^16.13.1 react-dom@^16.13.1 react-router-dom + +yarn add @patternfly/patternfly @patternfly/react-core @patternfly/react-table +yarn add @react-keycloak/web keycloak-js + +yarn add redux react-redux redux-logger redux-thunk + +yarn add @redhat-cloud-services/frontend-components-notifications axios typesafe-actions + +# Dev dependencies + +yarn add -D @types/react @types/react-dom @types/react-router-dom +yarn add -D @types/react-redux @types/redux-logger redux-devtools-extension + +yarn add -D axios-mock-adapter + +yarn add -D enzyme enzyme-adapter-react-16 jest-enzyme @types/enzyme @types/enzyme-adapter-react-16 +yarn add -D @testing-library/react-hooks + +yarn add -D husky lint-staged prettier source-map-explorer + +yarn add -D node-sass@^4.14.1 + +## ------------------------------- +# package.json + +## scripts +# "analyze": "source-map-explorer 'build/static/js/*.js'", + +# "jest": { +# "collectCoverageFrom": [ +# "src/**/*.{js,jsx,ts,tsx}", +# "!/node_modules/", +# "!src/**/*.stories.*" +# ] +# }, +# "husky": { +# "hooks": { +# "pre-commit": "lint-staged" +# } +# }, +# "lint-staged": { +# "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ +# "prettier --write" +# ] +# } + +## ------------------------------- +# setupTests.ts + +# import "@testing-library/jest-dom"; + +# import "jest-enzyme"; +# import { configure } from "enzyme"; +# import Adapter from "enzyme-adapter-react-16"; +# configure({ adapter: new Adapter() }); diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000000..9106e282e3 --- /dev/null +++ b/cypress.json @@ -0,0 +1,12 @@ +{ + "baseUrl": "http://localhost:3000/", + "env": { + "auth_base_url": "http://localhost:8180/auth", + "auth_realm": "konveyor", + "auth_client_id": "tackle-ui", + "controls_base_url": "http://localhost:8080/controls" + }, + "viewportWidth": 1225, + "viewportHeight": 886, + "chromeWebSecurity": false +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/fixtures/users/admin.json b/cypress/fixtures/users/admin.json new file mode 100644 index 0000000000..56408b1635 --- /dev/null +++ b/cypress/fixtures/users/admin.json @@ -0,0 +1,4 @@ +{ + "username": "admin", + "password": "admin" + } \ No newline at end of file diff --git a/cypress/fixtures/users/alice.json b/cypress/fixtures/users/alice.json new file mode 100644 index 0000000000..e83fd16e2f --- /dev/null +++ b/cypress/fixtures/users/alice.json @@ -0,0 +1,4 @@ +{ + "username": "alice", + "password": "alice" +} \ No newline at end of file diff --git a/cypress/integration/businessServiceList.test.js b/cypress/integration/businessServiceList.test.js new file mode 100644 index 0000000000..b55bbdc4c7 --- /dev/null +++ b/cypress/integration/businessServiceList.test.js @@ -0,0 +1,158 @@ +/// + +context("Test business service list", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("alice").as("tokens"); + + cy.get("@tokens").then((tokens) => { + const headers = { + "Content-Type": "application/json", + "Accept": "application/hal+json", + Authorization: "Bearer " + tokens.access_token, + }; + + // Delete all business services + cy.request({ + method: "GET", + headers: headers, + url: `${Cypress.env("controls_base_url")}/business-service?size=1000`, + }).then((result) => { + result.body._embedded["business-service"].forEach((e) => { + cy.request({ + method: "DELETE", + headers: headers, + url: `${Cypress.env("controls_base_url")}/business-service/${e.id}`, + }); + }); + }); + + // Create business services + for (let i = 1; i <= 12; i++) { + cy.request({ + method: "POST", + headers: headers, + body: { + name: `service${i}`, + description: `description${i}`, + }, + url: `${Cypress.env("controls_base_url")}/business-service`, + }); + } + }); + }); + + it("Filtering", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/business-service", + }).as("apiCheck"); + + cy.visit("/controls/business-services"); + + // + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 10); + + // Apply first filter + cy.get("input[aria-label='filter-text']").type("service12"); + cy.get("button[aria-label='search']").click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 1).contains("service12"); + + // Apply second filter + cy.get("input[aria-label='filter-text']").type("service5"); + cy.get("button[aria-label='search']").click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 2).contains("service5"); + }); + + it("Pagination", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/business-service", + }).as("apiCheck"); + + cy.visit("/controls/business-services"); + + // Remember by default the table is sorted by name + + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 10); + cy.get("tbody > tr").contains("service1"); + cy.get("tbody > tr").contains("service7"); + + cy.get("button[data-action='next']").first().click(); + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 2); + cy.get("tbody > tr").contains("service8"); + cy.get("tbody > tr").contains("service9"); + + cy.get("button[data-action='previous']").first().click(); + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 10); + cy.get("tbody > tr").contains("service1"); + cy.get("tbody > tr").contains("service7"); + }); + + it("Edit", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/business-service", + }).as("apiCheck"); + + cy.visit("/controls/business-services"); + + cy.wait("@apiCheck"); + + // Open modal + cy.get("button[aria-label='edit']").first().click(); + + // Verify primary button is disabled + cy.get("button[aria-label='submit']").should("be.disabled"); + + // Fill form + cy.get("input[name='name']").clear().type("my business service"); + cy.get("textarea[name='description']").clear().type("my description"); + + cy.get("button[aria-label='submit']").should("not.be.disabled"); + cy.get("form").submit(); + + cy.wait("@apiCheck"); + + // Verify table + cy.get("tbody > tr").contains("my business service"); + cy.get("tbody > tr").contains("my description"); + }); + + it("Company list - delete", () => { + cy.intercept({ + method: "GET", + path: "/api/controls/business-service*", + }).as("apiCheck"); + + cy.intercept({ + method: "DELETE", + path: "/api/controls/business-service/*", + }).as("apiDeleteCheck"); + + cy.visit("/controls/business-services"); + + cy.wait("@apiCheck"); + + // Verify table has 12 elements + cy.get(".pf-c-options-menu__toggle-text").contains(12); + + // Open delete modal + cy.get("button[aria-label='delete']").first().click(); + cy.get("button[aria-label='confirm']").click(); + + cy.wait("@apiDeleteCheck"); + cy.wait("@apiCheck"); + + // Verify company has been deleted + cy.get(".pf-c-options-menu__toggle-text").contains(11); + }); +}); diff --git a/cypress/integration/newBusinessService.test.js b/cypress/integration/newBusinessService.test.js new file mode 100644 index 0000000000..7918af4367 --- /dev/null +++ b/cypress/integration/newBusinessService.test.js @@ -0,0 +1,59 @@ +/// + +context("Test NewBusinessService", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("alice").as("tokens"); + + cy.get("@tokens").then((tokens) => { + const headers = { + "Content-Type": "application/json", + "Accept": "application/hal+json", + Authorization: "Bearer " + tokens.access_token, + }; + + // Delete all business services + cy.request({ + method: "GET", + headers: headers, + url: `${Cypress.env("controls_base_url")}/business-service?size=1000`, + }).then((result) => { + result.body._embedded["business-service"].forEach((e) => { + cy.request({ + method: "DELETE", + headers: headers, + url: `${Cypress.env("controls_base_url")}/business-service/${e.id}`, + }); + }); + }); + }); + }); + + it("Minimun data", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/business-service", + }).as("apiCheck"); + + cy.visit("/controls/business-services"); + + // Open modal + cy.get("button[aria-label='create-business-service']").click(); + + // Verify primary button is disabled + cy.get("button[aria-label='submit']").should("be.disabled"); + + // Fill form + cy.get("input[name='name']").type("my business service"); + + cy.get("button[aria-label='submit']").should("not.be.disabled"); + cy.get("form").submit(); + + cy.wait("@apiCheck"); + + // Verify table + cy.get("tbody > tr") + .should("have.length", 1) + .contains("my business service"); + }); +}); diff --git a/cypress/integration/template.test.js b/cypress/integration/template.test.js new file mode 100644 index 0000000000..1669bf9b2d --- /dev/null +++ b/cypress/integration/template.test.js @@ -0,0 +1,16 @@ +/// + +context("Test template", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("alice"); + }); + + it("Action buttons disabled when form is invalid", () => { + cy.visit("/"); + + cy.get("#aboutButton").click(); + cy.get(".pf-c-about-modal-box").contains("Source code"); + cy.get("button[aria-label='Close Dialog']").click(); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000000..e670a39ac6 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,28 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + + require('@cypress/code-coverage/task')(on, config) + // include any other plugin code... + + // It's IMPORTANT to return the config object + // with any changed environment variables + return config +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000000..c3419e736a --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,27 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +import "cypress-keycloak-commands"; \ No newline at end of file diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000000..693ab840c5 --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +import '@cypress/code-coverage/support' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..a1945daa17 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: "3" + +services: + keycloak: + image: quay.io/keycloak/keycloak:12.0.2 + ports: + - 8180:8080 + environment: + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + KEYCLOAK_IMPORT: /tmp/konveyor-realm.json + DB_VENDOR: h2 + volumes: + - ./konveyor-realm.json:/tmp/konveyor-realm.json:z + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/auth"] + interval: 10s + timeout: 5s + retries: 5 + + controls-db: + image: postgres:13.1 + ports: + - 5432:5432 + environment: + POSTGRES_DB: controls_db + POSTGRES_USER: user + POSTGRES_PASSWORD: password + healthcheck: + test: ["CMD-SHELL", "pg_isready -U user -d controls_db"] + interval: 10s + timeout: 5s + retries: 5 + + controls: + image: quay.io/mrizzi/poc-controls:latest-native + ports: + - 8080:8080 + environment: + QUARKUS_HTTP_PORT: 8080 + QUARKUS_DATASOURCE_USERNAME: user + QUARKUS_DATASOURCE_PASSWORD: password + QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://controls-db:5432/controls_db + QUARKUS_OIDC_AUTH_SERVER_URL: http://keycloak:8080/auth/realms/konveyor + QUARKUS_OIDC_CLIENT_ID: controls-api + QUARKUS_OIDC_CREDENTIALS_SECRET: secret + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/controls/q/health"] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + keycloak: + condition: service_healthy + controls-db: + condition: service_healthy + + \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000000..8a803718aa --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e + +if [[ -z "$CONTROLS_API_URL" ]]; then + echo "You must provide CONTROLS_API_URL environment variable" 1>&2 + exit 1 +fi + +if [[ -z "$SSO_REALM" ]]; then + echo "You must provide SSO_REALM environment variable" 1>&2 + exit 1 +fi +if [[ -z "$SSO_SERVER_URL" ]]; then + echo "You must provide SSO_SERVER_URL environment variable" 1>&2 + exit 1 +fi +if [[ -z "$SSO_CLIENT_ID" ]]; then + echo "You must provide SSO_CLIENT_ID environment variable" 1>&2 + exit 1 +fi + +if [ -f ./nginx.conf.template ]; then + echo "---> Processing nginx.conf.template configuration file..." + envsubst '${CONTROLS_API_URL} ${SSO_SERVER_URL}' < ./nginx.conf.template > ./nginx.conf + cp -v ./nginx.conf "${NGINX_CONF_PATH}" + rm -f ./nginx.conf +fi + +if [ -f ./keycloak.json.template ]; then + echo "---> Processing keycloak.json.template configuration file..." + envsubst '${SSO_REALM} ${SSO_CLIENT_ID}' < ./keycloak.json.template > ./keycloak.json +fi + +echo "Container started" + +exec "$@" \ No newline at end of file diff --git a/i18next-parser.config.js b/i18next-parser.config.js new file mode 100644 index 0000000000..62a85f5b30 --- /dev/null +++ b/i18next-parser.config.js @@ -0,0 +1,24 @@ +module.exports = { + createOldCatalogs: true, // Save the \_old files + + indentation: 4, // Indentation of the catalog files + + keepRemoved: false, // Keep keys from the catalog that are no longer in code + + lexers: { + js: ["JsxLexer"], + ts: ["JsxLexer"], + jsx: ["JsxLexer"], + tsx: ["JsxLexer"], + + default: ["JsxLexer"], + }, + + locales: ["en", "es"], + + output: "public/locales/$LOCALE/$NAMESPACE.json", + + input: ["src/**/*.{js,jsx,ts,tsx}"], + + verbose: true, +}; diff --git a/konveyor-realm.json b/konveyor-realm.json new file mode 100644 index 0000000000..187e308bdf --- /dev/null +++ b/konveyor-realm.json @@ -0,0 +1,2318 @@ +{ + "id": "konveyor", + "realm": "konveyor", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "d723b5ff-6c33-4152-b0ac-3ad9c1b79e6c", + "name": "admin", + "composite": false, + "clientRole": false, + "containerId": "konveyor", + "attributes": {} + }, + { + "id": "88607edb-72b0-46fb-8d76-1a75a51a50f0", + "name": "user", + "composite": false, + "clientRole": false, + "containerId": "konveyor", + "attributes": {} + }, + { + "id": "85aa1467-987b-4a71-a7e1-92dffc90323b", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "konveyor", + "attributes": {} + }, + { + "id": "ca0cb92d-22d8-4590-8935-22182d2848e3", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "konveyor", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "6849a9ed-4c25-4cc9-88c8-609cd633f1c7", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "08320d88-a140-4511-a094-92eecffa2f31", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "e58675c2-c05c-427c-b920-a978a3928b73", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "9a78c649-6529-477f-a1e0-36d3165407e2", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "227e0d33-7b83-45db-86d5-03467fe24147", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "manage-events", + "view-clients", + "manage-authorization", + "impersonation", + "query-clients", + "view-realm", + "manage-users", + "view-events", + "query-users", + "create-client", + "manage-identity-providers", + "view-identity-providers", + "view-users", + "query-realms", + "view-authorization", + "manage-clients", + "manage-realm" + ] + } + }, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "f7c107c9-2aac-458a-8f12-93986cce61ab", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "b3a531d7-f745-469a-8a0e-a751d48b6d7b", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "7e6891b0-1f91-45c9-81d1-ba2fb825fc38", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "b4e357bf-bb32-4ee1-ac95-1ffc5ee927f6", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "724b6adf-ea10-45b0-a856-05f014f8aca6", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "f29ee958-a37f-428a-b316-669235df7e59", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "f2d6d0de-7ebc-4f0f-a156-f279570ad228", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "52025042-ac41-401b-9657-83d064d04084", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "850038e6-b323-423b-aab7-64788d8dcbea", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "629459d6-1e42-46bb-b313-dd3fa66aca5d", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "e2c72797-82a7-440e-9cea-55f0219b7aaf", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "6d629082-9597-483c-85e3-349b3b6761e6", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "5c5f86a5-2f47-4a58-9ad9-c1b770b3f1e0", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + }, + { + "id": "02b68098-a625-428f-8339-3053d4ab1ba4", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "attributes": {} + } + ], + "security-admin-console": [], + "controls-api": [ + { + "id": "d056105b-2d60-4415-addb-9639ac3bfd74", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "5cda179e-8443-4467-a488-292976d25bf1", + "attributes": {} + } + ], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "6c7bca9c-701c-4caf-9978-68211d766538", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "e91a0a7f-f825-4155-abff-a547a9594d63", + "attributes": {} + } + ], + "account": [ + { + "id": "905b98ab-40a2-4f22-90e3-98a3475f8e7f", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "466f7590-d495-402d-a955-08c54ae31385", + "attributes": {} + }, + { + "id": "e230efe8-767b-4903-8bb9-d612fc4d4c63", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "466f7590-d495-402d-a955-08c54ae31385", + "attributes": {} + }, + { + "id": "1352f674-2227-4c5c-b687-c76d99e3acb6", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "466f7590-d495-402d-a955-08c54ae31385", + "attributes": {} + }, + { + "id": "116701c3-049d-4bd3-95ad-ad838484844e", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "466f7590-d495-402d-a955-08c54ae31385", + "attributes": {} + }, + { + "id": "791910d1-4a2f-46a2-9920-a61c83938a17", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "466f7590-d495-402d-a955-08c54ae31385", + "attributes": {} + }, + { + "id": "0a046e35-6bf2-47e5-8ecb-fffbd2c05a2c", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "466f7590-d495-402d-a955-08c54ae31385", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRoles": [ + "uma_authorization", + "offline_access" + ], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "af134cab-f41c-4675-b141-205f975db679", + "username": "admin", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "type": "password", + "hashedSaltedValue": "NICTtwsvSxJ5hL8hLAuleDUv9jwZcuXgxviMXvR++cciyPtiIEStEaJUyfA9DOir59awjPrHOumsclPVjNBplA==", + "salt": "T/2P5o5oxFJUEk68BRURRg==", + "hashIterations": 27500, + "counter": 0, + "algorithm": "pbkdf2-sha256", + "digits": 0, + "period": 0, + "createdDate": 1554245879354, + "config": {} + } + ], + "disableableCredentialTypes": [ + "password" + ], + "requiredActions": [], + "realmRoles": [ + "admin", + "user" + ], + "notBefore": 0, + "groups": [] + }, + { + "id": "eb4123a3-b722-4798-9af5-8957f823657a", + "username": "alice", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "type": "password", + "hashedSaltedValue": "A3okqV2T/ybXTVEgKfosoSjP8Yc9IZbFP/SY4cEd6hag7TABQrQ6nUSuwagGt96l8cw1DTijO75PqX6uiTXMzw==", + "salt": "sl4mXx6T9FypPH/s9TngfQ==", + "hashIterations": 27500, + "counter": 0, + "algorithm": "pbkdf2-sha256", + "digits": 0, + "period": 0, + "createdDate": 1554245879116, + "config": {} + } + ], + "disableableCredentialTypes": [ + "password" + ], + "requiredActions": [], + "realmRoles": [ + "user" + ], + "notBefore": 0, + "groups": [] + }, + { + "id": "f2f78ee5-b6f9-4e7b-a837-8301a30a6d73", + "createdTimestamp": 1602936578977, + "username": "service-account-controls-api", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "controls-api", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "uma_authorization", + "offline_access" + ], + "clientRoles": { + "controls-api": [ + "uma_protection" + ], + "account": [ + "manage-account", + "view-profile" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "466f7590-d495-402d-a955-08c54ae31385", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/konveyor/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "0136c3ef-0dfd-4b13-a6d0-2c8b6358edec", + "defaultRoles": [ + "manage-account", + "view-profile" + ], + "redirectUris": [ + "/realms/konveyor/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f5256301-90fa-4e2c-9829-0adf0d2f828d", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/konveyor/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "994a58fd-d98a-4403-9ff8-a6e9d078825f", + "redirectUris": [ + "/realms/konveyor/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "e0267bcc-919b-4b4d-92ae-54fe779db0f5", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0f7c2edf-9597-494e-8003-907342652b21", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "a951803a-79c7-46a6-8197-e32835286971", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "e91a0a7f-f825-4155-abff-a547a9594d63", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "e1f7edd7-e15c-43b4-8736-ff8204d16836", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "7cd8f4cd-abf4-4d20-904f-7381a29a38b8", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "c41b709a-a012-4c69-89d7-4f926dba0619", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "407f46da-f770-4666-9147-2bcfc5c1caf6", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/konveyor/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "e571b211-2550-475d-b87f-116ff54091ee", + "redirectUris": [ + "/admin/konveyor/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "831ca78a-ea57-468f-a135-fc48068d0b78", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5cda179e-8443-4467-a488-292976d25bf1", + "clientId": "controls-api", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "3f282184-c2a5-435d-833e-e6246ba5b03e", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "29d86f78-0ae8-44da-a7c0-30d490bf72f1", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "b4a1d1e4-15bd-4158-ad2f-c88949a25e0e", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Administration resource", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "8cd6f99d-8b98-4d4c-9e8a-8411ccaac46c", + "uris": [ + "/organizations/*" + ] + }, + { + "name": "User resources", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "15afecd7-f0cb-4cc9-9fc0-5526e5679110", + "uris": [ + "/user/*" + ] + } + ], + "policies": [ + { + "id": "24062286-2edc-40b6-a2cb-c62866b011ed", + "name": "Only administrators", + "description": "Only administrators can access", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"admin\",\"required\":false}]" + } + }, + { + "id": "79b7be85-68b2-4d23-8247-629e18910305", + "name": "Only users", + "description": "Only users can access", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"user\",\"required\":false}]" + } + }, + { + "id": "230ede88-dfd6-4ab3-8332-83460a89f8ba", + "name": "Administration Resource Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Administration resource\"]", + "applyPolicies": "[\"Only administrators\"]" + } + }, + { + "id": "334d8378-498c-4895-b8d2-ec27957a4bcd", + "name": "User Resource Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"User resources\"]", + "applyPolicies": "[\"Only users\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "695da74e-39ca-4e46-a2b6-6a673f92d4e2", + "clientId": "tackle-ui", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://localhost:3000/*" + ], + "webOrigins": [ + "http://localhost:3000" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "backchannel.logout.session.required": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "7ad4c83f-7793-470b-b01d-b8d4b6e49941", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "adbc4487-d95b-4567-a36b-629ac3b00a36", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "f06793e3-9c45-49b7-ad5b-08c14e2be29f", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1516caf6-6a5a-482d-8186-9c3ec9b5c874", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "0ba3782c-9f7a-4ced-9e4e-a8145d23bf6c", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "3873fc26-b5ef-457c-aab2-a1157b05db4d", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "33549263-f0f5-4807-8835-7f8e4684c0ef", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "f04e99a6-cca0-4acc-ad00-d2a9bbd26ff2", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "cedfca34-d051-44e3-8698-1e4d82f31ea1", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "876e7269-3aab-4252-b041-88a02f2fc3b5", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b8308a16-b44f-43c6-aab7-12d624fe82ad", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "e87fbc27-2c39-429b-9807-93b95241eab2", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "cf6392e7-574e-44da-b143-7c811574b237", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1a497de4-3929-4e45-9373-8dd4b9c92d31", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "6200624f-c90d-4aaf-b521-7b8435002a09", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "1cf2dc37-1836-4180-8b2b-06aef384bfca", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "864f324b-7b71-4cff-8a35-e08b3063fd40", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "5041e8d2-adb9-4814-8af8-4aee25ca3395", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "a8707020-de49-4801-ba18-b875ed9c5aaa", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "42659871-244a-454a-b231-0c15348dee42", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "6bfd6d10-70a9-488c-8306-c1288709b53b", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "9db1df0f-fa93-48be-b97f-a3b6414ab3f3", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "7e36717b-3b03-4e97-b8ac-c9e682f5cb4c", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "6cba0b69-c17a-4bbe-b55c-cabe48c61571", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "e57fce2e-b7b8-4665-9e17-755d85aeccc0", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "fd4aa449-c66f-4172-ae30-fa9e9ad82fe0", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "4d8e0ac0-7d66-45cb-bbf7-2e2b49cbf2a5", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "1d70dda3-b20c-483a-b08d-8d20407df7a7", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "f96d90e9-c464-4d6f-b0c5-4d36763ba0fb", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "cb6059d6-339d-4e0f-9591-223935104f97", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "3f1bb88d-a3e7-4472-9763-1c186514f3d2", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "6c36c6da-fcd0-4578-9704-5b245b40ce0a", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "986c6019-d84a-4a7f-a54d-da9da869b462", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "915b79ce-9d2f-4c19-8fac-12924bf19891", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "d7cde5d0-4c14-42ca-95ac-bcc07fcbbdb4", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "web-origins", + "roles", + "profile", + "email" + ], + "defaultOptionalClientScopes": [ + "microprofile-jwt", + "address", + "phone", + "offline_access" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "3e91f878-e4bd-4c0f-b320-4cab942d9ad1", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "183b6fda-b761-4612-a0d4-acea8dd9d98a", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "b75b53d5-2afa-464f-8624-2de149656a14", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "d491ad5e-534a-4492-a168-780b3314f8a0", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "ffeec36f-9ae2-4ef3-ae49-64c43cfab0a5", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "47a66c7c-373f-487d-bfb2-acd7ab389339", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "ae20dd81-3850-44cb-bd4a-1fee7d5a285e", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "a4547fd0-1a1f-4722-aff0-2c49c4608a32", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "263a7924-21c7-4ab2-9a72-dbad9a309eaa", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "f9670a25-1608-4bd6-b089-0170b0edf4d9", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "da59404c-a6d8-428e-b8a1-c99d74a9ea72", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "79af7e20-fd58-4dc2-9313-a7003f0cb1f7", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "84b80847-d734-442f-883f-cd24747cd878", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "77bfc179-e474-4d49-b748-b33964b27e41", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "71a5e6cf-6bce-45f7-800c-4860fb6b8a0a", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "18ab230f-b431-45f3-89d7-31712300a568", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "0f2e4cab-b078-4833-9330-5ffbe055279b", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "4e9af401-af96-44b8-a5bc-d16462b3f366", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "b31fb78e-fc01-4f55-a29b-87e31849dd7a", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a016ba60-5add-4bb7-a077-843f58d1c333", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "33b06dc1-001a-4621-81a3-1f7c0f20665a", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "50aa4aeb-3f2b-4fbc-996c-aab5951e3676", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "aedd580b-9c1d-4c53-9c8f-447d6497b682", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a1436eea-651b-4e9b-99b8-6b7c511b04b6", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9a35d938-cf17-4601-aa4c-87324b86f830", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d75b41c7-a3cb-4f8e-96ff-47ea426e2bb6", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6e6c98a8-18f4-4ca6-bc58-da58d743f765", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ecf34725-8dbd-43b2-b5e9-20fe6964657a", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ec2cd88f-5511-4f7d-b6cf-c022cebca4ef", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "93fff189-ed75-4326-a110-2aae42f59e91", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "76d4a83e-6fde-447c-9111-05bacfba27c0", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "e27c12d6-18ad-4c83-b601-8e3166242be3", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "4242993c-47b3-4357-9e8d-9864fcc0123a", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": {}, + "keycloakVersion": "11.0.2", + "userManagedAccessAllowed": false + } diff --git a/nginx.conf.template b/nginx.conf.template new file mode 100644 index 0000000000..7bcf0af993 --- /dev/null +++ b/nginx.conf.template @@ -0,0 +1,74 @@ +# For more information on configuration, see: +# * Official English Documentation: http://nginx.org/en/docs/ +# * Official Russian Documentation: http://nginx.org/ru/docs/ + + +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + # perl_modules /opt/app-root/etc/perl; + # perl_require Version.pm; + # perl_set $perl_version Version::installed; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Load modular configuration files from the /etc/nginx/conf.d directory. + # See http://nginx.org/en/docs/ngx_core_module.html#include + # for more information. + include /opt/app-root/etc/nginx.d/*.conf; + + server { + listen 8080 default_server; + listen [::]:8080 default_server; + server_name _; + root /opt/app-root/src; + + # Load configuration files for the default server block. + include /opt/app-root/etc/nginx.default.d/*.conf; + + location / { + try_files $uri $uri/ /index.html; + } + + location /auth { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass ${SSO_SERVER_URL}; + } + + location /api/controls { + rewrite ^/api/controls/(.*)$ /controls/$1 break; + proxy_pass ${CONTROLS_API_URL}; + } + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } + } + +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000000..72d3f06726 --- /dev/null +++ b/package.json @@ -0,0 +1,118 @@ +{ + "name": "tackle-ui", + "version": "0.1", + "private": true, + "dependencies": { + "@patternfly/patternfly": "4.82.0", + "@patternfly/react-core": "4.92.1", + "@patternfly/react-table": "4.20.21", + "@react-keycloak/web": "^3.4.0", + "@redhat-cloud-services/frontend-components-notifications": "3.0.3", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "@types/jest": "^26.0.15", + "@types/node": "^12.0.0", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "axios": "^0.21.1", + "formik": "^2.2.6", + "i18next": "^19.8.4", + "i18next-http-backend": "^1.0.22", + "keycloak-js": "12.0.2", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-i18next": "^11.8.5", + "react-redux": "^7.2.2", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "redux": "^4.0.5", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", + "typesafe-actions": "^5.1.0", + "typescript": "^4.0.3", + "web-vitals": "^0.2.4", + "yup": "^0.32.8" + }, + "scripts": { + "analyze": "source-map-explorer 'build/static/js/*.js'", + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "extract": "i18next --config i18next-parser.config.js", + "start": "react-scripts -r @cypress/instrument-cra start", + "build": "react-scripts build", + "build:instrumentation": "CYPRESS_INSTRUMENT_PRODUCTION=true react-scripts -r @cypress/instrument-cra build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "storybook": "start-storybook -p 6006 -s public", + "build-storybook": "build-storybook -s public", + "ui:start": "node server.js" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@cypress/code-coverage": "^3.9.2", + "@cypress/instrument-cra": "^1.4.0", + "@storybook/addon-actions": "^6.1.14", + "@storybook/addon-essentials": "^6.1.14", + "@storybook/addon-links": "^6.1.14", + "@storybook/node-logger": "^6.1.14", + "@storybook/preset-create-react-app": "^3.1.5", + "@storybook/react": "^6.1.14", + "@storybook/theming": "^6.1.14", + "@testing-library/react-hooks": "^4.0.1", + "@types/enzyme": "^3.10.8", + "@types/enzyme-adapter-react-16": "^1.0.6", + "@types/react-redux": "^7.1.15", + "@types/react-router-dom": "^5.1.7", + "@types/redux-logger": "^3.0.8", + "@types/yup": "^0.29.11", + "axios-mock-adapter": "^1.19.0", + "cypress": "6.5.0", + "cypress-keycloak-commands": "^1.2.0", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.5", + "http-proxy-middleware": "^1.0.6", + "husky": "^4.3.7", + "i18next-parser": "^3.6.0", + "jest-enzyme": "^7.1.2", + "lint-staged": "^10.5.3", + "node-sass": "^4.14.1", + "prettier": "^2.2.1", + "redux-devtools-extension": "^2.13.8", + "source-map-explorer": "^2.5.2" + }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!/node_modules/", + "!src/**/*.stories.*" + ] + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ + "prettier --write" + ] + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000..2e1ff655df Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000000..f947c1de6b --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + Tackle + + + +
+ + + diff --git a/public/keycloak.json b/public/keycloak.json new file mode 100644 index 0000000000..dde90ff09d --- /dev/null +++ b/public/keycloak.json @@ -0,0 +1,6 @@ +{ + "realm": "konveyor", + "auth-server-url": "http://localhost:8180/auth/", + "resource": "tackle-ui", + "public-client": true +} diff --git a/public/keycloak.json.template b/public/keycloak.json.template new file mode 100644 index 0000000000..00e8eaacdf --- /dev/null +++ b/public/keycloak.json.template @@ -0,0 +1,6 @@ +{ + "realm": "${SSO_REALM}", + "auth-server-url": "/auth", + "resource": "${SSO_CLIENT_ID}", + "public-client": true +} diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000000..52326e9749 --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,46 @@ +{ + "sidebar": { + "applicationInventory": "Application inventory", + "reports": "Reports", + "controls": "Controls" + }, + "terms": { + "name": "Name", + "description": "Description", + "owner": "Owner", + "controls": "Controls", + "businessServices": "Business services", + "stakeholders": "Stakeholders", + "stakeholderGroups": "Stakeholder groups", + "tags": "Tags" + }, + "actions": { + "edit": "Edit", + "delete": "Delete", + "cancel": "Cancel", + "createNew": "Create new", + "create": "Create", + "save": "Save" + }, + "dialog": { + "title": { + "delete": "Delete '{{what}}'", + "newBusinessService": "New business service", + "updateBusinessService": "Update business service" + }, + "message": { + "delete": "Are you sure you want to delete '{{what}}'? This will delete all resources associated with it and cannot be undone. Make sure this is something you really want to do!" + } + }, + "toastr": { + "success": { + "added": "Success! {{what}} was added as a {{type}}." + } + }, + "validation": { + "required": "This field is required.", + "minLength": "This field must contain at least {{length}} characters.", + "maxLength": "This field must contain fewer than {{length}} characters.", + "onlyCharactersAndUnderscore": "This field must contain only alphanumeric characters including underscore." + } +} \ No newline at end of file diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json new file mode 100644 index 0000000000..f126b339c4 --- /dev/null +++ b/public/locales/es/translation.json @@ -0,0 +1,46 @@ +{ + "sidebar": { + "applicationInventory": "Inventario de aplicaciones", + "reports": "Reportes", + "controls": "Controles" + }, + "terms": { + "name": "Nombre", + "description": "Descripción", + "owner": "Propietario", + "controls": "Controles", + "businessServices": "Servicios de negocio", + "stakeholders": "Interesados", + "stakeholderGroups": "Grupos de interesados", + "tags": "Etiquetas" + }, + "actions": { + "edit": "Editar", + "delete": "Eliminar", + "cancel": "Cancelar", + "createNew": "Crear nuevo", + "create": "Crear", + "save": "Guardar" + }, + "dialog": { + "title": { + "delete": "Eliminar '{{what}}'", + "newBusinessService": "Nuevo servicio de negocio", + "updateBusinessService": "Actualizar servicio de negocio" + }, + "message": { + "delete": "¿Estás seguro de querer eliminar '{{what}}'?. Esta acción eliminará todos los recursos asociados al mismo y no puede ser revertida. ¡Asegúrate de que esto es lo que realmente quieres!" + } + }, + "toastr": { + "success": { + "added": "Éxito! {{what}} fue creado como un {{type}}." + } + }, + "validation": { + "required": "Este campo es requerido.", + "minLength": "Este campo debe de tener al menos {{length}} caracteres.", + "maxLength": "Este campo debe de tener menos de {{length}} caracteres.", + "onlyCharactersAndUnderscore": "Este campo debe de contener solo caracteres alfanuméricos incluyendo el subguión." + } +} \ No newline at end of file diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000..b2c87e81ce Binary files /dev/null and b/public/logo.png differ diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000000..e8002ef401 Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000000..80afa588e3 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000000..d2744dd4ad --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "MTA Pathfinder", + "name": "MTA Pathfinder", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/server.js b/server.js new file mode 100644 index 0000000000..2d6e3fb01a --- /dev/null +++ b/server.js @@ -0,0 +1,21 @@ +const express = require("express"); +const path = require("path"); +const app = express(), bodyParser = require("body-parser"); + +const setupProxy = require('./src/setupProxy'); + +port = 3000; + +setupProxy(app); + +app.use(bodyParser.json()); +app.use(express.static(path.join(__dirname, "build"))); + +// Handles any requests that don't match the ones above +app.get("*", (req, res) => { + res.sendFile(path.join(__dirname, "/build/index.html")); +}); + +app.listen(port, () => { + console.log(`Server listening on the port::${port}`); +}); diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 0000000000..49c10e6616 --- /dev/null +++ b/src/App.scss @@ -0,0 +1,2 @@ +@import "~@patternfly/patternfly/patternfly.css"; +@import "~@patternfly/patternfly/patternfly-addons.css"; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000000..f6bc229fdc --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { BrowserRouter } from "react-router-dom"; + +import { AppRoutes } from "./Routes"; +import "./App.scss"; + +import { DefaultLayout } from "./layout"; + +import NotificationsPortal from "@redhat-cloud-services/frontend-components-notifications/NotificationPortal"; +import "@redhat-cloud-services/frontend-components-notifications/index.css"; + +import { ConfirmDialogContainer } from "./shared/containers/confirm-dialog-container"; + +const App: React.FC = () => { + return ( + + + + + + + + ); +}; + +export default App; diff --git a/src/Constants.ts b/src/Constants.ts new file mode 100644 index 0000000000..6a0ceb1d2d --- /dev/null +++ b/src/Constants.ts @@ -0,0 +1,6 @@ +import { PageQuery } from "api/models"; + +export const DEFAULT_PAGINATION: PageQuery = { + page: 1, + perPage: 10, +}; diff --git a/src/Paths.ts b/src/Paths.ts new file mode 100644 index 0000000000..5924d45b3b --- /dev/null +++ b/src/Paths.ts @@ -0,0 +1,31 @@ +export const formatPath = (path: Paths, data: any) => { + let url = path as string; + + for (const k of Object.keys(data)) { + url = url.replace(":" + k, data[k]); + } + + return url; +}; + +export enum Paths { + base = "/", + notFound = "/not-found", + + applicationInventory = "/application-inventory", + reports = "/reports", + + controls = "/controls", + controls_businessServices = "/controls/business-services", + controls_stakeholders = "/controls/stakeholders", + controls_stakeholderGroups = "/controls/stakeholder-groups", + controls_tags = "/controls/tags", +} + +export interface OptionalCompanyRoute { + company?: string; +} + +export interface CompanytRoute { + company: string; +} diff --git a/src/Routes.tsx b/src/Routes.tsx new file mode 100644 index 0000000000..bcf5363fea --- /dev/null +++ b/src/Routes.tsx @@ -0,0 +1,34 @@ +import React, { lazy, Suspense } from "react"; +import { Route, Switch, Redirect } from "react-router-dom"; + +import { AppPlaceholder } from "shared/components"; +import { Paths } from "./Paths"; + +const ApplicationInventory = lazy( + () => import("./pages/application-inventory") +); +const Reports = lazy(() => import("./pages/reports")); +const Controls = lazy(() => import("./pages/controls")); + +export const AppRoutes = () => { + const routes = [ + { + component: ApplicationInventory, + path: Paths.applicationInventory, + exact: false, + }, + { component: Reports, path: Paths.reports, exact: false }, + { component: Controls, path: Paths.controls, exact: false }, + ]; + + return ( + }> + + {routes.map(({ path, component, ...rest }, index) => ( + + ))} + + + + ); +}; diff --git a/src/api/models.tsx b/src/api/models.tsx new file mode 100644 index 0000000000..20474d9874 --- /dev/null +++ b/src/api/models.tsx @@ -0,0 +1,44 @@ +export interface PageQuery { + page: number; + perPage: number; +} + +export interface SortByQuery { + index: number; + direction: "asc" | "desc"; +} + +export interface PageRepresentation { + meta: Meta; + data: T[]; +} + +export interface Meta { + count: number; +} + +export interface BusinessService { + id?: number; + name: string; + description?: string; + owner?: Stakeholder; +} + +export interface Stakeholder { + displayName: string; + email: string; +} + +export interface BusinessServicePage { + _embedded: { + "business-service": BusinessService[]; + }; + total_count: number; +} + +export interface StakeholderPage { + _embedded: { + stakeholder: Stakeholder[]; + }; + total_count: number; +} diff --git a/src/api/rest.tsx b/src/api/rest.tsx new file mode 100644 index 0000000000..0a5de1a349 --- /dev/null +++ b/src/api/rest.tsx @@ -0,0 +1,153 @@ +import { AxiosPromise } from "axios"; +import { APIClient } from "axios-config"; + +import { + BusinessService, + BusinessServicePage, + PageQuery, + StakeholderPage, +} from "./models"; + +export const BASE_URL = "controls"; +export const BUSINESS_SERVICES = BASE_URL + "/business-service"; +export const STAKEHOLDERS = BASE_URL + "/stakeholder"; + +const headers = { Accept: "application/hal+json" }; + +type Direction = "asc" | "desc"; + +// Business services + +export enum BusinessServiceSortBy { + NAME, + OWNER, +} +export interface BusinessServiceSortByQuery { + field: BusinessServiceSortBy; + direction?: Direction; +} + +export const getBusinessServices = ( + filters: { + name?: string[]; + description?: string[]; + owner?: string[]; + }, + pagination: PageQuery, + sortBy?: BusinessServiceSortByQuery +): AxiosPromise => { + let sortByQuery: string | undefined = undefined; + if (sortBy) { + let field; + switch (sortBy.field) { + case BusinessServiceSortBy.NAME: + field = "name"; + break; + case BusinessServiceSortBy.OWNER: + field = "owner"; + break; + default: + throw new Error("Could not define SortBy field name"); + } + sortByQuery = `${sortBy.direction === "desc" ? "-" : ""}${field}`; + } + + const query: string[] = []; + + const params = { + page: pagination.page - 1, + size: pagination.perPage, + sort: sortByQuery, + name: filters.name, + description: filters.description, + "owner.displayName": filters.owner, + }; + + Object.keys(params).forEach((key) => { + const value = (params as any)[key]; + + if (value !== undefined && value !== null) { + let queryParamValues: string[] = []; + if (Array.isArray(value)) { + queryParamValues = value; + } else { + queryParamValues = [value]; + } + queryParamValues.forEach((v) => query.push(`${key}=${v}`)); + } + }); + + return APIClient.get(`${BUSINESS_SERVICES}?${query.join("&")}`, { headers }); +}; + +export const deleteBusinessService = (id: number): AxiosPromise => { + return APIClient.delete(`${BUSINESS_SERVICES}/${id}`); +}; + +export const createBusinessService = ( + obj: BusinessService +): AxiosPromise => { + return APIClient.post(`${BUSINESS_SERVICES}`, obj); +}; + +export const updateBusinessService = ( + obj: BusinessService +): AxiosPromise => { + return APIClient.put(`${BUSINESS_SERVICES}/${obj.id}`, obj); +}; + +// Stakeholders + +export enum StakeholderSortBy { + EMAIL, + DISPLAY_NAME, +} +export interface StakeholderSortByQuery { + field: StakeholderSortBy; + direction?: Direction; +} + +export const getStakeholders = ( + filters: { + filterText?: string; + }, + pagination: PageQuery, + sortBy?: StakeholderSortByQuery +): AxiosPromise => { + let sortByQuery: string | undefined = undefined; + if (sortBy) { + let field; + switch (sortBy.field) { + case StakeholderSortBy.EMAIL: + field = "email"; + break; + case StakeholderSortBy.DISPLAY_NAME: + field = "displayName"; + break; + default: + throw new Error("Could not define SortBy field name"); + } + sortByQuery = `${sortBy.direction === "desc" ? "-" : ""}${field}`; + } + + const query: string[] = []; + + const params = { + page: pagination.page - 1, + size: pagination.perPage, + sort: sortByQuery, + filter: filters.filterText, + }; + Object.keys(params).forEach((key) => { + const value = (params as any)[key]; + if (value !== undefined) { + query.push(`${key}=${value}`); + } + }); + + return APIClient.get(`${STAKEHOLDERS}?${query.join("&")}`, { headers }); +}; + +export const getAllStakeholders = (): AxiosPromise => { + return APIClient.get(`${STAKEHOLDERS}?size=1000`, { headers }); +}; diff --git a/src/axios-config/apiClient.tsx b/src/axios-config/apiClient.tsx new file mode 100644 index 0000000000..5b778389c9 --- /dev/null +++ b/src/axios-config/apiClient.tsx @@ -0,0 +1,45 @@ +import axios, { AxiosPromise } from "axios"; + +export class APIClient { + public static request( + path: string, + body: any = null, + method: + | "get" + | "post" + | "put" + | "delete" + | "options" + | "patch" + | undefined = "get", + config = {} + ): AxiosPromise { + return axios.request( + Object.assign( + {}, + { + url: path, + method, + data: body, + }, + config + ) + ); + } + + public static post(path: string, body: any, config = {}): AxiosPromise { + return this.request(path, body, "post", config); + } + + public static put(path: string, body: any, config = {}): AxiosPromise { + return this.request(path, body, "put", config); + } + + public static get(path: string, config = {}): AxiosPromise { + return this.request(path, null, "get", config); + } + + public static delete(path: string, config = {}) { + return this.request(path, null, "delete", config); + } +} diff --git a/src/axios-config/apiInit.ts b/src/axios-config/apiInit.ts new file mode 100644 index 0000000000..1440d53121 --- /dev/null +++ b/src/axios-config/apiInit.ts @@ -0,0 +1,20 @@ +import axios from "axios"; + +export const API_BASE_URL = "/api"; + +export const initApi = () => { + axios.defaults.baseURL = API_BASE_URL; +}; + +export const initInterceptors = (getToken: () => Promise) => { + axios.interceptors.request.use( + async (config) => { + const token = await getToken(); + if (token) config.headers["Authorization"] = "Bearer " + token; + return config; + }, + (error) => { + Promise.reject(error); + } + ); +}; diff --git a/src/axios-config/index.ts b/src/axios-config/index.ts new file mode 100644 index 0000000000..e8fe92e24c --- /dev/null +++ b/src/axios-config/index.ts @@ -0,0 +1,2 @@ +export { APIClient } from "./apiClient"; +export { initApi, initInterceptors } from "./apiInit"; diff --git a/src/frontend-components-notifications.d.ts b/src/frontend-components-notifications.d.ts new file mode 100644 index 0000000000..3b48cdd6ad --- /dev/null +++ b/src/frontend-components-notifications.d.ts @@ -0,0 +1,3 @@ +declare module "@redhat-cloud-services/frontend-components-notifications/NotificationPortal"; +declare module "@redhat-cloud-services/frontend-components-notifications/notificationsReducer"; +declare module "@redhat-cloud-services/frontend-components-notifications"; diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000000..ed75cae894 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,18 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from "i18next-http-backend"; + +i18n + .use(Backend) + .use(initReactI18next) + .init({ + lng: "en", + fallbackLng: "en", + debug: false, + + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/src/images/avatar.svg b/src/images/avatar.svg new file mode 100644 index 0000000000..11c80b85ff --- /dev/null +++ b/src/images/avatar.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/logo-navbar-patternfly.svg b/src/images/logo-navbar-patternfly.svg new file mode 100644 index 0000000000..232a493d1c --- /dev/null +++ b/src/images/logo-navbar-patternfly.svg @@ -0,0 +1,33 @@ + + + + PatternFly Logo + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/images/logo-navbar.svg b/src/images/logo-navbar.svg new file mode 100644 index 0000000000..61bf32f4ff --- /dev/null +++ b/src/images/logo-navbar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/images/logo.svg b/src/images/logo.svg new file mode 100644 index 0000000000..099049afb0 --- /dev/null +++ b/src/images/logo.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/images/tackle.png b/src/images/tackle.png new file mode 100644 index 0000000000..6e61e7a5a1 Binary files /dev/null and b/src/images/tackle.png differ diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000000..ec2585e8c0 --- /dev/null +++ b/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000000..224f475ad0 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; + +import { Provider } from "react-redux"; +import configureStore from "./store"; + +import { initApi, initInterceptors } from "axios-config"; + +import { ReactKeycloakProvider } from "@react-keycloak/web"; +import keycloak from "./keycloak"; + +import i18n from "./i18n"; +import { NinjaErrorBoundary } from "./ninja-error-boundary"; + +initApi(); +i18n.init(); + +ReactDOM.render( + + Loading...} + isLoadingCheck={(keycloak) => { + if (keycloak.authenticated) { + initInterceptors(() => { + return new Promise((resolve, reject) => { + if (keycloak.token) { + keycloak + .updateToken(5) + .then(() => resolve(keycloak.token!)) + .catch(() => reject("Failed to refresh token")); + } else { + keycloak.login(); + reject("Not logged in"); + } + }); + }); + + const kcLocale = (keycloak.tokenParsed as any)["locale"]; + if (kcLocale) { + i18n.changeLanguage(kcLocale); + } + } + + return !keycloak.authenticated; + }} + > + + + + + + + , + document.getElementById("root") +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/keycloak.ts b/src/keycloak.ts new file mode 100644 index 0000000000..78b2851433 --- /dev/null +++ b/src/keycloak.ts @@ -0,0 +1,7 @@ +import Keycloak from "keycloak-js"; + +// Setup Keycloak instance as needed +// Pass initialization options as required or leave blank to load from 'keycloak.json' +const keycloak = Keycloak(process.env.PUBLIC_URL + "/keycloak.json"); + +export default keycloak; diff --git a/src/layout/AppAboutModal/AppAboutModal.tsx b/src/layout/AppAboutModal/AppAboutModal.tsx new file mode 100644 index 0000000000..7828b3d681 --- /dev/null +++ b/src/layout/AppAboutModal/AppAboutModal.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { + AboutModal, + TextContent, + TextList, + TextListItem, +} from "@patternfly/react-core"; +import brandImage from "images/tackle.png"; + +export interface AppAboutModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const AppAboutModal: React.FC = ({ + isOpen, + onClose, +}) => { + return ( + + + + Source code + some content here + + + + ); +}; diff --git a/src/layout/AppAboutModal/index.ts b/src/layout/AppAboutModal/index.ts new file mode 100644 index 0000000000..b8867503c4 --- /dev/null +++ b/src/layout/AppAboutModal/index.ts @@ -0,0 +1 @@ +export { AppAboutModal } from "./AppAboutModal"; diff --git a/src/layout/AppAboutModal/tests/AppAboutModal.test.tsx b/src/layout/AppAboutModal/tests/AppAboutModal.test.tsx new file mode 100644 index 0000000000..669d6e5906 --- /dev/null +++ b/src/layout/AppAboutModal/tests/AppAboutModal.test.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { AppAboutModal } from "../AppAboutModal"; + +it("AppAboutModal", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap b/src/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap new file mode 100644 index 0000000000..edc635c48b --- /dev/null +++ b/src/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppAboutModal 1`] = ` + + + + + Source code + + + some content here + + + + +`; diff --git a/src/layout/AppAboutModalState/AppAboutModalState.tsx b/src/layout/AppAboutModalState/AppAboutModalState.tsx new file mode 100644 index 0000000000..5bff397096 --- /dev/null +++ b/src/layout/AppAboutModalState/AppAboutModalState.tsx @@ -0,0 +1,31 @@ +import React, { useState } from "react"; +import { AppAboutModal } from "../AppAboutModal"; + +export interface ChildrenProps { + isOpen: boolean; + toggleModal: () => void; +} + +export interface AppAboutModalStateProps { + children: (args: ChildrenProps) => any; +} + +export const AppAboutModalState: React.FC = ({ + children, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleModal = () => { + setIsOpen((current) => !current); + }; + + return ( + <> + {children({ + isOpen, + toggleModal, + })} + + + ); +}; diff --git a/src/layout/AppAboutModalState/index.ts b/src/layout/AppAboutModalState/index.ts new file mode 100644 index 0000000000..97ee348331 --- /dev/null +++ b/src/layout/AppAboutModalState/index.ts @@ -0,0 +1 @@ +export { AppAboutModalState } from "./AppAboutModalState"; diff --git a/src/layout/DefaultLayout/DefaultLayout.tsx b/src/layout/DefaultLayout/DefaultLayout.tsx new file mode 100644 index 0000000000..977674e93e --- /dev/null +++ b/src/layout/DefaultLayout/DefaultLayout.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { Page, SkipToContent } from "@patternfly/react-core"; +import { HeaderApp } from "../HeaderApp"; +import { SidebarApp } from "../SidebarApp"; + +export interface DefaultLayoutProps {} + +export const DefaultLayout: React.FC = ({ children }) => { + const pageId = "main-content-page-layout-horizontal-nav"; + const PageSkipToContent = ( + Skip to content + ); + + return ( + + } + sidebar={} + isManagedSidebar + skipToContent={PageSkipToContent} + mainContainerId={pageId} + > + {children} + + + ); +}; diff --git a/src/layout/DefaultLayout/index.ts b/src/layout/DefaultLayout/index.ts new file mode 100644 index 0000000000..5e24f9f25d --- /dev/null +++ b/src/layout/DefaultLayout/index.ts @@ -0,0 +1 @@ +export { DefaultLayout } from "./DefaultLayout"; diff --git a/src/layout/DefaultLayout/tests/DefaultLayout.test.tsx b/src/layout/DefaultLayout/tests/DefaultLayout.test.tsx new file mode 100644 index 0000000000..1c2cd561ba --- /dev/null +++ b/src/layout/DefaultLayout/tests/DefaultLayout.test.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { DefaultLayout } from "../DefaultLayout"; + +it("Test snapshot", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/layout/DefaultLayout/tests/__snapshots__/DefaultLayout.test.tsx.snap b/src/layout/DefaultLayout/tests/__snapshots__/DefaultLayout.test.tsx.snap new file mode 100644 index 0000000000..a9990775b4 --- /dev/null +++ b/src/layout/DefaultLayout/tests/__snapshots__/DefaultLayout.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test snapshot 1`] = ` + + } + isBreadcrumbWidthLimited={false} + isManagedSidebar={true} + isNotificationDrawerExpanded={false} + mainContainerId="main-content-page-layout-horizontal-nav" + mainTabIndex={-1} + onNotificationDrawerExpand={[Function]} + onPageResize={[Function]} + sidebar={} + skipToContent={ + + Skip to content + + } + /> + +`; diff --git a/src/layout/HeaderApp/HeaderApp.tsx b/src/layout/HeaderApp/HeaderApp.tsx new file mode 100644 index 0000000000..c0180b8e38 --- /dev/null +++ b/src/layout/HeaderApp/HeaderApp.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { + PageHeader, + Brand, + PageHeaderTools, + Avatar, + PageHeaderToolsGroup, + PageHeaderToolsItem, + Button, + ButtonVariant, +} from "@patternfly/react-core"; +import { HelpIcon } from "@patternfly/react-icons"; + +import { AppAboutModalState } from "../AppAboutModalState"; +import { SSOMenu } from "./SSOMenu"; +import { MobileDropdown } from "./MobileDropdown"; + +import navBrandImage from "images/tackle.png"; +import imgAvatar from "images/avatar.svg"; + +export const HeaderApp: React.FC = () => { + const toolbar = ( + + + + + {({ toggleModal }) => { + return ( + + ); + }} + + + + + + + + + + ); + + return ( + } + headerTools={toolbar} + showNavToggle + /> + ); +}; diff --git a/src/layout/HeaderApp/MobileDropdown.tsx b/src/layout/HeaderApp/MobileDropdown.tsx new file mode 100644 index 0000000000..01af4bcbf2 --- /dev/null +++ b/src/layout/HeaderApp/MobileDropdown.tsx @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import { Dropdown, DropdownItem, KebabToggle } from "@patternfly/react-core"; +import { HelpIcon } from "@patternfly/react-icons"; +import { AppAboutModal } from "../AppAboutModal"; + +export const MobileDropdown: React.FC = () => { + const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); + const [isAboutModalOpen, setAboutModalOpen] = useState(false); + + const onKebabDropdownToggle = (isOpen: boolean) => { + setIsKebabDropdownOpen(isOpen); + }; + const onKebabDropdownSelect = () => { + setIsKebabDropdownOpen((current) => !current); + }; + + const toggleAboutModal = () => { + setAboutModalOpen((current) => !current); + }; + + return ( + <> + } + isOpen={isKebabDropdownOpen} + dropdownItems={[ + + +  About + , + ]} + /> + + + ); +}; diff --git a/src/layout/HeaderApp/SSOMenu.tsx b/src/layout/HeaderApp/SSOMenu.tsx new file mode 100644 index 0000000000..b6ffb4180b --- /dev/null +++ b/src/layout/HeaderApp/SSOMenu.tsx @@ -0,0 +1,59 @@ +import React, { useState } from "react"; +import { useKeycloak } from "@react-keycloak/web"; +import { + Dropdown, + DropdownGroup, + DropdownItem, + DropdownToggle, + PageHeaderToolsItem, +} from "@patternfly/react-core"; + +export const SSOMenu: React.FC = () => { + const { keycloak } = useKeycloak(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const onDropdownSelect = () => { + setIsDropdownOpen((current) => !current); + }; + + const onDropdownToggle = (isOpen: boolean) => { + setIsDropdownOpen(isOpen); + }; + + return ( + + ); +}; diff --git a/src/layout/HeaderApp/index.ts b/src/layout/HeaderApp/index.ts new file mode 100644 index 0000000000..bf23788665 --- /dev/null +++ b/src/layout/HeaderApp/index.ts @@ -0,0 +1 @@ +export { HeaderApp } from "./HeaderApp"; diff --git a/src/layout/HeaderApp/tests/HeaderApp.test.tsx b/src/layout/HeaderApp/tests/HeaderApp.test.tsx new file mode 100644 index 0000000000..b4ab767b65 --- /dev/null +++ b/src/layout/HeaderApp/tests/HeaderApp.test.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { HeaderApp } from "../HeaderApp"; + +it("Test snapshot", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/layout/HeaderApp/tests/__snapshots__/HeaderApp.test.tsx.snap b/src/layout/HeaderApp/tests/__snapshots__/HeaderApp.test.tsx.snap new file mode 100644 index 0000000000..c5cc5434ec --- /dev/null +++ b/src/layout/HeaderApp/tests/__snapshots__/HeaderApp.test.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test snapshot 1`] = ` + + + + + [Function] + + + + + + + + + + + + } + logo={ + + } + showNavToggle={true} +/> +`; diff --git a/src/layout/LayoutUtils.ts b/src/layout/LayoutUtils.ts new file mode 100644 index 0000000000..6343954e96 --- /dev/null +++ b/src/layout/LayoutUtils.ts @@ -0,0 +1,2 @@ +type ThemeType = "light" | "dark"; +export const LayoutTheme: ThemeType = "dark"; diff --git a/src/layout/SidebarApp/SidebarApp.tsx b/src/layout/SidebarApp/SidebarApp.tsx new file mode 100644 index 0000000000..9fa1100394 --- /dev/null +++ b/src/layout/SidebarApp/SidebarApp.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Nav, NavItem, PageSidebar, NavList } from "@patternfly/react-core"; + +import { Paths } from "Paths"; +import { LayoutTheme } from "../LayoutUtils"; + +export const SidebarApp: React.FC = () => { + const { t } = useTranslation(); + + const renderPageNav = () => { + return ( + + ); + }; + + return ; +}; diff --git a/src/layout/SidebarApp/index.ts b/src/layout/SidebarApp/index.ts new file mode 100644 index 0000000000..1d637bfff5 --- /dev/null +++ b/src/layout/SidebarApp/index.ts @@ -0,0 +1 @@ +export { SidebarApp } from "./SidebarApp"; diff --git a/src/layout/SidebarApp/tests/SidebarApp.test.tsx b/src/layout/SidebarApp/tests/SidebarApp.test.tsx new file mode 100644 index 0000000000..7e2b78eac7 --- /dev/null +++ b/src/layout/SidebarApp/tests/SidebarApp.test.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { SidebarApp } from "../SidebarApp"; + +it("Test snapshot", () => { + jest.mock("react-i18next", () => ({ + useTranslation: () => { + return { + t: (str: any) => str, + i18n: { + changeLanguage: () => new Promise(() => {}), + }, + }; + }, + })); + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/layout/SidebarApp/tests/__snapshots__/SidebarApp.test.tsx.snap b/src/layout/SidebarApp/tests/__snapshots__/SidebarApp.test.tsx.snap new file mode 100644 index 0000000000..95bc757db5 --- /dev/null +++ b/src/layout/SidebarApp/tests/__snapshots__/SidebarApp.test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test snapshot 1`] = ` + + + + + sidebar.applicationInventory + + + + + sidebar.reports + + + + + sidebar.controls + + + + + } + theme="dark" +/> +`; diff --git a/src/layout/index.ts b/src/layout/index.ts new file mode 100644 index 0000000000..67565f156a --- /dev/null +++ b/src/layout/index.ts @@ -0,0 +1,4 @@ +export { AppAboutModalState as ButtonAboutApp } from "./AppAboutModalState"; +export { DefaultLayout } from "./DefaultLayout"; +export { HeaderApp } from "./HeaderApp"; +export { SidebarApp } from "./SidebarApp"; diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000000..9dfc1c058c --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ninja-error-boundary.tsx b/src/ninja-error-boundary.tsx new file mode 100644 index 0000000000..08000c0742 --- /dev/null +++ b/src/ninja-error-boundary.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + Title, +} from "@patternfly/react-core"; +import { UserNinjaIcon } from "@patternfly/react-icons"; + +interface NinjaErrorBoundaryProps {} +interface NinjaErrorBoundaryState { + hasError: boolean; +} + +export class NinjaErrorBoundary extends React.Component< + NinjaErrorBoundaryProps, + NinjaErrorBoundaryState +> { + constructor(props: NinjaErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: any) { + return { hasError: true }; + } + + componentDidCatch(error: any, errorInfo: any) { + console.log(error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + + + + + Ops! Something went wrong. + + + Try to refresh your page or contact your admin. + + + + ); + } + + return this.props.children; + } +} diff --git a/src/pages/application-inventory/application-inventory.tsx b/src/pages/application-inventory/application-inventory.tsx new file mode 100644 index 0000000000..4290c69a8b --- /dev/null +++ b/src/pages/application-inventory/application-inventory.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const ApplicationInventory: React.FC = () => { + return <>Application inventory: this should be ready soon; +}; diff --git a/src/pages/application-inventory/index.ts b/src/pages/application-inventory/index.ts new file mode 100644 index 0000000000..78dc44aa54 --- /dev/null +++ b/src/pages/application-inventory/index.ts @@ -0,0 +1 @@ +export { ApplicationInventory as default } from "./application-inventory"; diff --git a/src/pages/controls/business-services/business-services.tsx b/src/pages/controls/business-services/business-services.tsx new file mode 100644 index 0000000000..bdbb3fc2fc --- /dev/null +++ b/src/pages/controls/business-services/business-services.tsx @@ -0,0 +1,493 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { AxiosResponse } from "axios"; +import { useTranslation } from "react-i18next"; + +import { + Button, + ButtonVariant, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + Flex, + FlexItem, + Title, + ToolbarChip, + ToolbarChipGroup, + ToolbarFilter, + ToolbarGroup, + ToolbarItem, +} from "@patternfly/react-core"; +import { cellWidth, ICell, sortable, TableText } from "@patternfly/react-table"; +import { AddCircleOIcon } from "@patternfly/react-icons"; + +import { useDispatch } from "react-redux"; +import { alertActions } from "store/alert"; +import { confirmDialogActions } from "store/confirmDialog"; + +import { + AppPlaceholder, + ConditionalRender, + AppTableWithControls, +} from "shared/components"; +import { + useTableControls, + useFetchBusinessServices, + useDeleteBusinessService, +} from "shared/hooks"; + +import { BusinessService, SortByQuery } from "api/models"; +import { BusinessServiceSortBy, BusinessServiceSortByQuery } from "api/rest"; +import { getAxiosErrorMessage } from "utils/utils"; + +import { NewBusinessServiceModal } from "./components/new-business-service-modal"; +import { UpdateBusinessServiceModal } from "./components/update-business-service-modal"; +import { + FilterOption, + SearchFilter, +} from "./components/search-filter/search-filter"; + +enum FilterKey { + NAME = "name", + DESCRIPTION = "description", + OWNER = "owner", +} + +const toBusinessServiceSortByQuery = ( + sortBy?: SortByQuery +): BusinessServiceSortByQuery | undefined => { + if (!sortBy) { + return undefined; + } + + let field: BusinessServiceSortBy; + switch (sortBy.index) { + case 0: + field = BusinessServiceSortBy.NAME; + break; + case 2: + field = BusinessServiceSortBy.OWNER; + break; + default: + throw new Error("Invalid column index=" + sortBy.index); + } + + return { + field, + direction: sortBy.direction, + }; +}; + +const BUSINESS_SERVICE_FIELD = "businessService"; + +// const getRow = (rowData: IRowData): BusinessService => { +// return rowData[BUSINESS_SERVICE_FIELD]; +// }; + +export const BusinessServices: React.FC = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const [nameFilters, setNameFilters] = useState([]); + const [descriptionFilters, setDescriptionFilters] = useState([]); + const [ownerFilters, setOwnerFilters] = useState([]); + + const [isNewModalOpen, setIsNewModalOpen] = useState(false); + const [rowToUpdate, setRowToUpdate] = useState(); + + const { deleteBusinessService } = useDeleteBusinessService(); + + const { + businessServices, + isFetching, + fetchError, + fetchBusinessServices, + } = useFetchBusinessServices(true); + + const { + paginationQuery, + sortByQuery, + handlePaginationChange, + handleSortChange, + } = useTableControls({ + sortByQuery: { direction: "asc", index: 0 }, + }); + + const refreshTable = useCallback(() => { + fetchBusinessServices( + { + name: nameFilters, + description: descriptionFilters, + owner: ownerFilters, + }, + paginationQuery, + toBusinessServiceSortByQuery(sortByQuery) + ); + }, [ + nameFilters, + descriptionFilters, + ownerFilters, + paginationQuery, + sortByQuery, + fetchBusinessServices, + ]); + + useEffect(() => { + fetchBusinessServices( + { + name: nameFilters, + description: descriptionFilters, + owner: ownerFilters, + }, + paginationQuery, + toBusinessServiceSortByQuery(sortByQuery) + ); + }, [ + nameFilters, + descriptionFilters, + ownerFilters, + paginationQuery, + sortByQuery, + fetchBusinessServices, + ]); + + const columns: ICell[] = [ + { title: t("terms.name"), transforms: [sortable, cellWidth(30)] }, + { title: t("terms.description"), transforms: [cellWidth(40)] }, + { + title: t("terms.owner"), + transforms: [ + // sortable + ], + }, + { + title: "", + props: { + className: "pf-u-text-align-right", + }, + }, + ]; + + const itemsToRow = (items: BusinessService[]) => { + return items.map((item) => ({ + [BUSINESS_SERVICE_FIELD]: item, + cells: [ + { + title: item.name, + }, + { + title: ( + {item.description} + ), + }, + { + title: item.owner?.displayName, + }, + { + title: ( + + + + + + + + + ), + }, + ], + })); + }; + + // const actions: IActions = [ + // { + // title: t("actions.edit"), + // onClick: ( + // event: React.MouseEvent, + // rowIndex: number, + // rowData: IRowData + // ) => { + // const row: BusinessService = getRow(rowData); + // editRow(row); + // }, + // }, + // { + // title: t("actions.delete"), + // onClick: ( + // event: React.MouseEvent, + // rowIndex: number, + // rowData: IRowData + // ) => { + // const row: BusinessService = getRow(rowData); + // deleteRow(row); + // }, + // }, + // ]; + + const editRow = (row: BusinessService) => { + setRowToUpdate(row); + }; + + const deleteRow = (row: BusinessService) => { + dispatch( + confirmDialogActions.openDialog({ + title: t("dialog.title.delete", { what: row.name }), + message: t("dialog.message.delete", { what: row.name }), + variant: ButtonVariant.danger, + confirmBtnLabel: t("actions.delete"), + cancelBtnLabel: t("actions.cancel"), + onConfirm: () => { + dispatch(confirmDialogActions.processing()); + deleteBusinessService( + row, + () => { + dispatch(confirmDialogActions.closeDialog()); + refreshTable(); + }, + (error) => { + dispatch(confirmDialogActions.closeDialog()); + dispatch(alertActions.addDanger(getAxiosErrorMessage(error))); + } + ); + }, + }) + ); + }; + + // Advanced filters + + const filterOptions: FilterOption[] = [ + { + key: FilterKey.NAME, + name: t("terms.name"), + }, + { + key: FilterKey.DESCRIPTION, + name: t("terms.description"), + }, + { + key: FilterKey.OWNER, + name: t("terms.owner"), + }, + ]; + + const handleOnClearAllFilters = () => { + setNameFilters([]); + setDescriptionFilters([]); + setOwnerFilters([]); + }; + + const handleOnFilterApplied = (key: string, filterText: string) => { + if (key === FilterKey.NAME) { + setNameFilters([...nameFilters, filterText]); + } else if (key === FilterKey.DESCRIPTION) { + setDescriptionFilters([...descriptionFilters, filterText]); + } else if (key === FilterKey.OWNER) { + setOwnerFilters([...ownerFilters, filterText]); + } else { + throw new Error("Can not apply filter " + key + ". It's not supported"); + } + + handlePaginationChange({ page: 1 }); + }; + + const handleOnDeleteFilter = ( + category: string | ToolbarChipGroup, + chip: ToolbarChip | string + ) => { + if (typeof chip !== "string") { + throw new Error("Can not delete filter. Chip must be a string"); + } + + let categoryKey: string; + if (typeof category === "string") { + categoryKey = category; + } else { + categoryKey = category.key; + } + + if (categoryKey === FilterKey.NAME) { + setNameFilters(nameFilters.filter((f) => f !== chip)); + } else if (categoryKey === FilterKey.DESCRIPTION) { + setDescriptionFilters(descriptionFilters.filter((f) => f !== chip)); + } else if (categoryKey === FilterKey.OWNER) { + setOwnerFilters(ownerFilters.filter((f) => f !== chip)); + } else { + throw new Error( + "Can not delete chip. Chip " + chip + " is not supported" + ); + } + }; + + const handleOnDeleteFilterGroup = (category: string | ToolbarChipGroup) => { + let categoryKey: string; + if (typeof category === "string") { + categoryKey = category; + } else { + categoryKey = category.key; + } + + if (categoryKey === FilterKey.NAME) { + setNameFilters([]); + } else if (categoryKey === FilterKey.DESCRIPTION) { + setDescriptionFilters([]); + } else if (categoryKey === FilterKey.OWNER) { + setOwnerFilters([]); + } else { + throw new Error("Can not delete ChipGroup. ChipGroup is not supported"); + } + }; + + // Create Modal + + const handleOnOpenCreateNewBusinessServiceModal = () => { + setIsNewModalOpen(true); + }; + + const handleOnBusinessServiceCreated = ( + response: AxiosResponse + ) => { + setIsNewModalOpen(false); + refreshTable(); + + dispatch( + alertActions.addSuccess( + t("toastr.success.added", { + what: response.data.name, + type: "business service", + }) + ) + ); + }; + + const handleOnCancelCreateBusinessService = () => { + setIsNewModalOpen(false); + }; + + // Update Modal + + const handleOnBusinessServiceUpdated = () => { + setRowToUpdate(undefined); + refreshTable(); + }; + + const handleOnCancelUpdateBusinessService = () => { + setRowToUpdate(undefined); + }; + + return ( + <> + } + > + + 0 + } + toolbarToggle={ + + + {null} + + + {null} + + + + + + } + toolbar={ + + + + + + } + noDataState={ + + + + No business services available + + + Create a new business service to start seeing data here. + + + } + /> + + + + + + ); +}; diff --git a/src/pages/controls/business-services/components/business-service-form/business-service-form.tsx b/src/pages/controls/business-services/components/business-service-form/business-service-form.tsx new file mode 100644 index 0000000000..ffdce9e647 --- /dev/null +++ b/src/pages/controls/business-services/components/business-service-form/business-service-form.tsx @@ -0,0 +1,217 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AxiosError, AxiosPromise, AxiosResponse } from "axios"; +import { useFormik, FormikProvider, FormikHelpers } from "formik"; +import { object, string } from "yup"; + +import { + ActionGroup, + Alert, + Button, + ButtonVariant, + Form, + FormGroup, + TextArea, + TextInput, +} from "@patternfly/react-core"; + +import { createBusinessService, updateBusinessService } from "api/rest"; +import { BusinessService, Stakeholder } from "api/models"; +import { + getAxiosErrorMessage, + getValidatedFromError, + getValidatedFromErrorTouched, +} from "utils/utils"; + +import { SelectStakeholderFormField } from "../select-stakeholder-form-field"; +import { useFetchStakeholders } from "shared/hooks"; + +export interface FormValues { + name: string; + description?: string; + owner?: Stakeholder; +} + +export interface BusinessServiceFormProps { + businessService?: BusinessService; + onSaved: (response: AxiosResponse) => void; + onCancel: () => void; +} + +export const BusinessServiceForm: React.FC = ({ + businessService, + onSaved, + onCancel, +}) => { + const { t } = useTranslation(); + + const [error, setError] = useState(); + + const { + stakeholders, + isFetching, + fetchError, + fetchAllStakeholders, + } = useFetchStakeholders(); + + useEffect(() => { + fetchAllStakeholders(); + }, [fetchAllStakeholders]); + + const initialValues: FormValues = { + name: businessService?.name || "", + description: businessService?.description || "", + owner: businessService?.owner, + }; + + const validationSchema = object().shape({ + name: string() + .trim() + .required(t("validation.required")) + .min(3, t("validation.minLength", { length: 3 })) + .max(120, t("validation.maxLength", { length: 120 })) + .matches(/^[- \w]+$/, t("validation.onlyCharactersAndUnderscore")), + description: string() + .trim() + .max(250, t("validation.maxLength", { length: 250 })), + }); + + const onSubmit = ( + formValues: FormValues, + formikHelpers: FormikHelpers + ) => { + const payload: BusinessService = { + name: formValues.name, + description: formValues.description, + owner: formValues.owner ? { ...formValues.owner } : undefined, + }; + + let promise: AxiosPromise; + if (businessService) { + promise = updateBusinessService({ + ...businessService, + ...payload, + }); + } else { + promise = createBusinessService(payload); + } + + promise + .then((response) => { + formikHelpers.setSubmitting(false); + onSaved(response); + }) + .catch((error) => { + formikHelpers.setSubmitting(false); + setError(error); + }); + }; + + const formik = useFormik({ + enableReinitialize: true, + initialValues: initialValues, + validationSchema: validationSchema, + onSubmit: onSubmit, + }); + + const onChangeField = (value: string, event: React.FormEvent) => { + formik.handleChange(event); + }; + + return ( + +
+ {error && ( + + )} + + + + +