import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useState } from 'react'
import { fn } from 'storybook/test'
import { PolicyConfigurationPanel } from '../components/features/policy-configuration-panel'
import type { ApprovalLevel, PermissionCategory } from '../types/permissions'
const meta = {
title: 'Features/PolicyConfigurationPanel',
component: PolicyConfigurationPanel,
parameters: {
layout: 'padded',
docs: {
description: {
component:
'Grouped list of AI guardrails permission categories with per-policy approval dropdowns. ' +
'Categories are pure data — the panel owns its presentation state internally: which categories ' +
'are expanded and the last bulk level picked per category (bulk selections reset when `editMode` ' +
'changes). In edit mode each category header offers a bulk "global permission" dropdown that ' +
'fires `onCategoryPermissionChange`; each policy row fires `onPolicyPermissionChange`. ' +
'Used by the OpenFrame AI Settings → Guardrails tab.',
},
},
},
tags: ['autodocs'],
argTypes: {
editMode: { control: 'boolean' },
},
decorators: [
(Story) => (
),
],
} satisfies Meta
export default meta
type Story = StoryObj
const deviceCategories: PermissionCategory[] = [
{
id: 'device-actions',
name: 'Device Actions',
policies: [
{
id: 'OPENFRAME_RMM:DEVICE_REBOOT',
naturalKey: 'OPENFRAME_RMM:DEVICE_REBOOT',
name: 'Reboot Device',
commandPattern: 'device.reboot --id {deviceId}',
toolName: 'OPENFRAME_RMM',
approvalLevel: 'ASK_USER',
},
{
id: 'FLEET_MDM:DEVICE_LOCK',
naturalKey: 'FLEET_MDM:DEVICE_LOCK',
name: 'Lock Device',
commandPattern: 'fleet mdm lock --host {hostId}',
toolName: 'FLEET_MDM',
approvalLevel: 'ASK_TECHNICIAN',
},
{
id: 'FLEET_MDM:DEVICE_WIPE',
naturalKey: 'FLEET_MDM:DEVICE_WIPE',
name: 'Wipe Device',
commandPattern: 'fleet mdm wipe --host {hostId}',
toolName: 'FLEET_MDM',
approvalLevel: 'DENY',
},
],
},
{
id: 'remote-access',
name: 'Remote Access',
policies: [
{
id: 'MESHCENTRAL:REMOTE_SHELL',
naturalKey: 'MESHCENTRAL:REMOTE_SHELL',
name: 'Open Remote Shell',
commandPattern: 'mesh.shell --node {nodeId}',
toolName: 'MESHCENTRAL',
approvalLevel: 'ASK_TECHNICIAN',
},
{
id: 'MESHCENTRAL:FILE_TRANSFER',
naturalKey: 'MESHCENTRAL:FILE_TRANSFER',
name: 'Transfer Files',
commandPattern: 'mesh.files --node {nodeId} --upload|--download',
toolName: 'MESHCENTRAL',
approvalLevel: 'DENY',
},
],
},
{
id: 'scripts-automation',
name: 'Scripts & Automation',
policies: [
{
id: 'OPENFRAME_RMM:RUN_POWERSHELL',
naturalKey: 'OPENFRAME_RMM:RUN_POWERSHELL',
name: 'Run PowerShell Script',
commandPattern: 'powershell -ExecutionPolicy Bypass -File {script}',
toolName: 'OPENFRAME_RMM',
approvalLevel: 'ASK_USER',
},
{
id: 'OPENFRAME_RMM:RUN_BASH',
naturalKey: 'OPENFRAME_RMM:RUN_BASH',
name: 'Run Bash Script',
commandPattern: 'bash -c {script}',
toolName: 'OPENFRAME_RMM',
approvalLevel: 'ASK_USER',
},
],
},
]
const dataCategories: PermissionCategory[] = [
{
id: 'data-read',
name: 'Read Access',
policies: [
{
id: 'OSQUERY:QUERY_INVENTORY',
naturalKey: 'OSQUERY:QUERY_INVENTORY',
name: 'Query Device Inventory',
commandPattern: 'SELECT * FROM system_info;',
toolName: 'OSQUERY',
approvalLevel: 'ALLOW',
},
{
id: 'SYSTEM:READ_LOGS',
naturalKey: 'SYSTEM:READ_LOGS',
name: 'Read System Logs',
commandPattern: 'logs.read --stream {stream} --since {ts}',
toolName: 'SYSTEM',
approvalLevel: 'ALLOW',
},
],
},
]
/** Applies a level to one policy; mirrors what the guardrails editor hook does. */
const applyPolicyLevel = (
categories: PermissionCategory[],
categoryId: string,
policyId: string,
level: ApprovalLevel,
): PermissionCategory[] =>
categories.map(category =>
category.id !== categoryId
? category
: {
...category,
policies: category.policies.map(policy =>
policy.id === policyId ? { ...policy, approvalLevel: level } : policy,
),
},
)
/** Bulk-applies a level to every policy in a category. */
const applyCategoryLevel = (
categories: PermissionCategory[],
categoryId: string,
level: ApprovalLevel,
): PermissionCategory[] =>
categories.map(category =>
category.id !== categoryId
? category
: { ...category, policies: category.policies.map(policy => ({ ...policy, approvalLevel: level })) },
)
/** Read-only view: approval levels render as text, no bulk dropdowns. */
export const ReadOnly: Story = {
args: {
categories: deviceCategories,
editMode: false,
onPolicyPermissionChange: fn(),
onCategoryPermissionChange: fn(),
},
}
/**
* Interactive edit mode: per-policy dropdowns plus a bulk "global permission"
* dropdown per category header. The wrapper applies changes back into the
* category data the same way the OpenFrame guardrails editor does.
*/
export const EditMode: Story = {
args: {
categories: deviceCategories,
editMode: true,
onPolicyPermissionChange: fn(),
onCategoryPermissionChange: fn(),
},
render: args => {
const [categories, setCategories] = useState(args.categories)
return (
{
args.onPolicyPermissionChange(categoryId, policyId, level)
setCategories(prev => applyPolicyLevel(prev, categoryId, policyId, level))
}}
onCategoryPermissionChange={(categoryId, level) => {
args.onCategoryPermissionChange(categoryId, level)
setCategories(prev => applyCategoryLevel(prev, categoryId, level))
}}
/>
)
},
}
/**
* How the guardrails tab composes the panel: one panel per policy group, each
* under a small secondary heading.
*/
export const GroupedPolicySections: Story = {
args: {
categories: deviceCategories,
editMode: true,
onPolicyPermissionChange: fn(),
onCategoryPermissionChange: fn(),
},
render: args => {
const [groups, setGroups] = useState>({
'Device Management': deviceCategories,
'Data Access': dataCategories,
})
const updateGroup = (groupName: string, next: (categories: PermissionCategory[]) => PermissionCategory[]) =>
setGroups(prev => ({ ...prev, [groupName]: next(prev[groupName]) }))
return (
{Object.entries(groups).map(([groupName, categories]) => (
{groupName}
{
args.onPolicyPermissionChange(categoryId, policyId, level)
updateGroup(groupName, prev => applyPolicyLevel(prev, categoryId, policyId, level))
}}
onCategoryPermissionChange={(categoryId, level) => {
args.onCategoryPermissionChange(categoryId, level)
updateGroup(groupName, prev => applyCategoryLevel(prev, categoryId, level))
}}
/>
))}
)
},
}