[NEW] Admin refactor Second phase (#17551)
* [NEW] Redesign Administration > Connectivity Services (#17525) * [NEW] Admin Rewrite -> Integrations Page (#17505) * [NEW] Admin Rewrite -> OAuth Apps (#17646) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com>pull/17663/head
parent
811688e119
commit
11a7ed87bb
@ -1,16 +0,0 @@ |
||||
<template name="cloudCallback"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{> header sectionName="Cloud_connect"}} |
||||
<div class="content"> |
||||
{{#requiresPermission 'manage-cloud'}} |
||||
{{#if callbackError.error}} |
||||
<p>{{_ "Cloud_error_in_authenticating"}}</p> |
||||
|
||||
<p>{{_ "Cloud_error_code"}} {{ callbackError.errorCode }}</p> |
||||
{{/if}} |
||||
{{/requiresPermission}} |
||||
</div> |
||||
</section> |
||||
</div> |
||||
</template> |
||||
@ -1,46 +0,0 @@ |
||||
import './callback.html'; |
||||
|
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import queryString from 'query-string'; |
||||
|
||||
import { SideNav } from '../../../ui-utils/client'; |
||||
|
||||
|
||||
Template.cloudCallback.onCreated(function() { |
||||
const instance = this; |
||||
|
||||
instance.loading = new ReactiveVar(true); |
||||
instance.callbackError = new ReactiveVar({ error: false }); |
||||
|
||||
const params = queryString.parse(location.search); |
||||
|
||||
if (params.error_code) { |
||||
instance.callbackError.set({ error: true, errorCode: params.error_code }); |
||||
} else { |
||||
Meteor.call('cloud:finishOAuthAuthorization', params.code, params.state, (error) => { |
||||
if (error) { |
||||
console.warn('cloud:finishOAuthAuthorization', error); |
||||
return; |
||||
} |
||||
|
||||
FlowRouter.go('/admin/cloud'); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
Template.cloudCallback.helpers({ |
||||
callbackError() { |
||||
return Template.instance().callbackError.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.cloudCallback.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,145 +0,0 @@ |
||||
<template name="cloud"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{#header sectionName="Connectivity_Services" hideHelp=true fixedHeight=true fullpage=true}} |
||||
<div class="rc-header__section-button"> |
||||
{{#unless info.workspaceRegistered}} |
||||
<button class="rc-button rc-button--small rc-button--primary rc-button--outline js-register"> |
||||
{{_ "Cloud_Register_manually"}} |
||||
</button> |
||||
{{/unless}} |
||||
<a href="https://cloud.rocket.chat" class="rc-button rc-button--primary action cloud-console-btn" target="_blank">{{_ "Cloud_console"}}</a> |
||||
</div> |
||||
{{/header}} |
||||
<div class="content"> |
||||
{{#requiresPermission 'manage-cloud'}} |
||||
<div class="section"> |
||||
|
||||
<div class="section-title"> |
||||
<div class="section-title-text"> |
||||
{{_ "Cloud_what_is_it"}} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="section-content"> |
||||
<p>{{_ "Cloud_what_is_it_description"}}</p> |
||||
</div> |
||||
|
||||
<details> |
||||
<div class="section-content"> |
||||
<p>{{_ "Cloud_what_is_it_services_like"}}</p> |
||||
<ul style="list-style-type:disc;margin-left:15px;"> |
||||
<li>{{_ "Register_Server_Registered_Push_Notifications"}}</li> |
||||
<li>{{_ "Register_Server_Registered_Livechat"}}</li> |
||||
<li>{{_ "Register_Server_Registered_OAuth"}}</li> |
||||
<li>{{_ "Register_Server_Registered_Marketplace"}}</li> |
||||
</ul> |
||||
</div> |
||||
<div class="section-content"> |
||||
{{_ "Cloud_what_is_it_additional"}} |
||||
</div> |
||||
</details> |
||||
</div> |
||||
<div class="section"> |
||||
{{#if info.connectToCloud}} |
||||
{{#if info.workspaceRegistered}} |
||||
<div class="section-content border-component-color"> |
||||
<p>{{_ "Cloud_workspace_connected"}}</p> |
||||
<div class="input-line double-col"> |
||||
{{#if isLoggedIn}} |
||||
<label class="setting-label" title=""></label> |
||||
<div class="setting-field"> |
||||
<button type="button" class="rc-button rc-button--primary action logout-btn" target="_blank">{{_ "Cloud_logout"}}</button> |
||||
</div> |
||||
{{else}} |
||||
<label class="setting-label" title=""></label> |
||||
<div class="setting-field"> |
||||
<button type="button" class="rc-button rc-button--primary action login-btn" target="_blank">{{_ "Cloud_login_to_cloud"}}</button> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="section-content border-component-color"> |
||||
<p>{{_ "Cloud_workspace_disconnect"}}</p> |
||||
<div class="input-line double-col"> |
||||
<label class="setting-label" title=""></label> |
||||
<div class="setting-field"> |
||||
<button type="button" class="rc-button rc-button--danger action disconnect-btn">{{_ "Disconnect"}}</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
<div class="section-content border-component-color"> |
||||
<div class="input-line double-col"> |
||||
<label class="setting-label" title="cloudEmail">{{_ "Email"}}</label> |
||||
<div class="setting-field"> |
||||
<input class="input-monitor rc-input__element" type="text" name="cloudEmail" value="{{ info.email }}"> |
||||
<div class="settings-description secondary-font-color">{{_ "Cloud_address_to_send_registration_to"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label class="setting-label" title=""></label> |
||||
<div class="setting-field"> |
||||
<button type="button" class="rc-button rc-button--primary action update-email-btn" style="float:left">{{_ "Cloud_update_email"}}</button> |
||||
<button type="button" class="rc-button rc-button--primary action resend-email-btn" style="float:left;margin-left:5px;">{{_ "Cloud_resend_email"}}</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="input-line double-col"> |
||||
<label class="setting-label" title="cloudToken">{{_ "Token"}}</label> |
||||
<div class="setting-field"> |
||||
<input class="input-monitor rc-input__element" type="text" name="cloudToken" value="{{ info.token }}"> |
||||
<div class="settings-description secondary-font-color">{{_ "Cloud_manually_input_token"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label class="setting-label" title=""></label> |
||||
<div class="setting-field"> |
||||
<button type="button" class="rc-button rc-button--primary action connect-btn">{{_ "Connect"}}</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<p>{{_ "Cloud_connect_support"}}: <a href="mailto:support@rocket.chat?subject=[Self Hosted Registration]&body=WorkspaceId: {{ info.workspaceId }}%0D%0ADeployment Id: {{ info.uniqueId }}%0D%0AIssue: <please describe your issue here>">support@rocket.chat</a></p> |
||||
</div> |
||||
{{/if}} |
||||
{{else}} |
||||
<div class="section-title"> |
||||
<div class="section-title-text"> |
||||
{{_ "Cloud_registration_required"}} |
||||
</div> |
||||
</div> |
||||
<div class="section-content border-component-color"> |
||||
<p>{{_ "Cloud_registration_required_description"}}</p> |
||||
<button type="button" class="rc-button rc-button--primary action register-btn">{{_ "Cloud_registration_requried_link_text"}}</button> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
{{#if info.connectToCloud}} |
||||
<div class="section"> |
||||
<div class="section-title"> |
||||
<div class="section-title-text"> |
||||
{{_ "Cloud_troubleshooting"}} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="section-content border-component-color"> |
||||
<p>{{_ "Cloud_workspace_support"}}</p> |
||||
<div class="input-line double-col"> |
||||
<label class="setting-label" title=""></label> |
||||
<div class="setting-field"> |
||||
<button type="button" class="rc-button rc-button--danger action sync-btn">{{_ "Sync"}}</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="section-content"> |
||||
{{_ "Cloud_status_page_description"}}: <a href="https://status.rocket.chat" target="_blank">status.rocket.chat</a> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{/requiresPermission}} |
||||
</div> |
||||
</section> |
||||
</div> |
||||
</template> |
||||
@ -1,233 +0,0 @@ |
||||
import './cloud.html'; |
||||
|
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import queryString from 'query-string'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { t } from '../../../utils'; |
||||
import { SideNav, modal } from '../../../ui-utils/client'; |
||||
|
||||
|
||||
Template.cloud.onCreated(function() { |
||||
const instance = this; |
||||
instance.info = new ReactiveVar(); |
||||
instance.loading = new ReactiveVar(true); |
||||
instance.isLoggedIn = new ReactiveVar(false); |
||||
|
||||
instance.loadRegStatus = function _loadRegStatus() { |
||||
Meteor.call('cloud:checkRegisterStatus', (error, info) => { |
||||
if (error) { |
||||
console.warn('cloud:checkRegisterStatus', error); |
||||
return; |
||||
} |
||||
|
||||
instance.info.set(info); |
||||
instance.loading.set(false); |
||||
}); |
||||
}; |
||||
|
||||
instance.getLoggedIn = function _getLoggedIn() { |
||||
Meteor.call('cloud:checkUserLoggedIn', (error, result) => { |
||||
if (error) { |
||||
console.warn(error); |
||||
return; |
||||
} |
||||
|
||||
instance.isLoggedIn.set(result); |
||||
}); |
||||
}; |
||||
|
||||
instance.oauthAuthorize = function _oauthAuthorize() { |
||||
Meteor.call('cloud:getOAuthAuthorizationUrl', (error, url) => { |
||||
if (error) { |
||||
console.warn(error); |
||||
return; |
||||
} |
||||
|
||||
window.location.href = url; |
||||
}); |
||||
}; |
||||
|
||||
instance.logout = function _logout() { |
||||
Meteor.call('cloud:logout', (error) => { |
||||
if (error) { |
||||
console.warn(error); |
||||
return; |
||||
} |
||||
|
||||
instance.getLoggedIn(); |
||||
}); |
||||
}; |
||||
|
||||
instance.connectWorkspace = function _connectWorkspace(token) { |
||||
Meteor.call('cloud:connectWorkspace', token, (error, success) => { |
||||
if (error) { |
||||
toastr.error(error); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
if (!success) { |
||||
toastr.error('An error occured connecting'); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
toastr.success(t('Connected')); |
||||
|
||||
instance.loadRegStatus(); |
||||
}); |
||||
}; |
||||
|
||||
instance.disconnectWorkspace = function _disconnectWorkspace() { |
||||
Meteor.call('cloud:disconnectWorkspace', (error, success) => { |
||||
if (error) { |
||||
toastr.error(error); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
if (!success) { |
||||
toastr.error('An error occured disconnecting'); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
toastr.success(t('Disconnected')); |
||||
|
||||
instance.loadRegStatus(); |
||||
}); |
||||
}; |
||||
|
||||
instance.syncWorkspace = function _syncWorkspace() { |
||||
Meteor.call('cloud:syncWorkspace', (error, success) => { |
||||
if (error) { |
||||
toastr.error(error); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
if (!success) { |
||||
toastr.error('An error occured syncing'); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
toastr.success(t('Sync Complete')); |
||||
|
||||
instance.loadRegStatus(); |
||||
}); |
||||
}; |
||||
|
||||
instance.registerWorkspace = function _registerWorkspace() { |
||||
Meteor.call('cloud:registerWorkspace', (error, success) => { |
||||
if (error) { |
||||
toastr.error(error); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
if (!success) { |
||||
toastr.error('An error occured'); |
||||
instance.loadRegStatus(); |
||||
return; |
||||
} |
||||
|
||||
return instance.syncWorkspace(); |
||||
}); |
||||
}; |
||||
|
||||
const params = queryString.parse(location.search); |
||||
|
||||
if (params.token) { |
||||
instance.connectWorkspace(params.token); |
||||
} else { |
||||
instance.loadRegStatus(); |
||||
} |
||||
|
||||
instance.getLoggedIn(); |
||||
}); |
||||
|
||||
Template.cloud.helpers({ |
||||
info() { |
||||
return Template.instance().info.get(); |
||||
}, |
||||
isLoggedIn() { |
||||
return Template.instance().isLoggedIn.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.cloud.events({ |
||||
'click .js-register'() { |
||||
modal.open({ |
||||
template: 'cloudRegisterManually', |
||||
showCancelButton: false, |
||||
showConfirmButton: false, |
||||
showFooter: false, |
||||
closeOnCancel: true, |
||||
html: true, |
||||
confirmOnEnter: false, |
||||
}); |
||||
}, |
||||
'click .update-email-btn'() { |
||||
const val = $('input[name=cloudEmail]').val(); |
||||
|
||||
Meteor.call('cloud:updateEmail', val, false, (error) => { |
||||
if (error) { |
||||
console.warn(error); |
||||
return; |
||||
} |
||||
|
||||
toastr.success(t('Saved')); |
||||
}); |
||||
}, |
||||
|
||||
'click .resend-email-btn'() { |
||||
const val = $('input[name=cloudEmail]').val(); |
||||
|
||||
Meteor.call('cloud:updateEmail', val, true, (error) => { |
||||
if (error) { |
||||
console.warn(error); |
||||
return; |
||||
} |
||||
|
||||
toastr.success(t('Requested')); |
||||
}); |
||||
}, |
||||
|
||||
'click .login-btn'(e, i) { |
||||
i.oauthAuthorize(); |
||||
}, |
||||
|
||||
'click .logout-btn'(e, i) { |
||||
i.logout(); |
||||
}, |
||||
|
||||
'click .connect-btn'(e, i) { |
||||
const token = $('input[name=cloudToken]').val(); |
||||
|
||||
i.connectWorkspace(token); |
||||
}, |
||||
|
||||
'click .register-btn'(e, i) { |
||||
i.registerWorkspace(); |
||||
}, |
||||
|
||||
'click .disconnect-btn'(e, i) { |
||||
i.disconnectWorkspace(); |
||||
}, |
||||
|
||||
'click .sync-btn'(e, i) { |
||||
i.syncWorkspace(); |
||||
}, |
||||
}); |
||||
|
||||
Template.cloud.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,26 +0,0 @@ |
||||
.rc-promtp { |
||||
display: flex; |
||||
|
||||
min-height: 188px; |
||||
padding: 1rem; |
||||
|
||||
border-radius: 2px; |
||||
background: #2f343d; |
||||
flex-flow: column wrap; |
||||
justify-content: space-between; |
||||
|
||||
&--element, |
||||
&--element[disabled] { |
||||
flex: 1 1 auto; |
||||
|
||||
resize: none; |
||||
|
||||
color: #cbced1; |
||||
border: none; |
||||
background: none; |
||||
|
||||
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
} |
||||
} |
||||
@ -1,36 +0,0 @@ |
||||
<template name="cloudRegisterManually"> |
||||
{{> header sectionName="Cloud_Register_manually" hideHelp=true fullpage=true}} |
||||
{{# if copyStep }} |
||||
<form class="preferences-page__content"> |
||||
<p class="rc-modal__description">{{_ "Cloud_register_offline_helper" }}</p> |
||||
<div class="rc-promtp"> |
||||
<textarea class="rc-promtp--element" disabled>{{clientKey}}</textarea> |
||||
<button class="rc-button rc-button--primary js-copy" data-clipboard-text="{{clientKey}}"> |
||||
{{>icon icon='copy'}} {{_ "Copy"}} |
||||
</button> |
||||
</div> |
||||
<p class="rc-modal__description js-cloud">{{#if cloudLink}} {{{cloudLink}}} {{else}} <a href="https://cloud.rocket.chat" rel="noopener noreferrer" class="cloud-console-btn" target="_blank"></a>{{/if}}</p> |
||||
</form> |
||||
|
||||
<footer class="rc-modal__footer rc-modal__footer--empty"> |
||||
<button class="rc-button rc-button--primary js-next">{{_ "Next"}}</button> |
||||
</footer> |
||||
|
||||
{{else}} |
||||
|
||||
<form class="preferences-page__content"> |
||||
<p class="rc-modal__description">{{_ "Cloud_register_offline_finish_helper"}}</p> |
||||
<div class="rc-promtp"> |
||||
<textarea class="js-cloud-key rc-promtp--element" placeholder="{{_ "Paste_here"}}" disabled={{isLoading}}></textarea> |
||||
</div> |
||||
</form> |
||||
|
||||
<footer class="rc-modal__footer rc-modal__footer--empty"> |
||||
<button class="rc-button rc-button--secondary js-back">{{_ "Back"}}</button> |
||||
<button class="rc-button rc-button--primary js-finish" disabled='{{disabled}}'> |
||||
{{#if isLoading}} {{> loading}} {{/if}} |
||||
<span style="{{#if isLoading}} visibility:hidden {{/if}}">{{_ "Finish Registration"}}</span> |
||||
</button> |
||||
</footer> |
||||
{{/if}} |
||||
</template> |
||||
@ -1,106 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import Clipboard from 'clipboard'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { APIClient } from '../../../utils/client'; |
||||
import { modal } from '../../../ui-utils/client'; |
||||
|
||||
import './cloudRegisterManually.html'; |
||||
import './cloudRegisterManually.css'; |
||||
|
||||
const CLOUD_STEPS = { |
||||
COPY: 0, |
||||
PASTE: 1, |
||||
DONE: 2, |
||||
ERROR: 3, |
||||
}; |
||||
|
||||
Template.cloudRegisterManually.events({ |
||||
'submit form'(e) { |
||||
e.preventDefault(); |
||||
}, |
||||
'input .js-cloud-key'(e, instance) { |
||||
instance.state.set('cloudKey', e.currentTarget.value); |
||||
}, |
||||
'click .js-next'(event, instance) { |
||||
instance.state.set('step', CLOUD_STEPS.PASTE); |
||||
}, |
||||
'click .js-back'(event, instance) { |
||||
instance.state.set('step', CLOUD_STEPS.COPY); |
||||
}, |
||||
'click .js-finish'(event, instance) { |
||||
instance.state.set('loading', true); |
||||
|
||||
APIClient |
||||
.post('v1/cloud.manualRegister', {}, { cloudBlob: instance.state.get('cloudKey') }) |
||||
.then(() => modal.open({ |
||||
type: 'success', |
||||
title: TAPi18n.__('Success'), |
||||
text: TAPi18n.__('Cloud_register_success'), |
||||
confirmButtonText: TAPi18n.__('Ok'), |
||||
closeOnConfirm: false, |
||||
showCancelButton: false, |
||||
}, () => window.location.reload())) |
||||
.catch(() => modal.open({ |
||||
type: 'error', |
||||
title: TAPi18n.__('Error'), |
||||
text: TAPi18n.__('Cloud_register_error'), |
||||
})) |
||||
.then(() => instance.state.set('loading', false)); |
||||
}, |
||||
}); |
||||
|
||||
Template.cloudRegisterManually.helpers({ |
||||
cloudLink() { |
||||
return Template.instance().cloudLink.get(); |
||||
}, |
||||
copyStep() { |
||||
return Template.instance().state.get('step') === CLOUD_STEPS.COPY; |
||||
}, |
||||
clientKey() { |
||||
return Template.instance().state.get('clientKey'); |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().state.get('loading'); |
||||
}, |
||||
step() { |
||||
return Template.instance().state.get('step'); |
||||
}, |
||||
disabled() { |
||||
const { state } = Template.instance(); |
||||
|
||||
const shouldDisable = state.get('cloudKey').trim().length === 0 || state.get('loading'); |
||||
|
||||
return shouldDisable && 'disabled'; |
||||
}, |
||||
}); |
||||
|
||||
Template.cloudRegisterManually.onRendered(function() { |
||||
const clipboard = new Clipboard('.js-copy'); |
||||
clipboard.on('success', function() { |
||||
toastr.success(TAPi18n.__('Copied')); |
||||
}); |
||||
|
||||
const btn = this.find('.cloud-console-btn'); |
||||
// After_copy_the_text_go_to_cloud
|
||||
this.cloudLink.set(TAPi18n.__('Cloud_click_here').replace(/(\[(.*)\]\(\))/ig, (_, __, text) => btn.outerHTML.replace('</a>', `${ text }</a>`))); |
||||
}); |
||||
|
||||
Template.cloudRegisterManually.onCreated(function() { |
||||
this.cloudLink = new ReactiveVar(); |
||||
this.state = new ReactiveDict({ |
||||
step: CLOUD_STEPS.COPY, |
||||
loading: false, |
||||
clientKey: '', |
||||
cloudKey: '', |
||||
error: '', |
||||
}); |
||||
|
||||
Meteor.call('cloud:getWorkspaceRegisterData', (error, result) => { |
||||
this.state.set('clientKey', result); |
||||
}); |
||||
}); |
||||
@ -1,2 +0,0 @@ |
||||
import './cloud'; |
||||
import './callback'; |
||||
@ -1,33 +0,0 @@ |
||||
import './admin/callback'; |
||||
import './admin/cloud'; |
||||
import './admin/cloudRegisterManually'; |
||||
|
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute, registerAdminSidebarItem } from '../../../client/admin'; |
||||
import { hasAtLeastOnePermission } from '../../authorization'; |
||||
|
||||
registerAdminRoute('/cloud', { |
||||
name: 'cloud', |
||||
async action() { |
||||
await import('./admin'); |
||||
BlazeLayout.render('main', { center: 'cloud', old: true }); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/cloud/oauth-callback', { |
||||
name: 'cloud-oauth-callback', |
||||
async action() { |
||||
await import('./admin'); |
||||
BlazeLayout.render('main', { center: 'cloudCallback', old: true }); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminSidebarItem({ |
||||
icon: 'cloud-plus', |
||||
href: 'cloud', |
||||
i18nLabel: 'Connectivity_Services', |
||||
permissionGranted() { |
||||
return hasAtLeastOnePermission(['manage-cloud']); |
||||
}, |
||||
}); |
||||
@ -1,34 +0,0 @@ |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { hasAllPermission } from '../../authorization/client'; |
||||
import { APIClient } from '../../utils/client'; |
||||
|
||||
export async function getIntegration(integrationId, uid) { |
||||
if (!integrationId) { |
||||
return; |
||||
} |
||||
|
||||
const reqParams = { |
||||
integrationId, |
||||
}; |
||||
|
||||
if (!hasAllPermission('manage-outgoing-integrations')) { |
||||
if (!hasAllPermission('manage-own-outgoing-integrations')) { |
||||
toastr.error(TAPi18n.__('No_integration_found')); |
||||
FlowRouter.go('admin-integrations'); |
||||
return; |
||||
} |
||||
reqParams.createdBy = uid; |
||||
} |
||||
|
||||
try { |
||||
const { integration } = await APIClient.v1.get('integrations.get', reqParams); |
||||
|
||||
return integration; |
||||
} catch (e) { |
||||
toastr.error(TAPi18n.__('Error')); |
||||
console.error(e); |
||||
} |
||||
} |
||||
@ -1,3 +0,0 @@ |
||||
import '../lib/rocketchat'; |
||||
import './startup'; |
||||
import './route'; |
||||
@ -1,75 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute } from '../../../client/admin'; |
||||
import { t } from '../../utils'; |
||||
|
||||
const dynamic = () => import('./views'); |
||||
|
||||
registerAdminRoute('/integrations', { |
||||
name: 'admin-integrations', |
||||
async action() { |
||||
await dynamic(); |
||||
return BlazeLayout.render('main', { |
||||
center: 'integrations', |
||||
pageTitle: t('Integrations'), |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/integrations/new', { |
||||
name: 'admin-integrations-new', |
||||
async action() { |
||||
await dynamic(); |
||||
return BlazeLayout.render('main', { |
||||
center: 'integrationsNew', |
||||
pageTitle: t('Integration_New'), |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/integrations/incoming/:id?', { |
||||
name: 'admin-integrations-incoming', |
||||
async action(params) { |
||||
await dynamic(); |
||||
return BlazeLayout.render('main', { |
||||
center: 'pageSettingsContainer', |
||||
pageTitle: t('Integration_Incoming_WebHook'), |
||||
pageTemplate: 'integrationsIncoming', |
||||
params, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/integrations/outgoing/:id?', { |
||||
name: 'admin-integrations-outgoing', |
||||
async action(params) { |
||||
await dynamic(); |
||||
return BlazeLayout.render('main', { |
||||
center: 'integrationsOutgoing', |
||||
pageTitle: t('Integration_Outgoing_WebHook'), |
||||
params, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/integrations/outgoing/:id?/history', { |
||||
name: 'admin-integrations-outgoing-history', |
||||
async action(params) { |
||||
await dynamic(); |
||||
return BlazeLayout.render('main', { |
||||
center: 'integrationsOutgoingHistory', |
||||
pageTitle: t('Integration_Outgoing_WebHook_History'), |
||||
params, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/integrations/additional/zapier', { |
||||
name: 'admin-integrations-additional-zapier', |
||||
async action() { |
||||
await dynamic(); |
||||
BlazeLayout.render('main', { |
||||
center: 'integrationsAdditionalZapier', |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,3 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
export const integrationHistoryStreamer = new Meteor.Streamer('integrationHistory'); |
||||
@ -1,75 +0,0 @@ |
||||
.admin-integrations-new-panel { |
||||
& .admin-integrations-new-item { |
||||
display: flex; |
||||
|
||||
padding: 20px 10px; |
||||
|
||||
cursor: pointer; |
||||
|
||||
color: #444444; |
||||
border-bottom: 1px solid #dddddd; |
||||
align-items: center; |
||||
|
||||
&:hover { |
||||
background-color: #fafafa; |
||||
} |
||||
|
||||
& > i { |
||||
color: #aaaaaa; |
||||
|
||||
font-size: 2rem; |
||||
} |
||||
|
||||
& .admin-integrations-new-item-body { |
||||
display: flex; |
||||
flex-direction: column; |
||||
|
||||
padding: 0 20px; |
||||
flex-grow: 1; |
||||
} |
||||
|
||||
& .admin-integrations-new-item-title { |
||||
font-size: 1.4rem; |
||||
font-weight: 500; |
||||
line-height: 2.1rem; |
||||
} |
||||
|
||||
& .admin-integrations-new-item-description { |
||||
color: #aaaaaa; |
||||
|
||||
font-size: 1rem; |
||||
line-height: 1.5rem; |
||||
} |
||||
} |
||||
|
||||
& > a:last-child > .admin-integrations-new-item { |
||||
border-bottom: none; |
||||
} |
||||
} |
||||
|
||||
.message-example { |
||||
& li { |
||||
list-style: none; |
||||
} |
||||
} |
||||
|
||||
.integrate-other-ways { |
||||
& p { |
||||
font-size: 1rem; |
||||
line-height: 1.5rem; |
||||
|
||||
& a { |
||||
color: #175cc4 !important; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.content.zapier { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
|
||||
#zapier-goes-here { |
||||
width: 95%; |
||||
} |
||||
} |
||||
@ -1,10 +0,0 @@ |
||||
<template name="integrationsAdditionalZapier"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{> header sectionName="Zapier" hideHelp=true fixedHeight=true}} |
||||
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br> |
||||
<div class="content zapier"> |
||||
<div id="zapier-goes-here"></div> |
||||
</div> |
||||
</section> |
||||
<script src="https://zapier.com/apps/embed/widget.js?services=rocketchat&html_id=zapier-goes-here"></script> |
||||
</template> |
||||
@ -1,11 +0,0 @@ |
||||
import './integrations.html'; |
||||
import './integrations'; |
||||
import './integrationsNew.html'; |
||||
import './integrationsNew'; |
||||
import './integrationsIncoming.html'; |
||||
import './integrationsIncoming'; |
||||
import './integrationsOutgoing.html'; |
||||
import './integrationsOutgoing'; |
||||
import './integrationsOutgoingHistory.html'; |
||||
import './integrationsOutgoingHistory'; |
||||
import './additional/zapier.html'; |
||||
@ -1,74 +0,0 @@ |
||||
<template name="integrations"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{#header sectionName=pageTitle buttons=true}} |
||||
{{#if hasPermission}} |
||||
<div class="rc-header__section-button"> |
||||
<a href="{{pathFor "admin-integrations-additional-zapier"}}" class="rc-button rc-button--primary zapier-btn">Zapier</a> |
||||
<a href="{{pathFor "admin-integrations-new"}}" class="rc-button rc-button--primary">{{_ "New_integration"}}</a> |
||||
</div> |
||||
{{/if}} |
||||
{{/header}} |
||||
|
||||
<div class="content"> |
||||
{{#unless hasPermission}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<div class="rocket-form"> |
||||
<div class="section integrate-other-ways"> |
||||
<h1>Zapier</h1> |
||||
<p>{{{_ "additional_integrations_Zapier"}}}</p> |
||||
<br> |
||||
<h1>{{_ "Bots"}}</h1> |
||||
<p>{{{_ "additional_integrations_Bots"}}}</p> |
||||
</div> |
||||
|
||||
<div class="section"> |
||||
<div class="admin-integrations-new-panel"> |
||||
{{#each integrations}} |
||||
{{#if $eq type 'webhook-incoming'}} |
||||
<a href="{{pathFor "admin-integrations-incoming" id=_id}}"> |
||||
<div class="admin-integrations-new-item"> |
||||
<i class="icon-login"></i> |
||||
<div class="admin-integrations-new-item-body"> |
||||
<div class="admin-integrations-new-item-title"> |
||||
{{_ "Incoming_WebHook"}} {{#if name}}- {{name}}{{/if}} |
||||
</div> |
||||
<div class="admin-integrations-new-item-description"> |
||||
{{{_ "Post_to_s_as_s" channel username}}} |
||||
</div> |
||||
<div class="admin-integrations-new-item-description"> |
||||
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}} |
||||
</div> |
||||
</div> |
||||
<i class="icon-angle-right"></i> |
||||
</div> |
||||
</a> |
||||
{{/if}} |
||||
{{else}} |
||||
<h1>{{_ "There_are_no_integrations"}}</h1> |
||||
{{/each}} |
||||
{{#each integrations}} |
||||
{{#if $eq type 'webhook-outgoing'}} |
||||
<a href="{{pathFor "admin-integrations-outgoing" id=_id}}"> |
||||
<div class="admin-integrations-new-item"> |
||||
<i class="icon-login"></i> |
||||
<div class="admin-integrations-new-item-body"> |
||||
<div class="admin-integrations-new-item-title"> |
||||
{{_ "Outgoing_WebHook"}} {{#if name}}- {{name}}{{/if}} |
||||
</div> |
||||
<div class="admin-integrations-new-item-description"> |
||||
{{{_ "Created_at_s_by_s_triggered_by_s" (dateFormated _createdAt) _createdBy.username (eventTypeI18n event)}}} |
||||
</div> |
||||
</div> |
||||
<i class="icon-angle-right"></i> |
||||
</div> |
||||
</a> |
||||
{{/if}} |
||||
{{/each}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
</template> |
||||
@ -1,65 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import moment from 'moment'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { integrations } from '../../lib/rocketchat'; |
||||
import { SideNav } from '../../../ui-utils/client'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
|
||||
const ITEMS_COUNT = 50; |
||||
|
||||
Template.integrations.helpers({ |
||||
hasPermission() { |
||||
return hasAtLeastOnePermission([ |
||||
'manage-outgoing-integrations', |
||||
'manage-own-outgoing-integrations', |
||||
'manage-incoming-integrations', |
||||
'manage-own-incoming-integrations', |
||||
]); |
||||
}, |
||||
integrations() { |
||||
return Template.instance().integrations.get(); |
||||
}, |
||||
dateFormated(date) { |
||||
return moment(date).format('L LT'); |
||||
}, |
||||
eventTypeI18n(event) { |
||||
return TAPi18n.__(integrations.outgoingEvents[event].label); |
||||
}, |
||||
}); |
||||
|
||||
Template.integrations.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
|
||||
Template.integrations.onCreated(async function() { |
||||
this.integrations = new ReactiveVar([]); |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
|
||||
this.autorun(async () => { |
||||
const offset = this.offset.get(); |
||||
const { integrations, total } = await APIClient.v1.get(`integrations.list?sort={"type":1}&count=${ ITEMS_COUNT }&offset=${ offset }`); |
||||
this.total.set(total); |
||||
this.integrations.set(this.integrations.get().concat(integrations)); |
||||
}); |
||||
}); |
||||
|
||||
Template.integrations.events({ |
||||
'scroll .content': _.throttle(function(e, instance) { |
||||
if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight)) { |
||||
const integrations = instance.integrations.get(); |
||||
if (instance.total.get() <= integrations.length) { |
||||
return; |
||||
} |
||||
return instance.offset.set(instance.offset.get() + ITEMS_COUNT); |
||||
} |
||||
}, 200), |
||||
}); |
||||
@ -1,138 +0,0 @@ |
||||
<template name="integrationsIncoming"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{#header sectionName=pageTitle buttons=true}} |
||||
<div class="rc-header__section-button"> |
||||
<button class="rc-button rc-button--cancel delete" disabled="{{$not canDelete}}"><span>{{_ "Delete"}}</span></button> |
||||
<button class="rc-button rc-button--primary save" disabled="{{enabled}}"><span>{{_ "Save_changes"}}</span></button> |
||||
</div> |
||||
{{/header}} |
||||
<div class="content"> |
||||
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br> |
||||
<div class="permissions-manager"> |
||||
{{#if hasPermission}} |
||||
<div class="rocket-form"> |
||||
<div class="section"> |
||||
<div class="section-content"> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Enabled"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="enabled" value="1" checked="{{$eq data.enabled true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="enabled" value="0" checked="{{$neq data.enabled true}}" /> {{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Name"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="name" value="{{data.name}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "You_should_name_it_to_easily_manage_your_integrations"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Post_to_Channel"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="channel" value="{{data.channel}}" placeholder="{{_ 'User_or_channel_name'}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here"}}</div> |
||||
<div class="settings-description secondary-font-color">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Post_as"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="username" value="{{data.username}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div> |
||||
<div class="settings-description secondary-font-color">{{_ "Should_exists_a_user_with_this_username"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Alias"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="alias" value="{{data.alias}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "Choose_the_alias_that_will_appear_before_the_username_in_messages"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Avatar_URL"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="url" class="rc-input__element" name="avatar" value="{{data.avatar}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "You_can_change_a_different_avatar_too"}}</div> |
||||
<div class="settings-description secondary-font-color">{{_ "Should_be_a_URL_of_an_image"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Emoji"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "You_can_use_an_emoji_as_avatar"}}</div> |
||||
<div class="settings-description secondary-font-color">{{{_ "Example_s" ":ghost:"}}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Script_Enabled"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="scriptEnabled" value="1" checked="{{$eq data.scriptEnabled true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="scriptEnabled" value="0" checked="{{$neq data.scriptEnabled true}}" /> {{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Script"}}</label> |
||||
<div> |
||||
<div class="code-mirror-box"> |
||||
<div class="title"> |
||||
{{_ "Script"}} |
||||
</div> |
||||
{{> CodeMirror name="script" options=editorOptions code=data.script }} |
||||
<div class="buttons"> |
||||
<button class="rc-button rc-button--primary button-fullscreen">{{_ "Full_Screen"}}</button> |
||||
<button class="rc-button rc-button--primary button-restore">{{_ "Exit_Full_Screen"}}</button> |
||||
</div> |
||||
</div> |
||||
{{#if data.hasScriptError }} |
||||
<div class="code-error-box"> |
||||
<div class="title color-content-background-color background-error-color"> |
||||
{{data.scriptError.name}} |
||||
</div> |
||||
<pre class="script-error background-transparent-lightest error-color error-border">{{data.scriptError.message}}</pre> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
{{#if data.token}} |
||||
<div class="input-line double-col"> |
||||
<label>Webhook URL</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="webhookurl" value="{{data.url}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color">{{_ "Send_your_JSON_payloads_to_this_URL"}}</div> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=webhookurl]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>Token</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="completeToken" value="{{data.completeToken}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=completeToken]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Example"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json json-example">{{{exampleJson}}}</code></pre> |
||||
{{#if curl}} |
||||
<input type="text" class="rc-input__element" name="curl" value="{{curl}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=curl]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
<div class="input-line message-example"> |
||||
{{> message msg=exampleMsg settings=exampleSettings u=exampleUser}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
{{_ "Not_authorized"}} |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
</section> |
||||
</template> |
||||
@ -1,251 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import hljs from 'highlight.js'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { exampleMsg, exampleSettings, exampleUser } from './messageExample'; |
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { modal, SideNav } from '../../../ui-utils/client'; |
||||
import { t, handleError } from '../../../utils'; |
||||
import { getIntegration } from '../getIntegration'; |
||||
|
||||
Template.integrationsIncoming.onCreated(async function _incomingIntegrationsOnCreated() { |
||||
const params = Template.instance().data.params ? Template.instance().data.params() : undefined; |
||||
this.integration = new ReactiveVar({}); |
||||
this.record = new ReactiveVar({ |
||||
username: 'rocket.cat', |
||||
}); |
||||
if (params && params.id) { |
||||
const integration = await getIntegration(params.id, Meteor.userId()); |
||||
if (integration) { |
||||
this.integration.set(integration); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
Template.integrationsIncoming.helpers({ |
||||
exampleMsg, |
||||
exampleUser, |
||||
exampleSettings, |
||||
hasPermission() { |
||||
return hasAtLeastOnePermission([ |
||||
'manage-incoming-integrations', |
||||
'manage-own-incoming-integrations', |
||||
]); |
||||
}, |
||||
|
||||
canDelete() { |
||||
return this.params && this.params() && typeof this.params().id !== 'undefined'; |
||||
}, |
||||
|
||||
data() { |
||||
const data = Template.instance().integration.get(); |
||||
if (data) { |
||||
const completeToken = `${ data._id }/${ data.token }`; |
||||
data.url = Meteor.absoluteUrl(`hooks/${ completeToken }`); |
||||
data.completeToken = completeToken; |
||||
data.hasScriptError = data.scriptEnabled && data.scriptError; |
||||
Template.instance().record.set(data); |
||||
return data; |
||||
} |
||||
|
||||
return Template.instance().record.curValue; |
||||
}, |
||||
exampleJson() { |
||||
const record = Template.instance().record.get(); |
||||
const data = { |
||||
username: record.alias, |
||||
icon_emoji: record.emoji, |
||||
icon_url: record.avatar, |
||||
text: 'Example message', |
||||
attachments: [{ |
||||
title: 'Rocket.Chat', |
||||
title_link: 'https://rocket.chat', |
||||
text: 'Rocket.Chat, the best open source chat', |
||||
image_url: '/images/integration-attachment-example.png', |
||||
color: '#764FA5', |
||||
}], |
||||
}; |
||||
|
||||
const invalidData = [null, '']; |
||||
Object.keys(data).forEach((key) => { |
||||
if (invalidData.includes(data[key])) { |
||||
delete data[key]; |
||||
} |
||||
}); |
||||
|
||||
return hljs.highlight('json', JSON.stringify(data, null, 2)).value; |
||||
}, |
||||
|
||||
curl() { |
||||
const record = Template.instance().record.get(); |
||||
|
||||
if (!record.url) { |
||||
return; |
||||
} |
||||
|
||||
const data = { |
||||
username: record.alias, |
||||
icon_emoji: record.emoji, |
||||
icon_url: record.avatar, |
||||
text: 'Example message', |
||||
attachments: [{ |
||||
title: 'Rocket.Chat', |
||||
title_link: 'https://rocket.chat', |
||||
text: 'Rocket.Chat, the best open source chat', |
||||
image_url: '/images/integration-attachment-example.png', |
||||
color: '#764FA5', |
||||
}], |
||||
}; |
||||
|
||||
const invalidData = [null, '']; |
||||
Object.keys(data).forEach((key) => { |
||||
if (invalidData.includes(data[key])) { |
||||
delete data[key]; |
||||
} |
||||
}); |
||||
|
||||
return `curl -X POST -H 'Content-Type: application/json' --data '${ JSON.stringify(data) }' ${ record.url }`; |
||||
}, |
||||
|
||||
editorOptions() { |
||||
return { |
||||
lineNumbers: true, |
||||
mode: 'javascript', |
||||
gutters: [ |
||||
// 'CodeMirror-lint-markers'
|
||||
'CodeMirror-linenumbers', |
||||
'CodeMirror-foldgutter', |
||||
], |
||||
// lint: true,
|
||||
foldGutter: true, |
||||
// lineWrapping: true,
|
||||
matchBrackets: true, |
||||
autoCloseBrackets: true, |
||||
matchTags: true, |
||||
showTrailingSpace: true, |
||||
highlightSelectionMatches: true, |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.integrationsIncoming.events({ |
||||
'blur input': (e, t) => { |
||||
const value = t.record.curValue || {}; |
||||
|
||||
value.name = $('[name=name]').val().trim(); |
||||
value.alias = $('[name=alias]').val().trim(); |
||||
value.emoji = $('[name=emoji]').val().trim(); |
||||
value.avatar = $('[name=avatar]').val().trim(); |
||||
value.channel = $('[name=channel]').val().trim(); |
||||
value.username = $('[name=username]').val().trim(); |
||||
|
||||
t.record.set(value); |
||||
}, |
||||
|
||||
'click .rc-header__section-button > .delete': () => { |
||||
const params = Template.instance().data.params(); |
||||
|
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('You_will_not_be_able_to_recover'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_delete_it'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, () => { |
||||
Meteor.call('deleteIncomingIntegration', params.id, (err) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Your_entry_has_been_deleted'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
|
||||
FlowRouter.go('admin-integrations'); |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'click .button-fullscreen': () => { |
||||
const codeMirrorBox = $('.code-mirror-box'); |
||||
codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color'); |
||||
codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); |
||||
}, |
||||
|
||||
'click .button-restore': () => { |
||||
const codeMirrorBox = $('.code-mirror-box'); |
||||
codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color'); |
||||
codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); |
||||
}, |
||||
|
||||
'click .rc-header__section-button > .save': () => { |
||||
const enabled = $('[name=enabled]:checked').val().trim(); |
||||
const name = $('[name=name]').val().trim(); |
||||
const alias = $('[name=alias]').val().trim(); |
||||
const emoji = $('[name=emoji]').val().trim(); |
||||
const avatar = $('[name=avatar]').val().trim(); |
||||
const channel = $('[name=channel]').val().trim(); |
||||
const username = $('[name=username]').val().trim(); |
||||
const scriptEnabled = $('[name=scriptEnabled]:checked').val().trim(); |
||||
const script = $('[name=script]').val().trim(); |
||||
|
||||
if (channel === '') { |
||||
return toastr.error(TAPi18n.__('The_channel_name_is_required')); |
||||
} |
||||
|
||||
if (username === '') { |
||||
return toastr.error(TAPi18n.__('The_username_is_required')); |
||||
} |
||||
|
||||
const integration = { |
||||
enabled: enabled === '1', |
||||
channel, |
||||
username, |
||||
alias: alias !== '' ? alias : undefined, |
||||
emoji: emoji !== '' ? emoji : undefined, |
||||
avatar: avatar !== '' ? avatar : undefined, |
||||
name: name !== '' ? name : undefined, |
||||
script: script !== '' ? script : undefined, |
||||
scriptEnabled: scriptEnabled === '1', |
||||
}; |
||||
|
||||
const params = Template.instance().data.params ? Template.instance().data.params() : undefined; |
||||
if (params && params.id) { |
||||
Meteor.call('updateIncomingIntegration', params.id, integration, (err) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
|
||||
toastr.success(TAPi18n.__('Integration_updated')); |
||||
}); |
||||
} else { |
||||
Meteor.call('addIncomingIntegration', integration, (err, data) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
|
||||
toastr.success(TAPi18n.__('Integration_added')); |
||||
FlowRouter.go('admin-integrations-incoming', { id: data._id }); |
||||
}); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.integrationsIncoming.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,51 +0,0 @@ |
||||
<template name="integrationsNew"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{> header sectionName=pageTitle}} |
||||
<div class="content"> |
||||
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br> |
||||
{{#if hasPermission}} |
||||
|
||||
<div class="rocket-form"> |
||||
<div class="section"> |
||||
<div class="admin-integrations-new-panel"> |
||||
{{#if canAddIncomingIntegration}} |
||||
<a href="{{pathFor "admin-integrations-incoming"}}"> |
||||
<div class="admin-integrations-new-item"> |
||||
<i class="icon-login"></i> |
||||
<div class="admin-integrations-new-item-body"> |
||||
<div class="admin-integrations-new-item-title"> |
||||
{{_ "Incoming_WebHook"}} |
||||
</div> |
||||
<div class="admin-integrations-new-item-description"> |
||||
{{_ "Send_data_into_RocketChat_in_realtime"}} |
||||
</div> |
||||
</div> |
||||
<i class="icon-angle-right"></i> |
||||
</div> |
||||
</a> |
||||
{{/if}} |
||||
{{#if canAddOutgoingIntegration}} |
||||
<a href="{{pathFor "admin-integrations-outgoing"}}"> |
||||
<div class="admin-integrations-new-item"> |
||||
<i class="icon-logout"></i> |
||||
<div class="admin-integrations-new-item-body"> |
||||
<div class="admin-integrations-new-item-title"> |
||||
{{_ "Outgoing_WebHook"}} |
||||
</div> |
||||
<div class="admin-integrations-new-item-description"> |
||||
{{_ "Outgoing_WebHook_Description"}} |
||||
</div> |
||||
</div> |
||||
<i class="icon-angle-right"></i> |
||||
</div> |
||||
</a> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
{{_ "Not_authorized"}} |
||||
{{/if}} |
||||
</div> |
||||
</section> |
||||
</template> |
||||
@ -1,35 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
|
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { SideNav } from '../../../ui-utils/client'; |
||||
|
||||
Template.integrationsNew.helpers({ |
||||
hasPermission() { |
||||
return hasAtLeastOnePermission([ |
||||
'manage-outgoing-integrations', |
||||
'manage-own-outgoing-integrations', |
||||
'manage-incoming-integrations', |
||||
'manage-own-incoming-integrations', |
||||
]); |
||||
}, |
||||
canAddIncomingIntegration() { |
||||
return hasAtLeastOnePermission([ |
||||
'manage-incoming-integrations', |
||||
'manage-own-incoming-integrations', |
||||
]); |
||||
}, |
||||
canAddOutgoingIntegration() { |
||||
return hasAtLeastOnePermission([ |
||||
'manage-outgoing-integrations', |
||||
'manage-own-outgoing-integrations', |
||||
]); |
||||
}, |
||||
}); |
||||
|
||||
Template.integrationsNew.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,241 +0,0 @@ |
||||
<template name="integrationsOutgoing"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{#header sectionName=pageTitle buttons=true}} |
||||
<div class="rc-header__section-button"> |
||||
<button class="rc-button rc-button--secondary history" disabled="{{$not showHistoryButton}}">{{_ "History"}}</button> |
||||
<button class="rc-button rc-button--cancel delete" disabled="{{$not canDelete}}"><span>{{_ "Delete"}}</span></button> |
||||
<button class="rc-button rc-button--primary save" disabled="{{$not hasTypeSelected}}"><span>{{_ "Save_changes"}}</span></button> |
||||
</div> |
||||
{{/header}} |
||||
|
||||
<div class="content"> |
||||
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br> |
||||
{{#unless hasPermission}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<div class="rocket-form"> |
||||
<div class="section"> |
||||
<div class="section-title"> |
||||
<div class="section-title-text">{{_ "Webhook Details"}}</div> |
||||
<div class="section-title-right"> |
||||
<button class="rc-button rc-button--primary collapse"><span>{{_ "Collapse"}}</span></button> |
||||
</div> |
||||
</div> |
||||
<div class="section-content"> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Event_Trigger"}}</label> |
||||
<div> |
||||
<select name="event" class="required rc-input__element"> |
||||
<option value=""></option> |
||||
{{#each eventTypes}} |
||||
<option value="{{value}}" selected="{{$eq data.event value}}" dir="auto">{{_ label}}</option> |
||||
{{/each}} |
||||
</select> |
||||
<div class="settings-description">{{_ "Event_Trigger_Description"}}</div> |
||||
</div> |
||||
</div> |
||||
{{#if hasTypeSelected}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Enabled"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="enabled" value="1" checked="{{$eq data.enabled true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="enabled" value="0" checked="{{$neq data.enabled true}}" /> {{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Name"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="name" value="{{data.name}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description">{{_ "You_should_name_it_to_easily_manage_your_integrations"}}</div> |
||||
</div> |
||||
</div> |
||||
{{#if shouldDisplayChannel}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Channel"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="channel" value="{{data.channel}}" placeholder="{{_ 'User_or_channel_name'}}" /> |
||||
<div class="settings-description">{{_ "Channel_to_listen_on"}}</div> |
||||
<div class="settings-description">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div> |
||||
<div class="settings-description">{{{_ "Integrations_for_all_channels"}}}</div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if shouldDisplayTriggerWords}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Trigger_Words"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="triggerWords" value="{{join data.triggerWords ','}}" /> |
||||
<div class="settings-description">{{_ "When a line starts with one of these words, post to the URL(s) below"}}</div> |
||||
<div class="settings-description">{{_ "Separate multiple words with commas"}}</div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if shouldDisplayTargetRoom}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "TargetRoom"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="targetRoom" value="{{data.targetRoom}}" placeholder="{{_ 'User_or_channel_name'}}" /> |
||||
<div class="settings-description">{{_ "TargetRoom_Description"}}</div> |
||||
<div class="settings-description">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "URLs"}}</label> |
||||
<div> |
||||
<textarea name="urls" class="rc-input__element" style="height: 100px;">{{join data.urls "\n"}}</textarea> |
||||
<div class="settings-description">{{_ "Enter as many URLs as you like, one per line, please"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Impersonate_user"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="impersonateUser" value="1" checked="{{$eq data.impersonateUser true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="impersonateUser" value="0" checked="{{$neq data.impersonateUser true}}" /> {{_ "False"}}</label> |
||||
<div class="settings-description">{{_ "Impersonate_user_description"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Post_as"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="username" value="{{data.username}}" /> |
||||
<div class="settings-description">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div> |
||||
<div class="settings-description">{{_ "Should_exists_a_user_with_this_username"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Alias"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="alias" value="{{data.alias}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description">{{_ "Choose_the_alias_that_will_appear_before_the_username_in_messages"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Avatar_URL"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="url" class="rc-input__element" name="avatar" value="{{data.avatar}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description">{{_ "You_can_change_a_different_avatar_too"}}</div> |
||||
<div class="settings-description">{{_ "Should_be_a_URL_of_an_image"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Emoji"}} ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" /> |
||||
<div class="settings-description">{{_ "You_can_use_an_emoji_as_avatar"}}</div> |
||||
<div class="settings-description">{{{_ "Example_s" ":ghost:"}}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>Token ({{_ "optional"}})</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="token" value="{{data.token}}" /> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Script_Enabled"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="scriptEnabled" value="1" checked="{{$eq data.scriptEnabled true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="scriptEnabled" value="0" checked="{{$neq data.scriptEnabled true}}" /> {{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Script"}}</label> |
||||
<div> |
||||
<div class="code-mirror-box"> |
||||
<div class="title"> |
||||
{{_ "Script"}} |
||||
</div> |
||||
{{> CodeMirror name="script" options=editorOptions code=data.script }} |
||||
<div class="buttons"> |
||||
<button class="rc-button rc-button--primary button-fullscreen">{{_ "Full_Screen"}}</button> |
||||
<button class="rc-button rc-button--primary button-restore">{{_ "Exit_Full_Screen"}}</button> |
||||
</div> |
||||
</div> |
||||
{{#if data.hasScriptError }} |
||||
<div class="code-error-box"> |
||||
<div class="title color-content-background-color background-error-color"> |
||||
{{data.scriptError.name}} |
||||
</div> |
||||
<pre class="script-error background-transparent-lightest error-color error-border">{{data.scriptError.message}}</pre> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Responding"}}</label> |
||||
<div> |
||||
<div class="settings-description">{{{_ "If the handler wishes to post a response back into the channel, the following JSON should be returned as the body of the response:"}}}</div> |
||||
<pre><code class="code-colors hljs json json-example">{{{exampleJson}}}</code></pre> |
||||
<div class="settings-description">{{{_ "Empty bodies or bodies with an empty text property will simply be ignored. Non-200 responses will be retried a reasonable number of times. A response will be posted using the alias and avatar specified above. You can override these informations as in the example above."}}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line message-example"> |
||||
{{> message msg=exampleMsg settings=exampleSettings u=exampleUser}} |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
{{#if hasTypeSelected}} |
||||
<div class="section section-collapsed"> |
||||
<div class="section-title"> |
||||
<div class="section-title-text">{{_ "Integration_Advanced_Settings"}}</div> |
||||
<div class="section-title-right"> |
||||
<button class="rc-button rc-button--primary expand"><span>{{_ "Expand"}}</span></button> |
||||
</div> |
||||
</div> |
||||
<div class="section-content"> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Retry_Failed_Url_Calls"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="retryFailedCalls" value="1" checked="{{$eq data.retryFailedCalls true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="retryFailedCalls" value="0" checked="{{$neq data.retryFailedCalls true}}" /> {{_ "False"}}</label> |
||||
<div class="settings-description">{{_ "Integration_Retry_Failed_Url_Calls_Description"}}</div> |
||||
</div> |
||||
</div> |
||||
{{#if data.retryFailedCalls}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Retry_Count"}}</label> |
||||
<div> |
||||
<input type="number" class="rc-input__element" name="retryCount" value="{{data.retryCount}}" /> |
||||
<div class="settings-description">{{_ "Integration_Retry_Count_Description"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Retry_Delay"}}</label> |
||||
<div> |
||||
<select name="retryDelay" class="required"> |
||||
<option value="powers-of-ten" selected="{{$eq data.retryDelay 'powers-of-ten'}}" dir="auto">powers-of-ten</option> |
||||
<option value="powers-of-two" selected="{{$eq data.retryDelay 'powers-of-two'}}" dir="auto">powers-of-two</option> |
||||
<option value="increments-of-two" selected="{{$eq data.retryDelay 'increments-of-two'}}" dir="auto">increments-of-two</option> |
||||
</select> |
||||
<div class="settings-description">{{{_ "Integration_Retry_Delay_Description"}}}</div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if shouldDisplayTriggerWords}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Word_Trigger_Placement"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="triggerWordAnywhere" value="1" checked="{{$eq data.triggerWordAnywhere true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="triggerWordAnywhere" value="0" checked="{{$neq data.triggerWordAnywhere true}}" /> {{_ "False"}}</label> |
||||
<div class="settings-description">{{_ "Integration_Word_Trigger_Placement_Description"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Run_When_Message_Is_Edited"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="runOnEdits" value="1" checked="{{$eq data.runOnEdits true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="runOnEdits" value="0" checked="{{$neq data.runOnEdits true}}" /> {{_ "False"}}</label> |
||||
<div class="settings-description">{{{_ "Integration_Run_When_Message_Is_Edited_Description"}}}</div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
</template> |
||||
@ -1,353 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Random } from 'meteor/random'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import hljs from 'highlight.js'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { exampleMsg, exampleSettings, exampleUser } from './messageExample'; |
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { modal, SideNav } from '../../../ui-utils'; |
||||
import { t, handleError } from '../../../utils/client'; |
||||
import { integrations } from '../../lib/rocketchat'; |
||||
import { getIntegration } from '../getIntegration'; |
||||
|
||||
Template.integrationsOutgoing.onCreated(async function _integrationsOutgoingOnCreated() { |
||||
const params = Template.instance().data.params ? Template.instance().data.params() : undefined; |
||||
this.record = new ReactiveVar({ |
||||
username: 'rocket.cat', |
||||
token: Random.id(24), |
||||
retryFailedCalls: true, |
||||
retryCount: 6, |
||||
retryDelay: 'powers-of-ten', |
||||
runOnEdits: true, |
||||
}); |
||||
|
||||
this.updateRecord = () => { |
||||
this.record.set({ |
||||
enabled: $('[name=enabled]:checked').val().trim() === '1', |
||||
event: $('[name=event]').val().trim(), |
||||
name: $('[name=name]').val().trim(), |
||||
alias: $('[name=alias]').val().trim(), |
||||
emoji: $('[name=emoji]').val().trim(), |
||||
avatar: $('[name=avatar]').val().trim(), |
||||
channel: $('[name=channel]').val() ? $('[name=channel]').val().trim() : undefined, |
||||
username: $('[name=username]').val().trim(), |
||||
triggerWords: $('[name=triggerWords]').val() ? $('[name=triggerWords]').val().trim() : undefined, |
||||
urls: $('[name=urls]').val().trim(), |
||||
token: $('[name=token]').val().trim(), |
||||
scriptEnabled: $('[name=scriptEnabled]:checked').val().trim() === '1', |
||||
script: $('[name=script]').val().trim(), |
||||
targetRoom: $('[name=targetRoom]').val() ? $('[name=targetRoom]').val().trim() : undefined, |
||||
triggerWordAnywhere: $('[name=triggerWordAnywhere]:checked').val().trim() === '1', |
||||
retryFailedCalls: $('[name=retryFailedCalls]:checked').val().trim() === '1', |
||||
retryCount: $('[name=retryCount]').val() ? $('[name=retryCount]').val().trim() : 6, |
||||
retryDelay: $('[name=retryDelay]').val() ? $('[name=retryDelay]').val().trim() : 'powers-of-ten', |
||||
runOnEdits: $('[name=runOnEdits]:checked').val().trim() === '1', |
||||
}); |
||||
}; |
||||
|
||||
const integration = await getIntegration(params.id, Meteor.userId()); |
||||
if (params.id && !integration) { |
||||
toastr.error(TAPi18n.__('No_integration_found')); |
||||
FlowRouter.go('admin-integrations'); |
||||
return; |
||||
} |
||||
|
||||
integration.hasScriptError = integration.scriptEnabled && integration.scriptError; |
||||
this.record.set(integration); |
||||
}); |
||||
|
||||
Template.integrationsOutgoing.helpers({ |
||||
exampleMsg, |
||||
exampleUser, |
||||
exampleSettings, |
||||
join(arr, sep) { |
||||
if (!arr || !arr.join) { |
||||
return arr; |
||||
} |
||||
|
||||
return arr.join(sep); |
||||
}, |
||||
|
||||
showHistoryButton() { |
||||
return this.params && this.params() && typeof this.params().id !== 'undefined'; |
||||
}, |
||||
|
||||
hasPermission() { |
||||
return hasAtLeastOnePermission([ |
||||
'manage-outgoing-integrations', |
||||
'manage-own-outgoing-integrations', |
||||
]); |
||||
}, |
||||
|
||||
data() { |
||||
return Template.instance().record.get(); |
||||
}, |
||||
|
||||
canDelete() { |
||||
return this.params && this.params() && typeof this.params().id !== 'undefined'; |
||||
}, |
||||
|
||||
eventTypes() { |
||||
return Object.values(integrations.outgoingEvents); |
||||
}, |
||||
|
||||
hasTypeSelected() { |
||||
const record = Template.instance().record.get(); |
||||
|
||||
return typeof record.event === 'string' && record.event !== ''; |
||||
}, |
||||
|
||||
shouldDisplayChannel() { |
||||
const record = Template.instance().record.get(); |
||||
|
||||
return typeof record.event === 'string' && integrations.outgoingEvents[record.event].use.channel; |
||||
}, |
||||
|
||||
shouldDisplayTriggerWords() { |
||||
const record = Template.instance().record.get(); |
||||
|
||||
return typeof record.event === 'string' && integrations.outgoingEvents[record.event].use.triggerWords; |
||||
}, |
||||
|
||||
shouldDisplayTargetRoom() { |
||||
const record = Template.instance().record.get(); |
||||
|
||||
return typeof record.event === 'string' && integrations.outgoingEvents[record.event].use.targetRoom; |
||||
}, |
||||
|
||||
exampleJson() { |
||||
const record = Template.instance().record.get(); |
||||
const data = { |
||||
username: record.alias, |
||||
icon_emoji: record.emoji, |
||||
icon_url: record.avatar, |
||||
text: 'Response text', |
||||
attachments: [{ |
||||
title: 'Rocket.Chat', |
||||
title_link: 'https://rocket.chat', |
||||
text: 'Rocket.Chat, the best open source chat', |
||||
image_url: '/images/integration-attachment-example.png', |
||||
color: '#764FA5', |
||||
}], |
||||
}; |
||||
|
||||
const invalidData = [null, '']; |
||||
Object.keys(data).forEach((key) => { |
||||
if (invalidData.includes(data[key])) { |
||||
delete data[key]; |
||||
} |
||||
}); |
||||
|
||||
return hljs.highlight('json', JSON.stringify(data, null, 2)).value; |
||||
}, |
||||
|
||||
editorOptions() { |
||||
return { |
||||
lineNumbers: true, |
||||
mode: 'javascript', |
||||
gutters: [ |
||||
// "CodeMirror-lint-markers",
|
||||
'CodeMirror-linenumbers', |
||||
'CodeMirror-foldgutter', |
||||
], |
||||
// lint: true,
|
||||
foldGutter: true, |
||||
// lineWrapping: true,
|
||||
matchBrackets: true, |
||||
autoCloseBrackets: true, |
||||
matchTags: true, |
||||
showTrailingSpace: true, |
||||
highlightSelectionMatches: true, |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.integrationsOutgoing.events({ |
||||
'blur input': (e, t) => { |
||||
t.updateRecord(); |
||||
}, |
||||
|
||||
'click input[type=radio]': (e, t) => { |
||||
t.updateRecord(); |
||||
}, |
||||
|
||||
'change select[name=event]': (e, t) => { |
||||
const record = t.record.get(); |
||||
record.event = $('[name=event]').val().trim(); |
||||
|
||||
t.record.set(record); |
||||
}, |
||||
|
||||
'click .rc-button.history': () => { |
||||
FlowRouter.go(`/admin/integrations/outgoing/${ FlowRouter.getParam('id') }/history`); |
||||
}, |
||||
|
||||
'click .expand': (e) => { |
||||
$(e.currentTarget).closest('.section').removeClass('section-collapsed'); |
||||
$(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse')); |
||||
$('.CodeMirror').each((index, codeMirror) => codeMirror.CodeMirror.refresh()); |
||||
}, |
||||
|
||||
'click .collapse': (e) => { |
||||
$(e.currentTarget).closest('.section').addClass('section-collapsed'); |
||||
$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); |
||||
}, |
||||
|
||||
'click .rc-header__section-button > .delete': () => { |
||||
const params = Template.instance().data.params(); |
||||
|
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('You_will_not_be_able_to_recover'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_delete_it'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, () => { |
||||
Meteor.call('deleteOutgoingIntegration', params.id, (err) => { |
||||
if (err) { |
||||
handleError(err); |
||||
} else { |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Your_entry_has_been_deleted'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
|
||||
FlowRouter.go('admin-integrations'); |
||||
} |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'click .button-fullscreen': () => { |
||||
$('.code-mirror-box').addClass('code-mirror-box-fullscreen content-background-color'); |
||||
$('.CodeMirror')[0].CodeMirror.refresh(); |
||||
}, |
||||
|
||||
'click .button-restore': () => { |
||||
$('.code-mirror-box').removeClass('code-mirror-box-fullscreen content-background-color'); |
||||
$('.CodeMirror')[0].CodeMirror.refresh(); |
||||
}, |
||||
|
||||
'click .rc-header__section-button > .save': () => { |
||||
const event = $('[name=event]').val().trim(); |
||||
const enabled = $('[name=enabled]:checked').val().trim(); |
||||
const name = $('[name=name]').val().trim(); |
||||
const impersonateUser = $('[name=impersonateUser]:checked').val().trim(); |
||||
const alias = $('[name=alias]').val().trim(); |
||||
const emoji = $('[name=emoji]').val().trim(); |
||||
const avatar = $('[name=avatar]').val().trim(); |
||||
const username = $('[name=username]').val().trim(); |
||||
const token = $('[name=token]').val().trim(); |
||||
const scriptEnabled = $('[name=scriptEnabled]:checked').val().trim(); |
||||
const script = $('[name=script]').val().trim(); |
||||
const retryFailedCalls = $('[name=retryFailedCalls]:checked').val().trim(); |
||||
let urls = $('[name=urls]').val().trim(); |
||||
|
||||
if (username === '' && impersonateUser === '0') { |
||||
return toastr.error(TAPi18n.__('The_username_is_required')); |
||||
} |
||||
|
||||
urls = urls.split('\n').filter((url) => url.trim() !== ''); |
||||
if (urls.length === 0) { |
||||
return toastr.error(TAPi18n.__('You_should_inform_one_url_at_least')); |
||||
} |
||||
|
||||
let triggerWords; |
||||
let triggerWordAnywhere; |
||||
let runOnEdits; |
||||
if (integrations.outgoingEvents[event].use.triggerWords) { |
||||
triggerWords = $('[name=triggerWords]').val().trim(); |
||||
triggerWords = triggerWords.split(',').filter((word) => word.trim() !== ''); |
||||
|
||||
triggerWordAnywhere = $('[name=triggerWordAnywhere]:checked').val().trim(); |
||||
runOnEdits = $('[name=runOnEdits]:checked').val().trim(); |
||||
} |
||||
|
||||
let channel; |
||||
if (integrations.outgoingEvents[event].use.channel) { |
||||
channel = $('[name=channel]').val().trim(); |
||||
|
||||
if (!channel || channel.trim() === '') { |
||||
return toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__('Channel') })); |
||||
} |
||||
} |
||||
|
||||
let targetRoom; |
||||
if (integrations.outgoingEvents[event].use.targetRoom) { |
||||
targetRoom = $('[name=targetRoom]').val().trim(); |
||||
|
||||
if (!targetRoom || targetRoom.trim() === '') { |
||||
return toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__('TargetRoom') })); |
||||
} |
||||
} |
||||
|
||||
let retryCount; |
||||
let retryDelay; |
||||
if (retryFailedCalls === '1') { |
||||
retryCount = parseInt($('[name=retryCount]').val().trim()); |
||||
retryDelay = $('[name=retryDelay]').val().trim(); |
||||
} |
||||
|
||||
const integration = { |
||||
event: event !== '' ? event : undefined, |
||||
enabled: enabled === '1', |
||||
username, |
||||
channel: channel !== '' ? channel : undefined, |
||||
targetRoom: targetRoom !== '' ? targetRoom : undefined, |
||||
alias: alias !== '' ? alias : undefined, |
||||
emoji: emoji !== '' ? emoji : undefined, |
||||
avatar: avatar !== '' ? avatar : undefined, |
||||
name: name !== '' ? name : undefined, |
||||
triggerWords: triggerWords !== '' ? triggerWords : undefined, |
||||
urls: urls !== '' ? urls : undefined, |
||||
token: token !== '' ? token : undefined, |
||||
script: script !== '' ? script : undefined, |
||||
scriptEnabled: scriptEnabled === '1', |
||||
impersonateUser: impersonateUser === '1', |
||||
retryFailedCalls: retryFailedCalls === '1', |
||||
retryCount: retryCount || 6, |
||||
retryDelay: retryDelay || 'powers-of-ten', |
||||
triggerWordAnywhere: triggerWordAnywhere === '1', |
||||
runOnEdits: runOnEdits === '1', |
||||
}; |
||||
|
||||
const params = Template.instance().data.params ? Template.instance().data.params() : undefined; |
||||
if (params && params.id) { |
||||
Meteor.call('updateOutgoingIntegration', params.id, integration, (err) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
|
||||
toastr.success(TAPi18n.__('Integration_updated')); |
||||
}); |
||||
} else { |
||||
Meteor.call('addOutgoingIntegration', integration, (err, data) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
|
||||
toastr.success(TAPi18n.__('Integration_added')); |
||||
FlowRouter.go('admin-integrations-outgoing', { id: data._id }); |
||||
}); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.integrationsOutgoing.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,143 +0,0 @@ |
||||
<template name="integrationsOutgoingHistory"> |
||||
<section class="page-container page-home page-static page-settings"> |
||||
{{#header sectionName=pageTitle buttons=true}} |
||||
<div class="rc-header__section-button"> |
||||
<button class="rc-button rc-button--cancel clear-history">{{_ "clear_history"}}</button> |
||||
</div> |
||||
{{/header}} |
||||
|
||||
<div class="content"> |
||||
{{#unless hasPermission}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<a href="{{pathFor "admin-integrations-outgoing" id=integrationId}}"><i class="icon-angle-left"></i> {{_ "Back_to_integration_detail"}}</a><br><br> |
||||
|
||||
<div class="rocket-form"> |
||||
{{#each history in histories}} |
||||
<div class="section section-collapsed"> |
||||
<div class="section-title"> |
||||
<div class="section-title-text"> |
||||
<i class="{{iconClass history}}"></i> |
||||
{{formatDate history._createdAt}} |
||||
</div> |
||||
<div class="section-title-right"> |
||||
<button class="rc-button rc-button--secondary replay" data-history-id="{{ history._id }}">Replay</button> |
||||
<button class="rc-button rc-button--primary expand"> |
||||
<span>{{_ "Expand"}}</span> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
<div class="section-content"> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Status"}}</label> |
||||
<div> |
||||
<input class="input-monitor" type="text" disabled value="{{statusI18n history.error}}"> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Time_Triggered"}}</label> |
||||
<div> |
||||
<input class="input-monitor" type="text" disabled value="{{formatDateDetail history._createdAt}}"> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error"}}</label> |
||||
<div> |
||||
<input class="input-monitor" type="text" disabled value="{{formatDateDetail history._updatedAt}}"> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Event_Trigger"}}</label> |
||||
<div> |
||||
<input class="input-monitor" type="text" disabled value="{{eventTypei18n history.event}}"> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Trigger_Step"}}</label> |
||||
<div> |
||||
<input class="input-monitor" type="text" disabled value="{{history.step}}"> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{ jsonStringify history.data }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{#if hasProperty history 'prepareSentMessage'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{ jsonStringify history.prepareSentMessage }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasProperty history 'processSentMessage'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{ jsonStringify history.processSentMessage }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasProperty history 'url'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "URL"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{ history.url }}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasProperty history 'httpCallData'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Data_Passed_To_URL"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{jsonStringify history.httpCallData }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasProperty history 'httpError'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Http_Response_Error"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{ jsonStringify history.httpError }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasProperty history 'httpResult'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Http_Response"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{ jsonStringify history.httpResult }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasProperty history 'errorStack'}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Integration_Outgoing_WebHook_History_Error_Stacktrace"}}</label> |
||||
<div> |
||||
<pre><code class="code-colors hljs json">{{{ jsonStringify history.errorStack }}}</code></pre> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
<div class="section"> |
||||
<div class="section-title"> |
||||
<div class="section-title-text"> |
||||
{{_ "Integration_Outgoing_WebHook_No_History"}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{/each}} |
||||
{{#if isLoading}} |
||||
<div class="load-more"> |
||||
{{> loading}} |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
</template> |
||||
@ -1,191 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import _ from 'underscore'; |
||||
import hljs from 'highlight.js'; |
||||
import moment from 'moment'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { handleError } from '../../../utils'; |
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { integrations } from '../../lib/rocketchat'; |
||||
import { SideNav } from '../../../ui-utils/client'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
import { getIntegration } from '../getIntegration'; |
||||
import { integrationHistoryStreamer } from '../streamer'; |
||||
|
||||
const HISTORY_COUNT = 25; |
||||
|
||||
Template.integrationsOutgoingHistory.onCreated(async function _integrationsOutgoingHistoryOnCreated() { |
||||
const params = Template.instance().data.params ? Template.instance().data.params() : undefined; |
||||
this.isLoading = new ReactiveVar(false); |
||||
this.history = new ReactiveVar([]); |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
|
||||
if (params && params.id) { |
||||
integrationHistoryStreamer.on(params.id, ({ type, id, diff, data }) => { |
||||
const histories = this.history.get(); |
||||
|
||||
if (type === 'inserted') { |
||||
this.history.set([{ ...data }].concat(histories)); |
||||
return; |
||||
} |
||||
|
||||
if (type === 'updated') { |
||||
const history = histories.find(({ _id }) => _id === id); |
||||
Object.assign(history, diff); |
||||
this.history.set(histories); |
||||
return; |
||||
} |
||||
|
||||
if (type === 'removed') { |
||||
this.history.set([]); |
||||
} |
||||
}); |
||||
|
||||
const integration = await getIntegration(params.id, Meteor.userId()); |
||||
|
||||
if (!integration) { |
||||
toastr.error(TAPi18n.__('No_integration_found')); |
||||
return FlowRouter.go('admin-integrations'); |
||||
} |
||||
this.autorun(async () => { |
||||
this.isLoading.set(true); |
||||
const { history, total } = await APIClient.v1.get(`integrations.history?id=${ integration._id }&count=${ HISTORY_COUNT }&offset=${ this.offset.get() }`); |
||||
this.history.set(this.history.get().concat(history)); |
||||
this.total.set(total); |
||||
this.isLoading.set(false); |
||||
}); |
||||
} else { |
||||
toastr.error(TAPi18n.__('No_integration_found')); |
||||
FlowRouter.go('admin-integrations'); |
||||
} |
||||
}); |
||||
|
||||
Template.integrationsOutgoingHistory.helpers({ |
||||
hasPermission() { |
||||
return hasAtLeastOnePermission(['manage-outgoing-integrations', 'manage-own-outgoing-integrations']); |
||||
}, |
||||
|
||||
isLoading() { |
||||
return Template.instance().isLoading.get(); |
||||
}, |
||||
|
||||
histories() { |
||||
return Template.instance().history.get().sort((a, b) => { |
||||
if (+a._updatedAt < +b._updatedAt) { |
||||
return 1; |
||||
} |
||||
|
||||
if (+a._updatedAt > +b._updatedAt) { |
||||
return -1; |
||||
} |
||||
|
||||
return 0; |
||||
}); |
||||
}, |
||||
|
||||
hasProperty(history, property) { |
||||
return typeof history[property] !== 'undefined' || history[property] != null; |
||||
}, |
||||
|
||||
iconClass(history) { |
||||
if (typeof history.error !== 'undefined' && history.error) { |
||||
return 'icon-cancel-circled error-color'; |
||||
} if (history.finished) { |
||||
return 'icon-ok-circled success-color'; |
||||
} |
||||
return 'icon-help-circled'; |
||||
}, |
||||
|
||||
statusI18n(error) { |
||||
return typeof error !== 'undefined' && error ? TAPi18n.__('Failure') : TAPi18n.__('Success'); |
||||
}, |
||||
|
||||
formatDate(date) { |
||||
return moment(date).format('L LTS'); |
||||
}, |
||||
|
||||
formatDateDetail(date) { |
||||
return moment(date).format('L HH:mm:ss:SSSS'); |
||||
}, |
||||
|
||||
eventTypei18n(event) { |
||||
return TAPi18n.__(integrations.outgoingEvents[event].label); |
||||
}, |
||||
|
||||
jsonStringify(data) { |
||||
if (!data) { |
||||
return ''; |
||||
} if (typeof data === 'object') { |
||||
return hljs.highlight('json', JSON.stringify(data, null, 2)).value; |
||||
} |
||||
return hljs.highlight('json', data).value; |
||||
}, |
||||
|
||||
integrationId() { |
||||
return this.params && this.params() && this.params().id; |
||||
}, |
||||
}); |
||||
|
||||
Template.integrationsOutgoingHistory.events({ |
||||
'click .expand': (e) => { |
||||
$(e.currentTarget).closest('.section').removeClass('section-collapsed'); |
||||
$(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse')); |
||||
$('.CodeMirror').each((index, codeMirror) => codeMirror.CodeMirror.refresh()); |
||||
}, |
||||
|
||||
'click .collapse': (e) => { |
||||
$(e.currentTarget).closest('.section').addClass('section-collapsed'); |
||||
$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); |
||||
}, |
||||
|
||||
'click .replay': (e, t) => { |
||||
if (!t || !t.data || !t.data.params || !t.data.params().id) { |
||||
return; |
||||
} |
||||
|
||||
const historyId = $(e.currentTarget).attr('data-history-id'); |
||||
|
||||
Meteor.call('replayOutgoingIntegration', { integrationId: t.data.params().id, historyId }, (e) => { |
||||
if (e) { |
||||
handleError(e); |
||||
} |
||||
}); |
||||
}, |
||||
|
||||
'click .clear-history': (e, t) => { |
||||
if (!t || !t.data || !t.data.params || !t.data.params().id) { |
||||
return; |
||||
} |
||||
|
||||
Meteor.call('clearIntegrationHistory', t.data.params().id, (e) => { |
||||
if (e) { |
||||
handleError(e); |
||||
return; |
||||
} |
||||
|
||||
toastr.success(TAPi18n.__('Integration_History_Cleared')); |
||||
|
||||
t.history.set([]); |
||||
}); |
||||
}, |
||||
|
||||
'scroll .content': _.throttle((e, instance) => { |
||||
const history = instance.history.get(); |
||||
if ((e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) && instance.total.get() > history.length) { |
||||
instance.offset.set(instance.offset.get() + HISTORY_COUNT); |
||||
} |
||||
}, 200), |
||||
}); |
||||
|
||||
Template.integrationsOutgoingHistory.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,39 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import { Random } from 'meteor/random'; |
||||
|
||||
export const exampleMsg = () => { |
||||
const record = Template.instance().record.get(); |
||||
return { |
||||
_id: Random.id(), |
||||
alias: record.alias, |
||||
emoji: record.emoji, |
||||
avatar: record.avatar, |
||||
msg: 'Example message', |
||||
bot: { |
||||
i: Random.id(), |
||||
}, |
||||
groupable: false, |
||||
attachments: [{ |
||||
title: 'Rocket.Chat', |
||||
title_link: 'https://rocket.chat', |
||||
text: 'Rocket.Chat, the best open source chat', |
||||
image_url: '/images/integration-attachment-example.png', |
||||
color: '#764FA5', |
||||
}], |
||||
ts: new Date(), |
||||
u: { |
||||
_id: Random.id(), |
||||
username: record.username, |
||||
}, |
||||
}; |
||||
}; |
||||
|
||||
export const exampleUser = () => ({ |
||||
u: { |
||||
_id: Random.id(), |
||||
}, |
||||
}); |
||||
|
||||
export const exampleSettings = () => ({ |
||||
settings: {}, |
||||
}); |
||||
@ -1,28 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute } from '../../../../client/admin'; |
||||
import { t } from '../../../utils'; |
||||
|
||||
registerAdminRoute('/oauth-apps', { |
||||
name: 'admin-oauth-apps', |
||||
async action() { |
||||
await import('./views'); |
||||
return BlazeLayout.render('main', { |
||||
center: 'oauthApps', |
||||
pageTitle: t('OAuth_Applications'), |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/oauth-app/:id?', { |
||||
name: 'admin-oauth-app', |
||||
async action(params) { |
||||
await import('./views'); |
||||
return BlazeLayout.render('main', { |
||||
center: 'pageSettingsContainer', |
||||
pageTitle: t('OAuth_Application'), |
||||
pageTemplate: 'oauthApp', |
||||
params, |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,4 +0,0 @@ |
||||
import './oauthApp.html'; |
||||
import './oauthApp'; |
||||
import './oauthApps.html'; |
||||
import './oauthApps'; |
||||
@ -1,72 +0,0 @@ |
||||
<template name="oauthApp"> |
||||
<div class="permissions-manager"> |
||||
{{#if hasPermission}} |
||||
<a href="{{pathFor "admin-oauth-apps"}}"><i class="icon-angle-left"></i> {{_ "Back_to_applications"}}</a><br><br> |
||||
<div class="rocket-form"> |
||||
<div class="section"> |
||||
<div class="section-content"> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Active"}}</label> |
||||
<div> |
||||
<label><input class="input-monitor" type="radio" name="active" value="1" checked="{{$eq data.active true}}" /> {{_ "True"}}</label> |
||||
<label><input class="input-monitor" type="radio" name="active" value="0" checked="{{$eq data.active false}}" /> {{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Application_Name"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="name" value="{{data.name}}" /> |
||||
<div class="settings-description secondary-font-color">{{_ "Give_the_application_a_name_This_will_be_seen_by_your_users"}}</div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Redirect_URI"}}</label> |
||||
<div> |
||||
<textarea class="rc-input__element" name="redirectUri" rows="4" style="height: auto">{{data.redirectUri}}</textarea> |
||||
<div class="settings-description secondary-font-color">{{_ "After_OAuth2_authentication_users_will_be_redirected_to_this_URL"}}</div> |
||||
</div> |
||||
</div> |
||||
{{#if data.clientId}} |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Client_ID"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="clientId" value="{{data.clientId}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=clientId]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Client_Secret"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="clientSecret" value="{{data.clientSecret}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=clientSecret]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Authorization_URL"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="authorization_url" value="{{data.authorization_url}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=authorization_url]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
</div> |
||||
</div> |
||||
<div class="input-line double-col"> |
||||
<label>{{_ "Access_Token_URL"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="access_token_url" value="{{data.access_token_url}}" readonly="readonly" /> |
||||
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=access_token_url]">{{_ "COPY_TO_CLIPBOARD"}}</button></div> |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
<div class="submit"> |
||||
{{#if data.clientId}} |
||||
<button class="rc-button rc-button--danger delete"><i class="icon-trash"></i><span>{{_ "Delete"}}</span></button> |
||||
{{/if}} |
||||
<button class="rc-button rc-button--primary save"><i class="icon-send"></i><span>{{_ "Save_changes"}}</span></button> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
{{_ "Not_authorized"}} |
||||
{{/if}} |
||||
</div> |
||||
</template> |
||||
@ -1,120 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { hasAllPermission } from '../../../../authorization'; |
||||
import { modal, SideNav } from '../../../../ui-utils/client'; |
||||
import { t, handleError } from '../../../../utils'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
Template.oauthApp.onCreated(async function() { |
||||
const params = this.data.params(); |
||||
this.oauthApp = new ReactiveVar({}); |
||||
this.record = new ReactiveVar({ |
||||
active: true, |
||||
}); |
||||
if (params && params.id) { |
||||
const { oauthApp } = await APIClient.v1.get(`oauth-apps.get?appId=${ params.id }`); |
||||
this.oauthApp.set(oauthApp); |
||||
} |
||||
}); |
||||
|
||||
Template.oauthApp.helpers({ |
||||
hasPermission() { |
||||
return hasAllPermission('manage-oauth-apps'); |
||||
}, |
||||
data() { |
||||
const instance = Template.instance(); |
||||
if (typeof instance.data.params === 'function') { |
||||
const params = instance.data.params(); |
||||
if (params && params.id) { |
||||
const data = Template.instance().oauthApp.get(); |
||||
if (data) { |
||||
data.authorization_url = Meteor.absoluteUrl('oauth/authorize'); |
||||
data.access_token_url = Meteor.absoluteUrl('oauth/token'); |
||||
if (Array.isArray(data.redirectUri)) { |
||||
data.redirectUri = data.redirectUri.join('\n'); |
||||
} |
||||
|
||||
Template.instance().record.set(data); |
||||
return data; |
||||
} |
||||
} |
||||
} |
||||
return Template.instance().record.curValue; |
||||
}, |
||||
}); |
||||
|
||||
Template.oauthApp.events({ |
||||
'click .submit > .delete'() { |
||||
const params = Template.instance().data.params(); |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('You_will_not_be_able_to_recover'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_delete_it'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, function() { |
||||
Meteor.call('deleteOAuthApp', params.id, function() { |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Your_entry_has_been_deleted'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
FlowRouter.go('admin-oauth-apps'); |
||||
}); |
||||
}); |
||||
}, |
||||
'click .submit > .save'() { |
||||
const instance = Template.instance(); |
||||
const name = $('[name=name]').val().trim(); |
||||
const active = $('[name=active]:checked').val().trim() === '1'; |
||||
const redirectUri = $('[name=redirectUri]').val().trim(); |
||||
if (name === '') { |
||||
return toastr.error(TAPi18n.__('The_application_name_is_required')); |
||||
} |
||||
if (redirectUri === '') { |
||||
return toastr.error(TAPi18n.__('The_redirectUri_is_required')); |
||||
} |
||||
const app = { |
||||
name, |
||||
active, |
||||
redirectUri, |
||||
}; |
||||
if (typeof instance.data.params === 'function') { |
||||
const params = instance.data.params(); |
||||
if (params && params.id) { |
||||
return Meteor.call('updateOAuthApp', params.id, app, function(err) { |
||||
if (err != null) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Application_updated')); |
||||
}); |
||||
} |
||||
} |
||||
Meteor.call('addOAuthApp', app, function(err, data) { |
||||
if (err != null) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Application_added')); |
||||
FlowRouter.go('admin-oauth-app', { id: data._id }); |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
Template.oauthApp.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,38 +0,0 @@ |
||||
<template name="oauthApps"> |
||||
<section class="permissions-manager page-container page-home page-static page-list"> |
||||
{{# header sectionName=pageTitle}} |
||||
<div class="rc-header__section-button"> |
||||
<a href="{{pathFor "admin-oauth-app"}}" class="rc-button rc-button--primary new-role">{{_ "New_Application"}}</a> |
||||
</div> |
||||
{{/header}} |
||||
<div class="content"> |
||||
{{#if hasPermission}} |
||||
<div class="rocket-form"> |
||||
<div class="section"> |
||||
<div class="admin-integrations-new-panel"> |
||||
{{#each applications}} |
||||
<a href="{{pathFor "admin-oauth-app" id=_id}}"> |
||||
<div class="admin-integrations-new-item"> |
||||
<div class="admin-integrations-new-item-body"> |
||||
<div class="admin-integrations-new-item-title"> |
||||
{{name}} |
||||
</div> |
||||
<div class="admin-integrations-new-item-description"> |
||||
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}} |
||||
</div> |
||||
</div> |
||||
<i class="icon-angle-right"></i> |
||||
</div> |
||||
</a> |
||||
{{else}} |
||||
<h1>{{_ "There_are_no_applications"}}</h1> |
||||
{{/each}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
{{_ "Not_authorized"}} |
||||
{{/if}} |
||||
</div> |
||||
</section> |
||||
</template> |
||||
@ -1,33 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import moment from 'moment'; |
||||
|
||||
import { hasAllPermission } from '../../../../authorization'; |
||||
import { SideNav } from '../../../../ui-utils/client'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
Template.oauthApps.onCreated(async function() { |
||||
this.oauthApps = new ReactiveVar([]); |
||||
const { oauthApps } = await APIClient.v1.get('oauth-apps.list'); |
||||
this.oauthApps.set(oauthApps); |
||||
}); |
||||
|
||||
Template.oauthApps.helpers({ |
||||
hasPermission() { |
||||
return hasAllPermission('manage-oauth-apps'); |
||||
}, |
||||
applications() { |
||||
return Template.instance().oauthApps.get(); |
||||
}, |
||||
dateFormated(date) { |
||||
return moment(date).format('L LT'); |
||||
}, |
||||
}); |
||||
|
||||
Template.oauthApps.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,5 +1,4 @@ |
||||
import './oauth/oauth2-client.html'; |
||||
import './oauth/oauth2-client'; |
||||
import './admin/startup'; |
||||
import './admin/route'; |
||||
import './oauth/stylesheets/oauth2.css'; |
||||
|
||||
@ -0,0 +1,146 @@ |
||||
import { Box, Button, ButtonGroup, Margins } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useQueryStringParameter, useRoute, useRouteParameter } from '../../contexts/RouterContext'; |
||||
import WhatIsItSection from './WhatIsItSection'; |
||||
import ConnectToCloudSection from './ConnectToCloudSection'; |
||||
import TroubleshootingSection from './TroubleshootingSection'; |
||||
import WorkspaceRegistrationSection from './WorkspaceRegistrationSection'; |
||||
import WorkspaceLoginSection from './WorkspaceLoginSection'; |
||||
import ManualWorkspaceRegistrationModal from './ManualWorkspaceRegistrationModal'; |
||||
import { cloudConsoleUrl } from './constants'; |
||||
|
||||
function CloudPage() { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const cloudRoute = useRoute('cloud'); |
||||
|
||||
const page = useRouteParameter('page'); |
||||
|
||||
const errorCode = useQueryStringParameter('error_code'); |
||||
const code = useQueryStringParameter('code'); |
||||
const state = useQueryStringParameter('state'); |
||||
const token = useQueryStringParameter('token'); |
||||
|
||||
const finishOAuthAuthorization = useMethod('cloud:finishOAuthAuthorization'); |
||||
const checkRegisterStatus = useMethod('cloud:checkRegisterStatus'); |
||||
const connectWorkspace = useMethod('cloud:connectWorkspace'); |
||||
|
||||
useEffect(() => { |
||||
const acceptOAuthAuthorization = async () => { |
||||
if (page !== 'oauth-callback') { |
||||
return; |
||||
} |
||||
|
||||
if (errorCode) { |
||||
dispatchToastMessage({ |
||||
type: 'error', |
||||
title: t('Cloud_error_in_authenticating'), |
||||
message: t('Cloud_error_code', { errorCode }), |
||||
}); |
||||
cloudRoute.push(); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
await finishOAuthAuthorization(code, state); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
cloudRoute.push(); |
||||
} |
||||
}; |
||||
|
||||
acceptOAuthAuthorization(); |
||||
}, [errorCode, code, state]); |
||||
|
||||
const [registerStatus, setRegisterStatus] = useSafely(useState()); |
||||
const [modal, setModal] = useState(null); |
||||
|
||||
const fetchRegisterStatus = useMutableCallback(async () => { |
||||
try { |
||||
const registerStatus = await checkRegisterStatus(); |
||||
setRegisterStatus(registerStatus); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
const acceptWorkspaceToken = async () => { |
||||
try { |
||||
if (token) { |
||||
const isConnected = await connectWorkspace(token); |
||||
|
||||
if (!isConnected) { |
||||
throw Error(t('An error occured connecting')); |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Connected') }); |
||||
} |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
await fetchRegisterStatus(); |
||||
} |
||||
}; |
||||
|
||||
acceptWorkspaceToken(); |
||||
}, [token]); |
||||
|
||||
const handleManualWorkspaceRegistrationButtonClick = () => { |
||||
const handleModalClose = () => { |
||||
setModal(null); |
||||
fetchRegisterStatus(); |
||||
}; |
||||
setModal(<ManualWorkspaceRegistrationModal onClose={handleModalClose} />); |
||||
}; |
||||
|
||||
const isConnectedToCloud = registerStatus?.connectToCloud; |
||||
const isWorkspaceRegistered = registerStatus?.workspaceRegistered; |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Connectivity_Services')}> |
||||
<ButtonGroup> |
||||
{!isWorkspaceRegistered && <Button onClick={handleManualWorkspaceRegistrationButtonClick}> |
||||
{t('Cloud_Register_manually')} |
||||
</Button>} |
||||
<Button is='a' primary href={cloudConsoleUrl} target='_blank' rel='noopener noreferrer'> |
||||
{t('Cloud_console')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
{modal} |
||||
<Box marginInline='auto' marginBlock='neg-x24' width='full' maxWidth='x580'> |
||||
<Margins block='x24'> |
||||
<WhatIsItSection /> |
||||
|
||||
{isConnectedToCloud && <> |
||||
{isWorkspaceRegistered |
||||
? <WorkspaceLoginSection onRegisterStatusChange={fetchRegisterStatus} /> |
||||
: <WorkspaceRegistrationSection |
||||
email={registerStatus?.email} |
||||
token={registerStatus?.token} |
||||
workspaceId={registerStatus?.workspaceId} |
||||
uniqueId={registerStatus?.uniqueId} |
||||
onRegisterStatusChange={fetchRegisterStatus} |
||||
/>} |
||||
|
||||
<TroubleshootingSection onRegisterStatusChange={fetchRegisterStatus} /> |
||||
</>} |
||||
|
||||
{!isConnectedToCloud && <ConnectToCloudSection onRegisterStatusChange={fetchRegisterStatus} />} |
||||
</Margins> |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
|
||||
export default CloudPage; |
||||
@ -0,0 +1,17 @@ |
||||
import React from 'react'; |
||||
|
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../NotAuthorizedPage'; |
||||
import CloudPage from './CloudPage'; |
||||
|
||||
function CloudRoute() { |
||||
const canManageCloud = usePermission('manage-cloud'); |
||||
|
||||
if (!canManageCloud) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <CloudPage />; |
||||
} |
||||
|
||||
export default CloudRoute; |
||||
@ -0,0 +1,61 @@ |
||||
import { Box, Button, ButtonGroup, Throbber } from '@rocket.chat/fuselage'; |
||||
import { useSafely } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
import Subtitle from '../../components/basic/Subtitle'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
|
||||
function ConnectToCloudSection({ |
||||
onRegisterStatusChange, |
||||
...props |
||||
}) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [isConnecting, setConnecting] = useSafely(useState(false)); |
||||
|
||||
const registerWorkspace = useMethod('cloud:registerWorkspace'); |
||||
const syncWorkspace = useMethod('cloud:syncWorkspace'); |
||||
|
||||
const handleRegisterButtonClick = async () => { |
||||
setConnecting(true); |
||||
|
||||
try { |
||||
const isRegistered = await registerWorkspace(); |
||||
|
||||
if (!isRegistered) { |
||||
throw Error(t('An error occured')); |
||||
} |
||||
|
||||
// TODO: sync on register?
|
||||
const isSynced = await syncWorkspace(); |
||||
|
||||
if (!isSynced) { |
||||
throw Error(t('An error occured syncing')); |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Sync Complete') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
await (onRegisterStatusChange && onRegisterStatusChange()); |
||||
setConnecting(false); |
||||
} |
||||
}; |
||||
|
||||
return <Box is='section' {...props}> |
||||
<Subtitle>{t('Cloud_registration_required')}</Subtitle> |
||||
<Box withRichContent> |
||||
<p>{t('Cloud_registration_required_description')}</p> |
||||
</Box> |
||||
<ButtonGroup> |
||||
<Button primary disabled={isConnecting} minHeight='x40' onClick={handleRegisterButtonClick}> |
||||
{isConnecting ? <Throbber is='span' inheritColor /> : t('Cloud_registration_required_link_text')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Box>; |
||||
} |
||||
|
||||
export default ConnectToCloudSection; |
||||
@ -0,0 +1,183 @@ |
||||
import { Box, Button, ButtonGroup, Icon, Scrollable, Throbber } from '@rocket.chat/fuselage'; |
||||
import Clipboard from 'clipboard'; |
||||
import React, { useEffect, useState, useRef } from 'react'; |
||||
|
||||
import { Modal } from '../../components/basic/Modal'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod, useEndpoint } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import MarkdownText from '../../components/basic/MarkdownText'; |
||||
import { cloudConsoleUrl } from './constants'; |
||||
|
||||
function CopyStep({ onNextButtonClick }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [clientKey, setClientKey] = useState(''); |
||||
|
||||
const getWorkspaceRegisterData = useMethod('cloud:getWorkspaceRegisterData'); |
||||
|
||||
useEffect(() => { |
||||
const loadWorkspaceRegisterData = async () => { |
||||
const clientKey = await getWorkspaceRegisterData(); |
||||
setClientKey(clientKey); |
||||
}; |
||||
|
||||
loadWorkspaceRegisterData(); |
||||
}, []); |
||||
|
||||
const copyRef = useRef(); |
||||
|
||||
useEffect(function() { |
||||
const clipboard = new Clipboard(copyRef.current); |
||||
clipboard.on('success', () => { |
||||
dispatchToastMessage({ type: 'success', message: t('Copied') }); |
||||
}); |
||||
|
||||
return () => { |
||||
clipboard.destroy(); |
||||
}; |
||||
}, []); |
||||
|
||||
return <> |
||||
<Modal.Content> |
||||
<Box withRichContent> |
||||
<p>{t('Cloud_register_offline_helper')}</p> |
||||
</Box> |
||||
<Box |
||||
display='flex' |
||||
flexDirection='column' |
||||
alignItems='stretch' |
||||
padding='x16' |
||||
flexGrow={1} |
||||
backgroundColor='neutral-800' |
||||
> |
||||
<Scrollable vertical> |
||||
<Box |
||||
height='x108' |
||||
fontFamily='mono' |
||||
fontScale='p1' |
||||
color='alternative' |
||||
style={{ wordBreak: 'break-all' }} |
||||
> |
||||
{clientKey} |
||||
</Box> |
||||
</Scrollable> |
||||
<Button ref={copyRef} primary data-clipboard-text={clientKey}> |
||||
<Icon name='copy' /> {t('Copy')} |
||||
</Button> |
||||
</Box> |
||||
<Box withRichContent> |
||||
<p> |
||||
<MarkdownText>{t('Cloud_click_here', { cloudConsoleUrl })}</MarkdownText> |
||||
</p> |
||||
</Box> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup> |
||||
<Button primary onClick={onNextButtonClick}>{t('Next')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</>; |
||||
} |
||||
|
||||
function PasteStep({ onBackButtonClick, onFinish }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [isLoading, setLoading] = useState(false); |
||||
const [cloudKey, setCloudKey] = useState(''); |
||||
|
||||
const handleCloudKeyChange = (e) => { |
||||
setCloudKey(e.currentTarget.value); |
||||
}; |
||||
|
||||
const registerManually = useEndpoint('POST', 'cloud.manualRegister'); |
||||
|
||||
const handleFinishButtonClick = async () => { |
||||
setLoading(true); |
||||
|
||||
try { |
||||
await registerManually({}, { cloudBlob: cloudKey }); |
||||
dispatchToastMessage({ type: 'success', message: t('Cloud_register_success') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: t('Cloud_register_error') }); |
||||
} finally { |
||||
setLoading(false); |
||||
onFinish && onFinish(); |
||||
} |
||||
}; |
||||
|
||||
return <> |
||||
<Modal.Content> |
||||
<Box withRichContent> |
||||
<p>{t('Cloud_register_offline_finish_helper')}</p> |
||||
</Box> |
||||
<Box |
||||
display='flex' |
||||
flexDirection='column' |
||||
alignItems='stretch' |
||||
padding='x16' |
||||
flexGrow={1} |
||||
backgroundColor='neutral-800' |
||||
> |
||||
<Scrollable vertical> |
||||
<Box |
||||
is='textarea' |
||||
height='x108' |
||||
fontFamily='mono' |
||||
fontScale='p1' |
||||
color='alternative' |
||||
style={{ wordBreak: 'break-all', resize: 'none' }} |
||||
placeholder={t('Paste_here')} |
||||
disabled={isLoading} |
||||
value={cloudKey} |
||||
autoComplete='off' |
||||
autoCorrect='off' |
||||
autoCapitalize='off' |
||||
spellCheck='false' |
||||
onChange={handleCloudKeyChange} |
||||
/> |
||||
</Scrollable> |
||||
</Box> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup> |
||||
<Button disabled={isLoading} onClick={onBackButtonClick}>{t('Back')}</Button> |
||||
<Button primary disabled={isLoading || !cloudKey.trim()} marginInlineStart='auto' onClick={handleFinishButtonClick}> |
||||
{isLoading ? <Throbber inheritColor /> : t('Finish Registration')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</>; |
||||
} |
||||
|
||||
const Steps = { |
||||
COPY: 'copy', |
||||
PASTE: 'paste', |
||||
}; |
||||
|
||||
function ManualWorkspaceRegistrationModal({ onClose, props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const [step, setStep] = useState(Steps.COPY); |
||||
|
||||
const handleNextButtonClick = () => { |
||||
setStep(Steps.PASTE); |
||||
}; |
||||
|
||||
const handleBackButtonClick = () => { |
||||
setStep(Steps.COPY); |
||||
}; |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Modal.Title>{t('Cloud_Register_manually')}</Modal.Title> |
||||
<Modal.Close onClick={onClose} /> |
||||
</Modal.Header> |
||||
{(step === Steps.COPY && <CopyStep onNextButtonClick={handleNextButtonClick} />) |
||||
|| (step === Steps.PASTE && <PasteStep onBackButtonClick={handleBackButtonClick} onFinish={onClose} />)} |
||||
</Modal>; |
||||
} |
||||
|
||||
export default ManualWorkspaceRegistrationModal; |
||||
@ -0,0 +1,63 @@ |
||||
import { Box, Button, ButtonGroup, Throbber } from '@rocket.chat/fuselage'; |
||||
import { useSafely } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import Subtitle from '../../components/basic/Subtitle'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { statusPageUrl } from './constants'; |
||||
|
||||
function TroubleshootingSection({ |
||||
onRegisterStatusChange, |
||||
...props |
||||
}) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [isSyncing, setSyncing] = useSafely(useState(false)); |
||||
|
||||
const syncWorkspace = useMethod('cloud:syncWorkspace'); |
||||
|
||||
const handleSyncButtonClick = async () => { |
||||
setSyncing(true); |
||||
|
||||
try { |
||||
const isSynced = await syncWorkspace(); |
||||
|
||||
if (!isSynced) { |
||||
throw Error(t('An error occured syncing')); |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Sync Complete') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
await (onRegisterStatusChange && onRegisterStatusChange()); |
||||
setSyncing(false); |
||||
} |
||||
}; |
||||
|
||||
return <Box is='section' {...props}> |
||||
<Subtitle>{t('Cloud_troubleshooting')}</Subtitle> |
||||
|
||||
<Box withRichContent> |
||||
<p>{t('Cloud_workspace_support')}</p> |
||||
</Box> |
||||
|
||||
<ButtonGroup> |
||||
<Button disabled={isSyncing} minHeight='x40' onClick={handleSyncButtonClick}> |
||||
{isSyncing ? <Throbber is='span' inheritColor /> : t('Sync')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
|
||||
<Box withRichContent> |
||||
<p> |
||||
{t('Cloud_status_page_description')}:{' '} |
||||
<a href={statusPageUrl} target='_blank' rel='noopener noreferrer'>{statusPageUrl}</a> |
||||
</p> |
||||
</Box> |
||||
</Box>; |
||||
} |
||||
|
||||
export default TroubleshootingSection; |
||||
@ -0,0 +1,32 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import Subtitle from '../../components/basic/Subtitle'; |
||||
|
||||
function WhatIsItSection(props) { |
||||
const t = useTranslation(); |
||||
|
||||
return <Box is='section' {...props}> |
||||
<Subtitle>{t('Cloud_what_is_it')}</Subtitle> |
||||
|
||||
<Box withRichContent> |
||||
<p>{t('Cloud_what_is_it_description')}</p> |
||||
|
||||
<details> |
||||
<p>{t('Cloud_what_is_it_services_like')}</p> |
||||
|
||||
<ul> |
||||
<li>{t('Register_Server_Registered_Push_Notifications')}</li> |
||||
<li>{t('Register_Server_Registered_Livechat')}</li> |
||||
<li>{t('Register_Server_Registered_OAuth')}</li> |
||||
<li>{t('Register_Server_Registered_Marketplace')}</li> |
||||
</ul> |
||||
|
||||
<p>{t('Cloud_what_is_it_additional')}</p> |
||||
</details> |
||||
</Box> |
||||
</Box>; |
||||
} |
||||
|
||||
export default WhatIsItSection; |
||||
@ -0,0 +1,108 @@ |
||||
import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; |
||||
import { useSafely } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
|
||||
function WorkspaceLoginSection({ |
||||
onRegisterStatusChange, |
||||
...props |
||||
}) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); |
||||
const getOAuthAuthorizationUrl = useMethod('cloud:getOAuthAuthorizationUrl'); |
||||
const logout = useMethod('cloud:logout'); |
||||
const disconnectWorkspace = useMethod('cloud:disconnectWorkspace'); |
||||
|
||||
const [isLoggedIn, setLoggedIn] = useSafely(useState(false)); |
||||
const [isLoading, setLoading] = useSafely(useState(true)); |
||||
|
||||
const handleLoginButtonClick = async () => { |
||||
setLoading(true); |
||||
|
||||
try { |
||||
const url = await getOAuthAuthorizationUrl(); |
||||
window.location.href = url; |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
const handleLogoutButtonClick = async () => { |
||||
setLoading(true); |
||||
|
||||
try { |
||||
await logout(); |
||||
const isLoggedIn = await checkUserLoggedIn(); |
||||
setLoggedIn(isLoggedIn); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
const handleDisconnectButtonClick = async () => { |
||||
setLoading(true); |
||||
|
||||
try { |
||||
const success = await disconnectWorkspace(); |
||||
|
||||
if (!success) { |
||||
throw Error(t('An error occured disconnecting')); |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Disconnected') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
await (onRegisterStatusChange && onRegisterStatusChange()); |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
const checkLoginState = async () => { |
||||
setLoading(true); |
||||
|
||||
try { |
||||
const isLoggedIn = await checkUserLoggedIn(); |
||||
setLoggedIn(isLoggedIn); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
checkLoginState(); |
||||
}, []); |
||||
|
||||
return <Box is='section' {...props}> |
||||
<Box withRichContent> |
||||
<p>{t('Cloud_workspace_connected')}</p> |
||||
</Box> |
||||
|
||||
<ButtonGroup> |
||||
{isLoggedIn |
||||
? <Button primary danger disabled={isLoading} onClick={handleLogoutButtonClick}>{t('Cloud_logout')}</Button> |
||||
: <Button primary disabled={isLoading} onClick={handleLoginButtonClick}>{t('Cloud_login_to_cloud')}</Button>} |
||||
</ButtonGroup> |
||||
|
||||
<Box withRichContent> |
||||
<p>{t('Cloud_workspace_disconnect')}</p> |
||||
</Box> |
||||
|
||||
<ButtonGroup> |
||||
<Button primary danger disabled={isLoading} onClick={handleDisconnectButtonClick}>{t('Disconnect')}</Button> |
||||
</ButtonGroup> |
||||
</Box>; |
||||
} |
||||
|
||||
export default WorkspaceLoginSection; |
||||
@ -0,0 +1,128 @@ |
||||
import { Box, Button, ButtonGroup, EmailInput, Field, Margins, TextInput } from '@rocket.chat/fuselage'; |
||||
import { useSafely, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState, useMemo } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { supportEmailAddress } from './constants'; |
||||
|
||||
function WorkspaceRegistrationSection({ |
||||
email: initialEmail, |
||||
token: initialToken, |
||||
workspaceId, |
||||
uniqueId, |
||||
onRegisterStatusChange, |
||||
...props |
||||
}) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const updateEmail = useMethod('cloud:updateEmail'); |
||||
const connectWorkspace = useMethod('cloud:connectWorkspace'); |
||||
|
||||
const [isProcessing, setProcessing] = useSafely(useState(false)); |
||||
const [email, setEmail] = useState(initialEmail); |
||||
const [token, setToken] = useState(initialToken); |
||||
|
||||
const supportMailtoUrl = useMemo(() => { |
||||
const subject = encodeURIComponent('Self Hosted Registration'); |
||||
const body = encodeURIComponent([ |
||||
`WorkspaceId: ${ workspaceId }`, |
||||
`Deployment Id: ${ uniqueId }`, |
||||
'Issue: <please describe your issue here>', |
||||
].join('\r\n')); |
||||
return `mailto:${ supportEmailAddress }?subject=${ subject }&body=${ body }`; |
||||
}, [workspaceId, uniqueId]); |
||||
|
||||
const handleEmailChange = ({ currentTarget: { value } }) => { |
||||
setEmail(value); |
||||
}; |
||||
|
||||
const handleTokenChange = ({ currentTarget: { value } }) => { |
||||
setToken(value); |
||||
}; |
||||
|
||||
const handleUpdateEmailButtonClick = async () => { |
||||
setProcessing(true); |
||||
|
||||
try { |
||||
await updateEmail(email, false); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setProcessing(false); |
||||
} |
||||
}; |
||||
|
||||
const handleResendEmailButtonClick = async () => { |
||||
setProcessing(true); |
||||
|
||||
try { |
||||
await updateEmail(email, true); |
||||
dispatchToastMessage({ type: 'success', message: t('Requested') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setProcessing(false); |
||||
} |
||||
}; |
||||
|
||||
const handleConnectButtonClick = async () => { |
||||
setProcessing(true); |
||||
|
||||
try { |
||||
const isConnected = await connectWorkspace(token); |
||||
|
||||
if (!isConnected) { |
||||
throw Error(t('An error occured connecting')); |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Connected') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
await (onRegisterStatusChange && onRegisterStatusChange()); |
||||
setProcessing(false); |
||||
} |
||||
}; |
||||
|
||||
const emailInputId = useUniqueId(); |
||||
const tokenInputId = useUniqueId(); |
||||
|
||||
return <Box marginBlock='neg-x24' {...props}> |
||||
<Margins block='x24'> |
||||
<Field> |
||||
<Field.Label htmlFor={emailInputId}>{t('Email')}</Field.Label> |
||||
<Field.Row> |
||||
<EmailInput id={emailInputId} disabled={isProcessing} value={email} onChange={handleEmailChange} /> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Cloud_address_to_send_registration_to')}</Field.Hint> |
||||
</Field> |
||||
|
||||
<ButtonGroup> |
||||
<Button disabled={isProcessing} onClick={handleUpdateEmailButtonClick}>{t('Cloud_update_email')}</Button> |
||||
<Button disabled={isProcessing} onClick={handleResendEmailButtonClick}>{t('Cloud_resend_email')}</Button> |
||||
</ButtonGroup> |
||||
|
||||
<Field> |
||||
<Field.Label htmlFor={tokenInputId}>{t('Token')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput id={tokenInputId} disabled={isProcessing} value={token} onChange={handleTokenChange} /> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Cloud_manually_input_token')}</Field.Hint> |
||||
</Field> |
||||
|
||||
<ButtonGroup> |
||||
<Button primary disabled={isProcessing} onClick={handleConnectButtonClick}>{t('Connect')}</Button> |
||||
</ButtonGroup> |
||||
|
||||
<Box withRichContent> |
||||
<p>{t('Cloud_connect_support')}: <a href={supportMailtoUrl} target='_blank' rel='noopener noreferrer'>{supportEmailAddress}</a></p> |
||||
</Box> |
||||
</Margins> |
||||
</Box>; |
||||
} |
||||
|
||||
export default WorkspaceRegistrationSection; |
||||
@ -0,0 +1,3 @@ |
||||
export const cloudConsoleUrl = 'https://cloud.rocket.chat'; |
||||
export const supportEmailAddress = 'support@rocket.chat'; |
||||
export const statusPageUrl = 'https://status.rocket.chat'; |
||||
@ -0,0 +1,202 @@ |
||||
import React, { useCallback, useState, useMemo, useEffect } from 'react'; |
||||
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { Modal } from '../../components/basic/Modal'; |
||||
import { useFileInput } from '../../hooks/useFileInput'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { validate, createSoundData } from './lib'; |
||||
|
||||
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{t('Custom_Sound_Status_Delete_Warning')} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary danger onClick={onDelete}>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
const SuccessModal = ({ onClose, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='success' name='checkmark-circled' size={20}/> |
||||
<Modal.Title>{t('Deleted')}</Modal.Title> |
||||
<Modal.Close onClick={onClose}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{t('Custom_Sound_Has_Been_Deleted')} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button primary onClick={onClose}>{t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export function EditSound({ _id, cache, ...props }) { |
||||
const t = useTranslation(); |
||||
const query = useMemo(() => ({ |
||||
query: JSON.stringify({ _id }), |
||||
}), [_id]); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental('custom-sounds.list', query); |
||||
|
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box pb='x20'> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button disabled><Throbber inheritColor/></Button> |
||||
<Button primary disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button primary danger disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
</Box>; |
||||
} |
||||
|
||||
if (error || !data || data.sounds.length < 1) { |
||||
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>; |
||||
} |
||||
|
||||
return <EditCustomSound data={data.sounds[0]} {...props}/>; |
||||
} |
||||
|
||||
export function EditCustomSound({ close, onChange, data, ...props }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const { _id, name: previousName } = data || {}; |
||||
const previousSound = data || {}; |
||||
|
||||
const [name, setName] = useState(''); |
||||
const [sound, setSound] = useState(); |
||||
const [modal, setModal] = useState(); |
||||
|
||||
useEffect(() => { |
||||
setName(previousName || ''); |
||||
setSound(previousSound || ''); |
||||
}, [previousName, previousSound, _id]); |
||||
|
||||
const deleteCustomSound = useMethod('deleteCustomSound'); |
||||
const uploadCustomSound = useMethod('uploadCustomSound'); |
||||
const insertOrUpdateSound = useMethod('insertOrUpdateSound'); |
||||
|
||||
const handleChangeFile = (soundFile) => { |
||||
setSound(soundFile); |
||||
}; |
||||
|
||||
const hasUnsavedChanges = useMemo(() => previousName !== name || previousSound !== sound, [name, sound]); |
||||
|
||||
const saveAction = async (sound) => { |
||||
const soundData = createSoundData(sound, name, { previousName, previousSound, _id }); |
||||
const validation = validate(soundData, sound); |
||||
if (validation.length === 0) { |
||||
let soundId; |
||||
try { |
||||
soundId = await insertOrUpdateSound(soundData); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
|
||||
soundData._id = soundId; |
||||
soundData.random = Math.round(Math.random() * 1000); |
||||
|
||||
if (sound && sound !== previousSound) { |
||||
dispatchToastMessage({ type: 'success', message: t('Uploading_file') }); |
||||
|
||||
const reader = new FileReader(); |
||||
reader.readAsBinaryString(sound); |
||||
reader.onloadend = () => { |
||||
try { |
||||
uploadCustomSound(reader.result, sound.type, soundData); |
||||
return dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
|
||||
validation.forEach((error) => dispatchToastMessage({ type: 'error', message: t('error-the-field-is-required', { field: t(error) }) })); |
||||
}; |
||||
|
||||
const handleSave = useCallback(async () => { |
||||
saveAction(sound); |
||||
onChange(); |
||||
}, [name, _id, sound]); |
||||
|
||||
const onDeleteConfirm = useCallback(async () => { |
||||
try { |
||||
await deleteCustomSound(_id); |
||||
setModal(() => <SuccessModal onClose={() => { setModal(undefined); close(); onChange(); }}/>); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
onChange(); |
||||
} |
||||
}, [_id]); |
||||
|
||||
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>); |
||||
|
||||
|
||||
const clickUpload = useFileInput(handleChangeFile, 'audio/mp3'); |
||||
|
||||
|
||||
return <> |
||||
<Box display='flex' flexDirection='column' fontScale='p1' color='default' mbs='x20' {...props}> |
||||
<Margins block='x4'> |
||||
<Field> |
||||
<Field.Label>{t('Name')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={name} onChange={(e) => setName(e.currentTarget.value)} placeholder={t('Name')} /> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Label alignSelf='stretch'>{t('Sound_File_mp3')}</Field.Label> |
||||
<Box display='flex' flexDirection='row' mbs='none'> |
||||
<Margins inline='x4'> |
||||
<Button square onClick={clickUpload}><Icon name='upload' size='x20'/></Button> |
||||
{(sound && sound.name) || 'none'} |
||||
</Margins> |
||||
</Box> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button onClick={close}>{t('Cancel')}</Button> |
||||
<Button primary onClick={handleSave} disabled={!hasUnsavedChanges}>{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button primary danger onClick={openConfirmDelete}><Icon name='trash' mie='x4'/>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
</Field> |
||||
</Margins> |
||||
</Box> |
||||
{ modal } |
||||
</>; |
||||
} |
||||
@ -0,0 +1,101 @@ |
||||
import React, { useState, useCallback } from 'react'; |
||||
import { Field, TextInput, Box, Icon, Margins, Button, ButtonGroup } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useFileInput } from '../../hooks/useFileInput'; |
||||
import { validate, createSoundData } from './lib'; |
||||
|
||||
export function NewSound({ goToNew, close, onChange, ...props }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [name, setName] = useState(''); |
||||
const [sound, setSound] = useState(); |
||||
|
||||
const uploadCustomSound = useMethod('uploadCustomSound'); |
||||
|
||||
const insertOrUpdateSound = useMethod('insertOrUpdateSound'); |
||||
|
||||
const handleChangeFile = (soundFile) => { |
||||
setSound(soundFile); |
||||
}; |
||||
|
||||
const clickUpload = useFileInput(handleChangeFile, 'audio/mp3'); |
||||
|
||||
const saveAction = async (name, soundFile) => { |
||||
const soundData = createSoundData(soundFile, name); |
||||
const validation = validate(soundData, sound); |
||||
if (validation.length === 0) { |
||||
let soundId; |
||||
try { |
||||
soundId = await insertOrUpdateSound(soundData); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
|
||||
soundData._id = soundId; |
||||
soundData.random = Math.round(Math.random() * 1000); |
||||
|
||||
if (soundId) { |
||||
dispatchToastMessage({ type: 'success', message: t('Uploading_file') }); |
||||
|
||||
const reader = new FileReader(); |
||||
reader.readAsBinaryString(soundFile); |
||||
reader.onloadend = () => { |
||||
try { |
||||
uploadCustomSound(reader.result, soundFile.type, soundData); |
||||
dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
} |
||||
return soundId; |
||||
} |
||||
validation.forEach((error) => { throw new Error({ type: 'error', message: t('error-the-field-is-required', { field: t(error) }) }); }); |
||||
}; |
||||
|
||||
const handleSave = useCallback(async () => { |
||||
try { |
||||
const result = await saveAction( |
||||
name, |
||||
sound, |
||||
); |
||||
dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Updated_Successfully') }); |
||||
goToNew(result)(); |
||||
onChange(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}, [name, sound]); |
||||
|
||||
return <Box display='flex' flexDirection='column' fontScale='p1' color='default' mbs='x20' {...props}> |
||||
<Margins block='x4'> |
||||
<Field> |
||||
<Field.Label>{t('Name')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={name} onChange={(e) => setName(e.currentTarget.value)} placeholder={t('Name')} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label alignSelf='stretch'>{t('Sound_File_mp3')}</Field.Label> |
||||
<Box display='flex' flexDirection='row' mbs='none'> |
||||
<Margins inline='x4'> |
||||
<Button square onClick={clickUpload}><Icon name='upload' size='x20'/></Button> |
||||
{(sound && sound.name) || 'none'} |
||||
</Margins> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button mie='x4' onClick={close}>{t('Cancel')}</Button> |
||||
<Button primary onClick={handleSave} disabled={name === ''}>{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
</Field> |
||||
</Margins> |
||||
</Box>; |
||||
} |
||||
@ -0,0 +1,165 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Field, TextInput, Box, ToggleSwitch, Icon, TextAreaInput, FieldGroup, Margins } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useAbsoluteUrl } from '../../contexts/ServerContext'; |
||||
import { useHilightCode } from '../../hooks/useHilightCode'; |
||||
import { useExampleData } from './exampleIncomingData'; |
||||
import Page from '../../components/basic/Page'; |
||||
|
||||
export default function IncomingWebhookForm({ formValues, formHandlers, extraData = {}, append, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const hilightCode = useHilightCode(); |
||||
|
||||
const absoluteUrl = useAbsoluteUrl(); |
||||
|
||||
const { |
||||
enabled, |
||||
channel, |
||||
username, |
||||
name, |
||||
alias, |
||||
avatarUrl, |
||||
emoji, |
||||
scriptEnabled, |
||||
script, |
||||
} = formValues; |
||||
|
||||
const { |
||||
handleEnabled, |
||||
handleChannel, |
||||
handleUsername, |
||||
handleName, |
||||
handleAlias, |
||||
handleAvatarUrl, |
||||
handleEmoji, |
||||
handleScriptEnabled, |
||||
handleScript, |
||||
} = formHandlers; |
||||
|
||||
const url = absoluteUrl(`hooks/${ extraData._id }/${ extraData.token }`); |
||||
|
||||
const [exampleData, curlData] = useExampleData({ |
||||
aditionalFields: { |
||||
...alias && { alias }, |
||||
...emoji && { emoji }, |
||||
...avatarUrl && { avatar: avatarUrl }, |
||||
}, |
||||
url, |
||||
}, [alias, emoji, avatarUrl]); |
||||
|
||||
const hilightedExampleJson = hilightCode('json', JSON.stringify(exampleData, null, 2)); |
||||
|
||||
return <Page.ScrollableContent pb='x24' mi='neg-x24' is='form' qa-admin-user-edit='form' { ...props }> |
||||
<Margins block='x16'> |
||||
<FieldGroup width='x600' alignSelf='center'> |
||||
{useMemo(() => <Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Enabled')} |
||||
<ToggleSwitch checked={enabled} onChange={handleEnabled} /> |
||||
</Field.Label> |
||||
</Field>, [enabled, handleEnabled])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{t('Name_optional')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={name} onChange={handleName}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('You_should_name_it_to_easily_manage_your_integrations')}</Field.Hint> |
||||
</Field>, [name, handleName])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{t('Post_to_Channel')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={channel} onChange={handleChannel} addon={<Icon name='at' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here')}</Field.Hint> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', '@', '#', '@john', '#general') }} /> |
||||
</Field>, [channel, handleChannel])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{t('Post_as')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={username} onChange={handleUsername} addon={<Icon name='user' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Choose_the_username_that_this_integration_will_post_as')}</Field.Hint> |
||||
<Field.Hint>{t('Should_exists_a_user_with_this_username')}</Field.Hint> |
||||
</Field>, [username, handleUsername])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Alias') } (${ t('optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={alias} onChange={handleAlias} addon={<Icon name='edit' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Choose_the_alias_that_will_appear_before_the_username_in_messages')}</Field.Hint> |
||||
</Field>, [alias, handleAlias])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Avatar_URL') } (${ t('optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={avatarUrl} onChange={handleAvatarUrl} addon={<Icon name='user-rounded' size='x20' alignSelf='center'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('You_can_change_a_different_avatar_too')}</Field.Hint> |
||||
<Field.Hint>{t('Should_be_a_URL_of_an_image')}</Field.Hint> |
||||
</Field>, [avatarUrl, handleAvatarUrl])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Emoji') } (${ t('optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={emoji} onChange={handleEmoji} addon={<Icon name='emoji' size='x20' alignSelf='center'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('You_can_use_an_emoji_as_avatar')}</Field.Hint> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Example_s', ':ghost:') }} /> |
||||
</Field>, [emoji, handleEmoji])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Script_Enabled')} |
||||
<ToggleSwitch checked={scriptEnabled} onChange={handleScriptEnabled} /> |
||||
</Field.Label> |
||||
</Field>, [scriptEnabled, handleScriptEnabled])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{t('Script')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={10} flexGrow={1} value={script} onChange={handleScript} addon={<Icon name='code' size='x20' alignSelf='center'/>}/> |
||||
</Field.Row> |
||||
</Field>, [script, handleScript])} |
||||
{useMemo(() => !extraData._id && <><Field> |
||||
<Field.Label>{t('Webhook_URL')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={t('Will_be_available_here_after_saving')} addon={<Icon name='permalink' size='x20'/>} disabled/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Send_your_JSON_payloads_to_this_URL')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Token')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={t('Will_be_available_here_after_saving')} addon={<Icon name='key' size='x20'/>} disabled/> |
||||
</Field.Row> |
||||
</Field></>, [extraData._id])} |
||||
{useMemo(() => extraData._id && <><Field> |
||||
<Field.Label>{t('Webhook_URL')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={url} addon={<Icon name='permalink' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Send_your_JSON_payloads_to_this_URL')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Token')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={`${ extraData._id }/${ extraData.token }`} addon={<Icon name='key' size='x20'/>}/> |
||||
</Field.Row> |
||||
</Field></>, [url, extraData._id, extraData.token])} |
||||
{useMemo(() => <Field> |
||||
<Field.Label>{t('Example_payload')}</Field.Label> |
||||
<Field.Row> |
||||
<Box fontScale='p1' withRichContent flexGrow={1}> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightedExampleJson }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>, [hilightedExampleJson])} |
||||
{useMemo(() => extraData._id && <Field> |
||||
<Field.Label>{t('Curl')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={curlData} addon={<Icon name='code' size='x20'/>}/> |
||||
</Field.Row> |
||||
</Field>, [curlData])} |
||||
{ append } |
||||
</FieldGroup> |
||||
</Margins> |
||||
</Page.ScrollableContent>; |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
import { Button, ButtonGroup, Icon, Tabs } from '@rocket.chat/fuselage'; |
||||
import React, { useEffect, useCallback } from 'react'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useRoute, useRouteParameter } from '../../contexts/RouterContext'; |
||||
import IntegrationsTable from './IntegrationsTable'; |
||||
import NewZapier from './new/NewZapier'; |
||||
import NewBot from './new/NewBot'; |
||||
|
||||
function IntegrationsPage() { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const handleNewButtonClick = useCallback(() => { |
||||
router.push({ context: 'new', type: 'incoming' }); |
||||
}, []); |
||||
|
||||
const context = useRouteParameter('context'); |
||||
useEffect(() => { |
||||
if (!context) { |
||||
router.push({ context: 'webhook-incoming' }); |
||||
} |
||||
}, [context]); |
||||
|
||||
const showTable = !['zapier', 'bots'].includes(context); |
||||
|
||||
const goToIncoming = useCallback(() => router.push({ context: 'webhook-incoming' }), []); |
||||
const goToOutgoing = useCallback(() => router.push({ context: 'webhook-outgoing' }), []); |
||||
const goToZapier = useCallback(() => router.push({ context: 'zapier' }), []); |
||||
const goToBots = useCallback(() => router.push({ context: 'bots' }), []); |
||||
|
||||
return <Page flexDirection='column'> |
||||
<Page.Header title={t('Integrations')}> |
||||
<ButtonGroup> |
||||
<Button onClick={handleNewButtonClick}> |
||||
<Icon name='plus'/> {t('New')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Tabs> |
||||
<Tabs.Item selected={context === 'webhook-incoming'} onClick={goToIncoming}>{t('Incoming')}</Tabs.Item> |
||||
<Tabs.Item selected={context === 'webhook-outgoing'} onClick={goToOutgoing}>{t('Outgoing')}</Tabs.Item> |
||||
<Tabs.Item selected={context === 'zapier'} onClick={goToZapier}>{t('Zapier')}</Tabs.Item> |
||||
<Tabs.Item selected={context === 'bots'} onClick={goToBots}>{t('Bots')}</Tabs.Item> |
||||
</Tabs> |
||||
<Page.Content> |
||||
{context === 'zapier' && <NewZapier />} |
||||
{context === 'bots' && <NewBot />} |
||||
{showTable && <IntegrationsTable type={context}/>} |
||||
</Page.Content> |
||||
</Page>; |
||||
} |
||||
|
||||
export default IntegrationsPage; |
||||
@ -0,0 +1,40 @@ |
||||
import React from 'react'; |
||||
|
||||
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext'; |
||||
import { useRouteParameter } from '../../contexts/RouterContext'; |
||||
import NotAuthorizedPage from '../NotAuthorizedPage'; |
||||
import IntegrationsPage from './IntegrationsPage'; |
||||
import NewIntegrationsPage from './new/NewIntegrationsPage'; |
||||
import EditIntegrationsPage from './edit/EditIntegrationsPage'; |
||||
import OutgoingWebhookHistoryPage from './edit/OutgoingWebhookHistoryPage'; |
||||
|
||||
function IntegrationsRoute() { |
||||
const canViewIntegrationsPage = useAtLeastOnePermission([ |
||||
'manage-incoming-integrations', |
||||
'manage-outgoing-integrations', |
||||
'manage-own-incoming-integrations', |
||||
'manage-own-outgoing-integrations', |
||||
]); |
||||
|
||||
const context = useRouteParameter('context'); |
||||
|
||||
if (!canViewIntegrationsPage) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
if (context === 'new') { |
||||
return <NewIntegrationsPage />; |
||||
} |
||||
|
||||
if (context === 'edit') { |
||||
return <EditIntegrationsPage />; |
||||
} |
||||
|
||||
if (context === 'history') { |
||||
return <OutgoingWebhookHistoryPage />; |
||||
} |
||||
|
||||
return <IntegrationsPage />; |
||||
} |
||||
|
||||
export default IntegrationsRoute; |
||||
@ -0,0 +1,97 @@ |
||||
import { Box, Table, TextInput, Icon } from '@rocket.chat/fuselage'; |
||||
import { useDebouncedValue, useResizeObserver } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useMemo, useCallback, useState, useEffect } from 'react'; |
||||
|
||||
import { GenericTable, Th } from '../../../app/ui/client/components/GenericTable'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; |
||||
|
||||
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; |
||||
|
||||
const FilterByTypeAndText = React.memo(({ setFilter, ...props }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const [text, setText] = useState(''); |
||||
|
||||
const handleChange = useCallback((event) => setText(event.currentTarget.value), []); |
||||
|
||||
useEffect(() => { |
||||
setFilter({ text }); |
||||
}, [text]); |
||||
|
||||
return <Box mb='x16' is='form' display='flex' flexDirection='column' {...props}> |
||||
<TextInput placeholder={t('Search_Integrations')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} /> |
||||
</Box>; |
||||
}); |
||||
|
||||
const useQuery = (params, sort) => useMemo(() => ({ |
||||
query: JSON.stringify({ name: { $regex: params.text || '', $options: 'i' }, type: params.type }), |
||||
sort: JSON.stringify({ [sort[0]]: sort[1] === 'asc' ? 1 : -1 }), |
||||
...params.itemsPerPage && { count: params.itemsPerPage }, |
||||
...params.current && { offset: params.current }, |
||||
}), [JSON.stringify(params), JSON.stringify(sort)]); |
||||
|
||||
const useResizeInlineBreakpoint = (sizes = [], debounceDelay = 0) => { |
||||
const { ref, borderBoxSize } = useResizeObserver({ debounceDelay }); |
||||
const inlineSize = borderBoxSize ? borderBoxSize.inlineSize : 0; |
||||
sizes = useMemo(() => sizes.map((current) => (inlineSize ? inlineSize > current : true)), [inlineSize]); |
||||
return [ref, ...sizes]; |
||||
}; |
||||
|
||||
export function IntegrationsTable({ type }) { |
||||
const t = useTranslation(); |
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
const [ref, isBig] = useResizeInlineBreakpoint([700], 200); |
||||
|
||||
const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 }); |
||||
const [sort, setSort] = useState(['name', 'asc']); |
||||
|
||||
const debouncedText = useDebouncedValue(params.text, 500); |
||||
const debouncedSort = useDebouncedValue(sort, 500); |
||||
const query = useQuery({ ...params, text: debouncedText, type }, debouncedSort); |
||||
|
||||
const { data } = useEndpointDataExperimental('integrations.list', query); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const onClick = (_id, type) => () => router.push({ |
||||
context: 'edit', |
||||
type: type === 'webhook-incoming' ? 'incoming' : 'outgoing', |
||||
id: _id, |
||||
}); |
||||
|
||||
const onHeaderClick = useCallback((id) => { |
||||
const [sortBy, sortDirection] = sort; |
||||
|
||||
if (sortBy === id) { |
||||
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']); |
||||
return; |
||||
} |
||||
setSort([id, 'asc']); |
||||
}, [sort]); |
||||
|
||||
const header = useMemo(() => [ |
||||
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w={isBig ? 'x280' : 'x240'}>{t('Name')}</Th>, |
||||
<Th key={'channel'} direction={sort[1]} active={sort[0] === 'channel'} onClick={onHeaderClick} sort='channel'>{t('Post_to')}</Th>, |
||||
<Th key={'_createdBy'} direction={sort[1]} active={sort[0] === '_createdBy'} onClick={onHeaderClick} sort='_createdBy'>{t('Created_by')}</Th>, |
||||
isBig && <Th key={'_createdAt'} direction={sort[1]} active={sort[0] === '_createdAt'} onClick={onHeaderClick} sort='_createdAt'>{t('Created_at')}</Th>, |
||||
<Th key={'username'} direction={sort[1]} active={sort[0] === 'username'} onClick={onHeaderClick} sort='username'>{t('Post_as')}</Th>, |
||||
].filter(Boolean), [sort, isBig]); |
||||
|
||||
const renderRow = useCallback(({ name, _id, type, username, _createdAt, _createdBy: { username: createdBy }, channel }) => { |
||||
const handler = useMemo(() => onClick(_id, type), []); |
||||
return <Table.Row key={_id} onKeyDown={handler} onClick={handler} tabIndex={0} role='link' action> |
||||
<Table.Cell style={style} color='default' fontScale='p2'>{name}</Table.Cell> |
||||
<Table.Cell style={style}>{channel.join(', ')}</Table.Cell> |
||||
<Table.Cell style={style}>{createdBy}</Table.Cell> |
||||
{isBig && <Table.Cell style={style}>{formatDateAndTime(_createdAt)}</Table.Cell>} |
||||
<Table.Cell style={style}>{username}</Table.Cell> |
||||
</Table.Row>; |
||||
}, []); |
||||
|
||||
return <GenericTable ref={ref} FilterComponent={FilterByTypeAndText} header={header} renderRow={renderRow} results={data && data.integrations} total={data && data.total} setParams={setParams} params={params} />; |
||||
} |
||||
|
||||
export default IntegrationsTable; |
||||
@ -0,0 +1,264 @@ |
||||
import { |
||||
Field, |
||||
TextInput, |
||||
Box, |
||||
ToggleSwitch, |
||||
Icon, |
||||
TextAreaInput, |
||||
FieldGroup, |
||||
Margins, |
||||
Select, |
||||
Accordion, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { useHilightCode } from '../../hooks/useHilightCode'; |
||||
import { useExampleData } from './exampleIncomingData'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import Page from '../../components/basic/Page'; |
||||
import { integrations as eventList } from '../../../app/integrations/lib/rocketchat'; |
||||
|
||||
|
||||
export default function OutgoingWebhookForm({ formValues, formHandlers, append, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
enabled, |
||||
impersonateUser, |
||||
event, |
||||
urls, |
||||
triggerWords, |
||||
targetRoom, |
||||
channel, |
||||
username, |
||||
name, |
||||
alias, |
||||
avatar: avatarUrl, |
||||
emoji, |
||||
token, |
||||
scriptEnabled, |
||||
script, |
||||
retryFailedCalls, |
||||
retryCount, |
||||
retryDelay, |
||||
triggerWordAnywhere, |
||||
} = formValues; |
||||
|
||||
const { |
||||
runOnEdits, |
||||
handleEvent, |
||||
handleEnabled, |
||||
handleName, |
||||
handleChannel, |
||||
handleTriggerWords, |
||||
handleTargetRoom, |
||||
handleUrls, |
||||
handleImpersonateUser, |
||||
handleUsername, |
||||
handleAlias, |
||||
handleAvatar, |
||||
handleEmoji, |
||||
handleToken, |
||||
handleScriptEnabled, |
||||
handleScript, |
||||
handleRetryFailedCalls, |
||||
handleRetryCount, |
||||
handleRetryDelay, |
||||
handleTriggerWordAnywhere, |
||||
handleRunOnEdits, |
||||
} = formHandlers; |
||||
|
||||
const retryDelayOptions = useMemo(() => [ |
||||
['powers-of-ten', t('powers-of-ten')], |
||||
['powers-of-two', t('powers-of-two')], |
||||
['increments-of-two', t('increments-of-two')], |
||||
], []); |
||||
|
||||
const { outgoingEvents } = eventList; |
||||
|
||||
const eventOptions = useMemo(() => Object.entries(outgoingEvents).map(([key, val]) => [key, t(val.label)]), []); |
||||
|
||||
const hilightCode = useHilightCode(); |
||||
|
||||
const showChannel = useMemo(() => outgoingEvents[event].use.channel, [event]); |
||||
const showTriggerWords = useMemo(() => outgoingEvents[event].use.triggerWords, [event]); |
||||
const showTargetRoom = useMemo(() => outgoingEvents[event].use.targetRoom, [event]); |
||||
|
||||
const [exampleData] = useExampleData({ |
||||
aditionalFields: { |
||||
...alias && { alias }, |
||||
...emoji && { emoji }, |
||||
...avatarUrl && { avatar: avatarUrl }, |
||||
}, |
||||
url: null, |
||||
}, [alias, emoji, avatarUrl]); |
||||
|
||||
const hilightedExampleJson = hilightCode('json', JSON.stringify(exampleData, null, 2)); |
||||
|
||||
return <Page.ScrollableContent pb='x24' mi='neg-x24' is='form' qa-admin-user-edit='form' { ...props }> |
||||
<Margins block='x16'> |
||||
<Accordion width='x600' alignSelf='center' > |
||||
<FieldGroup> |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Event_Trigger')}</Field.Label> |
||||
<Field.Row> |
||||
<Select flexGrow={1} value={event} options={eventOptions} onChange={handleEvent}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Event_Trigger_Description')}</Field.Hint> |
||||
</Field>, [event]) } |
||||
{ useMemo(() => <Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Enabled')} |
||||
<ToggleSwitch checked={enabled} onChange={handleEnabled} /> |
||||
</Field.Label> |
||||
</Field>, [enabled]) } |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Name_optional')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={name} onChange={handleName}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('You_should_name_it_to_easily_manage_your_integrations')}</Field.Hint> |
||||
</Field>, [name])} |
||||
{ useMemo(() => showChannel && <Field> |
||||
<Field.Label>{t('Channel')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={channel} onChange={handleChannel} addon={<Icon name='at' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Channel_to_listen_on')}</Field.Hint> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', '@', '#', '@john', '#general') }} /> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Integrations_for_all_channels') }} /> |
||||
</Field>, [showChannel, channel])} |
||||
{ useMemo(() => showTriggerWords && <Field> |
||||
<Field.Label>{t('Trigger_Words')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={triggerWords} onChange={handleTriggerWords}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('When_a_line_starts_with_one_of_there_words_post_to_the_URLs_below')}</Field.Hint> |
||||
<Field.Hint>{t('Separate_multiple_words_with_commas')}</Field.Hint> |
||||
</Field>, [triggerWords])} |
||||
{ useMemo(() => showTargetRoom && <Field> |
||||
<Field.Label>{t('TargetRoom')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={targetRoom} onChange={handleTargetRoom}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('TargetRoom_Description')}</Field.Hint> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', '@', '#', '@john', '#general') }} /> |
||||
</Field>, [showTargetRoom, targetRoom])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('URLs')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={10} flexGrow={1} value={urls} onChange={handleUrls} addon={<Icon name='permalink' size='x20'/>}/> |
||||
</Field.Row> |
||||
</Field>, [urls])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Impersonate_user')} |
||||
<ToggleSwitch checked={impersonateUser} onChange={handleImpersonateUser} /> |
||||
</Field.Label> |
||||
</Field>, [impersonateUser])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Post_as')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={username} onChange={handleUsername} addon={<Icon name='user' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Choose_the_username_that_this_integration_will_post_as')}</Field.Hint> |
||||
<Field.Hint>{t('Should_exists_a_user_with_this_username')}</Field.Hint> |
||||
</Field>, [username])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Alias') } (${ t('optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={alias} onChange={handleAlias} addon={<Icon name='edit' size='x20'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Choose_the_alias_that_will_appear_before_the_username_in_messages')}</Field.Hint> |
||||
</Field>, [alias])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Avatar_URL') } (${ t('optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={avatarUrl} onChange={handleAvatar} addon={<Icon name='user-rounded' size='x20' alignSelf='center'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('You_can_change_a_different_avatar_too')}</Field.Hint> |
||||
<Field.Hint>{t('Should_be_a_URL_of_an_image')}</Field.Hint> |
||||
</Field>, [avatarUrl])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Emoji') } (${ t('optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={emoji} onChange={handleEmoji} addon={<Icon name='emoji' size='x20' alignSelf='center'/>}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('You_can_use_an_emoji_as_avatar')}</Field.Hint> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Example_s', ':ghost:') }} /> |
||||
</Field>, [emoji])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{`${ t('Token') } (${ t('Optional') })`}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={token} onChange={handleToken} addon={<Icon name='key' size='x20'/>}/> |
||||
</Field.Row> |
||||
</Field>, [token])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Script_Enabled')} |
||||
<ToggleSwitch checked={scriptEnabled} onChange={handleScriptEnabled} /> |
||||
</Field.Label> |
||||
</Field>, [scriptEnabled])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Script')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={10} flexGrow={1} value={script} onChange={handleScript} addon={<Icon name='code' size='x20' alignSelf='center'/>}/> |
||||
</Field.Row> |
||||
</Field>, [script])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Responding')}</Field.Label> |
||||
<Field.Hint>{t('Response_description_pre')}</Field.Hint> |
||||
<Field.Row> |
||||
<Box fontScale='p1' withRichContent flexGrow={1}> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightedExampleJson }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Response_description_post')}</Field.Hint> |
||||
</Field>, [hilightedExampleJson])} |
||||
</FieldGroup> |
||||
<Accordion.Item title={t('Integration_Advanced_Settings')}> |
||||
<FieldGroup> |
||||
{ useMemo(() => <Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Integration_Retry_Failed_Url_Calls')} |
||||
<ToggleSwitch checked={retryFailedCalls} onChange={handleRetryFailedCalls} /> |
||||
</Field.Label> |
||||
<Field.Hint>{t('Integration_Retry_Failed_Url_Calls_Description')}</Field.Hint> |
||||
</Field>, [retryFailedCalls])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Retry_Count')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={retryCount} onChange={handleRetryCount}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Integration_Retry_Count_Description')}</Field.Hint> |
||||
</Field>, [retryCount])} |
||||
{ useMemo(() => <Field> |
||||
<Field.Label>{t('Integration_Retry_Delay')}</Field.Label> |
||||
<Field.Row> |
||||
<Select flexGrow={1} value={retryDelay} options={retryDelayOptions} onChange={handleRetryDelay}/> |
||||
</Field.Row> |
||||
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Integration_Retry_Delay_Description') }}/> |
||||
</Field>, [retryDelay])} |
||||
{ useMemo(() => event === 'sendMessage' && <FieldGroup> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Integration_Word_Trigger_Placement')} |
||||
<ToggleSwitch checked={triggerWordAnywhere} onChange={handleTriggerWordAnywhere} /> |
||||
</Field.Label> |
||||
<Field.Hint>{t('Integration_Word_Trigger_Placement_Description')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Integration_Word_Trigger_Placement')} |
||||
<ToggleSwitch checked={runOnEdits} onChange={handleRunOnEdits} /> |
||||
</Field.Label> |
||||
<Field.Hint>{t('Integration_Run_When_Message_Is_Edited_Description')}</Field.Hint> |
||||
</Field> |
||||
</FieldGroup>, [triggerWordAnywhere, runOnEdits])} |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
{ append } |
||||
</Accordion> |
||||
</Margins> |
||||
</Page.ScrollableContent>; |
||||
} |
||||
@ -0,0 +1,105 @@ |
||||
import React, { useMemo, useState } from 'react'; |
||||
import { Field, Box, Headline, Skeleton, Margins, Button } from '@rocket.chat/fuselage'; |
||||
|
||||
import { SuccessModal, DeleteWarningModal } from './EditIntegrationsPage'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; |
||||
import { useMethod } from '../../../contexts/ServerContext'; |
||||
import { useEndpointAction } from '../../../hooks/useEndpointAction'; |
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
import { useForm } from '../../../hooks/useForm'; |
||||
import IncomingWebhookForm from '../IncomingWebhookForm'; |
||||
|
||||
export default function EditIncomingWebhookWithData({ integrationId, ...props }) { |
||||
const t = useTranslation(); |
||||
const [cache, setCache] = useState(); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental('integrations.get', useMemo(() => ({ integrationId }), [integrationId, cache])); |
||||
|
||||
const onChange = () => setCache(new Date()); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box w='full' pb='x24' {...props}> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8' /> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
</Box>; |
||||
} |
||||
|
||||
if (error) { |
||||
return <Box mbs='x16' {...props}>{t('Oops_page_not_found')}</Box>; |
||||
} |
||||
|
||||
return <EditIncomingWebhook data={data.integration} onChange={onChange} {...props}/>; |
||||
} |
||||
|
||||
const getInitialValue = (data) => { |
||||
const initialValue = { |
||||
enabled: data.enabled, |
||||
channel: data.channel.join(', ') ?? '', |
||||
username: data.username ?? '', |
||||
name: data.name ?? '', |
||||
alias: data.alias ?? '', |
||||
avatarUrl: data.avatarUrl ?? '', |
||||
emoji: data.emoji ?? '', |
||||
scriptEnabled: data.scriptEnabled, |
||||
script: data.script, |
||||
}; |
||||
return initialValue; |
||||
}; |
||||
|
||||
function EditIncomingWebhook({ data, onChange, ...props }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const { values: formValues, handlers: formHandlers, reset } = useForm(getInitialValue(data)); |
||||
const [modal, setModal] = useState(); |
||||
|
||||
const deleteQuery = useMemo(() => ({ type: 'webhook-incoming', integrationId: data._id }), [data._id]); |
||||
const deleteIntegration = useEndpointAction('POST', 'integrations.remove', deleteQuery); |
||||
const saveIntegration = useMethod('updateIncomingIntegration'); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const handleDeleteIntegration = () => { |
||||
const closeModal = () => setModal(); |
||||
const onDelete = async () => { |
||||
const result = await deleteIntegration(); |
||||
if (result.success) { setModal(<SuccessModal onClose={() => { closeModal(); router.push({}); }}/>); } |
||||
}; |
||||
|
||||
setModal(<DeleteWarningModal onDelete={onDelete} onCancel={closeModal} />); |
||||
}; |
||||
|
||||
const handleSave = async () => { |
||||
try { |
||||
await saveIntegration(data._id, { ...formValues }); |
||||
dispatchToastMessage({ type: 'success', message: t('Integration_updated') }); |
||||
onChange(); |
||||
} catch (e) { |
||||
dispatchToastMessage({ type: 'error', message: e }); |
||||
} |
||||
}; |
||||
|
||||
const actionButtons = useMemo(() => <Field> |
||||
<Field.Row display='flex' flexDirection='column'> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'> |
||||
<Margins inlineEnd='x4'> |
||||
<Button flexGrow={1} type='reset' onClick={reset}>{t('Reset')}</Button> |
||||
<Button mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button> |
||||
</Margins> |
||||
</Box> |
||||
<Button mbs='x4' primary danger w='full' onClick={handleDeleteIntegration} >{t('Delete')}</Button> |
||||
</Field.Row> |
||||
</Field>); |
||||
|
||||
|
||||
return <> |
||||
<IncomingWebhookForm formHandlers={formHandlers} formValues={formValues} extraData={{ _id: data._id, token: data.token }} append={actionButtons} {...props}/> |
||||
{ modal } |
||||
</>; |
||||
} |
||||
@ -0,0 +1,82 @@ |
||||
import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; |
||||
import React, { useCallback } from 'react'; |
||||
|
||||
import Page from '../../../components/basic/Page'; |
||||
import EditIncomingWebhookWithData from './EditIncomingWebhook'; |
||||
import EditOutgoingWebhookWithData from './EditOutgoingWebhook'; |
||||
import { Modal } from '../../../components/basic/Modal'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useRouteParameter, useRoute } from '../../../contexts/RouterContext'; |
||||
|
||||
export const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{t('Integration_Delete_Warning')} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary danger onClick={onDelete}>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export const SuccessModal = ({ onClose, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='success' name='checkmark-circled' size={20}/> |
||||
<Modal.Title>{t('Deleted')}</Modal.Title> |
||||
<Modal.Close onClick={onClose}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{t('Your_entry_has_been_deleted')} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button primary onClick={onClose}>{t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default function NewIntegrationsPage({ ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const type = useRouteParameter('type'); |
||||
const integrationId = useRouteParameter('id'); |
||||
|
||||
const handleClickReturn = useCallback(() => { |
||||
router.push({ }); |
||||
}, []); |
||||
|
||||
const handleClickHistory = useCallback(() => { |
||||
router.push({ context: 'history', type: 'outgoing', id: integrationId }); |
||||
}, [integrationId]); |
||||
|
||||
return <Page flexDirection='column' {...props}> |
||||
<Page.Header title={type === 'incoming' ? t('Integration_Incoming_WebHook') : t('Integration_Outgoing_WebHook')} > |
||||
<ButtonGroup> |
||||
<Button onClick={handleClickReturn}> |
||||
<Icon name='back' size='x16'/> {t('Back')} |
||||
</Button> |
||||
{type === 'outgoing' && <Button onClick={handleClickHistory}>{t('History')}</Button>} |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
{ |
||||
(type === 'outgoing' && <EditOutgoingWebhookWithData integrationId={integrationId} key='outgoing'/>) |
||||
|| (type === 'incoming' && <EditIncomingWebhookWithData integrationId={integrationId} key='incoming'/>) |
||||
} |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
@ -0,0 +1,134 @@ |
||||
import React, { useMemo, useState } from 'react'; |
||||
import { |
||||
Field, |
||||
Box, |
||||
Headline, |
||||
Skeleton, |
||||
Margins, |
||||
Button, |
||||
} from '@rocket.chat/fuselage'; |
||||
|
||||
import { SuccessModal, DeleteWarningModal } from './EditIntegrationsPage'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; |
||||
import { useEndpointAction } from '../../../hooks/useEndpointAction'; |
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useMethod } from '../../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
import OutgoingWebhookForm from '../OutgoiongWebhookForm'; |
||||
import { useForm } from '../../../hooks/useForm'; |
||||
|
||||
export default function EditOutgoingWebhookWithData({ integrationId, ...props }) { |
||||
const t = useTranslation(); |
||||
const [cache, setCache] = useState(); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental('integrations.get', useMemo(() => ({ integrationId }), [integrationId, cache])); |
||||
|
||||
const onChange = () => setCache(new Date()); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box w='full' pb='x24' {...props}> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8' /> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
</Box>; |
||||
} |
||||
|
||||
if (error) { |
||||
return <Box mbs='x16' {...props}>{t('Oops_page_not_found')}</Box>; |
||||
} |
||||
|
||||
return <EditOutgoingWebhook data={data.integration} onChange={onChange} {...props}/>; |
||||
} |
||||
|
||||
const getInitialValue = (data) => { |
||||
const initialValue = { |
||||
enabled: data.enabled ?? true, |
||||
impersonateUser: data.impersonateUser, |
||||
event: data.event, |
||||
token: data.token, |
||||
urls: data.urls.join('\n') ?? '', |
||||
triggerWords: data.triggerWords?.join('; ') ?? '', |
||||
targetRoom: data.targetRoom ?? '', |
||||
channel: data.channel.join(', ') ?? '', |
||||
username: data.username ?? '', |
||||
name: data.name ?? '', |
||||
alias: data.alias ?? '', |
||||
avatarUrl: data.avatarUrl ?? '', |
||||
emoji: data.emoji ?? '', |
||||
scriptEnabled: data.scriptEnabled ?? false, |
||||
script: data.script ?? '', |
||||
retryFailedCalls: data.retryFailedCalls ?? true, |
||||
retryCount: data.retryCount ?? 5, |
||||
retryDelay: data.retryDelay ?? 'power-of-ten', |
||||
triggerrWordAnywhere: data.triggerrWordAnywhere ?? false, |
||||
runOnEdits: data.runOnEdits ?? true, |
||||
}; |
||||
return initialValue; |
||||
}; |
||||
|
||||
function EditOutgoingWebhook({ data, onChange, setSaveAction, ...props }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const { handlers: formHandlers, values: formValues, reset } = useForm(getInitialValue(data)); |
||||
const [modal, setModal] = useState(); |
||||
|
||||
const saveIntegration = useMethod('updateOutgoingIntegration'); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const deleteQuery = useMemo(() => ({ type: 'webhook-outgoing', integrationId: data._id }), [data._id]); |
||||
const deleteIntegration = useEndpointAction('POST', 'integrations.remove', deleteQuery); |
||||
|
||||
const handleDeleteIntegration = () => { |
||||
const closeModal = () => setModal(); |
||||
const onDelete = async () => { |
||||
const result = await deleteIntegration(); |
||||
if (result.success) { setModal(<SuccessModal onClose={() => { closeModal(); router.push({}); }}/>); } |
||||
}; |
||||
|
||||
setModal(<DeleteWarningModal onDelete={onDelete} onCancel={closeModal} />); |
||||
}; |
||||
|
||||
const { |
||||
urls, |
||||
triggerWords, |
||||
} = formValues; |
||||
|
||||
const handleSave = async () => { |
||||
try { |
||||
await saveIntegration(data._id, { |
||||
...formValues, |
||||
triggerWords: triggerWords.split(';'), |
||||
urls: urls.split('\n'), |
||||
}); |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Integration_updated') }); |
||||
onChange(); |
||||
} catch (e) { |
||||
dispatchToastMessage({ type: 'error', message: e }); |
||||
} |
||||
}; |
||||
|
||||
const actionButtons = useMemo(() => <Field> |
||||
<Field.Row display='flex' flexDirection='column'> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'> |
||||
<Margins inlineEnd='x4'> |
||||
<Button flexGrow={1} type='reset' onClick={reset}>{t('Reset')}</Button> |
||||
<Button mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button> |
||||
</Margins> |
||||
</Box> |
||||
<Button mbs='x4' primary danger w='full' onClick={handleDeleteIntegration}>{t('Delete')}</Button> |
||||
</Field.Row> |
||||
</Field>); |
||||
|
||||
|
||||
return <> |
||||
<OutgoingWebhookForm formValues={formValues} formHandlers={formHandlers} append={actionButtons} {...props}/> |
||||
{ modal } |
||||
</>; |
||||
} |
||||
@ -0,0 +1,268 @@ |
||||
import { Button, ButtonGroup, Icon, Headline, Skeleton, Box, Accordion, Field, FieldGroup, Pagination } from '@rocket.chat/fuselage'; |
||||
import React, { useMemo, useCallback, useState, useEffect } from 'react'; |
||||
|
||||
import Page from '../../../components/basic/Page'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useHilightCode } from '../../../hooks/useHilightCode'; |
||||
import { integrations as eventList } from '../../../../app/integrations/lib/rocketchat'; |
||||
import { useMethod } from '../../../contexts/ServerContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; |
||||
import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; |
||||
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
|
||||
function HistoryItem({ data, onChange, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const hilightCode = useHilightCode(); |
||||
|
||||
const replayOutgoingIntegration = useMethod('replayOutgoingIntegration'); |
||||
|
||||
const { |
||||
_id, |
||||
_createdAt, |
||||
_updatedAt, |
||||
httpResult, |
||||
event, |
||||
step, |
||||
httpCallData, |
||||
data: dataSentToTrigger, |
||||
prepareSentMessage, |
||||
processSentMessage, |
||||
url, |
||||
httpError, |
||||
errorStack, |
||||
error, |
||||
integration: { _id: integrationId }, |
||||
} = data; |
||||
|
||||
const handleClickReplay = useCallback((e) => { |
||||
e.stopPropagation(); |
||||
replayOutgoingIntegration({ integrationId, historyId: _id }); |
||||
onChange(); |
||||
}, [_id]); |
||||
|
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
|
||||
return <Accordion.Item |
||||
title={ |
||||
<Box display='inline-flex' w='full' flexDirection='row' justifyContent='space-between'> |
||||
<Box display='flex' flexDirection='row' alignItems='center'> |
||||
<Icon name='info-circled' size='x16' mie='x4'/>{formatDateAndTime(_createdAt)} |
||||
</Box> |
||||
<Button ghost onClick={handleClickReplay}>{t('Replay')}</Button> |
||||
</Box> |
||||
} |
||||
{...props} |
||||
> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Field.Label>{t('Status')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<code>{error ? t('Failure') : t('Success')}</code> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Time_Triggered')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<code>{_createdAt}</code> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Time_Ended_Or_Error')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<code>{_updatedAt}</code> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Event_Trigger')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<code>{t(eventList.outgoingEvents[event].label)}</code> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Trigger_Step')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<code>{step}</code> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(dataSentToTrigger, null, 2)) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
{prepareSentMessage && <Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(prepareSentMessage, null, 2)) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
{processSentMessage && <Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(processSentMessage, null, 2)) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
{url && <Field> |
||||
<Field.Label>{t('URL')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<code>{url}</code> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
{httpCallData && <Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Data_Passed_To_URL')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(httpCallData, null, 2)) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
{httpError && <Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Http_Response_Error')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(httpError, null, 2)) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
{httpResult && <Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Http_Response')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', httpResult) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
{errorStack && <Field> |
||||
<Field.Label>{t('Integration_Outgoing_WebHook_History_Error_Stacktrace')}</Field.Label> |
||||
<Field.Row> |
||||
<Box withRichContent w='full'> |
||||
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(errorStack, null, 2)) }}></code></pre> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>} |
||||
</FieldGroup> |
||||
</Accordion.Item>; |
||||
} |
||||
|
||||
function HistoryContent({ data, state, onChange, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const [loadedData, setLoadedData] = useState(); |
||||
useEffect(() => { |
||||
if (state === ENDPOINT_STATES.DONE) { setLoadedData(data); } |
||||
}, [state]); |
||||
|
||||
if (!loadedData || state === ENDPOINT_STATES.LOADING) { |
||||
return <Box w='full' pb='x24' {...props}> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8' /> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
</Box>; |
||||
} |
||||
|
||||
if (loadedData.history.length < 1) { |
||||
return <Box mbs='x16' {...props}>{t('Integration_Outgoing_WebHook_No_History')}</Box>; |
||||
} |
||||
|
||||
return <> |
||||
<Accordion w='full' maxWidth='x600' alignSelf='center' key='content'> |
||||
{loadedData.history.map((current) => <HistoryItem |
||||
data={current} |
||||
key={current._id} |
||||
onChange={onChange} |
||||
/>)} |
||||
</Accordion> |
||||
</>; |
||||
} |
||||
|
||||
function OutgoingWebhookHistoryPage(props) { |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const t = useTranslation(); |
||||
|
||||
const [cache, setCache] = useState(); |
||||
const [current, setCurrent] = useState(); |
||||
const [itemsPerPage, setItemsPerPage] = useState(); |
||||
const onChange = useCallback(() => { |
||||
setCache(new Date()); |
||||
}); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const clearHistory = useMethod('clearIntegrationHistory'); |
||||
|
||||
const handleClearHistory = async () => { |
||||
try { |
||||
await clearHistory(); |
||||
dispatchToastMessage({ type: 'success', message: t('Integration_History_Cleared') }); |
||||
onChange(); |
||||
} catch (e) { |
||||
dispatchToastMessage({ type: 'error', message: e }); |
||||
} |
||||
}; |
||||
|
||||
const handleClickReturn = () => { |
||||
router.push({ }); |
||||
}; |
||||
|
||||
const id = useRouteParameter('id'); |
||||
|
||||
const query = useMemo(() => ({ |
||||
id, |
||||
cout: itemsPerPage, |
||||
offset: current, |
||||
}), [id, itemsPerPage, current, cache]); |
||||
|
||||
const { data, state } = useEndpointDataExperimental('integrations.history', query); |
||||
|
||||
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), []); |
||||
|
||||
return <Page flexDirection='column' {...props}> |
||||
<Page.Header title={t('Integration_Outgoing_WebHook_History')}> |
||||
<ButtonGroup> |
||||
<Button onClick={handleClickReturn}> |
||||
<Icon name='back' size='x16'/> {t('Back')} |
||||
</Button> |
||||
<Button primary danger onClick={handleClearHistory} disabled={!(data && data.history.length > 0)}> |
||||
<Icon name='trash'/> {t('clear_history')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<HistoryContent key='historyContent' data={data} state={state} onChange={onChange} /> |
||||
<Pagination |
||||
current={current} |
||||
itemsPerPage={itemsPerPage} |
||||
itemsPerPageLabel={t('Items_per_page:')} |
||||
showingResultsLabel={showingResultsLabel} |
||||
count={(data && data.total) || 0} |
||||
onSetItemsPerPage={setItemsPerPage} |
||||
onSetCurrent={setCurrent} |
||||
/> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
|
||||
export default OutgoingWebhookHistoryPage; |
||||
@ -0,0 +1,20 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
export function useExampleData({ aditionalFields, url }, dep) { |
||||
const exampleData = { |
||||
...aditionalFields, |
||||
text: 'Example message', |
||||
attachments: [{ |
||||
title: 'Rocket.Chat', |
||||
title_link: 'https://rocket.chat', |
||||
text: 'Rocket.Chat, the best open source chat', |
||||
image_url: '/images/integration-attachment-example.png', |
||||
color: '#764FA5', |
||||
}], |
||||
}; |
||||
|
||||
return useMemo(() => [ |
||||
exampleData, |
||||
`curl -X POST -H 'Content-Type: application/json' --data '${ JSON.stringify(exampleData) }' ${ url }`, |
||||
], dep); |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
|
||||
export default function NewBot() { |
||||
const t = useTranslation(); |
||||
return <Box pb='x20' fontScale='s1' key='bots' dangerouslySetInnerHTML={{ __html: t('additional_integrations_Bots') }}/>; |
||||
} |
||||
@ -0,0 +1,50 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Field, Box, Margins, Button } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useEndpointAction } from '../../../hooks/useEndpointAction'; |
||||
import { useForm } from '../../../hooks/useForm'; |
||||
import IncomingWebhookForm from '../IncomingWebhookForm'; |
||||
|
||||
const initialState = { |
||||
enabled: false, |
||||
channel: '', |
||||
username: '', |
||||
name: '', |
||||
alias: '', |
||||
avatarUrl: '', |
||||
emoji: '', |
||||
scriptEnabled: false, |
||||
script: '', |
||||
}; |
||||
|
||||
export default function NewIncomingWebhook(props) { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const { values: formValues, handlers: formHandlers, reset } = useForm(initialState); |
||||
|
||||
const saveAction = useEndpointAction('POST', 'integrations.create', useMemo(() => ({ ...formValues, type: 'webhook-incoming' }), [JSON.stringify(formValues)]), t('Integration_added')); |
||||
|
||||
const handleSave = async () => { |
||||
const result = await saveAction(); |
||||
if (result.success) { |
||||
router.push({ context: 'edit', type: 'incoming', id: result.integration._id }); |
||||
} |
||||
}; |
||||
|
||||
const actionButtons = useMemo(() => <Field> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'> |
||||
<Margins inlineEnd='x4'> |
||||
<Button flexGrow={1} type='reset' onClick={reset}>{t('Reset')}</Button> |
||||
<Button mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button> |
||||
</Margins> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field>); |
||||
|
||||
return <IncomingWebhookForm formValues={formValues} formHandlers={formHandlers} append={actionButtons} {...props}/>; |
||||
} |
||||
@ -0,0 +1,58 @@ |
||||
import { Tabs, Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; |
||||
import React, { useCallback } from 'react'; |
||||
|
||||
import Page from '../../../components/basic/Page'; |
||||
import NewIncomingWebhook from './NewIncomingWebhook'; |
||||
import NewOutgoingWebhook from './NewOutgoingWebhook'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useRouteParameter, useRoute } from '../../../contexts/RouterContext'; |
||||
|
||||
|
||||
export default function NewIntegrationsPage({ ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const handleClickTab = (type) => () => { |
||||
router.push({ context: 'new', type }); |
||||
}; |
||||
|
||||
const handleClickReturn = useCallback(() => { |
||||
router.push({ }); |
||||
}, []); |
||||
|
||||
const tab = useRouteParameter('type'); |
||||
|
||||
const handleIncomingTab = useCallback(handleClickTab('incoming'), []); |
||||
const handleOutgoingTab = useCallback(handleClickTab('outgoing'), []); |
||||
|
||||
return <Page flexDirection='column' {...props}> |
||||
<Page.Header title={t('Integrations')} > |
||||
<ButtonGroup> |
||||
<Button onClick={handleClickReturn}> |
||||
<Icon name='back' size='x16'/> {t('Back')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Tabs> |
||||
<Tabs.Item |
||||
selected={tab === 'incoming'} |
||||
onClick={handleIncomingTab} |
||||
> |
||||
{t('Incoming')} |
||||
</Tabs.Item> |
||||
<Tabs.Item |
||||
selected={tab === 'outgoing'} |
||||
onClick={handleOutgoingTab} |
||||
> |
||||
{t('Outgoing')} |
||||
</Tabs.Item> |
||||
</Tabs> |
||||
<Page.ScrollableContentWithShadow> |
||||
{ |
||||
(tab === 'incoming' && <NewIncomingWebhook key='incoming'/>) |
||||
|| (tab === 'outgoing' && <NewOutgoingWebhook key='outgoing'/>) |
||||
} |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
import React, { useMemo, useCallback } from 'react'; |
||||
import { Field, Button } from '@rocket.chat/fuselage'; |
||||
import { useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useEndpointAction } from '../../../hooks/useEndpointAction'; |
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useForm } from '../../../hooks/useForm'; |
||||
import OutgoingWebhookForm from '../OutgoiongWebhookForm'; |
||||
|
||||
const defaultData = { |
||||
type: 'webhook-outgoing', |
||||
enabled: true, |
||||
impersonateUser: false, |
||||
event: 'sendMessage', |
||||
urls: '', |
||||
triggerWords: '', |
||||
targetRoom: '', |
||||
channel: '', |
||||
username: '', |
||||
name: '', |
||||
alias: '', |
||||
avatar: '', |
||||
emoji: '', |
||||
scriptEnabled: false, |
||||
script: '', |
||||
retryFailedCalls: true, |
||||
retryCount: 6, |
||||
retryDelay: 'powers-of-ten', |
||||
triggerWordAnywhere: false, |
||||
runOnEdits: true, |
||||
}; |
||||
|
||||
export default function NewOutgoingWebhook({ data = defaultData, onChange, setSaveAction, ...props }) { |
||||
const t = useTranslation(); |
||||
const router = useRoute('admin-integrations'); |
||||
|
||||
const { values: formValues, handlers: formHandlers } = useForm({ ...data, token: useUniqueId() }); |
||||
|
||||
const { |
||||
urls, |
||||
triggerWords, |
||||
} = formValues; |
||||
|
||||
const query = useMemo(() => ({ |
||||
...formValues, |
||||
urls: urls.split('\n'), |
||||
triggerWords: triggerWords.split(';'), |
||||
}), [JSON.stringify(formValues)]); |
||||
|
||||
const saveIntegration = useEndpointAction('POST', 'integrations.create', query, t('Integration_added')); |
||||
|
||||
const handleSave = useCallback(async () => { |
||||
const result = await saveIntegration(); |
||||
if (result.success) { |
||||
router.push({ id: result.integration._id, context: 'edit', type: 'outgoing' }); |
||||
} |
||||
}, [saveIntegration, router]); |
||||
|
||||
const saveButton = useMemo(() => <Field> |
||||
<Field.Row> |
||||
<Button w='full' mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button> |
||||
</Field.Row> |
||||
</Field>); |
||||
|
||||
|
||||
return <OutgoingWebhookForm formValues={formValues} formHandlers={formHandlers} append={saveButton} {...props}/>; |
||||
} |
||||
@ -0,0 +1,43 @@ |
||||
import React, { useEffect, useState } from 'react'; |
||||
import { Box, Skeleton, Margins } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
|
||||
const blogSpotStyleScriptImport = (src) => new Promise((resolve) => { |
||||
const script = document.createElement('script'); |
||||
script.type = 'text/javascript'; |
||||
document.body.appendChild(script); |
||||
|
||||
const resolveFunc = (event) => resolve(event.currentTarget); |
||||
|
||||
script.onreadystatechange = resolveFunc; |
||||
script.onload = resolveFunc; |
||||
script.src = src; |
||||
}); |
||||
|
||||
export default function NewZapier({ ...props }) { |
||||
const t = useTranslation(); |
||||
const [script, setScript] = useState(); |
||||
useEffect(() => { |
||||
const importZapier = async () => { |
||||
const scriptEl = await blogSpotStyleScriptImport('https://zapier.com/apps/embed/widget.js?services=rocketchat&html_id=zapier-goes-here'); |
||||
setScript(scriptEl); |
||||
}; |
||||
if (!script) { importZapier(); } |
||||
return () => script && script.parentNode.removeChild(script); |
||||
}, [script]); |
||||
|
||||
return <> |
||||
<Box pb='x20' fontScale='s1' dangerouslySetInnerHTML={{ __html: t('additional_integrations_Zapier') }}/> |
||||
{!script && <Box display='flex' flexDirection='column' alignItems='stretch' mbs={10}> |
||||
<Margins blockEnd={14}> |
||||
<Skeleton variant='rect' height={71}/> |
||||
<Skeleton variant='rect' height={71}/> |
||||
<Skeleton variant='rect' height={71}/> |
||||
<Skeleton variant='rect' height={71}/> |
||||
<Skeleton variant='rect' height={71}/> |
||||
</Margins> |
||||
</Box>} |
||||
<Box id='zapier-goes-here' {...props} /> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,87 @@ |
||||
import React, { useCallback, useState } from 'react'; |
||||
import { |
||||
Button, |
||||
ButtonGroup, |
||||
TextInput, |
||||
Field, |
||||
TextAreaInput, |
||||
ToggleSwitch, |
||||
FieldGroup, |
||||
} from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
|
||||
|
||||
export default function OAuthAddApp(props) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [newData, setNewData] = useState({ |
||||
name: '', |
||||
active: false, |
||||
redirectUri: '', |
||||
}); |
||||
|
||||
const saveApp = useMethod('addOAuthApp'); |
||||
|
||||
const router = useRoute('admin-oauth-apps'); |
||||
|
||||
const close = useCallback(() => router.push({}), [router]); |
||||
|
||||
const handleSave = useCallback(async () => { |
||||
try { |
||||
await saveApp( |
||||
newData, |
||||
); |
||||
close(); |
||||
dispatchToastMessage({ type: 'success', message: t('Application_added') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}, [JSON.stringify(newData)]); |
||||
|
||||
const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) }); |
||||
|
||||
const { |
||||
active, |
||||
name, |
||||
redirectUri, |
||||
} = newData; |
||||
|
||||
return <VerticalBar.ScrollableContent w='full' {...props}> |
||||
<FieldGroup maxWidth='x600' alignSelf='center' w='full'> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Active')} |
||||
<ToggleSwitch checked={active} onChange={handleChange('active', () => !active)}/> |
||||
</Field.Label> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Application_Name')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={name} onChange={handleChange('name')} /> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Give_the_application_a_name_This_will_be_seen_by_your_users')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Redirect_URI')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={5} value={redirectUri} onChange={handleChange('redirectUri')}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button onClick={close}>{t('Cancel')}</Button> |
||||
<Button primary onClick={handleSave} >{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</VerticalBar.ScrollableContent>; |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
import React from 'react'; |
||||
import { Button, Icon } from '@rocket.chat/fuselage'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
// import VerticalBar from '../../components/basic/VerticalBar';
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useRouteParameter, useRoute } from '../../contexts/RouterContext'; |
||||
import OAuthAppsTable from './OAuthAppsTable'; |
||||
import OAuthEditAppWithData from './OAuthEditApp'; |
||||
import OAuthAddApp from './OAuthAddApp'; |
||||
|
||||
export function OAuthAppsPage() { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('admin-oauth-apps'); |
||||
|
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('id'); |
||||
|
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={t('OAuth_Applications')}> |
||||
{context && <Button alignSelf='flex-end' onClick={() => router.push({})}> |
||||
<Icon name='back'/>{t('Back')} |
||||
</Button>} |
||||
{!context && <Button primary alignSelf='flex-end' onClick={() => router.push({ context: 'new' })}> |
||||
<Icon name='plus'/>{t('New_Application')} |
||||
</Button>} |
||||
</Page.Header> |
||||
<Page.Content> |
||||
{!context && <OAuthAppsTable />} |
||||
{context === 'edit' && <OAuthEditAppWithData _id={id}/>} |
||||
{context === 'new' && <OAuthAddApp />} |
||||
</Page.Content> |
||||
</Page> |
||||
</Page>; |
||||
} |
||||
|
||||
export default OAuthAppsPage; |
||||
@ -0,0 +1,15 @@ |
||||
import React from 'react'; |
||||
|
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../NotAuthorizedPage'; |
||||
import OAuthAppsPage from './OAuthAppsPage'; |
||||
|
||||
export default function MailerRoute() { |
||||
const canAccessOAuthApps = usePermission('manage-oauth-apps'); |
||||
|
||||
if (!canAccessOAuthApps) { |
||||
return <NotAuthorizedPage/>; |
||||
} |
||||
|
||||
return <OAuthAppsPage />; |
||||
} |
||||
@ -0,0 +1,40 @@ |
||||
import { Table } from '@rocket.chat/fuselage'; |
||||
import React, { useMemo, useCallback } from 'react'; |
||||
|
||||
import { GenericTable, Th } from '../../../app/ui/client/components/GenericTable'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; |
||||
|
||||
export function OAuthAppsTable() { |
||||
const t = useTranslation(); |
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
|
||||
const { data } = useEndpointDataExperimental('oauth-apps.list', useMemo(() => ({}), [])); |
||||
|
||||
const router = useRoute('admin-oauth-apps'); |
||||
|
||||
const onClick = (_id) => () => router.push({ |
||||
context: 'edit', |
||||
id: _id, |
||||
}); |
||||
|
||||
const header = useMemo(() => [ |
||||
<Th key={'name'}>{t('Name')}</Th>, |
||||
<Th key={'_createdBy'}>{t('Created_by')}</Th>, |
||||
<Th key={'_createdAt'}>{t('Created_at')}</Th>, |
||||
]); |
||||
|
||||
const renderRow = useCallback(({ _id, name, _createdAt, _createdBy: { username: createdBy } }) => |
||||
<Table.Row key={_id} onKeyDown={onClick(_id)} onClick={onClick(_id)} tabIndex={0} role='link' action qa-oauth-app-id={_id}> |
||||
<Table.Cell withTruncatedText color='default' fontScale='p2'>{name}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{createdBy}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{formatDateAndTime(_createdAt)}</Table.Cell> |
||||
</Table.Row>, |
||||
); |
||||
|
||||
return <GenericTable header={header} renderRow={renderRow} results={data && data.oauthApps} />; |
||||
} |
||||
|
||||
export default OAuthAppsTable; |
||||
@ -0,0 +1,222 @@ |
||||
import React, { useCallback, useState, useMemo } from 'react'; |
||||
import { |
||||
Box, |
||||
Button, |
||||
ButtonGroup, |
||||
TextInput, |
||||
Field, |
||||
Icon, |
||||
Skeleton, |
||||
Throbber, |
||||
InputBox, |
||||
TextAreaInput, |
||||
ToggleSwitch, |
||||
FieldGroup, |
||||
} from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod, useAbsoluteUrl } from '../../contexts/ServerContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { Modal } from '../../components/basic/Modal'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
|
||||
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{t('Application_delete_warning')} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary danger onClick={onDelete}>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
const SuccessModal = ({ onClose, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='success' name='checkmark-circled' size={20}/> |
||||
<Modal.Title>{t('Deleted')}</Modal.Title> |
||||
<Modal.Close onClick={onClose}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{t('Your_entry_has_been_deleted')} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button primary onClick={onClose}>{t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default function EditOauthAppWithData({ _id, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const [cache, setCache] = useState(); |
||||
|
||||
const onChange = useCallback(() => { |
||||
setCache(new Date()); |
||||
}, []); |
||||
|
||||
const query = useMemo(() => ({ |
||||
appId: _id, |
||||
}), [_id, cache]); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental('oauth-apps.get', query); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box pb='x20' maxWidth='x600' w='full' alignSelf='center'> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button disabled><Throbber inheritColor/></Button> |
||||
<Button primary disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button primary danger disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
</Box>; |
||||
} |
||||
|
||||
if (error || !data || !_id) { |
||||
return <Box fontScale='h1' pb='x20'>{t('error-application-not-found')}</Box>; |
||||
} |
||||
|
||||
return <EditOauthApp data={data.oauthApp} onChange={onChange} {...props}/>; |
||||
} |
||||
|
||||
function EditOauthApp({ onChange, data, ...props }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [newData, setNewData] = useState({ |
||||
name: data.name, |
||||
active: data.active, |
||||
redirectUri: Array.isArray(data.redirectUri) ? data.redirectUri.join('\n') : data.redirectUri, |
||||
}); |
||||
const [modal, setModal] = useState(); |
||||
|
||||
const router = useRoute('admin-oauth-apps'); |
||||
|
||||
const close = useCallback(() => router.push({}), [router]); |
||||
|
||||
const absoluteUrl = useAbsoluteUrl(); |
||||
const authUrl = useMemo(() => absoluteUrl('oauth/authorize')); |
||||
const tokenUrl = useMemo(() => absoluteUrl('oauth/token')); |
||||
|
||||
const saveApp = useMethod('updateOAuthApp'); |
||||
const deleteApp = useMethod('deleteOAuthApp'); |
||||
|
||||
const handleSave = useCallback(async () => { |
||||
try { |
||||
await saveApp( |
||||
data._id, |
||||
newData, |
||||
); |
||||
dispatchToastMessage({ type: 'success', message: t('Application_updated') }); |
||||
onChange(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}, [JSON.stringify(newData)]); |
||||
|
||||
const onDeleteConfirm = useCallback(async () => { |
||||
try { |
||||
await deleteApp(data._id); |
||||
setModal(() => <SuccessModal onClose={() => { setModal(); close(); }}/>); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}, [data._id]); |
||||
|
||||
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>); |
||||
|
||||
const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) }); |
||||
|
||||
const { |
||||
active, |
||||
name, |
||||
redirectUri, |
||||
} = newData; |
||||
|
||||
return <> |
||||
<VerticalBar.ScrollableContent w='full' {...props}> |
||||
<FieldGroup maxWidth='x600' alignSelf='center' w='full'> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Active')} |
||||
<ToggleSwitch checked={active} onChange={handleChange('active', () => !active)}/> |
||||
</Field.Label> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Application_Name')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={name} onChange={handleChange('name')} /> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Give_the_application_a_name_This_will_be_seen_by_your_users')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Redirect_URI')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={5} value={redirectUri} onChange={handleChange('redirectUri')}/> |
||||
</Field.Row> |
||||
<Field.Hint>{t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')}</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Client_ID')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={data.clientId} onChange={handleChange('clientId')} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Client_Secret')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={data.clientSecret} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Authorization_URL')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={authUrl} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Access_Token_URL')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={tokenUrl} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button onClick={close}>{t('Cancel')}</Button> |
||||
<Button primary onClick={handleSave} >{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button primary danger onClick={openConfirmDelete}><Icon name='trash' mie='x4'/>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</VerticalBar.ScrollableContent> |
||||
{ modal } |
||||
</>; |
||||
} |
||||
@ -0,0 +1,194 @@ |
||||
import React, { useCallback, useState, useMemo } from 'react'; |
||||
import { Box, Headline, Button, Margins, TextInput, Skeleton, Field, ToggleSwitch, Divider, Icon, Callout } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; |
||||
import { roomTypes } from '../../../../app/utils/client'; |
||||
import { useMethod } from '../../../contexts/ServerContext'; |
||||
import { usePermission } from '../../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../../NotAuthorizedPage'; |
||||
import { useEndpointAction } from '../../../hooks/useEndpointAction'; |
||||
import Page from '../../../components/basic/Page'; |
||||
|
||||
export function EditRoomContextBar({ rid }) { |
||||
const canViewRoomAdministration = usePermission('view-room-administration'); |
||||
return canViewRoomAdministration ? <EditRoomWithData rid={rid}/> : <NotAuthorizedPage/>; |
||||
} |
||||
|
||||
function EditRoomWithData({ rid }) { |
||||
const [cache, setState] = useState(); |
||||
|
||||
const { data = {}, state, error } = useEndpointDataExperimental('rooms.adminRooms.getRoom', useMemo(() => ({ rid }), [rid, cache])); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box w='full' pb='x24'> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8' /> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
<Headline.Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
</Box>; |
||||
} |
||||
|
||||
if (state === ENDPOINT_STATES.ERROR) { |
||||
return error.message; |
||||
} |
||||
|
||||
return <EditRoom room={data} onChange={() => setState(new Date())}/>; |
||||
} |
||||
|
||||
function EditRoom({ room, onChange }) { |
||||
const t = useTranslation(); |
||||
|
||||
const [deleted, setDeleted] = useState(false); |
||||
const [newData, setNewData] = useState({}); |
||||
const [changeArchivation, setChangeArchivation] = useState(false); |
||||
|
||||
const canDelete = usePermission(`delete-${ room.t }`); |
||||
|
||||
const hasUnsavedChanges = useMemo(() => Object.values(newData).filter((current) => current === null).length < Object.keys(newData).length, [JSON.stringify(newData)]); |
||||
const saveQuery = useMemo(() => ({ rid: room._id, ...Object.fromEntries(Object.entries(newData).filter(([, value]) => value !== null)) }), [room._id, JSON.stringify(newData)]); |
||||
|
||||
const archiveSelector = room.archived ? 'unarchive' : 'archive'; |
||||
const archiveMessage = archiveSelector === 'archive' ? 'Room_has_been_archived' : 'Room_has_been_archived'; |
||||
const archiveQuery = useMemo(() => ({ rid: room._id, action: room.archived ? 'unarchive' : 'archive' }), [room.rid, changeArchivation]); |
||||
|
||||
const saveAction = useEndpointAction('POST', 'rooms.saveRoomSettings', saveQuery, t('Room_updated_successfully')); |
||||
const archiveAction = useEndpointAction('POST', 'rooms.changeArchivationState', archiveQuery, t(archiveMessage)); |
||||
|
||||
const updateType = (type) => () => (type === 'p' ? 'c' : 'p'); |
||||
const areEqual = (a, b) => a === b || !(a || b); |
||||
|
||||
const handleChange = (field, currentValue, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: areEqual(getValue(e), currentValue) ? null : getValue(e) }); |
||||
const handleSave = async () => { |
||||
await Promise.all([hasUnsavedChanges && saveAction(), changeArchivation && archiveAction()].filter(Boolean)); |
||||
onChange('update'); |
||||
}; |
||||
|
||||
const deleteRoom = useMethod('eraseRoom'); |
||||
|
||||
const handleDelete = useCallback(async () => { |
||||
await deleteRoom(room._id); |
||||
setDeleted(true); |
||||
}, [room]); |
||||
|
||||
const roomName = room.t === 'd' ? room.usernames.join(' x ') : roomTypes.getRoomName(room.t, { type: room.t, ...room }); |
||||
const roomType = newData.roomType ?? room.t; |
||||
const readOnly = newData.readOnly ?? !!room.ro; |
||||
const isArchived = changeArchivation ? !room.archived : !!room.archived; |
||||
const isDefault = newData.default ?? !!room.default; |
||||
const isFavorite = newData.favorite ?? !!room.favorite; |
||||
const isFeatured = newData.featured ?? !!room.featured; |
||||
|
||||
return <Page.ScrollableContent pb='x24' mi='neg-x24' is='form'> |
||||
<Margins blockEnd='x16'> |
||||
|
||||
{deleted && <Callout type='danger' title={t('Room_has_been_deleted')}></Callout>} |
||||
|
||||
<Field> |
||||
<Field.Label>{t('Name')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput disabled={deleted || room.t === 'd'} value={newData.roomName ?? roomName} onChange={handleChange('roomName', roomName)} flexGrow={1}/> |
||||
</Field.Row> |
||||
</Field> |
||||
{ room.t !== 'd' && <> |
||||
<Field> |
||||
<Field.Label>{t('Owner')}</Field.Label> |
||||
<Field.Row> |
||||
<Box fontScale='p1'>{room.u?.username}</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Topic')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput disabled={deleted} value={(newData.roomTopic ?? room.topic) || ''} onChange={handleChange('roomTopic', room.topic)} flexGrow={1}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field mbs='x20'> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' alignItems='flex-start'> |
||||
<Box display='flex' flexDirection='column' alignItems='flex-end' flexGrow='1' color={roomType !== 'c' ? 'hint' : 'default'}> |
||||
<Box fontScale='s1'>{t('Public')}</Box> |
||||
<Box fontScale='p1' style={{ textAlign: 'end' }}>{t('All_users_in_the_channel_can_write_new_messages')}</Box> |
||||
</Box> |
||||
<Margins inline='x16'> |
||||
<ToggleSwitch disabled={deleted} checked={roomType === 'p'} onChange={handleChange('roomType', room.t, updateType(roomType))}/> |
||||
</Margins> |
||||
<Box display='flex' flexDirection='column' alignItems='flex-start' flexGrow='1' color={roomType !== 'p' ? 'hint' : 'default'}> |
||||
<Box fontScale='s1'>{t('Private')}</Box> |
||||
<Box fontScale='p1' style={{ textAlign: 'start' }}>{t('Just_invited_people_can_access_this_channel')}</Box> |
||||
</Box> |
||||
</Box> |
||||
</Field.Row> |
||||
<Divider /> |
||||
</Field> |
||||
<Field mbs='x20'> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' alignItems='flex-start'> |
||||
<Box display='flex' flexDirection='column' alignItems='flex-end' flexGrow='1' color={readOnly ? 'hint' : 'default'}> |
||||
<Box fontScale='s1'>{t('Collaborative')}</Box> |
||||
<Box fontScale='p1' style={{ textAlign: 'end' }}>{t('All_users_in_the_channel_can_write_new_messages')}</Box> |
||||
</Box> |
||||
<Margins inline='x16'> |
||||
<ToggleSwitch disabled={deleted} checked={readOnly} onChange={handleChange('readOnly', room.ro, () => !readOnly)}/> |
||||
</Margins> |
||||
<Box display='flex' flexDirection='column' alignItems='flex-start' flexGrow='1' color={!readOnly ? 'hint' : 'default'}> |
||||
<Box fontScale='s1'>{t('Read_only')}</Box> |
||||
<Box fontScale='p1' style={{ textAlign: 'start' }}>{t('Only_authorized_users_can_write_new_messages')}</Box> |
||||
</Box> |
||||
</Box> |
||||
</Field.Row> |
||||
<Divider /> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> |
||||
<Field.Label>{t('Archived')}</Field.Label> |
||||
<ToggleSwitch disabled={deleted} checked={isArchived} onChange={() => setChangeArchivation(!changeArchivation)}/> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> |
||||
<Field.Label>{t('Default')}</Field.Label> |
||||
<ToggleSwitch disabled={deleted} checked={isDefault} onChange={handleChange('default', room.default, () => !isDefault)}/> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> |
||||
<Field.Label>{t('Favorite')}</Field.Label> |
||||
<ToggleSwitch disabled={deleted} checked={isFavorite} onChange={handleChange('favorite', room.default, () => !isFavorite)}/> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> |
||||
<Field.Label>{t('Featured')}</Field.Label> |
||||
<ToggleSwitch disabled={deleted} checked={isFeatured} onChange={handleChange('featured', room.default, () => !isFeatured)}/> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'> |
||||
<Margins inlineEnd='x4'> |
||||
<Button disabled={deleted} flexGrow={1} type='reset' disabled={!hasUnsavedChanges && !changeArchivation} onClick={() => setNewData({})}>{t('Reset')}</Button> |
||||
<Button disabled={deleted} mie='none' flexGrow={1} disabled={!hasUnsavedChanges && !changeArchivation} onClick={handleSave}>{t('Save')}</Button> |
||||
</Margins> |
||||
</Box> |
||||
</Field.Row> |
||||
</Field> |
||||
</>} |
||||
<Field> |
||||
<Field.Row> |
||||
<Button primary danger disabled={deleted || !canDelete} onClick={handleDelete} display='flex' alignItems='center' justifyContent='center' flexGrow={1}><Icon name='trash' size='x16' />{t('Delete')}</Button> |
||||
</Field.Row> |
||||
</Field> |
||||
</Margins> |
||||
</Page.ScrollableContent>; |
||||
} |
||||
@ -0,0 +1,28 @@ |
||||
import { useState, useCallback } from 'react'; |
||||
|
||||
const getValue = (e) => (e.currentTarget ? e.currentTarget.value : e); |
||||
|
||||
const capitalize = (s) => { |
||||
if (typeof s !== 'string') { return ''; } |
||||
return s.charAt(0).toUpperCase() + s.slice(1); |
||||
}; |
||||
|
||||
export const useForm = (obj) => { |
||||
const resetCallbacks = []; |
||||
const ret = Object.keys(obj).sort().reduce((ret, key) => { |
||||
const value = obj[key]; |
||||
const [data, setData] = useState(value); |
||||
|
||||
ret.values = { ...ret.values, [key]: data }; |
||||
ret.handlers = { ...ret.handlers, [`handle${ capitalize(key) }`]: useCallback(typeof value !== 'boolean' ? (e) => setData(getValue(e)) : () => setData(!data), [data]) }; |
||||
resetCallbacks.push(() => setData(value)); |
||||
|
||||
return ret; |
||||
}, {}); |
||||
|
||||
ret.reset = () => { |
||||
resetCallbacks.forEach((reset) => reset()); |
||||
}; |
||||
|
||||
return ret; |
||||
}; |
||||
@ -0,0 +1,6 @@ |
||||
import hljs from 'highlight.js'; |
||||
import { useMemo } from 'react'; |
||||
|
||||
export function useHilightCode() { |
||||
return (language, text) => useMemo(() => hljs.highlight(language, text).value, [language, text]); |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue