mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-01 08:05:23 +00:00
chore: split dashboard quick actions into quarters and drop host status row lines
This commit is contained in:
1 parent
46b3adeb57
commit
dbf6c0117f
3 files changed
+78
-31
No files matched your search
@@ -24,6 +24,7 @@ import {
|
|||||||
import { getDefaultConnectionTab } from "@/lib/host-connection-tabs";
|
import { getDefaultConnectionTab } from "@/lib/host-connection-tabs";
|
||||||
import type { RecentActivityItem } from "@/main-axios";
|
import type { RecentActivityItem } from "@/main-axios";
|
||||||
import type { Host, TabType } from "@/types/ui-types";
|
import type { Host, TabType } from "@/types/ui-types";
|
||||||
|
import { quarterBorders } from "./quarter-borders";
|
||||||
|
|
||||||
export type VersionStatus =
|
export type VersionStatus =
|
||||||
"up_to_date" | "requires_update" | "beta" | "unknown";
|
"up_to_date" | "requires_update" | "beta" | "unknown";
|
||||||
@@ -161,17 +162,19 @@ function ActionButton({
|
|||||||
label,
|
label,
|
||||||
hint,
|
hint,
|
||||||
onClick,
|
onClick,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
label: string;
|
label: string;
|
||||||
hint: string;
|
hint: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="group/btn flex h-full min-w-0 items-center gap-2 border border-border/60 px-2 py-1.5 text-left transition-colors hover:border-accent-brand/40 hover:bg-muted/50"
|
className={`group/btn flex h-full min-w-0 items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-muted/50 ${className ?? ""}`}
|
||||||
>
|
>
|
||||||
<span className="flex size-6 shrink-0 items-center justify-center border border-border bg-muted transition-colors group-hover/btn:border-accent-brand/40 group-hover/btn:bg-accent-brand/20">
|
<span className="flex size-6 shrink-0 items-center justify-center border border-border bg-muted transition-colors group-hover/btn:border-accent-brand/40 group-hover/btn:bg-accent-brand/20">
|
||||||
<Icon className="size-3 text-accent-brand" />
|
<Icon className="size-3 text-accent-brand" />
|
||||||
@@ -208,39 +211,48 @@ export function QuickActions({
|
|||||||
: host.port;
|
: host.port;
|
||||||
return `${host.ip}:${port ?? host.port}`;
|
return `${host.ip}:${port ?? host.port}`;
|
||||||
};
|
};
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
icon: Plus,
|
||||||
|
label: t("dashboard.addHost"),
|
||||||
|
hint: t("dashboardTab.registerNewServer"),
|
||||||
|
onClick: () =>
|
||||||
|
onOpenSingletonTab("host-manager", "host-manager:add-host"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: KeyRound,
|
||||||
|
label: t("dashboard.addCredential"),
|
||||||
|
hint: t("dashboardTab.storeSshKeysOrPasswords"),
|
||||||
|
onClick: () =>
|
||||||
|
onOpenSingletonTab("host-manager", "host-manager:add-credential"),
|
||||||
|
},
|
||||||
|
...(isAdmin
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: Settings,
|
||||||
|
label: t("dashboard.adminSettings"),
|
||||||
|
hint: t("dashboardTab.manageUsersAndRoles"),
|
||||||
|
onClick: () => onOpenSingletonTab("admin-settings"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
icon: User,
|
||||||
|
label: t("dashboard.userProfile"),
|
||||||
|
hint: t("dashboardTab.manageYourAccount"),
|
||||||
|
onClick: () => onOpenSingletonTab("user-profile"),
|
||||||
|
},
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full flex-1 flex-col">
|
<div className="flex min-h-full flex-1 flex-col">
|
||||||
<div className="grid flex-1 auto-rows-fr grid-cols-1 gap-1 p-2 sm:grid-cols-2">
|
<div className="grid flex-1 auto-rows-fr grid-cols-1 sm:grid-cols-2">
|
||||||
<ActionButton
|
{actions.map((action, i) => (
|
||||||
icon={Plus}
|
|
||||||
label={t("dashboard.addHost")}
|
|
||||||
hint={t("dashboardTab.registerNewServer")}
|
|
||||||
onClick={() =>
|
|
||||||
onOpenSingletonTab("host-manager", "host-manager:add-host")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
icon={KeyRound}
|
|
||||||
label={t("dashboard.addCredential")}
|
|
||||||
hint={t("dashboardTab.storeSshKeysOrPasswords")}
|
|
||||||
onClick={() =>
|
|
||||||
onOpenSingletonTab("host-manager", "host-manager:add-credential")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isAdmin && (
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={Settings}
|
key={action.label}
|
||||||
label={t("dashboard.adminSettings")}
|
{...action}
|
||||||
hint={t("dashboardTab.manageUsersAndRoles")}
|
className={quarterBorders(i, actions.length)}
|
||||||
onClick={() => onOpenSingletonTab("admin-settings")}
|
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
<ActionButton
|
|
||||||
icon={User}
|
|
||||||
label={t("dashboard.userProfile")}
|
|
||||||
hint={t("dashboardTab.manageYourAccount")}
|
|
||||||
onClick={() => onOpenSingletonTab("user-profile")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{pinnedHosts.length > 0 && (
|
{pinnedHosts.length > 0 && (
|
||||||
<div className="flex flex-col border-t border-border">
|
<div className="flex flex-col border-t border-border">
|
||||||
@@ -308,7 +320,7 @@ export function HostStatusList({
|
|||||||
defaultConnectAction(actions, host)?.tabType;
|
defaultConnectAction(actions, host)?.tabType;
|
||||||
if (target) onOpenTab(host, target);
|
if (target) onOpenTab(host, target);
|
||||||
}}
|
}}
|
||||||
className="group/row flex min-w-0 cursor-pointer items-center gap-2 border-b border-border/60 px-3 py-1.5 last:border-0 hover:bg-muted/50"
|
className="group/row flex min-w-0 cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`size-1.5 shrink-0 rounded-full ${getStatusClasses(availability, scheme, "dot", statusLoading)}`}
|
className={`size-1.5 shrink-0 rounded-full ${getStatusClasses(availability, scheme, "dot", statusLoading)}`}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Lines only between cells, so the actions read as quarters: one column on
|
||||||
|
* small screens, two above. An odd last cell spans the full row.
|
||||||
|
*/
|
||||||
|
export function quarterBorders(index: number, count: number): string {
|
||||||
|
const classes = ["border-border"];
|
||||||
|
const last = index === count - 1;
|
||||||
|
if (!last) classes.push("border-b");
|
||||||
|
if (Math.floor(index / 2) === Math.floor((count - 1) / 2)) {
|
||||||
|
classes.push("sm:border-b-0");
|
||||||
|
}
|
||||||
|
if (index % 2 === 0 && !last) classes.push("sm:border-r");
|
||||||
|
if (last && count % 2 === 1) classes.push("sm:col-span-2");
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { quarterBorders } from "@/dashboard/quarter-borders";
|
||||||
|
|
||||||
|
describe("quarterBorders", () => {
|
||||||
|
it("draws a cross between four cells", () => {
|
||||||
|
expect([0, 1, 2, 3].map((i) => quarterBorders(i, 4))).toEqual([
|
||||||
|
"border-border border-b sm:border-r",
|
||||||
|
"border-border border-b",
|
||||||
|
"border-border border-b sm:border-b-0 sm:border-r",
|
||||||
|
"border-border sm:border-b-0",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stretches an odd last cell across the row", () => {
|
||||||
|
expect(quarterBorders(2, 3)).toBe(
|
||||||
|
"border-border sm:border-b-0 sm:col-span-2",
|
||||||
|
);
|
||||||
|
expect(quarterBorders(1, 3)).toBe("border-border border-b");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user