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
540 changes: 538 additions & 2 deletions src/Compliance.App/ClientApp/src/api-client/api.ts

Large diffs are not rendered by default.

750 changes: 749 additions & 1 deletion src/Compliance.App/ClientApp/src/api-client/operations.ts

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions src/Compliance.App/ClientApp/src/api-client/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@ export type Portia1EB0255AC1C1D800F4E43175A476BE2EEF9E1FBDC2D4430BCED070CC5D913F
"name": string;
} | null;

export type Portia411B3239057A525C3562E4BE0D1A71F30A3844A0A56231069471136CDD4A37E4 = {
"items": Array<{
"role_id": string;
"name": string;
} | null>;
"next_cursor": string | null;
} | null;

export type Portia444702261363EF785C20C78504EBD948CC2950CA076C0FEA93FBD650F83E4680 = {
"role_id": string;
"name": string;
} | null;

export type Portia445823D45DEBC21876B8F7D21A7706F96E8DB18D68A39E931F58C101C313F09E = {
"items": Array<{
"role_id": string;
"team_id": string;
} | null>;
"next_cursor": string | null;
} | null;

export type Portia534841146AA01FC616B441EB6CC816411AE29E916AB93C8329D97EB96B560E0A = {
"items": Array<{
"team_id": string;
Expand Down Expand Up @@ -38,3 +59,11 @@ export type PortiaEA6D5265BAC64B7B7ECBB403179F0802AEF5CDBFF6D32E0FCDF3EF54CB637D
} | null>;
"next_cursor": string | null;
} | null;

export type PortiaEC8A68FE610BE733B5FB5E10D0C0A79743573CEEBB9114A5BBD9B4542987EC3B = {
"items": Array<{
"role_id": string;
"permission": string;
} | null>;
"next_cursor": string | null;
} | null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { state } from '@askrjs/askr';
import {
Block,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Page,
PageHeader,
} from '@askrjs/themes/components';

import {
assignRolePermission,
assignTeamRole,
getRole,
listRolePermissions,
listRoleTeams,
removeRolePermission,
removeTeamRole,
type RolePermissionSummary,
type RoleSummary,
type RoleTeamSummary,
} from '../roles.js';

export function RoleDetailPage({ roleId }: { roleId: string }) {
const [role, setRole] = state<RoleSummary | null>(null);
const [permissions, setPermissions] = state<RolePermissionSummary[] | null>(null);
const [teams, setTeams] = state<RoleTeamSummary[] | null>(null);
const [permission, setPermission] = state('');
const [teamId, setTeamId] = state('');
const [error, setError] = state<string | null>(null);
const [submittingPermission, setSubmittingPermission] = state(false);
const [submittingTeam, setSubmittingTeam] = state(false);

function load() {
void getRole(roleId)
.then(setRole)
.catch((failure: unknown) =>
setError(failure instanceof Error ? failure.message : 'Unable to load the role.')
);
void listRolePermissions(roleId)
.then(setPermissions)
.catch((failure: unknown) =>
setError(failure instanceof Error ? failure.message : 'Unable to load the role permissions.')
);
void listRoleTeams(roleId)
.then(setTeams)
.catch((failure: unknown) =>
setError(failure instanceof Error ? failure.message : 'Unable to load the role teams.')
);
}

load();

async function addPermission(event?: { preventDefault?: () => void }) {
event?.preventDefault?.();
setError(null);
setSubmittingPermission(true);
try {
await assignRolePermission(roleId, permission());
setPermission('');
load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'Unable to add that permission.');
} finally {
setSubmittingPermission(false);
}
}

async function removePermission(targetPermission: string) {
setError(null);
try {
await removeRolePermission(roleId, targetPermission);
load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'Unable to remove that permission.');
}
}

async function addTeam(event?: { preventDefault?: () => void }) {
event?.preventDefault?.();
setError(null);
setSubmittingTeam(true);
try {
await assignTeamRole(roleId, teamId());
setTeamId('');
load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'Unable to grant that team this role.');
} finally {
setSubmittingTeam(false);
}
}

async function removeTeam(targetTeamId: string) {
setError(null);
try {
await removeTeamRole(roleId, targetTeamId);
load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'Unable to remove that team.');
}
}

return (
<Page>
<PageHeader
title={role()?.name ?? 'Role'}
description="Teams are identified by team ID until a directory search exists."
/>
{error() ? <p role="alert">{error()}</p> : null}
<Card>
<CardHeader>
<CardTitle>Permissions</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={(event) => void addPermission(event)}>
<Block direction="row" gap="md">
<input
type="text"
placeholder="Permission, e.g. tenant.access"
value={permission()}
onInput={(event: Event) => setPermission((event.target as HTMLInputElement).value)}
required
/>
<Button variant="primary" type="submit" disabled={submittingPermission()}>
{submittingPermission() ? 'Adding…' : 'Add permission'}
</Button>
</Block>
</form>
{permissions() === null ? (
<p>Loading…</p>
) : (
<Block direction="column" gap="sm">
{(permissions() ?? []).map((item) => (
<Block key={item.permission} direction="row" gap="md" align="center" justify="between">
<span>{item.permission}</span>
<Button variant="destructive" onPress={() => void removePermission(item.permission)}>
Remove
</Button>
</Block>
))}
</Block>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Teams</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={(event) => void addTeam(event)}>
<Block direction="row" gap="md">
<input
type="text"
placeholder="Team ID"
value={teamId()}
onInput={(event: Event) => setTeamId((event.target as HTMLInputElement).value)}
required
/>
<Button variant="primary" type="submit" disabled={submittingTeam()}>
{submittingTeam() ? 'Granting…' : 'Grant to team'}
</Button>
</Block>
</form>
{teams() === null ? (
<p>Loading…</p>
) : (
<Block direction="column" gap="sm">
{(teams() ?? []).map((item) => (
<Block key={item.teamId} direction="row" gap="md" align="center" justify="between">
<span>{item.teamId}</span>
<Button variant="destructive" onPress={() => void removeTeam(item.teamId)}>
Remove
</Button>
</Block>
))}
</Block>
)}
</CardContent>
</Card>
<Button asChild variant="ghost">
<a href="/roles">Back to roles</a>
</Button>
</Page>
);
}
103 changes: 103 additions & 0 deletions src/Compliance.App/ClientApp/src/features/roles/pages/roles-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { state } from '@askrjs/askr';
import {
Block,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Page,
PageHeader,
} from '@askrjs/themes/components';

import { defineRole, deleteRole, listRoles, type RoleSummary } from '../roles.js';

export function RolesListPage() {
const [roles, setRoles] = state<RoleSummary[] | null>(null);
const [name, setName] = state('');
const [error, setError] = state<string | null>(null);
const [submitting, setSubmitting] = state(false);

function load() {
void listRoles()
.then(setRoles)
.catch((failure: unknown) =>
setError(failure instanceof Error ? failure.message : 'Unable to load roles.')
);
}

load();

async function create(event?: { preventDefault?: () => void }) {
event?.preventDefault?.();
setError(null);
setSubmitting(true);
try {
await defineRole(name());
setName('');
load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'Unable to create the role.');
} finally {
setSubmitting(false);
}
}

async function remove(roleId: string) {
setError(null);
try {
await deleteRole(roleId);
load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'Unable to delete the role.');
}
}

return (
<Page>
<PageHeader
title="Roles"
description="Create roles, assign permissions, and grant them to teams."
/>
<Card>
<CardHeader>
<CardTitle>New role</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={(event) => void create(event)}>
<Block direction="row" gap="md">
<input
type="text"
placeholder="Role name"
value={name()}
onInput={(event: Event) => setName((event.target as HTMLInputElement).value)}
required
/>
<Button variant="primary" type="submit" disabled={submitting()}>
{submitting() ? 'Creating…' : 'Create role'}
</Button>
</Block>
</form>
</CardContent>
</Card>
{error() ? <p role="alert">{error()}</p> : null}
{roles() === null && !error() ? <p>Loading…</p> : null}
<Block direction="column" gap="md">
{(roles() ?? []).map((role) => (
<Card key={role.roleId}>
<CardContent>
<Block direction="row" gap="md" align="center" justify="between">
<Button asChild variant="ghost">
<a href={`/roles/${role.roleId}`}>{role.name}</a>
</Button>
<Button variant="destructive" onPress={() => void remove(role.roleId)}>
Delete
</Button>
</Block>
</CardContent>
</Card>
))}
</Block>
</Page>
);
}
Loading