Public Access
Give the window its v5b chrome and each session surface its own shell
This commit is contained in:
@@ -749,6 +749,15 @@ internal sealed class SshKeyRowViewModel(VaultItem<SshKeySecret> key, Guid vault
|
||||
|
||||
internal string Label => key.Secret.Label;
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── The vault name to print on this row, or empty when there is only one vault to be in.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="HostRowViewModel.VaultBadge" path="/remarks" />
|
||||
internal string VaultBadge { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this row has a vault to name.</summary>
|
||||
internal bool HasVaultBadge => VaultBadge.Length > 0;
|
||||
|
||||
/// <summary>What the list shows under the name: what is known about the key, never the key.</summary>
|
||||
internal string Description => key.Secret switch
|
||||
{
|
||||
@@ -823,6 +832,15 @@ internal sealed class CredentialRowViewModel(
|
||||
|
||||
internal string Label => credential.Secret.Label;
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── The vault name to print on this row, or empty when there is only one vault to be in.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="HostRowViewModel.VaultBadge" path="/remarks" />
|
||||
internal string VaultBadge { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this row has a vault to name.</summary>
|
||||
internal bool HasVaultBadge => VaultBadge.Length > 0;
|
||||
|
||||
/// <summary>What the list shows under the name: the account, never the password.</summary>
|
||||
/// <remarks>
|
||||
/// The username is the whole reason a credential is a separate item rather than two fields on a host, so
|
||||
@@ -1101,8 +1119,66 @@ internal sealed record VaultItemRowViewModel(
|
||||
{
|
||||
/// <summary>Whether this row has anything to say about its sync state.</summary>
|
||||
internal bool HasBadge => Badge.Length > 0;
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── The vault name to print on this row, or empty when there is only one vault to be in.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Additive: every other kind of row in this application already carries this fact (see
|
||||
/// <see cref="HostRowViewModel.VaultBadge"/>), and the keychain table is the one list that merged four
|
||||
/// of those kinds into one projection without carrying it along. Empty by the same convention the others
|
||||
/// follow — a badge on every row of a single-vault list is noise that says the same thing on all of them.
|
||||
/// </remarks>
|
||||
internal string VaultBadge { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this row has a vault to name.</summary>
|
||||
internal bool HasVaultBadge => VaultBadge.Length > 0;
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── Which hosts authenticate with this item, short-form — "prod-api-01 +2" — or empty when
|
||||
/// nothing does or the kind has no such fact.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Real for a key or a credential, which a host can be bound to — see <see cref="VaultViewModel.HostsBoundTo"/>,
|
||||
/// the same resolved-binding scan this reads, kept in step so the table and the deletion warning never
|
||||
/// disagree about who is counted. A tag's own version of this fact is its host count, already carried on
|
||||
/// <see cref="TagRowViewModel.Description"/>. A bucket has no binding concept in this codebase at all —
|
||||
/// nothing here resolves a host's authentication to an object store — so it stays empty rather than
|
||||
/// showing a region or an endpoint under a column that promises "who uses this".
|
||||
/// </remarks>
|
||||
internal string UsedBySummary { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this row has anything to say about who uses it.</summary>
|
||||
internal bool HasUsedBySummary => UsedBySummary.Length > 0;
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── The Material Icons glyph Keychain.dc.html draws beside each row's name, by kind.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Computed from <see cref="Kind"/> rather than stored, because it is a fact about the kind and not
|
||||
/// about the item — every key gets the same glyph regardless of algorithm, which is honest: this type
|
||||
/// carries no algorithm field (see docs/design-import-gaps.md).
|
||||
/// </remarks>
|
||||
internal string IconGlyph => Kind switch
|
||||
{
|
||||
VaultItemKind.Key => "",
|
||||
VaultItemKind.Credential => "",
|
||||
VaultItemKind.Tag => "",
|
||||
VaultItemKind.ObjectStore => "",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── One host on the keychain detail pane's own USED BY list, for the selected key or credential.
|
||||
/// </summary>
|
||||
/// <param name="Label">The host's name.</param>
|
||||
/// <param name="IsConnected">
|
||||
/// Whether a terminal is open on it right now — the same two-state truth <see cref="HostRowViewModel.IsConnected"/>
|
||||
/// draws a dot from everywhere else in this application. Never a third colour: nothing here pings anything.
|
||||
/// </param>
|
||||
internal sealed record UsedByHostRowViewModel(string Label, bool IsConnected);
|
||||
|
||||
/// <summary>Which list a deletion that has been asked for is aimed at.</summary>
|
||||
internal enum DeletionTarget
|
||||
{
|
||||
@@ -2108,6 +2184,19 @@ internal sealed partial class VaultViewModel(
|
||||
/// </remarks>
|
||||
internal ObservableCollection<VaultItemRowViewModel> VaultItems { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── Narrows <see cref="VaultItems"/> to rows whose name, type or detail contains this text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Keychain.dc.html draws a filter box this table never had; wired here rather than left as a control
|
||||
/// that does nothing, on the same "narrows the list and nothing else" reasoning
|
||||
/// <see cref="HostFilter"/> already carries — it can hide a row but can never change what a category or
|
||||
/// a selection means. Case-insensitive, and read against the same three fields the row already shows,
|
||||
/// so what matches is never a surprise to whoever typed it.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string itemFilter = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The selected row of the vault table.
|
||||
/// </summary>
|
||||
@@ -2171,6 +2260,31 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
internal bool HasSelectedVaultItem => SelectedVaultItem is not null;
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── The selected key or credential's own USED BY list, for the keychain's detail pane.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rebuilt in <see cref="OnSelectedVaultItemChanged"/> by <see cref="RebuildSelectedItemUsage"/>, off the
|
||||
/// same resolved-binding scan <see cref="HostsBoundTo"/> reads for the deletion warning. Empty for every
|
||||
/// other kind — a tag's own version of this fact is its host count, already on the row, and a bucket has
|
||||
/// no binding concept to scan at all.
|
||||
/// </remarks>
|
||||
internal ObservableCollection<UsedByHostRowViewModel> SelectedItemUsedByHosts { get; } = [];
|
||||
|
||||
/// <summary>Whether the selected item has anything on its USED BY list.</summary>
|
||||
internal bool HasSelectedItemUsedByHosts => SelectedItemUsedByHosts.Count > 0;
|
||||
|
||||
/// <summary>The "in use · N hosts" chip text, or empty when nothing authenticates with the selected item.</summary>
|
||||
internal string SelectedItemInUseSummary => SelectedItemUsedByHosts.Count switch
|
||||
{
|
||||
0 => string.Empty,
|
||||
1 => "in use · 1 host",
|
||||
var n => $"in use · {n} hosts",
|
||||
};
|
||||
|
||||
/// <summary>Whether the "in use" chip has anything to say.</summary>
|
||||
internal bool HasSelectedItemInUseSummary => SelectedItemInUseSummary.Length > 0;
|
||||
|
||||
/// <summary>Whether the selected row is one with an editor behind it.</summary>
|
||||
internal bool SelectedItemIsEditable => SelectedVaultItem?.Kind is
|
||||
VaultItemKind.Key or VaultItemKind.Credential or VaultItemKind.ObjectStore or VaultItemKind.Tag;
|
||||
@@ -5272,7 +5386,10 @@ internal sealed partial class VaultViewModel(
|
||||
var unreadable = 0;
|
||||
var rows = new List<SshKeyRowViewModel>();
|
||||
|
||||
foreach (var vault in session.ReadableVaults)
|
||||
var readableVaults = session.ReadableVaults.ToList();
|
||||
var several = readableVaults.Count > 1;
|
||||
|
||||
foreach (var vault in readableVaults)
|
||||
{
|
||||
var listing = await session.SshKeys
|
||||
.ListAsync(vault.VaultId, cancellationToken)
|
||||
@@ -5281,7 +5398,11 @@ internal sealed partial class VaultViewModel(
|
||||
unreadable += listing.Unreadable;
|
||||
|
||||
rows.AddRange(listing.Items.Select(
|
||||
item => new SshKeyRowViewModel(item, vault.VaultId, vault.Name)));
|
||||
item => new SshKeyRowViewModel(item, vault.VaultId, vault.Name)
|
||||
{
|
||||
// Only when there is something to tell apart, as the host grid's badge is.
|
||||
VaultBadge = several ? vault.Name.ToUpperInvariant() : string.Empty,
|
||||
}));
|
||||
}
|
||||
|
||||
Keys.Clear();
|
||||
@@ -5312,7 +5433,10 @@ internal sealed partial class VaultViewModel(
|
||||
var unreadable = 0;
|
||||
var rows = new List<CredentialRowViewModel>();
|
||||
|
||||
foreach (var vault in session.ReadableVaults)
|
||||
var readableVaults = session.ReadableVaults.ToList();
|
||||
var several = readableVaults.Count > 1;
|
||||
|
||||
foreach (var vault in readableVaults)
|
||||
{
|
||||
var listing = await session.Credentials
|
||||
.ListAsync(vault.VaultId, cancellationToken)
|
||||
@@ -5321,7 +5445,11 @@ internal sealed partial class VaultViewModel(
|
||||
unreadable += listing.Unreadable;
|
||||
|
||||
rows.AddRange(listing.Items.Select(
|
||||
item => new CredentialRowViewModel(item, vault.VaultId, vault.Name)));
|
||||
item => new CredentialRowViewModel(item, vault.VaultId, vault.Name)
|
||||
{
|
||||
// Only when there is something to tell apart, as the host grid's badge is.
|
||||
VaultBadge = several ? vault.Name.ToUpperInvariant() : string.Empty,
|
||||
}));
|
||||
}
|
||||
|
||||
Credentials.Clear();
|
||||
@@ -12017,7 +12145,12 @@ internal sealed partial class VaultViewModel(
|
||||
"TAG",
|
||||
tag.Description,
|
||||
tag.Badge,
|
||||
tag.HasUnsyncedChanges));
|
||||
tag.HasUnsyncedChanges)
|
||||
{
|
||||
// A tag's own "used by" is the host count it already carries — the same fact
|
||||
// Description prints, so the two never disagree.
|
||||
UsedBySummary = tag.HostCount > 0 ? tag.Description : string.Empty,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12047,6 +12180,61 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── Which hosts authenticate with a key or a credential, short-form: "a", "a, b" or "a +N".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reads the same resolved-binding scan <see cref="HostsBoundTo"/> reads for the deletion warning, so the
|
||||
/// table's own USED BY column and that warning can never disagree about who is counted. Three names and a
|
||||
/// count past that would be the deletion warning's own budget; this one is a 1.4fr table column, so it
|
||||
/// stops at two names plus a count — see Keychain.dc.html's own "prod-api-01 +2" sample.
|
||||
/// </remarks>
|
||||
private string UsedBySummaryFor(ResolvedBindingKind kind, Guid entityId)
|
||||
{
|
||||
var names = Hosts
|
||||
.Where(row => row.Resolved.Binding is { } binding
|
||||
&& binding.Kind == kind && binding.EntityId == entityId)
|
||||
.Select(row => row.Label)
|
||||
.ToList();
|
||||
|
||||
return names.Count switch
|
||||
{
|
||||
0 => string.Empty,
|
||||
1 => names[0],
|
||||
2 => $"{names[0]}, {names[1]}",
|
||||
_ => $"{names[0]} +{names.Count - 1}",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>── v5b ── Narrows <see cref="VaultItems"/> to rows <see cref="ItemFilter"/> matches.</summary>
|
||||
/// <remarks>
|
||||
/// Its own method for the reason <see cref="AddTagRows"/> is out of <see cref="RebuildVaultItems"/> —
|
||||
/// length — and applied last, by removal, rather than threaded into each of the four kind-specific
|
||||
/// loops: one pass over the merged list is simpler than four copies of the same three-field check, and
|
||||
/// the filter has no opinion about which kind a row is.
|
||||
/// </remarks>
|
||||
private void ApplyItemFilter()
|
||||
{
|
||||
var needle = ItemFilter.Trim();
|
||||
|
||||
if (needle.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = VaultItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var row = VaultItems[i];
|
||||
|
||||
if (!Contains(row.Name) && !Contains(row.Type) && !Contains(row.Detail))
|
||||
{
|
||||
VaultItems.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
bool Contains(string value) => value.Contains(needle, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refills the vault table from the typed lists.
|
||||
/// </summary>
|
||||
@@ -12078,7 +12266,11 @@ internal sealed partial class VaultViewModel(
|
||||
"SSH KEY",
|
||||
key.Description,
|
||||
key.Badge,
|
||||
key.HasUnsyncedChanges));
|
||||
key.HasUnsyncedChanges)
|
||||
{
|
||||
VaultBadge = key.VaultBadge,
|
||||
UsedBySummary = UsedBySummaryFor(ResolvedBindingKind.SshKey, key.EntityId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12093,12 +12285,17 @@ internal sealed partial class VaultViewModel(
|
||||
"PASSWORD",
|
||||
credential.Description,
|
||||
credential.Badge,
|
||||
credential.HasUnsyncedChanges));
|
||||
credential.HasUnsyncedChanges)
|
||||
{
|
||||
VaultBadge = credential.VaultBadge,
|
||||
UsedBySummary = UsedBySummaryFor(ResolvedBindingKind.Credential, credential.EntityId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AddTagRows();
|
||||
AddBucketRows();
|
||||
ApplyItemFilter();
|
||||
|
||||
// The selection survives a reload, as every other list's does, and for the same reason: a background
|
||||
// sync every minute would otherwise move the detail pane out from under whoever was reading it.
|
||||
@@ -12127,6 +12324,8 @@ internal sealed partial class VaultViewModel(
|
||||
OnPropertyChanged(nameof(SelectedDetailHeading));
|
||||
OnPropertyChanged(nameof(ShowsItemActions));
|
||||
|
||||
RebuildSelectedItemUsage(value);
|
||||
|
||||
// Every kind this table can delete, because one selection covers all of their lists.
|
||||
DisarmIfAimedElsewhere(DeletionTarget.Key, value?.EntityId);
|
||||
DisarmIfAimedElsewhere(DeletionTarget.Credential, value?.EntityId);
|
||||
@@ -12168,6 +12367,39 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ── v5b ── Rebuilds the detail pane's own USED BY list for whichever key or credential is selected.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The same resolved-binding scan <see cref="HostsBoundTo"/> reads for the deletion warning and
|
||||
/// <see cref="UsedBySummaryFor"/> reads for the table's own column, kept in step so none of the three
|
||||
/// ever disagrees about who authenticates with the selected item. Empty for every other kind.
|
||||
/// </remarks>
|
||||
private void RebuildSelectedItemUsage(VaultItemRowViewModel? value)
|
||||
{
|
||||
SelectedItemUsedByHosts.Clear();
|
||||
|
||||
ResolvedBindingKind? kind = value?.Kind switch
|
||||
{
|
||||
VaultItemKind.Key => ResolvedBindingKind.SshKey,
|
||||
VaultItemKind.Credential => ResolvedBindingKind.Credential,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (kind is { } resolvedKind && value is not null)
|
||||
{
|
||||
foreach (var row in Hosts.Where(row => row.Resolved.Binding is { } binding
|
||||
&& binding.Kind == resolvedKind && binding.EntityId == value.EntityId))
|
||||
{
|
||||
SelectedItemUsedByHosts.Add(new UsedByHostRowViewModel(row.Label, row.IsConnected));
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasSelectedItemUsedByHosts));
|
||||
OnPropertyChanged(nameof(SelectedItemInUseSummary));
|
||||
OnPropertyChanged(nameof(HasSelectedItemInUseSummary));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The tag editor joins the other four. Without this, arming a key's deletion and then pressing + TAG
|
||||
/// left the confirmation card live in the detail pane with the tag's own boxes directly under it — so
|
||||
@@ -12177,6 +12409,9 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
partial void OnPendingChangesChanged(int value) => OnPropertyChanged(nameof(SectionSummary));
|
||||
|
||||
/// <summary>── v5b ── Re-narrows the table as the filter box is typed into.</summary>
|
||||
partial void OnItemFilterChanged(string value) => RebuildVaultItems();
|
||||
|
||||
partial void OnUnreadableItemsChanged(int value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasUnreadableItems));
|
||||
|
||||
Reference in New Issue
Block a user