[MM-50352] Improve URL validation and add/edit server experience (#2720)

* [MM-50352] Improve URL validation and add/edit server experience

* Fix build

* Fix translations

* First pass of fixes

* Some changes to avoid 2 clicks, tests

* PR feedback

* Update translations

* PR feedback

* Fix translations

* PR feedback

* E2E test fixes
This commit is contained in:
Devin Binnie
2023-05-24 09:04:38 -04:00
committed by GitHub
parent a87e770c73
commit 1239add076
25 changed files with 712 additions and 275 deletions

View File

@@ -53,42 +53,31 @@ describe('Add Server Modal', function desc() {
});
describe('MM-T4389 Invalid messages', () => {
it('MM-T4389_1 should not be valid if no server name or URL has been set', async () => {
await newServerView.click('#saveNewServerModal');
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
existingName.should.be.true;
existingUrl.should.be.true;
});
it('should not be valid if a server with the same name exists', async () => {
await newServerView.type('#serverNameInput', config.teams[0].name);
await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#serverNameInput.is-invalid');
it('MM-T4389_1 should not be valid and save should be disabled if no server name or URL has been set', async () => {
const existing = await newServerView.isVisible('#nameValidation.error');
existing.should.be.true;
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
(disabled === '').should.be.true;
});
it('should not be valid if a server with the same URL exists', async () => {
it('should warn the user if a server with the same URL exists, but still allow them to save', async () => {
await newServerView.type('#serverNameInput', 'some-new-server');
await newServerView.type('#serverUrlInput', config.teams[0].url);
await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
await newServerView.waitForSelector('#urlValidation.warning');
const existing = await newServerView.isVisible('#urlValidation.warning');
existing.should.be.true;
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
(disabled === '').should.be.false;
});
describe('Valid server name', async () => {
beforeEach(async () => {
await newServerView.type('#serverNameInput', 'TestServer');
await newServerView.click('#saveNewServerModal');
});
it('MM-T4389_2 Name should not be marked invalid, URL should be marked invalid', async () => {
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
it('MM-T4389_2 Name should not be marked invalid, but should not be able to save', async () => {
await newServerView.waitForSelector('#nameValidation.error', {state: 'detached'});
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
existingName.should.be.false;
existingUrl.should.be.true;
(disabled === '').should.be.true;
});
});
@@ -96,12 +85,11 @@ describe('Add Server Modal', function desc() {
describe('Valid server url', () => {
beforeEach(async () => {
await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.click('#saveNewServerModal');
});
it('MM-T4389_3 URL should not be marked invalid, name should be marked invalid', async () => {
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
const existingUrl = await newServerView.isVisible('#urlValidation.error');
const existingName = await newServerView.isVisible('#nameValidation.error');
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
existingName.should.be.true;
existingUrl.should.be.false;
@@ -112,8 +100,8 @@ describe('Add Server Modal', function desc() {
it('MM-T2826_1 should not be valid if an invalid server address has been set', async () => {
await newServerView.type('#serverUrlInput', 'superInvalid url');
await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
await newServerView.waitForSelector('#urlValidation.error');
const existing = await newServerView.isVisible('#urlValidation.error');
existing.should.be.true;
});
@@ -121,6 +109,7 @@ describe('Add Server Modal', function desc() {
beforeEach(async () => {
await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.type('#serverNameInput', 'TestServer');
await newServerView.waitForSelector('#urlValidation.warning');
});
it('should be possible to click add', async () => {

View File

@@ -49,7 +49,8 @@ describe('Configure Server Modal', function desc() {
it('MM-T5117 should be valid if display name and URL are set', async () => {
await configureServerModal.type('#input_name', 'TestServer');
await configureServerModal.type('#input_url', 'http://example.org');
await configureServerModal.type('#input_url', 'https://community.mattermost.com');
await configureServerModal.waitForSelector('#customMessage_url.Input___success');
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
(connectButtonDisabled === '').should.be.false;
@@ -57,11 +58,8 @@ describe('Configure Server Modal', function desc() {
it('MM-T5118 should not be valid if an invalid URL has been set', async () => {
await configureServerModal.type('#input_name', 'TestServer');
await configureServerModal.type('#input_url', 'lorem.ipsum.dolor.sit.amet');
await configureServerModal.click('#connectConfigureServer');
await asyncSleep(1000);
await configureServerModal.type('#input_url', '!@#$%^&*()');
await configureServerModal.waitForSelector('#customMessage_url.Input___error');
const errorClass = await configureServerModal.getAttribute('#customMessage_url', 'class');
errorClass.should.contain('Input___error');

View File

@@ -101,20 +101,8 @@ describe('EditServerModal', function desc() {
it('MM-T2826_3 should not edit server if an invalid server address has been set', async () => {
await editServerView.type('#serverUrlInput', 'superInvalid url');
await editServerView.click('#saveNewServerModal');
const existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true;
});
it('should not edit server if another server with the same name or URL exists', async () => {
await editServerView.fill('#serverNameInput', config.teams[1].name);
await editServerView.click('#saveNewServerModal');
let existing = await editServerView.isVisible('#serverNameInput.is-invalid');
existing.should.be.true;
await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.fill('#serverUrlInput', config.teams[1].url);
existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
await editServerView.waitForSelector('#urlValidation.error');
const existing = await editServerView.isVisible('#urlValidation.error');
existing.should.be.true;
});

View File

@@ -121,13 +121,20 @@
"renderer.components.autoSaveIndicator.saving": "Saving...",
"renderer.components.configureServer.cardtitle": "Enter your server details",
"renderer.components.configureServer.connect.default": "Connect",
"renderer.components.configureServer.connect.override": "Connect anyway",
"renderer.components.configureServer.connect.saving": "Connecting…",
"renderer.components.configureServer.name.info": "The name that will be displayed in your server list",
"renderer.components.configureServer.name.placeholder": "Server display name",
"renderer.components.configureServer.subtitle": "Set up your first server to connect to your<br></br>teams communication hub",
"renderer.components.configureServer.title": "Lets connect to a server",
"renderer.components.configureServer.url.info": "The URL of your Mattermost server",
"renderer.components.configureServer.url.insecure": "Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.",
"renderer.components.configureServer.url.notMattermost": "The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.",
"renderer.components.configureServer.url.ok": "Server URL is valid. Server version: {serverVersion}",
"renderer.components.configureServer.url.placeholder": "Server URL",
"renderer.components.configureServer.url.urlNotMatched": "The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
"renderer.components.configureServer.url.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
"renderer.components.configureServer.url.validating": "Validating...",
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
@@ -139,17 +146,21 @@
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.newServerModal.error.nameRequired": "Name is required.",
"renderer.components.newServerModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newServerModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newServerModal.error.urlRequired": "URL is required.",
"renderer.components.newServerModal.serverDisplayName": "Server Display Name",
"renderer.components.newServerModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newServerModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newServerModal.success.ok": "Server URL is valid. Server version: {serverVersion}",
"renderer.components.newServerModal.title.add": "Add Server",
"renderer.components.newServerModal.title.edit": "Edit Server",
"renderer.components.newServerModal.validating": "Validating...",
"renderer.components.newServerModal.warning.insecure": "Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.",
"renderer.components.newServerModal.warning.notMattermost": "The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.",
"renderer.components.newServerModal.warning.urlNotMatched": "The server URL does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
"renderer.components.newServerModal.warning.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
"renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.",
"renderer.components.removeServerModal.confirm": "Confirm you wish to remove the {serverName} server?",
"renderer.components.removeServerModal.title": "Remove Server",

View File

@@ -170,3 +170,5 @@ export const UPDATE_APPSTATE_FOR_VIEW_ID = 'update-appstate-for-view-id';
export const MAIN_WINDOW_CREATED = 'main-window-created';
export const MAIN_WINDOW_RESIZED = 'main-window-resized';
export const MAIN_WINDOW_FOCUSED = 'main-window-focused';
export const VALIDATE_SERVER_URL = 'validate-server-url';

View File

@@ -44,6 +44,17 @@ export const DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT = DOWNLOADS_DROPDOWN_MENU_HEIGH
export const DOWNLOADS_DROPDOWN_MAX_ITEMS = 50;
export const DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT = 4000; // 4 sec
export const URLValidationStatus = {
OK: 'OK',
Missing: 'MISSING',
Invalid: 'INVALID',
Insecure: 'INSECURE',
URLExists: 'URL_EXISTS',
NotMattermost: 'NOT_MATTERMOST',
URLNotMatched: 'URL_NOT_MATCHED',
URLUpdated: 'URL_UPDATED',
};
// supported custom login paths (oath, saml)
export const customLoginRegexPaths = [
/^\/oauth\/authorize$/i,

View File

@@ -41,6 +41,7 @@ import {
WINDOW_MINIMIZE,
WINDOW_RESTORE,
DOUBLE_CLICK_ON_WINDOW,
VALIDATE_SERVER_URL,
} from 'common/communication';
import Config from 'common/config';
import {isTrustedURL, parseURL} from 'common/utils/url';
@@ -97,6 +98,7 @@ import {
handleEditServerModal,
handleNewServerModal,
handleRemoveServerModal,
handleServerURLValidation,
switchServer,
} from './servers';
import {
@@ -287,6 +289,7 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
ipcMain.handle(VALIDATE_SERVER_URL, handleServerURLValidation);
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
ipcMain.on(START_UPGRADE, handleStartUpgrade);

View File

@@ -47,7 +47,6 @@ describe('main/app/intercom', () => {
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
ServerManager.getAllServers.mockReturnValue([]);
ServerManager.hasServers.mockReturnValue(false);
});
@@ -56,7 +55,7 @@ describe('main/app/intercom', () => {
ModalManager.addModal.mockReturnValue(promise);
handleWelcomeScreenModal();
expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', '/some/index.html', '/some/preload.js', [], {}, true);
expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', '/some/index.html', '/some/preload.js', null, {}, true);
});
});

View File

@@ -97,7 +97,7 @@ export function handleWelcomeScreenModal() {
if (!mainWindow) {
return;
}
const modalPromise = ModalManager.addModal<UniqueServer[], UniqueServer>('welcomeScreen', html, preload, ServerManager.getAllServers().map((server) => server.toUniqueServer()), mainWindow, !ServerManager.hasServers());
const modalPromise = ModalManager.addModal<null, UniqueServer>('welcomeScreen', html, preload, null, mainWindow, !ServerManager.hasServers());
if (modalPromise) {
modalPromise.then((data) => {
const newServer = ServerManager.addServer(data);

View File

@@ -1,9 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MattermostServer} from 'common/servers/MattermostServer';
import ServerManager from 'common/servers/serverManager';
import {URLValidationStatus} from 'common/utils/constants';
import {getDefaultViewsForConfigServer} from 'common/views/View';
import {ServerInfo} from 'main/server/serverInfo';
import ModalManager from 'main/views/modalManager';
import {getLocalURLString, getLocalPreload} from 'main/utils';
import MainWindow from 'main/windows/mainWindow';
@@ -28,10 +31,17 @@ jest.mock('common/servers/serverManager', () => ({
getView: jest.fn(),
getLastActiveTabForServer: jest.fn(),
getServerLog: jest.fn(),
lookupViewByURL: jest.fn(),
}));
jest.mock('common/servers/MattermostServer', () => ({
MattermostServer: jest.fn(),
}));
jest.mock('common/views/View', () => ({
getDefaultViewsForConfigServer: jest.fn(),
}));
jest.mock('main/server/serverInfo', () => ({
ServerInfo: jest.fn(),
}));
jest.mock('main/views/modalManager', () => ({
addModal: jest.fn(),
}));
@@ -315,4 +325,156 @@ describe('main/app/servers', () => {
}));
});
});
describe('handleServerURLValidation', () => {
beforeEach(() => {
MattermostServer.mockImplementation(({url}) => ({url}));
ServerInfo.mockImplementation(({url}) => ({
fetchRemoteInfo: jest.fn().mockImplementation(() => ({
serverVersion: '7.8.0',
siteName: 'Mattermost',
siteURL: url,
})),
}));
});
afterEach(() => {
jest.resetAllMocks();
});
it('should return Missing when you get no URL', async () => {
const result = await Servers.handleServerURLValidation({});
expect(result.status).toBe(URLValidationStatus.Missing);
});
it('should return Invalid when you pass in invalid characters', async () => {
const result = await Servers.handleServerURLValidation({}, '!@#$%^&*()!@#$%^&*()');
expect(result.status).toBe(URLValidationStatus.Invalid);
});
it('should include HTTPS when missing', async () => {
const result = await Servers.handleServerURLValidation({}, 'server.com');
expect(result.status).toBe(URLValidationStatus.OK);
expect(result.validatedURL).toBe('https://server.com/');
});
it('should correct typos in the protocol', async () => {
const result = await Servers.handleServerURLValidation({}, 'htpst://server.com');
expect(result.status).toBe(URLValidationStatus.OK);
expect(result.validatedURL).toBe('https://server.com/');
});
it('should replace HTTP with HTTPS when applicable', async () => {
const result = await Servers.handleServerURLValidation({}, 'http://server.com');
expect(result.status).toBe(URLValidationStatus.OK);
expect(result.validatedURL).toBe('https://server.com/');
});
it('should generate a warning when the server already exists', async () => {
ServerManager.lookupViewByURL.mockReturnValue({server: {id: 'server-1', url: new URL('https://server.com')}});
const result = await Servers.handleServerURLValidation({}, 'https://server.com');
expect(result.status).toBe(URLValidationStatus.URLExists);
expect(result.validatedURL).toBe('https://server.com/');
});
it('should generate a warning if the server exists when editing', async () => {
ServerManager.lookupViewByURL.mockReturnValue({server: {name: 'Server 1', id: 'server-1', url: new URL('https://server.com')}});
const result = await Servers.handleServerURLValidation({}, 'https://server.com', 'server-2');
expect(result.status).toBe(URLValidationStatus.URLExists);
expect(result.validatedURL).toBe('https://server.com/');
expect(result.existingServerName).toBe('Server 1');
});
it('should not generate a warning if editing the same server', async () => {
ServerManager.lookupViewByURL.mockReturnValue({server: {name: 'Server 1', id: 'server-1', url: new URL('https://server.com')}});
const result = await Servers.handleServerURLValidation({}, 'https://server.com', 'server-1');
expect(result.status).toBe(URLValidationStatus.OK);
expect(result.validatedURL).toBe('https://server.com/');
});
it('should attempt HTTP when HTTPS fails, and generate a warning', async () => {
ServerInfo.mockImplementation(({url}) => ({
fetchRemoteInfo: jest.fn().mockImplementation(() => {
if (url.startsWith('https:')) {
return undefined;
}
return {
serverVersion: '7.8.0',
siteName: 'Mattermost',
siteURL: url,
};
}),
}));
const result = await Servers.handleServerURLValidation({}, 'http://server.com');
expect(result.status).toBe(URLValidationStatus.Insecure);
expect(result.validatedURL).toBe('http://server.com/');
});
it('should show a warning when the ping request times out', async () => {
ServerInfo.mockImplementation(() => ({
fetchRemoteInfo: jest.fn().mockImplementation(() => {
throw new Error();
}),
}));
const result = await Servers.handleServerURLValidation({}, 'https://not-server.com');
expect(result.status).toBe(URLValidationStatus.NotMattermost);
expect(result.validatedURL).toBe('https://not-server.com/');
});
it('should update the users URL when the Site URL is different', async () => {
ServerInfo.mockImplementation(() => ({
fetchRemoteInfo: jest.fn().mockImplementation(() => {
return {
serverVersion: '7.8.0',
siteName: 'Mattermost',
siteURL: 'https://mainserver.com/',
};
}),
}));
const result = await Servers.handleServerURLValidation({}, 'https://server.com');
expect(result.status).toBe(URLValidationStatus.URLUpdated);
expect(result.validatedURL).toBe('https://mainserver.com/');
});
it('should warn the user when the Site URL is different but unreachable', async () => {
ServerInfo.mockImplementation(({url}) => ({
fetchRemoteInfo: jest.fn().mockImplementation(() => {
if (url === 'https://mainserver.com/') {
return undefined;
}
return {
serverVersion: '7.8.0',
siteName: 'Mattermost',
siteURL: 'https://mainserver.com/',
};
}),
}));
const result = await Servers.handleServerURLValidation({}, 'https://server.com');
expect(result.status).toBe(URLValidationStatus.URLNotMatched);
expect(result.validatedURL).toBe('https://server.com/');
});
it('should warn the user when the Site URL already exists as another server', async () => {
ServerManager.lookupViewByURL.mockReturnValue({server: {name: 'Server 1', id: 'server-1', url: new URL('https://mainserver.com')}});
ServerInfo.mockImplementation(() => ({
fetchRemoteInfo: jest.fn().mockImplementation(() => {
return {
serverVersion: '7.8.0',
siteName: 'Mattermost',
siteURL: 'https://mainserver.com',
};
}),
}));
const result = await Servers.handleServerURLValidation({}, 'https://server.com');
expect(result.status).toBe(URLValidationStatus.URLExists);
expect(result.validatedURL).toBe('https://mainserver.com/');
expect(result.existingServerName).toBe('Server 1');
});
});
});

View File

@@ -1,18 +1,23 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IpcMainEvent, ipcMain} from 'electron';
import {IpcMainEvent, IpcMainInvokeEvent, ipcMain} from 'electron';
import {UniqueServer, Server} from 'types/config';
import {URLValidationResult} from 'types/server';
import {UPDATE_SHORTCUT_MENU} from 'common/communication';
import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager';
import {MattermostServer} from 'common/servers/MattermostServer';
import {isValidURI, isValidURL, parseURL} from 'common/utils/url';
import {URLValidationStatus} from 'common/utils/constants';
import ViewManager from 'main/views/viewManager';
import ModalManager from 'main/views/modalManager';
import MainWindow from 'main/windows/mainWindow';
import {getLocalPreload, getLocalURLString} from 'main/utils';
import {ServerInfo} from 'main/server/serverInfo';
const log = new Logger('App.Servers');
@@ -49,7 +54,7 @@ export const handleNewServerModal = () => {
if (!mainWindow) {
return;
}
const modalPromise = ModalManager.addModal<UniqueServer[], Server>('newServer', html, preload, ServerManager.getAllServers().map((server) => server.toUniqueServer()), mainWindow, !ServerManager.hasServers());
const modalPromise = ModalManager.addModal<null, Server>('newServer', html, preload, null, mainWindow, !ServerManager.hasServers());
if (modalPromise) {
modalPromise.then((data) => {
const newServer = ServerManager.addServer(data);
@@ -80,14 +85,11 @@ export const handleEditServerModal = (e: IpcMainEvent, id: string) => {
if (!server) {
return;
}
const modalPromise = ModalManager.addModal<{currentServers: UniqueServer[]; server: UniqueServer}, Server>(
const modalPromise = ModalManager.addModal<UniqueServer, Server>(
'editServer',
html,
preload,
{
currentServers: ServerManager.getAllServers().map((server) => server.toUniqueServer()),
server: server.toUniqueServer(),
},
server.toUniqueServer(),
mainWindow);
if (modalPromise) {
modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => {
@@ -132,3 +134,96 @@ export const handleRemoveServerModal = (e: IpcMainEvent, id: string) => {
log.warn('There is already an edit server modal');
}
};
export const handleServerURLValidation = async (e: IpcMainInvokeEvent, url?: string, currentId?: string): Promise<URLValidationResult> => {
log.debug('handleServerURLValidation', url, currentId);
// If the URL is missing or null, reject
if (!url) {
return {status: URLValidationStatus.Missing};
}
let httpUrl = url;
if (!isValidURL(url)) {
// If it already includes the protocol, tell them it's invalid
if (isValidURI(url)) {
httpUrl = url.replace(/^(.+):/, 'https:');
} else {
// Otherwise add HTTPS for them
httpUrl = `https://${url}`;
}
}
// Make sure the final URL is valid
const parsedURL = parseURL(httpUrl);
if (!parsedURL) {
return {status: URLValidationStatus.Invalid};
}
// Try and add HTTPS to see if we can get a more secure URL
let secureURL = parsedURL;
if (parsedURL.protocol === 'http:') {
secureURL = parseURL(parsedURL.toString().replace(/^http:/, 'https:')) ?? parsedURL;
}
// Tell the user if they already have a server for this URL
const existingServer = ServerManager.lookupViewByURL(secureURL, true);
if (existingServer && existingServer.server.id !== currentId) {
return {status: URLValidationStatus.URLExists, existingServerName: existingServer.server.name, validatedURL: existingServer.server.url.toString()};
}
// Try and get remote info from the most secure URL, otherwise use the insecure one
let remoteURL = secureURL;
let remoteInfo = await testRemoteServer(secureURL);
if (!remoteInfo) {
if (secureURL.toString() !== parsedURL.toString()) {
remoteURL = parsedURL;
remoteInfo = await testRemoteServer(parsedURL);
}
}
// If we can't get the remote info, warn the user that this might not be the right URL
// If the original URL was invalid, don't replace that as they probably have a typo somewhere
if (!remoteInfo) {
return {status: URLValidationStatus.NotMattermost, validatedURL: parsedURL.toString()};
}
// If we were only able to connect via HTTP, warn the user that the connection is not secure
if (remoteURL.protocol === 'http:') {
return {status: URLValidationStatus.Insecure, serverVersion: remoteInfo.serverVersion, validatedURL: remoteURL.toString()};
}
// If the URL doesn't match the Site URL, set the URL to the correct one
if (remoteInfo.siteURL && remoteURL.toString() !== new URL(remoteInfo.siteURL).toString()) {
const parsedSiteURL = parseURL(remoteInfo.siteURL);
if (parsedSiteURL) {
// Check the Site URL as well to see if it's already pre-configured
const existingServer = ServerManager.lookupViewByURL(parsedSiteURL, true);
if (existingServer && existingServer.server.id !== currentId) {
return {status: URLValidationStatus.URLExists, existingServerName: existingServer.server.name, validatedURL: existingServer.server.url.toString()};
}
// If we can't reach the remote Site URL, there's probably a configuration issue
const remoteSiteURLInfo = await testRemoteServer(parsedSiteURL);
if (!remoteSiteURLInfo) {
return {status: URLValidationStatus.URLNotMatched, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteURL.toString()};
}
}
// Otherwise fix it for them and return
return {status: URLValidationStatus.URLUpdated, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteInfo.siteURL};
}
return {status: URLValidationStatus.OK, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteInfo.siteURL};
};
const testRemoteServer = async (parsedURL: URL) => {
const server = new MattermostServer({name: 'temp', url: parsedURL.toString()}, false);
const serverInfo = new ServerInfo(server);
try {
const remoteInfo = await serverInfo.fetchRemoteInfo();
return remoteInfo;
} catch (error) {
return undefined;
}
};

View File

@@ -88,6 +88,7 @@ import {
GET_ORDERED_SERVERS,
GET_ORDERED_TABS_FOR_SERVER,
SERVERS_UPDATE,
VALIDATE_SERVER_URL,
} from 'common/communication';
console.log('Preload initialized');
@@ -135,6 +136,7 @@ contextBridge.exposeInMainWorld('desktop', {
getOrderedServers: () => ipcRenderer.invoke(GET_ORDERED_SERVERS),
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId),
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
getVersion: () => ipcRenderer.invoke('get-app-version'),

View File

@@ -49,6 +49,7 @@ export class ServerInfo {
private onGetConfig = (data: ClientConfig) => {
this.remoteInfo.serverVersion = data.Version;
this.remoteInfo.siteURL = data.SiteURL;
this.remoteInfo.siteName = data.SiteName;
this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.BuildBoards === 'true';
}

View File

@@ -1,14 +1,13 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useCallback, useEffect} from 'react';
import React, {useState, useCallback, useEffect, useRef} from 'react';
import {useIntl, FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {UniqueServer} from 'types/config';
import {isValidURL, parseURL} from 'common/utils/url';
import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants';
import {MODAL_TRANSITION_TIMEOUT, URLValidationStatus} from 'common/utils/constants';
import womanLaptop from 'renderer/assets/svg/womanLaptop.svg';
@@ -22,7 +21,6 @@ import 'renderer/css/components/ConfigureServer.scss';
import 'renderer/css/components/LoadingScreen.css';
type ConfigureServerProps = {
currentServers: UniqueServer[];
server?: UniqueServer;
mobileView?: boolean;
darkMode?: boolean;
@@ -36,7 +34,6 @@ type ConfigureServerProps = {
};
function ConfigureServer({
currentServers,
server,
mobileView,
darkMode,
@@ -56,34 +53,61 @@ function ConfigureServer({
id,
} = server || {};
const mounted = useRef(false);
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
const [name, setName] = useState(prevName || '');
const [url, setUrl] = useState(prevURL || '');
const [nameError, setNameError] = useState('');
const [urlError, setURLError] = useState('');
const [urlError, setURLError] = useState<{type: STATUS; value: string}>();
const [showContent, setShowContent] = useState(false);
const [waiting, setWaiting] = useState(false);
const canSave = name && url && !nameError && !urlError;
const [validating, setValidating] = useState(false);
const validationTimestamp = useRef<number>();
const validationTimeout = useRef<NodeJS.Timeout>();
const editing = useRef(false);
const canSave = name && url && !nameError && !validating && urlError && urlError.type !== STATUS.ERROR;
useEffect(() => {
setTransition('inFromRight');
setShowContent(true);
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const checkProtocolInURL = (checkURL: string): Promise<string> => {
if (isValidURL(checkURL)) {
return Promise.resolve(checkURL);
const fetchValidationResult = (urlToValidate: string) => {
setValidating(true);
setURLError({
type: STATUS.INFO,
value: formatMessage({id: 'renderer.components.configureServer.url.validating', defaultMessage: 'Validating...'}),
});
const requestTime = Date.now();
validationTimestamp.current = requestTime;
validateURL(urlToValidate).then(({validatedURL, serverName, message}) => {
if (editing.current) {
setValidating(false);
setURLError(undefined);
return;
}
return window.desktop.modals.pingDomain(checkURL).
then((result: string) => {
const newURL = `${result}://${checkURL}`;
setUrl(newURL);
return newURL;
}).
catch(() => {
console.error(`Could not ping url: ${checkURL}`);
return checkURL;
if (!validationTimestamp.current || requestTime < validationTimestamp.current) {
return;
}
if (validatedURL) {
setUrl(validatedURL);
}
if (serverName) {
setName((prev) => {
return prev.length ? prev : serverName;
});
}
if (message) {
setTransition(undefined);
setURLError(message);
}
setValidating(false);
});
};
@@ -97,46 +121,76 @@ function ConfigureServer({
});
}
if (currentServers.find(({name: existingName}) => existingName === newName)) {
return formatMessage({
id: 'renderer.components.newServerModal.error.serverNameExists',
defaultMessage: 'A server with the same name already exists.',
});
}
return '';
};
const validateURL = async (fullURL: string) => {
if (!fullURL) {
return formatMessage({
const validateURL = async (url: string) => {
let message;
const validationResult = await window.desktop.validateServerURL(url);
if (validationResult.validatedURL) {
setUrl(validationResult.validatedURL);
}
if (validationResult?.status === URLValidationStatus.Missing) {
message = {
type: STATUS.ERROR,
value: formatMessage({
id: 'renderer.components.newServerModal.error.urlRequired',
defaultMessage: 'URL is required.',
});
}),
};
}
if (!parseURL(fullURL)) {
return formatMessage({
if (validationResult?.status === URLValidationStatus.Invalid) {
message = {
type: STATUS.ERROR,
value: formatMessage({
id: 'renderer.components.newServerModal.error.urlIncorrectFormatting',
defaultMessage: 'URL is not formatted correctly.',
});
}),
};
}
if (!isValidURL(fullURL)) {
return formatMessage({
id: 'renderer.components.newServerModal.error.urlNeedsHttp',
defaultMessage: 'URL should start with http:// or https://.',
});
if (validationResult?.status === URLValidationStatus.Insecure) {
message = {
type: STATUS.WARNING,
value: formatMessage({id: 'renderer.components.configureServer.url.insecure', defaultMessage: 'Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.'}),
};
}
if (currentServers.find(({url: existingURL}) => parseURL(existingURL)?.toString === parseURL(fullURL)?.toString())) {
return formatMessage({
id: 'renderer.components.newServerModal.error.serverUrlExists',
defaultMessage: 'A server with the same URL already exists.',
});
if (validationResult?.status === URLValidationStatus.NotMattermost) {
message = {
type: STATUS.WARNING,
value: formatMessage({id: 'renderer.components.configureServer.url.notMattermost', defaultMessage: 'The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.'}),
};
}
return '';
if (validationResult?.status === URLValidationStatus.URLNotMatched) {
message = {
type: STATUS.WARNING,
value: formatMessage({id: 'renderer.components.configureServer.url.urlNotMatched', defaultMessage: 'The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}'}, {serverVersion: validationResult.serverVersion}),
};
}
if (validationResult?.status === URLValidationStatus.URLUpdated) {
message = {
type: STATUS.INFO,
value: formatMessage({id: 'renderer.components.configureServer.url.urlUpdated', defaultMessage: 'The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}'}, {serverVersion: validationResult.serverVersion}),
};
}
if (validationResult?.status === URLValidationStatus.OK) {
message = {
type: STATUS.SUCCESS,
value: formatMessage({id: 'renderer.components.configureServer.url.ok', defaultMessage: 'Server URL is valid. Server version: {serverVersion}'}, {serverVersion: validationResult.serverVersion}),
};
}
return {
validatedURL: validationResult.validatedURL,
serverName: validationResult.serverName,
message,
};
};
const handleNameOnChange = ({target: {value}}: React.ChangeEvent<HTMLInputElement>) => {
@@ -151,8 +205,18 @@ function ConfigureServer({
setUrl(value);
if (urlError) {
setURLError('');
setURLError(undefined);
}
editing.current = true;
clearTimeout(validationTimeout.current as unknown as number);
validationTimeout.current = setTimeout(() => {
if (!mounted.current) {
return;
}
editing.current = false;
fetchValidationResult(value);
}, 1000);
};
const handleOnSaveButtonClick = (e: React.MouseEvent) => {
@@ -183,21 +247,11 @@ function ConfigureServer({
return;
}
const fullURL = await checkProtocolInURL(url.trim());
const urlError = await validateURL(fullURL);
if (urlError) {
setTransition(undefined);
setURLError(urlError);
setWaiting(false);
return;
}
setTransition('outToLeft');
setTimeout(() => {
onConnect({
url: fullURL,
url,
name,
id,
});
@@ -269,7 +323,7 @@ function ConfigureServer({
/>
</div>
</div>
<div className={classNames('ConfigureServer__card', transition, {'with-error': nameError || urlError})}>
<div className={classNames('ConfigureServer__card', transition, {'with-error': nameError || urlError?.type === STATUS.ERROR})}>
<div
className='ConfigureServer__card-content'
onKeyDown={handleOnCardEnterKeyDown}
@@ -286,10 +340,7 @@ function ConfigureServer({
inputSize={SIZE.LARGE}
value={url}
onChange={handleURLOnChange}
customMessage={urlError ? ({
type: STATUS.ERROR,
value: urlError,
}) : ({
customMessage={urlError ?? ({
type: STATUS.INFO,
value: formatMessage({id: 'renderer.components.configureServer.url.info', defaultMessage: 'The URL of your Mattermost server'}),
})}
@@ -321,7 +372,10 @@ function ConfigureServer({
extraClasses='ConfigureServer__card-form-button'
saving={waiting}
onClick={handleOnSaveButtonClick}
defaultMessage={formatMessage({id: 'renderer.components.configureServer.connect.default', defaultMessage: 'Connect'})}
defaultMessage={urlError?.type === STATUS.WARNING ?
formatMessage({id: 'renderer.components.configureServer.connect.override', defaultMessage: 'Connect anyway'}) :
formatMessage({id: 'renderer.components.configureServer.connect.default', defaultMessage: 'Connect'})
}
savingMessage={formatMessage({id: 'renderer.components.configureServer.connect.saving', defaultMessage: 'Connecting…'})}
disabled={!canSave}
darkMode={darkMode}

View File

@@ -22,7 +22,7 @@ export enum SIZE {
}
export type CustomMessageInputType = {
type: 'info' | 'error' | 'warning' | 'success';
type: STATUS;
value: string;
} | null;

View File

@@ -3,18 +3,20 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText, Spinner} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {UniqueServer} from 'types/config';
import {URLValidationResult} from 'types/server';
import {isValidURL} from 'common/utils/url';
import {URLValidationStatus} from 'common/utils/constants';
import 'renderer/css/components/NewServerModal.scss';
type Props = {
onClose?: () => void;
onSave?: (server: UniqueServer) => void;
server?: UniqueServer;
currentServers?: UniqueServer[];
editMode?: boolean;
show?: boolean;
restoreFocus?: boolean;
@@ -29,11 +31,15 @@ type State = {
serverId?: string;
serverOrder: number;
saveStarted: boolean;
validationStarted: boolean;
validationResult?: URLValidationResult;
}
class NewServerModal extends React.PureComponent<Props, State> {
wasShown?: boolean;
serverUrlInputRef?: HTMLInputElement;
validationTimeout?: NodeJS.Timeout;
mounted: boolean;
static defaultProps = {
restoreFocus: true,
@@ -43,48 +49,37 @@ class NewServerModal extends React.PureComponent<Props, State> {
super(props);
this.wasShown = false;
this.mounted = false;
this.state = {
serverName: '',
serverUrl: '',
serverOrder: props.currentOrder || 0,
saveStarted: false,
validationStarted: false,
};
}
initializeOnShow() {
componentDidMount(): void {
this.mounted = true;
}
componentWillUnmount(): void {
this.mounted = false;
}
initializeOnShow = () => {
this.setState({
serverName: this.props.server ? this.props.server.name : '',
serverUrl: this.props.server ? this.props.server.url : '',
serverId: this.props.server?.id,
saveStarted: false,
validationStarted: false,
validationResult: undefined,
});
}
getServerNameValidationError() {
if (!this.state.saveStarted) {
return null;
if (this.props.editMode && this.props.server) {
this.validateServerURL(this.props.server.url);
}
if (this.props.currentServers) {
const currentServers = [...this.props.currentServers];
if (currentServers.find((server) => server.id !== this.state.serverId && server.name === this.state.serverName)) {
return (
<FormattedMessage
id='renderer.components.newServerModal.error.serverNameExists'
defaultMessage='A server with the same name already exists.'
/>
);
}
}
return this.state.serverName.length > 0 ? null : (
<FormattedMessage
id='renderer.components.newServerModal.error.nameRequired'
defaultMessage='Name is required.'
/>
);
}
getServerNameValidationState() {
return this.getServerNameValidationError() === null ? null : 'error';
}
handleServerNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -93,108 +88,205 @@ class NewServerModal extends React.PureComponent<Props, State> {
});
}
getServerUrlValidationError() {
if (!this.state.saveStarted) {
return null;
handleServerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const serverUrl = e.target.value;
this.setState({serverUrl, validationResult: undefined});
this.validateServerURL(serverUrl);
}
if (this.props.currentServers) {
const currentServers = [...this.props.currentServers];
if (currentServers.find((server) => server.id !== this.state.serverId && server.url === this.state.serverUrl)) {
validateServerURL = (serverUrl: string) => {
clearTimeout(this.validationTimeout as unknown as number);
this.validationTimeout = setTimeout(() => {
if (!this.mounted) {
return;
}
const currentTimeout = this.validationTimeout;
this.setState({validationStarted: true});
window.desktop.validateServerURL(serverUrl, this.props.server?.id).then((validationResult) => {
if (!this.mounted) {
return;
}
if (currentTimeout !== this.validationTimeout) {
return;
}
this.setState({validationResult, validationStarted: false, serverUrl: validationResult.validatedURL ?? serverUrl, serverName: this.state.serverName ? this.state.serverName : validationResult.serverName ?? ''});
});
}, 1000);
}
isServerURLErrored = () => {
return this.state.validationResult?.status === URLValidationStatus.Invalid ||
this.state.validationResult?.status === URLValidationStatus.Missing;
}
getServerURLMessage = () => {
if (this.state.validationStarted) {
return (
<FormattedMessage
id='renderer.components.newServerModal.error.serverUrlExists'
defaultMessage='A server with the same URL already exists.'
<div>
<Spinner
className='NewServerModal-validationSpinner'
animation='border'
size='sm'
/>
<FormattedMessage
id='renderer.components.newServerModal.validating'
defaultMessage='Validating...'
/>
</div>
);
}
if (!this.state.validationResult) {
return null;
}
if (this.state.serverUrl.length === 0) {
switch (this.state.validationResult?.status) {
case URLValidationStatus.Missing:
return (
<div
id='urlValidation'
className='error'
>
<i className='icon-close-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.error.urlRequired'
defaultMessage='URL is required.'
/>
</div>
);
}
if (!(/^https?:\/\/.*/).test(this.state.serverUrl.trim())) {
return (
<FormattedMessage
id='renderer.components.newServerModal.error.urlNeedsHttp'
defaultMessage='URL should start with http:// or https://.'
/>
);
}
if (!isValidURL(this.state.serverUrl.trim())) {
case URLValidationStatus.Invalid:
return (
<div
id='urlValidation'
className='error'
>
<i className='icon-close-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.error.urlIncorrectFormatting'
defaultMessage='URL is not formatted correctly.'
/>
</div>
);
}
return null;
}
getServerUrlValidationState() {
return this.getServerUrlValidationError() === null ? null : 'error';
}
handleServerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const serverUrl = e.target.value;
this.setState({serverUrl});
}
addProtocolToUrl = (serverUrl: string): Promise<void> => {
if (serverUrl.startsWith('http://') || serverUrl.startsWith('https://')) {
return Promise.resolve(undefined);
}
return window.desktop.modals.pingDomain(serverUrl).
then((result: string) => {
this.setState({serverUrl: `${result}://${this.state.serverUrl}`});
}).
catch(() => {
console.error(`Could not ping url: ${serverUrl}`);
});
}
getError() {
const nameError = this.getServerNameValidationError();
const urlError = this.getServerUrlValidationError();
if (nameError && urlError) {
case URLValidationStatus.URLExists:
return (
<>
{nameError}
<br/>
{urlError}
</>
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.error.serverUrlExists'
defaultMessage='A server named {serverName} with the same Site URL already exists.'
values={{serverName: this.state.validationResult.existingServerName}}
/>
</div>
);
case URLValidationStatus.Insecure:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.insecure'
defaultMessage='Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.'
/>
</div>
);
case URLValidationStatus.NotMattermost:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.notMattermost'
defaultMessage='The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.'
/>
</div>
);
case URLValidationStatus.URLNotMatched:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.urlNotMatched'
defaultMessage='The server URL does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}'
values={{serverVersion: this.state.validationResult.serverVersion}}
/>
</div>
);
case URLValidationStatus.URLUpdated:
return (
<div
id='urlValidation'
className='info'
>
<i className='icon-information-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.urlUpdated'
defaultMessage='The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}'
values={{serverVersion: this.state.validationResult.serverVersion}}
/>
</div>
);
}
return (
<div
id='urlValidation'
className='success'
>
<i className='icon-check-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.success.ok'
defaultMessage='Server URL is valid. Server version: {serverVersion}'
values={{serverVersion: this.state.validationResult.serverVersion}}
/>
</div>
);
}
getServerNameMessage = () => {
if (!this.state.serverName.length) {
return (
<div
id='nameValidation'
className='error'
>
<i className='icon-close-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.error.nameRequired'
defaultMessage='Name is required.'
/>
</div>
);
} else if (nameError) {
return nameError;
} else if (urlError) {
return urlError;
}
return null;
}
validateForm() {
return this.getServerNameValidationState() === null &&
this.getServerUrlValidationState() === null;
save = () => {
if (!this.state.validationResult) {
return;
}
if (this.isServerURLErrored()) {
return;
}
save = async () => {
await this.addProtocolToUrl(this.state.serverUrl);
this.setState({
saveStarted: true,
}, () => {
if (this.validateForm()) {
this.props.onSave?.({
url: this.state.serverUrl,
name: this.state.serverName,
id: this.state.serverId,
});
}
});
}
@@ -291,7 +383,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
this.props.setInputRef(ref);
}
}}
isInvalid={Boolean(this.getServerUrlValidationState())}
isInvalid={this.isServerURLErrored()}
autoFocus={true}
/>
<FormControl.Feedback/>
@@ -318,7 +410,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
}}
isInvalid={Boolean(this.getServerNameValidationState())}
isInvalid={!this.state.serverName.length}
/>
<FormControl.Feedback/>
<FormText className='NewServerModal-noBottomSpace'>
@@ -329,15 +421,15 @@ class NewServerModal extends React.PureComponent<Props, State> {
</FormText>
</FormGroup>
</form>
<div
className='NewServerModal-validation'
>
{this.getServerNameMessage()}
{this.getServerURLMessage()}
</div>
</Modal.Body>
<Modal.Footer>
<div
className='pull-left modal-error'
>
{this.getError()}
</div>
{this.props.onClose &&
<Button
id='cancelNewServerModal'
@@ -354,7 +446,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
<Button
id='saveNewServerModal'
onClick={this.save}
disabled={!this.validateForm()}
disabled={!this.state.serverName.length || !this.state.validationResult || this.isServerURLErrored()}
variant='primary'
>
{this.getSaveButtonLabel()}

View File

@@ -1,4 +0,0 @@
.NewServerModal-noBottomSpace {
padding-bottom: 0px;
margin-bottom: 0px;
}

View File

@@ -0,0 +1,38 @@
.NewServerModal-noBottomSpace {
padding-bottom: 0px;
margin-bottom: 0px;
}
.NewServerModal-validation {
margin-top: 8px;
margin-right: auto;
font-size: 12px;
display: flex;
flex-direction: column;
> div {
margin-top: 4px;
> span {
margin-left: 4px;
}
}
.error {
color: #d24b4e;
}
.warning {
color: #c79e3f;
}
.success {
color: #06d6a0;
}
}
.NewServerModal-validationSpinner {
width: 0.75rem;
height: 0.75rem;
margin-left: 2px;
margin-right: 4px;
}

View File

@@ -2,7 +2,6 @@
@import url("HoveringURL.css");
@import url("MainPage.css");
@import url("MattermostView.css");
@import url("NewServerModal.css");
@import url("PermissionRequestDialog.css");
@import url("TabBar.css");
@import url("UpdaterPage.css");

View File

@@ -1,3 +1,7 @@
@import url("components/index.css");
@import url("fonts.css");
@import '~@mattermost/compass-icons/css/compass-icons.css';
body {
background-color: transparent;
}

View File

@@ -17,11 +17,6 @@ import setupDarkMode from '../darkMode';
setupDarkMode();
type ModalInfo = {
server: UniqueServer;
currentServers: UniqueServer[];
};
const onClose = () => {
window.desktop.modals.cancelModal();
};
@@ -32,12 +27,10 @@ const onSave = (data: UniqueServer) => {
const EditServerModalWrapper: React.FC = () => {
const [server, setServer] = useState<UniqueServer>();
const [currentServers, setCurrentServers] = useState<UniqueServer[]>();
useEffect(() => {
window.desktop.modals.getModalInfo<ModalInfo>().then(({server, currentServers}) => {
window.desktop.modals.getModalInfo<UniqueServer>().then((server) => {
setServer(server);
setCurrentServers(currentServers);
});
}, []);
@@ -49,7 +42,6 @@ const EditServerModalWrapper: React.FC = () => {
editMode={true}
show={Boolean(server)}
server={server}
currentServers={currentServers}
/>
</IntlProvider>
);

View File

@@ -27,15 +27,11 @@ const onSave = (data: UniqueServer) => {
const NewServerModalWrapper: React.FC = () => {
const [unremoveable, setUnremovable] = useState<boolean>();
const [currentServers, setCurrentServers] = useState<UniqueServer[]>();
useEffect(() => {
window.desktop.modals.isModalUncloseable().then((uncloseable) => {
setUnremovable(uncloseable);
});
window.desktop.modals.getModalInfo<UniqueServer[]>().then((servers) => {
setCurrentServers(servers);
});
}, []);
return (
@@ -45,7 +41,6 @@ const NewServerModalWrapper: React.FC = () => {
onSave={onSave}
editMode={false}
show={true}
currentServers={currentServers}
/>
</IntlProvider>
);

View File

@@ -23,7 +23,6 @@ const WelcomeScreenModalWrapper = () => {
const [darkMode, setDarkMode] = useState(false);
const [getStarted, setGetStarted] = useState(false);
const [mobileView, setMobileView] = useState(false);
const [currentServers, setCurrentServers] = useState<UniqueServer[]>([]);
const handleWindowResize = () => {
setMobileView(window.innerWidth < MOBILE_SCREEN_WIDTH);
@@ -38,10 +37,6 @@ const WelcomeScreenModalWrapper = () => {
setDarkMode(result);
});
window.desktop.modals.getModalInfo<UniqueServer[]>().then((result) => {
setCurrentServers(result);
});
handleWindowResize();
window.addEventListener('resize', handleWindowResize);
@@ -60,7 +55,6 @@ const WelcomeScreenModalWrapper = () => {
<ConfigureServer
mobileView={mobileView}
darkMode={darkMode}
currentServers={currentServers}
onConnect={onConnect}
/>
) : (

View File

@@ -3,6 +3,7 @@
export type RemoteInfo = {
serverVersion?: string;
siteName?: string;
siteURL?: string;
hasFocalboard?: boolean;
hasPlaybooks?: boolean;
@@ -11,5 +12,14 @@ export type RemoteInfo = {
export type ClientConfig = {
Version: string;
SiteURL: string;
SiteName: string;
BuildBoards: string;
}
export type URLValidationResult = {
status: string;
validatedURL?: string;
existingServerName?: string;
serverVersion?: string;
serverName?: string;
}

View File

@@ -8,6 +8,7 @@ import {Language} from '../../i18n/i18n';
import {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer} from './config';
import {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
import {SaveQueueItem} from './settings';
import {URLValidationResult} from './server';
declare global {
interface Window {
@@ -56,6 +57,7 @@ declare global {
getOrderedServers: () => Promise<UniqueServer[]>;
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
onUpdateServers: (listener: () => void) => void;
validateServerURL: (url: string, currentId?: string) => Promise<URLValidationResult>;
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
getVersion: () => Promise<{name: string; version: string}>;