Files
prowler/ui/lib/organizations.ts
T

114 lines
3.7 KiB
TypeScript

/**
* Organization vocabulary, keyed by organization type and node kind.
*
* Every hierarchy surface (providers table, row actions, deletion dialogs,
* onboarding copy) reads its wording from here instead of branching on a
* per-provider boolean. The table is typed `satisfies Record<OrganizationType,
* …>`, so adding an organization type is a compile error until its vocabulary
* exists — a new type can never silently inherit AWS wording.
*/
import {
NODE_KIND,
NodeKind,
ORGANIZATION_TYPE,
OrganizationType,
} from "@/types/organizations";
interface CandidateNoun {
singular: string;
plural: string;
}
interface OrgTypeTerminology {
/** Hierarchy container label, used when a node carries no `kind`. */
containerLabel: string;
/** Where the provider-side organization/node name comes from. */
nameSourceLabel: string;
/** What a discovered candidate is called in the onboarding flow. */
candidateNoun: CandidateNoun;
}
const ORGANIZATION_TERMINOLOGY = {
[ORGANIZATION_TYPE.AWS]: {
containerLabel: "Organizational Unit",
nameSourceLabel: "AWS",
candidateNoun: { singular: "account", plural: "accounts" },
},
[ORGANIZATION_TYPE.AZURE]: {
containerLabel: "Management Group",
nameSourceLabel: "Azure",
candidateNoun: { singular: "subscription", plural: "subscriptions" },
},
[ORGANIZATION_TYPE.GCP]: {
containerLabel: "Folder",
nameSourceLabel: "Google Cloud",
candidateNoun: { singular: "project", plural: "projects" },
},
} as const satisfies Record<OrganizationType, OrgTypeTerminology>;
const NODE_KIND_LABEL = {
[NODE_KIND.ORGANIZATIONAL_UNIT]: "Organizational Unit",
[NODE_KIND.FOLDER]: "Folder",
} as const satisfies Record<NodeKind, string>;
const NODE_KINDS: readonly string[] = Object.values(NODE_KIND);
/**
* The organization-type enum mirrors a server-side one, so a type this build
* doesn't know about can still arrive on the wire. Rendering neutral wording
* beats crashing a table cell — or claiming AWS.
*/
const NEUTRAL_TERMINOLOGY: OrgTypeTerminology = {
containerLabel: "Group",
nameSourceLabel: "the cloud provider",
candidateNoun: { singular: "account", plural: "accounts" },
};
const ORGANIZATION_TYPES: readonly string[] = Object.values(ORGANIZATION_TYPE);
// Membership check, not a `??` on the lookup: the tables are object literals, so
// an inherited key ("toString") would resolve to a truthy non-string.
function terminologyFor(orgType: OrganizationType): OrgTypeTerminology {
return ORGANIZATION_TYPES.includes(orgType)
? ORGANIZATION_TERMINOLOGY[orgType]
: NEUTRAL_TERMINOLOGY;
}
/**
* Container label for a hierarchy node. `kind` decides when present (canonical
* contract); the organization type is the fallback. Never derived from ID
* prefixes.
*/
export function getNodeLabel(
orgType: OrganizationType,
kind?: NodeKind,
): string {
// `kind` is typed but unvalidated: node rows pass the wire attribute through.
const knownKind = toNodeKind(kind);
return knownKind
? NODE_KIND_LABEL[knownKind]
: terminologyFor(orgType).containerLabel;
}
/** Provider-side source of the organization name (edit-name helper copy). */
export function getNameSourceLabel(orgType: OrganizationType): string {
return terminologyFor(orgType).nameSourceLabel;
}
/**
* What discovered candidates are called for this organization type.
*/
export function getCandidateNoun(orgType: OrganizationType): CandidateNoun {
return terminologyFor(orgType).candidateNoun;
}
/**
* Narrows a tree item's opaque `kind` string (the generic `TreeDataItem` carries
* no organization types) to a canonical node kind.
*/
export function toNodeKind(kind?: string): NodeKind | undefined {
return kind && NODE_KINDS.includes(kind) ? (kind as NodeKind) : undefined;
}