Files
guru-connect/dashboard/src/components/layout/icons.tsx
Mike Swanson 96b4fd7721
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 4m43s
Build and Test / Build Agent (Windows) (push) Successful in 8m48s
Build and Test / Security Audit (push) Successful in 4m38s
Build and Test / Build Summary (push) Has been skipped
feat(dashboard): GuruConnect v2 Users admin view
Admin-only user management: list, create, edit role/permissions/status,
reset password, and disable/delete, against the v2 users API.

- Admin-gated three ways: AdminRoute on /users (calm access-denied panel
  for non-admins, no redirect loop or data fetch), Sidebar hides the nav
  item, and every mutation relies on the server AdminUser 403 as the real
  authority. isAdmin is derived from the server-validated user, not the
  client token.
- Users table: role badge (admin/operator/viewer), permissions summary,
  enabled/disabled status, created, last-login. Sticky header, skeleton,
  empty/error states. Self row tagged "You".
- Create/edit use the real roles and permission strings
  (view/control/transfer/manage_users/manage_clients); admin permissions
  are server-implicit and shown locked. Passwords: typed or Web Crypto
  generated (rejection-sampled, copy-once reveal), type=password +
  autoComplete=new-password, cleared from state on open/close/success,
  never logged/persisted/in-URL; blank on edit means unchanged.
- Self-lockout guards: cannot disable, delete, or demote your own admin
  account (controls disabled + submit-handler checks, matched on the
  authoritative user id). Server mirrors self-disable/self-delete; the
  self-demotion guard is client-side (server todo filed).
- useUpdateUser sequences user-update then permissions-set; invalidates
  ["users"] on settled so the table reconciles after a partial failure,
  with an actionable message if only permissions failed.

Passed Code Review (no blockers after fixes) and local gates
(tsc/lint/build green). Completes the v2 dashboard view set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:18:40 -07:00

173 lines
3.8 KiB
TypeScript

// Inline stroke icons (no icon-library dependency). 18px on a 24 viewBox.
import type { SVGProps } from "react";
type IconProps = SVGProps<SVGSVGElement>;
function base(props: IconProps) {
return {
width: 18,
height: 18,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.8,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
...props,
};
}
export function MachinesIcon(props: IconProps) {
return (
<svg {...base(props)}>
<rect x="2" y="4" width="20" height="13" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
);
}
export function SessionsIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M4 6h16M4 12h16M4 18h10" />
</svg>
);
}
export function CodesIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M8 6 3 12l5 6M16 6l5 6-5 6M13 4l-2 16" />
</svg>
);
}
export function UsersIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13A4 4 0 0 1 16 11" />
</svg>
);
}
export function LogoutIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" />
</svg>
);
}
export function KeyIcon(props: IconProps) {
return (
<svg {...base(props)}>
<circle cx="7.5" cy="15.5" r="4.5" />
<path d="m10.7 12.3 8.3-8.3M16 6l3 3M14 8l2 2" />
</svg>
);
}
export function TrashIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
</svg>
);
}
export function InfoIcon(props: IconProps) {
return (
<svg {...base(props)}>
<circle cx="12" cy="12" r="9" />
<path d="M12 16v-4M12 8h.01" />
</svg>
);
}
export function SearchIcon(props: IconProps) {
return (
<svg {...base(props)}>
<circle cx="11" cy="11" r="7" />
<path d="m21 21-4.3-4.3" />
</svg>
);
}
export function RefreshIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M21 12a9 9 0 1 1-3-6.7L21 8M21 3v5h-5" />
</svg>
);
}
export function CopyIcon(props: IconProps) {
return (
<svg {...base(props)}>
<rect x="9" y="9" width="12" height="12" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
);
}
export function JoinIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
</svg>
);
}
export function StopIcon(props: IconProps) {
return (
<svg {...base(props)}>
<rect x="5" y="5" width="14" height="14" rx="2" />
</svg>
);
}
export function PlusIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M12 5v14M5 12h14" />
</svg>
);
}
export function EditIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
);
}
export function EyeIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
export function EyeOffIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M9.9 4.24A9.1 9.1 0 0 1 12 4c6.5 0 10 7 10 7a18 18 0 0 1-2.16 3.19M6.6 6.6A18 18 0 0 0 2 11s3.5 7 10 7a9 9 0 0 0 5.4-1.6" />
<path d="m9.5 9.5a3 3 0 0 0 4.2 4.2M3 3l18 18" />
</svg>
);
}
export function ShuffleIcon(props: IconProps) {
return (
<svg {...base(props)}>
<path d="M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5" />
</svg>
);
}