Connect With Non-Terminal Widgets (#304)

- Adds connection buttons for previews
- Makes it possible for graphs and previews to connect on backend
(without a terminal open to connection)
- Changes the wsh install message
This commit is contained in:
Sylvie Crowe 2024-09-02 12:34:49 -07:00 committed by GitHub
parent 63cfe1d279
commit 226bc4ee6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 99 additions and 10 deletions

View File

@ -353,12 +353,21 @@ const ChangeConnectionBlockModal = React.memo(
}) => {
const [connSelected, setConnSelected] = React.useState("");
const changeConnModalOpen = jotai.useAtomValue(changeConnModalAtom);
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const changeConnection = React.useCallback(
async (connName: string) => {
const oldCwd = blockData?.meta?.file ?? "";
let newCwd: string;
if (oldCwd == "") {
newCwd = "";
} else {
newCwd = "~";
}
await WshServer.SetMetaCommand({
oref: WOS.makeORef("block", blockId),
meta: { connection: connName },
meta: { connection: connName, file: newCwd },
});
await services.BlockService.EnsureConnection(blockId).catch((e) => console.log(e));
await WshServer.ControllerRestartCommand({ blockid: blockId });
},
[blockId]

View File

@ -7,6 +7,9 @@ import * as WOS from "./wos";
// blockservice.BlockService (block)
class BlockServiceType {
EnsureConnection(arg2: string): Promise<void> {
return WOS.callBackendService("block", "EnsureConnection", Array.from(arguments))
}
GetControllerStatus(arg2: string): Promise<BlockControllerRuntimeStatus> {
return WOS.callBackendService("block", "GetControllerStatus", Array.from(arguments))
}

View File

@ -52,6 +52,7 @@ export class PreviewModel implements ViewModel {
previewTextRef: React.RefObject<HTMLDivElement>;
editMode: jotai.Atom<boolean>;
canPreview: jotai.PrimitiveAtom<boolean>;
manageConnection: jotai.Atom<boolean>;
fileName: jotai.Atom<string>;
connection: jotai.Atom<string>;
@ -81,6 +82,7 @@ export class PreviewModel implements ViewModel {
this.ceReadOnly = jotai.atom(true);
this.canPreview = jotai.atom(false);
this.openFileModal = jotai.atom(false);
this.manageConnection = jotai.atom(true);
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.viewIcon = jotai.atom((get) => {
let blockData = get(this.blockAtom);

View File

@ -19,11 +19,13 @@ import (
"time"
"github.com/kevinburke/ssh_config"
"github.com/skeema/knownhosts"
"github.com/wavetermdev/thenextwave/pkg/remote"
"github.com/wavetermdev/thenextwave/pkg/userinput"
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wps"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wshutil"
@ -84,7 +86,7 @@ func (conn *SSHConn) FireConnChangeEvent() {
},
Data: status,
}
log.Printf("connstatus change %q => %s\n", conn.GetName(), status.Status)
log.Printf("sending event: %+#v", event)
wps.Broker.Publish(event)
}
@ -241,7 +243,7 @@ func (conn *SSHConn) StartConnServer() error {
return nil
}
func (conn *SSHConn) checkAndInstallWsh(ctx context.Context) error {
func (conn *SSHConn) checkAndInstallWsh(ctx context.Context, clientDisplayName string) error {
client := conn.GetClient()
if client == nil {
return fmt.Errorf("client is nil")
@ -252,27 +254,33 @@ func (conn *SSHConn) checkAndInstallWsh(ctx context.Context) error {
if err == nil && clientVersion == expectedVersion {
return nil
}
// TODO add some progress to SSHConn about install status
var queryText string
var title string
if err != nil {
queryText = "Waveterm requires `wsh` shell extensions installed on your client to ensure a seamless experience. Would you like to install them?"
title = "Install Wsh Shell Extensions"
queryText = fmt.Sprintf("Wave requires Wave Shell Extensions to be \n"+
"installed on `%s` \n"+
"to ensure a seamless experience. \n\n"+
"Would you like to install them?", clientDisplayName)
title = "Install Wave Shell Extensions"
} else {
queryText = fmt.Sprintf("Waveterm requires `wsh` shell extensions installed on your client to be updated from %s to %s. Would you like to update?", clientVersion, expectedVersion)
title = "Update Wsh Shell Extensions"
queryText = fmt.Sprintf("Wave requires the Wave Shell Extensions \n"+
"installed on `%s` \n"+
"to be updated from %s to %s. \n\n"+
"Would you like to update?", clientDisplayName, clientVersion, expectedVersion)
title = "Update Wave Shell Extensions"
}
request := &userinput.UserInputRequest{
ResponseType: "confirm",
QueryText: queryText,
Title: title,
Markdown: true,
CheckBoxMsg: "Don't show me this again",
}
response, err := userinput.GetUserInput(ctx, request)
if err != nil || !response.Confirm {
return err
}
log.Printf("attempting to install wsh to `%s@%s`", client.User(), client.RemoteAddr().String())
log.Printf("attempting to install wsh to `%s`", clientDisplayName)
clientOs, err := remote.GetClientOs(client)
if err != nil {
return err
@ -346,6 +354,8 @@ func (conn *SSHConn) connectInternal(ctx context.Context) error {
if err != nil {
return err
}
fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String()))
clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr)
conn.WithLock(func() {
conn.Client = client
})
@ -353,7 +363,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context) error {
if err != nil {
return err
}
installErr := conn.checkAndInstallWsh(ctx)
installErr := conn.checkAndInstallWsh(ctx, clientDisplayName)
if installErr != nil {
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr)
}
@ -408,6 +418,48 @@ func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool) *SSH
return conn
}
// Convenience function for ensuring a connection is established
func EnsureConnection(ctx context.Context, blockData *waveobj.Block) error {
connectionName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
if connectionName == "" {
return nil
}
credentialCtx, cancelFunc := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFunc()
opts, err := remote.ParseOpts(connectionName)
if err != nil {
return err
}
conn := GetConn(credentialCtx, opts, true)
statusChan := make(chan string, 1)
go func() {
// we need to wait for connected/disconnected/error
// to ensure the connection has been established before
// continuing in the original thread
for {
// GetStatus has a lock which makes this reasonable to loop over
status := conn.GetStatus()
if credentialCtx.Err() != nil {
// prevent infinite loop from context
statusChan <- Status_Error
return
}
if status == Status_Connected || status == Status_Disconnected || status == Status_Error {
statusChan <- status
return
}
}
}()
status := <-statusChan
if status == Status_Error {
return fmt.Errorf("connection error: %v", conn.Error)
} else if status == Status_Disconnected {
return fmt.Errorf("disconnected: %v", conn.Error)
}
return nil
}
func DisconnectClient(opts *remote.SSHOpts) error {
conn := getConnInternal(opts)
if conn == nil {

View File

@ -11,6 +11,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
@ -83,3 +84,11 @@ func (bs *BlockService) SaveWaveAiData(ctx context.Context, blockId string, hist
}
return nil
}
func (bs *BlockService) EnsureConnection(ctx context.Context, blockId string) error {
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil {
return err
}
return conncontroller.EnsureConnection(ctx, block)
}

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wcore"
@ -133,6 +134,14 @@ func (svc *ObjectService) SetActiveTab(uiContext waveobj.UIContext, tabId string
return nil, fmt.Errorf("error getting tab: %w", err)
}
for _, blockId := range tab.BlockIds {
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil {
return nil, fmt.Errorf("error getting block: %w", err)
}
err = conncontroller.EnsureConnection(ctx, blockData)
if err != nil {
return nil, fmt.Errorf("unable to ensure connection: %v", err)
}
blockErr := blockcontroller.StartBlockController(ctx, tabId, blockId)
if blockErr != nil {
// we don't want to fail the set active tab operation if a block controller fails to start

View File

@ -11,6 +11,7 @@ import (
"github.com/google/uuid"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wps"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
@ -173,6 +174,10 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
if err != nil {
return nil, fmt.Errorf("error creating block: %w", err)
}
err = conncontroller.EnsureConnection(ctx, blockData)
if err != nil {
return nil, fmt.Errorf("unable to ensure connection: %v", err)
}
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
if controllerName != "" {
err = blockcontroller.StartBlockController(ctx, tabId, blockData.OID)