diff options
Diffstat (limited to 'webapp/components/integrations')
11 files changed, 2069 insertions, 0 deletions
diff --git a/webapp/components/integrations/components/add_command.jsx b/webapp/components/integrations/components/add_command.jsx new file mode 100644 index 000000000..e72670e47 --- /dev/null +++ b/webapp/components/integrations/components/add_command.jsx @@ -0,0 +1,567 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {browserHistory, Link} from 'react-router/es6'; +import SpinnerButton from 'components/spinner_button.jsx'; +import Constants from 'utils/constants.jsx'; + +const REQUEST_POST = 'P'; +const REQUEST_GET = 'G'; + +export default class AddCommand extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateDisplayName = this.updateDisplayName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateTrigger = this.updateTrigger.bind(this); + this.updateUrl = this.updateUrl.bind(this); + this.updateMethod = this.updateMethod.bind(this); + this.updateUsername = this.updateUsername.bind(this); + this.updateIconUrl = this.updateIconUrl.bind(this); + this.updateAutocomplete = this.updateAutocomplete.bind(this); + this.updateAutocompleteHint = this.updateAutocompleteHint.bind(this); + this.updateAutocompleteDescription = this.updateAutocompleteDescription.bind(this); + + this.state = { + displayName: '', + description: '', + trigger: '', + url: '', + method: REQUEST_POST, + username: '', + iconUrl: '', + autocomplete: false, + autocompleteHint: '', + autocompleteDescription: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + const command = { + display_name: this.state.displayName, + description: this.state.description, + trigger: this.state.trigger.trim(), + url: this.state.url.trim(), + method: this.state.method, + username: this.state.username, + icon_url: this.state.iconUrl, + auto_complete: this.state.autocomplete + }; + + if (command.auto_complete) { + command.auto_complete_desc = this.state.autocompleteDescription; + command.auto_complete_hint = this.state.autocompleteHint; + } + + if (!command.trigger) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_command.triggerRequired' + defaultMessage='A trigger word is required' + /> + ) + }); + + return; + } + + if (command.trigger.indexOf('/') === 0) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_command.triggerInvalidSlash' + defaultMessage='A trigger word cannot begin with a /' + /> + ) + }); + + return; + } + + if (command.trigger.indexOf(' ') !== -1) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_command.triggerInvalidSpace' + defaultMessage='A trigger word must not contain spaces' + /> + ) + }); + return; + } + + if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_command.triggerInvalidLength' + defaultMessage='A trigger word must contain between {min} and {max} characters' + values={{ + min: Constants.MIN_TRIGGER_LENGTH, + max: Constants.MAX_TRIGGER_LENGTH + }} + /> + ) + }); + + return; + } + + if (!command.url) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_command.urlRequired' + defaultMessage='A request URL is required' + /> + ) + }); + + return; + } + + AsyncClient.addCommand( + command, + () => { + browserHistory.push('/' + this.props.team.name + '/integrations/commands'); + }, + (err) => { + this.setState({ + saving: false, + serverError: err.message + }); + } + ); + } + + updateDisplayName(e) { + this.setState({ + displayName: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateTrigger(e) { + this.setState({ + trigger: e.target.value + }); + } + + updateUrl(e) { + this.setState({ + url: e.target.value + }); + } + + updateMethod(e) { + this.setState({ + method: e.target.value + }); + } + + updateUsername(e) { + this.setState({ + username: e.target.value + }); + } + + updateIconUrl(e) { + this.setState({ + iconUrl: e.target.value + }); + } + + updateAutocomplete(e) { + this.setState({ + autocomplete: e.target.checked + }); + } + + updateAutocompleteHint(e) { + this.setState({ + autocompleteHint: e.target.value + }); + } + + updateAutocompleteDescription(e) { + this.setState({ + autocompleteDescription: e.target.value + }); + } + + render() { + let autocompleteFields = null; + if (this.state.autocomplete) { + autocompleteFields = [( + <div + key='autocompleteHint' + className='form-group' + > + <label + className='control-label col-sm-4' + htmlFor='autocompleteHint' + > + <FormattedMessage + id='add_command.autocompleteHint' + defaultMessage='Autocomplete Hint' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='autocompleteHint' + type='text' + maxLength='1024' + className='form-control' + value={this.state.autocompleteHint} + onChange={this.updateAutocompleteHint} + placeholder={Utils.localizeMessage('add_command.autocompleteHint.placeholder', 'Example: [Patient Name]')} + /> + <div className='form__help'> + <FormattedMessage + id='add_command.autocompleteDescription.help' + defaultMessage='Optional hint in the autocomplete list about command parameters' + /> + </div> + </div> + </div> + ), + ( + <div + key='autocompleteDescription' + className='form-group' + > + <label + className='control-label col-sm-4' + htmlFor='autocompleteDescription' + > + <FormattedMessage + id='add_command.autocompleteDescription' + defaultMessage='Autocomplete Description' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='description' + type='text' + maxLength='128' + className='form-control' + value={this.state.autocompleteDescription} + onChange={this.updateAutocompleteDescription} + placeholder={Utils.localizeMessage('add_command.autocompleteDescription.placeholder', 'Example: "Returns search results for patient records"')} + /> + <div className='form__help'> + <FormattedMessage + id='add_command.autocompleteDescription.help' + defaultMessage='Optional short description of slash command for the autocomplete list.' + /> + </div> + </div> + </div> + )]; + } + + return ( + <div className='backstage-content row'> + <BackstageHeader> + <Link to={'/' + this.props.team.name + '/integrations/commands'}> + <FormattedMessage + id='installed_command.header' + defaultMessage='Slash Commands' + /> + </Link> + <FormattedMessage + id='add_command.header' + defaultMessage='Add' + /> + </BackstageHeader> + <div className='backstage-form'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='displayName' + > + <FormattedMessage + id='add_command.displayName' + defaultMessage='Display Name' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='displayName' + type='text' + maxLength='64' + className='form-control' + value={this.state.displayName} + onChange={this.updateDisplayName} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='description' + > + <FormattedMessage + id='add_command.description' + defaultMessage='Description' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='description' + type='text' + maxLength='128' + className='form-control' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='trigger' + > + <FormattedMessage + id='add_command.trigger' + defaultMessage='Command Trigger Word' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='trigger' + type='text' + maxLength={Constants.MAX_TRIGGER_LENGTH} + className='form-control' + value={this.state.trigger} + onChange={this.updateTrigger} + placeholder={Utils.localizeMessage('add_command.trigger.placeholder', 'Command trigger e.g. "hello" not including the slash')} + /> + <div className='form__help'> + <FormattedMessage + id='add_command.trigger.help1' + defaultMessage='Examples: /patient, /client /employee' + /> + </div> + <div className='form__help'> + <FormattedMessage + id='add_command.trigger.help2' + defaultMessage='Reserved: /echo, /join, /logout, /me, /shrug' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='url' + > + <FormattedMessage + id='add_command.url' + defaultMessage='Request URL' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='url' + type='text' + maxLength='1024' + className='form-control' + value={this.state.url} + onChange={this.updateUrl} + placeholder={Utils.localizeMessage('add_command.url.placeholder', 'Must start with http:// or https://')} + /> + <div className='form__help'> + <FormattedMessage + id='add_command.url.help' + defaultMessage='The callback URL to receive the HTTP POST or GET event request when the slash command is run.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='method' + > + <FormattedMessage + id='add_command.method' + defaultMessage='Request Method' + /> + </label> + <div className='col-md-5 col-sm-8'> + <select + id='method' + className='form-control' + value={this.state.method} + onChange={this.updateMethod} + > + <option value={REQUEST_POST}> + {Utils.localizeMessage('add_command.method.post', 'POST')} + </option> + <option value={REQUEST_GET}> + {Utils.localizeMessage('add_command.method.get', 'GET')} + </option> + </select> + <div className='form__help'> + <FormattedMessage + id='add_command.method.help' + defaultMessage='The type of command request issued to the Request URL.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='username' + > + <FormattedMessage + id='add_command.username' + defaultMessage='Response Username' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='username' + type='text' + maxLength='64' + className='form-control' + value={this.state.username} + onChange={this.updateUsername} + placholder={Utils.localizeMessage('add_command.username.placeholder', 'Username')} + /> + <div className='form__help'> + <FormattedMessage + id='add_command.username.help' + defaultMessage='Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and the symbols "-", "_", and ".".' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='iconUrl' + > + <FormattedMessage + id='add_command.iconUrl' + defaultMessage='Response Icon' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='iconUrl' + type='text' + maxLength='1024' + className='form-control' + value={this.state.iconUrl} + onChange={this.updateIconUrl} + placeholder={Utils.localizeMessage('add_command.iconUrl.placeholder', 'https://www.example.com/myicon.png')} + /> + <div className='form__help'> + <FormattedMessage + id='add_command.iconUrl.help' + defaultMessage='Choose a profile picture override for the post responses to this slash command. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.' + /> + </div> + </div> + </div> + <div className='form-group padding-bottom'> + <div className='col-sm-12'> + <div className='checkbox'> + <input + type='checkbox' + checked={this.state.autocomplete} + onChange={this.updateAutocomplete} + /> + <FormattedMessage + id='add_command.autocomplete' + defaultMessage='Autocomplete' + /> + </div> + <div className='form__help'> + <FormattedMessage + id='add_command.autocomplete.help' + defaultMessage='Show this command in the autocomplete list' + /> + </div> + </div> + </div> + {autocompleteFields} + <div className='backstage-form__footer'> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + <Link + className='btn btn-sm' + to={'/' + this.props.team.name + '/integrations/commands'} + > + <FormattedMessage + id='add_command.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_command.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook.jsx new file mode 100644 index 000000000..122600c90 --- /dev/null +++ b/webapp/components/integrations/components/add_incoming_webhook.jsx @@ -0,0 +1,216 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; + +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {browserHistory, Link} from 'react-router/es6'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddIncomingWebhook extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateDisplayName = this.updateDisplayName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + + this.state = { + displayName: '', + description: '', + channelId: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + if (!this.state.channelId) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_incoming_webhook.channelRequired' + defaultMessage='A valid channel is required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + display_name: this.state.displayName, + description: this.state.description + }; + + AsyncClient.addIncomingHook( + hook, + () => { + browserHistory.push('/' + this.props.team.name + '/integrations/incoming_webhooks'); + }, + (err) => { + this.setState({ + saving: false, + serverError: err.message + }); + } + ); + } + + updateDisplayName(e) { + this.setState({ + displayName: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + render() { + return ( + <div className='backstage-content'> + <BackstageHeader> + <Link to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}> + <FormattedMessage + id='installed_incoming_webhooks.header' + defaultMessage='Incoming Webhooks' + /> + </Link> + <FormattedMessage + id='add_incoming_webhook.header' + defaultMessage='Add' + /> + </BackstageHeader> + <div className='backstage-form'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='displayName' + > + <FormattedMessage + id='add_incoming_webhook.displayName' + defaultMessage='Display Name' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='displayName' + type='text' + maxLength='64' + className='form-control' + value={this.state.displayName} + onChange={this.updateDisplayName} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='description' + > + <FormattedMessage + id='add_incoming_webhook.description' + defaultMessage='Description' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='description' + type='text' + maxLength='128' + className='form-control' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='channelId' + > + <FormattedMessage + id='add_incoming_webhook.channel' + defaultMessage='Channel' + /> + </label> + <div className='col-md-5 col-sm-8'> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + selectOpen={true} + selectPrivate={true} + /> + </div> + </div> + <div className='backstage-form__footer'> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + <Link + className='btn btn-sm' + to={'/' + this.props.team.name + '/integrations/incoming_webhooks'} + > + <FormattedMessage + id='add_incoming_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_incoming_webhook.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx new file mode 100644 index 000000000..bd49fedc9 --- /dev/null +++ b/webapp/components/integrations/components/add_outgoing_webhook.jsx @@ -0,0 +1,349 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; + +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {browserHistory, Link} from 'react-router/es6'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddOutgoingWebhook extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateDisplayName = this.updateDisplayName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateContentType = this.updateContentType.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + this.updateTriggerWords = this.updateTriggerWords.bind(this); + this.updateCallbackUrls = this.updateCallbackUrls.bind(this); + + this.state = { + displayName: '', + description: '', + contentType: 'application/x-www-form-urlencoded', + channelId: '', + triggerWords: '', + callbackUrls: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + const triggerWords = []; + if (this.state.triggerWords) { + for (let triggerWord of this.state.triggerWords.split('\n')) { + triggerWord = triggerWord.trim(); + + if (triggerWord.length > 0) { + triggerWords.push(triggerWord); + } + } + } + + if (!this.state.channelId && triggerWords.length === 0) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.triggerWordsOrChannelRequired' + defaultMessage='A valid channel or a list of trigger words is required' + /> + ) + }); + + return; + } + + const callbackUrls = []; + for (let callbackUrl of this.state.callbackUrls.split('\n')) { + callbackUrl = callbackUrl.trim(); + + if (callbackUrl.length > 0) { + callbackUrls.push(callbackUrl); + } + } + + if (callbackUrls.length === 0) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.callbackUrlsRequired' + defaultMessage='One or more callback URLs are required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + trigger_words: triggerWords, + callback_urls: callbackUrls, + display_name: this.state.displayName, + content_type: this.state.contentType, + description: this.state.description + }; + + AsyncClient.addOutgoingHook( + hook, + () => { + browserHistory.push('/' + this.props.team.name + '/integrations/outgoing_webhooks'); + }, + (err) => { + this.setState({ + saving: false, + serverError: err.message + }); + } + ); + } + + updateDisplayName(e) { + this.setState({ + displayName: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateContentType(e) { + this.setState({ + contentType: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + updateTriggerWords(e) { + this.setState({ + triggerWords: e.target.value + }); + } + + updateCallbackUrls(e) { + this.setState({ + callbackUrls: e.target.value + }); + } + + render() { + const contentTypeOption1 = 'application/x-www-form-urlencoded'; + const contentTypeOption2 = 'application/json'; + return ( + <div className='backstage-content'> + <BackstageHeader> + <Link to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}> + <FormattedMessage + id='installed_outgoing_webhooks.header' + defaultMessage='Outgoing Webhooks' + /> + </Link> + <FormattedMessage + id='add_outgoing_webhook.header' + defaultMessage='Add' + /> + </BackstageHeader> + <div className='backstage-form'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='displayName' + > + <FormattedMessage + id='add_outgoing_webhook.displayName' + defaultMessage='Display Name' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='displayName' + type='text' + maxLength='64' + className='form-control' + value={this.state.displayName} + onChange={this.updateDisplayName} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='description' + > + <FormattedMessage + id='add_outgoing_webhook.description' + defaultMessage='Description' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='description' + type='text' + maxLength='128' + className='form-control' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='contentType' + > + <FormattedMessage + id='add_outgoing_webhook.content_Type' + defaultMessage='Content Type' + /> + </label> + <div className='col-md-5 col-sm-8'> + <select + className='form-control' + value={this.state.contentType} + onChange={this.updateContentType} + > + <option + value={contentTypeOption1} + > + {contentTypeOption1} + </option> + <option + value={contentTypeOption2} + > + {contentTypeOption2} + </option> + </select> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='channelId' + > + <FormattedMessage + id='add_outgoing_webhook.channel' + defaultMessage='Channel' + /> + </label> + <div className='col-md-5 col-sm-8'> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + selectOpen={true} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='triggerWords' + > + <FormattedMessage + id='add_outgoing_webhook.triggerWords' + defaultMessage='Trigger Words (One Per Line)' + /> + </label> + <div className='col-md-5 col-sm-8'> + <textarea + id='triggerWords' + rows='3' + maxLength='1000' + className='form-control' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='callbackUrls' + > + <FormattedMessage + id='add_outgoing_webhook.callbackUrls' + defaultMessage='Callback URLs (One Per Line)' + /> + </label> + <div className='col-md-5 col-sm-8'> + <textarea + id='callbackUrls' + rows='3' + maxLength='1000' + className='form-control' + value={this.state.callbackUrls} + onChange={this.updateCallbackUrls} + /> + </div> + </div> + <div className='backstage-form__footer'> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + <Link + className='btn btn-sm' + to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'} + > + <FormattedMessage + id='add_outgoing_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_outgoing_webhook.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/installed_command.jsx b/webapp/components/integrations/components/installed_command.jsx new file mode 100644 index 000000000..658126f19 --- /dev/null +++ b/webapp/components/integrations/components/installed_command.jsx @@ -0,0 +1,147 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledCommand extends React.Component { + static get propTypes() { + return { + command: React.PropTypes.object.isRequired, + onRegenToken: React.PropTypes.func.isRequired, + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string + }; + } + + constructor(props) { + super(props); + + this.handleRegenToken = this.handleRegenToken.bind(this); + this.handleDelete = this.handleDelete.bind(this); + + this.matchesFilter = this.matchesFilter.bind(this); + } + + handleRegenToken(e) { + e.preventDefault(); + + this.props.onRegenToken(this.props.command); + } + + handleDelete(e) { + e.preventDefault(); + + this.props.onDelete(this.props.command); + } + + matchesFilter(command, filter) { + if (!filter) { + return true; + } + + return command.display_name.toLowerCase().indexOf(filter) !== -1 || + command.description.toLowerCase().indexOf(filter) !== -1 || + command.trigger.toLowerCase().indexOf(filter) !== -1; + } + + render() { + const command = this.props.command; + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; + + if (!this.matchesFilter(command, filter)) { + return null; + } + + let name; + if (command.display_name) { + name = command.display_name; + } else { + name = ( + <FormattedMessage + id='installed_commands.unnamed_command' + defaultMessage='Unnamed Slash Command' + /> + ); + } + + let description = null; + if (command.description) { + description = ( + <div className='item-details__row'> + <span className='item-details__description'> + {command.description} + </span> + </div> + ); + } + + let trigger = '- /' + command.trigger; + if (command.auto_complete && command.auto_complete_hint) { + trigger += ' ' + command.auto_complete_hint; + } + + return ( + <div className='backstage-list__item'> + <div className='item-details'> + <div className='item-details__row'> + <span className='item-details__name'> + {name} + </span> + <span className='item-details__trigger'> + {trigger} + </span> + </div> + {description} + <div className='item-details__row'> + <span className='item-details__token'> + <FormattedMessage + id='installed_integrations.token' + defaultMessage='Token: {token}' + values={{ + token: command.token + }} + /> + </span> + </div> + <div className='item-details__row'> + <span className='item-details__creation'> + <FormattedMessage + id='installed_integrations.creation' + defaultMessage='Created by {creator} on {createAt, date, full}' + values={{ + creator: Utils.displayUsername(command.creator_id), + createAt: command.create_at + }} + /> + </span> + </div> + </div> + <div className='item-actions'> + <a + href='#' + onClick={this.handleRegenToken} + > + <FormattedMessage + id='installed_integrations.regenToken' + defaultMessage='Regenerate Token' + /> + </a> + {' - '} + <a + href='#' + onClick={this.handleDelete} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/installed_commands.jsx b/webapp/components/integrations/components/installed_commands.jsx new file mode 100644 index 000000000..597ba7005 --- /dev/null +++ b/webapp/components/integrations/components/installed_commands.jsx @@ -0,0 +1,107 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import IntegrationStore from 'stores/integration_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import {FormattedMessage} from 'react-intl'; +import InstalledCommand from './installed_command.jsx'; + +export default class InstalledCommands extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + + this.regenCommandToken = this.regenCommandToken.bind(this); + this.deleteCommand = this.deleteCommand.bind(this); + + const teamId = TeamStore.getCurrentId(); + + this.state = { + commands: IntegrationStore.getCommands(teamId), + loading: !IntegrationStore.hasReceivedCommands(teamId) + }; + } + + componentDidMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableCommands === 'true') { + AsyncClient.listTeamCommands(); + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + const teamId = TeamStore.getCurrentId(); + + this.setState({ + commands: IntegrationStore.getCommands(teamId), + loading: !IntegrationStore.hasReceivedCommands(teamId) + }); + } + + regenCommandToken(command) { + AsyncClient.regenCommandToken(command.id); + } + + deleteCommand(command) { + AsyncClient.deleteCommand(command.id); + } + + render() { + const commands = this.state.commands.map((command) => { + return ( + <InstalledCommand + key={command.id} + command={command} + onRegenToken={this.regenCommandToken} + onDelete={this.deleteCommand} + /> + ); + }); + + return ( + <BackstageList + header={ + <FormattedMessage + id='installed_commands.header' + defaultMessage='Installed Slash Commands' + /> + } + addText={ + <FormattedMessage + id='installed_commands.add' + defaultMessage='Add Slash Command' + /> + } + addLink={'/' + this.props.team.name + '/integrations/commands/add'} + emptyText={ + <FormattedMessage + id='installed_commands.empty' + defaultMessage='No slash commands found' + /> + } + searchPlaceholder={Utils.localizeMessage('installed_commands.search', 'Search Slash Commands')} + loading={this.state.loading} + > + {commands} + </BackstageList> + ); + } +} diff --git a/webapp/components/integrations/components/installed_incoming_webhook.jsx b/webapp/components/integrations/components/installed_incoming_webhook.jsx new file mode 100644 index 000000000..2cf3f24b8 --- /dev/null +++ b/webapp/components/integrations/components/installed_incoming_webhook.jsx @@ -0,0 +1,132 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledIncomingWebhook extends React.Component { + static get propTypes() { + return { + incomingWebhook: React.PropTypes.object.isRequired, + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string + }; + } + + constructor(props) { + super(props); + + this.handleDelete = this.handleDelete.bind(this); + } + + handleDelete(e) { + e.preventDefault(); + + this.props.onDelete(this.props.incomingWebhook); + } + + matchesFilter(incomingWebhook, channel, filter) { + if (!filter) { + return true; + } + + if (incomingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 || + incomingWebhook.description.toLowerCase().indexOf(filter) !== -1) { + return true; + } + + if (incomingWebhook.channel_id) { + if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + return false; + } + + render() { + const incomingWebhook = this.props.incomingWebhook; + const channel = ChannelStore.get(incomingWebhook.channel_id); + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; + + if (!this.matchesFilter(incomingWebhook, channel, filter)) { + return null; + } + + let displayName; + if (incomingWebhook.display_name) { + displayName = incomingWebhook.display_name; + } else if (channel) { + displayName = channel.display_name; + } else { + displayName = ( + <FormattedMessage + id='installed_incoming_webhooks.unknown_channel' + defaultMessage='A Private Webhook' + /> + ); + } + + let description = null; + if (incomingWebhook.description) { + description = ( + <div className='item-details__row'> + <span className='item-details__description'> + {incomingWebhook.description} + </span> + </div> + ); + } + + return ( + <div className='backstage-list__item'> + <div className='item-details'> + <div className='item-details__row'> + <span className='item-details__name'> + {displayName} + </span> + </div> + {description} + <div className='item-details__row'> + <span className='item-details__url'> + <FormattedMessage + id='installed_integrations.url' + defaultMessage='URL: {url}' + values={{ + url: Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id + }} + /> + </span> + </div> + <div className='tem-details__row'> + <span className='item-details__creation'> + <FormattedMessage + id='installed_integrations.creation' + defaultMessage='Created by {creator} on {createAt, date, full}' + values={{ + creator: Utils.displayUsername(incomingWebhook.user_id), + createAt: incomingWebhook.create_at + }} + /> + </span> + </div> + </div> + <div className='item-actions'> + <a + href='#' + onClick={this.handleDelete} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/installed_incoming_webhooks.jsx b/webapp/components/integrations/components/installed_incoming_webhooks.jsx new file mode 100644 index 000000000..a3bcf904e --- /dev/null +++ b/webapp/components/integrations/components/installed_incoming_webhooks.jsx @@ -0,0 +1,101 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import IntegrationStore from 'stores/integration_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import {FormattedMessage} from 'react-intl'; +import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; + +export default class InstalledIncomingWebhooks extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + + this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this); + + const teamId = TeamStore.getCurrentId(); + + this.state = { + incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId), + loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) + }; + } + + componentDidMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + AsyncClient.listIncomingHooks(); + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + const teamId = TeamStore.getCurrentId(); + + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId), + loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) + }); + } + + deleteIncomingWebhook(incomingWebhook) { + AsyncClient.deleteIncomingHook(incomingWebhook.id); + } + + render() { + const incomingWebhooks = this.state.incomingWebhooks.map((incomingWebhook) => { + return ( + <InstalledIncomingWebhook + key={incomingWebhook.id} + incomingWebhook={incomingWebhook} + onDelete={this.deleteIncomingWebhook} + /> + ); + }); + + return ( + <BackstageList + header={ + <FormattedMessage + id='installed_incoming_webhooks.header' + defaultMessage='Installed Incoming Webhooks' + /> + } + addText={ + <FormattedMessage + id='installed_incoming_webhooks.add' + defaultMessage='Add Incoming Webhook' + /> + } + addLink={'/' + this.props.team.name + '/integrations/incoming_webhooks/add'} + emptyText={ + <FormattedMessage + id='installed_incoming_webhooks.empty' + defaultMessage='No incoming webhooks found' + /> + } + searchPlaceholder={Utils.localizeMessage('installed_incoming_webhooks.search', 'Search Incoming Webhooks')} + loading={this.state.loading} + > + {incomingWebhooks} + </BackstageList> + ); + } +} diff --git a/webapp/components/integrations/components/installed_outgoing_webhook.jsx b/webapp/components/integrations/components/installed_outgoing_webhook.jsx new file mode 100644 index 000000000..852231823 --- /dev/null +++ b/webapp/components/integrations/components/installed_outgoing_webhook.jsx @@ -0,0 +1,200 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledOutgoingWebhook extends React.Component { + static get propTypes() { + return { + outgoingWebhook: React.PropTypes.object.isRequired, + onRegenToken: React.PropTypes.func.isRequired, + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string + }; + } + + constructor(props) { + super(props); + + this.handleRegenToken = this.handleRegenToken.bind(this); + this.handleDelete = this.handleDelete.bind(this); + } + + handleRegenToken(e) { + e.preventDefault(); + + this.props.onRegenToken(this.props.outgoingWebhook); + } + + handleDelete(e) { + e.preventDefault(); + + this.props.onDelete(this.props.outgoingWebhook); + } + + matchesFilter(outgoingWebhook, channel, filter) { + if (!filter) { + return true; + } + + if (outgoingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 || + outgoingWebhook.description.toLowerCase().indexOf(filter) !== -1) { + return true; + } + + for (const trigger of outgoingWebhook.trigger_words) { + if (trigger.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + if (channel) { + if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + return false; + } + + render() { + const outgoingWebhook = this.props.outgoingWebhook; + const channel = ChannelStore.get(outgoingWebhook.channel_id); + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; + + if (!this.matchesFilter(outgoingWebhook, channel, filter)) { + return null; + } + + let displayName; + if (outgoingWebhook.display_name) { + displayName = outgoingWebhook.display_name; + } else if (channel) { + displayName = channel.display_name; + } else { + displayName = ( + <FormattedMessage + id='installed_outgoing_webhooks.unknown_channel' + defaultMessage='A Private Webhook' + /> + ); + } + + let description = null; + if (outgoingWebhook.description) { + description = ( + <div className='item-details__row'> + <span className='item-details__description'> + {outgoingWebhook.description} + </span> + </div> + ); + } + + let triggerWords = null; + if (outgoingWebhook.trigger_words && outgoingWebhook.trigger_words.length > 0) { + triggerWords = ( + <div className='item-details__row'> + <span className='item-details__trigger-words'> + <FormattedMessage + id='installed_integrations.triggerWords' + defaultMessage='Trigger Words: {triggerWords}' + values={{ + triggerWords: outgoingWebhook.trigger_words.join(', ') + }} + /> + </span> + </div> + ); + } + + let urls = ( + <div className='item-details__row'> + <span className='item-details__url'> + <FormattedMessage + id='installed_integrations.callback_urls' + defaultMessage='Callback URLs: {urls}' + values={{ + urls: outgoingWebhook.callback_urls.join(', ') + }} + /> + </span> + </div> + ); + + return ( + <div className='backstage-list__item'> + <div className='item-details'> + <div className='item-details__row'> + <span className='item-details__name'> + {displayName} + </span> + </div> + {description} + <div className='item-details__row'> + <span className='item-details__content_type'> + <FormattedMessage + id='installed_integrations.content_type' + defaultMessage='Content-Type: {contentType}' + values={{ + contentType: outgoingWebhook.content_type || 'application/x-www-form-urlencoded' + }} + /> + </span> + </div> + {triggerWords} + <div className='item-details__row'> + <span className='item-details__token'> + <FormattedMessage + id='installed_integrations.token' + defaultMessage='Token: {token}' + values={{ + token: outgoingWebhook.token + }} + /> + </span> + </div> + <div className='item-details__row'> + <span className='item-details__creation'> + <FormattedMessage + id='installed_integrations.creation' + defaultMessage='Created by {creator} on {createAt, date, full}' + values={{ + creator: Utils.displayUsername(outgoingWebhook.creator_id), + createAt: outgoingWebhook.create_at + }} + /> + </span> + </div> + {urls} + </div> + <div className='item-actions'> + <a + href='#' + onClick={this.handleRegenToken} + > + <FormattedMessage + id='installed_integrations.regenToken' + defaultMessage='Regen Token' + /> + </a> + {' - '} + <a + href='#' + onClick={this.handleDelete} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/installed_outgoing_webhooks.jsx b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx new file mode 100644 index 000000000..ebc9a6fc1 --- /dev/null +++ b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx @@ -0,0 +1,107 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import IntegrationStore from 'stores/integration_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import {FormattedMessage} from 'react-intl'; +import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; + +export default class InstalledOutgoingWebhooks extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + + this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this); + this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this); + + const teamId = TeamStore.getCurrentId(); + + this.state = { + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId), + loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId) + }; + } + + componentDidMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + AsyncClient.listOutgoingHooks(); + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + const teamId = TeamStore.getCurrentId(); + + this.setState({ + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId), + loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId) + }); + } + + regenOutgoingWebhookToken(outgoingWebhook) { + AsyncClient.regenOutgoingHookToken(outgoingWebhook.id); + } + + deleteOutgoingWebhook(outgoingWebhook) { + AsyncClient.deleteOutgoingHook(outgoingWebhook.id); + } + + render() { + const outgoingWebhooks = this.state.outgoingWebhooks.map((outgoingWebhook) => { + return ( + <InstalledOutgoingWebhook + key={outgoingWebhook.id} + outgoingWebhook={outgoingWebhook} + onRegenToken={this.regenOutgoingWebhookToken} + onDelete={this.deleteOutgoingWebhook} + /> + ); + }); + + return ( + <BackstageList + header={ + <FormattedMessage + id='installed_outgoing_webhooks.header' + defaultMessage='Installed Outgoing Webhooks' + /> + } + addText={ + <FormattedMessage + id='installed_outgoing_webhooks.add' + defaultMessage='Add Outgoing Webhook' + /> + } + addLink={'/' + this.props.team.name + '/integrations/outgoing_webhooks/add'} + emptyText={ + <FormattedMessage + id='installed_outgoing_webhooks.empty' + defaultMessage='No outgoing webhooks found' + /> + } + searchPlaceholder={Utils.localizeMessage('installed_outgoing_webhooks.search', 'Search Outgoing Webhooks')} + loading={this.state.loading} + > + {outgoingWebhooks} + </BackstageList> + ); + } +} diff --git a/webapp/components/integrations/components/integration_option.jsx b/webapp/components/integrations/components/integration_option.jsx new file mode 100644 index 000000000..483e6a888 --- /dev/null +++ b/webapp/components/integrations/components/integration_option.jsx @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router/es6'; + +export default class IntegrationOption extends React.Component { + static get propTypes() { + return { + image: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + description: React.PropTypes.node.isRequired, + link: React.PropTypes.string.isRequired + }; + } + + render() { + const {image, title, description, link} = this.props; + + return ( + <Link + to={link} + className='integration-option' + > + <img + className='integration-option__image' + src={image} + /> + <div className='integration-option__title'> + {title} + </div> + <div className='integration-option__description'> + {description} + </div> + </Link> + ); + } +} diff --git a/webapp/components/integrations/components/integrations.jsx b/webapp/components/integrations/components/integrations.jsx new file mode 100644 index 000000000..7894ced5d --- /dev/null +++ b/webapp/components/integrations/components/integrations.jsx @@ -0,0 +1,104 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import IntegrationOption from './integration_option.jsx'; + +import WebhookIcon from 'images/webhook_icon.jpg'; + +export default class Integrations extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + render() { + const options = []; + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + options.push( + <IntegrationOption + key='incomingWebhook' + image={WebhookIcon} + title={ + <FormattedMessage + id='integrations.incomingWebhook.title' + defaultMessage='Incoming Webhook' + /> + } + description={ + <FormattedMessage + id='integrations.incomingWebhook.description' + defaultMessage='Incoming webhooks allow external integrations to send messages' + /> + } + link={'/' + this.props.team.name + '/integrations/incoming_webhooks'} + /> + ); + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + options.push( + <IntegrationOption + key='outgoingWebhook' + image={WebhookIcon} + title={ + <FormattedMessage + id='integrations.outgoingWebhook.title' + defaultMessage='Outgoing Webhook' + /> + } + description={ + <FormattedMessage + id='integrations.outgoingWebhook.description' + defaultMessage='Outgoing webhooks allow external integrations to receive and respond to messages' + /> + } + link={'/' + this.props.team.name + '/integrations/outgoing_webhooks'} + /> + ); + } + + if (window.mm_config.EnableCommands === 'true') { + options.push( + <IntegrationOption + key='command' + image={WebhookIcon} + title={ + <FormattedMessage + id='integrations.command.title' + defaultMessage='Slash Command' + /> + } + description={ + <FormattedMessage + id='integrations.command.description' + defaultMessage='Slash commands send events to an external integration' + /> + } + link={'/' + this.props.team.name + '/integrations/commands'} + /> + ); + } + + return ( + <div className='backstage-content row'> + <div className='backstage-header'> + <h1> + <FormattedMessage + id='integrations.header' + defaultMessage='Integrations' + /> + </h1> + </div> + <div> + {options} + </div> + </div> + ); + } +} + |