Access Request Workflow
blocksecurityJust-In-Time (JIT) IAM temporary privilege escalation request modal and approval queue: governance metrics cards, elevated session countdown timers, approval form dialog, and active session manager with instant revoke and audit logging.
Installation
$pnpm dlx uipkge-ng@latest add access-request-workflow$npx uipkge-ng@latest add access-request-workflow$yarn dlx uipkge-ng@latest add access-request-workflow$bunx uipkge-ng@latest add access-request-workflowManifest:
https://uipkge.dev/r/angular/access-request-workflow.jsonInstalls to:src/app/components/blocks/access-request-workflow/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
AccessRequesterinterface AccessRequester {
name: string
email: string
team: string
avatar: string
}AccessRequestinterface AccessRequest {
id: string
requester: AccessRequester
targetRole: string
system: string
duration: string
timeLeftLabel: string
secondsLeft: number
justification: string
ticketId: string
approver: string
status: AccessRequestStatus
createdAt: string
expiresAt: string
}Files installed (2)
src/app/components/blocks/access-request-workflow/access-request-workflow.component.ts36.7 kB
// Just-In-Time (JIT) IAM temporary privilege escalation: governance metric cards, // elevated session countdown timers, request form dialog, and an approval queue with // instant revoke and audit actions. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { Check, Clock, Copy, FileText, History, Hourglass, KeyRound, Lock, LucideAngularModule, MoreHorizontal, Plus, Search, Send, ShieldAlert, ShieldCheck, UserCheck, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTextareaComponent } from '@/ui/textarea/textarea.component' export type AccessRequestStatus = 'active' | 'pending' | 'expired' | 'rejected' export type AccessRequestTab = 'all' | 'pending' | 'active' | 'expired' export interface AccessRequester { name: string email: string team: string avatar: string } export interface AccessRequest { id: string requester: AccessRequester targetRole: string system: string duration: string timeLeftLabel: string secondsLeft: number justification: string ticketId: string approver: string status: AccessRequestStatus createdAt: string expiresAt: string } const initialRequests: AccessRequest[] = [ { id: 'req-1', requester: { name: 'Elena Rostova', email: '[email protected]', team: 'SecOps Team', avatar: 'ER' }, targetRole: 'AWS Production Admin', system: 'AWS Cloud IAM (us-east-1)', duration: '4 Hours', timeLeftLabel: '2h 45m left', secondsLeft: 9918, justification: 'Investigating latency spike on payment gateway for incident #SEC-849', ticketId: 'SEC-849', approver: 'Marcus Vance', status: 'active', createdAt: '1h 15m ago', expiresAt: 'Today at 16:30', }, { id: 'req-2', requester: { name: 'Marcus Vance', email: '[email protected]', team: 'DevOps & Infra', avatar: 'MV' }, targetRole: 'Production DB Read-Only', system: 'PostgreSQL Aurora Primary', duration: '1 Hour (Emergency)', timeLeftLabel: '48m left', secondsLeft: 2892, justification: 'Emergency replica query check during PostgreSQL failover testing', ticketId: 'INC-2044', approver: 'Sarah Chen', status: 'active', createdAt: '12m ago', expiresAt: 'Today at 14:15', }, { id: 'req-3', requester: { name: 'Sarah Chen', email: '[email protected]', team: 'Core Platform', avatar: 'SC' }, targetRole: 'Customer PII Decryption', system: 'Customer Data Vault (KMS)', duration: '8 Hours (1 Shift)', timeLeftLabel: 'Pending Approval', secondsLeft: 28800, justification: 'GDPR user deletion verification request #COMP-1102', ticketId: 'COMP-1102', approver: 'Marcus Vance', status: 'pending', createdAt: '10m ago', expiresAt: 'Awaiting approval', }, { id: 'req-4', requester: { name: 'David Kim', email: '[email protected]', team: 'Fintech Systems', avatar: 'DK' }, targetRole: 'Stripe Dashboard Full Access', system: 'Stripe Merchant Gateway', duration: '1 Hour (Emergency)', timeLeftLabel: 'Pending Approval', secondsLeft: 3600, justification: 'Refunding disputed charges for compromised merchant accounts', ticketId: 'FIN-582', approver: 'Marcus Vance', status: 'pending', createdAt: '25m ago', expiresAt: 'Awaiting approval', }, { id: 'req-5', requester: { name: 'Alex Morgan', email: '[email protected]', team: 'Data Platform', avatar: 'AM' }, targetRole: 'Production DB Read-Only', system: 'Snowflake Core Analytics', duration: '4 Hours', timeLeftLabel: 'Expired', secondsLeft: 0, justification: 'Quarterly compliance metrics export and audit schema migration verification', ticketId: 'AUD-309', approver: 'Elena Rostova', status: 'expired', createdAt: 'Yesterday', expiresAt: 'Expired at 18:00', }, ] function formatSecondsValue(seconds: number): string { if (seconds <= 0) return '00:00:00' const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) const s = seconds % 60 return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` } function timeLeftLabelFor(seconds: number): string { const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) return h > 0 ? `${h}h ${m}m left` : `${m}m left` } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-access-request-workflow, [ui-access-request-workflow]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, UiTextareaComponent, ], template: ` <div data-slot="access-request-workflow" [class]="rootClass"> <!-- Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-lg"> <lucide-icon [img]="KeyRound" class="size-5" /> </div> <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">{{ title }}</h1> </div> <p class="text-muted-foreground mt-1 text-sm">{{ subtitle }}</p> </div> <button ui-button class="shrink-0 gap-2 shadow-xs" (click)="isDialogOpen.set(true)"> <lucide-icon [img]="Plus" class="size-4" /> Request New Access </button> </div> <!-- Request New Access dialog --> <ui-dialog [open]="isDialogOpen()" (openChange)="isDialogOpen.set($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <h2 ui-dialog-title class="flex items-center gap-2"> <lucide-icon [img]="ShieldAlert" class="text-primary size-5" /> Request Temporary Access </h2> <p ui-dialog-description> Submit a Just-In-Time (JIT) access request with duration and business justification. </p> </div> <form class="space-y-4 py-2" (submit)="handleCreateRequest($event)"> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Target System / Role</label> <ui-select [value]="formRole()" (valueChange)="formRole.set($event)"> <button ui-select-trigger class="w-full"> <ui-select-value placeholder="Select target role" /> </button> <ui-select-content> <ui-select-item value="Production DB Read-Only">Production DB Read-Only</ui-select-item> <ui-select-item value="AWS Production Admin">AWS Production Admin</ui-select-item> <ui-select-item value="Stripe Dashboard Full Access">Stripe Dashboard Full Access</ui-select-item> <ui-select-item value="Customer PII Decryption">Customer PII Decryption</ui-select-item> </ui-select-content> </ui-select> </div> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Access Duration</label> <ui-select [value]="formDuration()" (valueChange)="formDuration.set($event)"> <button ui-select-trigger class="w-full"> <ui-select-value placeholder="Select duration" /> </button> <ui-select-content> <ui-select-item value="1 Hour (Emergency)">1 Hour (Emergency)</ui-select-item> <ui-select-item value="4 Hours">4 Hours</ui-select-item> <ui-select-item value="8 Hours (1 Shift)">8 Hours (1 Shift)</ui-select-item> <ui-select-item value="24 Hours max">24 Hours max</ui-select-item> </ui-select-content> </ui-select> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Approver / Team</label> <ui-select [value]="formApprover()" (valueChange)="formApprover.set($event)"> <button ui-select-trigger class="w-full"> <ui-select-value placeholder="Select approver" /> </button> <ui-select-content> <ui-select-item value="Security Lead: Marcus Vance">Security Lead: Marcus Vance</ui-select-item> <ui-select-item value="Infrastructure Team: Sarah Chen"> Infrastructure Team: Sarah Chen </ui-select-item> <ui-select-item value="Compliance Officer: Elena Rostova"> Compliance Officer: Elena Rostova </ui-select-item> <ui-select-item value="On-Call Lead: David Kim">On-Call Lead: David Kim</ui-select-item> </ui-select-content> </ui-select> </div> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Business Justification</label> <ui-textarea [value]="formJustification()" (valueChange)="formJustification.set($event)" placeholder="Investigating latency spike on payment gateway for incident #SEC-849" [rows]="3" class="resize-none text-xs" /> <p class="text-muted-foreground text-xs"> All elevated actions are recorded and signed in the immutable IAM audit stream. </p> </div> <div ui-dialog-footer class="pt-2"> <button ui-button type="button" variant="outline" (click)="isDialogOpen.set(false)">Cancel</button> <button ui-button type="submit" class="gap-2"> <lucide-icon [img]="Send" class="size-4" /> Submit Request </button> </div> </form> </ui-dialog-content> </ui-dialog> <!-- 4 Access Governance KPI Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="border-border/80 relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Active Elevated Sessions</h3> <div class="bg-success/10 text-success rounded-md p-1.5"> <lucide-icon [img]="ShieldAlert" class="size-4" /> </div> </div> <div ui-card-content> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ activeCount() }}</div> <p class="text-muted-foreground mt-1 text-xs">Expiring within 8h</p> </div> </div> <div ui-card class="border-border/80 relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Pending Approvals</h3> <div class="bg-warning/10 text-warning rounded-md p-1.5"> <lucide-icon [img]="Clock" class="size-4" /> </div> </div> <div ui-card-content> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ pendingCount() }}</div> <p class="text-muted-foreground mt-1 text-xs">Requires security lead review</p> </div> </div> <div ui-card class="border-border/80 relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Requests Expiring Today</h3> <div class="bg-chart-1/10 text-chart-1 rounded-md p-1.5"> <lucide-icon [img]="Hourglass" class="size-4" /> </div> </div> <div ui-card-content> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ expiringCount() }}</div> <p class="text-muted-foreground mt-1 text-xs">Auto-revocation armed</p> </div> </div> <div ui-card class="border-border/80 relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">30-Day Total Requests</h3> <div class="bg-primary/10 text-primary rounded-md p-1.5"> <lucide-icon [img]="History" class="size-4" /> </div> </div> <div ui-card-content> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ total30Day() }}</div> <p class="text-muted-foreground mt-1 text-xs">98.2% compliance audit pass</p> </div> </div> </div> <!-- Pending & Active Requests Table Card --> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="border-border/60 border-b pb-4"> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-foreground text-base font-semibold">Privilege Escalation Queue</h3> <p ui-card-description class="text-muted-foreground mt-0.5 text-xs"> Review, approve, or reject active and pending privilege requests. </p> </div> <div class="flex flex-col gap-2 sm:flex-row sm:items-center"> <div class="relative w-full sm:w-60"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" placeholder="Search requester, role, ticket..." class="h-8 pl-8 text-xs" /> </div> <div class="border-border bg-muted/40 flex items-center rounded-lg border p-0.5"> <button type="button" [class]="tabClass('all')" (click)="setSelectedTab('all')"> All ({{ requests().length }}) </button> <button type="button" [class]="tabClass('pending')" (click)="setSelectedTab('pending')"> Pending ({{ pendingCount() }}) </button> <button type="button" [class]="tabClass('active')" (click)="setSelectedTab('active')"> Active ({{ activeCount() }}) </button> <button type="button" [class]="tabClass('expired')" (click)="setSelectedTab('expired')"> Expired ({{ expiredTabCount() }}) </button> </div> </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="w-[220px] text-xs font-medium">Requester</th> <th ui-table-head class="w-[200px] text-xs font-medium">Target Role & System</th> <th ui-table-head class="w-[170px] text-xs font-medium">Duration & Timer</th> <th ui-table-head class="min-w-[260px] text-xs font-medium">Business Justification</th> <th ui-table-head class="w-[180px] text-xs font-medium">Status</th> <th ui-table-head class="w-[160px] text-right text-xs font-medium">Actions</th> </tr> </thead> <tbody ui-table-body> @if (filteredRequests().length === 0) { <tr ui-table-row> <td ui-table-cell [attr.colspan]="6" class="text-muted-foreground h-32 text-center text-xs"> No access requests match your filter. </td> </tr> } @else { @for (req of filteredRequests(); track req.id) { <tr ui-table-row class="hover:bg-muted/30 transition-colors"> <td ui-table-cell> <div class="flex items-center gap-3"> <ui-avatar class="border-border/60 size-8 border"> <ui-avatar-fallback class="bg-muted text-foreground text-xs font-semibold"> {{ req.requester.avatar }} </ui-avatar-fallback> </ui-avatar> <div class="min-w-0"> <div class="text-foreground truncate text-xs font-medium">{{ req.requester.name }}</div> <div class="text-muted-foreground truncate text-xs">{{ req.requester.email }}</div> </div> </div> </td> <td ui-table-cell> <div class="flex flex-col gap-1"> <span ui-badge [variant]="roleBadgeVariant(req.targetRole)" class="w-fit text-xs font-medium"> {{ req.targetRole }} </span> <span class="text-muted-foreground truncate text-xs">{{ req.system }}</span> </div> </td> <td ui-table-cell> <div class="flex flex-col gap-1"> <span class="text-foreground text-xs font-medium">{{ req.duration }}</span> @if (req.status === 'active') { <div class="text-success flex items-center gap-1.5 font-mono text-xs tabular-nums"> <lucide-icon [img]="Clock" class="size-3 shrink-0" /> <span>{{ formatSeconds(req.secondsLeft) }}</span> </div> } @else if (req.status === 'pending') { <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs tabular-nums"> <lucide-icon [img]="Clock" class="size-3 shrink-0" /> <span>Awaiting start</span> </div> } @else { <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs tabular-nums"> <lucide-icon [img]="Clock" class="size-3 shrink-0" /> <span>Session ended</span> </div> } </div> </td> <td ui-table-cell> <div class="flex flex-col gap-1"> <div class="flex items-center gap-1.5"> <span class="border-border bg-muted/60 text-foreground inline-flex items-center rounded border px-1.5 py-0.5 font-mono text-xs font-medium" > #{{ req.ticketId }} </span> <span class="text-muted-foreground text-xs"> Approver: {{ shortApprover(req.approver) }} </span> </div> <p class="text-muted-foreground line-clamp-2 text-xs" [attr.title]="req.justification"> {{ req.justification }} </p> </div> </td> <td ui-table-cell> <div class="flex items-center"> @if (req.status === 'active') { <span ui-badge variant="success" class="gap-1.5 text-xs font-medium"> <span class="relative flex size-2"> <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" ></span> <span class="bg-success relative inline-flex size-2 rounded-full"></span> </span> <span>Active - {{ req.timeLeftLabel }}</span> </span> } @else if (req.status === 'pending') { <span ui-badge variant="warning" class="gap-1.5 text-xs font-medium"> <span class="bg-warning size-2 animate-pulse rounded-full"></span> <span>Pending Approval</span> </span> } @else if (req.status === 'expired') { <span ui-badge variant="secondary" class="text-muted-foreground gap-1.5 text-xs font-normal" > <span class="bg-muted-foreground/50 size-2 rounded-full"></span> <span>Expired</span> </span> } @else { <span ui-badge variant="destructive" class="gap-1.5 text-xs font-normal"> <span class="bg-destructive size-2 rounded-full"></span> <span>Rejected</span> </span> } </div> </td> <td ui-table-cell class="text-right"> <div class="flex items-center justify-end gap-1.5"> @if (req.status === 'pending') { <button ui-button size="sm" class="h-7 gap-1 px-2.5 text-xs shadow-xs" (click)="handleApprove(req.id)" > <lucide-icon [img]="Check" class="size-3.5" /> Approve </button> <button ui-button size="sm" variant="outline" class="text-destructive hover:bg-destructive/10 hover:text-destructive h-7 gap-1 px-2 text-xs" (click)="handleReject(req.id)" > <lucide-icon [img]="X" class="size-3.5" /> Reject </button> } @if (req.status === 'active') { <button ui-button size="sm" variant="outline" class="text-destructive border-destructive/30 hover:bg-destructive/10 hover:text-destructive h-7 gap-1 px-2 text-xs" (click)="handleRevoke(req.id)" > <lucide-icon [img]="Lock" class="size-3.5" /> Revoke </button> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-7"> <lucide-icon [img]="MoreHorizontal" class="size-3.5" /> <span class="sr-only">More actions</span> </button> <ui-dropdown-menu-content align="end" class="w-48"> <div ui-dropdown-menu-label class="text-xs">Elevated Session</div> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item (select)="handleExtend(req.id)"> <lucide-icon [img]="Clock" class="mr-2 size-3.5" /> <span>Extend 30 Minutes</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="handleCopyToken(req.id)"> <lucide-icon [img]="Copy" class="mr-2 size-3.5" /> <span>{{ copiedId() === req.id ? 'Copied Token!' : 'Copy Token' }}</span> </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="text-destructive focus:text-destructive" (select)="handleRevoke(req.id)" > <lucide-icon [img]="ShieldAlert" class="mr-2 size-3.5" /> <span>Revoke Immediately</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> } @if (req.status !== 'pending' && req.status !== 'active') { <button ui-button size="sm" variant="ghost" class="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" (click)="handleReRequest(req.id)" > Re-request </button> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-7"> <lucide-icon [img]="MoreHorizontal" class="size-3.5" /> <span class="sr-only">More actions</span> </button> <ui-dropdown-menu-content align="end" class="w-44"> <ui-dropdown-menu-item (select)="handleReRequest(req.id)"> <lucide-icon [img]="History" class="mr-2 size-3.5" /> <span>Request Again</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="FileText" class="mr-2 size-3.5" /> <span>View Audit Log</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> } </div> </td> </tr> } } </tbody> </table> </div> </div> </div> </div> `, }) export class UiAccessRequestWorkflowComponent { protected readonly Check = Check protected readonly Clock = Clock protected readonly Copy = Copy protected readonly FileText = FileText protected readonly History = History protected readonly Hourglass = Hourglass protected readonly KeyRound = KeyRound protected readonly Lock = Lock protected readonly MoreHorizontal = MoreHorizontal protected readonly Plus = Plus protected readonly Search = Search protected readonly Send = Send protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly UserCheck = UserCheck protected readonly X = X @Input() title = 'Access Requests & Privilege Elevation' @Input() subtitle = 'Request temporary Just-In-Time production access with audit approval.' @Input('class') className?: string readonly requests = signal<AccessRequest[]>(initialRequests.map((r) => ({ ...r, requester: { ...r.requester } }))) readonly selectedTab = signal<AccessRequestTab>('all') readonly searchQuery = signal('') readonly isDialogOpen = signal(false) readonly formRole = signal('AWS Production Admin') readonly formDuration = signal('4 Hours') readonly formApprover = signal('Security Lead: Marcus Vance') readonly formJustification = signal('Investigating latency spike on payment gateway for incident #SEC-849') readonly copiedId = signal<string | null>(null) private copyTimer: ReturnType<typeof setTimeout> | undefined constructor() { const destroyRef = inject(DestroyRef) const timer = setInterval(() => this.tick(), 1000) destroyRef.onDestroy(() => { clearInterval(timer) clearTimeout(this.copyTimer) }) } readonly activeCount = computed(() => this.requests().filter((r) => r.status === 'active').length) readonly pendingCount = computed(() => this.requests().filter((r) => r.status === 'pending').length) readonly expiredTabCount = computed( () => this.requests().filter((r) => r.status === 'expired' || r.status === 'rejected').length, ) readonly expiringCount = computed( () => this.requests().filter((r) => r.status === 'active' && r.secondsLeft <= 3600 * 4).length, ) readonly total30Day = computed(() => 42 + this.requests().filter((r) => r.id.startsWith('req-custom')).length) readonly filteredRequests = computed(() => { const tab = this.selectedTab() const q = this.searchQuery().trim().toLowerCase() return this.requests().filter((req) => { if (tab === 'pending' || tab === 'active') { if (req.status !== tab) return false } else if (tab === 'expired') { if (req.status !== 'expired' && req.status !== 'rejected') return false } if (q) { const hay = [req.requester.name, req.targetRole, req.ticketId, req.justification, req.system] .join(' ') .toLowerCase() if (!hay.includes(q)) return false } return true }) }) get rootClass(): string { return cn('flex flex-col gap-6', this.className) } tabClass(tab: AccessRequestTab): string { return cn( 'rounded-md px-2.5 py-1 text-xs font-medium transition-colors', this.selectedTab() === tab ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } setSelectedTab(tab: AccessRequestTab): void { this.selectedTab.set(tab) } formatSeconds(seconds: number): string { return formatSecondsValue(seconds) } roleBadgeVariant(role: string): BadgeVariant { if (role.includes('AWS') || role.includes('Admin')) return 'destructive' if (role.includes('PII') || role.includes('Decryption')) return 'warning' if (role.includes('DB') || role.includes('Read-Only')) return 'info' if (role.includes('Stripe')) return 'secondary' return 'default' } shortApprover(approver: string): string { return approver .replace('Security Lead: ', '') .replace('Infrastructure Team: ', '') .replace('Compliance Officer: ', '') .replace('On-Call Lead: ', '') } tick(): void { this.requests.update((prev) => prev.map((req) => { if (req.status === 'active' && req.secondsLeft > 0) { const nextSec = req.secondsLeft - 1 if (nextSec <= 0) { return { ...req, secondsLeft: 0, status: 'expired' as AccessRequestStatus, timeLeftLabel: 'Expired' } } return { ...req, secondsLeft: nextSec, timeLeftLabel: timeLeftLabelFor(nextSec) } } return req }), ) } handleCreateRequest(event: SubmitEvent): void { event.preventDefault() if (!this.formJustification().trim()) return const duration = this.formDuration() let seconds = 3600 * 4 if (duration.includes('1 Hour')) seconds = 3600 else if (duration.includes('8 Hours')) seconds = 3600 * 8 else if (duration.includes('24 Hours')) seconds = 3600 * 24 const role = this.formRole() let system = 'AWS Cloud IAM (us-east-1)' if (role.includes('DB')) system = 'PostgreSQL Aurora Primary' else if (role.includes('Stripe')) system = 'Stripe Merchant Gateway' else if (role.includes('PII')) system = 'Customer Data Vault (KMS)' const newReq: AccessRequest = { id: `req-custom-${Date.now()}`, requester: { name: 'Current Operator (You)', email: '[email protected]', team: 'Platform Engineering', avatar: 'OP', }, targetRole: role, system, duration, timeLeftLabel: 'Pending Approval', secondsLeft: seconds, justification: this.formJustification(), ticketId: `SEC-${Math.floor(100 + Math.random() * 900)}`, approver: this.formApprover(), status: 'pending', createdAt: 'Just now', expiresAt: 'Awaiting approval', } this.requests.update((prev) => [newReq, ...prev]) this.isDialogOpen.set(false) } handleApprove(id: string): void { this.requests.update((prev) => prev.map((req) => req.id === id ? { ...req, status: 'active' as const, timeLeftLabel: timeLeftLabelFor(req.secondsLeft) } : req, ), ) } handleReject(id: string): void { this.requests.update((prev) => prev.map((req) => (req.id === id ? { ...req, status: 'rejected' as const, timeLeftLabel: 'Rejected' } : req)), ) } handleRevoke(id: string): void { this.requests.update((prev) => prev.map((req) => req.id === id ? { ...req, status: 'expired' as const, secondsLeft: 0, timeLeftLabel: 'Revoked' } : req, ), ) } handleExtend(id: string): void { this.requests.update((prev) => prev.map((req) => { if (req.id === id && req.status === 'active') { const nextSec = req.secondsLeft + 1800 return { ...req, secondsLeft: nextSec, timeLeftLabel: timeLeftLabelFor(nextSec) } } return req }), ) } handleReRequest(id: string): void { this.requests.update((prev) => prev.map((req) => req.id === id ? { ...req, status: 'pending' as const, timeLeftLabel: 'Pending Approval', secondsLeft: 3600 * 4 } : req, ), ) } handleCopyToken(id: string): void { this.copiedId.set(id) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => { if (this.copiedId() === id) this.copiedId.set(null) }, 2000) } }src/app/components/blocks/access-request-workflow/index.ts0.2 kB
export { UiAccessRequestWorkflowComponent, type AccessRequest, type AccessRequester, type AccessRequestStatus, type AccessRequestTab, } from './access-request-workflow.component'
Raw manifest:https://uipkge.dev/r/angular/access-request-workflow.json