mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +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
|
- 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
|
- 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
|
- 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
|
## 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,
|
"when": 1792430772041,
|
||||||
"tag": "0075_session_policy",
|
"tag": "0075_session_policy",
|
||||||
"breakpoints": true
|
"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,
|
"when": 1792430770990,
|
||||||
"tag": "0075_session_policy",
|
"tag": "0075_session_policy",
|
||||||
"breakpoints": true
|
"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,
|
"when": 1792430769899,
|
||||||
"tag": "0070_session_policy",
|
"tag": "0070_session_policy",
|
||||||
"breakpoints": true
|
"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: "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: "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: "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) {
|
for (const migration of sshDataMigrations) {
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ export const hosts = mysqlTable(
|
|||||||
// legacy algorithms, agent, environment). JSON.
|
// legacy algorithms, agent, environment). JSON.
|
||||||
sshOptions: text("ssh_options"),
|
sshOptions: text("ssh_options"),
|
||||||
notes: text("notes"),
|
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),
|
enableSsh: boolean("enable_ssh").notNull().default(true),
|
||||||
|
|
||||||
sshPort: int("ssh_port").default(22),
|
sshPort: int("ssh_port").default(22),
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ export const hosts = pgTable(
|
|||||||
// legacy algorithms, agent, environment). JSON.
|
// legacy algorithms, agent, environment). JSON.
|
||||||
sshOptions: text("ssh_options"),
|
sshOptions: text("ssh_options"),
|
||||||
notes: text("notes"),
|
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),
|
enableSsh: boolean("enable_ssh").notNull().default(true),
|
||||||
|
|
||||||
sshPort: integer("ssh_port").default(22),
|
sshPort: integer("ssh_port").default(22),
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ export const hosts = sqliteTable(
|
|||||||
// legacy algorithms, agent, environment). JSON.
|
// legacy algorithms, agent, environment). JSON.
|
||||||
sshOptions: text("ssh_options"),
|
sshOptions: text("ssh_options"),
|
||||||
notes: text("notes"),
|
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),
|
enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true),
|
||||||
|
|
||||||
sshPort: integer("ssh_port").default(22),
|
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;
|
const PROTOCOL_ENABLE_FIELDS = ["enableSsh"] as const;
|
||||||
|
|
||||||
export function normalizeProtocolEnableFields(
|
export function normalizeProtocolEnableFields(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
isOptionalAuthType,
|
isOptionalAuthType,
|
||||||
isOptionalBoolean,
|
isOptionalBoolean,
|
||||||
isValidPort,
|
isValidPort,
|
||||||
|
normalizeHostLookFields,
|
||||||
normalizeProtocolEnableFields,
|
normalizeProtocolEnableFields,
|
||||||
OWNER_PRIVATE_AUTH_FIELDS,
|
OWNER_PRIVATE_AUTH_FIELDS,
|
||||||
OWNER_PRIVATE_SSH_OPTION_FIELDS,
|
OWNER_PRIVATE_SSH_OPTION_FIELDS,
|
||||||
@@ -335,6 +336,7 @@ router.post(
|
|||||||
? JSON.stringify(portKnockSequence)
|
? JSON.stringify(portKnockSequence)
|
||||||
: null,
|
: null,
|
||||||
...normalizeProtocolEnableFields(hostData),
|
...normalizeProtocolEnableFields(hostData),
|
||||||
|
...normalizeHostLookFields(hostData),
|
||||||
sshPort: sshPort || port || 22,
|
sshPort: sshPort || port || 22,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -912,6 +914,7 @@ router.put(
|
|||||||
? JSON.stringify(portKnockSequence)
|
? JSON.stringify(portKnockSequence)
|
||||||
: null,
|
: null,
|
||||||
...normalizeProtocolEnableFields(hostData),
|
...normalizeProtocolEnableFields(hostData),
|
||||||
|
...normalizeHostLookFields(hostData),
|
||||||
sshPort: sshPort || port || 22,
|
sshPort: sshPort || port || 22,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ async function exportRow(
|
|||||||
: resolvedHost.tags || [],
|
: resolvedHost.tags || [],
|
||||||
pin: !!resolvedHost.pin,
|
pin: !!resolvedHost.pin,
|
||||||
notes: resolvedHost.notes || null,
|
notes: resolvedHost.notes || null,
|
||||||
|
color: resolvedHost.color || null,
|
||||||
|
icon: resolvedHost.icon || null,
|
||||||
// Every plugin's host settings, secrets redacted, for import to hand back.
|
// Every plugin's host settings, secrets redacted, for import to hand back.
|
||||||
pluginSettings: pluginSettings.get(host.id as number) ?? {},
|
pluginSettings: pluginSettings.get(host.id as number) ?? {},
|
||||||
// Each plugin protocol's login, secrets included like the SSH ones.
|
// Each plugin protocol's login, secrets included like the SSH ones.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { applyPluginHostImportSettings } from "../../database/routes/host-plugin
|
|||||||
import {
|
import {
|
||||||
isNonEmptyString,
|
isNonEmptyString,
|
||||||
isValidPort,
|
isValidPort,
|
||||||
|
normalizeHostLookFields,
|
||||||
normalizeImportedHost,
|
normalizeImportedHost,
|
||||||
} from "../../database/routes/host-normalizers.js";
|
} from "../../database/routes/host-normalizers.js";
|
||||||
import { hasHostLocks, hostLock, manualCreateBlockedBy } from "../locks.js";
|
import { hasHostLocks, hostLock, manualCreateBlockedBy } from "../locks.js";
|
||||||
@@ -571,6 +572,7 @@ export async function runBulkImport(
|
|||||||
? "true"
|
? "true"
|
||||||
: "false",
|
: "false",
|
||||||
notes: hostData.notes || null,
|
notes: hostData.notes || null,
|
||||||
|
...normalizeHostLookFields(hostData as Record<string, unknown>),
|
||||||
useSocks5: hostData.useSocks5 ? 1 : 0,
|
useSocks5: hostData.useSocks5 ? 1 : 0,
|
||||||
socks5Host: hostData.socks5Host || null,
|
socks5Host: hostData.socks5Host || null,
|
||||||
socks5Port: hostData.socks5Port || 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,
|
keepsStoredSshAuth,
|
||||||
isOptionalBoolean,
|
isOptionalBoolean,
|
||||||
isValidPort,
|
isValidPort,
|
||||||
|
normalizeHostLookFields,
|
||||||
normalizeImportedHost,
|
normalizeImportedHost,
|
||||||
normalizeProtocolEnableFields,
|
normalizeProtocolEnableFields,
|
||||||
renameFolderPath,
|
renameFolderPath,
|
||||||
@@ -529,3 +530,25 @@ describe("impliedSshAuthType", () => {
|
|||||||
expect(impliedSshAuthType({})).toBeUndefined();
|
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;
|
terminalConfig?: HostTerminalConfig;
|
||||||
sshOptions?: HostSshOptions;
|
sshOptions?: HostSshOptions;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
/** Hex color, like #3b82f6. */
|
||||||
|
color?: string | null;
|
||||||
|
/** Lucide icon name. */
|
||||||
|
icon?: string | null;
|
||||||
|
|
||||||
useSocks5?: boolean;
|
useSocks5?: boolean;
|
||||||
socks5Host?: string;
|
socks5Host?: string;
|
||||||
@@ -182,6 +186,10 @@ export interface HostData {
|
|||||||
terminalConfig?: HostTerminalConfig;
|
terminalConfig?: HostTerminalConfig;
|
||||||
sshOptions?: HostSshOptions;
|
sshOptions?: HostSshOptions;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
/** Hex color, like #3b82f6. */
|
||||||
|
color?: string | null;
|
||||||
|
/** Lucide icon name. */
|
||||||
|
icon?: string | null;
|
||||||
|
|
||||||
useSocks5?: boolean;
|
useSocks5?: boolean;
|
||||||
socks5Host?: string;
|
socks5Host?: string;
|
||||||
|
|||||||
Reference in new issue
Block a user