SSH Configuration Import Alert Modal (#218)

* add an alert modal for the sshconfig import button

When the sshconfig import button is pressed, there currently is no
visual indicator of what changed. This adds an alert modal to pop up
only in the case where the gui button is used.

* improve alert modal for sshconfig imports

The previous message for SSH configuration imports was vague and did not
provide detailed information as what happened during the import. This
clarifies that by specifying which remotes were deleted, created, and
updated. Updates are only ran and recorded if they would actually change
something.

* fix port value limiting

The SSH config import port limiting was correct but set off a warning in
linters. It has been updated to do the same behavior in a different way.

Also, port limiting was never added to manually adding a new remote.
This change adds it there as well.

* change user-facing term to connection

Previously, the ssh configuration alert modal used to use the word
"remote" to describe connections. "Remote" is the internal name but it
isn't consistent with what is being displayed to users. So it has been
replaced with "Connection" instead to match.

* change remote to connection for ssh import buttons

Like the previous change, the word "remote" was used instead of
"connection." This was for the tooltips added to connections that had
been imported in the connections menu.

* update one more remote -> connection
This commit is contained in:
Sylvie Crowe 2024-01-09 16:13:23 -08:00 committed by GitHub
parent fad48b0d09
commit c2a894b280
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 18 deletions

View File

@ -1004,7 +1004,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Remotes imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
@ -1017,7 +1017,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
<Tooltip
message={
<span>
Remotes imported from an ssh config file can be deleted, but will come back upon
Connections imported from an ssh config file can be deleted, but will come back upon
importing again. They will stay removed if you follow{" "}
<a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
</span>

View File

@ -3811,6 +3811,10 @@ class Model {
this.remotesModel.openEditModal({ ...rview.remoteedit });
}
}
if (interactive && "alertmessage" in update) {
let alertMessage: AlertMessageType = update.alertmessage;
this.showAlert(alertMessage);
}
if ("cmdline" in update) {
this.inputModel.updateCmdLine(update.cmdline);
}
@ -4482,7 +4486,7 @@ class CommandRunner {
}
importSshConfig() {
GlobalModel.submitCommand("remote", "parse", null, null, false);
GlobalModel.submitCommand("remote", "parse", null, { nohist: "1", visual: "1" }, true);
}
screenSelectLine(lineArg: string, focusVal?: string) {

View File

@ -285,6 +285,7 @@ type ModelUpdateType = {
clientdata?: ClientDataType;
historyviewdata?: HistoryViewDataType;
remoteview?: RemoteViewType;
alertmessage?: AlertMessageType;
};
type HistoryViewDataType = {

View File

@ -1255,6 +1255,10 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b
if portVal == 0 && uhPort != 0 {
portVal = uhPort
}
if portVal < 0 || portVal > 65535 {
// 0 is used as a sentinel value for the default in this case
return nil, fmt.Errorf("invalid port argument, \"%d\" is not in the range of 1 to 65535", portVal)
}
sshOpts.SSHPort = portVal
canonicalName = remoteUser + "@" + remoteHost
if portVal != 0 && portVal != 22 {
@ -1514,6 +1518,47 @@ type HostInfoType struct {
Ignore bool
}
func createSshImportSummary(changeList map[string][]string) string {
totalNumChanges := len(changeList["create"]) + len(changeList["delete"]) + len(changeList["update"]) + len(changeList["createErr"]) + len(changeList["deleteErr"]) + len(changeList["updateErr"])
if totalNumChanges == 0 {
return "No changes made from ssh config import"
}
remoteStatusMsgs := map[string]string{
"delete": "Deleted %d connection%s: %s",
"create": "Created %d connection%s: %s",
"update": "Edited %d connection%s: %s",
"deleteErr": "Error deleting %d connection%s: %s",
"createErr": "Error creating %d connection%s: %s",
"updateErr": "Error editing %d connection%s: %s",
}
changeTypeKeys := []string{"delete", "create", "update", "deleteErr", "createErr", "updateErr"}
var outMsgs []string
for _, changeTypeKey := range changeTypeKeys {
changes := changeList[changeTypeKey]
if len(changes) > 0 {
rawStatusMsg := remoteStatusMsgs[changeTypeKey]
var pluralize string
if len(changes) == 1 {
pluralize = ""
} else {
pluralize = "s"
}
newMsg := fmt.Sprintf(rawStatusMsg, len(changes), pluralize, strings.Join(changes, ", "))
outMsgs = append(outMsgs, newMsg)
}
}
var pluralize string
if totalNumChanges == 1 {
pluralize = ""
} else {
pluralize = "s"
}
return fmt.Sprintf("%d connection%s changed:\n\n%s", totalNumChanges, pluralize, strings.Join(outMsgs, "\n\n"))
}
func NewHostInfo(hostName string) (*HostInfoType, error) {
userName, _ := ssh_config.GetStrict(hostName, "User")
if userName == "" {
@ -1539,7 +1584,7 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
// do not make assumptions about port if incorrectly configured
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port\n", hostName, canonicalName, portStr)
}
if int(int16(portVal)) != portVal {
if portVal <= 0 || portVal > 65535 {
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port\n", portVal)
}
}
@ -1603,6 +1648,8 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
hostInfoInConfig[hostInfo.CanonicalName] = hostInfo
}
remoteChangeList := make(map[string][]string)
// remove all previously imported remotes that
// no longer have a canonical pattern in the config files
for importedRemoteCanonicalName, importedRemote := range previouslyImportedRemotes {
@ -1611,17 +1658,17 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
if !importedRemote.Archived && (hostInfo == nil || hostInfo.Ignore) {
err = remote.ArchiveRemote(ctx, importedRemote.RemoteId)
if err != nil {
remoteChangeList["deleteErr"] = append(remoteChangeList["deleteErr"], importedRemote.RemoteCanonicalName)
log.Printf("sshconfig-import: failed to remove remote \"%s\" (%s)\n", importedRemote.RemoteAlias, importedRemote.RemoteCanonicalName)
} else {
remoteChangeList["delete"] = append(remoteChangeList["delete"], importedRemote.RemoteCanonicalName)
log.Printf("sshconfig-import: archived remote \"%s\" (%s)\n", importedRemote.RemoteAlias, importedRemote.RemoteCanonicalName)
}
}
}
var updatedRemotes []string
for _, hostInfo := range parsedHostData {
previouslyImportedRemote := previouslyImportedRemotes[hostInfo.CanonicalName]
updatedRemotes = append(updatedRemotes, hostInfo.CanonicalName)
if hostInfo.Ignore {
log.Printf("sshconfig-import: ignore remote[%s] as specified in config file\n", hostInfo.CanonicalName)
continue
@ -1637,15 +1684,23 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
}
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
if msh == nil {
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
log.Printf("strange, msh for remote %s [%s] not found\n", hostInfo.CanonicalName, previouslyImportedRemote.RemoteId)
continue
} else {
err := msh.UpdateRemote(ctx, editMap)
if err != nil {
log.Printf("error updating remote[%s]: %v\n", hostInfo.CanonicalName, err)
continue
}
}
if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host {
// silently skip this one. it didn't fail, but no changes were needed
continue
}
err := msh.UpdateRemote(ctx, editMap)
if err != nil {
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
log.Printf("error updating remote[%s]: %v\n", hostInfo.CanonicalName, err)
continue
}
remoteChangeList["update"] = append(remoteChangeList["update"], hostInfo.CanonicalName)
log.Printf("sshconfig-import: found previously imported remote with canonical name \"%s\": it has been updated\n", hostInfo.CanonicalName)
} else {
sshOpts := &sstore.SSHOpts{
@ -1674,21 +1729,31 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
}
err := remote.AddRemote(ctx, r, false)
if err != nil {
remoteChangeList["createErr"] = append(remoteChangeList["createErr"], hostInfo.CanonicalName)
log.Printf("sshconfig-import: failed to add remote \"%s\" (%s): it is being skipped\n", hostInfo.Host, hostInfo.CanonicalName)
continue
}
remoteChangeList["create"] = append(remoteChangeList["create"], hostInfo.CanonicalName)
log.Printf("sshconfig-import: created remote \"%s\" (%s)\n", hostInfo.Host, hostInfo.CanonicalName)
}
}
update := &sstore.ModelUpdate{Remotes: remote.GetAllRemoteRuntimeState()}
update.Info = &sstore.InfoMsgType{}
if len(updatedRemotes) == 0 {
update.Info.InfoMsg = "no connections imported from ssh config."
outMsg := createSshImportSummary(remoteChangeList)
visualEdit := resolveBool(pk.Kwargs["visual"], false)
if visualEdit {
update := &sstore.ModelUpdate{}
update.AlertMessage = &sstore.AlertMessageType{
Title: "SSH Config Import",
Message: outMsg,
Markdown: true,
}
return update, nil
} else {
update.Info.InfoMsg = fmt.Sprintf("imported %d connection(s) from ssh config file: %s\n", len(updatedRemotes), strings.Join(updatedRemotes, ", "))
update := &sstore.ModelUpdate{}
update.Info = &sstore.InfoMsgType{}
update.Info.InfoMsg = outMsg
return update, nil
}
return update, nil
}
func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {

View File

@ -60,6 +60,7 @@ type ModelUpdate struct {
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
ScreenTombstones []*ScreenTombstoneType `json:"screentombstones,omitempty"`
SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"`
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
}
func (*ModelUpdate) UpdateType() string {
@ -128,6 +129,13 @@ type RemoteEditType struct {
HasPassword bool `json:"haspassword,omitempty"`
}
type AlertMessageType struct {
Title string `json:"title,omitempty"`
Message string `json:"message"`
Confirm bool `json:"confirm,omitempty"`
Markdown bool `json:"markdown,omitempty"`
}
type InfoMsgType struct {
InfoTitle string `json:"infotitle"`
InfoError string `json:"infoerror,omitempty"`