Public Access
Put a host inside the group it is filed under, rather than beside it
The group cards were headings with a navigation gesture bolted on. Opening one narrowed the grid to its hosts, but the level above already held every host in the keychain — so a card could only ever subtract, filing something changed nothing but a chip, and a keychain with forty machines was forty cards however carefully anybody had arranged them. The trail said ALL HOSTS and meant it. So the grid holds one level of the tree, the way a directory pane holds one directory. A host filed under a group is inside that group and is not also on the screen the group's card sits on; the outermost level is what nothing has been filed into. A group is a place now, and the cards, the trail and the drop target were already the vocabulary for saying so. The find box is the exception and had to be one. Typed into, it searches the open group and everything under it, which from the outermost level is every machine in the keychain. A box scoped to the level it was typed on would answer "no host matches that" about a host this keychain has got, and finding a machine without first remembering where it was filed is most of what the box is for. The accent chip on a card is what tells a searched-up host from one that lives at this level. Two things came free with the change and are handled rather than left. The phone would have broken. Its list binds SidebarRows, which was a projection over VisibleHosts, and it has no group cards and nowhere to open one into — so level-scoping would have left it drawing only the hosts nobody had filed. RebuildSidebarRows takes its own pass over the hosts now, narrowed by the vault switches and the box and by nothing else, which is the whole tree flattened under headings: exactly what it drew before. And anything created inside a group disappeared the moment it was saved. The host editor opens on the group the screen is about rather than only on a selected card, and + NEW GROUP defaults its parent to the group that is open. Deliberately not the selected card there: a highlighted card is what EDIT and DELETE are aimed at, and reading it as "and the next group goes inside it" would nest one because somebody had clicked something. The host editor takes both because it always took the selection, and its picker shows the answer before anything is written. Dropping a host on a group card now takes the card off the grid, and the selection goes with it — Connect, Edit and Delete all read that property and none of them should be aimed at a card that has left the screen. The status line is what says where it went, which is why the manual check now asks for it to be read. Coming back out through the editor lands the host on this level again, and there the selection survives. An empty grid has two more things it can say: that every host is filed away, which the level-at-a-time grid made reachable and which is not the same sentence as "there are none", and that nothing under this group matches what was typed — with ALL HOSTS named as the way to widen it. Not changed, and next door: a group card counts the hosts filed directly under it, so a group holding only subgroups reads "0 hosts". That was already true and is more visible now that its subgroups' hosts are not spilled onto the level above. 1522 tests pass. Four are new — the level rule and the phone's flat list asserted together, the box reaching two levels down, the all-filed sentence, and where a host and a group made inside a group end up. MovingAHostToAGroup was asserting the old outcome and is rewritten rather than adjusted: it held that the host stayed selected, and what it holds now is that the host leaves the level it came from.
This commit is contained in:
@@ -3584,12 +3584,22 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// What dragging a host card onto a group card does. It is the same write the editor makes — one field
|
||||
/// of the host, pushed straight away — reached without opening a form, because filing thirty imported
|
||||
/// machines through the editor is thirty rounds of open, pick, save.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The card goes into the group and off the level it was dragged from</b>, which is the whole of what
|
||||
/// a drop looks like on a grid that holds one level of the tree — the host is inside the card it was
|
||||
/// dropped on now, and that is where it is drawn. It used to stay put and gain a chip. The selection
|
||||
/// goes with it rather than being restored onto something nobody can see: Connect, Edit and Delete all
|
||||
/// read that property, and none of them should be aimed at a card that has left the screen. See
|
||||
/// <c>VaultViewModel.Matches</c> and <c>RebuildVisibleHosts</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task MovingAHostToAGroup_FilesItAndLeavesItSelected()
|
||||
public async Task MovingAHostToAGroup_FilesItAndTakesItOffTheLevelItCameFrom()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
@@ -3603,23 +3613,166 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
await vault.MoveHostToGroupCommand.ExecuteAsync(new HostGroupMove(host, group));
|
||||
|
||||
vault.Hosts.Single().Host.GroupId.ShouldBe(group);
|
||||
vault.SelectedHost.ShouldNotBeNull().EntityId.ShouldBe(host.EntityId, "the reload replaces every row");
|
||||
vault.VisibleHosts.ShouldBeEmpty("the grid is the outermost level and the host is inside a group");
|
||||
vault.SelectedHost.ShouldBeNull("nothing on screen is it any more");
|
||||
|
||||
// Under the group's own heading now, which is what the phone's list draws.
|
||||
// Under the group's own heading now, which is what the phone's list draws — that list is the whole
|
||||
// tree flattened, so the host is still in it.
|
||||
vault.SidebarRows.OfType<SidebarGroupHeader>()
|
||||
.Single(header => header.GroupId == group)
|
||||
.Count.ShouldBe(1);
|
||||
|
||||
// And the name on the card, which is what the desktop's grid draws instead of that heading — the one
|
||||
// thing on screen that changes where the host was dropped rather than where it came from.
|
||||
// The name is on the card, which is what the desktop's grid draws instead of that heading, and what
|
||||
// says which group a searched-up card came out of.
|
||||
vault.Hosts.Single().GroupLabel.ShouldBe("production");
|
||||
vault.Hosts.Single().HasGroup.ShouldBeTrue();
|
||||
|
||||
// Opening the group is where it went, and the way to it.
|
||||
vault.OpenGroupCommand.Execute(vault.Groups.Single());
|
||||
|
||||
vault.VisibleHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-db");
|
||||
|
||||
// And back out again, which is what the host's own editor is for now that the drop has one target.
|
||||
vault.OpenGroupCommand.Execute(null);
|
||||
|
||||
await vault.MoveHostToGroupCommand.ExecuteAsync(new HostGroupMove(vault.Hosts.Single(), null));
|
||||
|
||||
vault.Hosts.Single().Host.GroupId.ShouldBeNull();
|
||||
vault.Hosts.Single().HasGroup.ShouldBeFalse("and the chip goes with it");
|
||||
|
||||
// Coming out of a group is the direction that lands the host back on this level, so here the
|
||||
// selection does survive the move.
|
||||
vault.VisibleHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-db");
|
||||
vault.SelectedHost.ShouldNotBeNull().Label.ShouldBe("prod-db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The grid holds one level of the tree, the way a directory pane holds one directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A host filed under a group is inside that group and is not also on the screen the group's card sits
|
||||
/// on. While it was both, opening a group could only ever take hosts away — the level above already had
|
||||
/// all of them — and the cards were headings rather than places. The phone's list is the deliberate
|
||||
/// exception and is asserted here beside it: it draws the whole tree flat under headings, because it has
|
||||
/// no cards to open and nowhere to open one into.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AHostFiledUnderAGroup_IsDrawnInsideItAndNotAtTheLevelAbove()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await AddHostAsync(vault, "stage-web");
|
||||
await AddGroupAsync(vault, "production");
|
||||
await FileAsync(vault, "prod-db", "production");
|
||||
|
||||
vault.VisibleHosts.Select(row => row.Label)
|
||||
.ShouldBe(["stage-web"], "the outermost level is what nothing has been filed into");
|
||||
|
||||
vault.SidebarRows.OfType<HostRowViewModel>().Select(row => row.Label)
|
||||
.ShouldBe(["prod-db", "stage-web"], "the phone's list is the whole tree flattened");
|
||||
|
||||
vault.OpenGroupCommand.Execute(vault.Groups.Single());
|
||||
|
||||
vault.VisibleHosts.Select(row => row.Label).ShouldBe(["prod-db"]);
|
||||
|
||||
vault.OpenGroupCommand.Execute(null);
|
||||
|
||||
vault.VisibleHosts.Select(row => row.Label).ShouldBe(["stage-web"]);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The one thing on the screen that crosses a group boundary, and it has to be one. A search that looked
|
||||
/// only at the level it was typed on would answer "no host matches that" about a machine this keychain
|
||||
/// has got — and finding a machine without first remembering where it was filed is most of what the box
|
||||
/// is for. Typed at the outermost level it reaches everything; typed inside a group it reaches that
|
||||
/// group and what is under it, which is the same rule read from where you are standing.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheFindBox_SearchesInsideTheGroupsRatherThanOnlyTheLevelOnScreen()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await AddHostAsync(vault, "stage-web");
|
||||
await AddGroupAsync(vault, "estate");
|
||||
await AddGroupAsync(vault, "production");
|
||||
await SetGroupParentAsync(vault, "production", "estate");
|
||||
await FileAsync(vault, "prod-db", "production");
|
||||
|
||||
vault.VisibleHosts.ShouldNotContain(row => string.Equals(row.Label, "prod-db", StringComparison.Ordinal));
|
||||
|
||||
vault.HostFilter = "prod-db";
|
||||
|
||||
vault.VisibleHosts.ShouldHaveSingleItem().Label
|
||||
.ShouldBe("prod-db", "two levels down, and the box reaches it");
|
||||
|
||||
// And inside a group it is that group's subtree: estate holds production, which holds the host.
|
||||
vault.OpenGroupCommand.Execute(
|
||||
vault.Groups.Single(row => string.Equals(row.Label, "estate", StringComparison.Ordinal)));
|
||||
|
||||
vault.VisibleHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-db");
|
||||
|
||||
vault.HostFilter = "stage";
|
||||
|
||||
vault.VisibleHosts.ShouldBeEmpty("stage-web is outside estate, so this search does not reach it");
|
||||
vault.NoVisibleHostsMessage.ShouldContain("ALL HOSTS", Case.Sensitive, "and it says how to widen it");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A grid with no cards in it has to say why, and "they are all filed away" is a different sentence from
|
||||
/// "there are none" and from "nothing matches what you typed". It is the answer the level-at-a-time grid
|
||||
/// made reachable: before it, a keychain with hosts in it always drew some.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AKeychainWhoseHostsAreAllFiled_SaysSoRatherThanLookingEmpty()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await AddGroupAsync(vault, "production");
|
||||
await FileAsync(vault, "prod-db", "production");
|
||||
|
||||
vault.HasVisibleHosts.ShouldBeFalse();
|
||||
vault.NoVisibleHostsMessage.ShouldContain("filed under a group");
|
||||
vault.NoVisibleHostsMessage.ShouldNotContain(
|
||||
"No hosts yet", Case.Sensitive, "telling somebody with hosts to add their first one answers nothing");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Where a new thing lands, now that the screen is somewhere rather than everywhere. A host created
|
||||
/// inside a group and filed under none would vanish from the screen it was created on, which is the
|
||||
/// papercut that comes free with a grid holding one level — so the editor opens on the group the user
|
||||
/// is standing in, and the picker shows it before anything is saved.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ANewHostOrGroupStartedInsideAGroup_IsMadeInsideIt()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddGroupAsync(vault, "production");
|
||||
|
||||
vault.OpenGroupCommand.Execute(vault.Groups.Single());
|
||||
|
||||
vault.NewHostCommand.Execute(null);
|
||||
|
||||
vault.EditorSelectedGroup.ShouldNotBeNull().Label.ShouldBe("production");
|
||||
|
||||
vault.EditorLabel = "prod-db";
|
||||
vault.EditorHostname = "db.internal";
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
vault.VisibleHosts.ShouldHaveSingleItem().Label
|
||||
.ShouldBe("prod-db", "so it is on the screen it was made on");
|
||||
|
||||
vault.NewGroupCommand.Execute(null);
|
||||
|
||||
vault.GroupEditorSelectedParent.ShouldNotBeNull().Label
|
||||
.ShouldBe("production", "+ NEW GROUP inside a group makes one inside it");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
|
||||
Reference in New Issue
Block a user