[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:
@@ -53,42 +53,31 @@ describe('Add Server Modal', function desc() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('MM-T4389 Invalid messages', () => {
|
describe('MM-T4389 Invalid messages', () => {
|
||||||
it('MM-T4389_1 should not be valid if no server name or URL has been set', async () => {
|
it('MM-T4389_1 should not be valid and save should be disabled if no server name or URL has been set', async () => {
|
||||||
await newServerView.click('#saveNewServerModal');
|
const existing = await newServerView.isVisible('#nameValidation.error');
|
||||||
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');
|
|
||||||
existing.should.be.true;
|
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('#serverNameInput', 'some-new-server');
|
||||||
await newServerView.type('#serverUrlInput', config.teams[0].url);
|
await newServerView.type('#serverUrlInput', config.teams[0].url);
|
||||||
await newServerView.click('#saveNewServerModal');
|
await newServerView.waitForSelector('#urlValidation.warning');
|
||||||
const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
|
const existing = await newServerView.isVisible('#urlValidation.warning');
|
||||||
existing.should.be.true;
|
existing.should.be.true;
|
||||||
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
|
(disabled === '').should.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Valid server name', async () => {
|
describe('Valid server name', async () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await newServerView.type('#serverNameInput', 'TestServer');
|
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 () => {
|
it('MM-T4389_2 Name should not be marked invalid, but should not be able to save', async () => {
|
||||||
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
|
await newServerView.waitForSelector('#nameValidation.error', {state: 'detached'});
|
||||||
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
|
|
||||||
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
existingName.should.be.false;
|
|
||||||
existingUrl.should.be.true;
|
|
||||||
(disabled === '').should.be.true;
|
(disabled === '').should.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -96,12 +85,11 @@ describe('Add Server Modal', function desc() {
|
|||||||
describe('Valid server url', () => {
|
describe('Valid server url', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await newServerView.type('#serverUrlInput', 'http://example.org');
|
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 () => {
|
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('#urlValidation.error');
|
||||||
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
|
const existingName = await newServerView.isVisible('#nameValidation.error');
|
||||||
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
existingName.should.be.true;
|
existingName.should.be.true;
|
||||||
existingUrl.should.be.false;
|
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 () => {
|
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.type('#serverUrlInput', 'superInvalid url');
|
||||||
await newServerView.click('#saveNewServerModal');
|
await newServerView.waitForSelector('#urlValidation.error');
|
||||||
const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
|
const existing = await newServerView.isVisible('#urlValidation.error');
|
||||||
existing.should.be.true;
|
existing.should.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,6 +109,7 @@ describe('Add Server Modal', function desc() {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await newServerView.type('#serverUrlInput', 'http://example.org');
|
await newServerView.type('#serverUrlInput', 'http://example.org');
|
||||||
await newServerView.type('#serverNameInput', 'TestServer');
|
await newServerView.type('#serverNameInput', 'TestServer');
|
||||||
|
await newServerView.waitForSelector('#urlValidation.warning');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to click add', async () => {
|
it('should be possible to click add', async () => {
|
||||||
|
@@ -49,7 +49,8 @@ describe('Configure Server Modal', function desc() {
|
|||||||
|
|
||||||
it('MM-T5117 should be valid if display name and URL are set', async () => {
|
it('MM-T5117 should be valid if display name and URL are set', async () => {
|
||||||
await configureServerModal.type('#input_name', 'TestServer');
|
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');
|
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
|
||||||
(connectButtonDisabled === '').should.be.false;
|
(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 () => {
|
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_name', 'TestServer');
|
||||||
await configureServerModal.type('#input_url', 'lorem.ipsum.dolor.sit.amet');
|
await configureServerModal.type('#input_url', '!@#$%^&*()');
|
||||||
|
await configureServerModal.waitForSelector('#customMessage_url.Input___error');
|
||||||
await configureServerModal.click('#connectConfigureServer');
|
|
||||||
|
|
||||||
await asyncSleep(1000);
|
|
||||||
|
|
||||||
const errorClass = await configureServerModal.getAttribute('#customMessage_url', 'class');
|
const errorClass = await configureServerModal.getAttribute('#customMessage_url', 'class');
|
||||||
errorClass.should.contain('Input___error');
|
errorClass.should.contain('Input___error');
|
||||||
|
@@ -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 () => {
|
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.type('#serverUrlInput', 'superInvalid url');
|
||||||
await editServerView.click('#saveNewServerModal');
|
await editServerView.waitForSelector('#urlValidation.error');
|
||||||
const existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
|
const existing = await editServerView.isVisible('#urlValidation.error');
|
||||||
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');
|
|
||||||
existing.should.be.true;
|
existing.should.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
15
i18n/en.json
15
i18n/en.json
@@ -121,13 +121,20 @@
|
|||||||
"renderer.components.autoSaveIndicator.saving": "Saving...",
|
"renderer.components.autoSaveIndicator.saving": "Saving...",
|
||||||
"renderer.components.configureServer.cardtitle": "Enter your server details",
|
"renderer.components.configureServer.cardtitle": "Enter your server details",
|
||||||
"renderer.components.configureServer.connect.default": "Connect",
|
"renderer.components.configureServer.connect.default": "Connect",
|
||||||
|
"renderer.components.configureServer.connect.override": "Connect anyway",
|
||||||
"renderer.components.configureServer.connect.saving": "Connecting…",
|
"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.info": "The name that will be displayed in your server list",
|
||||||
"renderer.components.configureServer.name.placeholder": "Server display name",
|
"renderer.components.configureServer.name.placeholder": "Server display name",
|
||||||
"renderer.components.configureServer.subtitle": "Set up your first server to connect to your<br></br>team’s communication hub",
|
"renderer.components.configureServer.subtitle": "Set up your first server to connect to your<br></br>team’s communication hub",
|
||||||
"renderer.components.configureServer.title": "Let’s connect to a server",
|
"renderer.components.configureServer.title": "Let’s connect to a server",
|
||||||
"renderer.components.configureServer.url.info": "The URL of your Mattermost 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.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.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.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:",
|
"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.contextMenu.ariaLabel": "Context menu",
|
||||||
"renderer.components.mainPage.titleBar": "Mattermost",
|
"renderer.components.mainPage.titleBar": "Mattermost",
|
||||||
"renderer.components.newServerModal.error.nameRequired": "Name is required.",
|
"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.serverUrlExists": "A server with the same URL already exists.",
|
||||||
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
|
"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.error.urlRequired": "URL is required.",
|
||||||
"renderer.components.newServerModal.serverDisplayName": "Server Display Name",
|
"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.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
|
||||||
"renderer.components.newServerModal.serverURL": "Server URL",
|
"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.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.add": "Add Server",
|
||||||
"renderer.components.newServerModal.title.edit": "Edit 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.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.confirm": "Confirm you wish to remove the {serverName} server?",
|
||||||
"renderer.components.removeServerModal.title": "Remove Server",
|
"renderer.components.removeServerModal.title": "Remove Server",
|
||||||
|
@@ -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_CREATED = 'main-window-created';
|
||||||
export const MAIN_WINDOW_RESIZED = 'main-window-resized';
|
export const MAIN_WINDOW_RESIZED = 'main-window-resized';
|
||||||
export const MAIN_WINDOW_FOCUSED = 'main-window-focused';
|
export const MAIN_WINDOW_FOCUSED = 'main-window-focused';
|
||||||
|
|
||||||
|
export const VALIDATE_SERVER_URL = 'validate-server-url';
|
||||||
|
@@ -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_MAX_ITEMS = 50;
|
||||||
export const DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT = 4000; // 4 sec
|
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)
|
// supported custom login paths (oath, saml)
|
||||||
export const customLoginRegexPaths = [
|
export const customLoginRegexPaths = [
|
||||||
/^\/oauth\/authorize$/i,
|
/^\/oauth\/authorize$/i,
|
||||||
|
@@ -41,6 +41,7 @@ import {
|
|||||||
WINDOW_MINIMIZE,
|
WINDOW_MINIMIZE,
|
||||||
WINDOW_RESTORE,
|
WINDOW_RESTORE,
|
||||||
DOUBLE_CLICK_ON_WINDOW,
|
DOUBLE_CLICK_ON_WINDOW,
|
||||||
|
VALIDATE_SERVER_URL,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {isTrustedURL, parseURL} from 'common/utils/url';
|
import {isTrustedURL, parseURL} from 'common/utils/url';
|
||||||
@@ -97,6 +98,7 @@ import {
|
|||||||
handleEditServerModal,
|
handleEditServerModal,
|
||||||
handleNewServerModal,
|
handleNewServerModal,
|
||||||
handleRemoveServerModal,
|
handleRemoveServerModal,
|
||||||
|
handleServerURLValidation,
|
||||||
switchServer,
|
switchServer,
|
||||||
} from './servers';
|
} from './servers';
|
||||||
import {
|
import {
|
||||||
@@ -287,6 +289,7 @@ function initializeInterCommunicationEventListeners() {
|
|||||||
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
|
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
|
||||||
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
|
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
|
||||||
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
|
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
|
||||||
|
ipcMain.handle(VALIDATE_SERVER_URL, handleServerURLValidation);
|
||||||
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
|
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
|
||||||
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
|
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
|
||||||
ipcMain.on(START_UPGRADE, handleStartUpgrade);
|
ipcMain.on(START_UPGRADE, handleStartUpgrade);
|
||||||
|
@@ -47,7 +47,6 @@ describe('main/app/intercom', () => {
|
|||||||
getLocalPreload.mockReturnValue('/some/preload.js');
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
MainWindow.get.mockReturnValue({});
|
MainWindow.get.mockReturnValue({});
|
||||||
|
|
||||||
ServerManager.getAllServers.mockReturnValue([]);
|
|
||||||
ServerManager.hasServers.mockReturnValue(false);
|
ServerManager.hasServers.mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ describe('main/app/intercom', () => {
|
|||||||
ModalManager.addModal.mockReturnValue(promise);
|
ModalManager.addModal.mockReturnValue(promise);
|
||||||
|
|
||||||
handleWelcomeScreenModal();
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -97,7 +97,7 @@ export function handleWelcomeScreenModal() {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
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) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const newServer = ServerManager.addServer(data);
|
const newServer = ServerManager.addServer(data);
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
import ServerManager from 'common/servers/serverManager';
|
import ServerManager from 'common/servers/serverManager';
|
||||||
|
import {URLValidationStatus} from 'common/utils/constants';
|
||||||
import {getDefaultViewsForConfigServer} from 'common/views/View';
|
import {getDefaultViewsForConfigServer} from 'common/views/View';
|
||||||
|
|
||||||
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
import ModalManager from 'main/views/modalManager';
|
import ModalManager from 'main/views/modalManager';
|
||||||
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
@@ -28,10 +31,17 @@ jest.mock('common/servers/serverManager', () => ({
|
|||||||
getView: jest.fn(),
|
getView: jest.fn(),
|
||||||
getLastActiveTabForServer: jest.fn(),
|
getLastActiveTabForServer: jest.fn(),
|
||||||
getServerLog: jest.fn(),
|
getServerLog: jest.fn(),
|
||||||
|
lookupViewByURL: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('common/servers/MattermostServer', () => ({
|
||||||
|
MattermostServer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('common/views/View', () => ({
|
jest.mock('common/views/View', () => ({
|
||||||
getDefaultViewsForConfigServer: jest.fn(),
|
getDefaultViewsForConfigServer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/server/serverInfo', () => ({
|
||||||
|
ServerInfo: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/views/modalManager', () => ({
|
jest.mock('main/views/modalManager', () => ({
|
||||||
addModal: jest.fn(),
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,18 +1,23 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {IpcMainEvent, ipcMain} from 'electron';
|
import {IpcMainEvent, IpcMainInvokeEvent, ipcMain} from 'electron';
|
||||||
|
|
||||||
import {UniqueServer, Server} from 'types/config';
|
import {UniqueServer, Server} from 'types/config';
|
||||||
|
import {URLValidationResult} from 'types/server';
|
||||||
|
|
||||||
import {UPDATE_SHORTCUT_MENU} from 'common/communication';
|
import {UPDATE_SHORTCUT_MENU} from 'common/communication';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
import ServerManager from 'common/servers/serverManager';
|
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 ViewManager from 'main/views/viewManager';
|
||||||
import ModalManager from 'main/views/modalManager';
|
import ModalManager from 'main/views/modalManager';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
||||||
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
|
|
||||||
const log = new Logger('App.Servers');
|
const log = new Logger('App.Servers');
|
||||||
|
|
||||||
@@ -49,7 +54,7 @@ export const handleNewServerModal = () => {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
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) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const newServer = ServerManager.addServer(data);
|
const newServer = ServerManager.addServer(data);
|
||||||
@@ -80,14 +85,11 @@ export const handleEditServerModal = (e: IpcMainEvent, id: string) => {
|
|||||||
if (!server) {
|
if (!server) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = ModalManager.addModal<{currentServers: UniqueServer[]; server: UniqueServer}, Server>(
|
const modalPromise = ModalManager.addModal<UniqueServer, Server>(
|
||||||
'editServer',
|
'editServer',
|
||||||
html,
|
html,
|
||||||
preload,
|
preload,
|
||||||
{
|
server.toUniqueServer(),
|
||||||
currentServers: ServerManager.getAllServers().map((server) => server.toUniqueServer()),
|
|
||||||
server: server.toUniqueServer(),
|
|
||||||
},
|
|
||||||
mainWindow);
|
mainWindow);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => {
|
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');
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -88,6 +88,7 @@ import {
|
|||||||
GET_ORDERED_SERVERS,
|
GET_ORDERED_SERVERS,
|
||||||
GET_ORDERED_TABS_FOR_SERVER,
|
GET_ORDERED_TABS_FOR_SERVER,
|
||||||
SERVERS_UPDATE,
|
SERVERS_UPDATE,
|
||||||
|
VALIDATE_SERVER_URL,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
console.log('Preload initialized');
|
console.log('Preload initialized');
|
||||||
@@ -135,6 +136,7 @@ contextBridge.exposeInMainWorld('desktop', {
|
|||||||
getOrderedServers: () => ipcRenderer.invoke(GET_ORDERED_SERVERS),
|
getOrderedServers: () => ipcRenderer.invoke(GET_ORDERED_SERVERS),
|
||||||
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
|
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
|
||||||
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
|
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
|
||||||
|
validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId),
|
||||||
|
|
||||||
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
|
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
|
||||||
getVersion: () => ipcRenderer.invoke('get-app-version'),
|
getVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
@@ -49,6 +49,7 @@ export class ServerInfo {
|
|||||||
private onGetConfig = (data: ClientConfig) => {
|
private onGetConfig = (data: ClientConfig) => {
|
||||||
this.remoteInfo.serverVersion = data.Version;
|
this.remoteInfo.serverVersion = data.Version;
|
||||||
this.remoteInfo.siteURL = data.SiteURL;
|
this.remoteInfo.siteURL = data.SiteURL;
|
||||||
|
this.remoteInfo.siteName = data.SiteName;
|
||||||
this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.BuildBoards === 'true';
|
this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.BuildBoards === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {useIntl, FormattedMessage} from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {UniqueServer} from 'types/config';
|
import {UniqueServer} from 'types/config';
|
||||||
|
|
||||||
import {isValidURL, parseURL} from 'common/utils/url';
|
import {MODAL_TRANSITION_TIMEOUT, URLValidationStatus} from 'common/utils/constants';
|
||||||
import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants';
|
|
||||||
|
|
||||||
import womanLaptop from 'renderer/assets/svg/womanLaptop.svg';
|
import womanLaptop from 'renderer/assets/svg/womanLaptop.svg';
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ import 'renderer/css/components/ConfigureServer.scss';
|
|||||||
import 'renderer/css/components/LoadingScreen.css';
|
import 'renderer/css/components/LoadingScreen.css';
|
||||||
|
|
||||||
type ConfigureServerProps = {
|
type ConfigureServerProps = {
|
||||||
currentServers: UniqueServer[];
|
|
||||||
server?: UniqueServer;
|
server?: UniqueServer;
|
||||||
mobileView?: boolean;
|
mobileView?: boolean;
|
||||||
darkMode?: boolean;
|
darkMode?: boolean;
|
||||||
@@ -36,7 +34,6 @@ type ConfigureServerProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function ConfigureServer({
|
function ConfigureServer({
|
||||||
currentServers,
|
|
||||||
server,
|
server,
|
||||||
mobileView,
|
mobileView,
|
||||||
darkMode,
|
darkMode,
|
||||||
@@ -56,34 +53,61 @@ function ConfigureServer({
|
|||||||
id,
|
id,
|
||||||
} = server || {};
|
} = server || {};
|
||||||
|
|
||||||
|
const mounted = useRef(false);
|
||||||
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
|
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
|
||||||
const [name, setName] = useState(prevName || '');
|
const [name, setName] = useState(prevName || '');
|
||||||
const [url, setUrl] = useState(prevURL || '');
|
const [url, setUrl] = useState(prevURL || '');
|
||||||
const [nameError, setNameError] = useState('');
|
const [nameError, setNameError] = useState('');
|
||||||
const [urlError, setURLError] = useState('');
|
const [urlError, setURLError] = useState<{type: STATUS; value: string}>();
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
const [waiting, setWaiting] = 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(() => {
|
useEffect(() => {
|
||||||
setTransition('inFromRight');
|
setTransition('inFromRight');
|
||||||
setShowContent(true);
|
setShowContent(true);
|
||||||
|
mounted.current = true;
|
||||||
|
return () => {
|
||||||
|
mounted.current = false;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const checkProtocolInURL = (checkURL: string): Promise<string> => {
|
const fetchValidationResult = (urlToValidate: string) => {
|
||||||
if (isValidURL(checkURL)) {
|
setValidating(true);
|
||||||
return Promise.resolve(checkURL);
|
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).
|
if (!validationTimestamp.current || requestTime < validationTimestamp.current) {
|
||||||
then((result: string) => {
|
return;
|
||||||
const newURL = `${result}://${checkURL}`;
|
}
|
||||||
setUrl(newURL);
|
if (validatedURL) {
|
||||||
return newURL;
|
setUrl(validatedURL);
|
||||||
}).
|
}
|
||||||
catch(() => {
|
if (serverName) {
|
||||||
console.error(`Could not ping url: ${checkURL}`);
|
setName((prev) => {
|
||||||
return checkURL;
|
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 '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateURL = async (fullURL: string) => {
|
const validateURL = async (url: string) => {
|
||||||
if (!fullURL) {
|
let message;
|
||||||
return formatMessage({
|
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',
|
id: 'renderer.components.newServerModal.error.urlRequired',
|
||||||
defaultMessage: 'URL is required.',
|
defaultMessage: 'URL is required.',
|
||||||
});
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parseURL(fullURL)) {
|
if (validationResult?.status === URLValidationStatus.Invalid) {
|
||||||
return formatMessage({
|
message = {
|
||||||
|
type: STATUS.ERROR,
|
||||||
|
value: formatMessage({
|
||||||
id: 'renderer.components.newServerModal.error.urlIncorrectFormatting',
|
id: 'renderer.components.newServerModal.error.urlIncorrectFormatting',
|
||||||
defaultMessage: 'URL is not formatted correctly.',
|
defaultMessage: 'URL is not formatted correctly.',
|
||||||
});
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidURL(fullURL)) {
|
if (validationResult?.status === URLValidationStatus.Insecure) {
|
||||||
return formatMessage({
|
message = {
|
||||||
id: 'renderer.components.newServerModal.error.urlNeedsHttp',
|
type: STATUS.WARNING,
|
||||||
defaultMessage: 'URL should start with http:// or https://.',
|
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())) {
|
if (validationResult?.status === URLValidationStatus.NotMattermost) {
|
||||||
return formatMessage({
|
message = {
|
||||||
id: 'renderer.components.newServerModal.error.serverUrlExists',
|
type: STATUS.WARNING,
|
||||||
defaultMessage: 'A server with the same URL already exists.',
|
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>) => {
|
const handleNameOnChange = ({target: {value}}: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -151,8 +205,18 @@ function ConfigureServer({
|
|||||||
setUrl(value);
|
setUrl(value);
|
||||||
|
|
||||||
if (urlError) {
|
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) => {
|
const handleOnSaveButtonClick = (e: React.MouseEvent) => {
|
||||||
@@ -183,21 +247,11 @@ function ConfigureServer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullURL = await checkProtocolInURL(url.trim());
|
|
||||||
const urlError = await validateURL(fullURL);
|
|
||||||
|
|
||||||
if (urlError) {
|
|
||||||
setTransition(undefined);
|
|
||||||
setURLError(urlError);
|
|
||||||
setWaiting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransition('outToLeft');
|
setTransition('outToLeft');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onConnect({
|
onConnect({
|
||||||
url: fullURL,
|
url,
|
||||||
name,
|
name,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
@@ -269,7 +323,7 @@ function ConfigureServer({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<div
|
||||||
className='ConfigureServer__card-content'
|
className='ConfigureServer__card-content'
|
||||||
onKeyDown={handleOnCardEnterKeyDown}
|
onKeyDown={handleOnCardEnterKeyDown}
|
||||||
@@ -286,10 +340,7 @@ function ConfigureServer({
|
|||||||
inputSize={SIZE.LARGE}
|
inputSize={SIZE.LARGE}
|
||||||
value={url}
|
value={url}
|
||||||
onChange={handleURLOnChange}
|
onChange={handleURLOnChange}
|
||||||
customMessage={urlError ? ({
|
customMessage={urlError ?? ({
|
||||||
type: STATUS.ERROR,
|
|
||||||
value: urlError,
|
|
||||||
}) : ({
|
|
||||||
type: STATUS.INFO,
|
type: STATUS.INFO,
|
||||||
value: formatMessage({id: 'renderer.components.configureServer.url.info', defaultMessage: 'The URL of your Mattermost server'}),
|
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'
|
extraClasses='ConfigureServer__card-form-button'
|
||||||
saving={waiting}
|
saving={waiting}
|
||||||
onClick={handleOnSaveButtonClick}
|
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…'})}
|
savingMessage={formatMessage({id: 'renderer.components.configureServer.connect.saving', defaultMessage: 'Connecting…'})}
|
||||||
disabled={!canSave}
|
disabled={!canSave}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
|
@@ -22,7 +22,7 @@ export enum SIZE {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CustomMessageInputType = {
|
export type CustomMessageInputType = {
|
||||||
type: 'info' | 'error' | 'warning' | 'success';
|
type: STATUS;
|
||||||
value: string;
|
value: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
|
@@ -3,18 +3,20 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
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 {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||||
|
|
||||||
import {UniqueServer} from 'types/config';
|
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 = {
|
type Props = {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onSave?: (server: UniqueServer) => void;
|
onSave?: (server: UniqueServer) => void;
|
||||||
server?: UniqueServer;
|
server?: UniqueServer;
|
||||||
currentServers?: UniqueServer[];
|
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
restoreFocus?: boolean;
|
restoreFocus?: boolean;
|
||||||
@@ -29,11 +31,15 @@ type State = {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
serverOrder: number;
|
serverOrder: number;
|
||||||
saveStarted: boolean;
|
saveStarted: boolean;
|
||||||
|
validationStarted: boolean;
|
||||||
|
validationResult?: URLValidationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewServerModal extends React.PureComponent<Props, State> {
|
class NewServerModal extends React.PureComponent<Props, State> {
|
||||||
wasShown?: boolean;
|
wasShown?: boolean;
|
||||||
serverUrlInputRef?: HTMLInputElement;
|
serverUrlInputRef?: HTMLInputElement;
|
||||||
|
validationTimeout?: NodeJS.Timeout;
|
||||||
|
mounted: boolean;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
restoreFocus: true,
|
restoreFocus: true,
|
||||||
@@ -43,48 +49,37 @@ class NewServerModal extends React.PureComponent<Props, State> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.wasShown = false;
|
this.wasShown = false;
|
||||||
|
this.mounted = false;
|
||||||
this.state = {
|
this.state = {
|
||||||
serverName: '',
|
serverName: '',
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
serverOrder: props.currentOrder || 0,
|
serverOrder: props.currentOrder || 0,
|
||||||
saveStarted: false,
|
saveStarted: false,
|
||||||
|
validationStarted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeOnShow() {
|
componentDidMount(): void {
|
||||||
|
this.mounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
this.mounted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeOnShow = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
serverName: this.props.server ? this.props.server.name : '',
|
serverName: this.props.server ? this.props.server.name : '',
|
||||||
serverUrl: this.props.server ? this.props.server.url : '',
|
serverUrl: this.props.server ? this.props.server.url : '',
|
||||||
serverId: this.props.server?.id,
|
serverId: this.props.server?.id,
|
||||||
saveStarted: false,
|
saveStarted: false,
|
||||||
|
validationStarted: false,
|
||||||
|
validationResult: undefined,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
getServerNameValidationError() {
|
if (this.props.editMode && this.props.server) {
|
||||||
if (!this.state.saveStarted) {
|
this.validateServerURL(this.props.server.url);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
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>) => {
|
handleServerNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -93,108 +88,205 @@ class NewServerModal extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getServerUrlValidationError() {
|
handleServerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!this.state.saveStarted) {
|
const serverUrl = e.target.value;
|
||||||
return null;
|
this.setState({serverUrl, validationResult: undefined});
|
||||||
|
this.validateServerURL(serverUrl);
|
||||||
}
|
}
|
||||||
if (this.props.currentServers) {
|
|
||||||
const currentServers = [...this.props.currentServers];
|
validateServerURL = (serverUrl: string) => {
|
||||||
if (currentServers.find((server) => server.id !== this.state.serverId && server.url === this.state.serverUrl)) {
|
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 (
|
return (
|
||||||
<FormattedMessage
|
<div>
|
||||||
id='renderer.components.newServerModal.error.serverUrlExists'
|
<Spinner
|
||||||
defaultMessage='A server with the same URL already exists.'
|
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 (
|
return (
|
||||||
|
<div
|
||||||
|
id='urlValidation'
|
||||||
|
className='error'
|
||||||
|
>
|
||||||
|
<i className='icon-close-circle'/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='renderer.components.newServerModal.error.urlRequired'
|
id='renderer.components.newServerModal.error.urlRequired'
|
||||||
defaultMessage='URL is required.'
|
defaultMessage='URL is required.'
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
case URLValidationStatus.Invalid:
|
||||||
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())) {
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
id='urlValidation'
|
||||||
|
className='error'
|
||||||
|
>
|
||||||
|
<i className='icon-close-circle'/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='renderer.components.newServerModal.error.urlIncorrectFormatting'
|
id='renderer.components.newServerModal.error.urlIncorrectFormatting'
|
||||||
defaultMessage='URL is not formatted correctly.'
|
defaultMessage='URL is not formatted correctly.'
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
case URLValidationStatus.URLExists:
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{nameError}
|
id='urlValidation'
|
||||||
<br/>
|
className='warning'
|
||||||
{urlError}
|
>
|
||||||
</>
|
<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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForm() {
|
save = () => {
|
||||||
return this.getServerNameValidationState() === null &&
|
if (!this.state.validationResult) {
|
||||||
this.getServerUrlValidationState() === null;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isServerURLErrored()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
save = async () => {
|
|
||||||
await this.addProtocolToUrl(this.state.serverUrl);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
saveStarted: true,
|
saveStarted: true,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (this.validateForm()) {
|
|
||||||
this.props.onSave?.({
|
this.props.onSave?.({
|
||||||
url: this.state.serverUrl,
|
url: this.state.serverUrl,
|
||||||
name: this.state.serverName,
|
name: this.state.serverName,
|
||||||
id: this.state.serverId,
|
id: this.state.serverId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +383,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
|
|||||||
this.props.setInputRef(ref);
|
this.props.setInputRef(ref);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isInvalid={Boolean(this.getServerUrlValidationState())}
|
isInvalid={this.isServerURLErrored()}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback/>
|
<FormControl.Feedback/>
|
||||||
@@ -318,7 +410,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
|
|||||||
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
|
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
isInvalid={Boolean(this.getServerNameValidationState())}
|
isInvalid={!this.state.serverName.length}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback/>
|
<FormControl.Feedback/>
|
||||||
<FormText className='NewServerModal-noBottomSpace'>
|
<FormText className='NewServerModal-noBottomSpace'>
|
||||||
@@ -329,15 +421,15 @@ class NewServerModal extends React.PureComponent<Props, State> {
|
|||||||
</FormText>
|
</FormText>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</form>
|
</form>
|
||||||
|
<div
|
||||||
|
className='NewServerModal-validation'
|
||||||
|
>
|
||||||
|
{this.getServerNameMessage()}
|
||||||
|
{this.getServerURLMessage()}
|
||||||
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<div
|
|
||||||
className='pull-left modal-error'
|
|
||||||
>
|
|
||||||
{this.getError()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.props.onClose &&
|
{this.props.onClose &&
|
||||||
<Button
|
<Button
|
||||||
id='cancelNewServerModal'
|
id='cancelNewServerModal'
|
||||||
@@ -354,7 +446,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
|
|||||||
<Button
|
<Button
|
||||||
id='saveNewServerModal'
|
id='saveNewServerModal'
|
||||||
onClick={this.save}
|
onClick={this.save}
|
||||||
disabled={!this.validateForm()}
|
disabled={!this.state.serverName.length || !this.state.validationResult || this.isServerURLErrored()}
|
||||||
variant='primary'
|
variant='primary'
|
||||||
>
|
>
|
||||||
{this.getSaveButtonLabel()}
|
{this.getSaveButtonLabel()}
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
.NewServerModal-noBottomSpace {
|
|
||||||
padding-bottom: 0px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
38
src/renderer/css/components/NewServerModal.scss
Normal file
38
src/renderer/css/components/NewServerModal.scss
Normal 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;
|
||||||
|
}
|
@@ -2,7 +2,6 @@
|
|||||||
@import url("HoveringURL.css");
|
@import url("HoveringURL.css");
|
||||||
@import url("MainPage.css");
|
@import url("MainPage.css");
|
||||||
@import url("MattermostView.css");
|
@import url("MattermostView.css");
|
||||||
@import url("NewServerModal.css");
|
|
||||||
@import url("PermissionRequestDialog.css");
|
@import url("PermissionRequestDialog.css");
|
||||||
@import url("TabBar.css");
|
@import url("TabBar.css");
|
||||||
@import url("UpdaterPage.css");
|
@import url("UpdaterPage.css");
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
|
@import url("components/index.css");
|
||||||
|
@import url("fonts.css");
|
||||||
|
@import '~@mattermost/compass-icons/css/compass-icons.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
@@ -17,11 +17,6 @@ import setupDarkMode from '../darkMode';
|
|||||||
|
|
||||||
setupDarkMode();
|
setupDarkMode();
|
||||||
|
|
||||||
type ModalInfo = {
|
|
||||||
server: UniqueServer;
|
|
||||||
currentServers: UniqueServer[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
window.desktop.modals.cancelModal();
|
window.desktop.modals.cancelModal();
|
||||||
};
|
};
|
||||||
@@ -32,12 +27,10 @@ const onSave = (data: UniqueServer) => {
|
|||||||
|
|
||||||
const EditServerModalWrapper: React.FC = () => {
|
const EditServerModalWrapper: React.FC = () => {
|
||||||
const [server, setServer] = useState<UniqueServer>();
|
const [server, setServer] = useState<UniqueServer>();
|
||||||
const [currentServers, setCurrentServers] = useState<UniqueServer[]>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.desktop.modals.getModalInfo<ModalInfo>().then(({server, currentServers}) => {
|
window.desktop.modals.getModalInfo<UniqueServer>().then((server) => {
|
||||||
setServer(server);
|
setServer(server);
|
||||||
setCurrentServers(currentServers);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -49,7 +42,6 @@ const EditServerModalWrapper: React.FC = () => {
|
|||||||
editMode={true}
|
editMode={true}
|
||||||
show={Boolean(server)}
|
show={Boolean(server)}
|
||||||
server={server}
|
server={server}
|
||||||
currentServers={currentServers}
|
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
|
@@ -27,15 +27,11 @@ const onSave = (data: UniqueServer) => {
|
|||||||
|
|
||||||
const NewServerModalWrapper: React.FC = () => {
|
const NewServerModalWrapper: React.FC = () => {
|
||||||
const [unremoveable, setUnremovable] = useState<boolean>();
|
const [unremoveable, setUnremovable] = useState<boolean>();
|
||||||
const [currentServers, setCurrentServers] = useState<UniqueServer[]>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.desktop.modals.isModalUncloseable().then((uncloseable) => {
|
window.desktop.modals.isModalUncloseable().then((uncloseable) => {
|
||||||
setUnremovable(uncloseable);
|
setUnremovable(uncloseable);
|
||||||
});
|
});
|
||||||
window.desktop.modals.getModalInfo<UniqueServer[]>().then((servers) => {
|
|
||||||
setCurrentServers(servers);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -45,7 +41,6 @@ const NewServerModalWrapper: React.FC = () => {
|
|||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
editMode={false}
|
editMode={false}
|
||||||
show={true}
|
show={true}
|
||||||
currentServers={currentServers}
|
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
|
@@ -23,7 +23,6 @@ const WelcomeScreenModalWrapper = () => {
|
|||||||
const [darkMode, setDarkMode] = useState(false);
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
const [getStarted, setGetStarted] = useState(false);
|
const [getStarted, setGetStarted] = useState(false);
|
||||||
const [mobileView, setMobileView] = useState(false);
|
const [mobileView, setMobileView] = useState(false);
|
||||||
const [currentServers, setCurrentServers] = useState<UniqueServer[]>([]);
|
|
||||||
|
|
||||||
const handleWindowResize = () => {
|
const handleWindowResize = () => {
|
||||||
setMobileView(window.innerWidth < MOBILE_SCREEN_WIDTH);
|
setMobileView(window.innerWidth < MOBILE_SCREEN_WIDTH);
|
||||||
@@ -38,10 +37,6 @@ const WelcomeScreenModalWrapper = () => {
|
|||||||
setDarkMode(result);
|
setDarkMode(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.desktop.modals.getModalInfo<UniqueServer[]>().then((result) => {
|
|
||||||
setCurrentServers(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
handleWindowResize();
|
handleWindowResize();
|
||||||
window.addEventListener('resize', handleWindowResize);
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
@@ -60,7 +55,6 @@ const WelcomeScreenModalWrapper = () => {
|
|||||||
<ConfigureServer
|
<ConfigureServer
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
currentServers={currentServers}
|
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
export type RemoteInfo = {
|
export type RemoteInfo = {
|
||||||
serverVersion?: string;
|
serverVersion?: string;
|
||||||
|
siteName?: string;
|
||||||
siteURL?: string;
|
siteURL?: string;
|
||||||
hasFocalboard?: boolean;
|
hasFocalboard?: boolean;
|
||||||
hasPlaybooks?: boolean;
|
hasPlaybooks?: boolean;
|
||||||
@@ -11,5 +12,14 @@ export type RemoteInfo = {
|
|||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
Version: string;
|
Version: string;
|
||||||
SiteURL: string;
|
SiteURL: string;
|
||||||
|
SiteName: string;
|
||||||
BuildBoards: string;
|
BuildBoards: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type URLValidationResult = {
|
||||||
|
status: string;
|
||||||
|
validatedURL?: string;
|
||||||
|
existingServerName?: string;
|
||||||
|
serverVersion?: string;
|
||||||
|
serverName?: string;
|
||||||
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import {Language} from '../../i18n/i18n';
|
|||||||
import {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer} from './config';
|
import {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer} from './config';
|
||||||
import {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
|
import {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
|
||||||
import {SaveQueueItem} from './settings';
|
import {SaveQueueItem} from './settings';
|
||||||
|
import {URLValidationResult} from './server';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -56,6 +57,7 @@ declare global {
|
|||||||
getOrderedServers: () => Promise<UniqueServer[]>;
|
getOrderedServers: () => Promise<UniqueServer[]>;
|
||||||
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
|
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
|
||||||
onUpdateServers: (listener: () => void) => void;
|
onUpdateServers: (listener: () => void) => void;
|
||||||
|
validateServerURL: (url: string, currentId?: string) => Promise<URLValidationResult>;
|
||||||
|
|
||||||
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
|
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
|
||||||
getVersion: () => Promise<{name: string; version: string}>;
|
getVersion: () => Promise<{name: string; version: string}>;
|
||||||
|
Reference in New Issue
Block a user