From 934dbd21776a1a0bc3c642ab92ef0bf40bd19e6b Mon Sep 17 00:00:00 2001 From: PiTrem Date: Mon, 15 Apr 2024 12:53:03 +0200 Subject: [PATCH] intern: rename ipadress to url Internal: revamp TPA api - instantiate local file cache - UI: rm former TPA Btn internal: fix admin TPA edit - filename parameters optional on upload - handle api record not found / invalid --- .gitignore | 1 + app/api/api.rb | 1 - app/api/chemotion/third_party_app_api.rb | 334 ++++++++---------- app/api/chemotion/ui_api.rb | 1 + app/models/json_web_token.rb | 2 +- app/models/third_party_app.rb | 12 +- app/packs/src/apps/admin/ThirdPartyApp.js | 273 ++++++-------- .../ResearchPlanDetailsAttachments.js | 23 +- .../apps/mydb/elements/list/AttachmentList.js | 23 ++ app/packs/src/endpoints/ApiServices.js | 5 + .../src/fetchers/ThirdPartyAppFetcher.js | 124 +------ app/packs/src/stores/alt/stores/UIStore.js | 3 +- config/environments/development.rb | 2 +- config/environments/production.rb | 1 - .../20230213102539_create_third_party_apps.rb | 6 +- 15 files changed, 323 insertions(+), 488 deletions(-) diff --git a/.gitignore b/.gitignore index 374bc8b919..64886f77f0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ /config/compute_props.yml /config/converter.yml /config/ketcher_service.yml +/config/structure_editors.yml !/config/data_collector_keys/.keep /config/database.yml diff --git a/app/api/api.rb b/app/api/api.rb index 42a86d5663..eb9482043c 100644 --- a/app/api/api.rb +++ b/app/api/api.rb @@ -62,7 +62,6 @@ def authenticate! def is_public_request? request.path.start_with?( '/api/v1/public/', - '/api/v1/public_third_party_app/', '/api/v1/chemscanner/', '/api/v1/chemspectra/', '/api/v1/ketcher/layout', diff --git a/app/api/chemotion/third_party_app_api.rb b/app/api/chemotion/third_party_app_api.rb index b6d2885c82..edf56c8737 100644 --- a/app/api/chemotion/third_party_app_api.rb +++ b/app/api/chemotion/third_party_app_api.rb @@ -1,232 +1,194 @@ # frozen_string_literal: true +TPA_EXPIRATION = 48.hours + module Chemotion # Publish-Subscription MessageAPI class ThirdPartyAppAPI < Grape::API helpers do - def extract_values(payload) - att_id = payload[0]['attID']&.to_i - user_id = payload[0]['userID']&.to_i - name_third_party_app = payload[0]['nameThirdPartyApp']&.to_s - [att_id, user_id, name_third_party_app] + # desc: expiry time for the token and the cached upload/download counters + def expiry_time + @expiry_time ||= TPA_EXPIRATION.from_now end - def decode_token(token) - payload = JWT.decode(token, Rails.application.secrets.secret_key_base) unless token.nil? - error!('401 Unauthorized', 401) if payload&.length&.zero? - extract_values(payload) + # desc: instantiate local file cache for TPA + def cache + @cache ||= ActiveSupport::Cache::FileStore.new('tmp/ThirdPartyApp', expires_in: TPA_EXPIRATION) end - def verify_token(token) - payload = decode_token(token) - @attachment = Attachment.find_by(id: payload[0]) - @user = User.find_by(id: payload[1]) - error!('401 Unauthorized', 401) if @attachment.nil? || @user.nil? + # desc: fetch the token and download/upload counters from the cache + def cached_token + @cached_token ||= cache.read(cache_key) end - def perform_download(token_cached, attachment) - if token_cached.counter <= 3 - attachment.read_file - else - error!('Too many requests with this token', 403) - end + # desc: define the cache key based on the attachment/user/app ids + def cache_key + @cache_key ||= "#{@attachment&.id}/#{@user&.id}/#{@app&.id}" end - def update_cache(cache_key, token_cached) - error!('Invalid token', 403) if token_cached.nil? - token_cached.counter = token_cached.counter + 1 - Rails.cache.write(cache_key, token_cached) + # desc: prepare the token payload from the params + def prepare_payload + @payload = { + 'appID' => params[:appID], + 'userID' => current_user.id, + 'attID' => params[:attID], + } end - def download_third_party_app(token) - content_type 'application/octet-stream' - verify_token(token) - payload = decode_token(token) - @attachment = Attachment.find_by(id: payload[0]) - @user = User.find_by(id: payload[1]) - header['Content-Disposition'] = "attachment; filename=#{@attachment.filename}" - env['api.format'] = :binary - cache_key = "token/#{payload[0]}/#{payload[1]}/#{payload[2]}" - token_cached = Rails.cache.read(cache_key) - update_cache(cache_key, token_cached) - perform_download(token_cached, @attachment) - end - - def upload_third_party_app(token, file_name, file, file_type) - payload = decode_token(token) - cache_key = "token/#{payload[0]}/#{payload[1]}/#{payload[2]}" - token_cached = Rails.cache.read(cache_key) - update_cache(cache_key, token_cached) - if token_cached.counter > 30 - error!('To many request with this token', 403) - else - attachment = Attachment.find_by(id: payload[0]) - new_attachment = Attachment.new(attachable: attachment.attachable, - created_by: attachment.created_by, - created_for: attachment.created_for, - content_type: file_type) - File.open(file[:tempfile].path, 'rb') do |f| - new_attachment.update(file_path: f, filename: file_name) - end - { message: 'File uploaded successfully' } - end + # desc: find records from the payload + def parse_payload(payload = @payload) + # TODO: implement attachment authorization + @attachment = Attachment.find(payload['attID']&.to_i) + @user = User.find(payload['userID']&.to_i) + @app = ThirdPartyApp.find(payload['appID']&.to_i) + rescue ActiveRecord::RecordNotFound + error!('Record not found', 404) end - def encode_token(payload, name_third_party_app) - cache_key = cache_key_for_encoded_token(payload) - Rails.cache.fetch(cache_key, expires_in: 48.hours) do - token = JsonWebToken.encode(payload, 48.hours.from_now) - CachedTokenThirdPartyApp.new(token, 0, name_third_party_app) - end + # desc: decrement the counters / check if token permission is expired + def update_cache(key) + return error!('Invalid token', 403) if cached_token.nil? || cached_token[:token] != params[:token] + + # TODO: expire token when both counters reach 0 + # IDEA: split counters into two caches? + return error!("Token #{key} permission expired", 403) if cached_token[key].negative? + + cached_token[key] -= 1 + cache.write(cache_key, cached_token) end - def cache_key_for_encoded_token(payload) - "encoded_token/#{payload[:attID]}/#{payload[:userID]}/#{payload[:nameThirdPartyApp]}" + # desc: return file for download to third party app + def download_third_party_app + update_cache(:download) + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{@attachment.filename}" + env['api.format'] = :binary + @attachment.read_file + end + + # desc: upload file from the third party app + def upload_third_party_app + update_cache(:upload) + new_attachment = Attachment.new( + attachable: @attachment.attachable, + created_by: @attachment.created_by, + created_for: @attachment.created_for, + filename: params[:attachmentName].presence&.strip || "#{@app.name[0, 20]}-#{params[:file][:filename]}", + ) + new_attachment.save + new_attachment.attachment_attacher.attach params[:file][:tempfile].to_io + new_attachment.save + { message: 'File uploaded successfully' } + end + + def encode_and_cache_token(payload = @payload) + @token = JsonWebToken.encode(payload, expiry_time) + cache.write( + cache_key, + { token: @token, download: 3, upload: 10 }, + expires_at: expiry_time, + ) end end - namespace :public_third_party_app do - desc 'download file from third party app' - params do - requires :token, type: String, desc: 'Token for authentication' - end - get '/download' do - error!('401 Unauthorized', 401) if params[:token].nil? - download_third_party_app(params[:token]) - end + # desc: public endpoint for third party apps to {down,up}load files + namespace :public do + resource :third_party_apps, requirements: { token: /.*/ } do + route_param :token, regexp: /^[\w-]+\.[\w-]+\.[\w-]+$/ do + after_validation do + parse_payload(JsonWebToken.decode(params[:token])) + end + desc 'download file to 3rd party app' + get '/', requirements: { token: /.*/ } do + download_third_party_app + end - desc 'Upload file from third party app' - params do - requires :token, type: String, desc: 'Token for authentication' - requires :attachmentName, type: String, desc: 'Name of the attachment' - requires :fileType, type: String, desc: 'Type of the file' - end - post '/upload' do - error!('401 Unauthorized', 401) if params[:token].nil? - error!('401 Unauthorized', 401) if params[:attachmentName].nil? - error!('401 Unauthorized', 401) if params[:fileType].nil? - verify_token(params[:token]) - upload_third_party_app(params[:token], - params[:attachmentName], - params[:file], - params[:file_type]) + desc 'Upload file from 3rd party app' + params do + requires :file, type: File, desc: 'File to upload' + optional :attachmentName, type: String, desc: 'Name of the file' + end + post '/' do + upload_third_party_app + end + end end end - resource :third_party_apps_administration do - before do - error(401) unless current_user.is_a?(Admin) + resource :third_party_apps do + rescue_from ActiveRecord::RecordNotFound do + error!('Record not found', 404) end + namespace :admin do + before do + error!('Unauthorized. User has to be admin.', 401) unless current_user.is_a?(Admin) + end + after_validation do + params[:name]&.strip! + params[:url]&.strip! + end - desc 'check that name is unique' - params do - requires :name - end - post '/name_unique' do - declared(params, include_missing: false) - result = ThirdPartyApp.all_names - if result.nil? - { message: 'Name is unique' } - elsif ThirdPartyApp.all_names.exclude?(params[:name]) - { message: 'Name is unique' } - else - { message: 'Name is not unique' } - end.to_json - rescue ActiveRecord::RecordInvalid - error!('Unauthorized. User has to be admin.', 401) - end - - desc 'create new third party app entry' - params do - requires :IPAddress, type: String, desc: 'The IPAddress in order to redirect to the app.' - requires :name, type: String, desc: 'name of third party app. User will chose correct app based on names.' - end - post '/new_third_party_app' do - declared(params, include_missing: false) - ThirdPartyApp.create!(IPAddress: params[:IPAddress], name: params[:name]) - status 201 - rescue ActiveRecord::RecordInvalid - error!('Unauthorized. User has to be admin.', 401) - end + desc 'create new third party app entry' + params do + requires :url, type: String, allow_blank: false, desc: 'The url in order to redirect to the app.' + requires :name, type: String, allow_blank: false, + desc: 'name of third party app. User will chose correct app based on names.' + end - desc 'update a third party app entry' - params do - requires :id, type: String, desc: 'The id of the app which should be updated' - requires :IPAddress, type: String, desc: 'The IPAddress in order to redirect to the app.' - requires :name, type: String, desc: 'name of third party app. User will chose correct app based on names.' - end - post '/update_third_party_app' do - declared(params, include_missing: false) - entry = ThirdPartyApp.find(params[:id]) - entry.update!(IPAddress: params[:IPAddress], name: params[:name]) - status 201 - rescue ActiveRecord::RecordInvalid - error!('Unauthorized. User has to be admin.', 401) - end + rescue_from ActiveRecord::RecordInvalid do |e| + error!(e.record.errors.full_messages.join(', '), 400) + end - desc 'delete third party app entry' - params do - requires :id, type: String, desc: 'The id of the app which should be deleted' - end - post '/delete_third_party_app' do - id = params[:id].to_i - ThirdPartyApp.delete(id) - status 201 - rescue ActiveRecord::RecordInvalid - error!('Unauthorized. User has to be admin.', 401) - end - end + post do + ThirdPartyApp.create!(declared(params)) + status 201 + end - resource :third_party_apps do - desc 'Find all thirdPartyApps' - get 'all' do - ThirdPartyApp.all - end + route_param :id, type: Integer, desc: '3rd party app id' do + desc 'update a third party app entry' + params do + optional :url, type: String, allow_blank: false, desc: 'The url where the 3rd party app lives.' + optional :name, type: String, allow_blank: false, desc: 'Name of third party app.' + end - desc 'get third party app by id' - params do - requires :id, type: String, desc: 'The id of the app' - end - get 'get_by_id' do - ThirdPartyApp.find(params[:id]) - end + put do + ThirdPartyApp.find(params[:id]).update!(declared(params, include_missing: false)) + status 201 + end - desc 'get ip address of third party app' - params do - requires :name, type: String, desc: 'The name of the app for which the ip address should be get.' + desc 'delete third party app entry' + delete do + ThirdPartyApp.delete(params[:id]) + status 201 + end + end end - get 'IP' do - tpa = ThirdPartyApp.find_by(name: params[:name]) - return tpa.IPAddress if tpa - error_msg = "Third party app with ID: #{id} not found" - { error: error_msg } + desc 'get all thirdPartyApps' + get do + ThirdPartyApp.all end - desc 'get public ip address of ELN' - get 'public_IP' do - uri = URI.parse(ENV['PUBLIC_URL'] || 'http://localhost:3000') - end - desc 'create token for use in download public_api' params do - requires :attID, type: String, desc: 'Attachment ID' - requires :userID, type: String, desc: 'User ID' - requires :nameThirdPartyApp, type: String, desc: 'name of the third party app' + requires :attID, type: Integer, desc: 'Attachment ID' + requires :appID, type: Integer, desc: 'id of the third party app' end - get 'Token' do - cache_key = "token/#{params[:attID]}/#{params[:userID]}/#{params[:nameThirdPartyApp]}" - payload = { attID: params[:attID], userID: params[:userID], nameThirdPartyApp: params[:nameThirdPartyApp] } - cached_token = encode_token(payload, params[:nameThirdPartyApp]) - Rails.cache.write(cache_key, cached_token, expires_in: 48.hours) - cached_token.token + + get 'token' do + prepare_payload + parse_payload + encode_and_cache_token + # redirect url with callback url to {down,up}load file: NB path should match the public endpoint + url = CGI.escape("#{Rails.application.config.root_url}/api/v1/public/third_party_apps/#{@token}") + "#{@app.url}?url=#{url}" end - end - resource :names do - desc 'Find all names of all third party app' - get 'all' do - ThirdPartyApp.all_names + route_param :id, type: Integer, desc: '3rd party app id' do + desc 'get a thirdPartyApps by id' + get do + ThirdPartyApp.find(params[:id]) + end end end end diff --git a/app/api/chemotion/ui_api.rb b/app/api/chemotion/ui_api.rb index e00a31c134..cc6866fb4f 100644 --- a/app/api/chemotion/ui_api.rb +++ b/app/api/chemotion/ui_api.rb @@ -29,6 +29,7 @@ class UiAPI < Grape::API has_converter: converter_config.present?, has_radar: radar_config.present?, collector_address: collector_address.presence, + third_party_apps: ThirdPartyApp.all } end end diff --git a/app/models/json_web_token.rb b/app/models/json_web_token.rb index 9d57d21fba..29ae387301 100644 --- a/app/models/json_web_token.rb +++ b/app/models/json_web_token.rb @@ -3,7 +3,7 @@ class JsonWebToken def self.encode(payload, exp = 6.months.from_now) payload[:exp] = exp.to_i - JWT.encode(payload, Rails.application.secret_key_base) + JWT.encode(payload, Rails.application.secret_key_base, 'HS256') end def self.decode(token) diff --git a/app/models/third_party_app.rb b/app/models/third_party_app.rb index 8e8ece876a..a77d9345c0 100644 --- a/app/models/third_party_app.rb +++ b/app/models/third_party_app.rb @@ -1,14 +1,6 @@ # frozen_string_literal: true class ThirdPartyApp < ApplicationRecord - def self.all_names - return nil if ThirdPartyApp.count.zero? - - entries = ThirdPartyApp.all - names = [] - entries.each do |e| - names << e.name - end - names - end + validates :name, presence: true, uniqueness: true, length: { maximum: 100 } + validates :url, presence: true, uniqueness: true, length: { maximum: 100 } end diff --git a/app/packs/src/apps/admin/ThirdPartyApp.js b/app/packs/src/apps/admin/ThirdPartyApp.js index dcb085880d..c9de5a3100 100644 --- a/app/packs/src/apps/admin/ThirdPartyApp.js +++ b/app/packs/src/apps/admin/ThirdPartyApp.js @@ -1,5 +1,7 @@ import React from 'react'; -import { Panel, Table, Button, Modal, FormGroup, ControlLabel, Col, FormControl, Tooltip, OverlayTrigger } from 'react-bootstrap'; +import { + Panel, Table, Button, Modal, FormGroup, ControlLabel, Col, FormControl, Tooltip, OverlayTrigger +} from 'react-bootstrap'; import ThirdPartyAppFetcher from 'src/fetchers/ThirdPartyAppFetcher'; const editTip = edit third party app; @@ -15,32 +17,18 @@ export default class ThirdPartyApp extends React.Component { showMsgModalDelete: false, messageNewThirdPartyAppModal: '', thirdPartyApps: [], - tpaNotEmpty: true, - errorMessageNewTPA: '', - errorMessageEditTPA: '', - thirdPartyAppNames: [""], + errorMessage: '', currentName: '', currentIP: '', currentID: '' }; - this.thirdPartyApps(); this.closeNewThirdPartyAppModal = this.closeNewThirdPartyAppModal.bind(this); this.closeEditThirdPartyAppModal = this.closeEditThirdPartyAppModal.bind(this); this.closeDeleteThirdPartyAppModal = this.closeDeleteThirdPartyAppModal.bind(this); - this.toggleTPANotEmpty = this.toggleTPANotEmpty.bind(this); - } - - toggleTPANotEmpty() { - if (this.state.thirdPartyApps.length == 0) { - this.setState(prevState => ({ - tpaNotEmpty: !prevState.tpaNotEmpty - }) - ); - } } componentDidMount() { - this.getThirdPartyAppNames(); + this.thirdPartyApps(); } thirdPartyApps() { @@ -49,23 +37,11 @@ export default class ThirdPartyApp extends React.Component { this.setState({ thirdPartyApps: result }); - }) - .then(() => { - if (this.state.thirdPartyApps.length == 0) { - this.setState({ - tpaNotEmpty: false - }); - } - }) - .then(() => { - this.toggleTPANotEmpty() }); } - new(name, IPAddress) { - ThirdPartyAppFetcher.newThirdPartyApp( - name, - IPAddress) + newApp(name, url) { + ThirdPartyAppFetcher.createOrUpdateThirdPartyApp(null, name, url) .then((result) => { if (result.error) { this.setState({ messageNewThirdPartyAppModal: result.error }); @@ -77,15 +53,16 @@ export default class ThirdPartyApp extends React.Component { return true; } - edit(name, IPAddress) { - return ThirdPartyAppFetcher.editThirdPartyApp( + edit(name, url) { + return ThirdPartyAppFetcher.createOrUpdateThirdPartyApp( this.state.currentID, name, - IPAddress) + url + ) .then((result) => { if (result.error) { return this.thirdPartyApps().then((res) => { - res.messageNewThirdPartyAppModal = result.error + res.messageNewThirdPartyAppModal = result.error; this.setState(res); return false; }); @@ -96,14 +73,14 @@ export default class ThirdPartyApp extends React.Component { delete(id) { ThirdPartyAppFetcher.deleteThirdPartyApp( - id) + id + ) .then((result) => { if (result.error) { this.setState({ messageNewThirdPartyAppModal: result.error }); return false; } this.thirdPartyApps(); - this.getThirdPartyAppNames(); return true; }); this.setState({ @@ -119,10 +96,15 @@ export default class ThirdPartyApp extends React.Component { } showEditThirdPartyAppModal(key) { + const { thirdPartyApps } = this.state; + // select app by key from thirdPartyApps + const app = thirdPartyApps.find((tpa) => tpa.id === key); this.setState({ - showMsgModalEdit: true - }) - this.getThirdPartyAppByID(key); + showMsgModalEdit: true, + currentName: app?.name, + currentIP: app?.url, + currentID: key + }); } showDeleteThirdPartyAppModal(key) { @@ -130,12 +112,12 @@ export default class ThirdPartyApp extends React.Component { showMsgModalDelete: true, currentID: key }); - this.getThirdPartyAppByID(key); } closeNewThirdPartyAppModal() { this.setState({ - showMsgModal: false + showMsgModal: false, + errorMessage: null, }); } @@ -152,71 +134,44 @@ export default class ThirdPartyApp extends React.Component { } checkInput(name, ip) { - return new Promise((resolve, reject) => { - + const { thirdPartyApps, currentID } = this.state; + let appId = 0; if (name.length < 1) { this.setState({ - errorMessageNewTPA: "name is shorter than 1 character" + errorMessage: 'Name is shorter than 1 character' }); - reject(); + return false; } - if ((ip.slice(0, 7) != "http://") && - (ip.slice(0, 8) != "https://")) { + if ((ip.slice(0, 7) !== 'http://') + && (ip.slice(0, 8) !== 'https://')) { this.setState({ - errorMessageNewTPA: "Begin of ip address has to be http:// or https://" + errorMessage: 'URL should start with http:// or https://' }); - reject(); + return false; } - - ThirdPartyAppFetcher.isNameUnique(name) - .then((result) => { - const message = JSON.parse(result).message - if (message == "Name is not unique") { - this.setState({ - errorMessageNewTPA: "name is not unique" - }); - reject(); - } else { - resolve(); - } - }) - - }) - - } - - getThirdPartyAppNames() { - ThirdPartyAppFetcher.fetchThirdPartyAppNames() - .then((result) => { - this.setState({ - thirdPartyAppNames: result - }) - }) - .then(() => { - if (this.state.thirdPartyAppNames == null || !Array.isArray(this.state.thirdPartyAppNames) || this.state.thirdPartyAppNames.length === 0) { - this.setState({ - thirdPartyAppNames: [] - }) - } + // check if name is already used + if (thirdPartyApps.find((tpa) => { appId = tpa.id; return currentID !== tpa.id && tpa.name === name; })) { + this.setState({ + errorMessage: `Name is already used by app with id: ${appId}` }); - } - - getThirdPartyAppByID(key) { - ThirdPartyAppFetcher.fetchThirdPartyAppByID(key) - .then((result) => { - this.setState({ - currentName: result.name, - currentIP: result.IPAddress, - currentID: key - }) + return false; + } + // check if url is already used + if (thirdPartyApps.find((tpa) => { appId = tpa.id; return currentID !== tpa.id && tpa.url === ip; })) { + this.setState({ + errorMessage: `URL already used by app with id: ${appId}` }); + return false; + } + return true; } renderDeleteThirdPartyAppModal() { + const { showMsgModalDelete, currentName, currentID } = this.state; return ( @@ -224,53 +179,57 @@ export default class ThirdPartyApp extends React.Component {
-

Do you really want to delete {this.state.currentName}?

- - -
- ) + ); } - renderEditThirdPartyAppModal() { - let nameRef = null; - let IPAddressRef = null; + let urlRef = null; const handleEdit = () => { - - const IPAddress = IPAddressRef.value; - const name = nameRef.value; - this.checkInput(name, IPAddress) - .then(() => { - this.edit(name, IPAddress).then(() => { - this.getThirdPartyAppNames(); + const url = urlRef.value?.trim(); + const name = nameRef.value?.trim(); + if (this.checkInput(name, url)) { + this.edit(name, url) + .then(() => { this.closeEditThirdPartyAppModal(); this.thirdPartyApps(); - }); - }) - .catch(() => { - }) - - } + }) + .catch(() => {}); + } + }; const handleNameChange = (event) => { - this.setState({ - currentName: event.target.value - }) - } + // if current errorMessage start with Name, clear it + const newState = { currentName: event.target.value }; + if (this.state.errorMessage.startsWith('Name')) { + newState.errorMessage = ''; + } + this.setState(newState); + }; const handleIPChange = (event) => { - this.setState({ - currentIP: event.target.value - }) - } + const newState = { currentIP: event.target.value }; + if (this.state.errorMessage.startsWith('URL')) { + newState.errorMessage = ''; + } + this.setState(newState); + }; return ( - { nameRef = ref; }} /> + { nameRef = ref; }} + /> @@ -296,46 +261,40 @@ export default class ThirdPartyApp extends React.Component { IP address: - { IPAddressRef = ref; }} /> + { urlRef = ref; }} /> - + - + - ) + ); } renderMessageModal() { - let nameRef = null; - let IPAddressRef = null; + let urlRef = null; const handleCreate = () => { - - this.getThirdPartyAppNames(); - const IPAddress = IPAddressRef.value; + const url = urlRef.value; const name = nameRef.value; - this.checkInput(name, IPAddress) - .then(() => { - this.new(name, IPAddress); - this.closeNewThirdPartyAppModal(); - }) - .catch(() => { - }); - - } + if (this.checkInput(name, url)) { + this.newApp(name, url); + this.closeNewThirdPartyAppModal(); + } + }; return ( - { IPAddressRef = ref; }} /> + { urlRef = ref; }} /> - + - + - ) + ); } - render() { - return (
{this.renderMessageModal()} @@ -411,12 +370,12 @@ export default class ThirdPartyApp extends React.Component { - {this.state.tpaNotEmpty && this.state.thirdPartyApps.map((entry) => ( + {this.state.thirdPartyApps.map((entry) => ( - + @@ -424,7 +383,7 @@ export default class ThirdPartyApp extends React.Component { {this.renderEditThirdPartyAppModal()} - +