mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: host color and icon columns that sync
This commit is contained in:
1 parent
90f5aebafb
commit
ea0e614183
21 files changed
+13970
No files matched your search
@@ -7,6 +7,7 @@
|
||||
- The mobile app can sign in through your browser with any login method, including passkeys and security keys
|
||||
- Plugins can say how they show in the mobile app
|
||||
- The plugin list tells the mobile app which plugin API each plugin needs, so the app can say when it or a plugin needs an update
|
||||
- Hosts can have a color and an icon, which sync to the desktop and mobile apps
|
||||
|
||||
## 26.10.5
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `ssh_data` ADD `color` text;--> statement-breakpoint
|
||||
ALTER TABLE `ssh_data` ADD `icon` text;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -533,6 +533,13 @@
|
||||
"when": 1792430772041,
|
||||
"tag": "0075_session_policy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 76,
|
||||
"version": "5",
|
||||
"when": 1792945361809,
|
||||
"tag": "0076_host_color_icon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "ssh_data" ADD COLUMN "color" text;--> statement-breakpoint
|
||||
ALTER TABLE "ssh_data" ADD COLUMN "icon" text;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -533,6 +533,13 @@
|
||||
"when": 1792430770990,
|
||||
"tag": "0075_session_policy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 76,
|
||||
"version": "7",
|
||||
"when": 1792945360507,
|
||||
"tag": "0076_host_color_icon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `ssh_data` ADD `color` text;--> statement-breakpoint
|
||||
ALTER TABLE `ssh_data` ADD `icon` text;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -498,6 +498,13 @@
|
||||
"when": 1792430769899,
|
||||
"tag": "0070_session_policy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 71,
|
||||
"version": "6",
|
||||
"when": 1792945434651,
|
||||
"tag": "0071_host_color_icon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1316,6 +1316,8 @@ const migrateSchema = () => {
|
||||
{ column: "ssh_port", sql: "ALTER TABLE ssh_data ADD COLUMN ssh_port INTEGER DEFAULT 22" },
|
||||
{ column: "connection_origin", sql: "ALTER TABLE ssh_data ADD COLUMN connection_origin TEXT" },
|
||||
{ column: "parent_host_id", sql: "ALTER TABLE ssh_data ADD COLUMN parent_host_id INTEGER REFERENCES ssh_data(id) ON DELETE SET NULL" },
|
||||
{ column: "color", sql: "ALTER TABLE ssh_data ADD COLUMN color TEXT" },
|
||||
{ column: "icon", sql: "ALTER TABLE ssh_data ADD COLUMN icon TEXT" },
|
||||
];
|
||||
|
||||
for (const migration of sshDataMigrations) {
|
||||
|
||||
@@ -193,6 +193,9 @@ export const hosts = mysqlTable(
|
||||
// legacy algorithms, agent, environment). JSON.
|
||||
sshOptions: text("ssh_options"),
|
||||
notes: text("notes"),
|
||||
// Hex color and Lucide icon name a layout may show next to the host.
|
||||
color: text("color"),
|
||||
icon: text("icon"),
|
||||
enableSsh: boolean("enable_ssh").notNull().default(true),
|
||||
|
||||
sshPort: int("ssh_port").default(22),
|
||||
|
||||
@@ -193,6 +193,9 @@ export const hosts = pgTable(
|
||||
// legacy algorithms, agent, environment). JSON.
|
||||
sshOptions: text("ssh_options"),
|
||||
notes: text("notes"),
|
||||
// Hex color and Lucide icon name a layout may show next to the host.
|
||||
color: text("color"),
|
||||
icon: text("icon"),
|
||||
enableSsh: boolean("enable_ssh").notNull().default(true),
|
||||
|
||||
sshPort: integer("ssh_port").default(22),
|
||||
|
||||
@@ -183,6 +183,9 @@ export const hosts = sqliteTable(
|
||||
// legacy algorithms, agent, environment). JSON.
|
||||
sshOptions: text("ssh_options"),
|
||||
notes: text("notes"),
|
||||
// Hex color and Lucide icon name a layout may show next to the host.
|
||||
color: text("color"),
|
||||
icon: text("icon"),
|
||||
enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true),
|
||||
|
||||
sshPort: integer("ssh_port").default(22),
|
||||
|
||||
@@ -30,6 +30,33 @@ export function applyHostKeyTypeUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_COLOR = /^#[0-9a-fA-F]{6}$/;
|
||||
const HOST_ICON = /^[a-z0-9-]{1,64}$/;
|
||||
|
||||
/**
|
||||
* Color and icon from a host write. A field left out stays as stored, so
|
||||
* editors that don't know about them never clear them. Bad values store null.
|
||||
*/
|
||||
export function normalizeHostLookFields(values: Record<string, unknown>): {
|
||||
color?: string | null;
|
||||
icon?: string | null;
|
||||
} {
|
||||
const result: { color?: string | null; icon?: string | null } = {};
|
||||
if (values.color !== undefined) {
|
||||
result.color =
|
||||
typeof values.color === "string" && HOST_COLOR.test(values.color)
|
||||
? values.color.toLowerCase()
|
||||
: null;
|
||||
}
|
||||
if (values.icon !== undefined) {
|
||||
result.icon =
|
||||
typeof values.icon === "string" && HOST_ICON.test(values.icon)
|
||||
? values.icon
|
||||
: null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const PROTOCOL_ENABLE_FIELDS = ["enableSsh"] as const;
|
||||
|
||||
export function normalizeProtocolEnableFields(
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
isOptionalAuthType,
|
||||
isOptionalBoolean,
|
||||
isValidPort,
|
||||
normalizeHostLookFields,
|
||||
normalizeProtocolEnableFields,
|
||||
OWNER_PRIVATE_AUTH_FIELDS,
|
||||
OWNER_PRIVATE_SSH_OPTION_FIELDS,
|
||||
@@ -335,6 +336,7 @@ router.post(
|
||||
? JSON.stringify(portKnockSequence)
|
||||
: null,
|
||||
...normalizeProtocolEnableFields(hostData),
|
||||
...normalizeHostLookFields(hostData),
|
||||
sshPort: sshPort || port || 22,
|
||||
};
|
||||
|
||||
@@ -912,6 +914,7 @@ router.put(
|
||||
? JSON.stringify(portKnockSequence)
|
||||
: null,
|
||||
...normalizeProtocolEnableFields(hostData),
|
||||
...normalizeHostLookFields(hostData),
|
||||
sshPort: sshPort || port || 22,
|
||||
};
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ async function exportRow(
|
||||
: resolvedHost.tags || [],
|
||||
pin: !!resolvedHost.pin,
|
||||
notes: resolvedHost.notes || null,
|
||||
color: resolvedHost.color || null,
|
||||
icon: resolvedHost.icon || null,
|
||||
// Every plugin's host settings, secrets redacted, for import to hand back.
|
||||
pluginSettings: pluginSettings.get(host.id as number) ?? {},
|
||||
// Each plugin protocol's login, secrets included like the SSH ones.
|
||||
|
||||
@@ -28,6 +28,7 @@ import { applyPluginHostImportSettings } from "../../database/routes/host-plugin
|
||||
import {
|
||||
isNonEmptyString,
|
||||
isValidPort,
|
||||
normalizeHostLookFields,
|
||||
normalizeImportedHost,
|
||||
} from "../../database/routes/host-normalizers.js";
|
||||
import { hasHostLocks, hostLock, manualCreateBlockedBy } from "../locks.js";
|
||||
@@ -571,6 +572,7 @@ export async function runBulkImport(
|
||||
? "true"
|
||||
: "false",
|
||||
notes: hostData.notes || null,
|
||||
...normalizeHostLookFields(hostData as Record<string, unknown>),
|
||||
useSocks5: hostData.useSocks5 ? 1 : 0,
|
||||
socks5Host: hostData.socks5Host || null,
|
||||
socks5Port: hostData.socks5Port || null,
|
||||
|
||||
@@ -214,3 +214,43 @@ describe("host.created", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("host color and icon", () => {
|
||||
async function stored(id: number) {
|
||||
const [row] = await ctx.drizzle
|
||||
.select({ color: hosts.color, icon: hosts.icon })
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, id));
|
||||
return row;
|
||||
}
|
||||
|
||||
it("saves them on create and keeps them when an update leaves them out", async () => {
|
||||
const created = await request(app)
|
||||
.post("/host/db/host")
|
||||
.send({
|
||||
...base,
|
||||
authType: "password",
|
||||
password: "pw",
|
||||
color: "#22C55E",
|
||||
icon: "database",
|
||||
});
|
||||
expect(created.status).toBe(200);
|
||||
expect(await stored(created.body.id)).toEqual({
|
||||
color: "#22c55e",
|
||||
icon: "database",
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.put(`/host/db/host/${created.body.id}`)
|
||||
.send({ ...base, name: "renamed" });
|
||||
expect(await stored(created.body.id)).toEqual({
|
||||
color: "#22c55e",
|
||||
icon: "database",
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.put(`/host/db/host/${created.body.id}`)
|
||||
.send({ ...base, color: null, icon: null });
|
||||
expect(await stored(created.body.id)).toEqual({ color: null, icon: null });
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
keepsStoredSshAuth,
|
||||
isOptionalBoolean,
|
||||
isValidPort,
|
||||
normalizeHostLookFields,
|
||||
normalizeImportedHost,
|
||||
normalizeProtocolEnableFields,
|
||||
renameFolderPath,
|
||||
@@ -529,3 +530,25 @@ describe("impliedSshAuthType", () => {
|
||||
expect(impliedSshAuthType({})).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeHostLookFields", () => {
|
||||
it("leaves both fields out when the write does not send them", () => {
|
||||
expect(normalizeHostLookFields({ name: "web" })).toEqual({});
|
||||
});
|
||||
|
||||
it("keeps a hex color and a Lucide name", () => {
|
||||
expect(
|
||||
normalizeHostLookFields({ color: "#3B82F6", icon: "server-cog" }),
|
||||
).toEqual({ color: "#3b82f6", icon: "server-cog" });
|
||||
});
|
||||
|
||||
it("stores null for cleared or bad values", () => {
|
||||
expect(normalizeHostLookFields({ color: "red", icon: "<script>" })).toEqual(
|
||||
{ color: null, icon: null },
|
||||
);
|
||||
expect(normalizeHostLookFields({ color: null, icon: "" })).toEqual({
|
||||
color: null,
|
||||
icon: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,10 @@ export type Host = {
|
||||
terminalConfig?: HostTerminalConfig;
|
||||
sshOptions?: HostSshOptions;
|
||||
notes?: string;
|
||||
/** Hex color, like #3b82f6. */
|
||||
color?: string | null;
|
||||
/** Lucide icon name. */
|
||||
icon?: string | null;
|
||||
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
@@ -182,6 +186,10 @@ export interface HostData {
|
||||
terminalConfig?: HostTerminalConfig;
|
||||
sshOptions?: HostSshOptions;
|
||||
notes?: string;
|
||||
/** Hex color, like #3b82f6. */
|
||||
color?: string | null;
|
||||
/** Lucide icon name. */
|
||||
icon?: string | null;
|
||||
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
|
||||
Reference in new issue
Block a user