Re-add Configure/New command (+ UI navigation/ConfigLocator)

feature/search
Kelvin Schoofs 6 years ago
parent 517a31301e
commit 4a98297155

@ -83,8 +83,7 @@
"commandPalette": [
{
"command": "sshfs.new",
"group": "SSH FS@1",
"when": "view == 'DISABLED'"
"group": "SSH FS@1"
},
{
"command": "sshfs.connect",
@ -100,8 +99,7 @@
},
{
"command": "sshfs.configure",
"group": "SSH FS@5",
"when": "view == 'DISABLED'"
"group": "SSH FS@5"
},
{
"command": "sshfs.reload",
@ -115,7 +113,7 @@
"view/item/context": [
{
"command": "sshfs.new",
"when": "view == 'sshfs-configs' && !viewItem && view == 'DISABLED'"
"when": "view == 'sshfs-configs' && !viewItem"
},
{
"command": "sshfs.connect",
@ -134,7 +132,7 @@
},
{
"command": "sshfs.configure",
"when": "view == 'sshfs-configs' && viewItem && view == 'DISABLED'",
"when": "view == 'sshfs-configs' && viewItem",
"group": "SSH FS@3"
}
]

@ -53,10 +53,8 @@ export function activate(context: vscode.ExtensionContext) {
if (name) func.call(manager, name);
}
registerCommand('sshfs.new', async () => {
const name = await vscode.window.showInputBox({ placeHolder: 'Name for the new SSH file system', validateInput: invalidConfigName });
if (name) vscode.window.showTextDocument(vscode.Uri.parse(`ssh://<config>/${name}.sshfs.jsonc`), { preview: false });
});
registerCommand('sshfs.new', async () => manager.openSettings({ type: 'newconfig' }));
registerCommand('sshfs.settings', () => manager.openSettings());
registerCommand('sshfs.connect', (name?: string) => pickAndClick(manager.commandConnect, name, false));
registerCommand('sshfs.disconnect', (name?: string) => pickAndClick(manager.commandDisconnect, name, true));
@ -65,7 +63,5 @@ export function activate(context: vscode.ExtensionContext) {
registerCommand('sshfs.reload', loadConfigs);
registerCommand('sshfs.settings', () => manager.openSettings());
vscode.window.createTreeView('sshfs-configs', { treeDataProvider: manager });
}

@ -1,11 +1,12 @@
import { Client, ClientChannel } from 'ssh2';
import * as vscode from 'vscode';
import { getConfig, getConfigs, loadConfigs, UPDATE_LISTENERS } from './config';
import { getConfig, getConfigs, loadConfigs, loadConfigsRaw, UPDATE_LISTENERS } from './config';
import { FileSystemConfig } from './fileSystemConfig';
import * as Logging from './logging';
import SSHFileSystem from './sshFileSystem';
import { catchingPromise, toPromise } from './toPromise';
import { Navigation } from './webviewMessages';
async function assertFs(man: Manager, uri: vscode.Uri) {
const fs = await man.getFs(uri);
@ -269,16 +270,16 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid
}
public async commandConfigure(name: string) {
Logging.info(`Command received to configure ${name}`);
// openConfigurationEditor(name);
vscode.window.showWarningMessage('Use the SSH FS config editor to modify/delete configurations');
// TODO: Make this work with the following code:
// this.openSettings(name);
// Would open the Settings UI, list all locations the (potentially) merged config originates from,
// and allow the user to pick (and edit) one of them. Maybe have a back-to-the-list button?
}
public async openSettings() {
const { open } = await import('./settings');
return open(this.context.extensionPath);
name = name.toLowerCase();
let configs = await loadConfigsRaw();
configs = configs.filter(c => c.name === name);
if (configs.length === 0) throw new Error('Unexpectedly found no matching configs?');
const config = configs.length === 1 ? configs[0] : configs;
this.openSettings({ config, type: 'editconfig' });
}
public async openSettings(navigation?: Navigation) {
const { open, navigate } = await import('./settings');
return navigation ? navigate(navigation) : open();
}
}

@ -5,14 +5,20 @@ import * as vscode from 'vscode';
import { deleteConfig, loadConfigsRaw, updateConfig } from './config';
import { getLocations } from './fileSystemConfig';
import { toPromise } from './toPromise';
import { Message } from './webviewMessages';
import { Message, Navigation } from './webviewMessages';
let webviewPanel: vscode.WebviewPanel | undefined;
let pendingNavigation: Navigation | undefined;
// Since the Extension Development Host runs with debugger, we can use this to detect if we're debugging
const DEBUG: number | undefined = process.execArgv.find(a => a.includes('--inspect')) ? 3000 : undefined;
if (DEBUG) console.warn('[vscode-sshfs] Detected we are running in debug mode');
function getExtensionPath(): string | undefined {
const ext = vscode.extensions.getExtension('Kelvin.vscode-sshfs');
return ext && ext.extensionPath;
}
async function getDebugContent(): Promise<string | false> {
if (!DEBUG) return false;
const URL = `http://localhost:${DEBUG}/`;
@ -25,13 +31,15 @@ async function getDebugContent(): Promise<string | false> {
}));
}
export async function open(extensionPath: string) {
export async function open() {
if (!webviewPanel) {
webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS Settings', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true });
webviewPanel.onDidDispose(() => webviewPanel = undefined);
webviewPanel.webview.onDidReceiveMessage(handleMessage);
let content = await getDebugContent().catch((e: Error) => (vscode.window.showErrorMessage(e.message), null));
if (!content) {
const extensionPath = getExtensionPath();
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)
content = fs.readFileSync(path.resolve(extensionPath, 'webview/build/index.html')).toString();
// Built index.html has e.g. `href="/static/js/stuff.js"`, need to make it use vscode-resource: and point to the built static directory
@ -42,13 +50,27 @@ export async function open(extensionPath: string) {
webviewPanel.reveal();
}
function postMessage(message: Message) {
export async function navigate(navigation: Navigation) {
pendingNavigation = navigation;
postMessage({ navigation, type: 'navigate' });
return open();
}
function postMessage<T extends Message>(message: T) {
if (!webviewPanel) return;
webviewPanel.webview.postMessage(message);
}
async function handleMessage(message: Message): Promise<any> {
console.log('Got message:', message);
if (message.type === 'navigated') pendingNavigation = undefined;
if (pendingNavigation) {
postMessage({
type: 'navigate',
navigation: pendingNavigation,
});
pendingNavigation = undefined;
}
switch (message.type) {
case 'requestData': {
const configs = await loadConfigsRaw();

@ -1,5 +1,7 @@
import { ConfigLocation, FileSystemConfig } from './fileSystemConfig';
/* Type of messages*/
export interface RequestDataMessage {
type: 'requestData';
}
@ -34,6 +36,15 @@ export interface PromptPathResultMessage {
uniqueId?: string;
}
export interface NavigateMessage {
type: 'navigate';
navigation: Navigation;
}
export interface NavigatedMessage {
type: 'navigated';
navigation: Navigation;
}
export interface MessageTypes {
requestData: RequestDataMessage;
responseData: ResponseDataMessage;
@ -41,6 +52,19 @@ export interface MessageTypes {
saveConfigResult: SaveConfigResultMessage;
promptPath: PromptPathMessage;
promptPathResult: PromptPathResultMessage;
navigate: NavigateMessage;
navigated: NavigatedMessage;
}
export type Message = MessageTypes[keyof MessageTypes];
/* Types related to NavigateMessage */
export interface NewConfigNavigation {
type: 'newconfig';
}
export interface EditConfigNavigation {
type: 'editconfig';
config: FileSystemConfig | FileSystemConfig[];
}
export type Navigation = NewConfigNavigation | EditConfigNavigation;

@ -12,6 +12,7 @@ interface DispatchProps {
}
interface OwnProps {
configs?: FileSystemConfig[];
displayName?(config: FileSystemConfig): string | undefined;
}
class ConfigList extends React.Component<StateProps & DispatchProps & OwnProps> {
public render() {
@ -24,8 +25,10 @@ class ConfigList extends React.Component<StateProps & DispatchProps & OwnProps>
</div>;
}
public editConfigClickHandler(config: FileSystemConfig) {
const { displayName } = this.props;
const name = displayName && displayName(config) || config.label || config.name;
const onClick = () => this.props.editConfig(config);
return <li key={config.name} onClick={onClick}>{config.label || config.name}</li>;
return <li key={config.name} onClick={onClick}>{name}</li>;
}
}

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

@ -0,0 +1,29 @@
import * as React from 'react';
import ConfigList from 'src/ConfigList';
import { connect, pickProperties } from 'src/redux';
import { FileSystemConfig, formatConfigLocation } from 'src/types/fileSystemConfig';
import { IConfigLocatorState } from 'src/view';
import './index.css';
function displayName(config: FileSystemConfig) {
return formatConfigLocation(config._location);
}
interface StateProps {
configs: FileSystemConfig[];
name: string;
}
class ConfigLocator extends React.Component<StateProps> {
public render() {
const { configs, name } = this.props;
return <div className="ConfigLocator">
<h2>Locations of {name}</h2>
<ConfigList configs={configs} displayName={displayName} />
</div>;
}
}
interface SubState { view: IConfigLocatorState }
export default connect(ConfigLocator)<StateProps, SubState>(
state => pickProperties(state.view, 'configs', 'name'),
);

@ -1,7 +1,7 @@
import * as React from 'react';
import ConfigList from 'src/ConfigList';
import { receivedData } from 'src/data/actions';
import { connect } from 'src/redux';
import { connect, pickProperties } from 'src/redux';
import { ConfigLocation, FileSystemConfig, formatConfigLocation, groupByLocation } from 'src/types/fileSystemConfig';
import { openNewConfig } from 'src/view/actions';
import { API } from 'src/vscode';
@ -9,11 +9,10 @@ import './index.css';
interface StateProps {
configs: FileSystemConfig[];
locations: ConfigLocation[];
}
interface DispatchProps {
refresh(): void;
add(locations: ConfigLocation[]): void;
add(): void;
}
class Homescreen extends React.Component<StateProps & DispatchProps> {
public componentDidMount() {
@ -25,7 +24,7 @@ class Homescreen extends React.Component<StateProps & DispatchProps> {
return <div className="Homescreen">
<h2>Configurations</h2>
<button onClick={this.props.refresh}>Refresh</button>
<button onClick={this.add}>Add</button>
<button onClick={this.props.add}>Add</button>
{grouped.map(([loc, configs]) => this.createGroup(loc, configs))}
</div>;
}
@ -35,13 +34,12 @@ class Homescreen extends React.Component<StateProps & DispatchProps> {
<ConfigList configs={configs} />
</div>;
}
public add = () => this.props.add(this.props.locations);
}
export default connect(Homescreen)<StateProps, DispatchProps>(
state => ({ configs: state.data.configs, locations: state.data.locations }),
state => pickProperties(state.data, 'configs'),
dispatch => ({
add: locations => dispatch(openNewConfig(locations)),
add: () => dispatch(openNewConfig()),
refresh: () => (dispatch(receivedData([], [])), API.postMessage({ type: 'requestData' })),
}),
);

@ -2,7 +2,7 @@ import * as React from 'react';
import { FieldDropdown } from 'src/FieldTypes/dropdown';
import { FieldGroup } from 'src/FieldTypes/group';
import { FieldString } from 'src/FieldTypes/string';
import { connect, pickProperties } from 'src/redux';
import { connect, pickProperties, State } from 'src/redux';
import { ConfigLocation, formatConfigLocation, invalidConfigName } from 'src/types/fileSystemConfig';
import { INewConfigState } from 'src/view';
import { newConfigSetLocation, newConfigSetName, openConfigEditor, openStartScreen } from 'src/view/actions';
@ -53,9 +53,13 @@ class NewConfig extends React.Component<StateProps & DispatchProps> {
public confirm = () => this.props.confirm(this.props.name, this.props.location);
}
interface SubState { view: INewConfigState }
interface SubState extends State { view: INewConfigState }
export default connect(NewConfig)<StateProps, DispatchProps, SubState>(
(state) => pickProperties(state.view, 'location', 'locations', 'name'),
(state) => ({
...pickProperties(state.view, 'name'),
...pickProperties(state.data, 'locations'),
location: state.view.location || state.data.locations[0],
}),
(dispatch) => ({
cancel: () => dispatch(openStartScreen()),
setLocation: loc => dispatch(newConfigSetLocation(loc)),

@ -1,5 +1,6 @@
import * as React from 'react';
import ConfigEditor from './ConfigEditor';
import ConfigLocator from './ConfigLocator';
import Homescreen from './Homescreen';
import NewConfig from './NewConfig';
import { connect, State } from './redux';
@ -11,6 +12,8 @@ function Router(props: StateProps) {
switch (props.view) {
case 'configeditor':
return <ConfigEditor />;
case 'configlocator':
return <ConfigLocator />;
case 'newconfig':
return <NewConfig />;
case 'startscreen':

@ -1,5 +1,7 @@
import { ConfigLocation, FileSystemConfig } from './fileSystemConfig';
/* Type of messages*/
export interface RequestDataMessage {
type: 'requestData';
}
@ -34,6 +36,15 @@ export interface PromptPathResultMessage {
uniqueId?: string;
}
export interface NavigateMessage {
type: 'navigate';
navigation: Navigation;
}
export interface NavigatedMessage {
type: 'navigated';
navigation: Navigation;
}
export interface MessageTypes {
requestData: RequestDataMessage;
responseData: ResponseDataMessage;
@ -41,6 +52,19 @@ export interface MessageTypes {
saveConfigResult: SaveConfigResultMessage;
promptPath: PromptPathMessage;
promptPathResult: PromptPathResultMessage;
navigate: NavigateMessage;
navigated: NavigatedMessage;
}
export type Message = MessageTypes[keyof MessageTypes];
/* Types related to NavigateMessage */
export interface NewConfigNavigation {
type: 'newconfig';
}
export interface EditConfigNavigation {
type: 'editconfig';
config: FileSystemConfig | FileSystemConfig[];
}
export type Navigation = NewConfigNavigation | EditConfigNavigation;

@ -9,6 +9,7 @@ export enum ActionType {
NEWCONFIG_SETLOCATION = 'NEWCONFIG_SETLOCATION',
// ConfigEditor
OPEN_CONFIGEDITOR = 'OPEN_CONFIGEDITOR',
OPEN_CONFIGLOCATOR = 'OPEN_CONFIGLOCATOR',
CONFIGEDITOR_SETNEWCONFIG = 'CONFIGEDITOR_SETNEWCONFIG',
CONFIGEDITOR_SETSTATUSMESSAGE = 'CONFIGEDITOR_SETSTATUSMESSAGE',
}
@ -22,6 +23,7 @@ export interface ActionTypes {
NEWCONFIG_SETLOCATION: IActionNewConfigSetLocation;
// ConfigEditor
OPEN_CONFIGEDITOR: IActionOpenConfigEditor;
OPEN_CONFIGLOCATOR: IActionOpenConfigLocator;
CONFIGEDITOR_SETNEWCONFIG: IActionConfigEditorSetNewConfig;
CONFIGEDITOR_SETSTATUSMESSAGE: IActionConfigEditorSetStatusMessage;
}
@ -44,11 +46,10 @@ export function openStartScreen(): IActionOpenStartscreen {
export interface IActionOpenNewConfig extends IAction {
type: ActionType.OPEN_NEWCONFIG;
locations: ConfigLocation[];
name: string;
}
export function openNewConfig(locations: ConfigLocation[], name = 'unnamed'): IActionOpenNewConfig {
return { type: ActionType.OPEN_NEWCONFIG, locations, name };
export function openNewConfig(name = 'unnamed'): IActionOpenNewConfig {
return { type: ActionType.OPEN_NEWCONFIG, name };
}
export interface IActionNewConfigSetLocation extends IAction {
@ -77,6 +78,15 @@ export function openConfigEditor(config: FileSystemConfig): IActionOpenConfigEdi
return { type: ActionType.OPEN_CONFIGEDITOR, config };
}
export interface IActionOpenConfigLocator extends IAction {
type: ActionType.OPEN_CONFIGLOCATOR;
configs: FileSystemConfig[];
name: string;
}
export function openConfigLocator(configs: FileSystemConfig[], name: string): IActionOpenConfigLocator {
return { type: ActionType.OPEN_CONFIGLOCATOR, configs, name };
}
export interface IActionConfigEditorSetNewConfig extends IAction {
type: ActionType.CONFIGEDITOR_SETNEWCONFIG;
config: FileSystemConfig;

@ -1,5 +1,6 @@
import { Store } from 'redux';
import { addListener, API } from 'src/vscode';
import * as actions from './actions';
export { reducer } from './reducers';
@ -7,5 +8,22 @@ export { actions }
export * from './state';
export function initStore(store: Store) {
// Nothing really
addListener((msg) => {
const { navigation } = msg;
switch (navigation.type) {
case 'newconfig':
return store.dispatch(actions.openNewConfig());
case 'editconfig': {
let { config } = navigation;
if (Array.isArray(config)) {
if (config.length !== 1) {
return store.dispatch(actions.openConfigLocator(config, config[0].name));
}
config = config[0];
}
return store.dispatch(actions.openConfigEditor(config));
}
}
API.postMessage({ type: 'navigated', navigation });
}, 'navigate');
}

@ -8,8 +8,8 @@ export function reducer(state = DEFAULT_STATE, action: Action): IState {
return { ...state, view: 'startscreen' };
// New Config
case ActionType.OPEN_NEWCONFIG: {
const { locations, name } = action;
return { ...state, view: 'newconfig', name, locations, location: locations[0] };
const { name } = action;
return { ...state, view: 'newconfig', name, location: undefined };
}
case ActionType.NEWCONFIG_SETNAME:
return { ...state as INewConfigState, name: action.name };
@ -20,6 +20,10 @@ export function reducer(state = DEFAULT_STATE, action: Action): IState {
const { config } = action;
return { ...state, view: 'configeditor', oldConfig: config, newConfig: config };
}
case ActionType.OPEN_CONFIGLOCATOR: {
const { name, configs } = action;
return { ...state, view: 'configlocator', name, configs };
}
case ActionType.CONFIGEDITOR_SETNEWCONFIG:
return { ...state as IConfigEditorState, newConfig: action.config };
case ActionType.CONFIGEDITOR_SETSTATUSMESSAGE:

@ -7,8 +7,7 @@ interface IViewState<V extends string> {
export type IStartScreenState = IViewState<'startscreen'>;
export interface INewConfigState extends IViewState<'newconfig'> {
locations: ConfigLocation[];
location: ConfigLocation;
location?: ConfigLocation;
name: string;
}
@ -18,7 +17,12 @@ export interface IConfigEditorState extends IViewState<'configeditor'> {
statusMessage?: string;
}
export type IState = IStartScreenState | INewConfigState | IConfigEditorState;
export interface IConfigLocatorState extends IViewState<'configlocator'> {
configs: FileSystemConfig[];
name: string;
}
export type IState = IStartScreenState | INewConfigState | IConfigEditorState | IConfigLocatorState;
export const DEFAULT_STATE: IState = {
view: 'startscreen',

Loading…
Cancel
Save