mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-60616: Shared Channels - System Console E2E (#28379)
* fix rc content types * api spec * don't ignore content type when parsing body * secure connections: - create - read - update - delete - share - accept * title * wip * manage shared channels, other fixes * revert * revert old * maxlines * add team selector, tooltip fix, msg fixes * cleanup * remote disabled state admindef * add team label to channels input, fix i18n * add filter * review fixes, other fixes * cleanup * refactor channel fetching * move modals * fix imports, fix collected id * remove unused action * tabs visibility when connection pending, hide tabs if none from home are shared. when connection is connected, always show tabs * api docs invite/uninvite response type * fix modal id ref * Remove the generate invite option for confirmed clusters * e2e: add empty state test * e2e: add create flow test * fix modal id ref * rev: double negation * rev: server side password for create flow * rev: remote cluster type 0 token, delete_at * testing a11y, ids * e2e --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
This commit is contained in:
parent
cd43bac181
commit
9ae2752593
@ -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');
|
||||||
|
};
|
@ -154,6 +154,7 @@ export default function SecureConnectionDetail(props: Props) {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
|
data-testid='organization-name-input'
|
||||||
value={remoteCluster?.display_name ?? ''}
|
value={remoteCluster?.display_name ?? ''}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
autoFocus={isCreating}
|
autoFocus={isCreating}
|
||||||
@ -170,6 +171,7 @@ export default function SecureConnectionDetail(props: Props) {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<TeamSelector
|
<TeamSelector
|
||||||
|
testId='destination-team-input'
|
||||||
value={remoteCluster.default_team_id ?? ''}
|
value={remoteCluster.default_team_id ?? ''}
|
||||||
teamsById={teamsById}
|
teamsById={teamsById}
|
||||||
onChange={handleTeamChange}
|
onChange={handleTeamChange}
|
||||||
|
@ -25,9 +25,14 @@ type Props = {
|
|||||||
export default function SecureConnectionRow(props: Props) {
|
export default function SecureConnectionRow(props: Props) {
|
||||||
const {remoteCluster: rc} = props;
|
const {remoteCluster: rc} = props;
|
||||||
|
|
||||||
|
const titleId = `${rc.remote_id}-title`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowLink to={getEditLocation(rc)}>
|
<RowLink
|
||||||
<Title>{rc.display_name}</Title>
|
to={getEditLocation(rc)}
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
>
|
||||||
|
<Title id={titleId}>{rc.display_name}</Title>
|
||||||
<Detail>
|
<Detail>
|
||||||
<ConnectionStatusLabel rc={rc}/>
|
<ConnectionStatusLabel rc={rc}/>
|
||||||
<RowMenu {...props}/>
|
<RowMenu {...props}/>
|
||||||
@ -59,10 +64,11 @@ const RowMenu = ({remoteCluster: rc, onDeleteSuccess, disabled}: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Menu.Container
|
<Menu.Container
|
||||||
menuButton={{
|
menuButton={{
|
||||||
id: `${menuId}-button`,
|
id: `${menuId}-button-${rc.remote_id}`,
|
||||||
class: classNames('btn btn-tertiary btn-sm', {disabled}),
|
class: classNames('btn btn-tertiary btn-sm connection-row-menu-button', {disabled}),
|
||||||
disabled,
|
disabled,
|
||||||
children: !disabled && <DotsHorizontalIcon size={16}/>,
|
children: !disabled && <DotsHorizontalIcon size={16}/>,
|
||||||
|
'aria-label': formatMessage({id: 'admin.secure_connection_row.menu-button.aria_label', defaultMessage: 'Connection options for {connection}'}, {connection: rc.display_name}),
|
||||||
}}
|
}}
|
||||||
menu={{
|
menu={{
|
||||||
id: menuId,
|
id: menuId,
|
||||||
@ -126,7 +132,7 @@ const RowLink = styled(Link<RemoteCluster>).attrs({className: 'secure-connection
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#${menuId}-button {
|
.connection-row-menu-button {
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -14,6 +14,7 @@ export type Props = {
|
|||||||
value: string;
|
value: string;
|
||||||
teamsById: IDMappedObjects<Team>;
|
teamsById: IDMappedObjects<Team>;
|
||||||
onChange: (teamId: string) => void;
|
onChange: (teamId: string) => void;
|
||||||
|
testId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamSelector = (props: Props): JSX.Element => {
|
const TeamSelector = (props: Props): JSX.Element => {
|
||||||
@ -33,6 +34,7 @@ const TeamSelector = (props: Props): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<DropdownInput
|
<DropdownInput
|
||||||
className='team_selector'
|
className='team_selector'
|
||||||
|
testId={props.testId}
|
||||||
required={true}
|
required={true}
|
||||||
onChange={handleTeamChange}
|
onChange={handleTeamChange}
|
||||||
value={value ? {label: value.display_name, value: value.id} : undefined}
|
value={value ? {label: value.display_name, value: value.id} : undefined}
|
||||||
|
@ -2210,6 +2210,7 @@
|
|||||||
"admin.secure_connection_detail.shared_channels.table.remote_actions.remove": "Remove",
|
"admin.secure_connection_detail.shared_channels.table.remote_actions.remove": "Remove",
|
||||||
"admin.secure_connection_detail.shared_channels.table.team_home": "Current Team",
|
"admin.secure_connection_detail.shared_channels.table.team_home": "Current Team",
|
||||||
"admin.secure_connection_detail.shared_channels.table.team_remote": "Destination Team",
|
"admin.secure_connection_detail.shared_channels.table.team_remote": "Destination Team",
|
||||||
|
"admin.secure_connection_row.menu-button.aria_label": "Connection options for {connection}",
|
||||||
"admin.secure_connection_row.menu.aria_label": "secure connection row menu",
|
"admin.secure_connection_row.menu.aria_label": "secure connection row menu",
|
||||||
"admin.secure_connection_row.menu.delete": "Delete",
|
"admin.secure_connection_row.menu.delete": "Delete",
|
||||||
"admin.secure_connection_row.menu.edit": "Edit",
|
"admin.secure_connection_row.menu.edit": "Edit",
|
||||||
|
Loading…
Reference in New Issue
Block a user