diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/connected_workspaces_management_spec.ts b/e2e-tests/cypress/tests/integration/channels/system_console/connected_workspaces_management_spec.ts
new file mode 100644
index 0000000000..d7ce5d28c1
--- /dev/null
+++ b/e2e-tests/cypress/tests/integration/channels/system_console/connected_workspaces_management_spec.ts
@@ -0,0 +1,393 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// ***************************************************************
+// - [#] indicates a test step (e.g. # Go to a page)
+// - [*] indicates an assertion (e.g. * Check the title)
+// - Use element ID when selecting an element. Create one if none.
+// ***************************************************************
+
+// Group: @channels @system_console
+
+import timeouts from '../../../fixtures/timeouts';
+import {getRandomId, stubClipboard} from '../../../utils';
+
+describe('Connected Workspaces', () => {
+ let testTeam: Cypress.Team;
+ let testTeam2: Cypress.Team;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ let testUser: Cypress.UserProfile;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ let admin: Cypress.UserProfile;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ let testChannel: Cypress.Channel;
+
+ before(() => {
+ cy.apiGetMe().then(({user: adminUser}) => {
+ admin = adminUser;
+
+ cy.apiCreateTeam('testing-team-2', 'Testing Team 2').then(({team}) => {
+ testTeam2 = team;
+ });
+
+ cy.apiInitSetup().then(({team, user, channel}) => {
+ testTeam = team;
+ testUser = user;
+ testChannel = channel;
+ });
+ });
+ });
+
+ it('configured', () => {
+ cy.apiRequireLicenseForFeature('SharedChannels');
+
+ // @ts-expect-error types update, need ConnectedWorkspacesSettings
+ cy.apiGetConfig().then(({config: {ConnectedWorkspacesSettings}}) => {
+ expect(ConnectedWorkspacesSettings.EnableSharedChannels).equal(true);
+ expect(ConnectedWorkspacesSettings.EnableRemoteClusterService).equal(true);
+ });
+ });
+
+ it('empty state', () => {
+ cy.visit('/admin_console');
+
+ cy.get("a[id='environment/secure_connections']").click();
+
+ cy.findByTestId('secureConnectionsSection').within(() => {
+ cy.findByRole('heading', {name: 'Connected Workspaces'});
+ cy.findByRole('heading', {name: 'Share channels'});
+ cy.contains('Connecting with an external workspace allows you to share channels with them');
+ });
+ });
+
+ describe('accept invitation lifecycle', () => {
+ const orgDisplayName = 'Testing Org Name ' + getRandomId();
+
+ before(() => {
+ cy.visit('/admin_console/environment/secure_connections');
+ });
+
+ it('accept - bad codes', () => {
+ // # Open create menu
+ cy.findAllByRole('button', {name: 'Add a connection'}).eq(1).click();
+
+ // # Open accept dialog
+ cy.findAllByRole('menuitem', {name: 'Accept an invitation'}).click();
+
+ cy.findByRole('dialog', {name: 'Accept a connection invite'}).as('dialog');
+
+ // * Verify dialog
+ cy.get('@dialog').within(() => {
+ cy.uiGetHeading('Accept a connection invite');
+
+ // * Verify instructions
+ cy.findByText('Accept a secure connection from another server');
+ cy.findByText('Enter the encrypted invitation code shared to you by the admin of the server you are connecting with.');
+
+ // # Enter org name
+ cy.findByRole('textbox', {name: 'Organization name'}).type(orgDisplayName);
+
+ // # Enter bad invitation code
+ cy.findByRole('textbox', {name: 'Encrypted invitation code'}).type('abc123');
+
+ // # Enter bad password
+ cy.findByRole('textbox', {name: 'Password'}).type('123abc');
+
+ // # Try accept
+ cy.uiGetButton('Accept').click();
+
+ // * Verify error shown
+ cy.findByText('There was an error while accepting the invite.');
+
+ // # Close dialog
+ cy.uiGetButton('Cancel').click();
+ });
+
+ // * Verify dialog closed
+ cy.get('@dialog').should('not.exist');
+ });
+ });
+
+ describe('create new connection lifecycle', () => {
+ const orgDisplayName = 'Testing Org Name ' + getRandomId();
+ const orgDisplayName2 = 'new display name here ' + getRandomId();
+
+ before(() => {
+ cy.visit('/admin_console/environment/secure_connections');
+ });
+
+ it('create', () => {
+ // # Click create
+ cy.findAllByRole('button', {name: 'Add a connection'}).first().click();
+ cy.findAllByRole('menuitem', {name: 'Create a connection'}).click();
+
+ // * Verify on create page
+ cy.location('pathname').should('include', '/secure_connections/create');
+
+ // * Verify name focused
+ // # Enter name
+ cy.findByTestId('organization-name-input').
+ should('be.focused').
+ type(orgDisplayName);
+
+ // # Select team
+ cy.findByTestId('destination-team-input').click().
+ findByRole('textbox').type(`${testTeam.display_name}{enter}`);
+
+ // # Save
+ cy.uiGetButton('Save').click();
+
+ // * Verify page change
+ cy.location('pathname').should('not.include', '/secure_connections/create');
+ });
+
+ it('created dialog', () => {
+ // * Verify connection created dialog
+ verifyInviteDialog('Connection created');
+ });
+
+ it('basic details', () => {
+ // * Verify name
+ cy.findByTestId('organization-name-input').
+ should('not.be.focused').
+ should('have.value', orgDisplayName);
+
+ // * Verify team
+ cy.findByTestId('destination-team-input').should('have.text', testTeam.display_name);
+
+ // * Verify connection status label
+ cy.findByText('Connection Pending');
+ });
+
+ it('shared channels - empty', () => {
+ cy.findByTestId('shared_channels_section').within(() => {
+ cy.findByRole('heading', {name: "You haven't shared any channels"});
+
+ cy.findByText('Please add channels to start sharing');
+ });
+ });
+
+ it('add channel', () => {
+ cy.findByTestId('shared_channels_section').within(() => {
+ // * Verify title
+ cy.findByRole('heading', {name: 'Shared Channels'});
+
+ // * Verify subtitle
+ cy.findByText("A list of all the channels shared with your organization and channels you're sharing externally.");
+
+ // # Open add channels dialog
+ cy.uiGetButton('Add channels').click();
+ });
+
+ cy.findByRole('dialog', {name: 'Select channels'}).as('dialog');
+
+ cy.get('@dialog').within(() => {
+ // * Verify instructions
+ cy.findByText('Please select a team and channels to share');
+
+ cy.findByRole('textbox', {name: 'Search and add channels'}).
+ should('be.focused').
+ type(testChannel.display_name, {force: true}).
+ wait(timeouts.HALF_SEC).
+ type('{enter}');
+
+ // # Share
+ cy.uiGetButton('Share').click();
+
+ // * Verify create modal closed
+ cy.get('@dialog').should('not.exist');
+ });
+ });
+
+ it('shared channels', () => {
+ cy.findByTestId('shared_channels_section').within(() => {
+ // * Verify tabs
+ cy.findByRole('tab', {name: orgDisplayName});
+ cy.findByRole('tab', {name: 'Your channels', selected: true});
+
+ cy.findByRole('table').findAllByRole('row').as('rows');
+
+ // * Verify table headers
+ cy.get('@rows').first().within(() => {
+ cy.findByRole('columnheader', {name: 'Name'});
+ cy.findByRole('columnheader', {name: 'Current Team'});
+ });
+
+ // * Verify shared channel row
+ cy.get('@rows').eq(1).as('sharedChannelRow').within(() => {
+ cy.findByRole('cell', {name: testChannel.display_name});
+ cy.findByRole('cell', {name: testTeam.display_name});
+ });
+ });
+ });
+
+ it('remove channel', () => {
+ // # Prompt remove channel
+ cy.findByRole('table').findAllByRole('row').eq(1).findByRole('button', {name: 'Remove'}).click();
+
+ // * Verify channel remove prompt
+ cy.findByRole('dialog', {name: 'Remove channel'}).as('dialog');
+
+ cy.get('@dialog').within(() => {
+ // * Verify heading
+ cy.uiGetHeading('Remove channel');
+
+ // * Verify instructions
+ cy.findByText('The channel will be removed from this connection and will no longer be shared with it.');
+
+ cy.uiGetButton('Remove').click();
+ });
+
+ // * Verify no channels shared
+ cy.uiGetHeading("You haven't shared any channels");
+ });
+
+ it('change display name and destination team', () => {
+ // * Verify no changes to save
+ cy.uiGetButton('Save').should('be.disabled');
+
+ // # Enter name
+ cy.findByTestId('organization-name-input').
+ focus().
+ clear().
+ type(orgDisplayName2);
+
+ // # Select team
+ cy.findByTestId('destination-team-input').click().
+ findByRole('textbox').type(`${testTeam2.display_name}{enter}`);
+
+ // # Save
+ cy.uiGetButton('Save').click();
+
+ // * Verify name
+ cy.findByTestId('organization-name-input').
+ should('have.value', orgDisplayName2);
+
+ // * Verify team
+ cy.findByTestId('destination-team-input').should('have.text', testTeam2.display_name);
+
+ cy.wait(timeouts.ONE_SEC);
+ });
+
+ it('can go back', () => {
+ // # Go back to list page
+ cy.get('a.back').click();
+
+ // * Verify back at list page
+ cy.findByRole('heading', {name: 'Connected Workspaces'});
+ });
+
+ it('connection row - basics', () => {
+ cy.findAllByRole('link', {name: orgDisplayName2}).as('row');
+
+ // # Open connection detail
+ cy.get('@row').click();
+
+ // # Go back to list page
+ cy.get('a.back').click();
+
+ // * Verify connection status
+ cy.get('@row').findByText('Connection Pending');
+
+ // # Open menu, click edit
+ cy.get('@row').findByRole('button', {name: `Connection options for ${orgDisplayName2}`}).click();
+ cy.findByRole('menu').findByRole('menuitem', {name: 'Edit'}).click();
+
+ // # Go back to list page
+ cy.get('a.back').click();
+ });
+
+ it('connection row - generate invitation', () => {
+ cy.findAllByRole('link', {name: orgDisplayName2}).as('row');
+
+ // # Open menu
+ cy.get('@row').findByRole('button', {name: `Connection options for ${orgDisplayName2}`}).click();
+
+ // # Generate invite
+ cy.findByRole('menu').findByRole('menuitem', {name: 'Generate invitation code'}).click();
+
+ verifyInviteDialog('Invitation code');
+ });
+
+ it('connection row - delete connection', () => {
+ cy.findAllByRole('link', {name: orgDisplayName2}).as('row');
+
+ // # Open menu
+ cy.get('@row').findByRole('button', {name: `Connection options for ${orgDisplayName2}`}).click();
+
+ // # Prompt delete
+ cy.findByRole('menu').findByRole('menuitem', {name: 'Delete'}).click();
+
+ // * Verify delete dialog
+ cy.findByRole('dialog', {name: 'Delete secure connection'}).as('dialog');
+ cy.get('@dialog').within(() => {
+ // * Verify heading
+ cy.uiGetHeading('Delete secure connection');
+
+ // # Delete
+ cy.uiGetButton('Yes, delete').click();
+ });
+
+ // * Verify connection deleted
+ cy.get('@row').should('not.exist');
+
+ // * Verify dialog closed
+ cy.get('@dialog').should('not.exist');
+ });
+ });
+});
+
+const verifyInviteDialog = (name: string) => {
+ stubClipboard().as('clipboard');
+
+ cy.findByRole('dialog', {name}).as('dialog').within(() => {
+ // * Verify heading
+ cy.uiGetHeading(name);
+
+ // * Verify instructions
+ cy.findByText('Please share the invitation code and password with the administrator of the server you want to connect with.');
+ cy.findByText('Share these two separately to avoid a security compromise');
+ cy.findByText('Share this code and password');
+
+ cy.findByRole('group', {name: 'Encrypted invitation code'}).as('invite');
+ cy.findByRole('group', {name: 'Password'}).as('password');
+
+ // # Copy invite
+ // * Verify copy button text
+ cy.get('@invite').
+ findByRole('button', {name: 'Copy'}).
+ click().
+ should('have.text', 'Copied');
+
+ // * Verify invite copied to clipboard
+ cy.get('@invite').
+ findByRole('textbox').invoke('val').
+ then((value) => {
+ cy.get('@clipboard').
+ its('contents').
+ should('contain', value);
+ });
+
+ // # Copy password
+ // * Verify copy button text
+ cy.get('@password').
+ findByRole('button', {name: 'Copy'}).
+ click().
+ should('have.text', 'Copied');
+
+ // * Verify password copied to clipboard
+ cy.get('@password').
+ findByRole('textbox').invoke('val').
+ then((value) => {
+ cy.get('@clipboard').
+ its('contents').
+ should('contain', value);
+ });
+
+ // # Close dialog
+ cy.uiGetButton('Done').click();
+ });
+
+ // * Verify dialog closed
+ cy.get('@dialog').should('not.exist');
+};
diff --git a/webapp/channels/src/components/admin_console/secure_connections/secure_connection_detail.tsx b/webapp/channels/src/components/admin_console/secure_connections/secure_connection_detail.tsx
index baea27fda6..16c06ff952 100644
--- a/webapp/channels/src/components/admin_console/secure_connections/secure_connection_detail.tsx
+++ b/webapp/channels/src/components/admin_console/secure_connections/secure_connection_detail.tsx
@@ -154,6 +154,7 @@ export default function SecureConnectionDetail(props: Props) {
>
- {rc.display_name}
+
+ {rc.display_name}
@@ -59,10 +64,11 @@ const RowMenu = ({remoteCluster: rc, onDeleteSuccess, disabled}: Props) => {
return (
,
+ 'aria-label': formatMessage({id: 'admin.secure_connection_row.menu-button.aria_label', defaultMessage: 'Connection options for {connection}'}, {connection: rc.display_name}),
}}
menu={{
id: menuId,
@@ -126,7 +132,7 @@ const RowLink = styled(Link).attrs({className: 'secure-connection
border-bottom: 0;
}
- #${menuId}-button {
+ .connection-row-menu-button {
padding: 0px 8px;
}
`;
diff --git a/webapp/channels/src/components/admin_console/secure_connections/team_selector.tsx b/webapp/channels/src/components/admin_console/secure_connections/team_selector.tsx
index d4df90feb5..9af9129922 100644
--- a/webapp/channels/src/components/admin_console/secure_connections/team_selector.tsx
+++ b/webapp/channels/src/components/admin_console/secure_connections/team_selector.tsx
@@ -14,6 +14,7 @@ export type Props = {
value: string;
teamsById: IDMappedObjects;
onChange: (teamId: string) => void;
+ testId: string;
}
const TeamSelector = (props: Props): JSX.Element => {
@@ -33,6 +34,7 @@ const TeamSelector = (props: Props): JSX.Element => {
return (