Webapp - Outgoing OAuth Connections (#25507)
* added store * make generated * add missing license headers * fix receiver name * i18n * i18n sorting * update migrations from master * make migrations-extract * update retrylayer tests * replaced sql query with id pagination * fixed flaky tests * missing columns * missing columns on save/update * typo * improved tests * remove enum from mysql colum * add password credentials to store * license changes * OAuthOutgoingConnectionInterface * Oauth -> OAuth * make generated * copied over installed_oauth_apps component and renamed things to installed_outgoing_oauth_connections * merge migrations * renamed migrations * model change suggestions * refactor test functionsn * migration typo * refactor store table names * updated sanitize test * cleanup merge * refactor symbol * "installed outgoing oauth connections" page works * move things into a nested folder * add and edit page stubs work * list endpoint * oauthoutgoingconnection -> outgoingoauthconnection * signature change * i18n update * granttype typo * naming * api list * uppercase typo * i18n * missing license header * fixed path in comments * updated openapi definitions * changes to support selecting command request url * sanitize connections * make generated * test license and no feature flag * removed t.fatal * updated testhelper calls * yaml schema fixes * switched interface name * suggested translation * missing i18n translation * management permission * moved permission initalization to proper place * endpoints * put tests * error check typo * fixed specific enttity urls * tests * read permission check * updated openapi definitions * i18n * GetConnectionByAudience method * notes * replaced GetConnectionsByAudience with a filter * added custom oauth token object * updated interface and usage * properly set enterprise interface * move retrieval logic to impl * webhook tests * translations * i18n: updates * address comments * endpoint and tests * i18n * api docs * fixed endpoint path * sq.like * use filter object instead of parameters * set url values if not empty * typos * converted some components to function components, and move around files * correctly check token url * restore flag to previous value * added command oauth handler * update enterprise imports * migrate last component to function component * Added enterprise import * refactor permissions and add necessary webapp code * Check correct flag in permission tree * allow partial updates * sort i18n webapp * missing test modification * fixed webapp i18n sorting * allow validating stored connections * added missing translation * fix finished adding connection link and text on result page * added missing permission to smoke tests * missing role in smoke test * updated translations * updated translations * support editing client secret on existing connection * fix some i18n strings * updated translations * better error messages * progress on using react select for command request url while maintaining typed in value * remove writeheader, test * HasValidGrantType * end early to avoid nil pointer errors * move slash command request url input box into its own component * wrap components related to oauth connections in config check * fix tests * i18n-extract * change some i18n strings to say "Outgoing OAuth 2.0 Connections" * remove debug code * fixed i18n * updated i18n file * feature configuration backend * typo * add system console setting * Revert "typo" This reverts commit 669da23e8ee47525ccaa6f59cbbd20bf8a121191. * Revert "updated i18n file" This reverts commit d0882c0dd7587533f0d0f7a7b7b190684186158a. * Revert "fixed i18n" This reverts commit 3108866bc19139182dfd094921c56cdefc4695ea. * fixed i18n * updated i18n file * typo * updated i18n * updated i18n * updated i18n * updated version to 9.6 * replace feature flag with system console configuration * i18n * updated tests * pr feedback * fix styling of disabled text box * fix styling of action links in integration console * server changes for validation feature * webapp changes for validation feature * pencil icon styling * styling fixes for oauth audience correct configuration message * fix sanitize test * remove max lengths from outgoing oauth connection form * use config var in webapp instead of feature flag * change asterisks to bullets * update api docs for validate endpoint * feedback from ux review * fix lint, types, tests * fix stylelint * implement validation button under the token url input * support wildcard for matching audience urls * updates for styling * update snapshots * add doc links for the outgoing oauth connections feature * change doc links to use permalink * add docs link to system console * fix: use limitedreader in json decoding * fix: form error in validation * management permission can read now * updated api documentation * doc typo * require one permission to read only * fix api connection list audience filter * fix audience matching and add loading indicator * fix team permissions on outgoing oauth connection api calls * fix api doc and test, for adding team id to query params * handle read permissions by adding a team in the payload * missing teamid query parameter in test * change validate button logic to not require audience urls to be filled out * fix redux type --------- Co-authored-by: Felipe Martin <me@fmartingr.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3f6c94cfc3
Коммит
4e071e861c
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {IncomingWebhook, OutgoingWebhook, Command, OAuthApp} from '@mattermost/types/integrations';
|
||||
import type {IncomingWebhook, OutgoingWebhook, Command, OAuthApp, OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import * as IntegrationActions from 'mattermost-redux/actions/integrations';
|
||||
import {getProfilesByIds} from 'mattermost-redux/actions/users';
|
||||
@@ -137,3 +137,34 @@ export function loadProfilesForOAuthApps(apps: OAuthApp[]): ActionFuncAsync {
|
||||
return {data: null};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadOutgoingOAuthConnectionsAndProfiles(teamId: string, page = 0, perPage = DEFAULT_PAGE_SIZE): ActionFuncAsync<null> {
|
||||
return async (dispatch) => {
|
||||
const {data} = await dispatch(IntegrationActions.getOutgoingOAuthConnections(teamId, page, perPage));
|
||||
if (data) {
|
||||
dispatch(loadProfilesForOutgoingOAuthConnections(data));
|
||||
}
|
||||
return {data: null};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadProfilesForOutgoingOAuthConnections(connections: OutgoingOAuthConnection[]): ActionFuncAsync<null> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const profilesToLoad: {[key: string]: boolean} = {};
|
||||
for (let i = 0; i < connections.length; i++) {
|
||||
const app = connections[i];
|
||||
if (!getUser(state, app.creator_id)) {
|
||||
profilesToLoad[app.creator_id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const list = Object.keys(profilesToLoad);
|
||||
if (list.length === 0) {
|
||||
return {data: null};
|
||||
}
|
||||
|
||||
dispatch(getProfilesByIds(list));
|
||||
return {data: null};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5179,6 +5179,19 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.INTEGRATION_MANAGEMENT)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableOutgoingOAuthConnections',
|
||||
label: defineMessage({id: 'admin.service.outgoingOAuthConnectionsTitle', defaultMessage: 'Enable Outgoing OAuth Connections: '}),
|
||||
help_text: defineMessage({id: 'admin.service.outgoingOAuthConnectionsDesc', defaultMessage: 'When true, outgoing webhooks and slash commands will use set up oauth connections to authenticate with third party services. See <link>documentation</link> to learn more.'}),
|
||||
help_text_values: {
|
||||
link: (text: string) => (
|
||||
<a href='https://mattermost.com/pl/outgoing-oauth-connections'>{text}</a>
|
||||
),
|
||||
},
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.INTEGRATION_MANAGEMENT)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableCommands',
|
||||
|
||||
@@ -222,6 +222,9 @@ export default class PermissionsTree extends React.PureComponent<Props, State> {
|
||||
if (config.EnableOAuthServiceProvider === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_OAUTH)) {
|
||||
integrationsGroup.permissions.push(Permissions.MANAGE_OAUTH);
|
||||
}
|
||||
if (config.EnableOutgoingOAuthConnections === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS)) {
|
||||
integrationsGroup.permissions.push(Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS);
|
||||
}
|
||||
if (config.EnableCommands === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_SLASH_COMMANDS)) {
|
||||
integrationsGroup.permissions.push(Permissions.MANAGE_SLASH_COMMANDS);
|
||||
}
|
||||
@@ -234,6 +237,7 @@ export default class PermissionsTree extends React.PureComponent<Props, State> {
|
||||
if (config.EnableCustomEmoji === 'true' && !integrationsGroup.permissions.includes(Permissions.DELETE_OTHERS_EMOJIS)) {
|
||||
integrationsGroup.permissions.push(Permissions.DELETE_OTHERS_EMOJIS);
|
||||
}
|
||||
|
||||
if (config.EnableGuestAccounts === 'true' && !teamsGroup.permissions.includes(Permissions.INVITE_GUEST)) {
|
||||
teamsGroup.permissions.push(Permissions.INVITE_GUEST);
|
||||
}
|
||||
|
||||
@@ -615,4 +615,14 @@ export const permissionRolesStrings: Record<string, Record<string, MessageDescri
|
||||
defaultMessage: 'Rename custom groups.',
|
||||
},
|
||||
}),
|
||||
manage_outgoing_oauth_connections: defineMessages({
|
||||
name: {
|
||||
id: 'admin.permissions.permission.manage_outgoing_oauth_connections.name',
|
||||
defaultMessage: 'Manage Outgoing OAuth Credentials',
|
||||
},
|
||||
description: {
|
||||
id: 'admin.permissions.permission.manage_outgoing_oauth_connections.description',
|
||||
defaultMessage: 'Create, edit, and delete outgoing OAuth credentials.',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ import EditOutgoingWebhook from 'components/integrations/edit_outgoing_webhook';
|
||||
import InstalledIncomingWebhooks from 'components/integrations/installed_incoming_webhooks';
|
||||
import InstalledOauthApps from 'components/integrations/installed_oauth_apps';
|
||||
import InstalledOutgoingWebhooks from 'components/integrations/installed_outgoing_webhooks';
|
||||
import AddOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/add_outgoing_oauth_connection';
|
||||
import EditOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/edit_outgoing_oauth_connection';
|
||||
import InstalledOutgoingOAuthConnections from 'components/integrations/outgoing_oauth_connections/installed_outgoing_oauth_connections';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
|
||||
@@ -76,6 +79,7 @@ type Props = {
|
||||
enableOutgoingWebhooks: boolean;
|
||||
enableCommands: boolean;
|
||||
enableOAuthServiceProvider: boolean;
|
||||
enableOutgoingOAuthConnections: boolean;
|
||||
canCreateOrDeleteCustomEmoji: boolean;
|
||||
canManageIntegrations: boolean;
|
||||
}
|
||||
@@ -110,12 +114,12 @@ const BackstageController = (props: Props) => {
|
||||
<Pluggable pluggableName='Root'/>
|
||||
<BackstageSidebar
|
||||
team={props.team}
|
||||
user={props.user}
|
||||
enableCustomEmoji={props.enableCustomEmoji}
|
||||
enableIncomingWebhooks={props.enableIncomingWebhooks}
|
||||
enableOutgoingWebhooks={props.enableOutgoingWebhooks}
|
||||
enableCommands={props.enableCommands}
|
||||
enableOAuthServiceProvider={props.enableOAuthServiceProvider}
|
||||
enableOutgoingOAuthConnections={props.enableOutgoingOAuthConnections}
|
||||
canCreateOrDeleteCustomEmoji={props.canCreateOrDeleteCustomEmoji}
|
||||
canManageIntegrations={props.canManageIntegrations}
|
||||
/>
|
||||
@@ -179,6 +183,24 @@ const BackstageController = (props: Props) => {
|
||||
path={`${props.match.url}/oauth2-apps/edit`}
|
||||
component={EditOauthApp}
|
||||
/>
|
||||
<BackstageRoute
|
||||
extraProps={extraProps}
|
||||
exact={true}
|
||||
path={`${props.match.url}/outgoing-oauth2-connections`}
|
||||
component={InstalledOutgoingOAuthConnections}
|
||||
/>
|
||||
<BackstageRoute
|
||||
extraProps={extraProps}
|
||||
exact={true}
|
||||
path={`${props.match.url}/outgoing-oauth2-connections/add`}
|
||||
component={AddOutgoingOAuthConnection}
|
||||
/>
|
||||
<BackstageRoute
|
||||
extraProps={extraProps}
|
||||
exact={true}
|
||||
path={`${props.match.url}/outgoing-oauth2-connections/edit`}
|
||||
component={EditOutgoingOAuthConnection}
|
||||
/>
|
||||
<BackstageRoute
|
||||
extraProps={extraProps}
|
||||
path={`${props.match.url}/confirm`}
|
||||
|
||||
@@ -16,7 +16,6 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
id: 'team-id',
|
||||
name: 'team_name',
|
||||
}),
|
||||
user: TestHelper.getUserMock({}),
|
||||
enableCustomEmoji: false,
|
||||
enableIncomingWebhooks: false,
|
||||
enableOutgoingWebhooks: false,
|
||||
@@ -24,6 +23,7 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
enableOAuthServiceProvider: false,
|
||||
canCreateOrDeleteCustomEmoji: false,
|
||||
canManageIntegrations: false,
|
||||
enableOutgoingOAuthConnections: false,
|
||||
};
|
||||
|
||||
describe('custom emoji', () => {
|
||||
@@ -146,6 +146,30 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('outgoing oauth connections', () => {
|
||||
const testCases = [
|
||||
{canManageIntegrations: false, enableOutgoingOAuthConnections: false, expectedResult: false},
|
||||
{canManageIntegrations: false, enableOutgoingOAuthConnections: true, expectedResult: false},
|
||||
{canManageIntegrations: true, enableOutgoingOAuthConnections: false, expectedResult: false},
|
||||
{canManageIntegrations: true, enableOutgoingOAuthConnections: true, expectedResult: true},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
it(`when outgoing oauth connections is ${testCase.enableOutgoingOAuthConnections} and can manage integrations is ${testCase.canManageIntegrations}`, () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
enableOutgoingOAuthConnections: testCase.enableOutgoingOAuthConnections,
|
||||
canManageIntegrations: testCase.canManageIntegrations,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<BackstageSidebar {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'outgoing-oauth2-connections'}).exists()).toBe(testCase.expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bots', () => {
|
||||
const testCases = [
|
||||
{canManageIntegrations: false, expectedResult: false},
|
||||
@@ -176,6 +200,7 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
enableCommands: true,
|
||||
enableOAuthServiceProvider: true,
|
||||
canManageIntegrations: true,
|
||||
enableOutgoingOAuthConnections: true,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<BackstageSidebar {...props}/>,
|
||||
@@ -185,6 +210,7 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'outgoing_webhooks'}).exists()).toBe(true);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'commands'}).exists()).toBe(true);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'oauth2-apps'}).exists()).toBe(true);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'outgoing-oauth2-connections'}).exists()).toBe(true);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'bots'}).exists()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -195,6 +221,7 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
enableOutgoingWebhooks: true,
|
||||
enableCommands: true,
|
||||
enableOAuthServiceProvider: true,
|
||||
enableOutgoingOAuthConnections: true,
|
||||
canManageIntegrations: false,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
@@ -205,6 +232,7 @@ describe('components/backstage/components/BackstageSidebar', () => {
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'outgoing_webhooks'}).exists()).toBe(false);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'commands'}).exists()).toBe(false);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'oauth2-apps'}).exists()).toBe(false);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'outgoing-oauth2-connections'}).exists()).toBe(false);
|
||||
expect(wrapper.find(BackstageCategory).find({name: 'bots'}).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
@@ -17,12 +16,12 @@ import BackstageSection from './backstage_section';
|
||||
|
||||
type Props = {
|
||||
team: Team;
|
||||
user: UserProfile;
|
||||
enableCustomEmoji: boolean;
|
||||
enableIncomingWebhooks: boolean;
|
||||
enableOutgoingWebhooks: boolean;
|
||||
enableCommands: boolean;
|
||||
enableOAuthServiceProvider: boolean;
|
||||
enableOutgoingOAuthConnections: boolean;
|
||||
canCreateOrDeleteCustomEmoji: boolean;
|
||||
canManageIntegrations: boolean;
|
||||
}
|
||||
@@ -156,6 +155,28 @@ export default class BackstageSidebar extends React.PureComponent<Props> {
|
||||
</SystemPermissionGate>
|
||||
);
|
||||
|
||||
let outgoingOAuthConnections: JSX.Element | null = null;
|
||||
if (this.props.enableOutgoingOAuthConnections) {
|
||||
outgoingOAuthConnections = (
|
||||
<TeamPermissionGate
|
||||
permissions={[Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]}
|
||||
teamId={this.props.team.id}
|
||||
>
|
||||
<BackstageSection
|
||||
name='outgoing-oauth2-connections'
|
||||
parentLink={'/' + this.props.team.name + '/integrations'}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='backstage_sidebar.integrations.outgoingOauthConnections'
|
||||
defaultMessage='Outgoing OAuth 2.0 Connections'
|
||||
/>
|
||||
}
|
||||
id='outgoingOauthConnections'
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BackstageCategory
|
||||
name='integrations'
|
||||
@@ -173,6 +194,7 @@ export default class BackstageSidebar extends React.PureComponent<Props> {
|
||||
{commands}
|
||||
{oauthApps}
|
||||
{botAccounts}
|
||||
{outgoingOAuthConnections}
|
||||
</BackstageCategory>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const enableOutgoingWebhooks = config.EnableOutgoingWebhooks === 'true';
|
||||
const enableCommands = config.EnableCommands === 'true';
|
||||
const enableOAuthServiceProvider = config.EnableOAuthServiceProvider === 'true';
|
||||
const enableOutgoingOAuthConnections = config.EnableOutgoingOAuthConnections === 'true';
|
||||
|
||||
let canCreateOrDeleteCustomEmoji = (haveISystemPermission(state, {permission: Permissions.CREATE_EMOJIS}) || haveISystemPermission(state, {permission: Permissions.DELETE_EMOJIS}));
|
||||
if (!canCreateOrDeleteCustomEmoji) {
|
||||
@@ -37,7 +38,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
}
|
||||
}
|
||||
|
||||
const canManageTeamIntegrations = (haveITeamPermission(state, '', Permissions.MANAGE_SLASH_COMMANDS) || haveITeamPermission(state, '', Permissions.MANAGE_OAUTH) || haveITeamPermission(state, '', Permissions.MANAGE_INCOMING_WEBHOOKS) || haveITeamPermission(state, '', Permissions.MANAGE_OUTGOING_WEBHOOKS));
|
||||
const canManageTeamIntegrations = (haveITeamPermission(state, team.id, Permissions.MANAGE_SLASH_COMMANDS) || haveITeamPermission(state, team.id, Permissions.MANAGE_OAUTH) || haveITeamPermission(state, team.id, Permissions.MANAGE_INCOMING_WEBHOOKS) || haveITeamPermission(state, team.id, Permissions.MANAGE_OUTGOING_WEBHOOKS));
|
||||
const canManageSystemBots = (haveISystemPermission(state, {permission: Permissions.MANAGE_BOTS}) || haveISystemPermission(state, {permission: Permissions.MANAGE_OTHERS_BOTS}));
|
||||
const canManageIntegrations = canManageTeamIntegrations || canManageSystemBots;
|
||||
|
||||
@@ -50,6 +51,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
enableOutgoingWebhooks,
|
||||
enableCommands,
|
||||
enableOAuthServiceProvider,
|
||||
enableOutgoingOAuthConnections,
|
||||
canCreateOrDeleteCustomEmoji,
|
||||
canManageIntegrations,
|
||||
};
|
||||
|
||||
@@ -169,13 +169,9 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[Function]}
|
||||
placeholder="Must start with http:// or https://"
|
||||
type="text"
|
||||
value="https://google.com/command"
|
||||
/>
|
||||
<div
|
||||
@@ -186,6 +182,19 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
|
||||
id="add_command.url.help"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="You can connect commands to <link>outgoing OAuth connections</link>."
|
||||
id="add_command.outgoing_oauth_connections.help_text"
|
||||
values={
|
||||
Object {
|
||||
"link": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -609,13 +618,9 @@ exports[`components/integrations/AbstractCommand should match snapshot when head
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[Function]}
|
||||
placeholder="Must start with http:// or https://"
|
||||
type="text"
|
||||
value="https://google.com/command"
|
||||
/>
|
||||
<div
|
||||
@@ -626,6 +631,19 @@ exports[`components/integrations/AbstractCommand should match snapshot when head
|
||||
id="add_command.url.help"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="You can connect commands to <link>outgoing OAuth connections</link>."
|
||||
id="add_command.outgoing_oauth_connections.help_text"
|
||||
values={
|
||||
Object {
|
||||
"link": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -1049,13 +1067,9 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[Function]}
|
||||
placeholder="Must start with http:// or https://"
|
||||
type="text"
|
||||
value="https://google.com/command"
|
||||
/>
|
||||
<div
|
||||
@@ -1066,6 +1080,19 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
|
||||
id="add_command.url.help"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="You can connect commands to <link>outgoing OAuth connections</link>."
|
||||
id="add_command.outgoing_oauth_connections.help_text"
|
||||
values={
|
||||
Object {
|
||||
"link": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -10,18 +10,18 @@ exports[`components/integrations/InstalledCommand should call onDelete function
|
||||
<div
|
||||
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
|
||||
>
|
||||
<div>
|
||||
<strong
|
||||
className="item-details__name"
|
||||
>
|
||||
<div
|
||||
className="item-details__name"
|
||||
>
|
||||
<strong>
|
||||
display_name
|
||||
</strong>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger auto_complete_hint
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger auto_complete_hint
|
||||
</span>
|
||||
<div
|
||||
className="item-actions"
|
||||
>
|
||||
@@ -116,18 +116,18 @@ exports[`components/integrations/InstalledCommand should call onRegenToken funct
|
||||
<div
|
||||
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
|
||||
>
|
||||
<div>
|
||||
<strong
|
||||
className="item-details__name"
|
||||
>
|
||||
<div
|
||||
className="item-details__name"
|
||||
>
|
||||
<strong>
|
||||
display_name
|
||||
</strong>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger auto_complete_hint
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger auto_complete_hint
|
||||
</span>
|
||||
<div
|
||||
className="item-actions"
|
||||
>
|
||||
@@ -224,18 +224,18 @@ exports[`components/integrations/InstalledCommand should match snapshot 1`] = `
|
||||
<div
|
||||
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
|
||||
>
|
||||
<div>
|
||||
<strong
|
||||
className="item-details__name"
|
||||
>
|
||||
<div
|
||||
className="item-details__name"
|
||||
>
|
||||
<strong>
|
||||
display_name
|
||||
</strong>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger auto_complete_hint
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger auto_complete_hint
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
@@ -298,18 +298,18 @@ exports[`components/integrations/InstalledCommand should match snapshot, not aut
|
||||
<div
|
||||
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
|
||||
>
|
||||
<div>
|
||||
<strong
|
||||
className="item-details__name"
|
||||
>
|
||||
<div
|
||||
className="item-details__name"
|
||||
>
|
||||
<strong>
|
||||
command_display_name
|
||||
</strong>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="item-details__trigger"
|
||||
>
|
||||
- /trigger
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
|
||||
@@ -17,6 +17,8 @@ import SpinnerButton from 'components/spinner_button';
|
||||
import {Constants, DeveloperLinks} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import OAuthConnectionAudienceInput from './outgoing_oauth_connections/oauth_connection_audience_input';
|
||||
|
||||
const REQUEST_POST = 'P';
|
||||
const REQUEST_GET = 'G';
|
||||
|
||||
@@ -65,7 +67,7 @@ type Props = {
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
type State= {
|
||||
type State = {
|
||||
saving: boolean;
|
||||
clientError: null | JSX.Element | string;
|
||||
trigger: string;
|
||||
@@ -87,7 +89,7 @@ export class AbstractCommand extends React.PureComponent<Props, State> {
|
||||
this.state = this.getStateFromCommand(this.props.initialCommand || {});
|
||||
}
|
||||
|
||||
getStateFromCommand = (command: Props['initialCommand']) => {
|
||||
getStateFromCommand = (command: Props['initialCommand']): State => {
|
||||
return {
|
||||
displayName: command?.display_name ?? '',
|
||||
description: command?.description ?? '',
|
||||
@@ -523,11 +525,7 @@ export class AbstractCommand extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='url'
|
||||
type='text'
|
||||
maxLength={1024}
|
||||
className='form-control'
|
||||
<OAuthConnectionAudienceInput
|
||||
value={this.state.url}
|
||||
onChange={this.updateUrl}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
@@ -541,6 +539,17 @@ export class AbstractCommand extends React.PureComponent<Props, State> {
|
||||
defaultMessage='Specify the callback URL to receive the HTTP POST or GET event request when the slash command is run.'
|
||||
/>
|
||||
</div>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id={'add_command.outgoing_oauth_connections.help_text'}
|
||||
defaultMessage={'You can connect commands to <link>outgoing OAuth connections</link>.'}
|
||||
values={{
|
||||
link: (text: string) => (
|
||||
<a href='https://mattermost.com/pl/outgoing-oauth-connections'>{text}</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
|
||||
@@ -148,7 +148,6 @@ describe('components/integrations/AbstractOutgoingWebhook', () => {
|
||||
|
||||
const selector = wrapper.find('#triggerWhen');
|
||||
selector.simulate('change', {target: {value: 1}});
|
||||
console.log('selector: ', selector.debug());
|
||||
expect(wrapper.state('triggerWhen')).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -522,6 +522,7 @@ export default class Bot extends React.PureComponent<Props, State> {
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
}
|
||||
modalClass='integrations-backstage-modal'
|
||||
show={this.state.confirmingId !== ''}
|
||||
onConfirm={this.revokeTokenConfirmed}
|
||||
onCancel={this.closeConfirm}
|
||||
|
||||
@@ -347,3 +347,114 @@ exports[`components/integrations/ConfirmIntegration should match snapshot, outgo
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/integrations/ConfirmIntegration should match snapshot, outgoingOAuthConnections case 1`] = `
|
||||
<div
|
||||
className="backstage-content row"
|
||||
>
|
||||
<BackstageHeader>
|
||||
<Link
|
||||
to="/team_test/integrations/outgoing-oauth2-connections"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Outgoing OAuth 2.0 Connections"
|
||||
id="installed_outgoing_oauth_connections.header"
|
||||
/>
|
||||
</Link>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Add"
|
||||
id="integrations.add"
|
||||
/>
|
||||
</BackstageHeader>
|
||||
<div
|
||||
className="backstage-form backstage-form__confirmation"
|
||||
>
|
||||
<h4
|
||||
className="backstage-form__title"
|
||||
id="formTitle"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Setup Successful"
|
||||
id="integrations.successful"
|
||||
/>
|
||||
</h4>
|
||||
<p
|
||||
key="add_outgoing_oauth_connection.doneHelp"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Your Outgoing OAuth 2.0 Connection is set up. When a request is sent to one of the following Audience URLs, the Client ID and Client Secret will now be used to retrieve a token from the Token URL, before sending the integration request (details at <link>Outgoing OAuth 2.0 Connections</link>)."
|
||||
id="add_outgoing_oauth_connection.doneHelp"
|
||||
values={
|
||||
Object {
|
||||
"link": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
key="add_outgoing_oauth_connection.clientId"
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Client ID**: {id}"
|
||||
id="add_outgoing_oauth_connection.clientId"
|
||||
values={
|
||||
Object {
|
||||
"id": "someid",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Client Secret**: \\\\*\\\\*\\\\*\\\\*\\\\*\\\\*\\\\*\\\\*"
|
||||
id="add_outgoing_oauth_connection.clientSecret"
|
||||
values={
|
||||
Object {
|
||||
"secret": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
className="word-break--all"
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Token URL**: \`{url}\`"
|
||||
id="add_outgoing_oauth_connection.token_url"
|
||||
values={
|
||||
Object {
|
||||
"url": "https://tokenurl.com",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
className="word-break--all"
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Audience URL(s)**: \`{url}\`"
|
||||
id="add_outgoing_oauth_connection.audience_urls"
|
||||
values={
|
||||
Object {
|
||||
"url": "https://myaudience.com",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="backstage-form__footer"
|
||||
>
|
||||
<Link
|
||||
className="btn btn-primary"
|
||||
id="doneButton"
|
||||
to="/team_test/integrations/outgoing-oauth2-connections"
|
||||
type="submit"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Done"
|
||||
id="integrations.done"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {Bot} from '@mattermost/types/bots';
|
||||
import type {IncomingWebhook, OAuthApp, OutgoingWebhook} from '@mattermost/types/integrations';
|
||||
import type {IncomingWebhook, OAuthApp, OutgoingOAuthConnection, OutgoingWebhook} from '@mattermost/types/integrations';
|
||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
||||
|
||||
import ConfirmIntegration from 'components/integrations/confirm_integration/confirm_integration';
|
||||
@@ -43,6 +43,16 @@ describe('components/integrations/ConfirmIntegration', () => {
|
||||
client_secret: '<==secret==>',
|
||||
callback_urls: ['https://someCallback', 'https://anotherCallback'],
|
||||
};
|
||||
const outgoingOAuthConnection = {
|
||||
id,
|
||||
audiences: ['https://myaudience.com'],
|
||||
client_id: 'someid',
|
||||
client_secret: '',
|
||||
grant_type: 'client_credentials',
|
||||
name: 'My OAuth Connection',
|
||||
oauth_token_url: 'https://tokenurl.com',
|
||||
};
|
||||
|
||||
const userId = 'b5tpgt4iepf45jt768jz84djhd';
|
||||
const bot = TestHelper.getBotMock({
|
||||
user_id: userId,
|
||||
@@ -50,6 +60,7 @@ describe('components/integrations/ConfirmIntegration', () => {
|
||||
});
|
||||
const commands = {[id]: TestHelper.getCommandMock({id, token})};
|
||||
const oauthApps = {[id]: oauthApp} as unknown as IDMappedObjects<OAuthApp>;
|
||||
const outgoingOAuthConnections = {[id]: outgoingOAuthConnection} as unknown as IDMappedObjects<OutgoingOAuthConnection>;
|
||||
const incomingHooks: IDMappedObjects<IncomingWebhook> = {[id]: TestHelper.getIncomingWebhookMock({id})};
|
||||
const outgoingHooks = {[id]: {id, token}} as unknown as IDMappedObjects<OutgoingWebhook>;
|
||||
const bots: Record<string, Bot> = {[userId]: bot};
|
||||
@@ -59,6 +70,7 @@ describe('components/integrations/ConfirmIntegration', () => {
|
||||
location,
|
||||
commands,
|
||||
oauthApps,
|
||||
outgoingOAuthConnections,
|
||||
incomingHooks,
|
||||
outgoingHooks,
|
||||
bots,
|
||||
@@ -82,6 +94,14 @@ describe('components/integrations/ConfirmIntegration', () => {
|
||||
expect(container.querySelector('.word-break--all')).toHaveTextContent('URL(s): https://someCallback, https://anotherCallback');
|
||||
});
|
||||
|
||||
test('should match snapshot, outgoingOAuthConnections case', () => {
|
||||
props.location.search = getSearchString('outgoing-oauth2-connections');
|
||||
const wrapper = shallow(
|
||||
<ConfirmIntegration {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, commands case', () => {
|
||||
props.location.search = getSearchString('commands');
|
||||
const wrapper = shallow(
|
||||
|
||||
@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import {Link, useHistory} from 'react-router-dom';
|
||||
|
||||
import type {Bot} from '@mattermost/types/bots';
|
||||
import type {Command, IncomingWebhook, OAuthApp, OutgoingWebhook} from '@mattermost/types/integrations';
|
||||
import type {Command, IncomingWebhook, OAuthApp, OutgoingOAuthConnection, OutgoingWebhook} from '@mattermost/types/integrations';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
||||
|
||||
@@ -26,9 +26,10 @@ type Props = {
|
||||
incomingHooks: IDMappedObjects<IncomingWebhook>;
|
||||
outgoingHooks: IDMappedObjects<OutgoingWebhook>;
|
||||
bots: Record<string, Bot>;
|
||||
outgoingOAuthConnections: Record<string, OutgoingOAuthConnection>;
|
||||
}
|
||||
|
||||
const ConfirmIntegration = ({team, location, commands, oauthApps, incomingHooks, outgoingHooks, bots}: Props): JSX.Element | null => {
|
||||
const ConfirmIntegration = ({team, location, commands, oauthApps, incomingHooks, outgoingHooks, bots, outgoingOAuthConnections}: Props): JSX.Element | null => {
|
||||
const history = useHistory();
|
||||
|
||||
const type = (new URLSearchParams(location.search)).get('type') || '';
|
||||
@@ -56,6 +57,7 @@ const ConfirmIntegration = ({team, location, commands, oauthApps, incomingHooks,
|
||||
const incomingHook = incomingHooks[id];
|
||||
const outgoingHook = outgoingHooks[id];
|
||||
const oauthApp = oauthApps[id];
|
||||
const outgoingOAuthConnection = outgoingOAuthConnections[id];
|
||||
const bot = bots[id];
|
||||
|
||||
if (type === Constants.Integrations.COMMAND && command) {
|
||||
@@ -243,6 +245,95 @@ const ConfirmIntegration = ({team, location, commands, oauthApps, incomingHooks,
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if (type === Constants.Integrations.OUTGOING_OAUTH_CONNECTIONS && outgoingOAuthConnection) {
|
||||
const clientId = outgoingOAuthConnection.client_id;
|
||||
const clientSecret = outgoingOAuthConnection.client_secret;
|
||||
const username = outgoingOAuthConnection.credentials_username;
|
||||
const password = outgoingOAuthConnection.credentials_password;
|
||||
|
||||
headerText = (
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.header'
|
||||
defaultMessage='Outgoing OAuth 2.0 Connections'
|
||||
/>
|
||||
);
|
||||
|
||||
helpText = [];
|
||||
helpText.push(
|
||||
<p key='add_outgoing_oauth_connection.doneHelp'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.doneHelp'
|
||||
defaultMessage='Your Outgoing OAuth 2.0 Connection is set up. When a request is sent to one of the following Audience URLs, the Client ID and Client Secret will now be used to retrieve a token from the Token URL, before sending the integration request (details at <link>Outgoing OAuth 2.0 Connections</link>).'
|
||||
values={{
|
||||
link: (msg: string) => (
|
||||
<ExternalLink
|
||||
href={DeveloperLinks.SETUP_OAUTH2} // TODO: dev docs for outgoing oauth connections feature
|
||||
location='confirm_integration'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>,
|
||||
);
|
||||
helpText.push(
|
||||
<p key='add_outgoing_oauth_connection.clientId'>
|
||||
<FormattedMarkdownMessage
|
||||
id='add_outgoing_oauth_connection.clientId'
|
||||
defaultMessage='**Client ID**: {id}'
|
||||
values={{id: clientId}}
|
||||
/>
|
||||
<br/>
|
||||
<FormattedMarkdownMessage
|
||||
id='add_outgoing_oauth_connection.clientSecret'
|
||||
defaultMessage='**Client Secret**: \*\*\*\*\*\*\*\*'
|
||||
values={{secret: clientSecret}}
|
||||
/>
|
||||
</p>,
|
||||
);
|
||||
|
||||
if (outgoingOAuthConnection.grant_type === 'password') {
|
||||
helpText.push(
|
||||
<p key='add_outgoing_oauth_connection.username'>
|
||||
<FormattedMarkdownMessage
|
||||
id='add_outgoing_oauth_connection.username'
|
||||
defaultMessage='**Username**: {username}'
|
||||
values={{username}}
|
||||
/>
|
||||
<CopyText
|
||||
idMessage='integrations.copy_username'
|
||||
defaultMessage='Copy Username'
|
||||
value={username || ''}
|
||||
/>
|
||||
<br/>
|
||||
<FormattedMarkdownMessage
|
||||
id='add_outgoing_oauth_connection.password'
|
||||
defaultMessage='**Password**: {password}'
|
||||
values={{password}}
|
||||
/>
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
|
||||
tokenText = (
|
||||
<>
|
||||
<p className='word-break--all'>
|
||||
<FormattedMarkdownMessage
|
||||
id='add_outgoing_oauth_connection.token_url'
|
||||
defaultMessage='**Token URL**: `{url}`'
|
||||
values={{url: outgoingOAuthConnection.oauth_token_url}}
|
||||
/>
|
||||
</p>
|
||||
<p className='word-break--all'>
|
||||
<FormattedMarkdownMessage
|
||||
id='add_outgoing_oauth_connection.audience_urls'
|
||||
defaultMessage='**Audience URL(s)**: `{url}`'
|
||||
values={{url: outgoingOAuthConnection.audiences.join(', ')}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
} else if (type === Constants.Integrations.BOT && bot) {
|
||||
const botToken = (new URLSearchParams(location.search)).get('token') || '';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getBotAccounts} from 'mattermost-redux/selectors/entities/bots';
|
||||
import {getCommands, getOAuthApps, getIncomingHooks, getOutgoingHooks} from 'mattermost-redux/selectors/entities/integrations';
|
||||
import {getCommands, getOAuthApps, getIncomingHooks, getOutgoingHooks, getOutgoingOAuthConnections} from 'mattermost-redux/selectors/entities/integrations';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
@@ -17,6 +17,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
incomingHooks: getIncomingHooks(state),
|
||||
outgoingHooks: getOutgoingHooks(state),
|
||||
bots: getBotAccounts(state),
|
||||
outgoingOAuthConnections: getOutgoingOAuthConnections(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const ModalId = 'delete_integration_confirm';
|
||||
type Props = {
|
||||
confirmButtonText?: React.ReactNode;
|
||||
linkText?: React.ReactNode;
|
||||
subtitleText?: React.ReactNode;
|
||||
modalMessage?: React.ReactNode;
|
||||
modalTitle?: React.ReactNode;
|
||||
onDelete: () => void;
|
||||
@@ -25,7 +26,7 @@ export default function DeleteIntegrationLink(props: Props) {
|
||||
confirmButtonText = (
|
||||
<FormattedMessage
|
||||
id='integrations.delete.confirm.button'
|
||||
defaultMessage='Delete'
|
||||
defaultMessage='Yes, delete it'
|
||||
/>
|
||||
),
|
||||
linkText = (
|
||||
@@ -50,11 +51,22 @@ export default function DeleteIntegrationLink(props: Props) {
|
||||
modalId: ModalId,
|
||||
dialogProps: {
|
||||
confirmButtonText,
|
||||
confirmButtonClass: 'btn btn-danger',
|
||||
modalClass: 'integrations-backstage-modal',
|
||||
message: (
|
||||
<div className='alert alert-warning'>
|
||||
<WarningIcon additionalClassName='mr-1'/>
|
||||
{props.modalMessage}
|
||||
</div>
|
||||
<>
|
||||
{props.subtitleText && (
|
||||
<p>
|
||||
{props.subtitleText}
|
||||
</p>
|
||||
)}
|
||||
<div className='alert alert-danger'>
|
||||
<WarningIcon additionalClassName='mr-1'/>
|
||||
<strong>
|
||||
{props.modalMessage}
|
||||
</strong>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
onConfirm: onDelete,
|
||||
title: modalTitle,
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`components/integrations/EditCommand should have match renderExtra 1`] =
|
||||
id="update_command.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
@@ -85,7 +85,7 @@ exports[`components/integrations/EditCommand should match snapshot 1`] = `
|
||||
id="update_command.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
|
||||
@@ -157,6 +157,7 @@ export default class EditCommand extends React.PureComponent<Props, State> {
|
||||
title={confirmTitle}
|
||||
message={confirmMessage}
|
||||
confirmButtonText={confirmButton}
|
||||
modalClass='integrations-backstage-modal'
|
||||
show={this.state.showConfirmModal}
|
||||
onConfirm={this.submitCommand}
|
||||
onCancel={this.confirmModalDismissed}
|
||||
|
||||
@@ -65,7 +65,6 @@ type Props = {
|
||||
};
|
||||
|
||||
type State = {
|
||||
showConfirmModal: boolean;
|
||||
serverError: string;
|
||||
};
|
||||
|
||||
@@ -76,7 +75,6 @@ export default class EditIncomingWebhook extends React.PureComponent<Props, Stat
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showConfirmModal: false,
|
||||
serverError: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`components/integrations/EditOAuthApp should have match renderExtra 1`]
|
||||
id="update_oauth_app.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
@@ -82,7 +82,7 @@ exports[`components/integrations/EditOAuthApp should match snapshot 1`] = `
|
||||
id="update_oauth_app.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
@@ -158,7 +158,7 @@ exports[`components/integrations/EditOAuthApp should match snapshot when EnableO
|
||||
id="update_oauth_app.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
@@ -234,7 +234,7 @@ exports[`components/integrations/EditOAuthApp should match snapshot, loading 1`]
|
||||
id="update_oauth_app.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
|
||||
@@ -127,6 +127,7 @@ export default class EditOAuthApp extends React.PureComponent<Props, State> {
|
||||
title={confirmTitle}
|
||||
message={confirmMessage}
|
||||
confirmButtonText={confirmButton}
|
||||
modalClass='integrations-backstage-modal'
|
||||
show={this.state.showConfirmModal}
|
||||
onConfirm={this.submitOAuthApp}
|
||||
onCancel={this.confirmModalDismissed}
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`components/integrations/EditOutgoingWebhook should have match renderExt
|
||||
id="update_outgoing_webhook.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
@@ -92,7 +92,7 @@ exports[`components/integrations/EditOutgoingWebhook should match snapshot 1`] =
|
||||
id="update_outgoing_webhook.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
@@ -191,7 +191,7 @@ exports[`components/integrations/EditOutgoingWebhook should match snapshot when
|
||||
id="update_outgoing_webhook.question"
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
modalClass="integrations-backstage-modal"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
|
||||
@@ -163,6 +163,7 @@ export default class EditOutgoingWebhook extends React.PureComponent<Props, Stat
|
||||
title={confirmTitle}
|
||||
message={confirmMessage}
|
||||
confirmButtonText={confirmButton}
|
||||
modalClass='integrations-backstage-modal'
|
||||
show={this.state.showConfirmModal}
|
||||
onConfirm={this.submitHook}
|
||||
onCancel={this.confirmModalDismissed}
|
||||
|
||||
@@ -16,6 +16,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const enableOutgoingWebhooks = config.EnableOutgoingWebhooks === 'true';
|
||||
const enableCommands = config.EnableCommands === 'true';
|
||||
const enableOAuthServiceProvider = config.EnableOAuthServiceProvider === 'true';
|
||||
const enableOutgoingOAuthConnections = config.EnableOutgoingOAuthConnections === 'true';
|
||||
|
||||
return {
|
||||
siteName,
|
||||
@@ -23,6 +24,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
enableOutgoingWebhooks,
|
||||
enableCommands,
|
||||
enableOAuthServiceProvider,
|
||||
enableOutgoingOAuthConnections,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -149,14 +149,14 @@ export default class InstalledCommand extends React.PureComponent<Props> {
|
||||
<div className='backstage-list__item'>
|
||||
<div className='item-details'>
|
||||
<div className='item-details__row d-flex flex-column flex-md-row justify-content-between'>
|
||||
<div>
|
||||
<strong className='item-details__name'>
|
||||
<div className='item-details__name'>
|
||||
<strong>
|
||||
{name}
|
||||
</strong>
|
||||
<span className='item-details__trigger'>
|
||||
{trigger}
|
||||
</span>
|
||||
</div>
|
||||
<span className='item-details__trigger'>
|
||||
{trigger}
|
||||
</span>
|
||||
{actions}
|
||||
</div>
|
||||
{description}
|
||||
|
||||
@@ -15,6 +15,7 @@ import TeamPermissionGate from 'components/permissions_gates/team_permission_gat
|
||||
import BotAccountsIcon from 'images/bot_default_icon.png';
|
||||
import IncomingWebhookIcon from 'images/incoming_webhook.jpg';
|
||||
import OAuthIcon from 'images/oauth_icon.png';
|
||||
import OutgoingOAuthConnectionsIcon from 'images/outgoing_oauth_connection.png';
|
||||
import OutgoingWebhookIcon from 'images/outgoing_webhook.jpg';
|
||||
import SlashCommandIcon from 'images/slash_command_icon.jpg';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -27,6 +28,7 @@ type Props = {
|
||||
enableOutgoingWebhooks: boolean;
|
||||
enableCommands: boolean;
|
||||
enableOAuthServiceProvider: boolean;
|
||||
enableOutgoingOAuthConnections: boolean;
|
||||
team: Team;
|
||||
}
|
||||
|
||||
@@ -154,6 +156,34 @@ export default class Integrations extends React.PureComponent <Props> {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.enableOutgoingOAuthConnections) {
|
||||
options.push(
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.team.id}
|
||||
permissions={[Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]}
|
||||
key='outgoingOAuthConnectionsPermission'
|
||||
>
|
||||
<IntegrationOption
|
||||
key='outgoingOAuthConnections'
|
||||
image={OutgoingOAuthConnectionsIcon}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='integrations.outgoingOAuthConnections.title'
|
||||
defaultMessage='Outgoing OAuth Connections'
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id='integrations.outgoingOAuthConnections.description'
|
||||
defaultMessage='Outgoing OAuth Connections allow custom integrations to communicate to external systems'
|
||||
/>
|
||||
}
|
||||
link={'/' + this.props.team.name + '/integrations/outgoing-oauth2-connections'}
|
||||
/>
|
||||
</TeamPermissionGate>,
|
||||
);
|
||||
}
|
||||
|
||||
options.push(
|
||||
<SystemPermissionGate
|
||||
permissions={['manage_bots']}
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,559 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integrations/AddOutgoingOAuthConnection should match snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<AddOutgoingOAuthConnection
|
||||
team={
|
||||
Object {
|
||||
"allow_open_invite": false,
|
||||
"allowed_domains": "",
|
||||
"company_name": "",
|
||||
"create_at": 0,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "name",
|
||||
"email": "",
|
||||
"group_constrained": false,
|
||||
"id": "dbcxd9wpzpbpfp8pad78xj12pr",
|
||||
"invite_id": "",
|
||||
"name": "test",
|
||||
"scheme_id": "id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<AbstractOutgoingOAuthConnection
|
||||
footer={
|
||||
Object {
|
||||
"defaultMessage": "Save",
|
||||
"id": "add_outgoing_oauth_connection.save",
|
||||
}
|
||||
}
|
||||
header={
|
||||
Object {
|
||||
"defaultMessage": "Add",
|
||||
"id": "add_outgoing_oauth_connection.add",
|
||||
}
|
||||
}
|
||||
loading={
|
||||
Object {
|
||||
"defaultMessage": "Saving...",
|
||||
"id": "add_outgoing_oauth_connection.saving",
|
||||
}
|
||||
}
|
||||
serverError=""
|
||||
submitAction={[Function]}
|
||||
team={
|
||||
Object {
|
||||
"allow_open_invite": false,
|
||||
"allowed_domains": "",
|
||||
"company_name": "",
|
||||
"create_at": 0,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "name",
|
||||
"email": "",
|
||||
"group_constrained": false,
|
||||
"id": "dbcxd9wpzpbpfp8pad78xj12pr",
|
||||
"invite_id": "",
|
||||
"name": "test",
|
||||
"scheme_id": "id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="backstage-content"
|
||||
>
|
||||
<BackstageHeader>
|
||||
<div
|
||||
className="backstage-header"
|
||||
>
|
||||
<h1>
|
||||
<Link
|
||||
to="/test/integrations/outgoing-oauth2-connections"
|
||||
>
|
||||
<LinkAnchor
|
||||
href="/test/integrations/outgoing-oauth2-connections"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
href="/test/integrations/outgoing-oauth2-connections"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Outgoing OAuth Connections"
|
||||
id="add_outgoing_oauth_connection.header"
|
||||
>
|
||||
<span>
|
||||
Outgoing OAuth Connections
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
<span
|
||||
className="backstage-header__divider"
|
||||
key="divider1"
|
||||
>
|
||||
<i
|
||||
className="fa fa-angle-right"
|
||||
title="Breadcrumb Icon"
|
||||
/>
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add"
|
||||
id="add_outgoing_oauth_connection.add"
|
||||
>
|
||||
<span>
|
||||
Add
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</h1>
|
||||
</div>
|
||||
</BackstageHeader>
|
||||
<div
|
||||
className="backstage-form"
|
||||
>
|
||||
<form
|
||||
className="form-horizontal"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
htmlFor="name"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Name"
|
||||
id="add_outgoing_oauth_connection.name.label"
|
||||
>
|
||||
<span>
|
||||
Name
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</label>
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
id="name"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specify the name for your OAuth connection."
|
||||
id="add_outgoing_oauth_connection.name.help"
|
||||
>
|
||||
<span>
|
||||
Specify the name for your OAuth connection.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
htmlFor="client_id"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Client ID"
|
||||
id="add_outgoing_oauth_connection.client_id.label"
|
||||
>
|
||||
<span>
|
||||
Client ID
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</label>
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="client_id"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specify the Client ID for your OAuth connection."
|
||||
id="add_outgoing_oauth_connection.client_id.help"
|
||||
>
|
||||
<span>
|
||||
Specify the Client ID for your OAuth connection.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
htmlFor="client_secret"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Client Secret"
|
||||
id="add_outgoing_oauth_connection.client_secret.label"
|
||||
>
|
||||
<span>
|
||||
Client Secret
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</label>
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="client_secret"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specify the Client Secret for your OAuth connection."
|
||||
id="add_outgoing_oauth_connection.client_secret.help"
|
||||
>
|
||||
<span>
|
||||
Specify the Client Secret for your OAuth connection.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
htmlFor="oauth_token_url"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="OAuth Token URL"
|
||||
id="add_outgoing_oauth_connection.oauth_token_url.label"
|
||||
>
|
||||
<span>
|
||||
OAuth Token URL
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</label>
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
id="token_url"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specify the OAuth Token URL for your OAuth connection."
|
||||
id="add_outgoing_oauth_connection.oauth_token_url.help"
|
||||
>
|
||||
<span>
|
||||
Specify the OAuth Token URL for your OAuth connection.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
<div
|
||||
className="outgoing-oauth-connection-validate-button-container"
|
||||
>
|
||||
<ValidateButton
|
||||
onClick={[Function]}
|
||||
setUnvalidated={[Function]}
|
||||
status="initial"
|
||||
>
|
||||
<button
|
||||
className="btn btn-tertiary btn-sm"
|
||||
id="validateConnection"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Validate Connection"
|
||||
id="add_outgoing_oauth_connection.validate"
|
||||
>
|
||||
<span>
|
||||
Validate Connection
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</button>
|
||||
</ValidateButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
htmlFor="audienceUrls"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Audience URLs (One Per Line)"
|
||||
id="add_outgoing_oauth_connection.audienceUrls.label"
|
||||
>
|
||||
<span>
|
||||
Audience URLs (One Per Line)
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</label>
|
||||
<div
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="audienceUrls"
|
||||
onChange={[Function]}
|
||||
rows={3}
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="form__help"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The URLs which will receive requests with the OAuth token, e.g. your custom slash command handler endpoint. Must be a valid URL and start with http:// or https://."
|
||||
id="add_outgoing_oauth_connection.audienceUrls.help"
|
||||
>
|
||||
<span>
|
||||
The URLs which will receive requests with the OAuth token, e.g. your custom slash command handler endpoint. Must be a valid URL and start with http:// or https://.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="backstage-form__footer"
|
||||
>
|
||||
<FormError
|
||||
error={null}
|
||||
errors={
|
||||
Array [
|
||||
"",
|
||||
"",
|
||||
]
|
||||
}
|
||||
type="backstage"
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-tertiary"
|
||||
to="/test/integrations/outgoing-oauth2-connections"
|
||||
>
|
||||
<LinkAnchor
|
||||
className="btn btn-tertiary"
|
||||
href="/test/integrations/outgoing-oauth2-connections"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
className="btn btn-tertiary"
|
||||
href="/test/integrations/outgoing-oauth2-connections"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="add_outgoing_oauth_connection.cancel"
|
||||
>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
<Memo(SpinnerButton)
|
||||
className="btn btn-primary"
|
||||
id="saveConnection"
|
||||
onClick={[Function]}
|
||||
spinning={false}
|
||||
spinningText="Saving..."
|
||||
type="submit"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={false}
|
||||
id="saveConnection"
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<Memo(LoadingWrapper)
|
||||
loading={false}
|
||||
text="Saving..."
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save"
|
||||
id="add_outgoing_oauth_connection.save"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</Memo(LoadingWrapper)>
|
||||
</button>
|
||||
</Memo(SpinnerButton)>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
className="outgoing-oauth-connections-docs-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Get help with <link>configuring outgoing OAuth connections</link>."
|
||||
id="add_outgoing_oauth_connection.documentation_link"
|
||||
values={
|
||||
Object {
|
||||
"link": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Get help with
|
||||
<a
|
||||
href="https://mattermost.com/pl/outgoing-oauth-connections"
|
||||
key=".$.1"
|
||||
>
|
||||
configuring outgoing OAuth connections
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-primary"
|
||||
confirmButtonText="Save anyway"
|
||||
message="This connection has not been validated, Do you want to save anyway?"
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
onExited={[Function]}
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={false}
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
</AddOutgoingOAuthConnection>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,137 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integrations/InstalledOutgoingOAuthConnection should match snapshot 1`] = `
|
||||
<div
|
||||
className="backstage-list__item"
|
||||
>
|
||||
<div
|
||||
className="item-details"
|
||||
>
|
||||
<div
|
||||
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
|
||||
>
|
||||
<strong
|
||||
className="item-details__name"
|
||||
>
|
||||
My OAuth Connection
|
||||
</strong>
|
||||
<div
|
||||
className="item-actions"
|
||||
>
|
||||
<Link
|
||||
to="/team_name/integrations/outgoing-oauth2-connections/edit?id=someid"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Edit"
|
||||
id="installed_integrations.edit"
|
||||
/>
|
||||
</Link>
|
||||
-
|
||||
<Connect(DeleteIntegrationLink)
|
||||
modalMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deleting this connection will break any integrations using it"
|
||||
id="installed_outgoing_oauth_connections.delete.wanring"
|
||||
/>
|
||||
}
|
||||
onDelete={[Function]}
|
||||
subtitleText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Are you sure you want to delete {connectionName}?"
|
||||
id="installed_outgoing_oauth_connections.delete.confirm"
|
||||
values={
|
||||
Object {
|
||||
"connectionName": <strong>
|
||||
My OAuth Connection
|
||||
</strong>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
>
|
||||
<span
|
||||
className="item-details__token"
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="Client ID: **{clientId}**"
|
||||
id="installed_integrations.client_id"
|
||||
values={
|
||||
Object {
|
||||
"clientId": "someid",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
>
|
||||
<span
|
||||
className="item-details__token"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Client Secret: ********"
|
||||
id="installed_outgoing_oauth_connections.client_secret"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
>
|
||||
<span
|
||||
className="item-details__url word-break--all"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Audience URLs: {urls}"
|
||||
id="installed_integrations.audience_urls"
|
||||
values={
|
||||
Object {
|
||||
"urls": "https://myaudience.com",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
>
|
||||
<span
|
||||
className="item-details__url word-break--all"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Token URL: {url}"
|
||||
id="installed_integrations.token_url"
|
||||
values={
|
||||
Object {
|
||||
"url": "https://tokenurl.com",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="item-details__row"
|
||||
>
|
||||
<span
|
||||
className="item-details__creation"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Created by {creator} on {createAt, date, full}"
|
||||
id="installed_integrations.creation"
|
||||
values={
|
||||
Object {
|
||||
"createAt": 1501365458934,
|
||||
"creator": "somename",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,260 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integrations/InstalledOutgoingOAuthConnections should match snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<InstalledOutgoingOAuthConnections
|
||||
team={
|
||||
Object {
|
||||
"allow_open_invite": false,
|
||||
"allowed_domains": "",
|
||||
"company_name": "",
|
||||
"create_at": 0,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "name",
|
||||
"email": "",
|
||||
"group_constrained": false,
|
||||
"id": "team_id",
|
||||
"invite_id": "",
|
||||
"name": "test",
|
||||
"scheme_id": "id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<BackstageList
|
||||
addButtonId="addOutgoingOauthConnection"
|
||||
addLink="/test/integrations/outgoing-oauth2-connections/add"
|
||||
addText="Add Outgoing OAuth Connection"
|
||||
emptyText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="No Outgoing OAuth Connections found"
|
||||
id="installed_outgoing_oauth_connections.empty"
|
||||
/>
|
||||
}
|
||||
emptyTextSearch={
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="No Outgoing OAuth Connections match {searchTerm}"
|
||||
id="installed_outgoing_oauth_connections.emptySearch"
|
||||
/>
|
||||
}
|
||||
header={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Outgoing OAuth Connections"
|
||||
id="installed_outgoing_oauth_connections.header"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Create {outgoingOauthConnections} to securely integrate bots and third-party apps with Mattermost."
|
||||
id="installed_outgoing_oauth_connections.help"
|
||||
values={
|
||||
Object {
|
||||
"outgoingOauthConnections": <ExternalLink
|
||||
href="https://mattermost.com/pl/setup-oauth-2.0"
|
||||
location="installed_outgoing_oauth_connections"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Outgoing OAuth Connections"
|
||||
id="installed_outgoing_oauth_connections.help.outgoingOauthConnections"
|
||||
/>
|
||||
</ExternalLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
loading={true}
|
||||
searchPlaceholder="Search Outgoing OAuth Connections"
|
||||
>
|
||||
<div
|
||||
className="backstage-content"
|
||||
>
|
||||
<div
|
||||
className="backstage-header"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Outgoing OAuth Connections"
|
||||
id="installed_outgoing_oauth_connections.header"
|
||||
>
|
||||
<span>
|
||||
Outgoing OAuth Connections
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</h1>
|
||||
<Link
|
||||
className="add-link"
|
||||
to="/test/integrations/outgoing-oauth2-connections/add"
|
||||
>
|
||||
<LinkAnchor
|
||||
className="add-link"
|
||||
href="/test/integrations/outgoing-oauth2-connections/add"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
className="add-link"
|
||||
href="/test/integrations/outgoing-oauth2-connections/add"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
id="addOutgoingOauthConnection"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Add Outgoing OAuth Connection
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</div>
|
||||
<div
|
||||
className="backstage-filters"
|
||||
>
|
||||
<div
|
||||
className="backstage-filter__search"
|
||||
>
|
||||
<SearchIcon>
|
||||
<i
|
||||
className="fa fa-search"
|
||||
title="Search Icon"
|
||||
/>
|
||||
</SearchIcon>
|
||||
<input
|
||||
className="form-control"
|
||||
id="searchInput"
|
||||
onChange={[Function]}
|
||||
placeholder="Search Outgoing OAuth Connections"
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 0,
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
type="search"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="backstage-list__help"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Create {outgoingOauthConnections} to securely integrate bots and third-party apps with Mattermost."
|
||||
id="installed_outgoing_oauth_connections.help"
|
||||
values={
|
||||
Object {
|
||||
"outgoingOauthConnections": <ExternalLink
|
||||
href="https://mattermost.com/pl/setup-oauth-2.0"
|
||||
location="installed_outgoing_oauth_connections"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Outgoing OAuth Connections"
|
||||
id="installed_outgoing_oauth_connections.help.outgoingOauthConnections"
|
||||
/>
|
||||
</ExternalLink>,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Create
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/pl/setup-oauth-2.0"
|
||||
key=".$.1"
|
||||
location="installed_outgoing_oauth_connections"
|
||||
>
|
||||
<a
|
||||
href="https://mattermost.com/pl/setup-oauth-2.0?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=installed_outgoing_oauth_connections&uid=current_user_id&sid="
|
||||
location="installed_outgoing_oauth_connections"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Outgoing OAuth Connections"
|
||||
id="installed_outgoing_oauth_connections.help.outgoingOauthConnections"
|
||||
>
|
||||
<span>
|
||||
Outgoing OAuth Connections
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</a>
|
||||
</ExternalLink>
|
||||
to securely integrate bots and third-party apps with Mattermost.
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
<div
|
||||
className="backstage-list"
|
||||
>
|
||||
<LoadingScreen>
|
||||
<div
|
||||
className="loading-screen"
|
||||
style={
|
||||
Object {
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="loading__content"
|
||||
>
|
||||
<p>
|
||||
Loading
|
||||
</p>
|
||||
<div
|
||||
className="round round-1"
|
||||
/>
|
||||
<div
|
||||
className="round round-2"
|
||||
/>
|
||||
<div
|
||||
className="round round-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingScreen>
|
||||
</div>
|
||||
</div>
|
||||
</BackstageList>
|
||||
</InstalledOutgoingOAuthConnections>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
@@ -0,0 +1,417 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudienceInput should match snapshot when an audience url with a wildcard is configured, and typed in value starts with configured audience url 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[MockFunction]}
|
||||
placeholder=""
|
||||
value="https://aud.com/api/it_matches"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
value="https://aud.com/api/it_matches"
|
||||
/>
|
||||
<div
|
||||
className="outgoing-oauth-audience-match-message-container"
|
||||
>
|
||||
<span>
|
||||
<InformationOutlineIcon
|
||||
size={20}
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height={20}
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width={20}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"
|
||||
/>
|
||||
</svg>
|
||||
</InformationOutlineIcon>
|
||||
</span>
|
||||
<span
|
||||
className="outgoing-oauth-audience-match-message"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Not linked to an OAuth connection"
|
||||
id="add_outgoing_oauth_connection.not_connected"
|
||||
>
|
||||
<span>
|
||||
Not linked to an OAuth connection
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
</div>
|
||||
</OAuthConnectionAudienceInput>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudienceInput should match snapshot when typed in value does not have an exact match 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[MockFunction]}
|
||||
placeholder=""
|
||||
value="https://aud.com/api/no_match"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
value="https://aud.com/api/no_match"
|
||||
/>
|
||||
<div
|
||||
className="outgoing-oauth-audience-match-message-container"
|
||||
>
|
||||
<span>
|
||||
<InformationOutlineIcon
|
||||
size={20}
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height={20}
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width={20}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"
|
||||
/>
|
||||
</svg>
|
||||
</InformationOutlineIcon>
|
||||
</span>
|
||||
<span
|
||||
className="outgoing-oauth-audience-match-message"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Not linked to an OAuth connection"
|
||||
id="add_outgoing_oauth_connection.not_connected"
|
||||
>
|
||||
<span>
|
||||
Not linked to an OAuth connection
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
</div>
|
||||
</OAuthConnectionAudienceInput>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudienceInput should match snapshot when typed in value matches a configured audience 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[MockFunction]}
|
||||
placeholder=""
|
||||
value="https://aud.com/api"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
value="https://aud.com/api"
|
||||
/>
|
||||
<div
|
||||
className="outgoing-oauth-audience-match-message-container"
|
||||
>
|
||||
<span>
|
||||
<InformationOutlineIcon
|
||||
size={20}
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height={20}
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width={20}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"
|
||||
/>
|
||||
</svg>
|
||||
</InformationOutlineIcon>
|
||||
</span>
|
||||
<span
|
||||
className="outgoing-oauth-audience-match-message"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Not linked to an OAuth connection"
|
||||
id="add_outgoing_oauth_connection.not_connected"
|
||||
>
|
||||
<span>
|
||||
Not linked to an OAuth connection
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
</div>
|
||||
</OAuthConnectionAudienceInput>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudienceInput should match snapshot with existing connections 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[MockFunction]}
|
||||
placeholder=""
|
||||
value=""
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="outgoing-oauth-audience-match-message-container"
|
||||
>
|
||||
<span>
|
||||
<InformationOutlineIcon
|
||||
size={20}
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height={20}
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width={20}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"
|
||||
/>
|
||||
</svg>
|
||||
</InformationOutlineIcon>
|
||||
</span>
|
||||
<span
|
||||
className="outgoing-oauth-audience-match-message"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Not linked to an OAuth connection"
|
||||
id="add_outgoing_oauth_connection.not_connected"
|
||||
>
|
||||
<span>
|
||||
Not linked to an OAuth connection
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
</div>
|
||||
</OAuthConnectionAudienceInput>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudienceInput should match snapshot with no existing connections 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "undefinedundefined",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<OAuthConnectionAudienceInput
|
||||
onChange={[MockFunction]}
|
||||
placeholder=""
|
||||
value=""
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id="url"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
value=""
|
||||
/>
|
||||
</OAuthConnectionAudienceInput>
|
||||
</Provider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {act} from 'react-dom/test-utils';
|
||||
import {Provider} from 'react-redux';
|
||||
import {BrowserRouter as Router} from 'react-router-dom';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import AbstractOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/abstract_outgoing_oauth_connection';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/integrations/AbstractOutgoingOAuthConnection', () => {
|
||||
const header = {id: 'Header', defaultMessage: 'Header'};
|
||||
const footer = {id: 'Footer', defaultMessage: 'Footer'};
|
||||
const loading = {id: 'Loading', defaultMessage: 'Loading'};
|
||||
const initialConnection: OutgoingOAuthConnection = {
|
||||
id: 'facxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'testConnection',
|
||||
client_secret: '',
|
||||
client_id: 'clientid',
|
||||
create_at: 1501365458934,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458934,
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://token.com',
|
||||
audiences: ['https://aud.com'],
|
||||
};
|
||||
|
||||
const outgoingOAuthConnections: Record<string, OutgoingOAuthConnection> = {
|
||||
facxd9wpzpbpfp8pad78xj75pr: initialConnection,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableOutgoingOAuthConnections: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {id: 'system_role', permissions: [Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
outgoingOAuthConnections,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const team = TestHelper.getTeamMock({name: 'test', id: initialConnection.id});
|
||||
|
||||
const baseProps: React.ComponentProps<typeof AbstractOutgoingOAuthConnection> = {
|
||||
team,
|
||||
header,
|
||||
footer,
|
||||
loading,
|
||||
renderExtra: <div>{'renderExtra'}</div>,
|
||||
serverError: '',
|
||||
initialConnection,
|
||||
submitAction: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const props = {...baseProps};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<AbstractOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, displays client error', () => {
|
||||
const submitAction = jest.fn().mockResolvedValue({data: true});
|
||||
|
||||
const newServerError = 'serverError';
|
||||
const props = {...baseProps, serverError: newServerError, submitAction};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<AbstractOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
wrapper.find('#audienceUrls').simulate('change', {target: {value: ''}});
|
||||
wrapper.find('button.btn-primary').simulate('click', {preventDefault() {
|
||||
return jest.fn();
|
||||
}});
|
||||
|
||||
expect(submitAction).not.toBeCalled();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('FormError').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should call action function', async () => {
|
||||
const submitAction = jest.fn().mockResolvedValue({data: true});
|
||||
|
||||
const props = {...baseProps, submitAction};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<AbstractOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('#name').simulate('change', {target: {value: 'name'}});
|
||||
wrapper.find('button.btn-primary').simulate('click', {preventDefault() {
|
||||
return jest.fn();
|
||||
}});
|
||||
|
||||
expect(submitAction).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,632 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChangeEvent, FormEvent} from 'react';
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import {AlertOutlineIcon, CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {validateOutgoingOAuthConnection} from 'mattermost-redux/actions/integrations';
|
||||
|
||||
import BackstageHeader from 'components/backstage/components/backstage_header';
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
import FormError from 'components/form_error';
|
||||
import SpinnerButton from 'components/spinner_button';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
type Props = {
|
||||
team: Team;
|
||||
header: MessageDescriptor;
|
||||
footer: MessageDescriptor;
|
||||
loading: MessageDescriptor;
|
||||
renderExtra?: JSX.Element;
|
||||
serverError: string;
|
||||
|
||||
initialConnection?: OutgoingOAuthConnection;
|
||||
|
||||
submitAction: (connection: OutgoingOAuthConnection) => Promise<void>;
|
||||
}
|
||||
|
||||
type State = {
|
||||
name: string;
|
||||
oauthTokenUrl: string;
|
||||
grantType: OutgoingOAuthConnection['grant_type'];
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
audienceUrls: string;
|
||||
};
|
||||
|
||||
enum ValidationStatus {
|
||||
INITIAL = 'initial',
|
||||
DIRTY = 'dirty',
|
||||
VALIDATING = 'validating',
|
||||
VALIDATED = 'validated',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
const useOutgoingOAuthForm = (connection: OutgoingOAuthConnection): [State, (state: Partial<State>) => void] => {
|
||||
const initialState: State = {
|
||||
name: connection.name || '',
|
||||
audienceUrls: connection.audiences ? connection.audiences.join('\n') : '',
|
||||
oauthTokenUrl: connection.oauth_token_url || '',
|
||||
clientId: connection.client_id || '',
|
||||
clientSecret: connection.client_secret || '',
|
||||
grantType: 'client_credentials',
|
||||
};
|
||||
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
return useMemo(() => [state, (newState: Partial<State>) => {
|
||||
setState((oldState) => ({...oldState, ...newState}));
|
||||
}], [state]);
|
||||
};
|
||||
|
||||
const initialState: OutgoingOAuthConnection = {
|
||||
id: '',
|
||||
name: '',
|
||||
creator_id: '',
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
oauth_token_url: '',
|
||||
grant_type: 'client_credentials',
|
||||
audiences: [],
|
||||
};
|
||||
|
||||
export default function AbstractOutgoingOAuthConnection(props: Props) {
|
||||
const [formState, setFormState] = useOutgoingOAuthForm(props.initialConnection || initialState);
|
||||
|
||||
const [storedError, setError] = useState<React.ReactNode>('');
|
||||
const [validationError, setValidationError] = useState<string>('');
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<ValidationStatus>(ValidationStatus.INITIAL);
|
||||
const [isEditingSecret, setIsEditingSecret] = useState(false);
|
||||
|
||||
const [isValidationModalOpen, setIsValidationModalOpen] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isNewConnection = !props.initialConnection;
|
||||
|
||||
const parseForm = (requireAudienceUrl: boolean): OutgoingOAuthConnection | undefined => {
|
||||
if (!formState.name) {
|
||||
setIsSubmitting(false);
|
||||
setError(
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.name.required'
|
||||
defaultMessage='Name for the OAuth connection is required.'
|
||||
/>,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!formState.clientId) {
|
||||
setIsSubmitting(false);
|
||||
setError(
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.client_id.required'
|
||||
defaultMessage='Client Id for the OAuth connection is required.'
|
||||
/>,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((isNewConnection || isEditingSecret) && !formState.clientSecret) {
|
||||
setIsSubmitting(false);
|
||||
setError(
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.client_secret.required'
|
||||
defaultMessage='Client Secret for the OAuth connection is required.'
|
||||
/>,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!formState.grantType) {
|
||||
setIsSubmitting(false);
|
||||
setError(
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.grant_type.required'
|
||||
defaultMessage='Grant Type for the OAuth connection is required.'
|
||||
/>,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!formState.oauthTokenUrl) {
|
||||
setIsSubmitting(false);
|
||||
setError(
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.oauth_token_url.required'
|
||||
defaultMessage='OAuth Token URL for the OAuth connection is required.'
|
||||
/>,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const audienceUrls = [];
|
||||
for (let audienceUrl of formState.audienceUrls.split('\n')) {
|
||||
audienceUrl = audienceUrl.trim();
|
||||
|
||||
if (audienceUrl.length > 0) {
|
||||
audienceUrls.push(audienceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (requireAudienceUrl && audienceUrls.length === 0) {
|
||||
setIsSubmitting(false);
|
||||
setError(
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.audienceUrls.required'
|
||||
defaultMessage='One or more audience URLs are required.'
|
||||
/>,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const connection = {
|
||||
name: formState.name,
|
||||
audiences: audienceUrls,
|
||||
client_id: formState.clientId,
|
||||
client_secret: formState.clientSecret,
|
||||
grant_type: formState.grantType,
|
||||
oauth_token_url: formState.oauthTokenUrl,
|
||||
} as OutgoingOAuthConnection;
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
const showSkipValidateModal = () => {
|
||||
setIsValidationModalOpen(true);
|
||||
};
|
||||
|
||||
const hideSkipValidateModal = () => {
|
||||
setIsValidationModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmitFromButton = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = parseForm(true);
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
|
||||
if (validationStatus !== ValidationStatus.VALIDATED && !(!isNewConnection && validationStatus === ValidationStatus.INITIAL)) {
|
||||
if (!isValidationModalOpen) {
|
||||
showSkipValidateModal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const res = props.submitAction(connection);
|
||||
res.then(() => setIsSubmitting(false));
|
||||
};
|
||||
|
||||
const handleValidate = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (validationStatus === ValidationStatus.VALIDATING) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setValidationStatus(ValidationStatus.VALIDATING);
|
||||
|
||||
const connection = parseForm(false);
|
||||
if (!connection) {
|
||||
// Defer to the form validation error
|
||||
setValidationStatus(ValidationStatus.INITIAL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.initialConnection?.id) {
|
||||
connection.id = props.initialConnection.id;
|
||||
}
|
||||
|
||||
const {error} = await dispatch(validateOutgoingOAuthConnection(props.team.id, connection));
|
||||
|
||||
if (error) {
|
||||
setValidationStatus(ValidationStatus.ERROR);
|
||||
setValidationError(error.message);
|
||||
} else {
|
||||
setValidationStatus(ValidationStatus.VALIDATED);
|
||||
}
|
||||
};
|
||||
|
||||
const setUnvalidated = (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (validationStatus !== ValidationStatus.DIRTY) {
|
||||
setValidationStatus(ValidationStatus.DIRTY);
|
||||
}
|
||||
|
||||
if (validationError) {
|
||||
setValidationError('');
|
||||
}
|
||||
};
|
||||
|
||||
const updateName = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFormState({
|
||||
name: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const updateClientId = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setUnvalidated();
|
||||
|
||||
setFormState({
|
||||
clientId: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const updateClientSecret = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setUnvalidated();
|
||||
|
||||
setFormState({
|
||||
clientSecret: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const updateOAuthTokenURL = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setUnvalidated();
|
||||
|
||||
setFormState({
|
||||
oauthTokenUrl: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const updateAudienceUrls = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setFormState({
|
||||
audienceUrls: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const startEditingClientSecret = () => {
|
||||
setIsEditingSecret(true);
|
||||
};
|
||||
|
||||
const headerToRender = props.header;
|
||||
const footerToRender = props.footer;
|
||||
|
||||
let clientSecretSection = (
|
||||
<input
|
||||
id='client_secret'
|
||||
type='text'
|
||||
autoComplete='off'
|
||||
className='form-control'
|
||||
value={formState.clientSecret}
|
||||
onChange={updateClientSecret}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isNewConnection && !isEditingSecret) {
|
||||
clientSecretSection = (
|
||||
<>
|
||||
<input
|
||||
id='client_secret'
|
||||
disabled={true}
|
||||
autoComplete='off'
|
||||
type='text'
|
||||
className='form-control disabled'
|
||||
value={'•'.repeat(40)}
|
||||
/>
|
||||
<span
|
||||
onClick={startEditingClientSecret}
|
||||
className='outgoing-oauth-connections-edit-secret'
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='backstage-content'>
|
||||
<BackstageHeader>
|
||||
<Link to={`/${props.team.name}/integrations/outgoing-oauth2-connections`}>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.header'
|
||||
defaultMessage='Outgoing OAuth Connections'
|
||||
/>
|
||||
</Link>
|
||||
<FormattedMessage
|
||||
id={headerToRender.id}
|
||||
defaultMessage={headerToRender.defaultMessage}
|
||||
/>
|
||||
</BackstageHeader>
|
||||
<div className='backstage-form'>
|
||||
<form className='form-horizontal'>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='name'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.name.label'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='name'
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={formState.name}
|
||||
onChange={updateName}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.name.help'
|
||||
defaultMessage='Specify the name for your OAuth connection.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='client_id'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.client_id.label'
|
||||
defaultMessage='Client ID'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='client_id'
|
||||
type='text'
|
||||
autoComplete='off'
|
||||
className='form-control'
|
||||
value={formState.clientId}
|
||||
onChange={updateClientId}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.client_id.help'
|
||||
defaultMessage='Specify the Client ID for your OAuth connection.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='client_secret'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.client_secret.label'
|
||||
defaultMessage='Client Secret'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
{clientSecretSection}
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.client_secret.help'
|
||||
defaultMessage='Specify the Client Secret for your OAuth connection.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='oauth_token_url'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.oauth_token_url.label'
|
||||
defaultMessage='OAuth Token URL'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='token_url'
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={formState.oauthTokenUrl}
|
||||
onChange={updateOAuthTokenURL}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.oauth_token_url.help'
|
||||
defaultMessage='Specify the OAuth Token URL for your OAuth connection.'
|
||||
/>
|
||||
</div>
|
||||
<div className='outgoing-oauth-connection-validate-button-container'>
|
||||
<ValidateButton
|
||||
onClick={handleValidate}
|
||||
setUnvalidated={setUnvalidated}
|
||||
status={validationStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='audienceUrls'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.audienceUrls.label'
|
||||
defaultMessage='Audience URLs (One Per Line)'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<textarea
|
||||
id='audienceUrls'
|
||||
rows={3}
|
||||
className='form-control'
|
||||
value={formState.audienceUrls}
|
||||
onChange={updateAudienceUrls}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.audienceUrls.help'
|
||||
defaultMessage='The URLs which will receive requests with the OAuth token, e.g. your custom slash command handler endpoint. Must be a valid URL and start with http:// or https://.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='backstage-form__footer'>
|
||||
<FormError
|
||||
type='backstage'
|
||||
errors={[props.serverError, storedError]}
|
||||
/>
|
||||
<Link
|
||||
className='btn btn-tertiary'
|
||||
to={`/${props.team.name}/integrations/outgoing-oauth2-connections`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Link>
|
||||
<SpinnerButton
|
||||
className='btn btn-primary'
|
||||
type='submit'
|
||||
spinning={isSubmitting}
|
||||
spinningText={intl.formatMessage(props.loading)}
|
||||
onClick={handleSubmitFromButton}
|
||||
id='saveConnection'
|
||||
>
|
||||
<FormattedMessage
|
||||
id={footerToRender.id}
|
||||
defaultMessage={footerToRender.defaultMessage}
|
||||
/>
|
||||
</SpinnerButton>
|
||||
{props.renderExtra}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className='outgoing-oauth-connections-docs-link'>
|
||||
<FormattedMessage
|
||||
id={'add_outgoing_oauth_connection.documentation_link'}
|
||||
defaultMessage={'Get help with <link>configuring outgoing OAuth connections</link>.'}
|
||||
values={{
|
||||
link: (text: string) => (
|
||||
<a href='https://mattermost.com/pl/outgoing-oauth-connections'>{text}</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
show={isValidationModalOpen}
|
||||
message={intl.formatMessage({
|
||||
id: 'add_outgoing_oauth_connection.save_without_validation_warning',
|
||||
defaultMessage: 'This connection has not been validated, Do you want to save anyway?',
|
||||
})}
|
||||
title={intl.formatMessage({
|
||||
id: 'add_outgoing_oauth_connection.confirm_save',
|
||||
defaultMessage: 'Save Outgoing OAuth Connection',
|
||||
})}
|
||||
confirmButtonText={intl.formatMessage({
|
||||
id: 'add_outgoing_oauth_connection.save_anyway',
|
||||
defaultMessage: 'Save anyway',
|
||||
})}
|
||||
onExited={hideSkipValidateModal}
|
||||
onCancel={hideSkipValidateModal}
|
||||
onConfirm={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ValidateButtonProps = {
|
||||
status: ValidationStatus;
|
||||
onClick: (e: FormEvent) => void;
|
||||
setUnvalidated: (e: FormEvent) => void;
|
||||
}
|
||||
|
||||
const ValidateButton = ({status, onClick, setUnvalidated}: ValidateButtonProps) => {
|
||||
if (status === ValidationStatus.ERROR) {
|
||||
return (
|
||||
<span
|
||||
className='outgoing-oauth-connection-validation-message validation-error'
|
||||
>
|
||||
<AlertOutlineIcon size={20}/>
|
||||
<FormattedMessage
|
||||
id={'add_outgoing_oauth_connection.validation_error'}
|
||||
defaultMessage={'Connection not validated. Please check the server logs for details or <link>try again</link>.'}
|
||||
values={{
|
||||
link: (text: string) => <a onClick={setUnvalidated}>{text}</a>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === ValidationStatus.VALIDATED) {
|
||||
return (
|
||||
<span
|
||||
className='outgoing-oauth-connection-validation-message validation-success'
|
||||
>
|
||||
<CheckCircleOutlineIcon size={20}/>
|
||||
<FormattedMessage
|
||||
id={'add_outgoing_oauth_connection.validated_connection'}
|
||||
defaultMessage={'Validated connection'}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === ValidationStatus.VALIDATING) {
|
||||
return (
|
||||
<span
|
||||
className='outgoing-oauth-connection-validation-message'
|
||||
>
|
||||
<LoadingSpinner
|
||||
text={(
|
||||
<FormattedMessage
|
||||
id={'add_outgoing_oauth_connection.validating'}
|
||||
defaultMessage={'Validating...'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const validateButton = (
|
||||
<button
|
||||
className='btn btn-tertiary btn-sm'
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
id='validateConnection'
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'add_outgoing_oauth_connection.validate'}
|
||||
defaultMessage={'Validate Connection'}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
return validateButton;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import {BrowserRouter as Router} from 'react-router-dom';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import AddOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/add_outgoing_oauth_connection';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/integrations/AddOutgoingOAuthConnection', () => {
|
||||
const team = TestHelper.getTeamMock({
|
||||
id: 'dbcxd9wpzpbpfp8pad78xj12pr',
|
||||
name: 'test',
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const baseProps: React.ComponentProps<typeof AddOutgoingOAuthConnection> = {
|
||||
team,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableOutgoingOAuthConnections: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {id: 'system_role', permissions: [Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const props = {...baseProps};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<AddOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {defineMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {addOutgoingOAuthConnection} from 'mattermost-redux/actions/integrations';
|
||||
|
||||
import AbstractOutgoingOAuthConnection from './abstract_outgoing_oauth_connection';
|
||||
|
||||
const HEADER = defineMessage({id: 'add_outgoing_oauth_connection.add', defaultMessage: 'Add'});
|
||||
const FOOTER = defineMessage({id: 'add_outgoing_oauth_connection.save', defaultMessage: 'Save'});
|
||||
const LOADING = defineMessage({id: 'add_outgoing_oauth_connection.saving', defaultMessage: 'Saving...'});
|
||||
|
||||
export type Props = {
|
||||
team: Team;
|
||||
};
|
||||
|
||||
const AddOutgoingOAuthConnection = ({team}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [serverError, setServerError] = useState('');
|
||||
|
||||
const submit = async (connection: OutgoingOAuthConnection) => {
|
||||
setServerError('');
|
||||
|
||||
const {data, error} = (await dispatch(addOutgoingOAuthConnection(team.id, connection))) as unknown as {data: OutgoingOAuthConnection; error: Error};
|
||||
if (data) {
|
||||
history.push(`/${team.name}/integrations/confirm?type=outgoing-oauth2-connections&id=${data.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setServerError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbstractOutgoingOAuthConnection
|
||||
team={team}
|
||||
header={HEADER}
|
||||
footer={FOOTER}
|
||||
loading={LOADING}
|
||||
submitAction={submit}
|
||||
serverError={serverError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddOutgoingOAuthConnection;
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import {BrowserRouter as Router} from 'react-router-dom';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import EditOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/edit_outgoing_oauth_connection';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/integrations/EditOutgoingOAuthConnection', () => {
|
||||
const team = TestHelper.getTeamMock({
|
||||
id: 'dbcxd9wpzpbpfp8pad78xj12pr',
|
||||
name: 'test',
|
||||
});
|
||||
|
||||
const outgoingOAuthConnections: Record<string, OutgoingOAuthConnection> = {
|
||||
facxd9wpzpbpfp8pad78xj75pr: {
|
||||
id: 'facxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'firstConnection',
|
||||
client_secret: '',
|
||||
create_at: 1501365458934,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458934,
|
||||
audiences: ['https://mysite.com/api', 'https://myothersite.com/api/v2'],
|
||||
client_id: 'client1',
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://oauthtoken1.com/oauth/token',
|
||||
},
|
||||
fzcxd9wpzpbpfp8pad78xj75pr: {
|
||||
id: 'fzcxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'secondConnection',
|
||||
client_secret: '',
|
||||
create_at: 1501365458935,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458935,
|
||||
audiences: ['https://myaudience.com/api'],
|
||||
client_id: 'client2',
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://oauthtoken2.com/oauth/token',
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableOutgoingOAuthConnections: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {id: 'system_role', permissions: [Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
outgoingOAuthConnections,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const connection: OutgoingOAuthConnection = {
|
||||
id: 'facxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'testApp',
|
||||
client_secret: '88cxd9wpzpbpfp8pad78xj75pr',
|
||||
create_at: 1501365458934,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458934,
|
||||
audiences: ['https://test.com/callback', 'https://test.com/callback2'],
|
||||
client_id: 'someclientid',
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://mytoken.url',
|
||||
};
|
||||
|
||||
const baseProps: React.ComponentProps<typeof EditOutgoingOAuthConnection> = {
|
||||
team,
|
||||
location: {
|
||||
search: '?id=facxd9wpzpbpfp8pad78xj75pr',
|
||||
} as any,
|
||||
};
|
||||
|
||||
test('should match snapshot, loading', () => {
|
||||
const props = {...baseProps, oauthApp: connection};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<EditOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const props = {...baseProps, oauthApp: connection};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<EditOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when EnableOAuthServiceProvider is false', () => {
|
||||
const props = {...baseProps, oauthApp: connection, enableOAuthServiceProvider: false};
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<EditOutgoingOAuthConnection {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage, defineMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {editOutgoingOAuthConnection, getOutgoingOAuthConnection} from 'mattermost-redux/actions/integrations';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getOutgoingOAuthConnections} from 'mattermost-redux/selectors/entities/integrations';
|
||||
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
|
||||
import AbstractOutgoingOAuthConnection from './abstract_outgoing_oauth_connection';
|
||||
|
||||
const HEADER = defineMessage({id: 'integrations.edit', defaultMessage: 'Edit'});
|
||||
const FOOTER = defineMessage({id: 'edit_outgoing_oauth_connection.update', defaultMessage: 'Update'});
|
||||
const LOADING = defineMessage({id: 'edit_outgoing_oauth_connection.updating', defaultMessage: 'Updating...'});
|
||||
|
||||
type Props = {
|
||||
team: Team;
|
||||
location: Location;
|
||||
};
|
||||
|
||||
const getConnectionIdFromSearch = (search: string): string => {
|
||||
return (new URLSearchParams(search)).get('id') || '';
|
||||
};
|
||||
|
||||
const EditOutgoingOAuthConnection = (props: Props) => {
|
||||
const connectionId = getConnectionIdFromSearch(props.location.search);
|
||||
const connections = useSelector(getOutgoingOAuthConnections);
|
||||
const existingConnection = connections[connectionId];
|
||||
|
||||
const [newConnection, setNewConnection] = useState(existingConnection);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [serverError, setServerError] = useState('');
|
||||
|
||||
const enableOAuthServiceProvider = useSelector(getConfig).EnableOAuthServiceProvider;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (enableOAuthServiceProvider) {
|
||||
dispatch(getOutgoingOAuthConnection(props.team.id, connectionId));
|
||||
}
|
||||
}, [connectionId, enableOAuthServiceProvider, props.team, dispatch]);
|
||||
|
||||
const handleInitialSubmit = async (connection: OutgoingOAuthConnection) => {
|
||||
setNewConnection(connection);
|
||||
|
||||
if (existingConnection.id) {
|
||||
connection.id = existingConnection.id;
|
||||
}
|
||||
|
||||
const audienceUrlsSame = (existingConnection.audiences.length === connection.audiences.length) &&
|
||||
existingConnection.audiences.every((v, i) => v === connection.audiences[i]);
|
||||
|
||||
if (audienceUrlsSame) {
|
||||
await createOutgoingOAuthConnection(connection);
|
||||
} else {
|
||||
handleConfirmModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmModal = () => {
|
||||
setShowConfirmModal(true);
|
||||
};
|
||||
|
||||
const confirmModalDismissed = () => {
|
||||
setShowConfirmModal(false);
|
||||
};
|
||||
|
||||
const createOutgoingOAuthConnection = async (connection: OutgoingOAuthConnection) => {
|
||||
setServerError('');
|
||||
|
||||
const res = await dispatch(editOutgoingOAuthConnection(props.team.id, connection));
|
||||
|
||||
if ('data' in res && res.data) {
|
||||
getHistory().push(`/${props.team.name}/integrations/outgoing-oauth2-connections`);
|
||||
return;
|
||||
}
|
||||
|
||||
confirmModalDismissed();
|
||||
|
||||
if ('error' in res) {
|
||||
const {error: err} = res as {error: Error};
|
||||
setServerError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const renderExtra = () => {
|
||||
const confirmButton = (
|
||||
<FormattedMessage
|
||||
id='update_command.update'
|
||||
defaultMessage='Update'
|
||||
/>
|
||||
);
|
||||
|
||||
const confirmTitle = (
|
||||
<FormattedMessage
|
||||
id='update_outgoing_oauth_connection.confirm'
|
||||
defaultMessage='Edit Outgoing OAuth Connection'
|
||||
/>
|
||||
);
|
||||
|
||||
const confirmMessage = (
|
||||
<FormattedMessage
|
||||
id='update_outgoing_oauth_connection.question'
|
||||
defaultMessage='Your changes may break any existing integrations using this connection. Are you sure you would like to update it?'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
title={confirmTitle}
|
||||
message={confirmMessage}
|
||||
confirmButtonText={confirmButton}
|
||||
modalClass='integrations-backstage-modal'
|
||||
show={showConfirmModal}
|
||||
onConfirm={() => createOutgoingOAuthConnection(newConnection)}
|
||||
onCancel={confirmModalDismissed}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!existingConnection) {
|
||||
return <LoadingScreen/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AbstractOutgoingOAuthConnection
|
||||
team={props.team}
|
||||
header={HEADER}
|
||||
footer={FOOTER}
|
||||
loading={LOADING}
|
||||
renderExtra={renderExtra()}
|
||||
submitAction={handleInitialSubmit}
|
||||
serverError={serverError}
|
||||
initialConnection={existingConnection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditOutgoingOAuthConnection;
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import DeleteIntegrationLink from 'components/integrations/delete_integration_link';
|
||||
import type {InstalledOutgoingOAuthConnectionProps} from 'components/integrations/outgoing_oauth_connections/installed_outgoing_oauth_connection';
|
||||
import InstalledOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/installed_outgoing_oauth_connection';
|
||||
|
||||
describe('components/integrations/InstalledOutgoingOAuthConnection', () => {
|
||||
const team = {name: 'team_name'};
|
||||
const outgoingOAuthConnection: OutgoingOAuthConnection = {
|
||||
id: 'someid',
|
||||
create_at: 1501365458934,
|
||||
creator_id: 'someuserid',
|
||||
update_at: 1501365458934,
|
||||
audiences: ['https://myaudience.com'],
|
||||
client_id: 'someid',
|
||||
client_secret: '',
|
||||
grant_type: 'client_credentials',
|
||||
name: 'My OAuth Connection',
|
||||
oauth_token_url: 'https://tokenurl.com',
|
||||
};
|
||||
|
||||
const baseProps: InstalledOutgoingOAuthConnectionProps = {
|
||||
team,
|
||||
outgoingOAuthConnection,
|
||||
creatorName: 'somename',
|
||||
onDelete: jest.fn(),
|
||||
filter: '',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const props = {...baseProps};
|
||||
const wrapper = shallow(
|
||||
<InstalledOutgoingOAuthConnection {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should have called props.onDelete on handleDelete ', () => {
|
||||
const newOnDelete = jest.fn();
|
||||
const props = {...baseProps, team, onDelete: newOnDelete};
|
||||
const wrapper = shallow(
|
||||
<InstalledOutgoingOAuthConnection {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(DeleteIntegrationLink).exists()).toBe(true);
|
||||
wrapper.find(DeleteIntegrationLink).props().onDelete();
|
||||
expect(newOnDelete).toBeCalled();
|
||||
expect(newOnDelete).toHaveBeenCalledWith(outgoingOAuthConnection);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
|
||||
import DeleteIntegrationLink from '../delete_integration_link';
|
||||
|
||||
export function matchesFilter(outgoingOAuthConnection: OutgoingOAuthConnection, filter?: string | null): boolean {
|
||||
if (!filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return outgoingOAuthConnection.name.toLowerCase().includes(filter);
|
||||
}
|
||||
|
||||
export type InstalledOutgoingOAuthConnectionProps = {
|
||||
team: Partial<Team>;
|
||||
outgoingOAuthConnection: OutgoingOAuthConnection;
|
||||
creatorName: string;
|
||||
filter?: string | null;
|
||||
|
||||
onDelete: (outgoingOAuthConnection: OutgoingOAuthConnection) => void;
|
||||
}
|
||||
|
||||
export type InstalledOutgoingOAuthConnectionState = {
|
||||
clientSecret: string;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
const InstalledOutgoingOAuthConnection = (props: InstalledOutgoingOAuthConnectionProps) => {
|
||||
const handleDelete = (): void => {
|
||||
props.onDelete(props.outgoingOAuthConnection);
|
||||
};
|
||||
|
||||
const {outgoingOAuthConnection, creatorName} = props;
|
||||
|
||||
if (!matchesFilter(outgoingOAuthConnection, props.filter)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let name;
|
||||
if (outgoingOAuthConnection.name) {
|
||||
name = outgoingOAuthConnection.name;
|
||||
} else {
|
||||
name = (
|
||||
<FormattedMessage
|
||||
id='installed_integrations.unnamed_outgoing_oauth_connection'
|
||||
defaultMessage='Unnamed Outgoing OAuth Connection'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const urls = (
|
||||
<>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__url word-break--all'>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.audience_urls'
|
||||
defaultMessage='Audience URLs: {urls}'
|
||||
values={{
|
||||
urls: outgoingOAuthConnection.audiences.join(', '),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__url word-break--all'>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.token_url'
|
||||
defaultMessage='Token URL: {url}'
|
||||
values={{
|
||||
url: outgoingOAuthConnection.oauth_token_url,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const actions = (
|
||||
<div className='item-actions'>
|
||||
<Link to={`/${props.team.name}/integrations/outgoing-oauth2-connections/edit?id=${outgoingOAuthConnection.id}`}>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.edit'
|
||||
defaultMessage='Edit'
|
||||
/>
|
||||
</Link>
|
||||
{' - '}
|
||||
<DeleteIntegrationLink
|
||||
subtitleText={
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.delete.confirm'
|
||||
defaultMessage='Are you sure you want to delete {connectionName}?'
|
||||
values={{
|
||||
connectionName: (
|
||||
<strong>
|
||||
{props.outgoingOAuthConnection.name}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
modalMessage={
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.delete.wanring'
|
||||
defaultMessage='Deleting this connection will break any integrations using it'
|
||||
/>
|
||||
}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const connectionInfo = (
|
||||
<>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__token'>
|
||||
<FormattedMarkdownMessage
|
||||
id='installed_integrations.client_id'
|
||||
defaultMessage='Client ID: **{clientId}**'
|
||||
values={{
|
||||
clientId: outgoingOAuthConnection.client_id,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__token'>
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.client_secret'
|
||||
defaultMessage='Client Secret: ********'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{outgoingOAuthConnection.grant_type === 'password' && (
|
||||
<>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__token'>
|
||||
<FormattedMarkdownMessage
|
||||
id='installed_outgoing_oauth_connections.username'
|
||||
defaultMessage='Username: **{username}**'
|
||||
values={{
|
||||
username: outgoingOAuthConnection.credentials_username,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__token'>
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.password'
|
||||
defaultMessage='Password: ********'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{urls}
|
||||
<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: creatorName,
|
||||
createAt: outgoingOAuthConnection.create_at,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='backstage-list__item' >
|
||||
<div className='item-details' >
|
||||
<div className='item-details__row d-flex flex-column flex-md-row justify-content-between'>
|
||||
<strong className='item-details__name'>
|
||||
{name}
|
||||
</strong>
|
||||
{actions}
|
||||
</div>
|
||||
{connectionInfo}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default InstalledOutgoingOAuthConnection;
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {act} from 'react-dom/test-utils';
|
||||
import {Provider} from 'react-redux';
|
||||
import {BrowserRouter as Router} from 'react-router-dom';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import InstalledOutgoingOAuthConnections from 'components/integrations/outgoing_oauth_connections/installed_outgoing_oauth_connections';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/integrations/InstalledOutgoingOAuthConnections', () => {
|
||||
const outgoingOAuthConnections: Record<string, OutgoingOAuthConnection> = {
|
||||
facxd9wpzpbpfp8pad78xj75pr: {
|
||||
id: 'facxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'firstConnection',
|
||||
client_secret: '',
|
||||
create_at: 1501365458934,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458934,
|
||||
audiences: ['https://mysite.com/api', 'https://myothersite.com/api/v2'],
|
||||
client_id: 'client1',
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://oauthtoken1.com/oauth/token',
|
||||
},
|
||||
fzcxd9wpzpbpfp8pad78xj75pr: {
|
||||
id: 'fzcxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'secondConnection',
|
||||
client_secret: '',
|
||||
create_at: 1501365458935,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458935,
|
||||
audiences: ['https://myaudience.com/api'],
|
||||
client_id: 'client2',
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://oauthtoken2.com/oauth/token',
|
||||
},
|
||||
};
|
||||
|
||||
const team = TestHelper.getTeamMock({name: 'test'});
|
||||
|
||||
const baseProps: React.ComponentProps<typeof InstalledOutgoingOAuthConnections> = {
|
||||
team,
|
||||
};
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableOutgoingOAuthConnections: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {id: 'system_role', permissions: [Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
outgoingOAuthConnections,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const props = {...baseProps};
|
||||
const store = mockStore(state);
|
||||
|
||||
await act(async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<InstalledOutgoingOAuthConnections {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {deleteOutgoingOAuthConnection} from 'mattermost-redux/actions/integrations';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getOutgoingOAuthConnections} from 'mattermost-redux/selectors/entities/integrations';
|
||||
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import {loadOutgoingOAuthConnectionsAndProfiles} from 'actions/integration_actions';
|
||||
|
||||
import BackstageList from 'components/backstage/components/backstage_list';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
|
||||
import {DeveloperLinks} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import InstalledOutgoingOAuthConnection, {matchesFilter} from './installed_outgoing_oauth_connection';
|
||||
|
||||
type Props = {
|
||||
team: Team;
|
||||
};
|
||||
|
||||
const InstalledOutgoingOAuthConnections = (props: Props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const canManageOutgoingOAuthConnections = useSelector((state) => haveITeamPermission(state as GlobalState, props.team.id, Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS));
|
||||
const enableOutgoingOAuthConnections = (useSelector(getConfig).EnableOutgoingOAuthConnections === 'true');
|
||||
const connections = useSelector(getOutgoingOAuthConnections);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
if (canManageOutgoingOAuthConnections) {
|
||||
(dispatch(loadOutgoingOAuthConnectionsAndProfiles(props.team.id)) as unknown as Promise<void>).then(
|
||||
() => setLoading(false),
|
||||
);
|
||||
}
|
||||
}, [canManageOutgoingOAuthConnections, props.team, dispatch]);
|
||||
|
||||
const deleteOutgoingOAuthConnectionLocal = (connection: OutgoingOAuthConnection): void => {
|
||||
if (connection && connection.id) {
|
||||
dispatch(deleteOutgoingOAuthConnection(connection.id));
|
||||
}
|
||||
};
|
||||
|
||||
const outgoingOauthConnectionCompare = (a: OutgoingOAuthConnection, b: OutgoingOAuthConnection): number => {
|
||||
let nameA = a.name.toString();
|
||||
if (!nameA) {
|
||||
nameA = intl.formatMessage({id: 'installed_integrations.unnamed_outgoing_oauth_connection', defaultMessage: 'Unnamed Outgoing OAuth Connection'});
|
||||
}
|
||||
|
||||
let nameB = b.name.toString();
|
||||
if (!nameB) {
|
||||
nameB = intl.formatMessage({id: 'installed_integrations.unnamed_outgoing_oauth_connection', defaultMessage: 'Unnamed Outgoing OAuth Connection'});
|
||||
}
|
||||
|
||||
return nameA.localeCompare(nameB);
|
||||
};
|
||||
|
||||
const outgoingOauthConnections = (filter?: string) => {
|
||||
const values = Object.values(connections);
|
||||
const filtered = values.filter((connection) => matchesFilter(connection, filter));
|
||||
const sorted = filtered.sort(outgoingOauthConnectionCompare);
|
||||
const mapped = sorted.map((connection) => {
|
||||
return (
|
||||
<InstalledOutgoingOAuthConnection
|
||||
key={connection.id}
|
||||
outgoingOAuthConnection={connection}
|
||||
onDelete={deleteOutgoingOAuthConnectionLocal}
|
||||
team={props.team}
|
||||
creatorName=''
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return mapped;
|
||||
};
|
||||
|
||||
const integrationsEnabled = enableOutgoingOAuthConnections && canManageOutgoingOAuthConnections;
|
||||
let childProps;
|
||||
if (integrationsEnabled) {
|
||||
childProps = {
|
||||
addLink: '/' + props.team.name + '/integrations/outgoing-oauth2-connections/add',
|
||||
addText: intl.formatMessage({id: 'installed_outgoing_oauth_connections.add', defaultMessage: 'Add Outgoing OAuth Connection'}),
|
||||
addButtonId: 'addOutgoingOauthConnection',
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<BackstageList
|
||||
header={
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.header'
|
||||
defaultMessage='Outgoing OAuth Connections'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.help'
|
||||
defaultMessage='Create {outgoingOauthConnections} to securely integrate bots and third-party apps with Mattermost.'
|
||||
values={{
|
||||
outgoingOauthConnections: (
|
||||
<ExternalLink
|
||||
href={DeveloperLinks.SETUP_OAUTH2}
|
||||
location='installed_outgoing_oauth_connections'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.help.outgoingOauthConnections'
|
||||
defaultMessage='Outgoing OAuth Connections'
|
||||
/>
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
emptyText={
|
||||
<FormattedMessage
|
||||
id='installed_outgoing_oauth_connections.empty'
|
||||
defaultMessage='No Outgoing OAuth Connections found'
|
||||
/>
|
||||
}
|
||||
emptyTextSearch={
|
||||
<FormattedMarkdownMessage
|
||||
id='installed_outgoing_oauth_connections.emptySearch'
|
||||
defaultMessage='No Outgoing OAuth Connections match {searchTerm}'
|
||||
/>
|
||||
}
|
||||
searchPlaceholder={intl.formatMessage({id: 'installed_outgoing_oauth_connections.search', defaultMessage: 'Search Outgoing OAuth Connections'})}
|
||||
loading={loading}
|
||||
{...childProps}
|
||||
>
|
||||
{(filter: string) => {
|
||||
const children = outgoingOauthConnections(filter);
|
||||
return [children, children.length > 0];
|
||||
}}
|
||||
</BackstageList>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstalledOutgoingOAuthConnections;
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import {BrowserRouter as Router} from 'react-router-dom';
|
||||
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import OAuthConnectionAudienceInput from 'components/integrations/outgoing_oauth_connections/oauth_connection_audience_input';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/integrations/outgoing_oauth_connections/OAuthConnectionAudienceInput', () => {
|
||||
const connection: OutgoingOAuthConnection = {
|
||||
id: 'facxd9wpzpbpfp8pad78xj75pr',
|
||||
name: 'testConnection',
|
||||
client_secret: '',
|
||||
client_id: 'clientid',
|
||||
create_at: 1501365458934,
|
||||
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
|
||||
update_at: 1501365458934,
|
||||
grant_type: 'client_credentials',
|
||||
oauth_token_url: 'https://token.com',
|
||||
audiences: ['https://aud.com/api'],
|
||||
};
|
||||
|
||||
const baseProps: React.ComponentProps<typeof OAuthConnectionAudienceInput> = {
|
||||
value: '',
|
||||
onChange: jest.fn(),
|
||||
placeholder: '',
|
||||
};
|
||||
|
||||
const team = TestHelper.getTeamMock({name: 'test'});
|
||||
|
||||
const stateFromOAuthConnections = (connections: Record<string, OutgoingOAuthConnection>) => {
|
||||
return {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableOutgoingOAuthConnections: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
teams: {
|
||||
[team.id]: team,
|
||||
},
|
||||
currentTeamId: team.id,
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {id: 'system_role', permissions: [Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS]},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
outgoingOAuthConnections: connections,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
test('should match snapshot with no existing connections', async () => {
|
||||
const props = {...baseProps};
|
||||
const state = stateFromOAuthConnections({});
|
||||
const store = mockStore(state);
|
||||
await act(async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<OAuthConnectionAudienceInput {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('should match snapshot with existing connections', async () => {
|
||||
const props = {...baseProps};
|
||||
const state = stateFromOAuthConnections({[connection.id]: connection});
|
||||
const store = mockStore(state);
|
||||
await act(async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<OAuthConnectionAudienceInput {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('should match snapshot when typed in value matches a configured audience', async () => {
|
||||
const props = {...baseProps, value: 'https://aud.com/api'};
|
||||
const state = stateFromOAuthConnections({[connection.id]: connection});
|
||||
const store = mockStore(state);
|
||||
await act(async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<OAuthConnectionAudienceInput {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('should match snapshot when typed in value does not have an exact match', async () => {
|
||||
const props = {...baseProps, value: 'https://aud.com/api/no_match'};
|
||||
const state = stateFromOAuthConnections({[connection.id]: connection});
|
||||
const store = mockStore(state);
|
||||
|
||||
await act(async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<OAuthConnectionAudienceInput {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('should match snapshot when an audience url with a wildcard is configured, and typed in value starts with configured audience url', async () => {
|
||||
const props = {...baseProps, value: 'https://aud.com/api/it_matches'};
|
||||
const state = stateFromOAuthConnections({[connection.id]: {...connection, audiences: ['https://aud.com/api/*']}});
|
||||
const store = mockStore(state);
|
||||
|
||||
await act(async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Router>
|
||||
<Provider store={store}>
|
||||
<OAuthConnectionAudienceInput {...props}/>
|
||||
</Provider>
|
||||
</Router>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {OauthIcon, InformationOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
|
||||
import {
|
||||
getOutgoingOAuthConnectionsForAudience as fetchOutgoingOAuthConnectionsForAudience,
|
||||
getOutgoingOAuthConnections as fetchOutgoingOAuthConnections,
|
||||
} from 'mattermost-redux/actions/integrations';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getOutgoingOAuthConnections} from 'mattermost-redux/selectors/entities/integrations';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
const OAuthConnectionAudienceInput = (props: Props) => {
|
||||
const mounted = useRef(false);
|
||||
const [matchedConnection, setMatchingOAuthConnection] = useState<OutgoingOAuthConnection | null>(null);
|
||||
const [loadingAudienceMatch, setLoadingAudienceMatch] = useState(false);
|
||||
|
||||
const oauthConnections = useSelector(getOutgoingOAuthConnections);
|
||||
const oauthConnectionsEnabled = useSelector(getConfig).EnableOutgoingOAuthConnections === 'true';
|
||||
const teamId = useSelector(getCurrentTeamId);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const matchConnectionsOnInput = useCallback(async (inputValue: string) => {
|
||||
const res = await dispatch(fetchOutgoingOAuthConnectionsForAudience(teamId, inputValue));
|
||||
setLoadingAudienceMatch(false);
|
||||
|
||||
if (res.data && res.data.length) {
|
||||
setMatchingOAuthConnection(res.data[0]);
|
||||
} else {
|
||||
setMatchingOAuthConnection(null);
|
||||
}
|
||||
}, [dispatch, teamId]);
|
||||
|
||||
const debouncedMatchConnections = useMemo(() => {
|
||||
return debounce((inputValue: string) => matchConnectionsOnInput(inputValue), 1000);
|
||||
}, [matchConnectionsOnInput]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted.current) {
|
||||
return;
|
||||
}
|
||||
mounted.current = true;
|
||||
|
||||
if (oauthConnectionsEnabled) {
|
||||
dispatch(fetchOutgoingOAuthConnections(teamId));
|
||||
if (props.value) {
|
||||
setLoadingAudienceMatch(true);
|
||||
matchConnectionsOnInput(props.value);
|
||||
}
|
||||
}
|
||||
}, [oauthConnectionsEnabled, props.value, teamId, matchConnectionsOnInput, dispatch, mounted]);
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.onChange(e);
|
||||
|
||||
if (oauthConnectionsEnabled) {
|
||||
setLoadingAudienceMatch(true);
|
||||
debouncedMatchConnections(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const connections = Object.values(oauthConnections);
|
||||
|
||||
const input = (
|
||||
<input
|
||||
autoComplete='off'
|
||||
id='url'
|
||||
maxLength={1024}
|
||||
className='form-control'
|
||||
value={props.value}
|
||||
onChange={onChange}
|
||||
placeholder={props.placeholder}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!connections.length) {
|
||||
return input;
|
||||
}
|
||||
|
||||
let oauthMessage: React.ReactNode;
|
||||
|
||||
if (loadingAudienceMatch) {
|
||||
oauthMessage = (
|
||||
<span>
|
||||
<LoadingSpinner/>
|
||||
</span>
|
||||
);
|
||||
} else if (matchedConnection) {
|
||||
oauthMessage = (
|
||||
<>
|
||||
<span>
|
||||
<OauthIcon
|
||||
size={20}
|
||||
/>
|
||||
</span>
|
||||
<span className='outgoing-oauth-audience-match-message'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.connected'
|
||||
defaultMessage='Connected to "{connectionName}"'
|
||||
values={{
|
||||
connectionName: matchedConnection.name,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
oauthMessage = (
|
||||
<>
|
||||
<span>
|
||||
<InformationOutlineIcon
|
||||
size={20}
|
||||
/>
|
||||
</span>
|
||||
<span className='outgoing-oauth-audience-match-message'>
|
||||
<FormattedMessage
|
||||
id='add_outgoing_oauth_connection.not_connected'
|
||||
defaultMessage='Not linked to an OAuth connection'
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{input}
|
||||
<div className='outgoing-oauth-audience-match-message-container'>
|
||||
{oauthMessage}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthConnectionAudienceInput;
|
||||
@@ -88,6 +88,7 @@
|
||||
"add_command.method.get": "GET",
|
||||
"add_command.method.help": "Specify the type of request, either POST or GET, sent to the endpoint that Mattermost hits to reach your application.",
|
||||
"add_command.method.post": "POST",
|
||||
"add_command.outgoing_oauth_connections.help_text": "You can connect commands to <link>outgoing OAuth connections</link>.",
|
||||
"add_command.save": "Save",
|
||||
"add_command.saving": "Saving...",
|
||||
"add_command.token": "**Token**: {token}",
|
||||
@@ -162,6 +163,44 @@
|
||||
"add_oauth_app.nameRequired": "Name for the OAuth 2.0 application is required.",
|
||||
"add_oauth_app.trusted.help": "If true, the OAuth 2.0 application is considered trusted by the Mattermost server and does not require the user to accept authorization. If false, a window opens to ask the user to accept or deny the authorization.",
|
||||
"add_oauth_app.url": "**URL(s)**: {url}",
|
||||
"add_outgoing_oauth_connection.add": "Add",
|
||||
"add_outgoing_oauth_connection.audience_urls": "**Audience URL(s)**: `{url}`",
|
||||
"add_outgoing_oauth_connection.audienceUrls.help": "The URLs which will receive requests with the OAuth token, e.g. your custom slash command handler endpoint. Must be a valid URL and start with http:// or https://.",
|
||||
"add_outgoing_oauth_connection.audienceUrls.label": "Audience URLs (One Per Line)",
|
||||
"add_outgoing_oauth_connection.audienceUrls.required": "One or more audience URLs are required.",
|
||||
"add_outgoing_oauth_connection.cancel": "Cancel",
|
||||
"add_outgoing_oauth_connection.client_id.help": "Specify the Client ID for your OAuth connection.",
|
||||
"add_outgoing_oauth_connection.client_id.label": "Client ID",
|
||||
"add_outgoing_oauth_connection.client_id.required": "Client Id for the OAuth connection is required.",
|
||||
"add_outgoing_oauth_connection.client_secret.help": "Specify the Client Secret for your OAuth connection.",
|
||||
"add_outgoing_oauth_connection.client_secret.label": "Client Secret",
|
||||
"add_outgoing_oauth_connection.client_secret.required": "Client Secret for the OAuth connection is required.",
|
||||
"add_outgoing_oauth_connection.clientId": "**Client ID**: {id}",
|
||||
"add_outgoing_oauth_connection.clientSecret": "**Client Secret**: \\*\\*\\*\\*\\*\\*\\*\\*",
|
||||
"add_outgoing_oauth_connection.confirm_save": "Save Outgoing OAuth Connection",
|
||||
"add_outgoing_oauth_connection.connected": "Connected to \"{connectionName}\"",
|
||||
"add_outgoing_oauth_connection.documentation_link": "Get help with <link>configuring outgoing OAuth connections</link>.",
|
||||
"add_outgoing_oauth_connection.doneHelp": "Your Outgoing OAuth 2.0 Connection is set up. When a request is sent to one of the following Audience URLs, the Client ID and Client Secret will now be used to retrieve a token from the Token URL, before sending the integration request (details at <link>Outgoing OAuth 2.0 Connections</link>).",
|
||||
"add_outgoing_oauth_connection.grant_type.required": "Grant Type for the OAuth connection is required.",
|
||||
"add_outgoing_oauth_connection.header": "Outgoing OAuth Connections",
|
||||
"add_outgoing_oauth_connection.name.help": "Specify the name for your OAuth connection.",
|
||||
"add_outgoing_oauth_connection.name.label": "Name",
|
||||
"add_outgoing_oauth_connection.name.required": "Name for the OAuth connection is required.",
|
||||
"add_outgoing_oauth_connection.not_connected": "Not linked to an OAuth connection",
|
||||
"add_outgoing_oauth_connection.oauth_token_url.help": "Specify the OAuth Token URL for your OAuth connection.",
|
||||
"add_outgoing_oauth_connection.oauth_token_url.label": "OAuth Token URL",
|
||||
"add_outgoing_oauth_connection.oauth_token_url.required": "OAuth Token URL for the OAuth connection is required.",
|
||||
"add_outgoing_oauth_connection.password": "**Password**: {password}",
|
||||
"add_outgoing_oauth_connection.save": "Save",
|
||||
"add_outgoing_oauth_connection.save_anyway": "Save anyway",
|
||||
"add_outgoing_oauth_connection.save_without_validation_warning": "This connection has not been validated, Do you want to save anyway?",
|
||||
"add_outgoing_oauth_connection.saving": "Saving...",
|
||||
"add_outgoing_oauth_connection.token_url": "**Token URL**: `{url}`",
|
||||
"add_outgoing_oauth_connection.username": "**Username**: {username}",
|
||||
"add_outgoing_oauth_connection.validate": "Validate Connection",
|
||||
"add_outgoing_oauth_connection.validated_connection": "Validated connection",
|
||||
"add_outgoing_oauth_connection.validating": "Validating...",
|
||||
"add_outgoing_oauth_connection.validation_error": "Connection not validated. Please check the server logs for details or <link>try again</link>.",
|
||||
"add_outgoing_webhook.callbackUrls": "Callback URLs (One Per Line)",
|
||||
"add_outgoing_webhook.callbackUrls.help": "Specify the URL that messages will be sent to. If the URL is private, add it as a {link}.",
|
||||
"add_outgoing_webhook.callbackUrls.helpLinkText": "trusted internal connection",
|
||||
@@ -1677,6 +1716,8 @@
|
||||
"admin.permissions.permission.manage_jobs.name": "Manage jobs",
|
||||
"admin.permissions.permission.manage_oauth.description": "Create, edit and delete OAuth 2.0 application tokens.",
|
||||
"admin.permissions.permission.manage_oauth.name": "Manage OAuth Applications",
|
||||
"admin.permissions.permission.manage_outgoing_oauth_connections.description": "Create, edit, and delete outgoing OAuth credentials.",
|
||||
"admin.permissions.permission.manage_outgoing_oauth_connections.name": "Manage Outgoing OAuth Credentials",
|
||||
"admin.permissions.permission.manage_outgoing_webhooks.description": "Create, edit, and delete outgoing webhooks.",
|
||||
"admin.permissions.permission.manage_outgoing_webhooks.name": "Manage Outgoing Webhooks",
|
||||
"admin.permissions.permission.manage_private_channel_properties.description": "Update private channel names, headers and purposes.",
|
||||
@@ -2249,6 +2290,8 @@
|
||||
"admin.service.mobileSessionHours": "Session Length Mobile (hours):",
|
||||
"admin.service.mobileSessionHoursDesc": "The number of hours from the last time a user entered their credentials to the expiry of the user's session. After changing this setting, the new session length will take effect after the next time the user enters their credentials.",
|
||||
"admin.service.mobileSessionHoursDesc.extendLength": "Set the number of hours from the last activity in Mattermost to the expiry of the user’s session on mobile. After changing this setting, the new session length will take effect after the next time the user enters their credentials.",
|
||||
"admin.service.outgoingOAuthConnectionsDesc": "When true, outgoing webhooks and slash commands will use set up oauth connections to authenticate with third party services. See <link>documentation</link> to learn more.",
|
||||
"admin.service.outgoingOAuthConnectionsTitle": "Enable Outgoing OAuth Connections: ",
|
||||
"admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed. See <link>documentation</link> to learn more.",
|
||||
"admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ",
|
||||
"admin.service.overrideDescription": "When true, webhooks, slash commands and other integrations will be allowed to change the username they are posting as. Note: Combined with allowing integrations to override profile picture icons, users may be able to perform phishing attacks by attempting to impersonate other users.",
|
||||
@@ -2952,6 +2995,7 @@
|
||||
"backstage_sidebar.integrations.incoming_webhooks": "Incoming Webhooks",
|
||||
"backstage_sidebar.integrations.oauthApps": "OAuth 2.0 Applications",
|
||||
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks",
|
||||
"backstage_sidebar.integrations.outgoingOauthConnections": "Outgoing OAuth 2.0 Connections",
|
||||
"billing_subscriptions.cloud_annual_renewal_alert_banner_title": "Your annual subscription expires in {days} days. Please renew now to avoid any disruption",
|
||||
"billing_subscriptions.cloud_annual_renewal_alert_banner_title_expired": "Your subscription has expired. Your workspace will be deleted in {days} days. Please renew now to avoid any disruption",
|
||||
"billing.subscription.info.mostRecentPaymentFailed": "Your most recent payment failed",
|
||||
@@ -3481,6 +3525,8 @@
|
||||
"edit_channel_purpose_modal.title2": "Edit Purpose for ",
|
||||
"edit_command.update": "Update",
|
||||
"edit_command.updating": "Updating...",
|
||||
"edit_outgoing_oauth_connection.update": "Update",
|
||||
"edit_outgoing_oauth_connection.updating": "Updating...",
|
||||
"edit_post.action_buttons.cancel": "Cancel",
|
||||
"edit_post.action_buttons.save": "Save",
|
||||
"edit_post.editPost": "Edit the post...",
|
||||
@@ -3831,6 +3877,7 @@
|
||||
"installed_incoming_webhooks.help.buildYourOwn": "Build Your Own",
|
||||
"installed_incoming_webhooks.search": "Search Incoming Webhooks",
|
||||
"installed_incoming_webhooks.unknown_channel": "A Private Webhook",
|
||||
"installed_integrations.audience_urls": "Audience URLs: {urls}",
|
||||
"installed_integrations.callback_urls": "Callback URLs: {urls}",
|
||||
"installed_integrations.client_id": "Client ID: **{clientId}**",
|
||||
"installed_integrations.client_secret": "Client Secret: **{clientSecret}**",
|
||||
@@ -3844,9 +3891,11 @@
|
||||
"installed_integrations.regenToken": "Regenerate Token",
|
||||
"installed_integrations.showSecret": "Show Secret",
|
||||
"installed_integrations.token": "Token: {token}",
|
||||
"installed_integrations.token_url": "Token URL: {url}",
|
||||
"installed_integrations.triggerWhen": "Trigger When: {triggerWhen}",
|
||||
"installed_integrations.triggerWords": "Trigger Words: {triggerWords}",
|
||||
"installed_integrations.unnamed_oauth_app": "Unnamed OAuth 2.0 Application",
|
||||
"installed_integrations.unnamed_outgoing_oauth_connection": "Unnamed Outgoing OAuth Connection",
|
||||
"installed_integrations.url": "URL: {url}",
|
||||
"installed_oauth_apps.add": "Add OAuth 2.0 Application",
|
||||
"installed_oauth_apps.callbackUrls": "Callback URLs (One Per Line)",
|
||||
@@ -3870,6 +3919,18 @@
|
||||
"installed_oauth_apps.trusted.no": "No",
|
||||
"installed_oauth_apps.trusted.yes": "Yes",
|
||||
"installed_oauth2_apps.header": "OAuth 2.0 Applications",
|
||||
"installed_outgoing_oauth_connections.add": "Add Outgoing OAuth Connection",
|
||||
"installed_outgoing_oauth_connections.client_secret": "Client Secret: ********",
|
||||
"installed_outgoing_oauth_connections.delete.confirm": "Are you sure you want to delete {connectionName}?",
|
||||
"installed_outgoing_oauth_connections.delete.wanring": "Deleting this connection will break any integrations using it",
|
||||
"installed_outgoing_oauth_connections.empty": "No Outgoing OAuth Connections found",
|
||||
"installed_outgoing_oauth_connections.emptySearch": "No Outgoing OAuth Connections match {searchTerm}",
|
||||
"installed_outgoing_oauth_connections.header": "Outgoing OAuth Connections",
|
||||
"installed_outgoing_oauth_connections.help": "Create {outgoingOauthConnections} to securely integrate bots and third-party apps with Mattermost.",
|
||||
"installed_outgoing_oauth_connections.help.outgoingOauthConnections": "Outgoing OAuth Connections",
|
||||
"installed_outgoing_oauth_connections.password": "Password: ********",
|
||||
"installed_outgoing_oauth_connections.search": "Search Outgoing OAuth Connections",
|
||||
"installed_outgoing_oauth_connections.username": "Username: **{username}**",
|
||||
"installed_outgoing_webhooks.add": "Add Outgoing Webhook",
|
||||
"installed_outgoing_webhooks.delete.confirm": "This action permanently deletes the outgoing webhook and breaks any integrations using it. Are you sure you want to delete it?",
|
||||
"installed_outgoing_webhooks.empty": "No outgoing webhooks found",
|
||||
@@ -3883,7 +3944,7 @@
|
||||
"integrations.add": "Add",
|
||||
"integrations.command.description": "Slash commands send events to external integrations",
|
||||
"integrations.command.title": "Slash Commands",
|
||||
"integrations.delete.confirm.button": "Delete",
|
||||
"integrations.delete.confirm.button": "Yes, delete it",
|
||||
"integrations.delete.confirm.title": "Delete Integration",
|
||||
"integrations.done": "Done",
|
||||
"integrations.edit": "Edit",
|
||||
@@ -3894,6 +3955,8 @@
|
||||
"integrations.incomingWebhook.title": "Incoming Webhooks",
|
||||
"integrations.oauthApps.description": "OAuth 2.0 allows external applications to make authorized requests to the Mattermost API",
|
||||
"integrations.oauthApps.title": "OAuth 2.0 Applications",
|
||||
"integrations.outgoingOAuthConnections.description": "Outgoing OAuth Connections allow custom integrations to communicate to external systems",
|
||||
"integrations.outgoingOAuthConnections.title": "Outgoing OAuth Connections",
|
||||
"integrations.outgoingWebhook.description": "Outgoing webhooks allow external integrations to receive and respond to messages",
|
||||
"integrations.outgoingWebhook.title": "Outgoing Webhooks",
|
||||
"integrations.successful": "Setup Successful",
|
||||
@@ -5307,6 +5370,8 @@
|
||||
"update_incoming_webhook.updating": "Updating...",
|
||||
"update_oauth_app.confirm": "Edit OAuth 2.0 application",
|
||||
"update_oauth_app.question": "Your changes may break the existing OAuth 2.0 application. Are you sure you would like to update it?",
|
||||
"update_outgoing_oauth_connection.confirm": "Edit Outgoing OAuth Connection",
|
||||
"update_outgoing_oauth_connection.question": "Your changes may break any existing integrations using this connection. Are you sure you would like to update it?",
|
||||
"update_outgoing_webhook.confirm": "Edit Outgoing Webhook",
|
||||
"update_outgoing_webhook.question": "Your changes may break the existing outgoing webhook. Are you sure you would like to update it?",
|
||||
"update_outgoing_webhook.update": "Update",
|
||||
|
||||
Двоичные данные
webapp/channels/src/images/outgoing_oauth_connection.png
Обычный файл
Двоичные данные
webapp/channels/src/images/outgoing_oauth_connection.png
Обычный файл
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 18 KiB |
@@ -21,6 +21,9 @@ export default keyMirror({
|
||||
DELETED_OAUTH_APP: null,
|
||||
RECEIVED_APPS_OAUTH_APP_IDS: null,
|
||||
RECEIVED_APPS_BOT_IDS: null,
|
||||
RECEIVED_OUTGOING_OAUTH_CONNECTIONS: null,
|
||||
RECEIVED_OUTGOING_OAUTH_CONNECTION: null,
|
||||
DELETED_OUTGOING_OAUTH_CONNECTION: null,
|
||||
|
||||
RECEIVED_DIALOG_TRIGGER_ID: null,
|
||||
RECEIVED_DIALOG: null,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import type {Command, CommandArgs, DialogSubmission, IncomingWebhook, OAuthApp, OutgoingWebhook, SubmitDialogResponse} from '@mattermost/types/integrations';
|
||||
import type {Command, CommandArgs, DialogSubmission, IncomingWebhook, OAuthApp, OutgoingOAuthConnection, OutgoingWebhook, SubmitDialogResponse} from '@mattermost/types/integrations';
|
||||
|
||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
@@ -297,6 +297,74 @@ export function getOAuthApps(page = 0, perPage: number = General.PAGE_SIZE_DEFAU
|
||||
});
|
||||
}
|
||||
|
||||
export function getOutgoingOAuthConnections(teamId: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getOutgoingOAuthConnections,
|
||||
onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTIONS],
|
||||
params: [
|
||||
teamId,
|
||||
page,
|
||||
perPage,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function getOutgoingOAuthConnectionsForAudience(teamId: string, audience: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getOutgoingOAuthConnectionsForAudience,
|
||||
onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTIONS],
|
||||
params: [
|
||||
teamId,
|
||||
audience,
|
||||
page,
|
||||
perPage,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function addOutgoingOAuthConnection(teamId: string, connection: OutgoingOAuthConnection) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.createOutgoingOAuthConnection,
|
||||
onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTION],
|
||||
params: [
|
||||
teamId,
|
||||
connection,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function editOutgoingOAuthConnection(teamId: string, connection: OutgoingOAuthConnection) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.editOutgoingOAuthConnection,
|
||||
onSuccess: IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTION,
|
||||
params: [
|
||||
teamId,
|
||||
connection,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function getOutgoingOAuthConnection(teamId: string, connectionId: string) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getOutgoingOAuthConnection,
|
||||
onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTION],
|
||||
params: [
|
||||
teamId,
|
||||
connectionId,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function validateOutgoingOAuthConnection(teamId: string, connection: OutgoingOAuthConnection) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.validateOutgoingOAuthConnection,
|
||||
params: [
|
||||
teamId,
|
||||
connection,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppsOAuthAppIDs() {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getAppsOAuthAppIDs,
|
||||
@@ -380,6 +448,26 @@ export function regenOAuthAppSecret(appId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteOutgoingOAuthConnection(id: string): ActionFuncAsync<boolean> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
await Client4.deleteOutgoingOAuthConnection(id);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: IntegrationTypes.DELETED_OUTGOING_OAUTH_CONNECTION,
|
||||
data: {id},
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function submitInteractiveDialog(submission: DialogSubmission): ActionFuncAsync<SubmitDialogResponse> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
@@ -48,6 +48,7 @@ const values = {
|
||||
MANAGE_OUTGOING_WEBHOOKS: 'manage_outgoing_webhooks',
|
||||
MANAGE_OTHERS_OUTGOING_WEBHOOKS: 'manage_others_outgoing_webhooks',
|
||||
MANAGE_OAUTH: 'manage_oauth',
|
||||
MANAGE_OUTGOING_OAUTH_CONNECTIONS: 'manage_outgoing_oauth_connections',
|
||||
MANAGE_SYSTEM_WIDE_OAUTH: 'manage_system_wide_oauth',
|
||||
CREATE_POST: 'create_post',
|
||||
CREATE_POST_PUBLIC: 'create_post_public',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type {AnyAction} from 'redux';
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import type {Command, IncomingWebhook, OutgoingWebhook, OAuthApp} from '@mattermost/types/integrations';
|
||||
import type {Command, IncomingWebhook, OutgoingWebhook, OAuthApp, OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
||||
|
||||
import {IntegrationTypes, UserTypes, ChannelTypes} from 'mattermost-redux/action_types';
|
||||
@@ -224,6 +224,33 @@ function appsOAuthAppIDs(state: string[] = [], action: AnyAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function outgoingOAuthConnections(state: IDMappedObjects<OutgoingOAuthConnection> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTIONS: {
|
||||
const nextState = {...state};
|
||||
for (const connection of action.data) {
|
||||
nextState[connection.id] = connection;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
case IntegrationTypes.RECEIVED_OUTGOING_OAUTH_CONNECTION:
|
||||
return {
|
||||
...state,
|
||||
[action.data.id]: action.data,
|
||||
};
|
||||
case IntegrationTypes.DELETED_OUTGOING_OAUTH_CONNECTION: {
|
||||
const nextState = {...state};
|
||||
Reflect.deleteProperty(nextState, action.data.id);
|
||||
return nextState;
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function appsBotIDs(state: string[] = [], action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case IntegrationTypes.RECEIVED_APPS_BOT_IDS: {
|
||||
@@ -294,6 +321,9 @@ export default combineReducers({
|
||||
// object to represent the list of ids for bots associated to apps
|
||||
appsBotIDs,
|
||||
|
||||
// object to represent registered outgoing oauth connections with connection id as the key
|
||||
outgoingOAuthConnections,
|
||||
|
||||
// object to represent built-in slash commands
|
||||
systemCommands,
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ export function getOAuthApps(state: GlobalState) {
|
||||
return state.entities.integrations.oauthApps;
|
||||
}
|
||||
|
||||
export function getOutgoingOAuthConnections(state: GlobalState) {
|
||||
return state.entities.integrations.outgoingOAuthConnections;
|
||||
}
|
||||
|
||||
export const getAppsOAuthAppIDs: (state: GlobalState) => string[] = createSelector(
|
||||
'getAppsOAuthAppIDs',
|
||||
appsEnabled,
|
||||
|
||||
@@ -126,6 +126,7 @@ const state: GlobalState = {
|
||||
commands: {},
|
||||
appsBotIDs: [],
|
||||
appsOAuthAppIDs: [],
|
||||
outgoingOAuthConnections: {},
|
||||
},
|
||||
files: {
|
||||
files: {},
|
||||
|
||||
@@ -192,7 +192,8 @@
|
||||
|
||||
.form-control[disabled],
|
||||
.form-control[readonly],
|
||||
fieldset[disabled] .form-control {
|
||||
fieldset[disabled] .form-control,
|
||||
.backstage-form .form-control[disabled] {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
.backstage-sidebar {
|
||||
position: absolute;
|
||||
width: 260px;
|
||||
width: 270px;
|
||||
padding: 46px 20px;
|
||||
vertical-align: top;
|
||||
|
||||
@@ -110,9 +110,9 @@
|
||||
.section-title,
|
||||
.subsection-title {
|
||||
display: block;
|
||||
padding-left: 2em;
|
||||
padding: 6px 12px 6px 34px;
|
||||
font-size: 0.95em;
|
||||
line-height: 29px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -226,6 +226,10 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.item-details__trigger {
|
||||
margin-left: 6px;
|
||||
}
|
||||
@@ -544,3 +548,66 @@
|
||||
.bot-token-has-error {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.outgoing-oauth-connections-edit-secret {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-pencil-outline {
|
||||
opacity: 56%;
|
||||
}
|
||||
}
|
||||
|
||||
.outgoing-oauth-connection-validate-button-container {
|
||||
height: 40px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.outgoing-oauth-connection-validation-message {
|
||||
&.validation-success {
|
||||
color: #06d6a0;
|
||||
}
|
||||
|
||||
&.validation-error {
|
||||
color: #d24b4e;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 5px;
|
||||
// margin-top: 2px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.outgoing-oauth-connections-docs-link {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.outgoing-oauth-audience-match-message-container {
|
||||
height: 27px;
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
}
|
||||
|
||||
.outgoing-oauth-audience-match-message {
|
||||
left: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
#confirmModal.integrations-backstage-modal {
|
||||
#confirmModalLabel,
|
||||
#confirmModalBody {
|
||||
color: rgb(51, 51, 51);
|
||||
}
|
||||
|
||||
#confirmModalBody p {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,6 +1908,7 @@ export const Constants = {
|
||||
INCOMING_WEBHOOK: 'incoming_webhooks',
|
||||
OUTGOING_WEBHOOK: 'outgoing_webhooks',
|
||||
OAUTH_APP: 'oauth2-apps',
|
||||
OUTGOING_OAUTH_CONNECTIONS: 'outgoing-oauth2-connections',
|
||||
BOT: 'bots',
|
||||
EXECUTE_CURRENT_COMMAND_ITEM_ID: '_execute_current_command',
|
||||
OPEN_COMMAND_IN_MODAL_ITEM_ID: '_open_command_in_modal',
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
DialogSubmission,
|
||||
IncomingWebhook,
|
||||
OAuthApp,
|
||||
OutgoingOAuthConnection,
|
||||
OutgoingWebhook,
|
||||
SubmitDialogResponse,
|
||||
} from '@mattermost/types/integrations';
|
||||
@@ -178,7 +179,7 @@ export default class Client4 {
|
||||
csrf = '';
|
||||
url = '';
|
||||
urlVersion = '/api/v4';
|
||||
userAgent: string|null = null;
|
||||
userAgent: string | null = null;
|
||||
enableLogging = false;
|
||||
defaultHeaders: {[x: string]: string} = {};
|
||||
userId = '';
|
||||
@@ -387,6 +388,15 @@ export default class Client4 {
|
||||
return `${this.getOAuthAppsRoute()}/${appId}`;
|
||||
}
|
||||
|
||||
getOutgoingOAuthConnectionsRoute() {
|
||||
return `${this.getBaseRoute()}/oauth/outgoing_connections`;
|
||||
}
|
||||
|
||||
getOutgoingOAuthConnectionRoute(connectionId: string) {
|
||||
return `${this.getBaseRoute()}/oauth/outgoing_connections/${connectionId}`;
|
||||
}
|
||||
|
||||
|
||||
getEmojisRoute() {
|
||||
return `${this.getBaseRoute()}/emoji`;
|
||||
}
|
||||
@@ -2783,6 +2793,50 @@ export default class Client4 {
|
||||
);
|
||||
};
|
||||
|
||||
getOutgoingOAuthConnections = (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch<OutgoingOAuthConnection[]>(
|
||||
`${this.getOutgoingOAuthConnectionsRoute()}${buildQueryString({team_id: teamId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getOutgoingOAuthConnectionsForAudience = (teamId: string, audience: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch<OutgoingOAuthConnection[]>(
|
||||
`${this.getOutgoingOAuthConnectionsRoute()}${buildQueryString({team_id: teamId, page, per_page: perPage, audience})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getOutgoingOAuthConnection = (teamId: string, connectionId: string) => {
|
||||
return this.doFetch<OutgoingOAuthConnection>(
|
||||
`${this.getOutgoingOAuthConnectionRoute(connectionId)}${buildQueryString({team_id: teamId})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
createOutgoingOAuthConnection = (teamId: string, connection: OutgoingOAuthConnection) => {
|
||||
this.trackEvent('api', 'api_outgoing_oauth_connection_register');
|
||||
|
||||
return this.doFetch<OutgoingOAuthConnection>(
|
||||
`${this.getOutgoingOAuthConnectionsRoute()}${buildQueryString({team_id: teamId})}`,
|
||||
{method: 'post', body: JSON.stringify(connection)},
|
||||
);
|
||||
};
|
||||
|
||||
editOutgoingOAuthConnection = (teamId: string, connection: OutgoingOAuthConnection) => {
|
||||
return this.doFetch<OutgoingOAuthConnection>(
|
||||
`${this.getOutgoingOAuthConnectionsRoute()}/${connection.id}${buildQueryString({team_id: teamId})}`,
|
||||
{method: 'put', body: JSON.stringify(connection)},
|
||||
);
|
||||
};
|
||||
|
||||
validateOutgoingOAuthConnection = (teamId: string, connection: OutgoingOAuthConnection) => {
|
||||
return this.doFetch<OutgoingOAuthConnection>(
|
||||
`${this.getOutgoingOAuthConnectionsRoute()}/validate${buildQueryString({team_id: teamId})}`,
|
||||
{method: 'post', body: JSON.stringify(connection)},
|
||||
);
|
||||
};
|
||||
|
||||
getOAuthAppInfo = (appId: string) => {
|
||||
return this.doFetch<OAuthApp>(
|
||||
`${this.getOAuthAppRoute(appId)}/info`,
|
||||
@@ -2806,6 +2860,15 @@ export default class Client4 {
|
||||
);
|
||||
};
|
||||
|
||||
deleteOutgoingOAuthConnection = (connectionId: string) => {
|
||||
this.trackEvent('api', 'api_apps_delete');
|
||||
|
||||
return this.doFetch<StatusOK>(
|
||||
`${this.getOutgoingOAuthConnectionRoute(connectionId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
submitInteractiveDialog = (data: DialogSubmission) => {
|
||||
this.trackEvent('api', 'api_interactive_messages_dialog_submitted');
|
||||
return this.doFetch<SubmitDialogResponse>(
|
||||
|
||||
@@ -82,6 +82,7 @@ export type ClientConfig = {
|
||||
EnableMobileFileUpload: string;
|
||||
EnableMultifactorAuthentication: string;
|
||||
EnableOAuthServiceProvider: string;
|
||||
EnableOutgoingOAuthConnections: string;
|
||||
EnableOpenServer: string;
|
||||
EnableOutgoingWebhooks: string;
|
||||
EnablePostIconOverride: string;
|
||||
|
||||
@@ -104,10 +104,26 @@ export type OAuthApp = {
|
||||
'is_trusted': boolean;
|
||||
};
|
||||
|
||||
export type OutgoingOAuthConnection = {
|
||||
'id': string;
|
||||
'name': string;
|
||||
'creator_id': string;
|
||||
'create_at': number;
|
||||
'update_at': number;
|
||||
'client_id': string;
|
||||
'client_secret'?: string;
|
||||
'credentials_username'?: string;
|
||||
'credentials_password'?: string;
|
||||
'oauth_token_url': string;
|
||||
'grant_type': 'client_credentials' | 'password';
|
||||
'audiences': string[];
|
||||
};
|
||||
|
||||
export type IntegrationsState = {
|
||||
incomingHooks: IDMappedObjects<IncomingWebhook>;
|
||||
outgoingHooks: IDMappedObjects<OutgoingWebhook>;
|
||||
oauthApps: IDMappedObjects<OAuthApp>;
|
||||
outgoingOAuthConnections: IDMappedObjects<OutgoingOAuthConnection>;
|
||||
appsOAuthAppIDs: string[];
|
||||
appsBotIDs: string[];
|
||||
systemCommands: IDMappedObjects<Command>;
|
||||
|
||||
Ссылка в новой задаче
Block a user