diff --git a/app/cloud/client/admin/callback.html b/app/cloud/client/admin/callback.html deleted file mode 100644 index 5d6c9c432f4c..000000000000 --- a/app/cloud/client/admin/callback.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/app/cloud/client/admin/callback.js b/app/cloud/client/admin/callback.js deleted file mode 100644 index de860026192e..000000000000 --- a/app/cloud/client/admin/callback.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/cloud/client/admin/cloud.html b/app/cloud/client/admin/cloud.html deleted file mode 100644 index 78c1e053390d..000000000000 --- a/app/cloud/client/admin/cloud.html +++ /dev/null @@ -1,145 +0,0 @@ - diff --git a/app/cloud/client/admin/cloud.js b/app/cloud/client/admin/cloud.js deleted file mode 100644 index cd9edf0a41a6..000000000000 --- a/app/cloud/client/admin/cloud.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/cloud/client/admin/cloudRegisterManually.css b/app/cloud/client/admin/cloudRegisterManually.css deleted file mode 100644 index 8cd3c5628e34..000000000000 --- a/app/cloud/client/admin/cloudRegisterManually.css +++ /dev/null @@ -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; - } -} diff --git a/app/cloud/client/admin/cloudRegisterManually.html b/app/cloud/client/admin/cloudRegisterManually.html deleted file mode 100644 index 738a36423501..000000000000 --- a/app/cloud/client/admin/cloudRegisterManually.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/app/cloud/client/admin/cloudRegisterManually.js b/app/cloud/client/admin/cloudRegisterManually.js deleted file mode 100644 index 223d9bc609ce..000000000000 --- a/app/cloud/client/admin/cloudRegisterManually.js +++ /dev/null @@ -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('', `${ text }`))); -}); - -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); - }); -}); diff --git a/app/cloud/client/admin/index.js b/app/cloud/client/admin/index.js deleted file mode 100644 index 92cb6032c24e..000000000000 --- a/app/cloud/client/admin/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './cloud'; -import './callback'; diff --git a/app/cloud/client/index.js b/app/cloud/client/index.js deleted file mode 100644 index ce6ceecd3d5c..000000000000 --- a/app/cloud/client/index.js +++ /dev/null @@ -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']); - }, -}); diff --git a/app/integrations/client/getIntegration.js b/app/integrations/client/getIntegration.js deleted file mode 100644 index d80c8ee690e5..000000000000 --- a/app/integrations/client/getIntegration.js +++ /dev/null @@ -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); - } -} diff --git a/app/integrations/client/index.js b/app/integrations/client/index.js deleted file mode 100644 index f7c2445d187d..000000000000 --- a/app/integrations/client/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import '../lib/rocketchat'; -import './startup'; -import './route'; diff --git a/app/integrations/client/route.js b/app/integrations/client/route.js deleted file mode 100644 index 08a7eb444653..000000000000 --- a/app/integrations/client/route.js +++ /dev/null @@ -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', - }); - }, -}); diff --git a/app/integrations/client/streamer.js b/app/integrations/client/streamer.js deleted file mode 100644 index 6bc2d6dc1327..000000000000 --- a/app/integrations/client/streamer.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -export const integrationHistoryStreamer = new Meteor.Streamer('integrationHistory'); diff --git a/app/integrations/client/stylesheets/integrations.css b/app/integrations/client/stylesheets/integrations.css deleted file mode 100644 index 83ee9e64cfe6..000000000000 --- a/app/integrations/client/stylesheets/integrations.css +++ /dev/null @@ -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%; - } -} diff --git a/app/integrations/client/views/additional/zapier.html b/app/integrations/client/views/additional/zapier.html deleted file mode 100644 index d95f0ac50691..000000000000 --- a/app/integrations/client/views/additional/zapier.html +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/app/integrations/client/views/index.js b/app/integrations/client/views/index.js deleted file mode 100644 index f7c6282cbd35..000000000000 --- a/app/integrations/client/views/index.js +++ /dev/null @@ -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'; diff --git a/app/integrations/client/views/integrations.html b/app/integrations/client/views/integrations.html deleted file mode 100644 index 0a39da5ae278..000000000000 --- a/app/integrations/client/views/integrations.html +++ /dev/null @@ -1,74 +0,0 @@ - diff --git a/app/integrations/client/views/integrations.js b/app/integrations/client/views/integrations.js deleted file mode 100644 index 0506b21e1697..000000000000 --- a/app/integrations/client/views/integrations.js +++ /dev/null @@ -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), -}); diff --git a/app/integrations/client/views/integrationsIncoming.html b/app/integrations/client/views/integrationsIncoming.html deleted file mode 100644 index 7a98d4ac9241..000000000000 --- a/app/integrations/client/views/integrationsIncoming.html +++ /dev/null @@ -1,138 +0,0 @@ - diff --git a/app/integrations/client/views/integrationsIncoming.js b/app/integrations/client/views/integrationsIncoming.js deleted file mode 100644 index 222205d64fa7..000000000000 --- a/app/integrations/client/views/integrationsIncoming.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/integrations/client/views/integrationsNew.html b/app/integrations/client/views/integrationsNew.html deleted file mode 100644 index 9c3ab5ee9ec3..000000000000 --- a/app/integrations/client/views/integrationsNew.html +++ /dev/null @@ -1,51 +0,0 @@ - diff --git a/app/integrations/client/views/integrationsNew.js b/app/integrations/client/views/integrationsNew.js deleted file mode 100644 index f5bd29c08e6f..000000000000 --- a/app/integrations/client/views/integrationsNew.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/integrations/client/views/integrationsOutgoing.html b/app/integrations/client/views/integrationsOutgoing.html deleted file mode 100644 index d9dc03ae209e..000000000000 --- a/app/integrations/client/views/integrationsOutgoing.html +++ /dev/null @@ -1,241 +0,0 @@ - diff --git a/app/integrations/client/views/integrationsOutgoing.js b/app/integrations/client/views/integrationsOutgoing.js deleted file mode 100644 index dba874aca11b..000000000000 --- a/app/integrations/client/views/integrationsOutgoing.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/integrations/client/views/integrationsOutgoingHistory.html b/app/integrations/client/views/integrationsOutgoingHistory.html deleted file mode 100644 index b453924e1f9d..000000000000 --- a/app/integrations/client/views/integrationsOutgoingHistory.html +++ /dev/null @@ -1,143 +0,0 @@ - diff --git a/app/integrations/client/views/integrationsOutgoingHistory.js b/app/integrations/client/views/integrationsOutgoingHistory.js deleted file mode 100644 index 66384848d6a4..000000000000 --- a/app/integrations/client/views/integrationsOutgoingHistory.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/integrations/client/views/messageExample.js b/app/integrations/client/views/messageExample.js deleted file mode 100644 index 2e553dae32b6..000000000000 --- a/app/integrations/client/views/messageExample.js +++ /dev/null @@ -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: {}, -}); diff --git a/app/oauth2-server-config/client/admin/route.js b/app/oauth2-server-config/client/admin/route.js deleted file mode 100644 index c274dbaa6087..000000000000 --- a/app/oauth2-server-config/client/admin/route.js +++ /dev/null @@ -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, - }); - }, -}); diff --git a/app/oauth2-server-config/client/admin/views/index.js b/app/oauth2-server-config/client/admin/views/index.js deleted file mode 100644 index 071605a56b89..000000000000 --- a/app/oauth2-server-config/client/admin/views/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import './oauthApp.html'; -import './oauthApp'; -import './oauthApps.html'; -import './oauthApps'; diff --git a/app/oauth2-server-config/client/admin/views/oauthApp.html b/app/oauth2-server-config/client/admin/views/oauthApp.html deleted file mode 100644 index 360a4ff57c99..000000000000 --- a/app/oauth2-server-config/client/admin/views/oauthApp.html +++ /dev/null @@ -1,72 +0,0 @@ - diff --git a/app/oauth2-server-config/client/admin/views/oauthApp.js b/app/oauth2-server-config/client/admin/views/oauthApp.js deleted file mode 100644 index fa866ba86a81..000000000000 --- a/app/oauth2-server-config/client/admin/views/oauthApp.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/oauth2-server-config/client/admin/views/oauthApps.html b/app/oauth2-server-config/client/admin/views/oauthApps.html deleted file mode 100644 index 99a142127802..000000000000 --- a/app/oauth2-server-config/client/admin/views/oauthApps.html +++ /dev/null @@ -1,38 +0,0 @@ - diff --git a/app/oauth2-server-config/client/admin/views/oauthApps.js b/app/oauth2-server-config/client/admin/views/oauthApps.js deleted file mode 100644 index 595af845a769..000000000000 --- a/app/oauth2-server-config/client/admin/views/oauthApps.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/oauth2-server-config/client/index.js b/app/oauth2-server-config/client/index.js index 917437a0b53a..5911484bf1c4 100644 --- a/app/oauth2-server-config/client/index.js +++ b/app/oauth2-server-config/client/index.js @@ -1,5 +1,4 @@ import './oauth/oauth2-client.html'; import './oauth/oauth2-client'; import './admin/startup'; -import './admin/route'; import './oauth/stylesheets/oauth2.css'; diff --git a/app/ui/client/components/GenericTable.js b/app/ui/client/components/GenericTable.js index 9d50a456aff5..c3a9d0cf5161 100644 --- a/app/ui/client/components/GenericTable.js +++ b/app/ui/client/components/GenericTable.js @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import React, { useMemo, useState, useEffect, useCallback, forwardRef } from 'react'; import { Box, Pagination, Skeleton, Table, Flex, Tile, Scrollable } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; @@ -35,15 +35,15 @@ const LoadingRow = ({ cols }) => )} ; -export function GenericTable({ +export const GenericTable = forwardRef(function GenericTable({ results, total, - renderRow, + renderRow: RenderRow, header, setParams = () => { }, params: paramsDefault = '', FilterComponent = () => null, -}) { +}, ref) { const t = useTranslation(); const [filter, setFilter] = useState(paramsDefault); @@ -65,40 +65,38 @@ export function GenericTable({ const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), []); return <> - <> - - {results && !results.length - ? - {t('No_data_found')} - - : <> - - - - { header && - - {header} - - } - - {results - ? results.map(renderRow) - : } - -
-
-
- - - } - + + {results && !results.length + ? + {t('No_data_found')} + + : <> + + + + { header && + + {header} + + } + + {results + ? results.map((props, index) => ) + : } + +
+
+
+ + + } ; -} +}); diff --git a/app/utils/client/lib/handleError.js b/app/utils/client/lib/handleError.js index 6e2fb92bb2e9..92117a95e216 100644 --- a/app/utils/client/lib/handleError.js +++ b/app/utils/client/lib/handleError.js @@ -22,11 +22,11 @@ export const handleError = function(error, useToastr = true) { } const details = Object.entries(error.details || {}) .reduce((obj, [key, value]) => ({ ...obj, [key]: s.escapeHTML(value) }), {}); - const message = TAPi18n.__(error.error, details); + const message = TAPi18n.__(error.error || error.message, details); const title = details.errorTitle && TAPi18n.__(details.errorTitle); return toastr.error(message, title); } - return s.escapeHTML(TAPi18n.__(error.error, error.details)); + return s.escapeHTML(TAPi18n.__(error.error || error.message, error.details)); }; diff --git a/client/admin/cloud/CloudPage.js b/client/admin/cloud/CloudPage.js new file mode 100644 index 000000000000..e4e285da741a --- /dev/null +++ b/client/admin/cloud/CloudPage.js @@ -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(); + }; + + const isConnectedToCloud = registerStatus?.connectToCloud; + const isWorkspaceRegistered = registerStatus?.workspaceRegistered; + + return + + + {!isWorkspaceRegistered && } + + + + + {modal} + + + + + {isConnectedToCloud && <> + {isWorkspaceRegistered + ? + : } + + + } + + {!isConnectedToCloud && } + + + + ; +} + +export default CloudPage; diff --git a/client/admin/cloud/CloudRoute.js b/client/admin/cloud/CloudRoute.js new file mode 100644 index 000000000000..53c71affae7f --- /dev/null +++ b/client/admin/cloud/CloudRoute.js @@ -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 ; + } + + return ; +} + +export default CloudRoute; diff --git a/client/admin/cloud/ConnectToCloudSection.js b/client/admin/cloud/ConnectToCloudSection.js new file mode 100644 index 000000000000..2b1009d35ccc --- /dev/null +++ b/client/admin/cloud/ConnectToCloudSection.js @@ -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 + {t('Cloud_registration_required')} + +

{t('Cloud_registration_required_description')}

+
+ + + +
; +} + +export default ConnectToCloudSection; diff --git a/client/admin/cloud/ManualWorkspaceRegistrationModal.js b/client/admin/cloud/ManualWorkspaceRegistrationModal.js new file mode 100644 index 000000000000..7952a3aa7053 --- /dev/null +++ b/client/admin/cloud/ManualWorkspaceRegistrationModal.js @@ -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 <> + + +

{t('Cloud_register_offline_helper')}

+
+ + + + {clientKey} + + + + + +

+ {t('Cloud_click_here', { cloudConsoleUrl })} +

+
+
+ + + + + + ; +} + +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 <> + + +

{t('Cloud_register_offline_finish_helper')}

+
+ + + + + +
+ + + + + + + ; +} + +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 + + {t('Cloud_Register_manually')} + + + {(step === Steps.COPY && ) + || (step === Steps.PASTE && )} + ; +} + +export default ManualWorkspaceRegistrationModal; diff --git a/client/admin/cloud/TroubleshootingSection.js b/client/admin/cloud/TroubleshootingSection.js new file mode 100644 index 000000000000..4d1c8e7238d7 --- /dev/null +++ b/client/admin/cloud/TroubleshootingSection.js @@ -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 + {t('Cloud_troubleshooting')} + + +

{t('Cloud_workspace_support')}

+
+ + + + + + +

+ {t('Cloud_status_page_description')}:{' '} + {statusPageUrl} +

+
+
; +} + +export default TroubleshootingSection; diff --git a/client/admin/cloud/WhatIsItSection.js b/client/admin/cloud/WhatIsItSection.js new file mode 100644 index 000000000000..3bafb0e65f57 --- /dev/null +++ b/client/admin/cloud/WhatIsItSection.js @@ -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 + {t('Cloud_what_is_it')} + + +

{t('Cloud_what_is_it_description')}

+ +
+

{t('Cloud_what_is_it_services_like')}

+ +
    +
  • {t('Register_Server_Registered_Push_Notifications')}
  • +
  • {t('Register_Server_Registered_Livechat')}
  • +
  • {t('Register_Server_Registered_OAuth')}
  • +
  • {t('Register_Server_Registered_Marketplace')}
  • +
+ +

{t('Cloud_what_is_it_additional')}

+
+
+
; +} + +export default WhatIsItSection; diff --git a/client/admin/cloud/WorkspaceLoginSection.js b/client/admin/cloud/WorkspaceLoginSection.js new file mode 100644 index 000000000000..7e2bad6cb0dd --- /dev/null +++ b/client/admin/cloud/WorkspaceLoginSection.js @@ -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 + +

{t('Cloud_workspace_connected')}

+
+ + + {isLoggedIn + ? + : } + + + +

{t('Cloud_workspace_disconnect')}

+
+ + + + +
; +} + +export default WorkspaceLoginSection; diff --git a/client/admin/cloud/WorkspaceRegistrationSection.js b/client/admin/cloud/WorkspaceRegistrationSection.js new file mode 100644 index 000000000000..8f2884afa63a --- /dev/null +++ b/client/admin/cloud/WorkspaceRegistrationSection.js @@ -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: ', + ].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 + + + {t('Email')} + + + + {t('Cloud_address_to_send_registration_to')} + + + + + + + + + {t('Token')} + + + + {t('Cloud_manually_input_token')} + + + + + + + +

{t('Cloud_connect_support')}: {supportEmailAddress}

+
+
+
; +} + +export default WorkspaceRegistrationSection; diff --git a/client/admin/cloud/constants.js b/client/admin/cloud/constants.js new file mode 100644 index 000000000000..c5ed79a1477e --- /dev/null +++ b/client/admin/cloud/constants.js @@ -0,0 +1,3 @@ +export const cloudConsoleUrl = 'https://cloud.rocket.chat'; +export const supportEmailAddress = 'support@rocket.chat'; +export const statusPageUrl = 'https://status.rocket.chat'; diff --git a/client/admin/customSounds/EditSound.js b/client/admin/customSounds/EditSound.js new file mode 100644 index 000000000000..0496a617fb0e --- /dev/null +++ b/client/admin/customSounds/EditSound.js @@ -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 + + + {t('Are_you_sure')} + + + + {t('Custom_Sound_Status_Delete_Warning')} + + + + + + + + ; +}; + +const SuccessModal = ({ onClose, ...props }) => { + const t = useTranslation(); + return + + + {t('Deleted')} + + + + {t('Custom_Sound_Has_Been_Deleted')} + + + + + + + ; +}; + +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 + + + + + + + + + + + + ; + } + + if (error || !data || data.sounds.length < 1) { + return {t('Custom_User_Status_Error_Invalid_User_Status')}; + } + + return ; +} + +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(() => { setModal(undefined); close(); onChange(); }}/>); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + onChange(); + } + }, [_id]); + + const openConfirmDelete = () => setModal(() => setModal(undefined)}/>); + + + const clickUpload = useFileInput(handleChangeFile, 'audio/mp3'); + + + return <> + + + + {t('Name')} + + setName(e.currentTarget.value)} placeholder={t('Name')} /> + + + + + {t('Sound_File_mp3')} + + + + {(sound && sound.name) || 'none'} + + + + + + + + + + + + + + + + + + + + + + { modal } + ; +} diff --git a/client/admin/customSounds/NewSound.js b/client/admin/customSounds/NewSound.js new file mode 100644 index 000000000000..b77671f1b07a --- /dev/null +++ b/client/admin/customSounds/NewSound.js @@ -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 + + + {t('Name')} + + setName(e.currentTarget.value)} placeholder={t('Name')} /> + + + + {t('Sound_File_mp3')} + + + + {(sound && sound.name) || 'none'} + + + + + + + + + + + + + ; +} diff --git a/client/admin/integrations/IncomingWebhookForm.js b/client/admin/integrations/IncomingWebhookForm.js new file mode 100644 index 000000000000..f25ba8b304f0 --- /dev/null +++ b/client/admin/integrations/IncomingWebhookForm.js @@ -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 + + + {useMemo(() => + + {t('Enabled')} + + + , [enabled, handleEnabled])} + {useMemo(() => + {t('Name_optional')} + + + + {t('You_should_name_it_to_easily_manage_your_integrations')} + , [name, handleName])} + {useMemo(() => + {t('Post_to_Channel')} + + }/> + + {t('Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here')} + + , [channel, handleChannel])} + {useMemo(() => + {t('Post_as')} + + }/> + + {t('Choose_the_username_that_this_integration_will_post_as')} + {t('Should_exists_a_user_with_this_username')} + , [username, handleUsername])} + {useMemo(() => + {`${ t('Alias') } (${ t('optional') })`} + + }/> + + {t('Choose_the_alias_that_will_appear_before_the_username_in_messages')} + , [alias, handleAlias])} + {useMemo(() => + {`${ t('Avatar_URL') } (${ t('optional') })`} + + }/> + + {t('You_can_change_a_different_avatar_too')} + {t('Should_be_a_URL_of_an_image')} + , [avatarUrl, handleAvatarUrl])} + {useMemo(() => + {`${ t('Emoji') } (${ t('optional') })`} + + }/> + + {t('You_can_use_an_emoji_as_avatar')} + + , [emoji, handleEmoji])} + {useMemo(() => + + {t('Script_Enabled')} + + + , [scriptEnabled, handleScriptEnabled])} + {useMemo(() => + {t('Script')} + + }/> + + , [script, handleScript])} + {useMemo(() => !extraData._id && <> + {t('Webhook_URL')} + + } disabled/> + + {t('Send_your_JSON_payloads_to_this_URL')} + + + {t('Token')} + + } disabled/> + + , [extraData._id])} + {useMemo(() => extraData._id && <> + {t('Webhook_URL')} + + }/> + + {t('Send_your_JSON_payloads_to_this_URL')} + + + {t('Token')} + + }/> + + , [url, extraData._id, extraData.token])} + {useMemo(() => + {t('Example_payload')} + + +
+
+
+
, [hilightedExampleJson])} + {useMemo(() => extraData._id && + {t('Curl')} + + }/> + + , [curlData])} + { append } +
+
+
; +} diff --git a/client/admin/integrations/IntegrationsPage.js b/client/admin/integrations/IntegrationsPage.js new file mode 100644 index 000000000000..ec9dc1adb688 --- /dev/null +++ b/client/admin/integrations/IntegrationsPage.js @@ -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 + + + + + + + {t('Incoming')} + {t('Outgoing')} + {t('Zapier')} + {t('Bots')} + + + {context === 'zapier' && } + {context === 'bots' && } + {showTable && } + + ; +} + +export default IntegrationsPage; diff --git a/client/admin/integrations/IntegrationsRoute.js b/client/admin/integrations/IntegrationsRoute.js new file mode 100644 index 000000000000..e1af75ed11e0 --- /dev/null +++ b/client/admin/integrations/IntegrationsRoute.js @@ -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 ; + } + + if (context === 'new') { + return ; + } + + if (context === 'edit') { + return ; + } + + if (context === 'history') { + return ; + } + + return ; +} + +export default IntegrationsRoute; diff --git a/client/admin/integrations/IntegrationsTable.js b/client/admin/integrations/IntegrationsTable.js new file mode 100644 index 000000000000..582485154019 --- /dev/null +++ b/client/admin/integrations/IntegrationsTable.js @@ -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 + } onChange={handleChange} value={text} /> + ; +}); + +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(() => [ + {t('Name')}, + {t('Post_to')}, + {t('Created_by')}, + isBig && {t('Created_at')}, + {t('Post_as')}, + ].filter(Boolean), [sort, isBig]); + + const renderRow = useCallback(({ name, _id, type, username, _createdAt, _createdBy: { username: createdBy }, channel }) => { + const handler = useMemo(() => onClick(_id, type), []); + return + {name} + {channel.join(', ')} + {createdBy} + {isBig && {formatDateAndTime(_createdAt)}} + {username} + ; + }, []); + + return ; +} + +export default IntegrationsTable; diff --git a/client/admin/integrations/OutgoiongWebhookForm.js b/client/admin/integrations/OutgoiongWebhookForm.js new file mode 100644 index 000000000000..2f6fe9fa4900 --- /dev/null +++ b/client/admin/integrations/OutgoiongWebhookForm.js @@ -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 + + + + { useMemo(() => + {t('Event_Trigger')} + + + + + , [retryDelay])} + { useMemo(() => event === 'sendMessage' && + + + {t('Integration_Word_Trigger_Placement')} + + + {t('Integration_Word_Trigger_Placement_Description')} + + + + {t('Integration_Word_Trigger_Placement')} + + + {t('Integration_Run_When_Message_Is_Edited_Description')} + + , [triggerWordAnywhere, runOnEdits])} + + + { append } + + + ; +} diff --git a/client/admin/integrations/edit/EditIncomingWebhook.js b/client/admin/integrations/edit/EditIncomingWebhook.js new file mode 100644 index 000000000000..2a6363d9f224 --- /dev/null +++ b/client/admin/integrations/edit/EditIncomingWebhook.js @@ -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 + + + + + + + ; + } + + if (error) { + return {t('Oops_page_not_found')}; + } + + return ; +} + +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( { closeModal(); router.push({}); }}/>); } + }; + + setModal(); + }; + + 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(() => + + + + + + + + + + ); + + + return <> + + { modal } + ; +} diff --git a/client/admin/integrations/edit/EditIntegrationsPage.js b/client/admin/integrations/edit/EditIntegrationsPage.js new file mode 100644 index 000000000000..3ffdb8168c79 --- /dev/null +++ b/client/admin/integrations/edit/EditIntegrationsPage.js @@ -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 + + + {t('Are_you_sure')} + + + + {t('Integration_Delete_Warning')} + + + + + + + + ; +}; + +export const SuccessModal = ({ onClose, ...props }) => { + const t = useTranslation(); + return + + + {t('Deleted')} + + + + {t('Your_entry_has_been_deleted')} + + + + + + + ; +}; + +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 + + + + {type === 'outgoing' && } + + + + { + (type === 'outgoing' && ) + || (type === 'incoming' && ) + } + + ; +} diff --git a/client/admin/integrations/edit/EditOutgoingWebhook.js b/client/admin/integrations/edit/EditOutgoingWebhook.js new file mode 100644 index 000000000000..7c20820b2a82 --- /dev/null +++ b/client/admin/integrations/edit/EditOutgoingWebhook.js @@ -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 + + + + + + + ; + } + + if (error) { + return {t('Oops_page_not_found')}; + } + + return ; +} + +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( { closeModal(); router.push({}); }}/>); } + }; + + setModal(); + }; + + 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(() => + + + + + + + + + + ); + + + return <> + + { modal } + ; +} diff --git a/client/admin/integrations/edit/OutgoingWebhookHistoryPage.js b/client/admin/integrations/edit/OutgoingWebhookHistoryPage.js new file mode 100644 index 000000000000..e931ade0b90d --- /dev/null +++ b/client/admin/integrations/edit/OutgoingWebhookHistoryPage.js @@ -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 + + {formatDateAndTime(_createdAt)} + + + + } + {...props} + > + + + {t('Status')} + + + {error ? t('Failure') : t('Success')} + + + + + {t('Integration_Outgoing_WebHook_History_Time_Triggered')} + + + {_createdAt} + + + + + {t('Integration_Outgoing_WebHook_History_Time_Ended_Or_Error')} + + + {_updatedAt} + + + + + {t('Event_Trigger')} + + + {t(eventList.outgoingEvents[event].label)} + + + + + {t('Integration_Outgoing_WebHook_History_Trigger_Step')} + + + {step} + + + + + {t('Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger')} + + +
+
+
+
+ {prepareSentMessage && + {t('Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script')} + + +
+
+
+
} + {processSentMessage && + {t('Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script')} + + +
+
+
+
} + {url && + {t('URL')} + + + {url} + + + } + {httpCallData && + {t('Integration_Outgoing_WebHook_History_Data_Passed_To_URL')} + + +
+
+
+
} + {httpError && + {t('Integration_Outgoing_WebHook_History_Http_Response_Error')} + + +
+
+
+
} + {httpResult && + {t('Integration_Outgoing_WebHook_History_Http_Response')} + + +
+
+
+
} + {errorStack && + {t('Integration_Outgoing_WebHook_History_Error_Stacktrace')} + + +
+
+
+
} +
+
; +} + +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 + + + + + + + ; + } + + if (loadedData.history.length < 1) { + return {t('Integration_Outgoing_WebHook_No_History')}; + } + + return <> + + {loadedData.history.map((current) => )} + + ; +} + +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 + + + + + + + + + + + ; +} + +export default OutgoingWebhookHistoryPage; diff --git a/client/admin/integrations/exampleIncomingData.js b/client/admin/integrations/exampleIncomingData.js new file mode 100644 index 000000000000..0226156f69fc --- /dev/null +++ b/client/admin/integrations/exampleIncomingData.js @@ -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); +} diff --git a/client/admin/integrations/new/NewBot.js b/client/admin/integrations/new/NewBot.js new file mode 100644 index 000000000000..54ff4615574e --- /dev/null +++ b/client/admin/integrations/new/NewBot.js @@ -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 ; +} diff --git a/client/admin/integrations/new/NewIncomingWebhook.js b/client/admin/integrations/new/NewIncomingWebhook.js new file mode 100644 index 000000000000..99cdbc628276 --- /dev/null +++ b/client/admin/integrations/new/NewIncomingWebhook.js @@ -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(() => + + + + + + + + + ); + + return ; +} diff --git a/client/admin/integrations/new/NewIntegrationsPage.js b/client/admin/integrations/new/NewIntegrationsPage.js new file mode 100644 index 000000000000..70dfc441127d --- /dev/null +++ b/client/admin/integrations/new/NewIntegrationsPage.js @@ -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 + + + + + + + + {t('Incoming')} + + + {t('Outgoing')} + + + + { + (tab === 'incoming' && ) + || (tab === 'outgoing' && ) + } + + ; +} diff --git a/client/admin/integrations/new/NewOutgoingWebhook.js b/client/admin/integrations/new/NewOutgoingWebhook.js new file mode 100644 index 000000000000..f211f07d5ba1 --- /dev/null +++ b/client/admin/integrations/new/NewOutgoingWebhook.js @@ -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(() => + + + + ); + + + return ; +} diff --git a/client/admin/integrations/new/NewZapier.js b/client/admin/integrations/new/NewZapier.js new file mode 100644 index 000000000000..a9f9e1513cfc --- /dev/null +++ b/client/admin/integrations/new/NewZapier.js @@ -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 <> + + {!script && + + + + + + + + } + + ; +} diff --git a/client/admin/oauthApps/OAuthAddApp.js b/client/admin/oauthApps/OAuthAddApp.js new file mode 100644 index 000000000000..a31a083288bd --- /dev/null +++ b/client/admin/oauthApps/OAuthAddApp.js @@ -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 + + + + {t('Active')} + !active)}/> + + + + {t('Application_Name')} + + + + {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} + + + {t('Redirect_URI')} + + + + {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} + + + + + + + + + + + ; +} diff --git a/client/admin/oauthApps/OAuthAppsPage.js b/client/admin/oauthApps/OAuthAppsPage.js new file mode 100644 index 000000000000..a42e5b863675 --- /dev/null +++ b/client/admin/oauthApps/OAuthAppsPage.js @@ -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 + + + {context && } + {!context && } + + + {!context && } + {context === 'edit' && } + {context === 'new' && } + + + ; +} + +export default OAuthAppsPage; diff --git a/client/admin/oauthApps/OAuthAppsRoute.js b/client/admin/oauthApps/OAuthAppsRoute.js new file mode 100644 index 000000000000..8f163d0cb483 --- /dev/null +++ b/client/admin/oauthApps/OAuthAppsRoute.js @@ -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 ; + } + + return ; +} diff --git a/client/admin/oauthApps/OAuthAppsTable.js b/client/admin/oauthApps/OAuthAppsTable.js new file mode 100644 index 000000000000..91b1ec301b06 --- /dev/null +++ b/client/admin/oauthApps/OAuthAppsTable.js @@ -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(() => [ + {t('Name')}, + {t('Created_by')}, + {t('Created_at')}, + ]); + + const renderRow = useCallback(({ _id, name, _createdAt, _createdBy: { username: createdBy } }) => + + {name} + {createdBy} + {formatDateAndTime(_createdAt)} + , + ); + + return ; +} + +export default OAuthAppsTable; diff --git a/client/admin/oauthApps/OAuthEditApp.js b/client/admin/oauthApps/OAuthEditApp.js new file mode 100644 index 000000000000..b525bbada583 --- /dev/null +++ b/client/admin/oauthApps/OAuthEditApp.js @@ -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 + + + {t('Are_you_sure')} + + + + {t('Application_delete_warning')} + + + + + + + + ; +}; + +const SuccessModal = ({ onClose, ...props }) => { + const t = useTranslation(); + return + + + {t('Deleted')} + + + + {t('Your_entry_has_been_deleted')} + + + + + + + ; +}; + +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 + + + + + + + + + + + + ; + } + + if (error || !data || !_id) { + return {t('error-application-not-found')}; + } + + return ; +} + +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(() => { setModal(); close(); }}/>); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, [data._id]); + + const openConfirmDelete = () => setModal(() => setModal(undefined)}/>); + + const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) }); + + const { + active, + name, + redirectUri, + } = newData; + + return <> + + + + + {t('Active')} + !active)}/> + + + + {t('Application_Name')} + + + + {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} + + + {t('Redirect_URI')} + + + + {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} + + + {t('Client_ID')} + + + + + + {t('Client_Secret')} + + + + + + {t('Authorization_URL')} + + + + + + {t('Access_Token_URL')} + + + + + + + + + + + + + + + + + + + + + + { modal } + ; +} diff --git a/client/admin/rooms/EditRoom.js b/client/admin/rooms/EditRoom.js index d60ca438a58d..f77b06334afb 100644 --- a/client/admin/rooms/EditRoom.js +++ b/client/admin/rooms/EditRoom.js @@ -1,5 +1,5 @@ import React, { useCallback, useState, useMemo } from 'react'; -import { Box, Headline, Button, Margins, TextInput, Skeleton, Field, ToggleSwitch, Divider, Icon, Callout } from '@rocket.chat/fuselage'; +import { Box, 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'; @@ -22,11 +22,11 @@ function EditRoomWithData({ rid }) { if (state === ENDPOINT_STATES.LOADING) { return - + - + - + ; } diff --git a/client/admin/rooms/edit/EditRoom.js b/client/admin/rooms/edit/EditRoom.js new file mode 100644 index 000000000000..7638bebcc00c --- /dev/null +++ b/client/admin/rooms/edit/EditRoom.js @@ -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 ? : ; +} + +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 + + + + + + + ; + } + + if (state === ENDPOINT_STATES.ERROR) { + return error.message; + } + + return 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 + + + {deleted && } + + + {t('Name')} + + + + + { room.t !== 'd' && <> + + {t('Owner')} + + {room.u?.username} + + + + {t('Topic')} + + + + + + + + + {t('Public')} + {t('All_users_in_the_channel_can_write_new_messages')} + + + + + + {t('Private')} + {t('Just_invited_people_can_access_this_channel')} + + + + + + + + + + {t('Collaborative')} + {t('All_users_in_the_channel_can_write_new_messages')} + + + !readOnly)}/> + + + {t('Read_only')} + {t('Only_authorized_users_can_write_new_messages')} + + + + + + + + + {t('Archived')} + setChangeArchivation(!changeArchivation)}/> + + + + + + + {t('Default')} + !isDefault)}/> + + + + + + + {t('Favorite')} + !isFavorite)}/> + + + + + + + {t('Featured')} + !isFeatured)}/> + + + + + + + + + + + + + + } + + + + + + + ; +} diff --git a/client/admin/routes.js b/client/admin/routes.js index 4df152660524..6ede81d1ef1d 100644 --- a/client/admin/routes.js +++ b/client/admin/routes.js @@ -72,6 +72,16 @@ registerAdminRoute('/mailer', { lazyRouteComponent: () => import('./mailer/MailerRoute'), }); +registerAdminRoute('/oauth-apps/:context?/:id?', { + name: 'admin-oauth-apps', + lazyRouteComponent: () => import('./oauthApps/OAuthAppsRoute'), +}); + +registerAdminRoute('/integrations/:context?/:type?/:id?', { + name: 'admin-integrations', + lazyRouteComponent: () => import('./integrations/IntegrationsRoute'), +}); + registerAdminRoute('/custom-user-status/:context?/:id?', { name: 'custom-user-status', lazyRouteComponent: () => import('./customUserStatus/CustomUserStatusRoute'), @@ -97,6 +107,11 @@ registerAdminRoute('/invites', { lazyRouteComponent: () => import('./invites/InvitesRoute'), }); +registerAdminRoute('/cloud/:page?', { + name: 'cloud', + lazyRouteComponent: () => import('./cloud/CloudRoute'), +}); + registerAdminRoute('/view-logs', { name: 'admin-view-logs', lazyRouteComponent: () => import('./viewLogs/ViewLogsRoute'), diff --git a/client/admin/sidebarItems.js b/client/admin/sidebarItems.js index a76b96d08dda..c5112049b571 100644 --- a/client/admin/sidebarItems.js +++ b/client/admin/sidebarItems.js @@ -42,6 +42,13 @@ registerAdminSidebarItem({ permissionGranted: () => hasPermission('create-invite-links'), }); +registerAdminSidebarItem({ + icon: 'cloud-plus', + href: 'cloud', + i18nLabel: 'Connectivity_Services', + permissionGranted: () => hasPermission('manage-cloud'), +}); + registerAdminSidebarItem({ href: 'admin-view-logs', i18nLabel: 'View_Logs', diff --git a/client/admin/users/EditUser.js b/client/admin/users/EditUser.js index a7b287f234ac..1feb1eb82c34 100644 --- a/client/admin/users/EditUser.js +++ b/client/admin/users/EditUser.js @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { Field, TextInput, Box, Headline, Skeleton, ToggleSwitch, Icon, TextAreaInput, MultiSelectFiltered, Margins, Button } from '@rocket.chat/fuselage'; +import { Field, TextInput, Box, Skeleton, ToggleSwitch, Icon, TextAreaInput, MultiSelectFiltered, Margins, Button } from '@rocket.chat/fuselage'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointData } from '../../hooks/useEndpointData'; @@ -18,11 +18,11 @@ export function EditUserWithData({ userId, ...props }) { if (state === ENDPOINT_STATES.LOADING) { return - + - + - + ; } diff --git a/client/admin/users/InviteUsers.js b/client/admin/users/InviteUsers.js index 5804f498c863..d47db76f7ff0 100644 --- a/client/admin/users/InviteUsers.js +++ b/client/admin/users/InviteUsers.js @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { Box, Headline, Button, Icon, TextAreaInput } from '@rocket.chat/fuselage'; +import { Box, Button, Icon, TextAreaInput } from '@rocket.chat/fuselage'; import { useTranslation } from '../../contexts/TranslationContext'; import { useMethod } from '../../contexts/ServerContext'; @@ -24,7 +24,7 @@ export function InviteUsers({ data, ...props }) { }); }; return - {t('Send_invitation_email')} + {t('Send_invitation_email')} {t('Send_invitation_email_info')} setText(e.currentTarget.value)}/>