diff --git a/.env.keycloak-auth b/.env.keycloak-auth new file mode 100644 index 00000000..ef2a5f2b --- /dev/null +++ b/.env.keycloak-auth @@ -0,0 +1,11 @@ +RECORD_MANAGER_API_URL=http://localhost:1235/record-manager/services/record-manager-server +RECORD_MANAGER_APP_TITLE=Record Manager +RECORD_MANAGER_DEV_SERVER_PORT=3000 +RECORD_MANAGER_PROD_SERVER_PORT=8080 +RECORD_MANAGER_LANGUAGE=cs +RECORD_MANAGER_NAVIGATOR_LANGUAGE=true +RECORD_MANAGER_BASENAME=/ +RECORD_MANAGER_EXTENSIONS=supplier +RECORD_MANAGER_AUTHENTICATION=oidc +RECORD_MANAGER_AUTH_SERVER_URL=http://localhost:1235/record-manager/services/auth/realms/record-manager +RECORD_MANAGER_AUTH_CLIENT_ID=record-manager diff --git a/deploy/internal-auth/docker-compose.yml b/deploy/internal-auth/docker-compose.yml index df3f3ea6..c7aed389 100644 --- a/deploy/internal-auth/docker-compose.yml +++ b/deploy/internal-auth/docker-compose.yml @@ -68,7 +68,7 @@ services: environment: FORMGEN_REPOSITORY_URL: "http://db-server:7200/repositories/record-manager-formgen" volumes: - - ./s-pipes-engine/scripts:/scripts/root + - ../shared/s-pipes-engine/scripts:/scripts/root:ro db-server: build: diff --git a/deploy/keycloak-auth/.env b/deploy/keycloak-auth/.env new file mode 100644 index 00000000..39ed738d --- /dev/null +++ b/deploy/keycloak-auth/.env @@ -0,0 +1,18 @@ +# Public origin of URL where Record Manager UI will run, e.g. https://kbss.fel.cvut.cz, https://kbss.fel.cvut.cz:8080, http://localhost. ! This option can be used only with running reverse proxy pointing to http://localhost:$INTERNAL_HOST_PORT/record-manager ! +#PUBLIC_ORIGIN=http://localhost + +# Path to root Record Manager application (by default it is set to "/record-manager") ! This option can be used only with running reverse proxy pointing to http://localhost:$INTERNAL_HOST_PORT/record-manager ! +#RECORD_MANAGER_ROOT_PATH=/record-manager-example + +# Prefix for name of all docker containers +RECORD_SET_NAME=kauth-example + +# Host machine port that provides main entrypoint for the application. The application will be locally accessible at http://localhost:$INTERNAL_HOST_PORT/$RECORD_MANAGER_ROOT_PATH (by default it is set to "1235") +#INTERNAL_HOST_PORT=1235 + +# URL to form generation service +FORMGEN_SERVICE_URL=http://s-pipes-engine:8080/s-pipes/service?_pId=clone-form + +RECORD_MANAGER_APP_TITLE=Record Manager + +LANGUAGE=en diff --git a/deploy/keycloak-auth/docker-compose.yml b/deploy/keycloak-auth/docker-compose.yml index 53f09d59..370ab93b 100644 --- a/deploy/keycloak-auth/docker-compose.yml +++ b/deploy/keycloak-auth/docker-compose.yml @@ -1,61 +1,94 @@ version: '3.9' +# Provide access to record-manager-ui that runs locally in dev mode +x-access-for-local-development: &local-dev-env + cors.allowedOrigins: "http://localhost:3000" + services: + nginx: + image: nginx:latest + container_name: ${RECORD_SET_NAME:-rm}-nginx + volumes: + - ./nginx/template-variables.conf:/etc/nginx/templates/variables.conf.template:ro + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/error.html:/usr/share/nginx/html/error.html:ro + ports: + - "127.0.0.1:${INTERNAL_HOST_PORT:-1235}:80" + restart: always + depends_on: + - record-manager + - record-manager-server + - s-pipes-engine + - db-server + environment: + RECORD_MANAGER_ORIGIN: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}" + RECORD_MANAGER_ROOT_PATH: "${RECORD_MANAGER_ROOT_PATH:-/record-manager}" + record-manager: image: ghcr.io/kbss-cvut/kbss-cvut/record-manager-ui:latest container_name: ${RECORD_SET_NAME:-rm}-record-manager - ports: - - "127.0.0.1:3000:80" + expose: + - "80" + restart: always depends_on: - record-manager-server environment: - APP_TITLE: "Record Manager" - BASENAME: "./" - LANGUAGE: "cs" - NAVIGATOR_LANGUAGE: "true" - API_URL: "http://localhost:8080/record-manager-server" - APP_INFO: "© KBSS CVUT v Praze, 2023" + API_URL: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}/services/record-manager-server" + APP_INFO: "${RECORD_MANAGER_APP_INFO:-}" + APP_TITLE: "${RECORD_MANAGER_APP_TITLE:-Record Manager}" + LANGUAGE: "${LANGUAGE:-en}" + NAVIGATOR_LANGUAGE: "false" + BASENAME: "${RECORD_MANAGER_ROOT_PATH:-/record-manager}" AUTHENTICATION: "oidc" - AUTH_SERVER_URL: "http://localhost:8088/realms/record-manager" - AUTH_CLIENT_ID: "record-manager-ui" - FORCE_BASENAME: "true" + AUTH_SERVER_URL: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}/services/auth/realms/record-manager" + AUTH_CLIENT_ID: "record-manager" + EXTENSION: "${RECORD_MANAGER_EXTENSIONS:-supplier}" record-manager-server: image: ghcr.io/kbss-cvut/kbss-cvut/record-manager:latest container_name: ${RECORD_SET_NAME:-rm}-record-manager-server - ports: - - "127.0.0.1:8080:8080" + expose: + - "8080" + restart: always depends_on: - s-pipes-engine - db-server - - auth-server environment: - REPOSITORYURL: "http://db-server:7200/repositories/record-manager-app" - FORMGENREPOSITORYURL: "http://db-server:7200/repositories/record-manager-formgen" - FORMGENSERVICEURL: "http://s-pipes-engine:8080/s-pipes/service?_pId=clone&sgovRepositoryUrl=https%3A%2F%2Fgraphdb.onto.fel.cvut.cz%2Frepositories%2Fkodi-slovnik-gov-cz" + <<: *local-dev-env + appContext: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}" + repositoryUrl: "http://db-server:7200/repositories/record-manager-app" + formGenRepositoryUrl: "http://db-server:7200/repositories/record-manager-formgen" + formGenServiceUrl: "${FORMGEN_SERVICE_URL}" SECURITY_PROVIDER: "oidc" - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI: "http://localhost:8088/realms/record-manager" + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}/services/auth/realms/record-manager" SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWKSETURI: "http://auth-server:8080/realms/record-manager/protocol/openid-connect/certs" - SERVER_SERVLET_CONTEXTPATH: "/record-manager-server" s-pipes-engine: image: ghcr.io/kbss-cvut/s-pipes/s-pipes-engine:latest container_name: ${RECORD_SET_NAME:-rm}-s-pipes-engine - ports: - - "127.0.0.1:8081:8080" + expose: + - "8080" + restart: always depends_on: - db-server + environment: + FORMGEN_REPOSITORY_URL: "http://db-server:7200/repositories/record-manager-formgen" + volumes: + - ../shared/s-pipes-engine/scripts:/scripts/root:ro + db-server: build: context: ../shared/db-server container_name: ${RECORD_SET_NAME:-rm}-db-server environment: - GDB_JAVA_OPTS: "-Ddefault.min.distinct.threshold=67108864" - ports: - - "127.0.0.1:7200:7200" + GDB_JAVA_OPTS: "-Dgraphdb.external-url=${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}/services/db-server" + expose: + - "7200" + restart: always volumes: - ../shared/db-server/init-data/forms:/root/graphdb-import/forms:ro - - data:/opt/graphdb/home + - db-server:/opt/graphdb/home + auth-server-db: image: postgres:13 container_name: ${RECORD_SET_NAME:-rm}-auth-server-db @@ -65,15 +98,16 @@ services: POSTGRES_PASSWORD: keycloak volumes: - auth-server-db:/var/lib/postgresql/data + auth-server: image: ghcr.io/kbss-cvut/keycloak-graphdb-user-replicator/keycloak-graphdb:latest container_name: ${RECORD_SET_NAME:-rm}-auth-server command: - - start --import-realm + - start --import-realm --features="token-exchange,admin-fine-grained-authz" environment: KC_IMPORT: realm-export.json - KC_HOSTNAME_URL: "http://localhost:8088" - KC_HOSTNAME_ADMIN_URL: "http://localhost:8088" + KC_HOSTNAME_URL: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}/services/auth/" + KC_HOSTNAME_ADMIN_URL: "${PUBLIC_ORIGIN:-http://localhost:${INTERNAL_HOST_PORT:-1235}}${RECORD_MANAGER_ROOT_PATH:-/record-manager}/services/auth/" KC_HOSTNAME_STRICT_BACKCHANNEL: false KC_HTTP_ENABLED: true KEYCLOAK_ADMIN: ${KC_ADMIN_USER} @@ -94,15 +128,15 @@ services: VOCABULARY_USER_EMAIL: "http://xmlns.com/foaf/0.1/mbox" ADD_ACCOUNTS: false REALM_ID: "record-manager" - ports: - - "127.0.0.1:8088:8080" + expose: + - "8080" volumes: - auth-server:/opt/keycloak/data - - ./keycloak:/opt/keycloak/data/import + - ../shared/keycloak:/opt/keycloak/data/import depends_on: - auth-server-db volumes: - data: + db-server: auth-server: auth-server-db: diff --git a/deploy/keycloak-auth/keycloak/realm-export.json b/deploy/keycloak-auth/keycloak/realm-export.json index 5c1ec675..62243669 100644 --- a/deploy/keycloak-auth/keycloak/realm-export.json +++ b/deploy/keycloak-auth/keycloak/realm-export.json @@ -418,8 +418,8 @@ "attributes": {} } ], - "record-manager": [], - "record-manager-ui": [] + "record-manager-server": [], + "record-manager": [] } }, "groups": [], @@ -547,7 +547,9 @@ "redirectUris": [ "/realms/record-manager/account/*" ], - "webOrigins": [], + "webOrigins": [ + "http://localhost" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -705,9 +707,9 @@ }, { "id": "34eebec0-1e19-4eab-af04-8729a1de47f4", - "clientId": "record-manager", - "name": "Record Manager", - "description": "Record Manager backend", + "clientId": "record-manager-server", + "name": "Record Manager Server", + "description": "Record Manager server", "rootUrl": "", "adminUrl": "", "baseUrl": "", @@ -758,21 +760,21 @@ }, { "id": "d6e815c8-6fd6-4ef6-acf7-2c099b5010fc", - "clientId": "record-manager-ui", - "name": "Record Manager UI", + "clientId": "record-manager", + "name": "Record Manager", "description": "Record Manager frontend", - "rootUrl": "http://localhost:3000", - "adminUrl": "http://localhost:3000", - "baseUrl": "http://localhost:3000", + "rootUrl": "http://localhost", + "adminUrl": "http://localhost", + "baseUrl": "http://localhost", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "http://localhost:3000/*" + "http://localhost/*" ], "webOrigins": [ - "http://localhost:3000" + "http://localhost" ], "notBefore": 0, "bearerOnly": false, diff --git a/deploy/keycloak-auth/nginx/error.html b/deploy/keycloak-auth/nginx/error.html new file mode 100644 index 00000000..3c7fce07 --- /dev/null +++ b/deploy/keycloak-auth/nginx/error.html @@ -0,0 +1,38 @@ + + + + + + + <!--# echo var="status" default="" --> + | Record manager + + + + + + + + + +

The application is currently being updated

+

You will be redirected to the new version in a few seconds.

+

If you see this message for more than a minute, something has gone wrong.

+ +

Sorry, something went wrong.

+

+ + + diff --git a/deploy/keycloak-auth/nginx/nginx.conf b/deploy/keycloak-auth/nginx/nginx.conf new file mode 100644 index 00000000..94da1c2c --- /dev/null +++ b/deploy/keycloak-auth/nginx/nginx.conf @@ -0,0 +1,98 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + + client_max_body_size 100M; + + include mime.types; + default_type application/octet-stream; + + map $status $status_text { + 400 'Bad Request'; + 401 'Unauthorized'; + 403 'Forbidden'; + 404 'Not Found'; + 405 'Method Not Allowed'; + 406 'Not Acceptable'; + 413 'Payload Too Large'; + 414 'URI Too Long'; + 431 'Request Header Fields Too Large'; + 500 'Internal Server Error'; + 501 'Not Implemented'; + 502 'Bad Gateway'; + 503 'Service Unavailable'; + 504 'Gateway Timeout'; + } + + include /etc/nginx/conf.d/variables.conf; + + server { + listen 80; + server_name localhost; + + error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 + 415 416 417 418 421 422 423 424 426 428 429 431 451 500 501 502 503 + 504 505 506 507 508 510 511 /error.html; + + location = /error.html { + ssi on; + internal; + root /usr/share/nginx/html; + } + + location = /record-manager { + return 302 $record_manager_origin$record_manager_root_path/; + } + + location /record-manager/ { + proxy_pass http://record-manager/; # keep the trailing slash to cut off matched prefix + } + + location /record-manager/services/record-manager-server/ { + proxy_pass http://record-manager-server:8080/record-manager/; # keep the trailing slash to cut off matched prefix + proxy_cookie_path /record-manager $record_manager_root_path/services; + } + + location = /record-manager/services/db-server { + return 302 $record_manager_origin$record_manager_root_path/services/db-server/; + } + + location /record-manager/services/db-server/ { + proxy_pass http://db-server:7200/; # keep the trailing slash to cut off matched prefix + } + + location /record-manager/services/s-pipes/ { + proxy_pass http://s-pipes-engine:8080/; # keep the trailing slash to cut off matched prefix + } + + location = /record-manager/services/auth { + return 302 $record_manager_origin$record_manager_root_path/services/auth/; + } + + location /record-manager/services/auth/ { + proxy_pass http://auth-server:8080/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Port $http_x_forwarded_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Increase buffer sizes to handle large headers sent by Keycloak and its clients + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + } + + location /health-check { + return 200; + access_log off; + } + } +} diff --git a/deploy/keycloak-auth/nginx/template-variables.conf b/deploy/keycloak-auth/nginx/template-variables.conf new file mode 100644 index 00000000..71eb7ebf --- /dev/null +++ b/deploy/keycloak-auth/nginx/template-variables.conf @@ -0,0 +1,7 @@ +map $host $record_manager_origin { + default "$RECORD_MANAGER_ORIGIN"; +} + +map $host $record_manager_root_path { + default "$RECORD_MANAGER_ROOT_PATH"; +} diff --git a/deploy/internal-auth/s-pipes-engine/scripts/form-generation.sms.ttl b/deploy/shared/s-pipes-engine/scripts/form-generation.sms.ttl similarity index 100% rename from deploy/internal-auth/s-pipes-engine/scripts/form-generation.sms.ttl rename to deploy/shared/s-pipes-engine/scripts/form-generation.sms.ttl diff --git a/doc/development.md b/doc/development.md index 4c320754..fd9bf310 100644 --- a/doc/development.md +++ b/doc/development.md @@ -16,6 +16,17 @@ All the services, including dockerized record-manager-ui, run by default at URL To attach simultaneously frontend for the development use setup from [.env.internal-auth](../.env.internal-auth), e.g., by running `ln -s .env.internal-auth .env; npm run dev`. +### Running with Dockerized Services and Keycloak Authorization + +This section describes the development scenario when developer uses +[dockerized services with keycloak authorization](../deploy/keycloak-auth/docker-compose.yml) to develop. +All the services, including dockerized record-manager-ui, run by default at URL starting with `http://localhost:1235/record-manager`. +To attach simultaneously frontend for the development use setup from [.env.keyclaok-auth](../.env.keyclaok-auth), +e.g., by running `ln -s .env.keycloak-auth .env; npm run dev`. + +`npm run dev` starts developement version of record-manager-ui at `http://localhost:3000`. In order to login to through keycloak `record-manager` realm needs to be configured: open the `record-manager` realm, under _Clients_ open the `record-manager` client and set +_Valid redirect URIs_ (by default it should be `http://localhost:3000/*`) and _Web Origins_ (by default it should be `http://localhost:3000`). + ### Running with all Services in Development Mode This section describes the development scenario when the developer runs all dependent services in development mode. @@ -23,7 +34,7 @@ By default, Record Manager UI runs at `http://localhost:3000` while Record Mana This requires setting up the CORS policy of the server appropriately, i.e., configuring `config.properties` to contain `security.sameSite=None` and set up also `cors.allowedOrigin` if needed. -## Add Configuration Parameters +## Adding Configuration Parameters When runtime configuration parameters are added to the application, they also need to be added to Docker processing so that environment variables can be used to set the variables. The following needs to be done: diff --git a/doc/setup.md b/doc/setup.md index 59caf1a5..64291c1c 100644 --- a/doc/setup.md +++ b/doc/setup.md @@ -54,7 +54,7 @@ RECORD_MANAGER_ROOT_PATH=/record-manager-example ### Set up with Keycloak Authorization -The deployment is pretty much self-contained based on [docker-compose.yml](../deploy/keycloak-auth/docker-compose.yml). It sets up the corresponding repositories, imports a realm where clients +The deployment is pretty much self-contained based on [docker-compose.yml](../deploy/keycloak-auth-no-proxy/docker-compose.yml). It sets up the corresponding repositories, imports a realm where clients are configured for both the Record Manager backend and frontend. All the services (except PostgreSQL used by Keycloak) in the deployment export their ports to the host system, so ensure the following ports are available on your system: 3000, 8080, 8081, 8088. diff --git a/js/components/MainView.js b/js/components/MainView.js index b1df3cf9..bfbee4d4 100644 --- a/js/components/MainView.js +++ b/js/components/MainView.js @@ -52,7 +52,7 @@ class MainView extends React.Component { if (isUsingOidcAuth()) { window.location = userProfileLink(); } else { - transitionToWithOpts(Routes.editUser, {params: {username: this.props.user.username}}); + this.props.transitionToWithOpts(Routes.editUser, {params: {username: this.props.user.username}}); } } @@ -157,6 +157,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { - loadUserProfile: bindActionCreators(loadUserProfile, dispatch) + loadUserProfile: bindActionCreators(loadUserProfile, dispatch), + transitionToWithOpts: bindActionCreators(transitionToWithOpts, dispatch) } } \ No newline at end of file diff --git a/js/components/dashboard/Dashboard.js b/js/components/dashboard/Dashboard.js index b3e44453..c7298d86 100644 --- a/js/components/dashboard/Dashboard.js +++ b/js/components/dashboard/Dashboard.js @@ -68,7 +68,7 @@ class Dashboard extends React.Component { if (!formTemplate) { return {this.i18n('dashboard.records-tile')} + onClick={() => this.props.handlers.showRecords()}>{this.i18n('dashboard.records-tile')} } const showRecordsOfTemplate = () => { diff --git a/js/components/institution/Institution.js b/js/components/institution/Institution.js index d3d8a64f..3ef87ca6 100644 --- a/js/components/institution/Institution.js +++ b/js/components/institution/Institution.js @@ -68,7 +68,7 @@ class Institution extends React.Component {
diff --git a/js/components/institution/InstitutionPatients.js b/js/components/institution/InstitutionPatients.js index 2c425dd2..7f7204c0 100644 --- a/js/components/institution/InstitutionPatients.js +++ b/js/components/institution/InstitutionPatients.js @@ -1,7 +1,7 @@ 'use strict'; import React from 'react'; -import {Card} from 'react-bootstrap'; +import {Button, Card} from 'react-bootstrap'; import withI18n from '../../i18n/withI18n'; import {injectIntl} from "react-intl"; @@ -20,6 +20,9 @@ const InstitutionPatients = (props) => { handlers={{onEdit: onEdit}} disableDelete={true} currentUser={currentUser}/> +
+ +
; }; diff --git a/js/components/record/Record.js b/js/components/record/Record.js index df1ad05f..63856316 100644 --- a/js/components/record/Record.js +++ b/js/components/record/Record.js @@ -42,6 +42,11 @@ class Record extends React.Component { render() { const {recordLoaded, recordSaved, showAlert, record, formTemplate, currentUser} = this.props; + if (!record?.formTemplate) { + if (formTemplate) { + record.formTemplate = formTemplate; + } + } if (recordLoaded.status === ACTION_STATUS.ERROR) { return ; diff --git a/js/components/record/RecordForm.js b/js/components/record/RecordForm.js index d1d245eb..4e63b7f3 100644 --- a/js/components/record/RecordForm.js +++ b/js/components/record/RecordForm.js @@ -34,14 +34,16 @@ class RecordForm extends React.Component { } componentDidMount() { - this.props.loadFormgen(ACTION_STATUS.PENDING); - this.loadWizard(); + if(this.props.record.formTemplate) { + this.props.loadFormgen(ACTION_STATUS.PENDING); + this.loadWizard(); + } } componentDidUpdate(prevProps, prevState, snapshot) { const {record} = this.props; - - if (prevProps.record.question?.uri !== record.question?.uri) { + if(record.formTemplate && record.formTemplate != prevProps.record.formTemplate) { + this.props.loadFormgen(ACTION_STATUS.PENDING); this.loadWizard(); } } @@ -119,11 +121,11 @@ class RecordForm extends React.Component { return ; - } else if (this.props.formgen.status === ACTION_STATUS.PENDING || !this.state.form) { + } else if (!!this.props.record.formTemplate && (this.props.formgen.status === ACTION_STATUS.PENDING || !this.state.form)) { return ; } - return { + {isAdmin && + {record.institution.name} + } {isAdmin && {getFormTemplateOptionName(record.formTemplate, formTemplateOptions)} diff --git a/js/components/record/RecordTable.js b/js/components/record/RecordTable.js index 091be368..f6cd4f3f 100644 --- a/js/components/record/RecordTable.js +++ b/js/components/record/RecordTable.js @@ -87,6 +87,9 @@ class RecordTable extends React.Component { && {this.i18n('records.id')} } {this.i18n('records.local-name')} + {(this._isAdmin()) + && {this.i18n('institution.panel-title')} + } {(this._isAdmin()) && {this.i18n('records.form-template')} } diff --git a/js/components/record/Records.js b/js/components/record/Records.js index e3afbc4f..5830726d 100644 --- a/js/components/record/Records.js +++ b/js/components/record/Records.js @@ -33,13 +33,14 @@ class Records extends React.Component { } render() { - const {showAlert, recordDeleted, formTemplate} = this.props; + const {showAlert, recordDeleted, formTemplate, recordsLoaded} = this.props; const showCreateButton = STUDY_CREATE_AT_MOST_ONE_RECORD ? (!this.props.recordsLoaded.records || (this.props.recordsLoaded.records.length < 1)) : true; const showPublishButton = this.props.currentUser.role === ROLE.ADMIN && EXTENSIONS === EXTENSION_CONSTANTS.OPERATOR; + const showExportButton = !!recordsLoaded.records; const createRecordDisabled = STUDY_CLOSED_FOR_ADDITION && (!this._isAdmin()); @@ -57,13 +58,16 @@ class Records extends React.Component { -
+
{showCreateButton ? : null} + {showExportButton ? + + : null} {showPublishButton ? ; + } + } + + _externalEditUserButton() { + const {user, currentUser, handlers} = this.props; + if (!isUsingOidcAuth()) { + return null; + } if (user.isNew || (currentUser.username !== user.username && currentUser.role !== ROLE.ADMIN)) { return null; } else { return ; + onClick={handlers.onKeycloakRedirect}> + {this.i18n('user.edit')} + ; } } @@ -191,13 +212,13 @@ class User extends React.Component {
@@ -205,13 +226,14 @@ class User extends React.Component {
@@ -232,8 +254,8 @@ class User extends React.Component {
{this._generateRolesOptions()} @@ -250,19 +272,20 @@ class User extends React.Component { }
{this._impersonateButton()} - {this._passwordChange()} - {this._saveAndSendEmailButton()} - {(currentUser.role === ROLE.ADMIN || currentUser.username === user.username) && - + {this._passwordChangeButton()} + {this._externalEditUserButton()} + {!isUsingOidcAuth() && this._saveAndSendEmailButton()} + {(currentUser.role === ROLE.ADMIN || currentUser.username === user.username) && + }