mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-29 18:11:40 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cdae7af9c | ||
|
|
3e17cdd62e |
No files matched your search
@@ -57,6 +57,7 @@ import { Switch } from "./ui/switch";
|
||||
import { TerminalFontSelect } from "./settings/TerminalFontSelect";
|
||||
import { useAvailableFonts } from "../application/state/fontStore";
|
||||
import { toast } from "./ui/toast";
|
||||
import { HostNotesEditor } from "./host/HostNotesEditor";
|
||||
import { GroupSshSettingsSection } from "./GroupSshSettingsSection";
|
||||
import { prepareProxyConfigForSave } from "./HostDetailsPanel.helpers";
|
||||
import { TerminalEncodingSelect } from "./TerminalEncodingSelect";
|
||||
@@ -547,6 +548,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelPropsWithResize> = ({
|
||||
|
||||
const result: GroupConfig = {
|
||||
path: newPath,
|
||||
notes: form.notes?.trim() || undefined,
|
||||
// Only include SSH fields if SSH section is enabled
|
||||
...(sshEnabled && {
|
||||
protocol: 'ssh' as const,
|
||||
@@ -994,6 +996,12 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelPropsWithResize> = ({
|
||||
</HostDetailsSection>
|
||||
</>)}
|
||||
|
||||
<HostNotesEditor
|
||||
panelKey={groupPath}
|
||||
value={form.notes ?? ""}
|
||||
onChange={(notes) => update("notes", notes)}
|
||||
/>
|
||||
|
||||
{/* Add Protocol Button — always at the bottom */}
|
||||
{addableProtocols.length > 0 && (
|
||||
<Dropdown open={addProtocolOpen} onOpenChange={setAddProtocolOpen}>
|
||||
|
||||
@@ -168,6 +168,7 @@ test("HostTreeView shows selected groups in multi-select mode", () => {
|
||||
children: {},
|
||||
hosts: [],
|
||||
}]}
|
||||
groupConfigs={[{ path: "production", notes: "# VPN instructions" }]}
|
||||
hosts={[]}
|
||||
expandedPaths={new Set<string>()}
|
||||
onTogglePath={() => undefined}
|
||||
@@ -188,6 +189,7 @@ test("HostTreeView shows selected groups in multi-select mode", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
assert.match(markup, /aria-label="Group notes"/);
|
||||
assert.match(markup, /data-selected="true"/);
|
||||
assert.match(markup, /data-group-path="production"/);
|
||||
assert.match(markup, /role="tree"/);
|
||||
|
||||
+18
-13
@@ -538,19 +538,24 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
? <CheckSquare size={16} />
|
||||
: <Square size={16} />
|
||||
: undefined}
|
||||
labelActions={!isMultiSelectMode && (
|
||||
<button
|
||||
aria-label={`Edit ${node.name}`}
|
||||
tabIndex={-1}
|
||||
data-host-tree-group-edit-button={node.path}
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded opacity-0 transition-colors hover:bg-secondary/80 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditGroup(node.path);
|
||||
}}
|
||||
>
|
||||
<Edit2 size={12} />
|
||||
</button>
|
||||
labelActions={(
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<HostNotesIndicator notes={groupConfigs.find((config) => config.path === node.path)?.notes} label="Group notes" />
|
||||
{!isMultiSelectMode && (
|
||||
<button
|
||||
aria-label={`Edit ${node.name}`}
|
||||
tabIndex={-1}
|
||||
data-host-tree-group-edit-button={node.path}
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded opacity-0 transition-colors hover:bg-secondary/80 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditGroup(node.path);
|
||||
}}
|
||||
>
|
||||
<Edit2 size={12} />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { HostNotesIndicator } from "./HostNotesIndicator";
|
||||
|
||||
test("notes indicators distinguish groups from hosts and hide empty notes", () => {
|
||||
assert.match(renderToStaticMarkup(<HostNotesIndicator notes="Host" />), /aria-label="Host notes"/);
|
||||
assert.match(renderToStaticMarkup(<HostNotesIndicator notes="# Project" label="Group notes" />), /aria-label="Group notes"/);
|
||||
assert.equal(renderToStaticMarkup(<HostNotesIndicator notes=" " label="Group notes" />), "");
|
||||
});
|
||||
@@ -7,11 +7,13 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card"
|
||||
export interface HostNotesIndicatorProps {
|
||||
notes?: string;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const HostNotesIndicator: React.FC<HostNotesIndicatorProps> = ({
|
||||
notes,
|
||||
className,
|
||||
label = "Host notes",
|
||||
}) => {
|
||||
const trimmed = notes?.trim();
|
||||
if (!trimmed) return null;
|
||||
@@ -25,7 +27,7 @@ export const HostNotesIndicator: React.FC<HostNotesIndicatorProps> = ({
|
||||
"inline-flex h-4 w-4 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-muted-foreground transition-colors hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
aria-label="Host notes"
|
||||
aria-label={label}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
@@ -893,6 +893,7 @@ export function VaultHostListSection({ ctx }: { ctx: VaultHostListSectionContext
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold flex items-center gap-1.5 min-w-0">
|
||||
<span className="truncate">{node.name}</span>
|
||||
<HostNotesIndicator notes={groupConfigs.find((config) => config.path === node.path)?.notes} label="Group notes" />
|
||||
{!isMultiSelectMode && viewMode !== "grid" && renderGroupEditButton(node.path, true)}
|
||||
{managedGroupPaths.has(node.path) && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded bg-primary/15 text-primary shrink-0">
|
||||
|
||||
@@ -581,3 +581,10 @@ test("applyGroupDefaults keeps host algorithm overrides instead of inheriting",
|
||||
);
|
||||
assert.deepEqual(result.algorithms, hostOverrides);
|
||||
});
|
||||
|
||||
test("group notes survive sanitization but are not connection defaults", () => {
|
||||
const config = { path: "Project", notes: "# VPN\nConnect before SSH.", username: "admin" };
|
||||
assert.equal(sanitizeGroupConfig(config).notes, config.notes);
|
||||
assert.equal(resolveGroupDefaults("Project", [config]).notes, undefined);
|
||||
assert.equal(resolveGroupDefaults("Project/Servers", [config]).notes, undefined);
|
||||
});
|
||||
@@ -116,7 +116,7 @@ export function resolveGroupDefaults(
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (key !== 'path' && value !== undefined) {
|
||||
if (key !== 'path' && key !== 'notes' && value !== undefined) {
|
||||
if (key === 'proxyProfileId') {
|
||||
delete merged.proxyConfig;
|
||||
}
|
||||
|
||||
@@ -412,6 +412,8 @@ export interface GroupNode {
|
||||
/** Default configuration for a group. Hosts in this group inherit these values when not explicitly set. */
|
||||
export interface GroupConfig {
|
||||
path: string;
|
||||
/** Markdown notes for this group only; not inherited by hosts or subgroups. */
|
||||
notes?: string;
|
||||
order?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
|
||||
@@ -620,3 +620,14 @@ test("mergeSyncPayloads keeps host telemetry off merged payloads", () => {
|
||||
assert.equal(unchanged.payload.hosts[0]?.lastConnectedAt, undefined);
|
||||
assert.equal(unchanged.summary.modified.local, 0);
|
||||
});
|
||||
|
||||
test("group notes sync edits and clearing without dropping connection settings", () => {
|
||||
const config = { path: "Project", notes: "# VPN\nOld instructions", username: "admin" };
|
||||
const base = payload({ customGroups: ["Project"], groupConfigs: [config] });
|
||||
const edited = payload({ customGroups: ["Project"], groupConfigs: [{ ...config, notes: "# VPN\nNew instructions" }] });
|
||||
const merged = mergeSyncPayloads(base, base, edited).payload;
|
||||
assert.equal(merged.groupConfigs?.[0].notes, "# VPN\nNew instructions");
|
||||
assert.equal(merged.groupConfigs?.[0].username, "admin");
|
||||
const cleared = payload({ customGroups: ["Project"], groupConfigs: [{ path: "Project", username: "admin" }] });
|
||||
assert.equal(mergeSyncPayloads(merged, merged, cleared).payload.groupConfigs?.[0].notes, undefined);
|
||||
});
|
||||
Reference in new issue
Block a user