diff --git a/csharp/ExcelAddIn/StateManager.cs b/csharp/ExcelAddIn/StateManager.cs index a4bb4e44e4e..fafcd158648 100644 --- a/csharp/ExcelAddIn/StateManager.cs +++ b/csharp/ExcelAddIn/StateManager.cs @@ -80,4 +80,8 @@ public void SetDefaultCredentials(CredentialsBase credentials) { public void Reconnect(EndpointId id) { _sessionProviders.Reconnect(id); } + + public void SwitchOnEmpty(EndpointId id, Action onEmpty, Action onNotEmpty) { + _sessionProviders.SwitchOnEmpty(id, onEmpty, onNotEmpty); + } } diff --git a/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs b/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs index cdf6053f327..3df7ee89504 100644 --- a/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs +++ b/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs @@ -1,14 +1,15 @@ -using Deephaven.ExcelAddIn.Viewmodels; +using System.Collections.Concurrent; +using Deephaven.ExcelAddIn.Managers; +using Deephaven.ExcelAddIn.Viewmodels; using Deephaven.ExcelAddIn.ViewModels; using Deephaven.ExcelAddIn.Views; -using System.Diagnostics; -using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Util; namespace Deephaven.ExcelAddIn.Factories; internal static class ConnectionManagerDialogFactory { public static void CreateAndShow(StateManager sm) { + var rowToManager = new ConcurrentDictionary(); + // The "new" button creates a "New/Edit Credentials" dialog void OnNewButtonClicked() { var cvm = CredentialsDialogViewModel.OfEmpty(); @@ -16,87 +17,58 @@ void OnNewButtonClicked() { dialog.Show(); } - var cmDialog = new ConnectionManagerDialog(OnNewButtonClicked); - cmDialog.Show(); - var cmso = new ConnectionManagerSessionObserver(sm, cmDialog); - var disposer = sm.SubscribeToSessions(cmso); - - cmDialog.Closed += (_, _) => { - disposer.Dispose(); - cmso.Dispose(); - }; - } -} - -internal class ConnectionManagerSessionObserver( - StateManager stateManager, - ConnectionManagerDialog cmDialog) : IObserver>, IDisposable { - private readonly List _disposables = new(); - - public void OnNext(AddOrRemove aor) { - if (!aor.IsAdd) { - // TODO(kosak) - Debug.WriteLine("Remove is not handled"); - return; + void OnDeleteButtonClicked(ConnectionManagerDialogRow[] rows) { + foreach (var row in rows) { + if (!rowToManager.TryGetValue(row, out var manager)) { + continue; + } + manager.DoDelete(); + } } - var endpointId = aor.Value; + void OnReconnectButtonClicked(ConnectionManagerDialogRow[] rows) { + foreach (var row in rows) { + if (!rowToManager.TryGetValue(row, out var manager)) { + continue; + } + manager.DoReconnect(); + } + } - var statusRow = new ConnectionManagerDialogRow(endpointId.Id, stateManager); - // We watch for session and credential state changes in our ID - var sessDisposable = stateManager.SubscribeToSession(endpointId, statusRow); - var credDisposable = stateManager.SubscribeToCredentials(endpointId, statusRow); + void OnMakeDefaultButtonClicked(ConnectionManagerDialogRow[] rows) { + // Make the last selected row the default + if (rows.Length == 0) { + return; + } - // And we also watch for credentials changes in the default session (just to keep - // track of whether we are still the default) - var dct = new DefaultCredentialsTracker(statusRow); - var defaultCredDisposable = stateManager.SubscribeToDefaultCredentials(dct); + var row = rows[^1]; + if (!rowToManager.TryGetValue(row, out var manager)) { + return; + } - // We'll do our AddRow on the GUI thread, and, while we're on the GUI thread, we'll add - // our disposables to our saved disposables. - cmDialog.Invoke(() => { - _disposables.Add(sessDisposable); - _disposables.Add(credDisposable); - _disposables.Add(defaultCredDisposable); - cmDialog.AddRow(statusRow); - }); - } + manager.DoSetAsDefault(); + } - public void Dispose() { - // Since the GUI thread is where we added these disposables, the GUI thread is where we will - // access and dispose them. - cmDialog.Invoke(() => { - var temp = _disposables.ToArray(); - _disposables.Clear(); - foreach (var disposable in temp) { - disposable.Dispose(); + void OnEditButtonClicked(ConnectionManagerDialogRow[] rows) { + foreach (var row in rows) { + if (!rowToManager.TryGetValue(row, out var manager)) { + continue; + } + manager.DoEdit(); } - }); - } + } - public void OnCompleted() { - // TODO(kosak) - throw new NotImplementedException(); - } + var cmDialog = new ConnectionManagerDialog(OnNewButtonClicked, OnDeleteButtonClicked, + OnReconnectButtonClicked, OnMakeDefaultButtonClicked, OnEditButtonClicked); + cmDialog.Show(); + var dm = new ConnectionManagerDialogManager(cmDialog, rowToManager, sm); + var disposer = sm.SubscribeToSessions(dm); - public void OnError(Exception error) { - // TODO(kosak) - throw new NotImplementedException(); + cmDialog.Closed += (_, _) => { + disposer.Dispose(); + dm.Dispose(); + }; } } -internal class DefaultCredentialsTracker(ConnectionManagerDialogRow statusRow) : IObserver> { - public void OnNext(StatusOr value) { - statusRow.SetDefaultCredentials(value); - } - public void OnCompleted() { - // TODO(kosak) - throw new NotImplementedException(); - } - - public void OnError(Exception error) { - // TODO(kosak) - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs b/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs index c748847e852..4d179569bff 100644 --- a/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs +++ b/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs @@ -1,4 +1,5 @@ -using Deephaven.ExcelAddIn.Models; +using System.Diagnostics; +using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Util; using Deephaven.ExcelAddIn.ViewModels; using ExcelAddIn.views; diff --git a/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs b/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs new file mode 100644 index 00000000000..8cf9ec5435d --- /dev/null +++ b/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs @@ -0,0 +1,66 @@ +using System.Collections.Concurrent; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Viewmodels; +using Deephaven.ExcelAddIn.Views; +using System.Diagnostics; +using Deephaven.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Managers; + +internal class ConnectionManagerDialogManager( + ConnectionManagerDialog cmDialog, + ConcurrentDictionary rowToManager, + StateManager stateManager) : IObserver>, IDisposable { + private readonly WorkerThread _workerThread = stateManager.WorkerThread; + private readonly Dictionary _idToRow = new(); + private readonly List _disposables = new(); + + public void OnNext(AddOrRemove aor) { + if (_workerThread.InvokeIfRequired(() => OnNext(aor))) { + return; + } + + if (aor.IsAdd) { + var endpointId = aor.Value; + var row = new ConnectionManagerDialogRow(endpointId.Id); + var statusRowManager = ConnectionManagerDialogRowManager.Create(row, endpointId, stateManager); + _ = rowToManager.TryAdd(row, statusRowManager); + _idToRow.Add(endpointId, row); + _disposables.Add(statusRowManager); + + cmDialog.AddRow(row); + return; + } + + // Remove! + if (!_idToRow.Remove(aor.Value, out var rowToDelete) || + !rowToManager.TryRemove(rowToDelete, out var rowManager)) { + return; + } + + cmDialog.RemoveRow(rowToDelete); + rowManager.Dispose(); + } + + public void Dispose() { + // Since the GUI thread is where we added these disposables, the GUI thread is where we will + // access and dispose them. + cmDialog.Invoke(() => { + var temp = _disposables.ToArray(); + _disposables.Clear(); + foreach (var disposable in temp) { + disposable.Dispose(); + } + }); + } + + public void OnCompleted() { + // TODO(kosak) + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + // TODO(kosak) + throw new NotImplementedException(); + } +} diff --git a/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs b/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs new file mode 100644 index 00000000000..366c0c596ab --- /dev/null +++ b/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs @@ -0,0 +1,140 @@ +using Deephaven.ExcelAddIn.Factories; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; +using Deephaven.ExcelAddIn.Viewmodels; +using Deephaven.ExcelAddIn.ViewModels; + +namespace Deephaven.ExcelAddIn.Managers; + +public sealed class ConnectionManagerDialogRowManager : IObserver>, + IObserver>, IObserver, IDisposable { + + public static ConnectionManagerDialogRowManager Create(ConnectionManagerDialogRow row, + EndpointId endpointId, StateManager stateManager) { + var result = new ConnectionManagerDialogRowManager(row, endpointId, stateManager); + result.Resubscribe(); + return result; + } + + private readonly ConnectionManagerDialogRow _row; + private readonly EndpointId _endpointId; + private readonly StateManager _stateManager; + private readonly WorkerThread _workerThread; + private readonly List _disposables = new(); + + private ConnectionManagerDialogRowManager(ConnectionManagerDialogRow row, EndpointId endpointId, + StateManager stateManager) { + _row = row; + _endpointId = endpointId; + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + } + + public void Dispose() { + Unsubcribe(); + } + + private void Resubscribe() { + if (_workerThread.InvokeIfRequired(Resubscribe)) { + return; + } + + if (_disposables.Count != 0) { + throw new Exception("State error: already subscribed"); + } + // We watch for session and credential state changes in our ID + var d1 = _stateManager.SubscribeToSession(_endpointId, this); + var d2 = _stateManager.SubscribeToCredentials(_endpointId, this); + // Now we have a problem. We would also like to watch for credential + // state changes in the default session. But the default session + // has the same observable type (IObservable>) + // as the specific session we are watching. To work around this, + // we create an Observer that translates StatusOr to + // MyWrappedSOSB and then we subscribe to that. + var converter = ObservableConverter.Create( + (StatusOr socb) => new MyWrappedSocb(socb), _workerThread); + var d3 = _stateManager.SubscribeToDefaultCredentials(converter); + var d4 = converter.Subscribe(this); + + _disposables.AddRange(new[] { d1, d2, d3, d4 }); + } + + private void Unsubcribe() { + if (_workerThread.InvokeIfRequired(Unsubcribe)) { + return; + } + var temp = _disposables.ToArray(); + _disposables.Clear(); + + foreach (var disposable in temp) { + disposable.Dispose(); + } + } + + public void OnNext(StatusOr value) { + _row.SetCredentialsSynced(value); + } + + public void OnNext(StatusOr value) { + _row.SetSessionSynced(value); + } + + public void OnNext(MyWrappedSocb value) { + _row.SetDefaultCredentialsSynced(value.Value); + } + + public void DoEdit() { + var creds = _row.GetCredentialsSynced(); + // If we have valid credentials, then make a populated viewmodel. + // If we don't, then make an empty viewmodel with only Id populated. + var cvm = creds.AcceptVisitor( + crs => CredentialsDialogViewModel.OfIdAndCredentials(_endpointId.Id, crs), + _ => CredentialsDialogViewModel.OfIdButOtherwiseEmpty(_endpointId.Id)); + var cd = CredentialsDialogFactory.Create(_stateManager, cvm); + cd.Show(); + } + + public void DoDelete() { + // Strategy: + // 1. Unsubscribe to everything + // 2. If it turns out that we were the last subscriber to the session, then great, the + // delete can proceed. + // 3. Otherwise (there is some other subscriber to the session), then the delete operation + // should be denied. In that case we restore our state by resubscribing to everything. + Unsubcribe(); + + _stateManager.SwitchOnEmpty(_endpointId, () => { }, Resubscribe); + } + + public void DoReconnect() { + _stateManager.Reconnect(_endpointId); + } + + public void DoSetAsDefault() { + // If the connection is already the default, do nothing. + if (_row.IsDefault) { + return; + } + + // If we don't have credentials, then we can't make them the default. + var credentials = _row.GetCredentialsSynced(); + if (!credentials.GetValueOrStatus(out var creds, out _)) { + return; + } + + _stateManager.SetDefaultCredentials(creds); + } + + public void OnCompleted() { + // TODO(kosak) + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + // TODO(kosak) + throw new NotImplementedException(); + } + + public record MyWrappedSocb(StatusOr Value) { + } +} diff --git a/csharp/ExcelAddIn/models/Session.cs b/csharp/ExcelAddIn/models/Session.cs index b3851ef7a77..3934ff74226 100644 --- a/csharp/ExcelAddIn/models/Session.cs +++ b/csharp/ExcelAddIn/models/Session.cs @@ -26,7 +26,13 @@ public override T Visit(Func onCore, Func } public override void Dispose() { - Utility.Exchange(ref _client, null)?.Dispose(); + var temp = Utility.Exchange(ref _client, null); + if (temp == null) { + return; + } + + // Do the actual dispose work on a helper thread. + Utility.RunInBackground(temp.Dispose); } public Client Client { @@ -52,7 +58,13 @@ public override void Dispose() { return; } - Utility.Exchange(ref _sessionManager, null)?.Dispose(); + var temp = Utility.Exchange(ref _sessionManager, null); + if (temp == null) { + return; + } + + // Do the actual dispose work on a helper thread. + Utility.RunInBackground(temp.Dispose); } public SessionManager SessionManager { diff --git a/csharp/ExcelAddIn/models/SimpleModels.cs b/csharp/ExcelAddIn/models/SimpleModels.cs index 02a89b370b7..4593e58fef9 100644 --- a/csharp/ExcelAddIn/models/SimpleModels.cs +++ b/csharp/ExcelAddIn/models/SimpleModels.cs @@ -4,6 +4,10 @@ public record AddOrRemove(bool IsAdd, T Value) { public static AddOrRemove OfAdd(T value) { return new AddOrRemove(true, value); } + + public static AddOrRemove OfRemove(T value) { + return new AddOrRemove(false, value); + } } public record EndpointId(string Id) { diff --git a/csharp/ExcelAddIn/providers/SessionProvider.cs b/csharp/ExcelAddIn/providers/SessionProvider.cs index 11882df8144..16a60e935a1 100644 --- a/csharp/ExcelAddIn/providers/SessionProvider.cs +++ b/csharp/ExcelAddIn/providers/SessionProvider.cs @@ -1,8 +1,8 @@ -using Deephaven.DeephavenClient.ExcelAddIn.Util; +using System.Diagnostics; +using Deephaven.DeephavenClient.ExcelAddIn.Util; using Deephaven.ExcelAddIn.Factories; using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Util; -using System.Net; namespace Deephaven.ExcelAddIn.Providers; @@ -12,7 +12,8 @@ internal class SessionProvider(WorkerThread workerThread) : IObservable> _credentialsObservers = new(); private readonly ObserverContainer> _sessionObservers = new(); /// - /// This is used to ignore the results from multiple invocations of "SetCredentials". + /// This is used to track the results from multiple invocations of "SetCredentials" and + /// to keep only the latest. /// private readonly SimpleAtomicReference _sharedSetCredentialsCookie = new(new object()); @@ -66,7 +67,8 @@ public IDisposable Subscribe(IObserver> observer) { return ActionAsDisposable.Create(() => { workerThread.Invoke(() => { - _sessionObservers.Remove(observer, out _); + _sessionObservers.Remove(observer, out var isLast); + Debug.WriteLine(isLast); }); }); } @@ -90,6 +92,19 @@ public void SetCredentials(CredentialsBase credentials) { Utility.RunInBackground(() => CreateSessionBaseInSeparateThread(credentials)); } + public void SwitchOnEmpty(Action callerOnEmpty, Action callerOnNotEmpty) { + if (workerThread.InvokeIfRequired(() => SwitchOnEmpty(callerOnEmpty, callerOnNotEmpty))) { + return; + } + + if (_credentialsObservers.Count != 0 || _sessionObservers.Count != 0) { + callerOnNotEmpty(); + return; + } + + callerOnEmpty(); + } + void CreateSessionBaseInSeparateThread(CredentialsBase credentials) { // Make a unique sentinel object to indicate that this thread should be // the one privileged to provide the system with the Session corresponding diff --git a/csharp/ExcelAddIn/providers/SessionProviders.cs b/csharp/ExcelAddIn/providers/SessionProviders.cs index 5e5db2366e3..042d0a3d1a7 100644 --- a/csharp/ExcelAddIn/providers/SessionProviders.cs +++ b/csharp/ExcelAddIn/providers/SessionProviders.cs @@ -1,4 +1,5 @@ -using Deephaven.DeephavenClient.ExcelAddIn.Util; +using System.Diagnostics; +using Deephaven.DeephavenClient.ExcelAddIn.Util; using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Util; @@ -10,7 +11,6 @@ internal class SessionProviders(WorkerThread workerThread) : IObservable> _endpointsObservers = new(); public IDisposable Subscribe(IObserver> observer) { - IDisposable? disposable = null; // We need to run this on our worker thread because we want to protect // access to our dictionary. workerThread.Invoke(() => { @@ -25,7 +25,7 @@ public IDisposable Subscribe(IObserver> observer) { return ActionAsDisposable.Create(() => { workerThread.Invoke(() => { - Utility.Exchange(ref disposable, null)?.Dispose(); + _endpointsObservers.Remove(observer, out _); }); }); } @@ -92,6 +92,35 @@ public void Reconnect(EndpointId id) { ApplyTo(id, sp => sp.Reconnect()); } + public void SwitchOnEmpty(EndpointId id, Action callerOnEmpty, Action callerOnNotEmpty) { + if (workerThread.InvokeIfRequired(() => SwitchOnEmpty(id, callerOnEmpty, callerOnNotEmpty))) { + return; + } + + Debug.WriteLine("It's SwitchOnEmpty time"); + if (!_providerMap.TryGetValue(id, out var sp)) { + // No provider. That's weird. callerOnEmpty I guess + callerOnEmpty(); + return; + } + + // Make a wrapped onEmpty that removes stuff from my dictionary and invokes + // the observer, then calls the caller's onEmpty + + Action? myOnEmpty = null; + myOnEmpty = () => { + if (workerThread.InvokeIfRequired(myOnEmpty!)) { + return; + } + _providerMap.Remove(id); + _endpointsObservers.OnNext(AddOrRemove.OfRemove(id)); + callerOnEmpty(); + }; + + sp.SwitchOnEmpty(myOnEmpty, callerOnNotEmpty); + } + + private void ApplyTo(EndpointId id, Action action) { if (workerThread.InvokeIfRequired(() => ApplyTo(id, action))) { return; diff --git a/csharp/ExcelAddIn/util/ObservableConverter.cs b/csharp/ExcelAddIn/util/ObservableConverter.cs new file mode 100644 index 00000000000..530f0785a63 --- /dev/null +++ b/csharp/ExcelAddIn/util/ObservableConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Deephaven.DeephavenClient.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Util; + +internal static class ObservableConverter { + public static ObservableConverter Create( + Func converter, WorkerThread workerThread) { + return new ObservableConverter(converter, workerThread); + } +} + +internal class ObservableConverter(Func converter, WorkerThread workerThread) : + IObserver, IObservable { + private readonly ObserverContainer _observers = new(); + + public void OnNext(TFrom value) { + var converted = converter(value); + _observers.OnNext(converted); + } + + public void OnCompleted() { + _observers.OnCompleted(); + } + + public void OnError(Exception error) { + _observers.OnError(error); + } + + public IDisposable Subscribe(IObserver observer) { + workerThread.Invoke(() => _observers.Add(observer, out _)); + + return ActionAsDisposable.Create(() => { + workerThread.Invoke(() => { + _observers.Remove(observer, out _); + }); + }); + } +} diff --git a/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs b/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs index 0e544131282..8692d3836ac 100644 --- a/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs +++ b/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs @@ -1,14 +1,11 @@ -using Deephaven.ExcelAddIn.Factories; -using Deephaven.ExcelAddIn.ViewModels; -using System.ComponentModel; +using System.ComponentModel; using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Util; namespace Deephaven.ExcelAddIn.Viewmodels; -public sealed class ConnectionManagerDialogRow(string id, StateManager stateManager) : - IObserver>, IObserver>, - INotifyPropertyChanged { +public sealed class ConnectionManagerDialogRow(string id) : INotifyPropertyChanged { + public event PropertyChangedEventHandler? PropertyChanged; private readonly object _sync = new(); @@ -41,40 +38,23 @@ public string ServerType { } } - public bool IsDefault => - _credentials.GetValueOrStatus(out var creds1, out _) && - _defaultCredentials.GetValueOrStatus(out var creds2, out _) && - creds1.Id == creds2.Id; - - public void SettingsClicked() { - var creds = GetCredentialsSynced(); - // If we have valid credentials, - var cvm = creds.AcceptVisitor( - crs => CredentialsDialogViewModel.OfIdAndCredentials(Id, crs), - _ => CredentialsDialogViewModel.OfIdButOtherwiseEmpty(Id)); - var cd = CredentialsDialogFactory.Create(stateManager, cvm); - cd.Show(); - } - - public void ReconnectClicked() { - stateManager.Reconnect(new EndpointId(Id)); - } - - public void IsDefaultClicked() { - // If the box is already checked, do nothing. - if (IsDefault) { - return; + public bool IsDefault { + get { + var creds = GetCredentialsSynced(); + var defaultCreds = GetDefaultCredentialsSynced(); + return creds.GetValueOrStatus(out var creds1, out _) && + defaultCreds.GetValueOrStatus(out var creds2, out _) && + creds1.Id == creds2.Id; } + } - // If we don't have credentials, then we can't make them the default. - if (!_credentials.GetValueOrStatus(out var creds, out _)) { - return; + public StatusOr GetCredentialsSynced() { + lock (_sync) { + return _credentials; } - - stateManager.SetDefaultCredentials(creds); } - public void OnNext(StatusOr value) { + public void SetCredentialsSynced(StatusOr value) { lock (_sync) { _credentials = value; } @@ -83,41 +63,30 @@ public void OnNext(StatusOr value) { OnPropertyChanged(nameof(IsDefault)); } - public void OnNext(StatusOr value) { + public StatusOr GetDefaultCredentialsSynced() { lock (_sync) { - _session = value; + return _defaultCredentials; } - - OnPropertyChanged(nameof(Status)); } - public void SetDefaultCredentials(StatusOr creds) { + public void SetDefaultCredentialsSynced(StatusOr value) { lock (_sync) { - _defaultCredentials = creds; + _defaultCredentials = value; } OnPropertyChanged(nameof(IsDefault)); } - public void OnCompleted() { - // TODO(kosak) - throw new NotImplementedException(); - } - - public void OnError(Exception error) { - // TODO(kosak) - throw new NotImplementedException(); - } - - private StatusOr GetCredentialsSynced() { + public StatusOr GetSessionSynced() { lock (_sync) { - return _credentials; + return _session; } } - private StatusOr GetSessionSynced() { + public void SetSessionSynced(StatusOr value) { lock (_sync) { - return _session; + _session = value; } + OnPropertyChanged(nameof(Status)); } private void OnPropertyChanged(string name) { diff --git a/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs b/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs index 6634640031f..2b3e613c7d3 100644 --- a/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs +++ b/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs @@ -31,7 +31,7 @@ public static CredentialsDialogViewModel OfIdAndCredentials(string id, Credentia result.JsonUrl = corePlus.JsonUrl; result.UserId = corePlus.User; result.Password = corePlus.Password; - result.OperateAs = corePlus.OperateAs; + result.OperateAs = corePlus.OperateAs.Equals(corePlus.User) ? "" : corePlus.OperateAs; result.ValidateCertificate = corePlus.ValidateCertificate; return Unit.Instance; }); @@ -68,15 +68,15 @@ void CheckMissing(string field, string name) { } } - CheckMissing(_id, "Connection Id"); + CheckMissing(Id, "Connection Id"); if (!_isCorePlus) { - CheckMissing(_connectionString, "Connection String"); + CheckMissing(ConnectionString, "Connection String"); } else { - CheckMissing(_jsonUrl, "JSON URL"); - CheckMissing(_userId, "User Id"); - CheckMissing(_password, "Password"); - CheckMissing(_operateAs, "Operate As"); + CheckMissing(JsonUrl, "JSON URL"); + CheckMissing(UserId, "User Id"); + CheckMissing(Password, "Password"); + CheckMissing(OperateAsToUse, "Operate As"); } if (missingFields.Count > 0) { @@ -86,8 +86,8 @@ void CheckMissing(string field, string name) { var epId = new EndpointId(_id); result = _isCorePlus - ? CredentialsBase.OfCorePlus(epId, _jsonUrl, _userId, _password, _operateAs, _validateCertificate) - : CredentialsBase.OfCore(epId, _connectionString, _sessionTypeIsPython); + ? CredentialsBase.OfCorePlus(epId, JsonUrl, UserId, Password, OperateAsToUse, ValidateCertificate) + : CredentialsBase.OfCore(epId, ConnectionString, SessionTypeIsPython); return true; } @@ -207,6 +207,8 @@ public string OperateAs { } } + public string OperateAsToUse => _operateAs.Length != 0 ? _operateAs : UserId; + public bool ValidateCertificate { get => _validateCertificate; set { diff --git a/csharp/ExcelAddIn/views/ConnectionManagerDialog.Designer.cs b/csharp/ExcelAddIn/views/ConnectionManagerDialog.Designer.cs index fee2096228f..604c97b2d12 100644 --- a/csharp/ExcelAddIn/views/ConnectionManagerDialog.Designer.cs +++ b/csharp/ExcelAddIn/views/ConnectionManagerDialog.Designer.cs @@ -27,6 +27,10 @@ private void InitializeComponent() { dataGridView1 = new DataGridView(); newButton = new Button(); connectionsLabel = new Label(); + editButton = new Button(); + deleteButton = new Button(); + reconnectButton = new Button(); + makeDefaultButton = new Button(); ((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit(); SuspendLayout(); // @@ -34,21 +38,25 @@ private void InitializeComponent() { // dataGridView1.AllowUserToAddRows = false; dataGridView1.AllowUserToDeleteRows = false; + dataGridView1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; dataGridView1.Location = new Point(68, 83); dataGridView1.Name = "dataGridView1"; dataGridView1.ReadOnly = true; dataGridView1.RowHeadersWidth = 62; + dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dataGridView1.Size = new Size(979, 454); dataGridView1.TabIndex = 0; // // newButton // - newButton.Location = new Point(869, 560); + newButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + newButton.Location = new Point(919, 560); newButton.Name = "newButton"; - newButton.Size = new Size(178, 34); - newButton.TabIndex = 1; - newButton.Text = "New Connection"; + newButton.Size = new Size(128, 34); + newButton.TabIndex = 5; + newButton.Text = "New..."; newButton.UseVisualStyleBackColor = true; newButton.Click += newButton_Click; // @@ -62,11 +70,59 @@ private void InitializeComponent() { connectionsLabel.TabIndex = 2; connectionsLabel.Text = "Connections"; // + // editButton + // + editButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + editButton.Location = new Point(776, 560); + editButton.Name = "editButton"; + editButton.Size = new Size(112, 34); + editButton.TabIndex = 4; + editButton.Text = "Edit..."; + editButton.UseVisualStyleBackColor = true; + editButton.Click += editButton_Click; + // + // deleteButton + // + deleteButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + deleteButton.Location = new Point(339, 560); + deleteButton.Name = "deleteButton"; + deleteButton.Size = new Size(112, 34); + deleteButton.TabIndex = 1; + deleteButton.Text = "Delete"; + deleteButton.UseVisualStyleBackColor = true; + deleteButton.Click += deleteButton_Click; + // + // reconnectButton + // + reconnectButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + reconnectButton.Location = new Point(636, 560); + reconnectButton.Name = "reconnectButton"; + reconnectButton.Size = new Size(112, 34); + reconnectButton.TabIndex = 3; + reconnectButton.Text = "Reconnect"; + reconnectButton.UseVisualStyleBackColor = true; + reconnectButton.Click += reconnectButton_Click; + // + // makeDefaultButton + // + makeDefaultButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + makeDefaultButton.Location = new Point(473, 560); + makeDefaultButton.Name = "makeDefaultButton"; + makeDefaultButton.Size = new Size(139, 34); + makeDefaultButton.TabIndex = 2; + makeDefaultButton.Text = "Make Default"; + makeDefaultButton.UseVisualStyleBackColor = true; + makeDefaultButton.Click += makeDefaultButton_Click; + // // ConnectionManagerDialog // AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(1115, 615); + Controls.Add(makeDefaultButton); + Controls.Add(reconnectButton); + Controls.Add(deleteButton); + Controls.Add(editButton); Controls.Add(connectionsLabel); Controls.Add(newButton); Controls.Add(dataGridView1); @@ -83,5 +139,9 @@ private void InitializeComponent() { private DataGridView dataGridView1; private Button newButton; private Label connectionsLabel; + private Button editButton; + private Button deleteButton; + private Button reconnectButton; + private Button makeDefaultButton; } } \ No newline at end of file diff --git a/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs b/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs index 750fd39e0de..a3177133202 100644 --- a/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs +++ b/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs @@ -2,74 +2,83 @@ namespace Deephaven.ExcelAddIn.Views; +using SelectedRowsAction = Action; + public partial class ConnectionManagerDialog : Form { private const string IsDefaultColumnName = "IsDefault"; - private const string SettingsButtonColumnName = "settings_button_column"; - private const string ReconnectButtonColumnName = "reconnect_button_column"; private readonly Action _onNewButtonClicked; + private readonly SelectedRowsAction _onDeleteButtonClicked; + private readonly SelectedRowsAction _onReconnectButtonClicked; + private readonly SelectedRowsAction _onMakeDefaultButtonClicked; + private readonly SelectedRowsAction _onEditButtonClicked; private readonly BindingSource _bindingSource = new(); - public ConnectionManagerDialog(Action onNewButtonClicked) { + public ConnectionManagerDialog(Action onNewButtonClicked, + SelectedRowsAction onDeleteButtonClicked, + SelectedRowsAction onReconnectButtonClicked, + SelectedRowsAction onMakeDefaultButtonClicked, + SelectedRowsAction onEditButtonClicked) { _onNewButtonClicked = onNewButtonClicked; + _onDeleteButtonClicked = onDeleteButtonClicked; + _onReconnectButtonClicked = onReconnectButtonClicked; + _onMakeDefaultButtonClicked = onMakeDefaultButtonClicked; + _onEditButtonClicked = onEditButtonClicked; InitializeComponent(); _bindingSource.DataSource = typeof(ConnectionManagerDialogRow); dataGridView1.DataSource = _bindingSource; - - var settingsButtonColumn = new DataGridViewButtonColumn { - Name = SettingsButtonColumnName, - HeaderText = "Credentials", - Text = "Edit", - UseColumnTextForButtonValue = true - }; - - var reconnectButtonColumn = new DataGridViewButtonColumn { - Name = ReconnectButtonColumnName, - HeaderText = "Reconnect", - Text = "Reconnect", - UseColumnTextForButtonValue = true - }; - - dataGridView1.Columns.Add(settingsButtonColumn); - dataGridView1.Columns.Add(reconnectButtonColumn); - - dataGridView1.CellClick += DataGridView1_CellClick; } public void AddRow(ConnectionManagerDialogRow row) { + if (InvokeRequired) { + Invoke(() => AddRow(row)); + return; + } _bindingSource.Add(row); + dataGridView1.ClearSelection(); } - private void DataGridView1_CellClick(object? sender, DataGridViewCellEventArgs e) { - if (e.RowIndex < 0) { + public void RemoveRow(ConnectionManagerDialogRow row) { + if (InvokeRequired) { + Invoke(() => RemoveRow(row)); return; } + _bindingSource.Remove(row); + } - if (_bindingSource[e.RowIndex] is not ConnectionManagerDialogRow row) { - return; - } - var name = dataGridView1.Columns[e.ColumnIndex].Name; + private void newButton_Click(object sender, EventArgs e) { + _onNewButtonClicked(); + } - switch (name) { - case SettingsButtonColumnName: { - row.SettingsClicked(); - break; - } + private void reconnectButton_Click(object sender, EventArgs e) { + var selections = GetSelectedRows(); + _onReconnectButtonClicked(selections); + } - case ReconnectButtonColumnName: { - row.ReconnectClicked(); - break; - } + private void editButton_Click(object sender, EventArgs e) { + var selections = GetSelectedRows(); + _onEditButtonClicked(selections); + } - case IsDefaultColumnName: { - row.IsDefaultClicked(); - break; - } - } + private void deleteButton_Click(object sender, EventArgs e) { + var selections = GetSelectedRows(); + _onDeleteButtonClicked(selections); } - private void newButton_Click(object sender, EventArgs e) { - _onNewButtonClicked(); + private void makeDefaultButton_Click(object sender, EventArgs e) { + var selections = GetSelectedRows(); + _onMakeDefaultButtonClicked(selections); + } + + private ConnectionManagerDialogRow[] GetSelectedRows() { + var result = new List(); + var sr = dataGridView1.SelectedRows; + var count = sr.Count; + for (var i = 0; i != count; ++i) { + result.Add((ConnectionManagerDialogRow)sr[i].DataBoundItem); + } + + return result.ToArray(); } } diff --git a/csharp/ExcelAddIn/views/ConnectionManagerDialog.resx b/csharp/ExcelAddIn/views/ConnectionManagerDialog.resx index b3e33e7e100..7f2cf2b8014 100644 --- a/csharp/ExcelAddIn/views/ConnectionManagerDialog.resx +++ b/csharp/ExcelAddIn/views/ConnectionManagerDialog.resx @@ -1,7 +1,7 @@