Skip to content
136 linesCodeBlameRaw
1import { Info } from "lucide-react";
2import { Link, data } from "react-router";
3
4import type { Route } from "./+types/org-chart";
5import { AgentPersonFace, PersonFace, TeamKindMark } from "../../components/people";
6import { Card } from "../../components/ui/card";
7import { Hint } from "../../components/ui/hint";
8import { page } from "../../lib/meta";
9import { type OrgNode, agentPath, orgChart, peopleAgent, personName, personPath } from "../../lib/people";
10import { teamPath } from "../../lib/teams";
11import { identity, workspaceAgents } from "../../lib/services.server";
12import { getViewer, roleIn, unwrap } from "../../lib/session.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Org chart · ${params.owner} · g1t` });
16}
17
18export async function loader({ params, context }: Route.LoaderArgs) {
19 const viewer = getViewer(context);
20 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
21 const [directory, agents] = await Promise.all([
22 identity.peopleDirectory(viewer, params.owner).then(unwrap),
23 workspaceAgents.list(params.owner, viewer!).catch(() => null),
24 ]);
25 return { slug: params.owner.toLowerCase(), directory, agents: agents?.ok ? agents.value.map(peopleAgent) : [] };
26}
27
28export default function OrgChartPage({ loaderData }: Route.ComponentProps) {
29 const { slug, directory, agents } = loaderData;
30 const chart = orgChart(directory, agents);
31 const lines = directory.people.some((person) => person.manager);
32
33 return (
34 <div className="space-y-6">
35 {!lines && (
36 <Card asChild radius="lg" className="flex items-start gap-2 px-3 py-2.5 text-sm text-muted">
37 <p>
38 <Info size={15} className="mt-0.5 shrink-0 text-faint" />
39 {directory.can_manage
40 ? "No one reports to anyone yet. Set who each person reports to under Edit profile on their profile."
41 : "No reporting lines yet. An owner sets who each person reports to on their profile."}
42 </p>
43 </Card>
44 )}
45 <Card asChild className="overflow-x-auto p-4 sm:p-5">
46 <section aria-label="Reporting lines">
47 <ul className="space-y-1">
48 {chart.roots.map((root) => (
49 <Branch key={root.person.username} slug={slug} node={root} />
50 ))}
51 </ul>
52 <p className="mt-4 text-xs text-faint">The agents on a team appear beside the person who leads it, as its members; each opens the agent's page in Agents.</p>
53 </section>
54 </Card>
55
56 {chart.unled.length > 0 && (
57 <section aria-labelledby="unled" className="space-y-3">
58 <h2 id="unled" className="text-sm font-medium text-muted">
59 Teams no person leads
60 </h2>
61 <ul className="grid gap-3 sm:grid-cols-2">
62 {chart.unled.map(({ team, agents: teamAgents }) => (
63 <Card asChild key={team.slug} className="p-4">
64 <li>
65 <div className="flex items-center gap-2">
66 <TeamKindMark people={team.people.length} agents={teamAgents.length} />
67 <Link to={teamPath(slug, team.slug)} className="font-medium hover:text-accent">
68 {team.name}
69 </Link>
70 {team.lead?.kind === "agent" && (
71 <span className="text-xs text-faint">led by {teamAgents.find((a) => a.id === (team.lead as { agent_id: string }).agent_id)?.display_name ?? "an agent"}</span>
72 )}
73 </div>
74 <div className="mt-3 flex flex-wrap gap-2">
75 {teamAgents.map((agent) => (
76 <Link key={agent.id} to={agentPath(slug, agent.handle)} className="inline-flex items-center gap-1.5 rounded-full border border-line py-0.5 pr-2.5 pl-0.5 text-xs hover:border-line-strong">
77 <AgentPersonFace agent={agent} size={20} ring="var(--color-surface)" />
78 {agent.display_name}
79 </Link>
80 ))}
81 </div>
82 </li>
83 </Card>
84 ))}
85 </ul>
86 </section>
87 )}
88 </div>
89 );
90}
91
92/** One person, the agents of the teams they lead, and everyone who reports to them below. */
93function Branch({ slug, node }: { slug: string; node: OrgNode }) {
94 const { person } = node;
95 return (
96 <li>
97 <div className="flex flex-wrap items-center gap-2 py-1">
98 <Link
99 to={personPath(slug, person.username)}
100 prefetch="intent"
101 className="inline-flex min-w-0 items-center gap-2.5 rounded-lg border border-line bg-bg py-1.5 pr-3 pl-1.5 transition-colors hover:border-accent/60"
102 >
103 <PersonFace person={person} size={28} />
104 <span className="grid min-w-0 text-left">
105 <span className="truncate text-sm font-medium">{personName(person)}</span>
106 <span className="truncate text-xs text-muted">{person.title ?? `@${person.username}`}</span>
107 </span>
108 </Link>
109 {node.agents.length > 0 && (
110 <span className="inline-flex items-center gap-2">
111 <span className="flex">
112 {node.agents.slice(0, 6).map((agent, index) => (
113 <Hint key={agent.id} label={`${agent.display_name} · ${agent.title || agent.role} · agent`}>
114 <Link to={agentPath(slug, agent.handle)} className={index ? "-ml-1.5" : undefined} aria-label={agent.display_name}>
115 <AgentPersonFace agent={agent} size={22} ring="var(--color-surface)" />
116 </Link>
117 </Hint>
118 ))}
119 </span>
120 <span className="text-xs text-faint">
121 {node.agents.length > 6 ? `+${node.agents.length - 6} · ` : ""}
122 {node.leads.map((team) => team.name).join(", ")}
123 </span>
124 </span>
125 )}
126 </div>
127 {node.reports.length > 0 && (
128 <ul className="ml-[17px] space-y-1 border-l border-dashed border-line-strong pl-5 sm:pl-7">
129 {node.reports.map((report) => (
130 <Branch key={report.person.username} slug={slug} node={report} />
131 ))}
132 </ul>
133 )}
134 </li>
135 );
136}