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
17 changes: 14 additions & 3 deletions apps/web/src/page/settings/kit/org-membership.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,21 @@ describe("leaveEligibility", () => {
})

describe("toOrgRole", () => {
it("reads unknown Clerk roles as member", () => {
it("reads unknown Clerk roles as analyst, the least a person can hold", () => {
expect(toOrgRole("org:admin")).toBe("org:admin")
expect(toOrgRole("org:billing")).toBe("org:member")
expect(orgRoleLabel("org:viewer")).toBe("Member")
expect(toOrgRole("org:billing")).toBe("org:analyst")
expect(orgRoleLabel("org:viewer")).toBe("Analyst")
})

it("shows root, which has no entry of its own, as a member", () => {
expect(orgRoleLabel("root")).toBe("Member")
})

it("names the analyst role in a role change prompt", () => {
const prompt = roleChangePrompt("Ada", "org:analyst")
expect(prompt.confirmLabel).toBe("Make analyst")
expect(prompt.question).toContain("Make Ada an analyst?")
expect(prompt.question).toContain("builds dashboards")
})
})

Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/page/settings/kit/org-membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ export type OrgRole = Policy.ClerkRoleKey
/** The assignable Clerk role keys, lowest role first. */
export const orgRoleKeys: ReadonlyArray<OrgRole> = Arr.map(Policy.assignableRoles, (entry) => entry.clerkKey)

const [memberEntry] = Policy.assignableRoles
// `root`, which Clerk never assigns, has no entry of its own and shows as a member.
const memberEntry = Policy.assignableRoles[1]

/** A Clerk role string's entry, read through `Policy.roleOf` (unknown roles are members). */
/** A Clerk role string's entry, read through `Policy.roleOf` (unknown roles are analysts). */
const entryOf = (role: string): Policy.AssignableRole => {
const policyRole = Policy.roleOf(role)
return Option.getOrElse(
Expand Down
13 changes: 8 additions & 5 deletions packages/domain/src/policy/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,20 @@ describe("Policy.grantsOf", () => {
describe("roleOf", () => {
it("maps the Clerk keys the Members page assigns", () => {
expect(Arr.map(Policy.assignableRoles, (entry) => Policy.roleOf(entry.clerkKey))).toEqual([
"analyst",
"member",
"engineer",
"admin",
])
})

it("reads the analyst key as analyst, below every assignable role", () => {
expect(Policy.roleOf("org:analyst")).toBe("analyst")
for (const entry of Policy.assignableRoles) {
expect(Policy.outranks(entry.role, "analyst")).toBe(true)
}
it("lists the assignable roles lowest first, so each outranks the one before it", () => {
const roles = Arr.map(Policy.assignableRoles, (entry) => entry.role)
expect(
Arr.every(Arr.zip(roles, Arr.drop(roles, 1)), ([lower, higher]) =>
Policy.outranks(higher, lower),
),
).toBe(true)
})
})

Expand Down
19 changes: 8 additions & 11 deletions packages/domain/src/policy/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ const byRank: Order.Order<Role> = Order.mapInput(Order.Number, (role) => RANK[ro

/** What the Members page shows for a role a person can be given in Clerk. */
export const assignableRoles = [
{
role: "analyst",
clerkKey: "org:analyst",
label: "Analyst",
summary:
"Follows incidents, reliability, product analytics and cost, and builds dashboards and funnels.",
},
{
role: "member",
clerkKey: "org:member",
Expand All @@ -43,16 +50,6 @@ export const assignableRoles = [
export type AssignableRole = (typeof assignableRoles)[number]
export type ClerkRoleKey = AssignableRole["clerkKey"]

/** For people who follow the product and build dashboards without operating it. Not offered on the Members page yet. */
const analystRole = {
role: "analyst",
clerkKey: "org:analyst",
label: "Analyst",
summary: "Follows incidents, reliability, product analytics and cost, and builds dashboards and funnels.",
} as const satisfies { role: Role; clerkKey: string; label: string; summary: string }

const clerkRoles = [analystRole, ...assignableRoles] as const

/**
* A Clerk or session role string as a {@link Role}. A role this code does not know reads as
* analyst, the least it can hold.
Expand All @@ -61,7 +58,7 @@ export const roleOf = (name: string): Role => {
if (name === "root") return "root"
return Option.getOrElse(
Option.map(
Arr.findFirst(clerkRoles, (entry) => entry.clerkKey === name),
Arr.findFirst(assignableRoles, (entry) => entry.clerkKey === name),
(entry): Role => entry.role,
),
(): Role => "analyst",
Expand Down
Loading