Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion SW.Bitween.Web/ClientApp/src/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
RotateCcw,
ScrollText,
Settings,
ShieldCheck,
SlidersHorizontal,
Users,
Webhook,
Expand Down Expand Up @@ -96,7 +97,11 @@ export const NAV_GROUPS: NavGroup[] = [
{
label: "Administration",
items: [
{ label: "Team", path: "/team", icon: Users, permissions: ["users.view", "roles.view"] },
// Who can sign in, then what signing in lets them do. Two entries rather than one
// "Team" with tabs inside it: they are gated on different permissions, so a session
// that holds only one of them used to land on a page whose other half was a dead tab.
{ label: "Members", path: "/team/members", icon: Users, permissions: ["users.view"] },
{ label: "Roles", path: "/team/roles", icon: ShieldCheck, permissions: ["roles.view"] },
{ label: "Settings", path: "/settings", icon: Settings, permissions: ["settings.view"] },
// Last in Administration: it reports on everything above it rather than configuring
// anything, and it is the one page whose value is that nobody can quietly change it.
Expand Down
2 changes: 1 addition & 1 deletion SW.Bitween.Web/ClientApp/src/pages/team/MemberDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Badge, Button, FormError, LoadingBlock } from "../../components/ui/basi
import { Checkbox, PasswordInput } from "../../components/ui/forms";
import { ConfirmDialog } from "../../components/ui/overlays";
import { formatDate, timeAgo, timeUntil } from "../../lib/dates";
import { statusBadge } from "./MembersTab";
import { statusBadge } from "./MembersPage";
import { keys } from "../../api/queryKeys";

function Section({ title, children }: { title: string; children: ReactNode }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query";
import { Search, UserPlus, UsersRound } from "lucide-react";
import { api, type User, type UserStatus } from "../../api";
import { Can } from "../../auth/guards";
import { PageHeader } from "../../components/layout/PageHeader";
import { Avatar } from "../../components/ui/Avatar";
import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics";
import { timeAgo } from "../../lib/dates";
Expand All @@ -25,7 +26,7 @@ export function statusBadge(status: UserStatus, lockedUntil?: string | null) {
return <Badge tone="ok">Active</Badge>;
}

export function MembersTab() {
export function MembersPage() {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const { id: openMemberId } = useParams();
Expand Down Expand Up @@ -67,6 +68,27 @@ export function MembersTab() {

return (
<div>
<PageHeader
title="Members"
description="Everyone who can sign in to this Bitween instance, and what each of them is allowed to do."
help={{
title: "How access works",
body: (
<>
<p>
Every member holds one or more <strong>roles</strong>, and every role is a list of{" "}
<strong>permissions</strong> — page by page, action by action. A member can do
something if any of their roles allows it; everything else is hidden from them.
</p>
<p>
To bring someone in: create or pick a role under <strong>Roles</strong>, then add
them here. You set their first password and pass it on — Bitween doesn't send mail.
</p>
</>
),
}}
/>

<div className="mb-4 flex flex-wrap items-center gap-3">
<div className="relative min-w-56 flex-1 sm:max-w-xs">
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-ink-400" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,40 @@ import { ChevronRight, Plus, ShieldCheck } from "lucide-react";
import { api } from "../../api";
import { allKeysIn, usePermissionCatalog } from "../../api/permissions";
import { Can } from "../../auth/guards";
import { PageHeader } from "../../components/layout/PageHeader";
import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics";
import { keys } from "../../api/queryKeys";

export function RolesTab() {
export function RolesPage() {
const navigate = useNavigate();
const roles = useQuery({ queryKey: keys.roles.list, queryFn: () => api.listRoles() });
const totalPermissions = allKeysIn(usePermissionCatalog().data ?? []).length;

if (roles.isPending) return <LoadingBlock label="Loading roles…" />;

return (
<div>
<div className="mb-4 flex items-center justify-between gap-3">
<p className="text-sm text-ink-500">
A role is a reusable set of permissions. Open one to see — and shape — exactly what its
members can do.
</p>
const header = (
<PageHeader
title="Roles"
description="A role is a reusable set of permissions. Open one to see — and shape — exactly what its members can do."
actions={
<Can permission="roles.create">
<Button variant="primary" onClick={() => navigate("/team/roles/new")}>
<Plus className="size-4" /> New role
</Button>
</Can>
}
/>
);

if (roles.isPending)
return (
<div>
{header}
<LoadingBlock label="Loading roles…" />
</div>
);

return (
<div>
{header}

{(roles.data ?? []).length === 0 ? (
<EmptyState icon={<ShieldCheck />} title="No roles yet">
Expand Down
72 changes: 0 additions & 72 deletions SW.Bitween.Web/ClientApp/src/pages/team/TeamPage.tsx

This file was deleted.

65 changes: 33 additions & 32 deletions SW.Bitween.Web/ClientApp/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@ import { AggregationsPage } from "./pages/aggregations/AggregationsPage";
import { NewAggregationPage } from "./pages/aggregations/NewAggregationPage";
import { RetryPoliciesPage } from "./pages/retry-policies/RetryPoliciesPage";
import { RetryPolicyPage } from "./pages/retry-policies/RetryPolicyPage";
import { MembersTab } from "./pages/team/MembersTab";
import { MembersPage } from "./pages/team/MembersPage";
import { RoleEditor } from "./pages/team/RoleEditor";
import { RolesTab } from "./pages/team/RolesTab";
import { TeamIndexRedirect, TeamPage } from "./pages/team/TeamPage";
import { RolesPage } from "./pages/team/RolesPage";
import { WorkGroupPage } from "./pages/work-groups/WorkGroupPage";
import { WorkGroupsPage } from "./pages/work-groups/WorkGroupsPage";

Expand All @@ -57,6 +56,12 @@ function HomeRedirect() {
return <Navigate to={homePath(session)} replace />;
}

/** An old "/team" link lands on whichever of the two pages this session can open. */
function TeamRedirect() {
const { can } = useSession();
return <Navigate to={can("users.view") ? "/team/members" : "/team/roles"} replace />;
}

const placeholderRoutes = NAV_GROUPS.flatMap((group) => group.items)
.filter((item) => item.planned)
.map((item) => ({
Expand Down Expand Up @@ -117,36 +122,32 @@ export const router = createBrowserRouter([
</RequirePermission>
),
},
// Members and Roles are two sidebar entries, not tabs inside a "Team" page, so
// "/team" itself is no longer a page — only a bookmark people may still hold.
{ path: "team", element: <TeamRedirect /> },
{
path: "team/members",
element: (
<RequirePermission permission="users.view">
<MembersPage />
</RequirePermission>
),
},
{
path: "team/members/:id",
element: (
<RequirePermission permission="users.view">
<MembersPage />
</RequirePermission>
),
},
{
path: "team",
element: <TeamPage />,
children: [
{ index: true, element: <TeamIndexRedirect /> },
{
path: "members",
element: (
<RequirePermission permission="users.view">
<MembersTab />
</RequirePermission>
),
},
{
path: "members/:id",
element: (
<RequirePermission permission="users.view">
<MembersTab />
</RequirePermission>
),
},
{
path: "roles",
element: (
<RequirePermission permission="roles.view">
<RolesTab />
</RequirePermission>
),
},
],
path: "team/roles",
element: (
<RequirePermission permission="roles.view">
<RolesPage />
</RequirePermission>
),
},
{
path: "team/roles/new",
Expand Down
Loading