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:
Srinaath Ravichandran 2020-01-10 16:23:19 -08:00 коммит произвёл GitHub
Родитель d496897b5a
Коммит cea9a86b8f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
67 изменённых файлов: 2852 добавлений и 85063 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -11,3 +11,4 @@ build/
.vscode/* .vscode/*
chrome-driver-log.txt chrome-driver-log.txt
.env .env
/packages/**/package-lock.json

1007
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.&nbsp;" 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>
&nbsp;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>
&nbsp;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>
&nbsp;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;
}
}
}

Просмотреть файл

@ -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 */

1860
packages/app/main/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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: {

11916
packages/extensions/json/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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",

11703
packages/extensions/luis/client/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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",

4988
packages/extensions/luis/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

11685
packages/extensions/qnamaker/client/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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",

5166
packages/extensions/qnamaker/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

11019
packages/sdk/client/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

11039
packages/sdk/shared/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

13534
packages/sdk/ui-react/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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();

1451
packages/tools/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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,