WIP: Ngrok Debugger Implementation (#2032)
* Basic ngrok debugger setup * Code cleanup * Renaming functions, test names, using shorthands wherever applicable * Pr feedback addressed
This commit is contained in:
Родитель
d496897b5a
Коммит
cea9a86b8f
|
@ -11,3 +11,4 @@ build/
|
||||||
.vscode/*
|
.vscode/*
|
||||||
chrome-driver-log.txt
|
chrome-driver-log.txt
|
||||||
.env
|
.env
|
||||||
|
/packages/**/package-lock.json
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -15,7 +15,8 @@
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"start": "run-s build:shared:dev webpackdevServer:dev",
|
"start": "run-s build:shared:dev webpackdevServer:dev",
|
||||||
"webpackdevServer:dev": "webpack-dev-server --mode development --hot --inline --progress --colors --content-base ./public",
|
"webpackdevServer:dev": "webpack-dev-server --mode development --hot --inline --progress --colors --content-base ./public",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"setupTestFrameworkScriptFile": "../../../../testSetup.js",
|
"setupTestFrameworkScriptFile": "../../../../testSetup.js",
|
||||||
|
@ -51,8 +52,8 @@
|
||||||
"@babel/preset-typescript": "^7.1.0",
|
"@babel/preset-typescript": "^7.1.0",
|
||||||
"@types/enzyme": "^3.1.10",
|
"@types/enzyme": "^3.1.10",
|
||||||
"@types/jest": "24.0.13",
|
"@types/jest": "24.0.13",
|
||||||
"@types/react": "~16.3.2",
|
"@types/react": "16.9.17",
|
||||||
"@types/react-dom": "^16.0.4",
|
"@types/react-dom": "16.9.4",
|
||||||
"@types/request": "^2.47.0",
|
"@types/request": "^2.47.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
|
@ -111,8 +112,8 @@
|
||||||
"botframework-webchat-core": "4.7.1",
|
"botframework-webchat-core": "4.7.1",
|
||||||
"eslint-plugin-react": "^7.12.3",
|
"eslint-plugin-react": "^7.12.3",
|
||||||
"markdown-it": "^8.4.2",
|
"markdown-it": "^8.4.2",
|
||||||
"react": "^16.8.6",
|
"react": "16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "16.8.6",
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.2.2",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
|
|
|
@ -39,11 +39,13 @@ export const CONTENT_TYPE_APP_SETTINGS = 'application/vnd.microsoft.bfemulator.d
|
||||||
export const CONTENT_TYPE_WELCOME_PAGE = 'application/vnd.microsoft.bfemulator.document.welcome';
|
export const CONTENT_TYPE_WELCOME_PAGE = 'application/vnd.microsoft.bfemulator.document.welcome';
|
||||||
export const CONTENT_TYPE_TRANSCRIPT = 'application/vnd.microsoft.bfemulator.document.transcript';
|
export const CONTENT_TYPE_TRANSCRIPT = 'application/vnd.microsoft.bfemulator.document.transcript';
|
||||||
export const CONTENT_TYPE_LIVE_CHAT = SharedConstants.ContentTypes.CONTENT_TYPE_LIVE_CHAT;
|
export const CONTENT_TYPE_LIVE_CHAT = SharedConstants.ContentTypes.CONTENT_TYPE_LIVE_CHAT;
|
||||||
|
export const CONTENT_TYPE_NGROK_DEBUGGER = SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER;
|
||||||
|
|
||||||
export const NAVBAR_BOT_EXPLORER = 'navbar.botExplorer';
|
export const NAVBAR_BOT_EXPLORER = 'navbar.botExplorer';
|
||||||
export const NAVBAR_SETTINGS = 'navbar.settings';
|
export const NAVBAR_SETTINGS = 'navbar.settings';
|
||||||
export const NAVBAR_NOTIFICATIONS = 'navbar.notifications';
|
export const NAVBAR_NOTIFICATIONS = 'navbar.notifications';
|
||||||
export const NAVBAR_RESOURCES = 'navbar:resources';
|
export const NAVBAR_RESOURCES = 'navbar.resources';
|
||||||
|
export const NAVBAR_NGROK_DEBUGGER = 'navbar.ngrokDebugger';
|
||||||
|
|
||||||
export const EDITOR_KEY_PRIMARY = 'primary';
|
export const EDITOR_KEY_PRIMARY = 'primary';
|
||||||
export const EDITOR_KEY_SECONDARY = 'secondary';
|
export const EDITOR_KEY_SECONDARY = 'secondary';
|
||||||
|
|
|
@ -39,3 +39,4 @@ export * from './savedBotUrlsActions';
|
||||||
export * from './updateActions';
|
export * from './updateActions';
|
||||||
export * from './userActions';
|
export * from './userActions';
|
||||||
export * from './windowStateActions';
|
export * from './windowStateActions';
|
||||||
|
export * from './ngrokTunnelActions';
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import {
|
||||||
|
updateNewTunnelInfo,
|
||||||
|
updateTunnelError,
|
||||||
|
updateTunnelStatus,
|
||||||
|
NgrokTunnelActions,
|
||||||
|
TunnelInfo,
|
||||||
|
TunnelError,
|
||||||
|
TunnelStatus,
|
||||||
|
} from './ngrokTunnelActions';
|
||||||
|
|
||||||
|
describe('Ngrok Tunnel Actions', () => {
|
||||||
|
it('should create an update tunnel info action', () => {
|
||||||
|
const payload: TunnelInfo = {
|
||||||
|
publicUrl: 'https://d1a2bf16.ngrok.io',
|
||||||
|
inspectUrl: 'http://127.0.0.1:4041',
|
||||||
|
logPath: 'ngrok.log',
|
||||||
|
postmanCollectionPath: 'postman.json',
|
||||||
|
};
|
||||||
|
const action = updateNewTunnelInfo(payload);
|
||||||
|
expect(action.type).toBe(NgrokTunnelActions.setDetails);
|
||||||
|
expect(action.payload).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a update tunnel error action', () => {
|
||||||
|
const payload: TunnelError = {
|
||||||
|
statusCode: 402,
|
||||||
|
errorMessage: 'Tunnel has expired',
|
||||||
|
};
|
||||||
|
const action = updateTunnelError(payload);
|
||||||
|
expect(action.type).toBe(NgrokTunnelActions.updateOnError);
|
||||||
|
expect(action.payload).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a tunnel status update action', () => {
|
||||||
|
const mockDate = new Date(1466424490000);
|
||||||
|
jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
|
||||||
|
const expectedStatus: TunnelStatus = TunnelStatus.Active;
|
||||||
|
const action = updateTunnelStatus(expectedStatus);
|
||||||
|
expect(action.type).toBe(NgrokTunnelActions.setStatus);
|
||||||
|
expect(action.payload.timestamp).toBe(new Date().toLocaleString());
|
||||||
|
expect(action.payload.status).toBe(expectedStatus);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
import { Action } from 'redux';
|
||||||
|
|
||||||
|
export enum NgrokTunnelActions {
|
||||||
|
setDetails = 'NgrokTunnel/SET_DETAILS',
|
||||||
|
updateOnError = 'NgrokTunnel/TUNNEL_ERROR',
|
||||||
|
setStatus = 'NgrokTunnel/STATUS_CHECK',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TunnelStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelInfo {
|
||||||
|
publicUrl: string;
|
||||||
|
inspectUrl: string;
|
||||||
|
logPath: string;
|
||||||
|
postmanCollectionPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelError {
|
||||||
|
statusCode: number;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelStatusAndTimestamp {
|
||||||
|
status: TunnelStatus;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NgrokTunnelAction<T> extends Action {
|
||||||
|
type: NgrokTunnelActions;
|
||||||
|
payload: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NgrokTunnelPayloadTypes = TunnelError | TunnelInfo | TunnelStatusAndTimestamp;
|
||||||
|
|
||||||
|
export function updateNewTunnelInfo(payload: TunnelInfo): NgrokTunnelAction<TunnelInfo> {
|
||||||
|
return {
|
||||||
|
type: NgrokTunnelActions.setDetails,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTunnelStatus(tunnelStatus: TunnelStatus): NgrokTunnelAction<TunnelStatusAndTimestamp> {
|
||||||
|
return {
|
||||||
|
type: NgrokTunnelActions.setStatus,
|
||||||
|
payload: {
|
||||||
|
status: tunnelStatus,
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTunnelError(payload: TunnelError): NgrokTunnelAction<TunnelError> {
|
||||||
|
return {
|
||||||
|
type: NgrokTunnelActions.updateOnError,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
|
@ -51,3 +51,4 @@ export * from './theme';
|
||||||
export * from './update';
|
export * from './update';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
export * from './windowState';
|
export * from './windowState';
|
||||||
|
export * from './ngrokTunnel';
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import {
|
||||||
|
TunnelStatus,
|
||||||
|
NgrokTunnelAction,
|
||||||
|
TunnelInfo,
|
||||||
|
NgrokTunnelPayloadTypes,
|
||||||
|
NgrokTunnelActions,
|
||||||
|
TunnelError,
|
||||||
|
TunnelStatusAndTs,
|
||||||
|
} from '../actions/ngrokTunnelActions';
|
||||||
|
|
||||||
|
import { ngrokTunnel, NgrokTunnelState } from './ngrokTunnel';
|
||||||
|
|
||||||
|
describe('Ngrok Tunnel reducer', () => {
|
||||||
|
const DEFAULT_STATE: NgrokTunnelState = {
|
||||||
|
inspectUrl: 'http://127.0.0.1:4040',
|
||||||
|
publicUrl: '',
|
||||||
|
logPath: '',
|
||||||
|
postmanCollectionPath: '',
|
||||||
|
errors: {} as TunnelError,
|
||||||
|
tunnelStatus: TunnelStatus.Inactive,
|
||||||
|
lastTunnelStatusCheckTS: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
const emptyAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
|
||||||
|
type: null,
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
ngrokTunnel({ ...DEFAULT_STATE }, emptyAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tunnel info should be set from payload', () => {
|
||||||
|
const payload: TunnelInfo = {
|
||||||
|
publicUrl: 'https://abc.io/',
|
||||||
|
inspectUrl: 'http://127.0.0.1:4003',
|
||||||
|
logPath: 'logger.txt',
|
||||||
|
postmanCollectionPath: 'postman.json',
|
||||||
|
};
|
||||||
|
const setDetailsAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
|
||||||
|
type: NgrokTunnelActions.setDetails,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
const startingState = { ...DEFAULT_STATE };
|
||||||
|
const endingState = ngrokTunnel(startingState, setDetailsAction);
|
||||||
|
|
||||||
|
expect(endingState.publicUrl).toBe(payload.publicUrl);
|
||||||
|
expect(endingState.logPath).toBe(payload.logPath);
|
||||||
|
expect(endingState.postmanCollectionPath).toBe(payload.postmanCollectionPath);
|
||||||
|
expect(endingState.inspectUrl).toBe(payload.inspectUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tunnel errors should be set from payload', () => {
|
||||||
|
const payload: TunnelError = {
|
||||||
|
statusCode: 422,
|
||||||
|
errorMessage: '<h1>Too many connections<h1>',
|
||||||
|
};
|
||||||
|
const updateErrorAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
|
||||||
|
type: NgrokTunnelActions.updateOnError,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
const startingState = { ...DEFAULT_STATE };
|
||||||
|
const endingState = ngrokTunnel(startingState, updateErrorAction);
|
||||||
|
|
||||||
|
expect(endingState.publicUrl).toBe(DEFAULT_STATE.publicUrl);
|
||||||
|
expect(endingState.errors.statusCode).toBe(payload.statusCode);
|
||||||
|
expect(endingState.errors.errorMessage).toBe(payload.errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tunnel status should be set from payload', () => {
|
||||||
|
const payload: TunnelStatusAndTs = {
|
||||||
|
status: TunnelStatus.Active,
|
||||||
|
ts: '12/27/2019, 1:30:00 PM',
|
||||||
|
};
|
||||||
|
const nextPayload: TunnelStatusAndTs = {
|
||||||
|
status: TunnelStatus.Error,
|
||||||
|
ts: '12/27/2019, 1:33:00 PM',
|
||||||
|
};
|
||||||
|
const actions: NgrokTunnelAction<NgrokTunnelPayloadTypes>[] = [
|
||||||
|
{
|
||||||
|
type: NgrokTunnelActions.setStatus,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NgrokTunnelActions.setStatus,
|
||||||
|
payload: nextPayload,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const startingState = { ...DEFAULT_STATE };
|
||||||
|
const transientState = ngrokTunnel(startingState, actions[0]);
|
||||||
|
expect(transientState.tunnelStatus).toBe(payload.status);
|
||||||
|
|
||||||
|
const finalState = ngrokTunnel(startingState, actions[1]);
|
||||||
|
expect(finalState.tunnelStatus).toBe(nextPayload.status);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import {
|
||||||
|
NgrokTunnelActions,
|
||||||
|
NgrokTunnelAction,
|
||||||
|
NgrokTunnelPayloadTypes,
|
||||||
|
TunnelError,
|
||||||
|
TunnelStatus,
|
||||||
|
TunnelStatusAndTimestamp,
|
||||||
|
} from '../actions/ngrokTunnelActions';
|
||||||
|
|
||||||
|
export interface NgrokTunnelState {
|
||||||
|
errors: TunnelError;
|
||||||
|
publicUrl: string;
|
||||||
|
inspectUrl: string;
|
||||||
|
logPath: string;
|
||||||
|
postmanCollectionPath: string;
|
||||||
|
tunnelStatus: TunnelStatus;
|
||||||
|
lastPingedTimestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STATE: NgrokTunnelState = {
|
||||||
|
inspectUrl: 'http://127.0.0.1:4040',
|
||||||
|
publicUrl: '',
|
||||||
|
logPath: '',
|
||||||
|
postmanCollectionPath: '',
|
||||||
|
errors: {} as TunnelError,
|
||||||
|
tunnelStatus: TunnelStatus.Inactive,
|
||||||
|
lastPingedTimestamp: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ngrokTunnel = (
|
||||||
|
state: NgrokTunnelState = DEFAULT_STATE,
|
||||||
|
action: NgrokTunnelAction<NgrokTunnelPayloadTypes>
|
||||||
|
): NgrokTunnelState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case NgrokTunnelActions.setDetails:
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NgrokTunnelActions.updateOnError:
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
errors: action.payload as TunnelError,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NgrokTunnelActions.setStatus:
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
tunnelStatus: (action.payload as TunnelStatusAndTimestamp).status,
|
||||||
|
lastPingedTimestamp: (action.payload as TunnelStatusAndTimestamp).timestamp,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
|
@ -73,6 +73,8 @@ import {
|
||||||
ResourcesState,
|
ResourcesState,
|
||||||
ThemeState,
|
ThemeState,
|
||||||
UpdateState,
|
UpdateState,
|
||||||
|
ngrokTunnel,
|
||||||
|
NgrokTunnelState,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
|
|
||||||
export interface RootState {
|
export interface RootState {
|
||||||
|
@ -93,6 +95,7 @@ export interface RootState {
|
||||||
settings?: Settings;
|
settings?: Settings;
|
||||||
theme?: ThemeState;
|
theme?: ThemeState;
|
||||||
update?: UpdateState;
|
update?: UpdateState;
|
||||||
|
ngrokTunnel?: NgrokTunnelState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_STATE = {};
|
const DEFAULT_STATE = {};
|
||||||
|
@ -132,6 +135,7 @@ function initStore(): Store<RootState> {
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
theme,
|
theme,
|
||||||
update,
|
update,
|
||||||
|
ngrokTunnel,
|
||||||
}),
|
}),
|
||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
storeEnhancer
|
storeEnhancer
|
||||||
|
|
|
@ -161,7 +161,7 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
|
||||||
<Row align={RowAlignment.Bottom}>
|
<Row align={RowAlignment.Bottom}>
|
||||||
<Checkbox label="Azure for US Government" checked={isAzureGov} onChange={this.onChannelServiceChange} />
|
<Checkbox label="Azure for US Government" checked={isAzureGov} onChange={this.onChannelServiceChange} />
|
||||||
<LinkButton
|
<LinkButton
|
||||||
ariaLabel="Learn more about Azure for US Government. "
|
ariaLabel="Learn more about Azure for US Government."
|
||||||
className={dialogStyles.dialogLink}
|
className={dialogStyles.dialogLink}
|
||||||
linkRole={true}
|
linkRole={true}
|
||||||
onClick={this.onAzureGovLinkClick}
|
onClick={this.onAzureGovLinkClick}
|
||||||
|
|
|
@ -32,13 +32,14 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { SharedConstants } from '@bfemulator/app-shared';
|
||||||
|
|
||||||
import * as Constants from '../../constants';
|
import * as Constants from '../../constants';
|
||||||
import { Document } from '../../state/reducers/editor';
|
import { Document } from '../../state/reducers/editor';
|
||||||
|
|
||||||
import { MarkdownPage } from './markdownPage/markdownPage';
|
import { MarkdownPage } from './markdownPage/markdownPage';
|
||||||
|
|
||||||
import { AppSettingsEditorContainer, EmulatorContainer, WelcomePageContainer } from './index';
|
import { AppSettingsEditorContainer, EmulatorContainer, WelcomePageContainer, NgrokDebuggerContainer } from './index';
|
||||||
|
|
||||||
interface EditorFactoryProps {
|
interface EditorFactoryProps {
|
||||||
document?: Document;
|
document?: Document;
|
||||||
|
@ -70,6 +71,9 @@ export class EditorFactory extends React.Component<EditorFactoryProps> {
|
||||||
case Constants.CONTENT_TYPE_MARKDOWN:
|
case Constants.CONTENT_TYPE_MARKDOWN:
|
||||||
return <MarkdownPage markdown={document.meta.markdown} onLine={document.meta.onLine} />;
|
return <MarkdownPage markdown={document.meta.markdown} onLine={document.meta.onLine} />;
|
||||||
|
|
||||||
|
case SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER:
|
||||||
|
return <NgrokDebuggerContainer documentId={document.documentId} dirty={this.props.document.dirty} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,3 +36,4 @@ export * from './editor';
|
||||||
export * from './panel/panel';
|
export * from './panel/panel';
|
||||||
export * from './emulator';
|
export * from './emulator';
|
||||||
export * from './welcomePage';
|
export * from './welcomePage';
|
||||||
|
export * from './ngrokDebugger/ngrokDebuggerContainer';
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Column, Row, LinkButton, MediumHeader, SmallHeader } from '@bfemulator/ui-react';
|
||||||
|
|
||||||
|
import { TunnelError, TunnelStatus } from '../../../state';
|
||||||
|
import { GenericDocument } from '../../layout';
|
||||||
|
|
||||||
|
import { NgrokErrorHandler } from './ngrokErrorHandler';
|
||||||
|
import * as styles from './ngrokDebuggerContainer.scss';
|
||||||
|
|
||||||
|
export interface NgrokDebuggerProps {
|
||||||
|
inspectUrl: string;
|
||||||
|
errors: TunnelError;
|
||||||
|
publicUrl: string;
|
||||||
|
logPath: string;
|
||||||
|
postmanCollectionPath: string;
|
||||||
|
tunnelStatus: TunnelStatus;
|
||||||
|
lastPingedTimestamp: string;
|
||||||
|
onAnchorClick: (linkRef: string) => void;
|
||||||
|
onSaveFileClick: (originalFilePath: string, dialogOptions: Electron.SaveDialogOptions) => void;
|
||||||
|
onPingTunnelClick: () => void;
|
||||||
|
onReconnectToNgrokClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDialogOptions = (title: string, buttonLabel: string = 'Save'): Electron.SaveDialogOptions => ({
|
||||||
|
title,
|
||||||
|
buttonLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NgrokDebugger = (props: NgrokDebuggerProps) => {
|
||||||
|
const [statusDisplay, setStatusDisplay] = useState(styles.tunnelInactive);
|
||||||
|
|
||||||
|
const convertToAnchorOnClick = (link: string) => {
|
||||||
|
props.onAnchorClick(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (props.tunnelStatus) {
|
||||||
|
case TunnelStatus.Active:
|
||||||
|
setStatusDisplay(styles.tunnelActive);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TunnelStatus.Error:
|
||||||
|
setStatusDisplay(styles.tunnelError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
setStatusDisplay(styles.tunnelInactive);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [props.lastPingedTimestamp]);
|
||||||
|
|
||||||
|
const errorDetailsContainer =
|
||||||
|
props.tunnelStatus === TunnelStatus.Error ? (
|
||||||
|
<div className={styles.errorDetailedViewer}>
|
||||||
|
<NgrokErrorHandler
|
||||||
|
errors={props.errors}
|
||||||
|
onExternalLinkClick={convertToAnchorOnClick}
|
||||||
|
onReconnectToNgrokClick={props.onReconnectToNgrokClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const tunnelConnections = (
|
||||||
|
<section>
|
||||||
|
<SmallHeader>Tunnel Connections</SmallHeader>
|
||||||
|
<ul className={styles.tunnelDetailsList}>
|
||||||
|
<li>
|
||||||
|
<legend>Inspect Url</legend>
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Ngrok Inspect Url."
|
||||||
|
linkRole={true}
|
||||||
|
onClick={() => convertToAnchorOnClick(props.inspectUrl)}
|
||||||
|
>
|
||||||
|
{props.inspectUrl}
|
||||||
|
</LinkButton>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<legend>Public Url</legend>
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Ngrok Public Url."
|
||||||
|
linkRole={true}
|
||||||
|
onClick={() => convertToAnchorOnClick(props.publicUrl)}
|
||||||
|
>
|
||||||
|
{props.publicUrl}
|
||||||
|
</LinkButton>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Download Log file."
|
||||||
|
linkRole={false}
|
||||||
|
onClick={() => props.onSaveFileClick(props.logPath, getDialogOptions('Save log file to disk.'))}
|
||||||
|
>
|
||||||
|
Click here
|
||||||
|
</LinkButton>
|
||||||
|
to download the ngrok log file for this tunnel session
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Download postman collection."
|
||||||
|
linkRole={false}
|
||||||
|
onClick={() =>
|
||||||
|
props.onSaveFileClick(props.postmanCollectionPath, getDialogOptions('Save Postman collection to disk.'))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Click here
|
||||||
|
</LinkButton>
|
||||||
|
to download a Postman collection to additionally inspect your tunnels
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericDocument className={styles.ngrokDebuggerContainer}>
|
||||||
|
<MediumHeader>Ngrok Status Viewer</MediumHeader>
|
||||||
|
<Row>
|
||||||
|
<Column>
|
||||||
|
<section>
|
||||||
|
<SmallHeader>Tunnel Health</SmallHeader>
|
||||||
|
<ul className={styles.tunnelDetailsList}>
|
||||||
|
<li>
|
||||||
|
<legend>Tunnel Status</legend>
|
||||||
|
<span className={[styles.tunnelHealthIndicator, statusDisplay].join(' ')} />
|
||||||
|
<span>{props.lastPingedTimestamp}</span>
|
||||||
|
</li>
|
||||||
|
{props.tunnelStatus !== TunnelStatus.Inactive ? (
|
||||||
|
<li>
|
||||||
|
<LinkButton linkRole={true} onClick={props.onPingTunnelClick}>
|
||||||
|
Click here
|
||||||
|
</LinkButton>
|
||||||
|
to ping the tunnel now
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{errorDetailsContainer}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{props.publicUrl ? tunnelConnections : null}
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</GenericDocument>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,87 @@
|
||||||
|
.ngrok-debugger-container {
|
||||||
|
:global {
|
||||||
|
input {
|
||||||
|
font-family: var(--default-font-family);
|
||||||
|
}
|
||||||
|
align-items: flex-center;
|
||||||
|
justify-content: flex-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-details-list {
|
||||||
|
background-color: var(--my-bots-well-bg);
|
||||||
|
border: var(--my-bots-well-border);
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-health-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
padding: 3px;
|
||||||
|
margin-right: 2px;
|
||||||
|
color: var(--focused-list-item);
|
||||||
|
|
||||||
|
&.tunnel-error {
|
||||||
|
background-color: var(--error-outline);
|
||||||
|
&::before {
|
||||||
|
content: "Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tunnel-inactive {
|
||||||
|
background-color: var(--info-outline);
|
||||||
|
&::before {
|
||||||
|
content: "Inactive";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tunnel-active {
|
||||||
|
background-color: var(--success-bg);
|
||||||
|
&::before {
|
||||||
|
content: "Active";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-detailed-viewer {
|
||||||
|
border: 1px solid var(--focused-outline);
|
||||||
|
color: var(--error-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-viewer {
|
||||||
|
background: var(--success-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-window {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--error-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacing {
|
||||||
|
padding: 0 0 2px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.well {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
packages/app/client/src/ui/editor/ngrokDebugger/ngrokDebuggerContainer.scss.d.ts
поставляемый
Normal file
12
packages/app/client/src/ui/editor/ngrokDebugger/ngrokDebuggerContainer.scss.d.ts
поставляемый
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// This is a generated file. Changes are likely to result in being overwritten
|
||||||
|
export const ngrokDebuggerContainer: string;
|
||||||
|
export const tunnelDetailsList: string;
|
||||||
|
export const tunnelHealthIndicator: string;
|
||||||
|
export const tunnelError: string;
|
||||||
|
export const tunnelInactive: string;
|
||||||
|
export const tunnelActive: string;
|
||||||
|
export const errorDetailedViewer: string;
|
||||||
|
export const consoleViewer: string;
|
||||||
|
export const errorWindow: string;
|
||||||
|
export const spacing: string;
|
||||||
|
export const well: string;
|
|
@ -0,0 +1,196 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
//TODO: More UI tests to be added
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { combineReducers, createStore } from 'redux';
|
||||||
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
|
|
||||||
|
import { ngrokTunnel } from '../../../state/reducers/ngrokTunnel';
|
||||||
|
import {
|
||||||
|
updateNewTunnelInfo,
|
||||||
|
TunnelInfo,
|
||||||
|
TunnelStatus,
|
||||||
|
updateTunnelStatus,
|
||||||
|
updateTunnelError,
|
||||||
|
} from '../../../state/actions/ngrokTunnelActions';
|
||||||
|
|
||||||
|
import { NgrokDebuggerContainer } from './ngrokDebuggerContainer';
|
||||||
|
import { NgrokDebugger } from './ngrokDebugger';
|
||||||
|
|
||||||
|
const mockClasses = jest.fn(() => ({
|
||||||
|
errorDetailedViewer: 'error-window',
|
||||||
|
tunnelActive: 'tunnel-active',
|
||||||
|
tunnelInactive: 'tunnel-inactive',
|
||||||
|
tunnelError: 'tunnel-error',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./ngrokDebuggerContainer.scss', () => ({
|
||||||
|
get errorDetailedViewer() {
|
||||||
|
return mockClasses().errorDetailedViewer;
|
||||||
|
},
|
||||||
|
get tunnelActive() {
|
||||||
|
return mockClasses().tunnelActive;
|
||||||
|
},
|
||||||
|
get tunnelError() {
|
||||||
|
return mockClasses().tunnelError;
|
||||||
|
},
|
||||||
|
get tunnelInactive() {
|
||||||
|
return mockClasses().tunnelInactive;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
remote: {
|
||||||
|
app: {
|
||||||
|
isPackaged: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ipcMain: new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(): any {
|
||||||
|
return () => ({});
|
||||||
|
},
|
||||||
|
has() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ipcRenderer: new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(): any {
|
||||||
|
return () => ({});
|
||||||
|
},
|
||||||
|
has() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Ngrok Debugger container', () => {
|
||||||
|
let parent: ReactWrapper;
|
||||||
|
let wrapper: ReactWrapper;
|
||||||
|
const mockStore = createStore(combineReducers({ ngrokTunnel }));
|
||||||
|
let mockDispatch = null;
|
||||||
|
let instance = null;
|
||||||
|
const mockClassesImpl = mockClasses();
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
parent = mount(
|
||||||
|
<Provider store={mockStore}>
|
||||||
|
<NgrokDebuggerContainer />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
wrapper = parent.find(NgrokDebugger);
|
||||||
|
instance = wrapper.instance();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const info: TunnelInfo = {
|
||||||
|
publicUrl: 'https://ncfdsd.ngrok.io/',
|
||||||
|
inspectUrl: 'http://127.0.0.1:4000',
|
||||||
|
logPath: 'ngrok.log',
|
||||||
|
postmanCollectionPath: 'postman.json',
|
||||||
|
};
|
||||||
|
mockDispatch = jest.spyOn(mockStore, 'dispatch');
|
||||||
|
mockStore.dispatch(updateNewTunnelInfo(info));
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Inactive));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render without errors', () => {
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Active));
|
||||||
|
expect(wrapper.find(NgrokDebugger)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the new tunnel status when tunnel status is changed from an action', () => {
|
||||||
|
expect(wrapper.find(mockClassesImpl.errorDetailedViewer)).toEqual({});
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||||
|
expect(wrapper.find(mockClassesImpl.errorDetailedViewer)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update classes when tunnel status changes', () => {
|
||||||
|
expect(wrapper.find(mockClassesImpl.tunnelInactive)).toBeDefined();
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Active));
|
||||||
|
expect(wrapper.find(mockClassesImpl.tunnelActive)).toBeDefined();
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||||
|
expect(wrapper.find(mockClassesImpl.tunnelError)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show that tunnel has expired', () => {
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||||
|
mockStore.dispatch(
|
||||||
|
updateTunnelError({
|
||||||
|
statusCode: 402,
|
||||||
|
errorMessage: 'Tunnel has expired',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(wrapper.text().includes('ngrok tunnel has expired')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show that tunnel has too many connections', () => {
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||||
|
mockStore.dispatch(
|
||||||
|
updateTunnelError({
|
||||||
|
statusCode: 429,
|
||||||
|
errorMessage: 'Tunnel has too many connections',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(wrapper.html().includes('Signup for Ngrok account')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show that tunnel has expired', () => {
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||||
|
mockStore.dispatch(
|
||||||
|
updateTunnelError({
|
||||||
|
statusCode: 402,
|
||||||
|
errorMessage: 'Tunnel has expired',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(wrapper.html().includes('ngrok tunnel has expired.')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a generic tunnel error message', () => {
|
||||||
|
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||||
|
mockStore.dispatch(
|
||||||
|
updateTunnelError({
|
||||||
|
statusCode: -9999,
|
||||||
|
errorMessage: 'Dummy tunnel error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(wrapper.html().includes('Looks like the ngrok tunnel does not exist anymore.')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import { SharedConstants } from '@bfemulator/app-shared';
|
||||||
|
|
||||||
|
import { executeCommand } from '../../../state/actions/commandActions';
|
||||||
|
import { RootState } from '../../../state/store';
|
||||||
|
|
||||||
|
import { NgrokDebugger, NgrokDebuggerProps } from './ngrokDebugger';
|
||||||
|
|
||||||
|
const onFileSaveCb = (result: boolean) => {
|
||||||
|
if (!result) {
|
||||||
|
// TODO: Show error dialog here
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('An error occured trying to save the file to disk');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState, ownProps: {}): Partial<NgrokDebuggerProps> => {
|
||||||
|
const {
|
||||||
|
inspectUrl,
|
||||||
|
errors,
|
||||||
|
publicUrl,
|
||||||
|
logPath,
|
||||||
|
postmanCollectionPath,
|
||||||
|
tunnelStatus,
|
||||||
|
lastPingedTimestamp: lastTunnelStatusCheckTS,
|
||||||
|
} = state.ngrokTunnel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
inspectUrl,
|
||||||
|
errors,
|
||||||
|
publicUrl,
|
||||||
|
logPath,
|
||||||
|
postmanCollectionPath,
|
||||||
|
tunnelStatus,
|
||||||
|
lastPingedTimestamp: lastTunnelStatusCheckTS,
|
||||||
|
...ownProps,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: (action: Action) => void) => ({
|
||||||
|
onAnchorClick: (url: string) =>
|
||||||
|
dispatch(executeCommand(true, SharedConstants.Commands.Electron.OpenExternal, null, url)),
|
||||||
|
onPingTunnelClick: () => dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.PingTunnel, null)),
|
||||||
|
onReconnectToNgrokClick: () => dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.Reconnect, null)),
|
||||||
|
onSaveFileClick: (originalFilePath: string, dialogOptions: Electron.SaveDialogOptions) => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand(
|
||||||
|
true,
|
||||||
|
SharedConstants.Commands.Electron.ShowSaveDialog,
|
||||||
|
(newFilePath: string) => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand(
|
||||||
|
true,
|
||||||
|
SharedConstants.Commands.Electron.CopyFile,
|
||||||
|
onFileSaveCb,
|
||||||
|
originalFilePath,
|
||||||
|
newFilePath
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
dialogOptions
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NgrokDebuggerContainer = connect(mapStateToProps, mapDispatchToProps)(NgrokDebugger);
|
|
@ -0,0 +1,112 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { LinkButton } from '@bfemulator/ui-react';
|
||||||
|
import { TunnelError } from 'packages/app/main/src/state';
|
||||||
|
|
||||||
|
export interface NgrokErrorHandlerProps {
|
||||||
|
errors: TunnelError;
|
||||||
|
onExternalLinkClick: (linkRef: string) => void;
|
||||||
|
onReconnectToNgrokClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NgrokErrorHandler = (props: NgrokErrorHandlerProps) => {
|
||||||
|
switch (props.errors.statusCode) {
|
||||||
|
case 429:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<legend>
|
||||||
|
Looks like you have hit your free tier limit on connections to your tunnel. Below you will find several
|
||||||
|
possible solutions.
|
||||||
|
</legend>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Try signing up here
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Signup for Ngrok account."
|
||||||
|
linkRole={true}
|
||||||
|
onClick={() => props.onExternalLinkClick('https://dashboard.ngrok.com/signup')}
|
||||||
|
>
|
||||||
|
https://dashboard.ngrok.com/signup
|
||||||
|
</LinkButton>
|
||||||
|
and register your auth token as per the steps in
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Github link for tunnelling issues."
|
||||||
|
linkRole={true}
|
||||||
|
onClick={() =>
|
||||||
|
props.onExternalLinkClick('https://github.com/microsoft/botframework-solutions/issues/2406')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
https://github.com/microsoft/botframework-solutions/issues/2406
|
||||||
|
</LinkButton>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>Upgrade to a paid account of ngrok</li>
|
||||||
|
<li>Wait for a few minutes without any activity</li>
|
||||||
|
</ol>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 402:
|
||||||
|
return (
|
||||||
|
<legend>
|
||||||
|
Looks like the ngrok tunnel has expired. Try reconnecting to Ngrok or examine the logs for a detailed
|
||||||
|
explanation of the error.
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Click here to reconnect to ngrok."
|
||||||
|
linkRole={false}
|
||||||
|
onClick={props.onReconnectToNgrokClick}
|
||||||
|
>
|
||||||
|
Click here to reconnect to ngrok
|
||||||
|
</LinkButton>
|
||||||
|
</legend>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<legend>
|
||||||
|
Looks like the ngrok tunnel does not exist anymore. Try reconnecting to Ngrok or examine the logs for a
|
||||||
|
detailed explanation of the error.
|
||||||
|
<LinkButton
|
||||||
|
ariaLabel="Click here to reconnect to ngrok."
|
||||||
|
linkRole={false}
|
||||||
|
onClick={props.onReconnectToNgrokClick}
|
||||||
|
>
|
||||||
|
Click here to reconnect to ngrok
|
||||||
|
</LinkButton>
|
||||||
|
</legend>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -31,14 +31,13 @@
|
||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
|
import { SharedConstants } from '@bfemulator/app-shared';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { combineReducers, createStore } from 'redux';
|
import { combineReducers, createStore } from 'redux';
|
||||||
|
|
||||||
import * as BotActions from '../../../state/actions/botActions';
|
import * as BotActions from '../../../state/actions/botActions';
|
||||||
import { beginAdd } from '../../../state/actions/notificationActions';
|
|
||||||
import { openContextMenuForBot } from '../../../state/actions/welcomePageActions';
|
import { openContextMenuForBot } from '../../../state/actions/welcomePageActions';
|
||||||
import { bot } from '../../../state/reducers/bot';
|
import { bot } from '../../../state/reducers/bot';
|
||||||
import { executeCommand } from '../../../state/actions/commandActions';
|
import { executeCommand } from '../../../state/actions/commandActions';
|
||||||
|
|
До Ширина: | Высота: | Размер: 3.7 KiB После Ширина: | Высота: | Размер: 3.7 KiB |
|
@ -42,6 +42,7 @@ import {
|
||||||
CONTENT_TYPE_MARKDOWN,
|
CONTENT_TYPE_MARKDOWN,
|
||||||
CONTENT_TYPE_TRANSCRIPT,
|
CONTENT_TYPE_TRANSCRIPT,
|
||||||
CONTENT_TYPE_WELCOME_PAGE,
|
CONTENT_TYPE_WELCOME_PAGE,
|
||||||
|
CONTENT_TYPE_NGROK_DEBUGGER,
|
||||||
} from '../../../../constants';
|
} from '../../../../constants';
|
||||||
import { getOtherTabGroup } from '../../../../state/helpers/editorHelpers';
|
import { getOtherTabGroup } from '../../../../state/helpers/editorHelpers';
|
||||||
import { Document, Editor } from '../../../../state/reducers/editor';
|
import { Document, Editor } from '../../../../state/reducers/editor';
|
||||||
|
@ -285,6 +286,9 @@ export class TabBar extends React.Component<TabBarProps, TabBarState> {
|
||||||
case CONTENT_TYPE_DEBUG:
|
case CONTENT_TYPE_DEBUG:
|
||||||
return 'Debug';
|
return 'Debug';
|
||||||
|
|
||||||
|
case CONTENT_TYPE_NGROK_DEBUGGER:
|
||||||
|
return 'Ngrok Status';
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:nth-child(5) {
|
||||||
|
// Ngrok Status Viewer
|
||||||
|
> div {
|
||||||
|
-webkit-mask-size: 31px;
|
||||||
|
-webkit-mask: url('../../media/ic_wrench.svg') no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
|
|
@ -129,7 +129,7 @@ describe('<NavBar/>', () => {
|
||||||
|
|
||||||
it('should render links for each section', () => {
|
it('should render links for each section', () => {
|
||||||
expect(instance).not.toBeNull();
|
expect(instance).not.toBeNull();
|
||||||
expect(instance.links).toHaveLength(4);
|
expect(instance.links).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a notification badge', () => {
|
it('should render a notification badge', () => {
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface NavBarProps {
|
||||||
showExplorer?: (show: boolean) => void;
|
showExplorer?: (show: boolean) => void;
|
||||||
navBarSelectionChanged?: (selection: string) => void;
|
navBarSelectionChanged?: (selection: string) => void;
|
||||||
openEmulatorSettings?: () => void;
|
openEmulatorSettings?: () => void;
|
||||||
|
openNgrokDebuggerPanel?: () => void;
|
||||||
notifications?: string[];
|
notifications?: string[];
|
||||||
explorerIsVisible?: boolean;
|
explorerIsVisible?: boolean;
|
||||||
botIsOpen?: boolean;
|
botIsOpen?: boolean;
|
||||||
|
@ -59,6 +60,7 @@ const selectionMap = [
|
||||||
Constants.NAVBAR_RESOURCES,
|
Constants.NAVBAR_RESOURCES,
|
||||||
Constants.NAVBAR_NOTIFICATIONS,
|
Constants.NAVBAR_NOTIFICATIONS,
|
||||||
Constants.NAVBAR_SETTINGS,
|
Constants.NAVBAR_SETTINGS,
|
||||||
|
Constants.NAVBAR_NGROK_DEBUGGER,
|
||||||
];
|
];
|
||||||
|
|
||||||
export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
||||||
|
@ -101,7 +103,14 @@ export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Settings
|
case 3:
|
||||||
|
this.props.openEmulatorSettings();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
this.props.openNgrokDebuggerPanel();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.props.openEmulatorSettings();
|
this.props.openEmulatorSettings();
|
||||||
break;
|
break;
|
||||||
|
@ -112,7 +121,7 @@ export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
||||||
const { selection } = this.state;
|
const { selection } = this.state;
|
||||||
const { explorerIsVisible, botIsOpen = false } = this.props;
|
const { explorerIsVisible, botIsOpen = false } = this.props;
|
||||||
|
|
||||||
return ['Bot Explorer', 'Resources', 'Notifications', 'Settings'].map((title, index) => {
|
return ['Bot Explorer', 'Resources', 'Notifications', 'Settings', 'Ngrok'].map((title, index) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-selected={explorerIsVisible && selection === selectionMap[index]}
|
aria-selected={explorerIsVisible && selection === selectionMap[index]}
|
||||||
|
|
|
@ -64,6 +64,16 @@ const mapDispatchToProps = (dispatch): NavBarProps => ({
|
||||||
},
|
},
|
||||||
trackEvent: (name: string, properties?: { [key: string]: any }) =>
|
trackEvent: (name: string, properties?: { [key: string]: any }) =>
|
||||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
|
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
|
||||||
|
openNgrokDebuggerPanel: () => {
|
||||||
|
dispatch(
|
||||||
|
EditorActions.open({
|
||||||
|
contentType: SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER,
|
||||||
|
documentId: SharedConstants.DocumentIds.DOCUMENT_ID_NGROK_DEBUGGER,
|
||||||
|
isGlobal: true,
|
||||||
|
meta: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NavBar = connect(mapStateToProps, mapDispatchToProps)(NavBarComponent);
|
export const NavBar = connect(mapStateToProps, mapDispatchToProps)(NavBarComponent);
|
||||||
|
|
|
@ -27,6 +27,7 @@ html {
|
||||||
--error-bg: #5A1D1D;
|
--error-bg: #5A1D1D;
|
||||||
--error-outline: #BE1100;
|
--error-outline: #BE1100;
|
||||||
--endpoint-warning: var(--neutral-14);
|
--endpoint-warning: var(--neutral-14);
|
||||||
|
--success-bg: #47B07F;
|
||||||
|
|
||||||
/* webchat overridden colors */
|
/* webchat overridden colors */
|
||||||
/* activity bubbles */
|
/* activity bubbles */
|
||||||
|
|
|
@ -24,6 +24,7 @@ html {
|
||||||
--error-text: #A1260D;
|
--error-text: #A1260D;
|
||||||
--error-bg: #F2DEDE;
|
--error-bg: #F2DEDE;
|
||||||
--error-outline: #BE1100;
|
--error-outline: #BE1100;
|
||||||
|
--success-bg: #47B07F;
|
||||||
--endpoint-warning: var(--neutral-14);
|
--endpoint-warning: var(--neutral-14);
|
||||||
|
|
||||||
/* webchat overridden colors */
|
/* webchat overridden colors */
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -8,6 +8,7 @@
|
||||||
"homepage": "https://github.com/Microsoft/BotFramework-Emulator",
|
"homepage": "https://github.com/Microsoft/BotFramework-Emulator",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "run-s typecheck build:electron",
|
"build": "run-s typecheck build:electron",
|
||||||
|
"rebuild:keytar:electron": "electron-rebuild",
|
||||||
"build:electron": "babel ./src --out-dir app/server --extensions \".ts,.tsx\" --ignore \"**/*.spec.ts\" && npm run copy:extension:stubs",
|
"build:electron": "babel ./src --out-dir app/server --extensions \".ts,.tsx\" --ignore \"**/*.spec.ts\" && npm run copy:extension:stubs",
|
||||||
"copy:extension:stubs": "ncp src/extensions app/extensions",
|
"copy:extension:stubs": "ncp src/extensions app/extensions",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
"start:react-app": "cd ../client && npm start",
|
"start:react-app": "cd ../client && npm start",
|
||||||
"start:watch": "nodemon",
|
"start:watch": "nodemon",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { ContextMenuService } from '../services/contextMenuService';
|
||||||
import { TelemetryService } from '../telemetry';
|
import { TelemetryService } from '../telemetry';
|
||||||
import { showOpenDialog, showSaveDialog } from '../utils';
|
import { showOpenDialog, showSaveDialog } from '../utils';
|
||||||
import { AppUpdater } from '../appUpdater';
|
import { AppUpdater } from '../appUpdater';
|
||||||
|
import { copyFileAsync } from '../utils/copyFileAsync';
|
||||||
|
|
||||||
const { shell } = Electron;
|
const { shell } = Electron;
|
||||||
|
|
||||||
|
@ -193,6 +194,17 @@ export class ElectronCommands {
|
||||||
return shell.moveItemToTrash(path.resolve(filePath));
|
return shell.moveItemToTrash(path.resolve(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Given source path. Copies it to Destination Path
|
||||||
|
@Command(Commands.CopyFile)
|
||||||
|
protected async copyFile(sourcePath: string, destinationPath: string) {
|
||||||
|
try {
|
||||||
|
await copyFileAsync(sourcePath, destinationPath);
|
||||||
|
} catch (ex) {
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Renames a file - the payload must contain the property "path" and "name"
|
// Renames a file - the payload must contain the property "path" and "name"
|
||||||
// This will also rename the file extension if one is provided in the "name" field
|
// This will also rename the file extension if one is provided in the "name" field
|
||||||
|
|
|
@ -57,4 +57,9 @@ export class NgrokCommands {
|
||||||
protected killNgrokProcess() {
|
protected killNgrokProcess() {
|
||||||
Emulator.getInstance().ngrok.kill();
|
Emulator.getInstance().ngrok.kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Command(Commands.PingTunnel)
|
||||||
|
protected pingForStatusOfTunnel() {
|
||||||
|
Emulator.getInstance().ngrok.pingTunnel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ jest.mock('electron', () => ({
|
||||||
app: {
|
app: {
|
||||||
on: () => void 0,
|
on: () => void 0,
|
||||||
setName: () => void 0,
|
setName: () => void 0,
|
||||||
|
getPath: () => '',
|
||||||
},
|
},
|
||||||
ipcMain: new Proxy(
|
ipcMain: new Proxy(
|
||||||
{},
|
{},
|
||||||
|
|
|
@ -46,6 +46,15 @@ import './fetchProxy';
|
||||||
import { Window } from './platform/window';
|
import { Window } from './platform/window';
|
||||||
import { azureLoggedInUserChanged } from './state/actions/azureAuthActions';
|
import { azureLoggedInUserChanged } from './state/actions/azureAuthActions';
|
||||||
import { rememberBounds } from './state/actions/windowStateActions';
|
import { rememberBounds } from './state/actions/windowStateActions';
|
||||||
|
import {
|
||||||
|
updateTunnelError,
|
||||||
|
TunnelInfo,
|
||||||
|
updateNewTunnelInfo,
|
||||||
|
TunnelStatus,
|
||||||
|
updateTunnelStatus,
|
||||||
|
TunnelError,
|
||||||
|
} from './state/actions/ngrokTunnelActions';
|
||||||
|
import * as EditorActions from './state/actions/editorActions';
|
||||||
import { dispatch, getSettings, store } from './state/store';
|
import { dispatch, getSettings, store } from './state/store';
|
||||||
import { TelemetryService } from './telemetry';
|
import { TelemetryService } from './telemetry';
|
||||||
import { botListsAreDifferent, ensureStoragePath, saveSettings, writeFile } from './utils';
|
import { botListsAreDifferent, ensureStoragePath, saveSettings, writeFile } from './utils';
|
||||||
|
@ -154,7 +163,9 @@ class EmulatorApplication {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeNgrokListeners() {
|
private initializeNgrokListeners() {
|
||||||
Emulator.getInstance().ngrok.ngrokEmitter.on('expired', this.onNgrokSessionExpired);
|
Emulator.getInstance().ngrok.ngrokEmitter.on('onTunnelError', this.onTunnelError);
|
||||||
|
Emulator.getInstance().ngrok.ngrokEmitter.on('onNewTunnelConnected', this.onNewTunnelConnected);
|
||||||
|
Emulator.getInstance().ngrok.ngrokEmitter.on('onTunnelStatusPing', this.onTunnelStatusPing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeSystemPreferencesListeners() {
|
private initializeSystemPreferencesListeners() {
|
||||||
|
@ -242,27 +253,39 @@ class EmulatorApplication {
|
||||||
dispatch(rememberBounds(bounds));
|
dispatch(rememberBounds(bounds));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ngrok listeners
|
private onTunnelStatusPing = async (status: TunnelStatus) => {
|
||||||
private onNgrokSessionExpired = async () => {
|
dispatch(updateTunnelStatus(status));
|
||||||
// when ngrok expires, spawn notification to reconnect
|
};
|
||||||
const ngrokNotification: Notification = newNotification(
|
|
||||||
'Your ngrok tunnel instance has expired. Would you like to reconnect to a new tunnel?'
|
private onNewTunnelConnected = async (tunnelInfo: TunnelInfo) => {
|
||||||
);
|
dispatch(updateNewTunnelInfo(tunnelInfo));
|
||||||
ngrokNotification.addButton('Dismiss', () => {
|
};
|
||||||
|
|
||||||
|
private onTunnelError = async (response: TunnelError) => {
|
||||||
|
// Avoid reporting the same error again and again to avoid notification flooding
|
||||||
|
if (store.getState().ngrokTunnel.errors.statusCode === response.statusCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const genericTunnelError =
|
||||||
|
'Oops.. Your ngrok tunnel seems to have an error. Please check the Ngrok Debug Console for more details';
|
||||||
|
dispatch(updateTunnelError({ ...response }));
|
||||||
|
|
||||||
|
const ngrokNotification: Notification = newNotification(genericTunnelError);
|
||||||
|
ngrokNotification.addButton('Debug Console', () => {
|
||||||
|
// Go to Ngrok from here
|
||||||
const { Commands } = SharedConstants;
|
const { Commands } = SharedConstants;
|
||||||
this.commandService.remoteCall(Commands.Notifications.Remove, ngrokNotification.id);
|
this.commandService.remoteCall(Commands.Notifications.Remove, ngrokNotification.id);
|
||||||
});
|
dispatch(
|
||||||
ngrokNotification.addButton('Reconnect', async () => {
|
EditorActions.open({
|
||||||
try {
|
contentType: SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER,
|
||||||
const { Commands } = SharedConstants;
|
documentId: SharedConstants.DocumentIds.DOCUMENT_ID_NGROK_DEBUGGER,
|
||||||
await this.commandService.call(Commands.Ngrok.Reconnect);
|
isGlobal: true,
|
||||||
this.commandService.remoteCall(Commands.Notifications.Remove, ngrokNotification.id);
|
meta: null,
|
||||||
} catch (e) {
|
})
|
||||||
await sendNotificationToClient(newNotification(e), this.commandService);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
await sendNotificationToClient(ngrokNotification, this.commandService);
|
await sendNotificationToClient(ngrokNotification, this.commandService);
|
||||||
Emulator.getInstance().ngrok.broadcastNgrokExpired();
|
Emulator.getInstance().ngrok.broadcastNgrokError(genericTunnelError);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onInvertedColorSchemeChanged = () => {
|
private onInvertedColorSchemeChanged = () => {
|
||||||
|
@ -316,7 +339,7 @@ class EmulatorApplication {
|
||||||
app.on('open-url', this.onAppOpenUrl);
|
app.on('open-url', this.onAppOpenUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAppOpenUrl = (event: any, url: string): void => {
|
private onAppOpenUrl = (event: Event, url: string): void => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isMac()) {
|
if (isMac()) {
|
||||||
protocolUsed = true;
|
protocolUsed = true;
|
||||||
|
|
|
@ -30,162 +30,265 @@
|
||||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import './fetchProxy';
|
import './fetchProxy';
|
||||||
import { intervals, NgrokInstance } from './ngrok';
|
import { intervals, NgrokInstance } from './ngrok';
|
||||||
|
import { TunnelStatus, TunnelError } from './state/actions/ngrokTunnelActions';
|
||||||
|
|
||||||
|
const mockExistsSync = jest.fn(() => true);
|
||||||
|
|
||||||
|
const headersMap: Map<string, string> = new Map();
|
||||||
|
headersMap.set('Server', 'Emulator');
|
||||||
|
const tunnelResponseGeneric = (status: number, errorBody: string, headers = headersMap) => {
|
||||||
|
return {
|
||||||
|
text: async () => errorBody,
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTunnelStatusResponse = jest.fn(() => tunnelResponseGeneric(200, 'success'));
|
||||||
|
|
||||||
|
const connectToNgrokInstance = async (ngrok: NgrokInstance) => {
|
||||||
|
try {
|
||||||
|
const result = await ngrok.connect({
|
||||||
|
addr: 61914,
|
||||||
|
path: 'Applications/ngrok',
|
||||||
|
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||||
|
proto: 'http',
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
const mockSpawn = {
|
const mockSpawn = {
|
||||||
on: () => {},
|
on: () => {},
|
||||||
stdin: { on: (type, cb) => void 0 },
|
stdin: { on: () => void 0 },
|
||||||
stdout: {
|
stdout: {
|
||||||
pause: () => void 0,
|
pause: () => void 0,
|
||||||
on: (type, cb) => {
|
on: (type, cb) => {
|
||||||
if (type === 'data') {
|
if (type === 'data') {
|
||||||
setTimeout(() =>
|
cb('t=2019-02-01T14:10:08-0800 lvl=info msg="starting web service" obj=web addr=127.0.0.1:4041');
|
||||||
cb('t=2019-02-01T14:10:08-0800 lvl=info msg="starting web service" obj=web addr=127.0.0.1:4041')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeListener: () => void 0,
|
removeListener: () => void 0,
|
||||||
},
|
},
|
||||||
stderr: { on: (type, cb) => void 0, pause: () => void 0 },
|
stderr: { on: () => void 0, pause: () => void 0 },
|
||||||
kill: () => void 0,
|
kill: () => void 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mockOk = 0;
|
||||||
jest.mock('child_process', () => ({
|
jest.mock('child_process', () => ({
|
||||||
spawn: () => mockSpawn,
|
spawn: () => mockSpawn,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mockOk = 0;
|
jest.mock('fs', () => ({
|
||||||
|
existsSync: () => mockExistsSync(),
|
||||||
|
createWriteStream: () => ({
|
||||||
|
write: jest.fn(),
|
||||||
|
end: jest.fn(),
|
||||||
|
}),
|
||||||
|
mkdirSync: jest.fn(),
|
||||||
|
writeFileSync: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('./utils/ensureStoragePath', () => ({ ensureStoragePath: () => '' }));
|
||||||
jest.mock('node-fetch', () => {
|
jest.mock('node-fetch', () => {
|
||||||
|
const ngrokPublicUrl = 'https://d1a2bf16.ngrok.io';
|
||||||
const mockJson = {
|
const mockJson = {
|
||||||
name: 'e2cfb800-266f-11e9-bc59-e5847cdee2d1',
|
name: 'e2cfb800-266f-11e9-bc59-e5847cdee2d1',
|
||||||
uri: '/api/tunnels/e2cfb800-266f-11e9-bc59-e5847cdee2d1',
|
uri: '/api/tunnels/e2cfb800-266f-11e9-bc59-e5847cdee2d1',
|
||||||
proto: 'https',
|
proto: 'https',
|
||||||
};
|
};
|
||||||
Object.defineProperty(mockJson, 'public_url', {
|
Object.defineProperty(mockJson, 'public_url', {
|
||||||
value: 'https://d1a2bf16.ngrok.io',
|
value: ngrokPublicUrl,
|
||||||
});
|
});
|
||||||
return async (input, init) => {
|
return async (input, params) => {
|
||||||
return {
|
switch (input) {
|
||||||
ok: ++mockOk > 0,
|
case ngrokPublicUrl:
|
||||||
json: async () => mockJson,
|
if (params.method === 'DELETE') {
|
||||||
text: async () => 'oh noes!',
|
return {
|
||||||
};
|
ok: ++mockOk > 0,
|
||||||
|
json: async () => mockJson,
|
||||||
|
text: async () => 'oh noes!',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mockTunnelStatusResponse();
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
ok: ++mockOk > 0,
|
||||||
|
json: async () => mockJson,
|
||||||
|
text: async () => 'oh noes!',
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockExistsSync = jest.fn(() => true);
|
|
||||||
jest.mock('fs', () => ({
|
|
||||||
existsSync: () => mockExistsSync(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('the ngrok ', () => {
|
describe('the ngrok ', () => {
|
||||||
const ngrok = new NgrokInstance();
|
let ngrok: NgrokInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ngrok = new NgrokInstance();
|
||||||
|
mockOk = 0;
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
ngrok.kill();
|
ngrok.kill();
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should spawn ngrok successfully when the happy path is followed', async () => {
|
describe('ngrok connect/disconnect operations', () => {
|
||||||
const result = await ngrok.connect({
|
it('should spawn ngrok successfully when the happy path is followed', async () => {
|
||||||
addr: 61914,
|
const result = await connectToNgrokInstance(ngrok);
|
||||||
path: '/Applications/ngrok',
|
expect(result).toEqual({
|
||||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
inspectUrl: 'http://127.0.0.1:4041',
|
||||||
proto: 'http',
|
url: 'https://d1a2bf16.ngrok.io',
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
inspectUrl: 'http://127.0.0.1:4041',
|
|
||||||
url: 'https://d1a2bf16.ngrok.io',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retry if the request to retrieve the ngrok url fails the first time', async () => {
|
|
||||||
mockOk = -5;
|
|
||||||
await ngrok.connect({
|
|
||||||
addr: 61914,
|
|
||||||
path: '/Applications/ngrok',
|
|
||||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
|
||||||
proto: 'http',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockOk).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should emit when the ngrok session is expired', async () => {
|
|
||||||
mockOk = 0;
|
|
||||||
intervals.retry = 100;
|
|
||||||
intervals.expirationPoll = 1;
|
|
||||||
intervals.expirationTime = -1;
|
|
||||||
let emitted = false;
|
|
||||||
ngrok.ngrokEmitter.on('expired', () => {
|
|
||||||
emitted = true;
|
|
||||||
});
|
|
||||||
await ngrok.connect({
|
|
||||||
addr: 61914,
|
|
||||||
path: '/Applications/ngrok',
|
|
||||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
|
||||||
proto: 'http',
|
|
||||||
});
|
|
||||||
await new Promise(resolve => {
|
|
||||||
setTimeout(resolve, 100);
|
|
||||||
});
|
|
||||||
expect(emitted).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disconnect', async () => {
|
|
||||||
let disconnected = false;
|
|
||||||
ngrok.ngrokEmitter.on('disconnect', url => {
|
|
||||||
disconnected = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
await ngrok.connect({
|
|
||||||
addr: 61914,
|
|
||||||
path: '/Applications/ngrok',
|
|
||||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
|
||||||
proto: 'http',
|
|
||||||
});
|
|
||||||
await ngrok.disconnect();
|
|
||||||
expect(disconnected).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw when the number of reties to retrieve the ngrok url are exhausted', async () => {
|
|
||||||
mockOk = -101;
|
|
||||||
let threw = false;
|
|
||||||
intervals.retry = 1;
|
|
||||||
try {
|
|
||||||
await ngrok.connect({
|
|
||||||
addr: 61914,
|
|
||||||
path: '/Applications/ngrok',
|
|
||||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
|
||||||
proto: 'http',
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
});
|
||||||
threw = e;
|
|
||||||
}
|
it('should retry if the request to retrieve the ngrok url fails the first time', async () => {
|
||||||
expect(threw.toString()).toBe('Error: oh noes!');
|
mockOk = -5;
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
expect(mockOk).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disconnect', async done => {
|
||||||
|
let disconnected = false;
|
||||||
|
ngrok.ngrokEmitter.on('disconnect', () => {
|
||||||
|
disconnected = true;
|
||||||
|
expect(disconnected).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
await ngrok.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when the number of reties to retrieve the ngrok url are exhausted.', async () => {
|
||||||
|
mockOk = -101;
|
||||||
|
let threw = false;
|
||||||
|
intervals.retry = 1;
|
||||||
|
try {
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
} catch (e) {
|
||||||
|
threw = e;
|
||||||
|
}
|
||||||
|
expect(threw.toString()).toBe('Error: oh noes!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if it failed to find an ngrok executable at the specified path.', async () => {
|
||||||
|
mockExistsSync.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
const path = join('Applications', 'ngrok');
|
||||||
|
let thrown;
|
||||||
|
try {
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e;
|
||||||
|
}
|
||||||
|
expect(thrown.toString()).toBe(
|
||||||
|
`Error: Could not find ngrok executable at path: ${path}. Make sure that the correct path to ngrok is configured in the Emulator app settings. Ngrok is required to receive a token from the Bot Framework token service.`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if it failed to find an ngrok executable at the specified path', async () => {
|
describe('ngrok tunnel heath status check operations', () => {
|
||||||
mockExistsSync.mockReturnValueOnce(false);
|
it('should emit ngrok error - Too many connections.', async done => {
|
||||||
|
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||||
const path = join('Applications', 'ngrok');
|
tunnelResponseGeneric(
|
||||||
let thrown;
|
429,
|
||||||
try {
|
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||||
await ngrok.connect({
|
)
|
||||||
addr: 61914,
|
);
|
||||||
path,
|
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
expect(error.statusCode).toBe(429);
|
||||||
proto: 'http',
|
done();
|
||||||
});
|
});
|
||||||
} catch (e) {
|
await connectToNgrokInstance(ngrok);
|
||||||
thrown = e;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
expect(thrown.toString()).toBe(
|
it('should emit ngrok error - Tunnel Expired.', async done => {
|
||||||
`Error: Could not find ngrok executable at path: ${path}. Make sure that the correct path to ngrok is configured in the Emulator app settings. Ngrok is required to receive a token from the Bot Framework token service.`
|
mockTunnelStatusResponse.mockReturnValueOnce(tunnelResponseGeneric(402, 'Tunnel has expired beyond the 8 hrs.'));
|
||||||
);
|
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||||
|
expect(error.statusCode).toBe(402);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit ngrok error - No server header present in the response headers.', async done => {
|
||||||
|
mockTunnelStatusResponse.mockReturnValueOnce(tunnelResponseGeneric(404, 'Tunnel not found.', new Map()));
|
||||||
|
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||||
|
expect(error.statusCode).toBe(404);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit ngrok error - Tunnel has errored out.', async done => {
|
||||||
|
mockTunnelStatusResponse.mockReturnValueOnce(tunnelResponseGeneric(500, 'Tunnel has errored out.'));
|
||||||
|
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||||
|
expect(error.statusCode).toBe(500);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit ngrok error - Too many connections.', async done => {
|
||||||
|
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||||
|
tunnelResponseGeneric(
|
||||||
|
429,
|
||||||
|
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||||
|
expect(error.statusCode).toBe(429);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check tunnel status every minute and report error', async done => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||||
|
expect(error.statusCode).toBe(429);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||||
|
tunnelResponseGeneric(
|
||||||
|
429,
|
||||||
|
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
jest.advanceTimersByTime(60001);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit onTunnelStatusPing with an error status', async done => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||||
|
tunnelResponseGeneric(
|
||||||
|
429,
|
||||||
|
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ngrok.ngrokEmitter.on('onTunnelStatusPing', (val: TunnelStatus) => {
|
||||||
|
expect(val).toBe(TunnelStatus.Error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
jest.advanceTimersByTime(60001);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check tunnel status every minute.', async done => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
await connectToNgrokInstance(ngrok);
|
||||||
|
ngrok.ngrokEmitter.on('onTunnelStatusPing', (msg: TunnelStatus) => {
|
||||||
|
expect(msg).toEqual(TunnelStatus.Active);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
jest.advanceTimersByTime(60001);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,13 +32,17 @@
|
||||||
//
|
//
|
||||||
import { ChildProcess, spawn } from 'child_process';
|
import { ChildProcess, spawn } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { clearTimeout, setTimeout } from 'timers';
|
||||||
import { platform } from 'os';
|
import { platform } from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { clearTimeout, setTimeout } from 'timers';
|
|
||||||
|
|
||||||
import { uniqueId } from '@bfemulator/sdk-shared';
|
import { uniqueId } from '@bfemulator/sdk-shared';
|
||||||
|
|
||||||
|
import { TunnelInfo, TunnelStatus } from './state/actions/ngrokTunnelActions';
|
||||||
|
import { ensureStoragePath, writeFile, writeStream, FileWriteStream } from './utils';
|
||||||
|
import { PostmanNgrokCollection } from './utils/postmanNgrokCollection';
|
||||||
|
|
||||||
/* eslint-enable typescript/no-var-requires */
|
/* eslint-enable typescript/no-var-requires */
|
||||||
export interface NgrokOptions {
|
export interface NgrokOptions {
|
||||||
addr: number;
|
addr: number;
|
||||||
|
@ -68,6 +72,8 @@ const defaultOptions: Partial<NgrokOptions> = {
|
||||||
|
|
||||||
const bin = 'ngrok' + (platform() === 'win32' ? '.exe' : '');
|
const bin = 'ngrok' + (platform() === 'win32' ? '.exe' : '');
|
||||||
const addrRegExp = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/;
|
const addrRegExp = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/;
|
||||||
|
const logPath: string = path.join(ensureStoragePath(), 'ngrok.log');
|
||||||
|
const postmanCollectionPath: string = path.join(ensureStoragePath(), 'ngrokCollection.json');
|
||||||
|
|
||||||
export const intervals = { retry: 200, expirationPoll: 1000 * 60 * 5, expirationTime: 1000 * 60 * 60 * 8 };
|
export const intervals = { retry: 200, expirationPoll: 1000 * 60 * 5, expirationTime: 1000 * 60 * 60 * 8 };
|
||||||
|
|
||||||
|
@ -80,8 +86,13 @@ export class NgrokInstance {
|
||||||
private ngrokProcess: ChildProcess;
|
private ngrokProcess: ChildProcess;
|
||||||
private tunnels = {};
|
private tunnels = {};
|
||||||
private inspectUrl = '';
|
private inspectUrl = '';
|
||||||
private ngrokStartTime: number;
|
private intervalForHealthCheck: NodeJS.Timer = null;
|
||||||
private ngrokExpirationTimer: NodeJS.Timer;
|
private ws: FileWriteStream = null;
|
||||||
|
private boundCheckTunnelStatus = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.boundCheckTunnelStatus = this.checkTunnelStatus.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
public running(): boolean {
|
public running(): boolean {
|
||||||
return this.ngrokProcess && !!this.ngrokProcess.pid;
|
return this.ngrokProcess && !!this.ngrokProcess.pid;
|
||||||
|
@ -93,7 +104,30 @@ export class NgrokInstance {
|
||||||
return this.pendingConnection;
|
return this.pendingConnection;
|
||||||
}
|
}
|
||||||
await this.getNgrokInspectUrl(options);
|
await this.getNgrokInspectUrl(options);
|
||||||
return this.runTunnel(options);
|
const tunnelInfo: { url; inspectUrl } = await this.runTunnel(options);
|
||||||
|
this.intervalForHealthCheck = setInterval(() => this.boundCheckTunnelStatus(tunnelInfo.url), 60000);
|
||||||
|
return tunnelInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkTunnelStatus(publicUrl: string): Promise<void> {
|
||||||
|
const response: Response = await fetch(publicUrl, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const isErrorResponse =
|
||||||
|
response.status === 429 || response.status === 402 || response.status === 500 || !response.headers.get('Server');
|
||||||
|
if (isErrorResponse) {
|
||||||
|
const errorMessage = await response.text();
|
||||||
|
this.ws.write('-- Tunnel Error Response --');
|
||||||
|
this.ws.write(errorMessage);
|
||||||
|
this.ws.write('-- End Response --');
|
||||||
|
this.ngrokEmitter.emit('onTunnelError', {
|
||||||
|
statusCode: response.status,
|
||||||
|
errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.ngrokEmitter.emit('onTunnelStatusPing', isErrorResponse ? TunnelStatus.Error : TunnelStatus.Active);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disconnect(url?: string) {
|
public async disconnect(url?: string) {
|
||||||
|
@ -108,6 +142,7 @@ export class NgrokInstance {
|
||||||
delete this.tunnels[response.url];
|
delete this.tunnels[response.url];
|
||||||
this.ngrokEmitter.emit('disconnect', response.url);
|
this.ngrokEmitter.emit('disconnect', response.url);
|
||||||
});
|
});
|
||||||
|
clearInterval(this.intervalForHealthCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
public kill() {
|
public kill() {
|
||||||
|
@ -120,10 +155,12 @@ export class NgrokInstance {
|
||||||
this.ngrokProcess.kill();
|
this.ngrokProcess.kill();
|
||||||
this.ngrokProcess = null;
|
this.ngrokProcess = null;
|
||||||
this.tunnels = {};
|
this.tunnels = {};
|
||||||
this.cleanUpNgrokExpirationTimer();
|
this.ws.end();
|
||||||
|
clearInterval(this.intervalForHealthCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getNgrokInspectUrl(opts: NgrokOptions): Promise<{ inspectUrl: string }> {
|
private async getNgrokInspectUrl(opts: NgrokOptions): Promise<{ inspectUrl: string }> {
|
||||||
|
this.ws = writeStream(logPath);
|
||||||
if (this.running()) {
|
if (this.running()) {
|
||||||
return { inspectUrl: this.inspectUrl };
|
return { inspectUrl: this.inspectUrl };
|
||||||
}
|
}
|
||||||
|
@ -157,25 +194,13 @@ export class NgrokInstance {
|
||||||
return { inspectUrl: this.inspectUrl };
|
return { inspectUrl: this.inspectUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks if the ngrok tunnel is due for expiration */
|
private updatePostmanCollectionWithNewUrls(inspectUrl: string): void {
|
||||||
private checkForNgrokExpiration(): void {
|
const postmanCopy = JSON.stringify(PostmanNgrokCollection);
|
||||||
const currentTime = Date.now();
|
const collectionWithUrlReplaced = postmanCopy.replace(/127.0.0.1:4040/g, inspectUrl.replace(/(^\w+:|^)\/\//, ''));
|
||||||
const timeElapsed = currentTime - this.ngrokStartTime;
|
writeFile(postmanCollectionPath, collectionWithUrlReplaced);
|
||||||
if (timeElapsed >= intervals.expirationTime) {
|
|
||||||
this.cleanUpNgrokExpirationTimer();
|
|
||||||
this.ngrokEmitter.emit('expired');
|
|
||||||
} else {
|
|
||||||
this.ngrokExpirationTimer = setTimeout(this.checkForNgrokExpiration.bind(this), intervals.expirationPoll);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears the ngrok expiration timer and resets the tunnel start time */
|
private async runTunnel(opts: NgrokOptions): Promise<{ url: string; inspectUrl: string }> {
|
||||||
private cleanUpNgrokExpirationTimer(): void {
|
|
||||||
this.ngrokStartTime = null;
|
|
||||||
clearTimeout(this.ngrokExpirationTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runTunnel(opts: NgrokOptions): Promise<{ url; inspectUrl }> {
|
|
||||||
let retries = 100;
|
let retries = 100;
|
||||||
const url = `${this.inspectUrl}/api/tunnels`;
|
const url = `${this.inspectUrl}/api/tunnels`;
|
||||||
const body = JSON.stringify(opts);
|
const body = JSON.stringify(opts);
|
||||||
|
@ -208,10 +233,16 @@ export class NgrokInstance {
|
||||||
if (opts.proto === 'http' && opts.bind_tls) {
|
if (opts.proto === 'http' && opts.bind_tls) {
|
||||||
this.tunnels[publicUrl.replace('https', 'http')] = uri + ' (http)';
|
this.tunnels[publicUrl.replace('https', 'http')] = uri + ' (http)';
|
||||||
}
|
}
|
||||||
this.ngrokStartTime = Date.now();
|
|
||||||
this.ngrokExpirationTimer = setTimeout(this.checkForNgrokExpiration.bind(this), intervals.expirationPoll);
|
|
||||||
|
|
||||||
this.ngrokEmitter.emit('connect', publicUrl, this.inspectUrl);
|
this.ngrokEmitter.emit('connect', publicUrl, this.inspectUrl);
|
||||||
|
this.updatePostmanCollectionWithNewUrls(this.inspectUrl);
|
||||||
|
const tunnelDetails: TunnelInfo = {
|
||||||
|
publicUrl,
|
||||||
|
inspectUrl: this.inspectUrl,
|
||||||
|
postmanCollectionPath,
|
||||||
|
logPath,
|
||||||
|
};
|
||||||
|
this.ngrokEmitter.emit('onNewTunnelConnected', tunnelDetails);
|
||||||
|
this.checkTunnelStatus(publicUrl);
|
||||||
this.pendingConnection = null;
|
this.pendingConnection = null;
|
||||||
return { url: publicUrl, inspectUrl: this.inspectUrl };
|
return { url: publicUrl, inspectUrl: this.inspectUrl };
|
||||||
}
|
}
|
||||||
|
@ -220,31 +251,40 @@ export class NgrokInstance {
|
||||||
private spawnNgrok(opts: NgrokOptions): ChildProcess {
|
private spawnNgrok(opts: NgrokOptions): ChildProcess {
|
||||||
const filename = `${opts.path ? path.basename(opts.path) : bin}`;
|
const filename = `${opts.path ? path.basename(opts.path) : bin}`;
|
||||||
const folder = opts.path ? path.dirname(opts.path) : path.join(__dirname, 'bin');
|
const folder = opts.path ? path.dirname(opts.path) : path.join(__dirname, 'bin');
|
||||||
const args = ['start', '--none', '--log=stdout', `--region=${opts.region}`];
|
try {
|
||||||
const ngrokPath = path.join(folder, filename);
|
this.ws.write('Ngrok Logger starting');
|
||||||
if (!existsSync(ngrokPath)) {
|
const args = ['start', '--none', `--log=stdout`, `--region=${opts.region}`];
|
||||||
throw new Error(
|
const ngrokPath = path.join(folder, filename);
|
||||||
`Could not find ngrok executable at path: ${ngrokPath}. ` +
|
if (!existsSync(ngrokPath)) {
|
||||||
`Make sure that the correct path to ngrok is configured in the Emulator app settings. ` +
|
throw new Error(
|
||||||
`Ngrok is required to receive a token from the Bot Framework token service.`
|
`Could not find ngrok executable at path: ${ngrokPath}. ` +
|
||||||
);
|
`Make sure that the correct path to ngrok is configured in the Emulator app settings. ` +
|
||||||
|
`Ngrok is required to receive a token from the Bot Framework token service.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ngrok = spawn(ngrokPath, args, { cwd: folder });
|
||||||
|
// Errors are emitted instead of throwing since ngrok is a long running process
|
||||||
|
ngrok.on('error', e => this.ngrokEmitter.emit('error', e));
|
||||||
|
|
||||||
|
ngrok.on('exit', () => {
|
||||||
|
this.tunnels = {};
|
||||||
|
clearInterval(this.intervalForHealthCheck);
|
||||||
|
this.ngrokEmitter.emit('disconnect');
|
||||||
|
});
|
||||||
|
|
||||||
|
ngrok.on('close', () => {
|
||||||
|
clearInterval(this.intervalForHealthCheck);
|
||||||
|
this.ngrokEmitter.emit('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
ngrok.stdout.on('data', data => {
|
||||||
|
this.ws.write(data.toString() + '\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
ngrok.stderr.on('data', (data: Buffer) => this.ngrokEmitter.emit('error', this.ws.write(data.toString())));
|
||||||
|
return ngrok;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
const ngrok = spawn(ngrokPath, args, { cwd: folder });
|
|
||||||
// Errors are emitted instead of throwing since ngrok is a long running process
|
|
||||||
ngrok.on('error', e => this.ngrokEmitter.emit('error', e));
|
|
||||||
|
|
||||||
ngrok.on('exit', () => {
|
|
||||||
this.tunnels = {};
|
|
||||||
this.cleanUpNgrokExpirationTimer();
|
|
||||||
this.ngrokEmitter.emit('disconnect');
|
|
||||||
});
|
|
||||||
|
|
||||||
ngrok.on('close', () => {
|
|
||||||
this.cleanUpNgrokExpirationTimer();
|
|
||||||
this.ngrokEmitter.emit('close');
|
|
||||||
});
|
|
||||||
|
|
||||||
ngrok.stderr.on('data', (data: Buffer) => this.ngrokEmitter.emit('error', data.toString()));
|
|
||||||
return ngrok;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@ import {
|
||||||
isLocalHostUrl,
|
isLocalHostUrl,
|
||||||
LogItem,
|
LogItem,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
ngrokExpirationItem,
|
|
||||||
textItem,
|
textItem,
|
||||||
} from '@bfemulator/sdk-shared';
|
} from '@bfemulator/sdk-shared';
|
||||||
|
|
||||||
|
@ -182,6 +181,10 @@ export class NgrokService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async pingTunnel(): Promise<void> {
|
||||||
|
await this.ngrok.checkTunnelStatus(this.serviceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
public get ngrokEmitter(): EventEmitter {
|
public get ngrokEmitter(): EventEmitter {
|
||||||
return this.ngrok.ngrokEmitter || undefined;
|
return this.ngrok.ngrokEmitter || undefined;
|
||||||
}
|
}
|
||||||
|
@ -190,11 +193,6 @@ export class NgrokService {
|
||||||
return this.ngrok.running() || false;
|
return this.ngrok.running() || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Logs a message in all active conversations that ngrok has expired */
|
|
||||||
public broadcastNgrokExpired(): void {
|
|
||||||
this.broadcast(ngrokExpirationItem('ngrok tunnel has expired.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Logs messages signifying that ngrok has reconnected in all active conversations */
|
/** Logs messages signifying that ngrok has reconnected in all active conversations */
|
||||||
public broadcastNgrokReconnected(): void {
|
public broadcastNgrokReconnected(): void {
|
||||||
const bypassNgrokLocalhost = getSettings().framework.bypassNgrokLocalhost;
|
const bypassNgrokLocalhost = getSettings().framework.bypassNgrokLocalhost;
|
||||||
|
@ -209,6 +207,12 @@ export class NgrokService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Logs messages signifying that ngrok has reconnected in all active conversations */
|
||||||
|
public broadcastNgrokError(errorMessage: string): void {
|
||||||
|
const { broadcast } = this;
|
||||||
|
broadcast(textItem(LogLevel.Error, errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
/** Logs an item to all open conversations */
|
/** Logs an item to all open conversations */
|
||||||
public broadcast(...logItems: LogItem[]): void {
|
public broadcast(...logItems: LogItem[]): void {
|
||||||
const { conversations } = Emulator.getInstance().server.state;
|
const { conversations } = Emulator.getInstance().server.state;
|
||||||
|
|
|
@ -39,3 +39,4 @@ export * from './savedBotUrlsActions';
|
||||||
export * from './updateActions';
|
export * from './updateActions';
|
||||||
export * from './userActions';
|
export * from './userActions';
|
||||||
export * from './windowStateActions';
|
export * from './windowStateActions';
|
||||||
|
export * from './ngrokTunnelActions';
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import {
|
||||||
|
updateNewTunnelInfo,
|
||||||
|
updateTunnelError,
|
||||||
|
updateTunnelStatus,
|
||||||
|
NgrokTunnelActions,
|
||||||
|
TunnelInfo,
|
||||||
|
TunnelError,
|
||||||
|
TunnelStatus,
|
||||||
|
} from './ngrokTunnelActions';
|
||||||
|
|
||||||
|
describe('Ngrok Tunnel Actions', () => {
|
||||||
|
it('should create an update tunnel info action', () => {
|
||||||
|
const payload: TunnelInfo = {
|
||||||
|
publicUrl: 'https://d1a2bf16.ngrok.io',
|
||||||
|
inspectUrl: 'http://127.0.0.1:4041',
|
||||||
|
logPath: 'ngrok.log',
|
||||||
|
postmanCollectionPath: 'postman.json',
|
||||||
|
};
|
||||||
|
const action = updateNewTunnelInfo(payload);
|
||||||
|
expect(action.type).toBe(NgrokTunnelActions.setDetails);
|
||||||
|
expect(action.payload).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a update tunnel error action', () => {
|
||||||
|
const payload: TunnelError = {
|
||||||
|
statusCode: 402,
|
||||||
|
errorMessage: 'Tunnel has expired',
|
||||||
|
};
|
||||||
|
const action = updateTunnelError(payload);
|
||||||
|
expect(action.type).toBe(NgrokTunnelActions.updateOnError);
|
||||||
|
expect(action.payload).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a tunnel status update action', () => {
|
||||||
|
const mockDate = new Date(1466424490000);
|
||||||
|
jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
|
||||||
|
const expectedStatus: TunnelStatus = TunnelStatus.Active;
|
||||||
|
const action = updateTunnelStatus(expectedStatus);
|
||||||
|
expect(action.type).toBe(NgrokTunnelActions.setStatus);
|
||||||
|
expect(action.payload.timestamp).toBe(new Date().toLocaleString());
|
||||||
|
expect(action.payload.status).toBe(expectedStatus);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
import { Action } from 'redux';
|
||||||
|
|
||||||
|
export enum NgrokTunnelActions {
|
||||||
|
setDetails = 'NgrokTunnel/SET_DETAILS',
|
||||||
|
updateOnError = 'NgrokTunnel/TUNNEL_ERROR',
|
||||||
|
setStatus = 'NgrokTunnel/STATUS_CHECK',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TunnelStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelInfo {
|
||||||
|
publicUrl: string;
|
||||||
|
inspectUrl: string;
|
||||||
|
logPath: string;
|
||||||
|
postmanCollectionPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelError {
|
||||||
|
statusCode: number;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelStatusAndTimestamp {
|
||||||
|
status: TunnelStatus;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NgrokTunnelAction<T> extends Action {
|
||||||
|
type: NgrokTunnelActions;
|
||||||
|
payload: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NgrokTunnelPayloadTypes = TunnelError | TunnelInfo | TunnelStatusAndTimestamp;
|
||||||
|
|
||||||
|
export function updateNewTunnelInfo(payload: TunnelInfo): NgrokTunnelAction<TunnelInfo> {
|
||||||
|
return {
|
||||||
|
type: NgrokTunnelActions.setDetails,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTunnelStatus(tunnelStatus: TunnelStatus): NgrokTunnelAction<TunnelStatusAndTimestamp> {
|
||||||
|
return {
|
||||||
|
type: NgrokTunnelActions.setStatus,
|
||||||
|
payload: {
|
||||||
|
status: tunnelStatus,
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTunnelError(payload: TunnelError): NgrokTunnelAction<TunnelError> {
|
||||||
|
return {
|
||||||
|
type: NgrokTunnelActions.updateOnError,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
|
@ -51,3 +51,4 @@ export * from './theme';
|
||||||
export * from './update';
|
export * from './update';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
export * from './windowState';
|
export * from './windowState';
|
||||||
|
export * from './ngrokTunnel';
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import {
|
||||||
|
TunnelStatus,
|
||||||
|
NgrokTunnelAction,
|
||||||
|
TunnelInfo,
|
||||||
|
NgrokTunnelPayloadTypes,
|
||||||
|
NgrokTunnelActions,
|
||||||
|
TunnelError,
|
||||||
|
TunnelStatusAndTs,
|
||||||
|
} from '../actions/ngrokTunnelActions';
|
||||||
|
|
||||||
|
import { ngrokTunnel, NgrokTunnelState } from './ngrokTunnel';
|
||||||
|
|
||||||
|
describe('Ngrok Tunnel reducer', () => {
|
||||||
|
const DEFAULT_STATE: NgrokTunnelState = {
|
||||||
|
inspectUrl: 'http://127.0.0.1:4040',
|
||||||
|
publicUrl: '',
|
||||||
|
logPath: '',
|
||||||
|
postmanCollectionPath: '',
|
||||||
|
errors: {} as TunnelError,
|
||||||
|
tunnelStatus: TunnelStatus.Inactive,
|
||||||
|
lastTunnelStatusCheckTS: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
const emptyAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
|
||||||
|
type: null,
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
ngrokTunnel({ ...DEFAULT_STATE }, emptyAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tunnel info should be set from payload', () => {
|
||||||
|
const payload: TunnelInfo = {
|
||||||
|
publicUrl: 'https://abc.io/',
|
||||||
|
inspectUrl: 'http://127.0.0.1:4003',
|
||||||
|
logPath: 'logger.txt',
|
||||||
|
postmanCollectionPath: 'postman.json',
|
||||||
|
};
|
||||||
|
const setDetailsAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
|
||||||
|
type: NgrokTunnelActions.setDetails,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
const startingState = { ...DEFAULT_STATE };
|
||||||
|
const endingState = ngrokTunnel(startingState, setDetailsAction);
|
||||||
|
|
||||||
|
expect(endingState.publicUrl).toBe(payload.publicUrl);
|
||||||
|
expect(endingState.logPath).toBe(payload.logPath);
|
||||||
|
expect(endingState.postmanCollectionPath).toBe(payload.postmanCollectionPath);
|
||||||
|
expect(endingState.inspectUrl).toBe(payload.inspectUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tunnel errors should be set from payload', () => {
|
||||||
|
const payload: TunnelError = {
|
||||||
|
statusCode: 422,
|
||||||
|
errorMessage: '<h1>Too many connections<h1>',
|
||||||
|
};
|
||||||
|
const updateErrorAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
|
||||||
|
type: NgrokTunnelActions.updateOnError,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
const startingState = { ...DEFAULT_STATE };
|
||||||
|
const endingState = ngrokTunnel(startingState, updateErrorAction);
|
||||||
|
|
||||||
|
expect(endingState.publicUrl).toBe(DEFAULT_STATE.publicUrl);
|
||||||
|
expect(endingState.errors.statusCode).toBe(payload.statusCode);
|
||||||
|
expect(endingState.errors.errorMessage).toBe(payload.errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tunnel status should be set from payload', () => {
|
||||||
|
const payload: TunnelStatusAndTs = {
|
||||||
|
status: TunnelStatus.Active,
|
||||||
|
ts: '12/27/2019, 1:30:00 PM',
|
||||||
|
};
|
||||||
|
const nextPayload: TunnelStatusAndTs = {
|
||||||
|
status: TunnelStatus.Error,
|
||||||
|
ts: '12/27/2019, 1:33:00 PM',
|
||||||
|
};
|
||||||
|
const actions: NgrokTunnelAction<NgrokTunnelPayloadTypes>[] = [
|
||||||
|
{
|
||||||
|
type: NgrokTunnelActions.setStatus,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NgrokTunnelActions.setStatus,
|
||||||
|
payload: nextPayload,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const startingState = { ...DEFAULT_STATE };
|
||||||
|
const transientState = ngrokTunnel(startingState, actions[0]);
|
||||||
|
expect(transientState.tunnelStatus).toBe(payload.status);
|
||||||
|
|
||||||
|
const finalState = ngrokTunnel(startingState, actions[1]);
|
||||||
|
expect(finalState.tunnelStatus).toBe(nextPayload.status);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import {
|
||||||
|
NgrokTunnelActions,
|
||||||
|
NgrokTunnelAction,
|
||||||
|
NgrokTunnelPayloadTypes,
|
||||||
|
TunnelError,
|
||||||
|
TunnelStatus,
|
||||||
|
TunnelStatusAndTimestamp,
|
||||||
|
} from '../actions/ngrokTunnelActions';
|
||||||
|
|
||||||
|
export interface NgrokTunnelState {
|
||||||
|
errors: TunnelError;
|
||||||
|
publicUrl: string;
|
||||||
|
inspectUrl: string;
|
||||||
|
logPath: string;
|
||||||
|
postmanCollectionPath: string;
|
||||||
|
tunnelStatus: TunnelStatus;
|
||||||
|
lastPingedTimestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STATE: NgrokTunnelState = {
|
||||||
|
inspectUrl: 'http://127.0.0.1:4040',
|
||||||
|
publicUrl: '',
|
||||||
|
logPath: '',
|
||||||
|
postmanCollectionPath: '',
|
||||||
|
errors: {} as TunnelError,
|
||||||
|
tunnelStatus: TunnelStatus.Inactive,
|
||||||
|
lastPingedTimestamp: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ngrokTunnel = (
|
||||||
|
state: NgrokTunnelState = DEFAULT_STATE,
|
||||||
|
action: NgrokTunnelAction<NgrokTunnelPayloadTypes>
|
||||||
|
): NgrokTunnelState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case NgrokTunnelActions.setDetails:
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NgrokTunnelActions.updateOnError:
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
errors: action.payload as TunnelError,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NgrokTunnelActions.setStatus:
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
tunnelStatus: (action.payload as TunnelStatusAndTimestamp).status,
|
||||||
|
lastPingedTimestamp: (action.payload as TunnelStatusAndTimestamp).timestamp,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
|
@ -81,6 +81,8 @@ import {
|
||||||
ResourcesState,
|
ResourcesState,
|
||||||
ThemeState,
|
ThemeState,
|
||||||
UpdateState,
|
UpdateState,
|
||||||
|
NgrokTunnelState,
|
||||||
|
ngrokTunnel,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
|
|
||||||
export interface RootState {
|
export interface RootState {
|
||||||
|
@ -101,6 +103,7 @@ export interface RootState {
|
||||||
settings?: Settings;
|
settings?: Settings;
|
||||||
theme?: ThemeState;
|
theme?: ThemeState;
|
||||||
update?: UpdateState;
|
update?: UpdateState;
|
||||||
|
ngrokTunnel?: NgrokTunnelState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_STATE = {
|
export const DEFAULT_STATE = {
|
||||||
|
@ -140,6 +143,7 @@ function initStore(): Store<RootState> {
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
theme,
|
theme,
|
||||||
update,
|
update,
|
||||||
|
ngrokTunnel,
|
||||||
}),
|
}),
|
||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
applyMiddleware(forwardToRenderer, sagaMiddleware)
|
applyMiddleware(forwardToRenderer, sagaMiddleware)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
import { copyFileAsync } from './copyFileAsync';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
copyFile: (sourcePath: string, destinationPath: string, cb: Function) => {
|
||||||
|
if (!destinationPath || !sourcePath) {
|
||||||
|
cb('Incorrect folder permissions.');
|
||||||
|
}
|
||||||
|
cb('');
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('copy files from source to destination asynchronously', () => {
|
||||||
|
it('should resolve promise if operation was successful', async done => {
|
||||||
|
await expect(copyFileAsync('1.txt', '2.txt')).resolves.toBeUndefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject promise if unsuccessful', async (done: any) => {
|
||||||
|
await expect(copyFileAsync('1.txt', '')).rejects.toBe('Error copying file: Incorrect folder permissions.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export const copyFileAsync = (sourcePath: string, destinationPath: string): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.copyFile(sourcePath, destinationPath, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(`Error copying file: ${err}`);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -48,3 +48,4 @@ export * from './showOpenDialog';
|
||||||
export * from './showSaveDialog';
|
export * from './showSaveDialog';
|
||||||
export * from './writeFile';
|
export * from './writeFile';
|
||||||
export * from './getThemes';
|
export * from './getThemes';
|
||||||
|
export * from './writeStream';
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/* eslint-disable typescript/camelcase */
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
export const PostmanNgrokCollection = {
|
||||||
|
info: {
|
||||||
|
_postman_id: '52c791ee-59ed-4c8b-8ffa-5872edeffe2b',
|
||||||
|
name: 'Emulator Ngrok Postman',
|
||||||
|
description:
|
||||||
|
'The ngrok client exposes a REST API that grants programmatic access to:\r\n\r\n- Collect status and metrics information\r\n- Collect and replay captured requests\r\n- Start and stop tunnels dynamically\r\n\r\nhttps://ngrok.com/docs#client-api',
|
||||||
|
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||||
|
},
|
||||||
|
item: [
|
||||||
|
{
|
||||||
|
name: 'Root entry',
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
header: [],
|
||||||
|
url: {
|
||||||
|
raw: 'http://127.0.0.1:4040/api',
|
||||||
|
protocol: 'http',
|
||||||
|
host: ['127', '0', '0', '1'],
|
||||||
|
port: '4040',
|
||||||
|
path: ['api'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'List tunnels',
|
||||||
|
event: [
|
||||||
|
{
|
||||||
|
listen: 'prerequest',
|
||||||
|
script: {
|
||||||
|
id: '8550ced6-0aa3-4515-a78e-0e5720955ed7',
|
||||||
|
exec: [''],
|
||||||
|
type: 'text/javascript',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listen: 'test',
|
||||||
|
script: {
|
||||||
|
id: 'fc71302b-f079-4dd4-aac7-043fff315c58',
|
||||||
|
exec: [
|
||||||
|
'var jsonData = JSON.parse(responseBody)',
|
||||||
|
'pm.globals.set("tunnel_name", jsonData.tunnels[0].name);',
|
||||||
|
],
|
||||||
|
type: 'text/javascript',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
header: [],
|
||||||
|
url: {
|
||||||
|
raw: 'http://127.0.0.1:4040/api/tunnels',
|
||||||
|
protocol: 'http',
|
||||||
|
host: ['127', '0', '0', '1'],
|
||||||
|
port: '4040',
|
||||||
|
path: ['api', 'tunnels'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create new tunnel',
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
header: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
body: {
|
||||||
|
mode: 'raw',
|
||||||
|
raw:
|
||||||
|
'{\r\n "name": "HTTPS_9090",\r\n "proto": "http",\r\n\t"addr": "9090",\r\n\t"bind_tls": true\t\r\n}',
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
raw: 'http://127.0.0.1:4040/api/tunnels',
|
||||||
|
protocol: 'http',
|
||||||
|
host: ['127', '0', '0', '1'],
|
||||||
|
port: '4040',
|
||||||
|
path: ['api', 'tunnels'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get tunnel details',
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
header: [],
|
||||||
|
url: {
|
||||||
|
raw: 'http://127.0.0.1:4040/api/tunnels/{{tunnel_name}}',
|
||||||
|
protocol: 'http',
|
||||||
|
host: ['127', '0', '0', '1'],
|
||||||
|
port: '4040',
|
||||||
|
path: ['api', 'tunnels', '{{tunnel_name}}'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete tunnel',
|
||||||
|
request: {
|
||||||
|
method: 'DELETE',
|
||||||
|
header: [],
|
||||||
|
body: {
|
||||||
|
mode: 'formdata',
|
||||||
|
formdata: [],
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
raw: 'http://127.0.0.1:4040/api/tunnels/{{tunnel_name}}',
|
||||||
|
protocol: 'http',
|
||||||
|
host: ['127', '0', '0', '1'],
|
||||||
|
port: '4040',
|
||||||
|
path: ['api', 'tunnels', '{{tunnel_name}}'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
protocolProfileBehavior: {},
|
||||||
|
};
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
import { writeStream as writeStreamAsync, FileWriteStream } from './writeStream';
|
||||||
|
|
||||||
|
// eslint-disable-next-line typescript/no-unused-vars
|
||||||
|
let fileMock = '';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
createWriteStream: () => ({
|
||||||
|
write: content => {
|
||||||
|
fileMock += content;
|
||||||
|
},
|
||||||
|
end: () => {
|
||||||
|
fileMock = '';
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fileMock = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('writing content file in chunks', () => {
|
||||||
|
it('should write to file multiple chunks of content', () => {
|
||||||
|
const pathToFile = '/Documents/BorFramework-Emulator/logger.txt';
|
||||||
|
const createStreamSpy = jest.spyOn((fs as any).default, 'createWriteStream');
|
||||||
|
const wsInstance: FileWriteStream = writeStreamAsync(pathToFile);
|
||||||
|
const writeSpy = jest.spyOn(createStreamSpy.mock.results[0].value, 'write');
|
||||||
|
wsInstance.write('test', false);
|
||||||
|
wsInstance.write('test', false);
|
||||||
|
wsInstance.write('test', true);
|
||||||
|
wsInstance.end();
|
||||||
|
expect(createStreamSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(writeSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(writeSpy).toHaveBeenLastCalledWith('test\n');
|
||||||
|
expect(writeSpy).toHaveBeenNthCalledWith(1, 'test');
|
||||||
|
createStreamSpy.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call end on the stream', () => {
|
||||||
|
const pathToFile = '/Documents/BorFramework-Emulator/logger.txt';
|
||||||
|
const createStreamSpy = jest.spyOn((fs as any).default, 'createWriteStream');
|
||||||
|
const wsInstance: FileWriteStream = writeStreamAsync(pathToFile);
|
||||||
|
const endSpy = jest.spyOn(createStreamSpy.mock.results[0].value, 'end');
|
||||||
|
wsInstance.write('test', false);
|
||||||
|
wsInstance.write('test', false);
|
||||||
|
wsInstance.write('test', true);
|
||||||
|
wsInstance.end();
|
||||||
|
expect(endSpy).toHaveBeenCalledTimes(1);
|
||||||
|
createStreamSpy.mockClear();
|
||||||
|
endSpy.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call create stream with the correct path', () => {
|
||||||
|
const pathToFile = '/Documents/BorFramework-Emulator/logger.txt';
|
||||||
|
const createStreamSpy = jest.spyOn((fs as any).default, 'createWriteStream');
|
||||||
|
writeStreamAsync(pathToFile);
|
||||||
|
expect(createStreamSpy).toHaveBeenLastCalledWith(pathToFile);
|
||||||
|
createStreamSpy.mockClear();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
//
|
||||||
|
// Microsoft Bot Framework: http://botframework.com
|
||||||
|
//
|
||||||
|
// Bot Framework Emulator Github:
|
||||||
|
// https://github.com/Microsoft/BotFramwork-Emulator
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// MIT License:
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
// the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
|
||||||
|
export interface FileWriteStream {
|
||||||
|
write: (content: string, newLine?: boolean) => void;
|
||||||
|
end: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writeStream = (pathToFile: string) => {
|
||||||
|
const stream = createWriteStream(pathToFile);
|
||||||
|
|
||||||
|
const write = (content: string, newLine: boolean = true) => {
|
||||||
|
stream.write(content + (newLine ? '\n' : ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
const end = () => {
|
||||||
|
stream.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
write,
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
};
|
|
@ -95,6 +95,7 @@ export const SharedConstants = {
|
||||||
UnlinkFile: 'shell:unlink-file',
|
UnlinkFile: 'shell:unlink-file',
|
||||||
RenameFile: 'shell:rename-file',
|
RenameFile: 'shell:rename-file',
|
||||||
QuitAndInstall: 'shell:quit-and-install',
|
QuitAndInstall: 'shell:quit-and-install',
|
||||||
|
CopyFile: 'shell:copy-file',
|
||||||
},
|
},
|
||||||
|
|
||||||
Emulator: {
|
Emulator: {
|
||||||
|
@ -150,6 +151,8 @@ export const SharedConstants = {
|
||||||
Ngrok: {
|
Ngrok: {
|
||||||
Reconnect: 'ngrok:reconnect',
|
Reconnect: 'ngrok:reconnect',
|
||||||
KillProcess: 'ngrok:killProcess',
|
KillProcess: 'ngrok:killProcess',
|
||||||
|
PingTunnel: 'ngrok:pingTunnel',
|
||||||
|
OpenStatusViewer: 'ngrok:openStatusViewer',
|
||||||
},
|
},
|
||||||
|
|
||||||
Notifications: {
|
Notifications: {
|
||||||
|
@ -201,6 +204,7 @@ export const SharedConstants = {
|
||||||
CONTENT_TYPE_APP_SETTINGS: 'application/vnd.microsoft.bfemulator.document.appsettings',
|
CONTENT_TYPE_APP_SETTINGS: 'application/vnd.microsoft.bfemulator.document.appsettings',
|
||||||
CONTENT_TYPE_WELCOME_PAGE: 'application/vnd.microsoft.bfemulator.document.welcome',
|
CONTENT_TYPE_WELCOME_PAGE: 'application/vnd.microsoft.bfemulator.document.welcome',
|
||||||
CONTENT_TYPE_TRANSCRIPT: 'application/vnd.microsoft.bfemulator.document.transcript',
|
CONTENT_TYPE_TRANSCRIPT: 'application/vnd.microsoft.bfemulator.document.transcript',
|
||||||
|
CONTENT_TYPE_NGROK_DEBUGGER: 'application/vnd.microsoft.bfemulator.document.ngrokDebugger',
|
||||||
},
|
},
|
||||||
Channels: {
|
Channels: {
|
||||||
ReadmeUrl: 'https://raw.githubusercontent.com/Microsoft/BotFramework-Emulator/master/content/CHANNELS.md',
|
ReadmeUrl: 'https://raw.githubusercontent.com/Microsoft/BotFramework-Emulator/master/content/CHANNELS.md',
|
||||||
|
@ -211,6 +215,7 @@ export const SharedConstants = {
|
||||||
DOCUMENT_ID_BOT_SETTINGS: 'bot:settings',
|
DOCUMENT_ID_BOT_SETTINGS: 'bot:settings',
|
||||||
DOCUMENT_ID_WELCOME_PAGE: 'welcome-page',
|
DOCUMENT_ID_WELCOME_PAGE: 'welcome-page',
|
||||||
DOCUMENT_ID_MARKDOWN_PAGE: 'markdown-page',
|
DOCUMENT_ID_MARKDOWN_PAGE: 'markdown-page',
|
||||||
|
DOCUMENT_ID_NGROK_DEBUGGER: 'app:ngrokDebugger',
|
||||||
},
|
},
|
||||||
EditorKeys: [EDITOR_KEY_PRIMARY, EDITOR_KEY_SECONDARY],
|
EditorKeys: [EDITOR_KEY_PRIMARY, EDITOR_KEY_SECONDARY],
|
||||||
NavBarItems: {
|
NavBarItems: {
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -13,8 +13,8 @@
|
||||||
"@bfemulator/sdk-client": "^1.0.0",
|
"@bfemulator/sdk-client": "^1.0.0",
|
||||||
"botframework-schema": "^4.3.4",
|
"botframework-schema": "^4.3.4",
|
||||||
"deep-diff": "^1.0.2",
|
"deep-diff": "^1.0.2",
|
||||||
"react": "^16.8.6",
|
"react": "16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "16.8.6",
|
||||||
"react-json-tree": "^0.11.2"
|
"react-json-tree": "^0.11.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -36,8 +36,8 @@
|
||||||
"@types/deep-diff": "^1.0.0",
|
"@types/deep-diff": "^1.0.0",
|
||||||
"@types/jest": "24.0.13",
|
"@types/jest": "24.0.13",
|
||||||
"@types/lscache": "^1.0.29",
|
"@types/lscache": "^1.0.29",
|
||||||
"@types/react": "~16.3.2",
|
"@types/react": "16.9.17",
|
||||||
"@types/react-dom": "^16.0.4",
|
"@types/react-dom": "16.9.4",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
"babel-preset-react-app": "^3.1.1",
|
"babel-preset-react-app": "^3.1.1",
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -14,8 +14,8 @@
|
||||||
"botframework-schema": "^4.3.4",
|
"botframework-schema": "^4.3.4",
|
||||||
"lscache": "^1.1.0",
|
"lscache": "^1.1.0",
|
||||||
"luis-apis": "2.5.1",
|
"luis-apis": "2.5.1",
|
||||||
"react": "^16.8.6",
|
"react": "16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "16.8.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --hot --inline --mode development --content-base ./public",
|
"start": "webpack-dev-server --hot --inline --mode development --content-base ./public",
|
||||||
|
@ -35,8 +35,8 @@
|
||||||
"@babel/preset-typescript": "^7.1.0",
|
"@babel/preset-typescript": "^7.1.0",
|
||||||
"@types/jest": "24.0.13",
|
"@types/jest": "24.0.13",
|
||||||
"@types/lscache": "^1.0.29",
|
"@types/lscache": "^1.0.29",
|
||||||
"@types/react": "~16.3.2",
|
"@types/react": "16.9.17",
|
||||||
"@types/react-dom": "^16.0.4",
|
"@types/react-dom": "16.9.4",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"copy-webpack-plugin": "^4.5.1",
|
"copy-webpack-plugin": "^4.5.1",
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -12,8 +12,8 @@
|
||||||
"botframework-config": "4.4.0",
|
"botframework-config": "4.4.0",
|
||||||
"botframework-schema": "^4.3.4",
|
"botframework-schema": "^4.3.4",
|
||||||
"qnamaker": "^1.3.0",
|
"qnamaker": "^1.3.0",
|
||||||
"react": "^16.8.6",
|
"react": "16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "16.8.6"
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -35,8 +35,8 @@
|
||||||
"@babel/runtime": "^7.1.5",
|
"@babel/runtime": "^7.1.5",
|
||||||
"@types/jest": "24.0.13",
|
"@types/jest": "24.0.13",
|
||||||
"@types/node": "8.9.3",
|
"@types/node": "8.9.3",
|
||||||
"@types/react": "~16.3.2",
|
"@types/react": "16.9.17",
|
||||||
"@types/react-dom": "^16.0.4",
|
"@types/react-dom": "16.9.4",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
"cross-env": "^5.1.3",
|
"cross-env": "^5.1.3",
|
||||||
"css-loader": "^1.0.1",
|
"css-loader": "^1.0.1",
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -26,8 +26,8 @@
|
||||||
"@babel/preset-env": "^7.1.0",
|
"@babel/preset-env": "^7.1.0",
|
||||||
"@babel/preset-typescript": "^7.1.0",
|
"@babel/preset-typescript": "^7.1.0",
|
||||||
"@types/jest": "24.0.13",
|
"@types/jest": "24.0.13",
|
||||||
"@types/react": "~16.3.2",
|
"@types/react": "16.9.17",
|
||||||
"@types/react-dom": "^16.0.4",
|
"@types/react-dom": "16.9.4",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"enzyme": "^3.3.0",
|
"enzyme": "^3.3.0",
|
||||||
|
@ -56,8 +56,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.5",
|
"@babel/runtime": "^7.1.5",
|
||||||
"@bfemulator/app-shared": "^1.0.0",
|
"@bfemulator/app-shared": "^1.0.0",
|
||||||
"react": "^16.8.6",
|
"react": "16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "16.8.6"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"setupTestFrameworkScriptFile": "../../../../testSetup.js",
|
"setupTestFrameworkScriptFile": "../../../../testSetup.js",
|
||||||
|
|
|
@ -221,7 +221,7 @@ export class Splitter extends React.Component<SplitterProps, SplitterState> {
|
||||||
this.panes[index].ref = ReactDom.findDOMNode(element);
|
this.panes[index].ref = ReactDom.findDOMNode(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onActuatorMouseDown = (e: MouseEvent<HTMLDivElement>, splitterIndex: number): void => {
|
private onActuatorMouseDown = (e: MouseEvent<HTMLDivElement>, splitterIndex: number): void => {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
// cache splitter dimensions
|
// cache splitter dimensions
|
||||||
this.splitters[splitterIndex].dimensions = this.splitters[splitterIndex].ref.getBoundingClientRect();
|
this.splitters[splitterIndex].dimensions = this.splitters[splitterIndex].ref.getBoundingClientRect();
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -15,6 +15,7 @@
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noImplicitThis": false,
|
"noImplicitThis": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
// "strictNullChecks": true,
|
// "strictNullChecks": true,
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
"suppressImplicitAnyIndexErrors": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
|
Загрузка…
Ссылка в новой задаче