Refactor webview a bit

feature/ssh-config
Kelvin Schoofs 4 years ago
parent 3c71b84e6d
commit 73b3e2dbe8

@ -6,7 +6,6 @@ import * as vscode from 'vscode';
import { getConfigs } from './config'; import { getConfigs } from './config';
import type { FileSystemConfig } from './fileSystemConfig'; import type { FileSystemConfig } from './fileSystemConfig';
import { censorConfig, Logging } from './logging'; import { censorConfig, Logging } from './logging';
import { navigate } from './settings';
import { toPromise } from './toPromise'; import { toPromise } from './toPromise';
// tslint:disable-next-line:variable-name // tslint:disable-next-line:variable-name
@ -131,7 +130,8 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
const answer = await vscode.window.showWarningMessage( const answer = await vscode.window.showWarningMessage(
`The field 'passphrase' was set to true, but no key was provided for ${config.username}@${config.name}`, 'Configure', 'Ignore'); `The field 'passphrase' was set to true, but no key was provided for ${config.username}@${config.name}`, 'Configure', 'Ignore');
if (answer === 'Configure') { if (answer === 'Configure') {
navigate({ type: 'editconfig', config }); const webview = await import('./webview');
webview.navigate({ type: 'editconfig', config });
return null; return null;
} }
} }

@ -311,7 +311,7 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider
this.openSettings({ config, type: 'editconfig' }); this.openSettings({ config, type: 'editconfig' });
} }
public async openSettings(navigation?: Navigation) { public async openSettings(navigation?: Navigation) {
const { open, navigate } = await import('./settings'); const { open, navigate } = await import('./webview');
return navigation ? navigate(navigation) : open(); return navigation ? navigate(navigation) : open();
} }
} }

@ -4,10 +4,12 @@ import * as path from 'path';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { deleteConfig, loadConfigsRaw, updateConfig } from './config'; import { deleteConfig, loadConfigsRaw, updateConfig } from './config';
import { getLocations } from './fileSystemConfig'; import { getLocations } from './fileSystemConfig';
import { DEBUG, Logging, LOGGING_NO_STACKTRACE } from './logging'; import { DEBUG, Logging as _Logging, LOGGING_NO_STACKTRACE } from './logging';
import { toPromise } from './toPromise'; import { toPromise } from './toPromise';
import type { Message, Navigation } from './webviewMessages'; import type { Message, Navigation } from './webviewMessages';
const Logging = _Logging.scope('WebView');
let webviewPanel: vscode.WebviewPanel | undefined; let webviewPanel: vscode.WebviewPanel | undefined;
let pendingNavigation: Navigation | undefined; let pendingNavigation: Navigation | undefined;
@ -31,20 +33,21 @@ async function getDebugContent(): Promise<string | false> {
body = body.replace(/\$WEBVIEW_CSPEXTRA/g, `connect-src ${URL} ${URL.replace('http://', 'ws://')};`); body = body.replace(/\$WEBVIEW_CSPEXTRA/g, `connect-src ${URL} ${URL.replace('http://', 'ws://')};`);
cb(null, body); cb(null, body);
}).on('error', err => { }).on('error', err => {
console.warn('Error connecting to React dev server:', err); Logging.warning(`Error connecting to React dev server: ${err}`);
cb(new Error('Could not connect to React dev server. Not running?')); cb(new Error('Could not connect to React dev server. Not running?'));
})); }));
} }
export async function open() { export async function open() {
if (!webviewPanel) { if (!webviewPanel) {
webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS Settings', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true }); const extensionPath = getExtensionPath();
webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true });
webviewPanel.onDidDispose(() => webviewPanel = undefined); webviewPanel.onDidDispose(() => webviewPanel = undefined);
if (extensionPath) webviewPanel.iconPath = vscode.Uri.file(path.join(extensionPath, 'resources/icon.svg'));
const { webview } = webviewPanel; const { webview } = webviewPanel;
webview.onDidReceiveMessage(handleMessage); webview.onDidReceiveMessage(handleMessage);
let content = await getDebugContent().catch((e: Error) => (vscode.window.showErrorMessage(e.message), null)); let content = await getDebugContent().catch((e: Error) => (vscode.window.showErrorMessage(e.message), null));
if (!content) { if (!content) {
const extensionPath = getExtensionPath();
if (!extensionPath) throw new Error('Could not get extensionPath'); if (!extensionPath) throw new Error('Could not get extensionPath');
// If we got here, we're either not in debug mode, or something went wrong (and an error message is shown) // If we got here, we're either not in debug mode, or something went wrong (and an error message is shown)
content = fs.readFileSync(path.resolve(extensionPath, 'webview/build/index.html')).toString(); content = fs.readFileSync(path.resolve(extensionPath, 'webview/build/index.html')).toString();
@ -63,19 +66,19 @@ export async function open() {
} }
export async function navigate(navigation: Navigation) { export async function navigate(navigation: Navigation) {
Logging.debug(`Navigation requested: ${JSON.stringify(navigation, null, 4)}`);
pendingNavigation = navigation; pendingNavigation = navigation;
postMessage({ navigation, type: 'navigate' }); postMessage({ navigation, type: 'navigate' });
return open(); return open();
} }
function postMessage<T extends Message>(message: T) { function postMessage<T extends Message>(message: T) {
if (!webviewPanel) return; webviewPanel?.webview.postMessage(message);
webviewPanel.webview.postMessage(message);
} }
async function handleMessage(message: Message): Promise<any> { async function handleMessage(message: Message): Promise<any> {
console.log('Got message:', message); if (!webviewPanel) return Logging.warning(`Got message without webviewPanel: ${JSON.stringify(message, null, 4)}`);
if (message.type === 'navigated') pendingNavigation = undefined; Logging.debug(`Got message: ${JSON.stringify(message, null, 4)}`);
if (pendingNavigation) { if (pendingNavigation) {
postMessage({ postMessage({
type: 'navigate', type: 'navigate',
@ -131,5 +134,22 @@ async function handleMessage(message: Message): Promise<any> {
type: 'promptPathResult', type: 'promptPathResult',
}); });
} }
case 'navigated': {
const { view } = message;
type View = 'startscreen' | 'newconfig' | 'configeditor' | 'configlocator';
let title: string | undefined;
switch (view as View) {
case 'configeditor':
title = 'SSH FS - Edit config';
break;
case 'configlocator':
title = 'SSH FS - Locate config';
break;
case 'newconfig':
title = 'SSH FS - New config';
break;
}
webviewPanel.title = title || 'SSH FS';
}
} }
} }

@ -42,7 +42,7 @@ export interface NavigateMessage {
} }
export interface NavigatedMessage { export interface NavigatedMessage {
type: 'navigated'; type: 'navigated';
navigation: Navigation; view: string;
} }
export interface MessageTypes { export interface MessageTypes {

@ -1,6 +1,6 @@
import { FieldDropdownWithInput } from 'src/FieldTypes/dropdownwithinput'; import { FieldDropdownWithInput } from '../FieldTypes/dropdownwithinput';
import { connect } from 'src/redux'; import { connect } from '../redux';
import { getGroups } from 'src/types/fileSystemConfig'; import { getGroups } from '../types/fileSystemConfig';
export interface StateProps { export interface StateProps {
values: string[]; values: string[];

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { FieldDropdown } from 'src/FieldTypes/dropdown'; import { FieldDropdown } from '../FieldTypes/dropdown';
import { FieldDropdownWithInput } from 'src/FieldTypes/dropdownwithinput'; import { FieldDropdownWithInput } from '../FieldTypes/dropdownwithinput';
import { FieldNumber } from 'src/FieldTypes/number'; import { FieldNumber } from '../FieldTypes/number';
import { FieldPath } from 'src/FieldTypes/path'; import { FieldPath } from '../FieldTypes/path';
import { FieldString } from 'src/FieldTypes/string'; import { FieldString } from '../FieldTypes/string';
import { FileSystemConfig, invalidConfigName } from 'src/types/fileSystemConfig'; import { FileSystemConfig, invalidConfigName } from '../types/fileSystemConfig';
import FieldConfigGroup from './configGroupField'; import FieldConfigGroup from './configGroupField';
import { PROXY_FIELD } from './proxyFields'; import { PROXY_FIELD } from './proxyFields';
@ -113,7 +113,7 @@ export function passphrase(config: FileSystemConfig, onChange: FSCChanged<'passp
export function sftpCommand(config: FileSystemConfig, onChange: FSCChanged<'sftpCommand'>): React.ReactElement { export function sftpCommand(config: FileSystemConfig, onChange: FSCChanged<'sftpCommand'>): React.ReactElement {
const callback = (newValue?: string) => onChange('sftpCommand', newValue); const callback = (newValue?: string) => onChange('sftpCommand', newValue);
const description = 'A command to run on the remote SSH session to start a SFTP session (defaults to sftp subsystem)'; const description = 'A command to run on the remote SSH session to start a SFTP session (defaults to sftp subsystem)';
return <FieldString key="root" label="SFTP Command" value={config.sftpCommand} onChange={callback} optional={true} validator={pathValidator} description={description} /> return <FieldString key="sftpCommand" label="SFTP Command" value={config.sftpCommand} onChange={callback} optional={true} validator={pathValidator} description={description} />
} }
export function sftpSudo(config: FileSystemConfig, onChange: FSCChanged<'sftpSudo'>): React.ReactElement { export function sftpSudo(config: FileSystemConfig, onChange: FSCChanged<'sftpSudo'>): React.ReactElement {

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { FieldGroup } from 'src/FieldTypes/group'; import { FieldGroup } from '../FieldTypes/group';
import { connect, pickProperties } from 'src/redux'; import { connect, pickProperties } from '../redux';
import { FileSystemConfig, formatConfigLocation } from 'src/types/fileSystemConfig'; import { FileSystemConfig, formatConfigLocation } from '../types/fileSystemConfig';
import { IConfigEditorState } from 'src/view'; import type { IConfigEditorState } from '../view';
import { configEditorSetNewConfig, configEditorSetStatusMessage, openStartScreen } from 'src/view/actions'; import { configEditorSetNewConfig, configEditorSetStatusMessage, openStartScreen } from '../view/actions';
import { deleteConfig, saveConfig } from 'src/vscode'; import { deleteConfig, saveConfig } from '../vscode';
import * as Fields from './fields'; import * as Fields from './fields';
import './index.css'; import './index.css';

@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { FieldDropdown } from 'src/FieldTypes/dropdown'; import { FieldDropdown } from '../FieldTypes/dropdown';
import { FieldDropdownWithInput } from 'src/FieldTypes/dropdownwithinput'; import { FieldDropdownWithInput } from '../FieldTypes/dropdownwithinput';
import { FieldNumber } from 'src/FieldTypes/number'; import { FieldNumber } from '../FieldTypes/number';
import { FieldString } from 'src/FieldTypes/string'; import { FieldString } from '../FieldTypes/string';
import { FileSystemConfig } from 'src/types/fileSystemConfig'; import { FileSystemConfig } from '../types/fileSystemConfig';
import { FieldFactory, FSCChanged, FSCChangedMultiple } from './fields'; import { FieldFactory, FSCChanged, FSCChangedMultiple } from './fields';
export function proxy(config: FileSystemConfig, onChange: FSCChanged<'proxy'>): React.ReactElement { export function proxy(config: FileSystemConfig, onChange: FSCChanged<'proxy'>): React.ReactElement {

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { connect } from 'src/redux'; import { connect } from '../redux';
import { FileSystemConfig } from 'src/types/fileSystemConfig'; import type { FileSystemConfig } from '../types/fileSystemConfig';
import { openConfigEditor } from 'src/view/actions'; import { openConfigEditor } from '../view/actions';
import './index.css'; import './index.css';
interface StateProps { interface StateProps {

@ -1,9 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import ConfigList from 'src/ConfigList'; import ConfigList from './ConfigList';
import { connect, pickProperties } from 'src/redux'; import { connect, pickProperties } from './redux';
import { FileSystemConfig, formatConfigLocation } from 'src/types/fileSystemConfig'; import { FileSystemConfig, formatConfigLocation } from './types/fileSystemConfig';
import { IConfigLocatorState } from 'src/view'; import type { IConfigLocatorState } from './view';
import './index.css';
function displayName(config: FileSystemConfig) { function displayName(config: FileSystemConfig) {
return formatConfigLocation(config._location); return formatConfigLocation(config._location);

@ -1,4 +0,0 @@
div.ConfigLocator {
padding: 5px;
}

@ -6,7 +6,7 @@ const CONTEXT = React.createContext<FieldGroup | undefined>(undefined);
export class FieldGroup<T = any> extends React.Component { export class FieldGroup<T = any> extends React.Component {
public static Consumer = CONTEXT.Consumer; public static Consumer = CONTEXT.Consumer;
protected static CONTEXT = CONTEXT; protected static CONTEXT = CONTEXT;
protected fields: Array<FieldBase<T>> = []; protected fields: FieldBase<T>[] = [];
public register(field: FieldBase<T>) { public register(field: FieldBase<T>) {
this.fields.push(field); this.fields.push(field);
} }

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { promptPath } from 'src/vscode'; import { promptPath } from '../vscode';
import { FieldBase } from './base'; import { FieldBase } from './base';
export class FieldPath extends FieldBase<string | undefined> { export class FieldPath extends FieldBase<string | undefined> {

@ -1,4 +0,0 @@
div.Homescreen {
padding: 5px;
}

@ -1,13 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { FieldDropdown } from 'src/FieldTypes/dropdown'; import { FieldDropdown } from './FieldTypes/dropdown';
import { FieldGroup } from 'src/FieldTypes/group'; import { FieldGroup } from './FieldTypes/group';
import { FieldString } from 'src/FieldTypes/string'; import { FieldString } from './FieldTypes/string';
import { connect, pickProperties, State } from 'src/redux'; import { connect, pickProperties, State } from './redux';
import { ConfigLocation, formatConfigLocation, invalidConfigName } from 'src/types/fileSystemConfig'; import { ConfigLocation, formatConfigLocation, invalidConfigName } from './types/fileSystemConfig';
import { INewConfigState } from 'src/view'; import type { INewConfigState } from './view';
import { newConfigSetLocation, newConfigSetName, openConfigEditor, openStartScreen } from 'src/view/actions'; import { newConfigSetLocation, newConfigSetName, openConfigEditor, openStartScreen } from './view/actions';
import { createConfig } from 'src/vscode'; import { createConfig } from './vscode';
import './index.css';
const LOCATION_DESCRIPTION = 'The file or Settings file to add the new configuration to'; const LOCATION_DESCRIPTION = 'The file or Settings file to add the new configuration to';

@ -1,4 +0,0 @@
div.Homescreen {
padding: 5px;
}

@ -1,12 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import ConfigList from 'src/ConfigList'; import ConfigList from './ConfigList';
import { receivedData } from 'src/data/actions'; import { receivedData } from './data/actions';
import { connect, pickProperties } from 'src/redux'; import { connect, pickProperties } from './redux';
import { ConfigLocation, FileSystemConfig, formatConfigLocation, groupByGroup, groupByLocation } from 'src/types/fileSystemConfig'; import { ConfigLocation, FileSystemConfig, formatConfigLocation, groupByGroup, groupByLocation } from './types/fileSystemConfig';
import { IStartScreenState } from 'src/view'; import { IStartScreenState } from './view';
import { openNewConfig, openStartScreen } from 'src/view/actions'; import { openNewConfig, openStartScreen } from './view/actions';
import { API } from 'src/vscode'; import { API } from './vscode';
import './index.css';
interface StateProps { interface StateProps {
configs: FileSystemConfig[]; configs: FileSystemConfig[];
@ -17,7 +16,7 @@ interface DispatchProps {
changeGroupBy(current: string): void; changeGroupBy(current: string): void;
add(): void; add(): void;
} }
class Homescreen extends React.Component<StateProps & DispatchProps> { class Startscreen extends React.Component<StateProps & DispatchProps> {
public componentDidMount() { public componentDidMount() {
this.props.refresh(); this.props.refresh();
} }
@ -43,7 +42,7 @@ class Homescreen extends React.Component<StateProps & DispatchProps> {
public changeGroupBy = () => this.props.changeGroupBy(this.props.groupBy); public changeGroupBy = () => this.props.changeGroupBy(this.props.groupBy);
} }
export default connect(Homescreen)<StateProps, DispatchProps>( export default connect(Startscreen)<StateProps, DispatchProps>(
state => ({ state => ({
...pickProperties(state.data, 'configs'), ...pickProperties(state.data, 'configs'),
...pickProperties(state.view as IStartScreenState, 'groupBy'), ...pickProperties(state.view as IStartScreenState, 'groupBy'),

@ -1,4 +1,4 @@
import { ConfigLocation, FileSystemConfig } from 'src/types/fileSystemConfig'; import type { ConfigLocation, FileSystemConfig } from '../types/fileSystemConfig';
export enum ActionType { export enum ActionType {
RECEIVED_DATA = 'RECEIVED_DATA', RECEIVED_DATA = 'RECEIVED_DATA',

@ -1,11 +1,11 @@
import { Store } from 'redux'; import type { Store } from 'redux';
import { addListener } from 'src/vscode'; import { addListener } from '../vscode';
import * as actions from './actions'; import * as actions from './actions';
export { reducer } from './reducers'; export { reducer } from './reducers';
export { actions }
export * from './state'; export * from './state';
export { actions };
export function initStore(store: Store) { export function initStore(store: Store) {
addListener((msg) => store.dispatch(actions.receivedData(msg.configs, msg.locations)), 'responseData'); addListener((msg) => store.dispatch(actions.receivedData(msg.configs, msg.locations)), 'responseData');

@ -1,4 +1,4 @@
import { ConfigLocation, FileSystemConfig } from 'src/types/fileSystemConfig'; import type { ConfigLocation, FileSystemConfig } from '../types/fileSystemConfig';
export interface IState { export interface IState {
configs: FileSystemConfig[]; configs: FileSystemConfig[];

@ -1,8 +0,0 @@
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'

@ -35,40 +35,17 @@ STORE.dispatch = (action) => {
STORE.subscribe(() => API.setState(STORE.getState())); STORE.subscribe(() => API.setState(STORE.getState()));
API.postMessage({ type: 'navigated', view: STORE.getState().view.view });
// Makes debugging easier (and this is inside our WebView context anyway) // Makes debugging easier (and this is inside our WebView context anyway)
(window as any).STORE = STORE; (window as any).STORE = STORE;
// type GetComponentProps<C> = C extends React.ComponentClass<infer P, any> ? P : never;
type GetComponentProps<C> = C extends React.ComponentClass<infer P, any> ? P : (C extends React.FunctionComponent<infer P2> ? P2 : {}); type GetComponentProps<C> = C extends React.ComponentClass<infer P, any> ? P : (C extends React.FunctionComponent<infer P2> ? P2 : {});
type GetComponentState<C> = C extends React.ComponentClass<any, infer S> ? S : {}; type GetComponentState<C> = C extends React.ComponentClass<any, infer S> ? S : {};
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// type ConnectReturn<TProps> = <C extends (React.ComponentClass | React.FunctionComponent)>(c: C) => React.ComponentClass<Omit<GetComponentProps<C> & TProps, keyof TProps>, GetComponentState<C>>;
/*
export function connect<TStateProps, TDispatchProps = {}, TState = State>(stateToProps: (state: TState) => TStateProps, dispatchToProps?: (dispatch: Dispatch<Action>) => TDispatchProps) {
return realConnect(stateToProps, dispatchToProps) as ConnectReturn<TStateProps & TDispatchProps>;
}
*/
/*
export function connect<TComponent extends (React.ComponentClass<any> | React.FunctionComponent<any>), TStateProps, TDispatchProps = {}, TState = State>(
component: TComponent,
stateToProps: (state: TState) => TStateProps,
dispatchToProps?: (dispatch: Dispatch<Action>, ownProps: Omit<GetComponentProps<TComponent>, keyof TStateProps & TDispatchProps>) => TDispatchProps,
): React.ComponentClass<Omit<GetComponentProps<TComponent> & TStateProps & TDispatchProps, keyof TStateProps & TDispatchProps>, GetComponentState<TComponent>> {
return realConnect(stateToProps, dispatchToProps)(component) as any;
}
*/
/*
type ConnectReturn<C extends (React.ComponentClass<any> | React.FunctionComponent<any>)> = <TStateProps, TDispatchProps = {}, TState = State>(
stateToProps: (state: TState) => TStateProps,
dispatchToProps?: (dispatch: Dispatch<Action>, ownProps: Omit<GetComponentProps<C>, keyof TStateProps & TDispatchProps>) => TDispatchProps,
) => React.ComponentClass<Omit<GetComponentProps<C> & TStateProps & TDispatchProps, keyof TStateProps & TDispatchProps>, GetComponentState<C>>;
*/
type OwnProps<C extends (React.ComponentClass<any> | React.FunctionComponent<any>),P> = Omit<GetComponentProps<C>, keyof P>; type OwnProps<C extends (React.ComponentClass<any> | React.FunctionComponent<any>), P> = Omit<GetComponentProps<C>, keyof P>;
interface ConnectReturn<C extends (React.ComponentClass<any> | React.FunctionComponent<any>)> { interface ConnectReturn<C extends (React.ComponentClass<any> | React.FunctionComponent<any>)> {
<TStateProps, TState = State>( <TStateProps, TState = State>(
@ -84,8 +61,8 @@ export function connect<TComponent extends (React.ComponentClass<any> | React.Fu
return (stateToProps: any, dispatchToProps?: any) => realConnect(stateToProps, dispatchToProps)(component) as any; return (stateToProps: any, dispatchToProps?: any) => realConnect(stateToProps, dispatchToProps)(component) as any;
} }
export function pickProperties<T,K extends keyof T>(obj: T, ...keys: K[]): Pick<T,K> { export function pickProperties<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const res: Pick<T,K> = {} as any; const res: Pick<T, K> = {} as any;
for (const key of keys) { for (const key of keys) {
res[key] = obj[key]; res[key] = obj[key];
} }

@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import ConfigEditor from './ConfigEditor'; import ConfigEditor from './ConfigEditor';
import ConfigLocator from './ConfigLocator'; import ConfigLocator from './ConfigLocator';
import Homescreen from './Homescreen';
import NewConfig from './NewConfig'; import NewConfig from './NewConfig';
import { connect, State } from './redux'; import { connect, State } from './redux';
import Startscreen from './Startscreen';
interface StateProps { interface StateProps {
view: State['view']['view']; view: State['view']['view'];
@ -18,7 +18,7 @@ function Router(props: StateProps) {
return <NewConfig />; return <NewConfig />;
case 'startscreen': case 'startscreen':
default: default:
return <Homescreen />; return <Startscreen />;
} }
} }

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { connect } from "src/redux"; import { connect } from "../redux";
function nop(...args: any) { function nop(...args: any) {
return; return;
@ -15,10 +15,10 @@ function nop(...args: any) {
class Test extends React.Component<S & D & P> { } class Test extends React.Component<S & D & P> { }
const TestC1 = connect(Test)<S>(state => ({ s: 123 })); const TestC1 = connect(Test)<S>(state => ({ s: 123 }));
nop(<TestC1 d={456} p={789} />); nop(<TestC1 d={456} p={789} />);
const TestC2 = connect(Test)<S,D>(state => ({ s: 123 }), dispatch => ({ d: 456 })); const TestC2 = connect(Test)<S, D>(state => ({ s: 123 }), dispatch => ({ d: 456 }));
nop(<TestC2 p={789} />); nop(<TestC2 p={789} />);
const TestC3 = connect(Test)<S, { state: 123}>(state => ({ s: 123 })); const TestC3 = connect(Test)<S, { state: 123 }>(state => ({ s: 123 }));
nop(<TestC3 d={456} p={789} />); nop(<TestC3 d={456} p={789} />);
const TestC4 = connect(Test)<S,D, { state: 123}>(state => ({ s: 123 }), dispatch => ({ d: 456 })); const TestC4 = connect(Test)<S, D, { state: 123 }>(state => ({ s: 123 }), dispatch => ({ d: 456 }));
nop(<TestC4 p={789} />); nop(<TestC4 p={789} />);
} }

@ -1,4 +1,4 @@
import { ConnectConfig } from 'ssh2'; import type { ConnectConfig } from 'ssh2';
export interface ProxyConfig { export interface ProxyConfig {
type: 'socks4' | 'socks5' | 'http'; type: 'socks4' | 'socks5' | 'http';
@ -43,8 +43,8 @@ export function getGroups(configs: FileSystemConfig[], expanded = false): string
return res; return res;
} }
export function groupByLocation(configs: FileSystemConfig[]): Array<[ConfigLocation, FileSystemConfig[]]> { export function groupByLocation(configs: FileSystemConfig[]): [ConfigLocation, FileSystemConfig[]][] {
const res: Array<[ConfigLocation, FileSystemConfig[]]> = []; const res: [ConfigLocation, FileSystemConfig[]][] = [];
function getForLoc(loc: ConfigLocation = 'Unknown') { function getForLoc(loc: ConfigLocation = 'Unknown') {
let found = res.find(([l]) => l === loc); let found = res.find(([l]) => l === loc);
if (found) return found; if (found) return found;
@ -58,8 +58,8 @@ export function groupByLocation(configs: FileSystemConfig[]): Array<[ConfigLocat
return res; return res;
} }
export function groupByGroup(configs: FileSystemConfig[]): Array<[string, FileSystemConfig[]]> { export function groupByGroup(configs: FileSystemConfig[]): [string, FileSystemConfig[]][] {
const res: Array<[string, FileSystemConfig[]]> = []; const res: [string, FileSystemConfig[]][] = [];
function getForGroup(group: string = '') { function getForGroup(group: string = '') {
let found = res.find(([l]) => l === group); let found = res.find(([l]) => l === group);
if (found) return found; if (found) return found;
@ -92,7 +92,7 @@ export interface FileSystemConfig extends ConnectConfig {
privateKeyPath?: string; privateKeyPath?: string;
/** A name of another config to use as a hop */ /** A name of another config to use as a hop */
hop?: string; hop?: string;
/** A command to run on the remote SSH session to start a SFTP session (defaults to sftp subsystem) */ /** The command to run on the remote SSH session to start a SFTP session (defaults to sftp subsystem) */
sftpCommand?: string; sftpCommand?: string;
/** Whether to use a sudo shell (and for which user) to run the sftpCommand in (sftpCommand defaults to /usr/lib/openssh/sftp-server if missing) */ /** Whether to use a sudo shell (and for which user) to run the sftpCommand in (sftpCommand defaults to /usr/lib/openssh/sftp-server if missing) */
sftpSudo?: string | boolean; sftpSudo?: string | boolean;
@ -104,6 +104,8 @@ export interface FileSystemConfig extends ConnectConfig {
_location?: ConfigLocation; _location?: ConfigLocation;
/** Internal property keeping track of where this config comes from (including merges) */ /** Internal property keeping track of where this config comes from (including merges) */
_locations: ConfigLocation[]; _locations: ConfigLocation[];
/** Internal property keeping track of whether this config is an actually calculated one, and if so, which config it originates from (normally itself) */
_calculated?: FileSystemConfig;
} }
export function invalidConfigName(name: string) { export function invalidConfigName(name: string) {

@ -1,4 +1,4 @@
import { ConfigLocation, FileSystemConfig } from './fileSystemConfig'; import type { ConfigLocation, FileSystemConfig } from './fileSystemConfig';
/* Type of messages*/ /* Type of messages*/
@ -42,7 +42,7 @@ export interface NavigateMessage {
} }
export interface NavigatedMessage { export interface NavigatedMessage {
type: 'navigated'; type: 'navigated';
navigation: Navigation; view: string;
} }
export interface MessageTypes { export interface MessageTypes {

@ -1,4 +1,4 @@
import { ConfigLocation, FileSystemConfig } from 'src/types/fileSystemConfig'; import type { ConfigLocation, FileSystemConfig } from '../types/fileSystemConfig';
export enum ActionType { export enum ActionType {
// Startscreen // Startscreen

@ -1,11 +1,11 @@
import { Store } from 'redux'; import type { Store } from 'redux';
import { addListener, API } from 'src/vscode'; import { addListener } from '../vscode';
import * as actions from './actions'; import * as actions from './actions';
export { reducer } from './reducers'; export { reducer } from './reducers';
export { actions }
export * from './state'; export * from './state';
export { actions };
export function initStore(store: Store) { export function initStore(store: Store) {
addListener((msg) => { addListener((msg) => {
@ -24,6 +24,5 @@ export function initStore(store: Store) {
return store.dispatch(actions.openConfigEditor(config)); return store.dispatch(actions.openConfigEditor(config));
} }
} }
API.postMessage({ type: 'navigated', navigation });
}, 'navigate'); }, 'navigate');
} }

@ -1,17 +1,23 @@
import { API } from '../vscode';
import { Action, ActionType } from './actions'; import { Action, ActionType } from './actions';
import { DEFAULT_STATE, IConfigEditorState, INewConfigState, IStartScreenState, IState } from './state'; import { DEFAULT_STATE, IConfigEditorState, INewConfigState, IStartScreenState, IState } from './state';
function setView(state: IState): IState {
API.postMessage({ type: 'navigated', view: state.view });
return state;
}
export function reducer(state = DEFAULT_STATE, action: Action): IState { export function reducer(state = DEFAULT_STATE, action: Action): IState {
switch (action.type) { switch (action.type) {
// Startscreen // Startscreen
case ActionType.OPEN_STARTSCREEN: { case ActionType.OPEN_STARTSCREEN: {
const groupBy = action.groupBy || (state as IStartScreenState).groupBy || 'group'; const groupBy = action.groupBy || (state as IStartScreenState).groupBy || 'group';
return { ...state, view: 'startscreen', groupBy }; return setView({ ...state, view: 'startscreen', groupBy });
} }
// New Config // New Config
case ActionType.OPEN_NEWCONFIG: { case ActionType.OPEN_NEWCONFIG: {
const { name } = action; const { name } = action;
return { ...state, view: 'newconfig', name, location: undefined }; return setView({ ...state, view: 'newconfig', name, location: undefined });
} }
case ActionType.NEWCONFIG_SETNAME: case ActionType.NEWCONFIG_SETNAME:
return { ...state as INewConfigState, name: action.name }; return { ...state as INewConfigState, name: action.name };
@ -20,11 +26,11 @@ export function reducer(state = DEFAULT_STATE, action: Action): IState {
// ConfigEditor // ConfigEditor
case ActionType.OPEN_CONFIGEDITOR: { case ActionType.OPEN_CONFIGEDITOR: {
const { config } = action; const { config } = action;
return { ...state, view: 'configeditor', oldConfig: config, newConfig: config }; return setView({ ...state, view: 'configeditor', oldConfig: config, newConfig: config });
} }
case ActionType.OPEN_CONFIGLOCATOR: { case ActionType.OPEN_CONFIGLOCATOR: {
const { name, configs } = action; const { name, configs } = action;
return { ...state, view: 'configlocator', name, configs }; return setView({ ...state, view: 'configlocator', name, configs });
} }
case ActionType.CONFIGEDITOR_SETNEWCONFIG: case ActionType.CONFIGEDITOR_SETNEWCONFIG:
return { ...state as IConfigEditorState, newConfig: action.config }; return { ...state as IConfigEditorState, newConfig: action.config };

@ -1,4 +1,4 @@
import { ConfigLocation, FileSystemConfig } from 'src/types/fileSystemConfig'; import type { ConfigLocation, FileSystemConfig } from '../types/fileSystemConfig';
interface IViewState<V extends string> { interface IViewState<V extends string> {
view: V; view: V;

@ -1,6 +1,6 @@
import { Message, MessageTypes, PromptPathResultMessage, SaveConfigResultMessage } from 'src/types/webviewMessages'; import type { ConfigLocation, FileSystemConfig } from './types/fileSystemConfig';
import { ConfigLocation, FileSystemConfig } from './types/fileSystemConfig'; import type { Message, MessageTypes, PromptPathResultMessage, SaveConfigResultMessage } from './types/webviewMessages';
interface VSCodeAPI { interface VSCodeAPI {
postMessage(msg: Message): void; postMessage(msg: Message): void;
@ -13,7 +13,7 @@ export const API: VSCodeAPI = acquireVsCodeApi();
export type Listener<T extends Message = Message> = (message: T) => void; export type Listener<T extends Message = Message> = (message: T) => void;
export type Filter = string | ((message: Message) => boolean); export type Filter = string | ((message: Message) => boolean);
let LISTENERS: Array<[Listener, Filter | undefined]> = []; let LISTENERS: [Listener, Filter | undefined][] = [];
export function addListener(listener: Listener): void; export function addListener(listener: Listener): void;
export function addListener<K extends Message['type']>(listener: Listener<MessageTypes[K]>, filter: K): void; export function addListener<K extends Message['type']>(listener: Listener<MessageTypes[K]>, filter: K): void;

@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"outDir": "build/dist", "outDir": "build/dist",
"module": "esnext", "module": "esnext",
"target": "es5", "target": "es5",

@ -7,7 +7,11 @@
"rules": { "rules": {
"interface-name": false, "interface-name": false,
"curly": false, "curly": false,
"no-console": false "no-console": false,
"array-type": [
true,
"array"
]
}, },
"linterOptions": { "linterOptions": {
"exclude": [ "exclude": [

Loading…
Cancel
Save