@flamingo-stack/openframe-frontend-core 0.0.442 → 0.0.443
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-J5NO7PAG.cjs → chunk-2CJ734N2.cjs} +3 -3
- package/dist/{chunk-J5NO7PAG.cjs.map → chunk-2CJ734N2.cjs.map} +1 -1
- package/dist/{chunk-UFBUQO7Z.cjs → chunk-323TCNF3.cjs} +18 -18
- package/dist/{chunk-UFBUQO7Z.cjs.map → chunk-323TCNF3.cjs.map} +1 -1
- package/dist/{chunk-63TSTDME.js → chunk-3ZE55IH6.js} +5 -5
- package/dist/{chunk-Y3NFB6HR.js → chunk-5U6OZ5FD.js} +4 -4
- package/dist/{chunk-IAZ5AN2E.js → chunk-6IPNXMB7.js} +6 -6
- package/dist/{chunk-XMCVUYKC.cjs → chunk-6NO4YYU7.cjs} +12 -12
- package/dist/{chunk-XMCVUYKC.cjs.map → chunk-6NO4YYU7.cjs.map} +1 -1
- package/dist/{chunk-QO6MCQYE.cjs → chunk-7CZ4MJSI.cjs} +13 -10
- package/dist/chunk-7CZ4MJSI.cjs.map +1 -0
- package/dist/{chunk-FPE3SQ5D.js → chunk-A7MG4SBL.js} +3 -3
- package/dist/{chunk-SM26C2L7.js → chunk-AZQOX2RS.js} +2 -2
- package/dist/{chunk-JYI4R25Q.js → chunk-DOVBNECK.js} +3 -3
- package/dist/{chunk-7KDR5P23.js → chunk-FQASXKBK.js} +2 -2
- package/dist/{chunk-LFWPWIAC.cjs → chunk-H4W3NCR2.cjs} +7 -7
- package/dist/{chunk-LFWPWIAC.cjs.map → chunk-H4W3NCR2.cjs.map} +1 -1
- package/dist/{chunk-DUQQEQ7G.cjs → chunk-IBIGXER5.cjs} +191 -153
- package/dist/chunk-IBIGXER5.cjs.map +1 -0
- package/dist/{chunk-IZ6A6K6N.cjs → chunk-KNNSS3CZ.cjs} +15 -15
- package/dist/{chunk-IZ6A6K6N.cjs.map → chunk-KNNSS3CZ.cjs.map} +1 -1
- package/dist/{chunk-AGV26PUR.js → chunk-LGMRNYTX.js} +3 -3
- package/dist/{chunk-YZJUHEPZ.js → chunk-LYYVTI34.js} +5 -5
- package/dist/{chunk-TAQMYVO6.cjs → chunk-M6XUXQ5Q.cjs} +40 -40
- package/dist/{chunk-TAQMYVO6.cjs.map → chunk-M6XUXQ5Q.cjs.map} +1 -1
- package/dist/{chunk-2BTDPDME.js → chunk-MG4AMIFX.js} +4 -1
- package/dist/chunk-MG4AMIFX.js.map +1 -0
- package/dist/{chunk-ZQYFDYWV.js → chunk-NQODKJ2F.js} +121 -83
- package/dist/{chunk-ZQYFDYWV.js.map → chunk-NQODKJ2F.js.map} +1 -1
- package/dist/{chunk-KUXGQ2FQ.cjs → chunk-OHKKI6EF.cjs} +32 -32
- package/dist/{chunk-KUXGQ2FQ.cjs.map → chunk-OHKKI6EF.cjs.map} +1 -1
- package/dist/{chunk-JOH5D76X.cjs → chunk-PRJFSF4W.cjs} +73 -73
- package/dist/{chunk-JOH5D76X.cjs.map → chunk-PRJFSF4W.cjs.map} +1 -1
- package/dist/{chunk-W3JZXLJ2.js → chunk-PVMPBWKX.js} +6 -6
- package/dist/{chunk-7KNOHGMI.js → chunk-RQVR7I73.js} +2 -2
- package/dist/{chunk-7QP65IE4.cjs → chunk-RYKANQE6.cjs} +5 -5
- package/dist/{chunk-7QP65IE4.cjs.map → chunk-RYKANQE6.cjs.map} +1 -1
- package/dist/{chunk-QAG357PE.js → chunk-UXJ47OJF.js} +2 -2
- package/dist/{chunk-JNC4LVD6.cjs → chunk-VACXEUGF.cjs} +29 -29
- package/dist/{chunk-JNC4LVD6.cjs.map → chunk-VACXEUGF.cjs.map} +1 -1
- package/dist/{chunk-LUKRPHEW.cjs → chunk-YPFQ2UIH.cjs} +73 -73
- package/dist/{chunk-LUKRPHEW.cjs.map → chunk-YPFQ2UIH.cjs.map} +1 -1
- package/dist/{chunk-IPEIOR77.cjs → chunk-ZVAAIP5Y.cjs} +40 -40
- package/dist/{chunk-IPEIOR77.cjs.map → chunk-ZVAAIP5Y.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/policy-configuration-panel.d.ts +11 -0
- package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +135 -135
- package/dist/components/index.js +11 -11
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +2 -2
- package/dist/types/permissions.d.ts +9 -6
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.d.ts +19 -3
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs +3 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/features/policy-configuration-panel.tsx +185 -122
- package/src/stories/PolicyConfigurationPanel.stories.tsx +254 -0
- package/src/types/permissions.ts +10 -7
- package/src/utils/__tests__/embed-authed-fetch.test.ts +39 -0
- package/src/utils/embed-authed-fetch.ts +27 -3
- package/dist/chunk-2BTDPDME.js.map +0 -1
- package/dist/chunk-DUQQEQ7G.cjs.map +0 -1
- package/dist/chunk-QO6MCQYE.cjs.map +0 -1
- /package/dist/{chunk-63TSTDME.js.map → chunk-3ZE55IH6.js.map} +0 -0
- /package/dist/{chunk-Y3NFB6HR.js.map → chunk-5U6OZ5FD.js.map} +0 -0
- /package/dist/{chunk-IAZ5AN2E.js.map → chunk-6IPNXMB7.js.map} +0 -0
- /package/dist/{chunk-FPE3SQ5D.js.map → chunk-A7MG4SBL.js.map} +0 -0
- /package/dist/{chunk-SM26C2L7.js.map → chunk-AZQOX2RS.js.map} +0 -0
- /package/dist/{chunk-JYI4R25Q.js.map → chunk-DOVBNECK.js.map} +0 -0
- /package/dist/{chunk-7KDR5P23.js.map → chunk-FQASXKBK.js.map} +0 -0
- /package/dist/{chunk-AGV26PUR.js.map → chunk-LGMRNYTX.js.map} +0 -0
- /package/dist/{chunk-YZJUHEPZ.js.map → chunk-LYYVTI34.js.map} +0 -0
- /package/dist/{chunk-W3JZXLJ2.js.map → chunk-PVMPBWKX.js.map} +0 -0
- /package/dist/{chunk-7KNOHGMI.js.map → chunk-RQVR7I73.js.map} +0 -0
- /package/dist/{chunk-QAG357PE.js.map → chunk-UXJ47OJF.js.map} +0 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { useRef, useEffect, useState } from 'react'
|
|
5
|
-
import { ChevronDown } from 'lucide-react'
|
|
5
|
+
import { ChevronDown, Shield } from 'lucide-react'
|
|
6
6
|
import { cn } from '../../utils/cn'
|
|
7
7
|
import { ToolIcon } from '../tool-icon'
|
|
8
8
|
import { Button } from '../ui/button'
|
|
@@ -34,62 +34,110 @@ const getApprovalLevelLabel = (level: ApprovalLevel | undefined, editMode: boole
|
|
|
34
34
|
return option?.label || level
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const ApprovalLevelDropdown: React.FC<{
|
|
38
|
+
value: ApprovalLevel | undefined;
|
|
39
|
+
onChange: (level: ApprovalLevel | undefined) => void;
|
|
40
|
+
/** Adds the "Clear Global Permission" item (category bulk dropdown). */
|
|
41
|
+
allowClear?: boolean;
|
|
42
|
+
triggerClassName?: string;
|
|
43
|
+
contentClassName?: string;
|
|
44
|
+
}> = ({ value, onChange, allowClear = false, triggerClassName, contentClassName }) => (
|
|
45
|
+
<DropdownMenu>
|
|
46
|
+
<DropdownMenuTrigger asChild>
|
|
47
|
+
<Button
|
|
48
|
+
variant="outline"
|
|
49
|
+
rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
|
|
50
|
+
className={cn(
|
|
51
|
+
"!text-h6 bg-ods-card border border-ods-border rounded-md flex gap-[var(--spacing-system-xsf)] items-center justify-between !px-[var(--spacing-system-xsf)] py-[var(--spacing-system-xsf)] hover:bg-ods-bg-hover transition-colors h-auto",
|
|
52
|
+
value ? "text-ods-text-primary" : "text-ods-text-secondary",
|
|
53
|
+
triggerClassName
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{getApprovalLevelLabel(value, true)}
|
|
57
|
+
</Button>
|
|
58
|
+
</DropdownMenuTrigger>
|
|
59
|
+
<DropdownMenuContent align="end" className={contentClassName}>
|
|
60
|
+
{allowClear && (
|
|
61
|
+
<DropdownMenuItem onClick={() => onChange(undefined)} className="text-h6">
|
|
62
|
+
Clear Global Permission
|
|
63
|
+
</DropdownMenuItem>
|
|
64
|
+
)}
|
|
65
|
+
{approvalLevelOptions.map(option => (
|
|
66
|
+
<DropdownMenuItem
|
|
67
|
+
key={option.value}
|
|
68
|
+
onClick={() => onChange(option.value)}
|
|
69
|
+
className="text-h6"
|
|
70
|
+
>
|
|
71
|
+
{option.label}
|
|
72
|
+
</DropdownMenuItem>
|
|
73
|
+
))}
|
|
74
|
+
</DropdownMenuContent>
|
|
75
|
+
</DropdownMenu>
|
|
76
|
+
)
|
|
77
|
+
|
|
37
78
|
const PolicyRow: React.FC<{
|
|
38
79
|
policy: PermissionPolicy;
|
|
39
80
|
categoryId: string;
|
|
40
81
|
editMode: boolean;
|
|
41
82
|
onPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
42
83
|
}> = ({ policy, categoryId, editMode, onPermissionChange }) => {
|
|
84
|
+
const handleChange = (level: ApprovalLevel | undefined) => {
|
|
85
|
+
if (level) onPermissionChange(categoryId, policy.id, level)
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
return (
|
|
44
|
-
<div className="bg-ods-bg border-b border-ods-border flex gap-
|
|
89
|
+
<div className="bg-ods-bg border-b last:border-b-0 border-ods-border flex gap-[var(--spacing-system-m)] items-start md:items-center px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)]">
|
|
45
90
|
{/* Tool Icon */}
|
|
46
|
-
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
|
|
91
|
+
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0">
|
|
47
92
|
<ToolIcon toolType={policy.toolName} size={16} />
|
|
48
93
|
</div>
|
|
49
94
|
|
|
50
|
-
{/* Policy Info */}
|
|
95
|
+
{/* Policy Info — on mobile the approval level stacks below the pattern */}
|
|
51
96
|
<div className="flex-1 flex flex-col min-w-0">
|
|
52
|
-
<p className="text-
|
|
97
|
+
<p className="text-h4 text-ods-text-primary truncate" title={policy.name}>
|
|
53
98
|
{policy.name}
|
|
54
99
|
</p>
|
|
55
|
-
<p
|
|
100
|
+
<p
|
|
101
|
+
className="text-h6 text-ods-text-secondary truncate md:whitespace-normal md:break-all"
|
|
102
|
+
title={policy.commandPattern}
|
|
103
|
+
>
|
|
56
104
|
{policy.commandPattern}
|
|
57
105
|
</p>
|
|
106
|
+
{editMode ? (
|
|
107
|
+
<div className="md:hidden mt-[var(--spacing-system-xsf)]">
|
|
108
|
+
<ApprovalLevelDropdown
|
|
109
|
+
value={policy.approvalLevel}
|
|
110
|
+
onChange={handleChange}
|
|
111
|
+
triggerClassName="w-full"
|
|
112
|
+
contentClassName="w-[180px]"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
) : policy.approvalLevel ? (
|
|
116
|
+
<span className="md:hidden text-h4 text-ods-text-primary">
|
|
117
|
+
{getApprovalLevelLabel(policy.approvalLevel)}
|
|
118
|
+
</span>
|
|
119
|
+
) : null}
|
|
58
120
|
</div>
|
|
59
121
|
|
|
60
|
-
{/* Approval Level
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{option.label}
|
|
80
|
-
</DropdownMenuItem>
|
|
81
|
-
))}
|
|
82
|
-
</DropdownMenuContent>
|
|
83
|
-
</DropdownMenu>
|
|
84
|
-
) : policy.approvalLevel ? (
|
|
85
|
-
<div className="px-3 py-2 w-[180px]">
|
|
86
|
-
<span className="text-h6 text-ods-text-primary">
|
|
87
|
-
{getApprovalLevelLabel(policy.approvalLevel, editMode)}
|
|
88
|
-
</span>
|
|
89
|
-
</div>
|
|
90
|
-
) : (
|
|
91
|
-
<div className="px-3 py-2 w-[180px]" />
|
|
92
|
-
)}
|
|
122
|
+
{/* Approval Level column (desktop only) */}
|
|
123
|
+
<div className="hidden md:block shrink-0">
|
|
124
|
+
{editMode ? (
|
|
125
|
+
<ApprovalLevelDropdown
|
|
126
|
+
value={policy.approvalLevel}
|
|
127
|
+
onChange={handleChange}
|
|
128
|
+
triggerClassName="w-[180px]"
|
|
129
|
+
contentClassName="w-[180px]"
|
|
130
|
+
/>
|
|
131
|
+
) : (
|
|
132
|
+
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] w-[180px]">
|
|
133
|
+
{policy.approvalLevel && (
|
|
134
|
+
<span className="text-h4 text-ods-text-primary">
|
|
135
|
+
{getApprovalLevelLabel(policy.approvalLevel)}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
93
141
|
</div>
|
|
94
142
|
)
|
|
95
143
|
}
|
|
@@ -97,7 +145,7 @@ const PolicyRow: React.FC<{
|
|
|
97
145
|
const useAnimatedHeight = (isExpanded: boolean) => {
|
|
98
146
|
const [height, setHeight] = useState<number | 'auto'>(0);
|
|
99
147
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
100
|
-
|
|
148
|
+
|
|
101
149
|
useEffect(() => {
|
|
102
150
|
if (contentRef.current) {
|
|
103
151
|
const contentHeight = contentRef.current.scrollHeight;
|
|
@@ -111,93 +159,73 @@ const useAnimatedHeight = (isExpanded: boolean) => {
|
|
|
111
159
|
const CategorySection: React.FC<{
|
|
112
160
|
category: PermissionCategory;
|
|
113
161
|
editMode: boolean;
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
isExpanded: boolean;
|
|
163
|
+
/** Last bulk level applied to this category (display-only, edit mode). */
|
|
164
|
+
bulkLevel: ApprovalLevel | undefined;
|
|
165
|
+
onToggle: (categoryId: string) => void;
|
|
166
|
+
onBulkLevelChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
|
|
116
167
|
onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
117
|
-
}> = ({
|
|
118
|
-
category,
|
|
119
|
-
editMode,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
}> = ({
|
|
169
|
+
category,
|
|
170
|
+
editMode,
|
|
171
|
+
isExpanded,
|
|
172
|
+
bulkLevel,
|
|
173
|
+
onToggle,
|
|
174
|
+
onBulkLevelChange,
|
|
175
|
+
onPolicyPermissionChange
|
|
123
176
|
}) => {
|
|
124
|
-
const { contentRef, height } = useAnimatedHeight(
|
|
125
|
-
|
|
177
|
+
const { contentRef, height } = useAnimatedHeight(isExpanded);
|
|
178
|
+
const handleBulkChange = (level: ApprovalLevel | undefined) => onBulkLevelChange(category.id, level)
|
|
179
|
+
|
|
126
180
|
return (
|
|
127
181
|
<>
|
|
128
182
|
{/* Category Header */}
|
|
129
|
-
<div
|
|
130
|
-
className="bg-ods-card border-t border-ods-border flex gap-
|
|
131
|
-
onClick={() =>
|
|
183
|
+
<div
|
|
184
|
+
className="bg-ods-card border-t first:border-t-0 border-ods-border flex gap-[var(--spacing-system-m)] items-center pl-[var(--spacing-system-m)] pr-[var(--spacing-system-xsf)] py-[var(--spacing-system-s)] cursor-pointer hover:bg-ods-bg-hover transition-colors"
|
|
185
|
+
onClick={() => onToggle(category.id)}
|
|
132
186
|
>
|
|
133
187
|
{/* Category Icon */}
|
|
134
|
-
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
|
|
188
|
+
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0">
|
|
135
189
|
<div className="text-ods-text-secondary">
|
|
136
|
-
{category.icon}
|
|
190
|
+
{category.icon ?? <Shield className="w-4 h-4" />}
|
|
137
191
|
</div>
|
|
138
192
|
</div>
|
|
139
193
|
|
|
140
|
-
{/* Category Info */}
|
|
141
|
-
<div className="flex-1 flex flex-col">
|
|
142
|
-
<p className="
|
|
194
|
+
{/* Category Info — on mobile the bulk dropdown stacks below the count */}
|
|
195
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
196
|
+
<p className="text-h4 text-ods-text-primary truncate" title={category.name}>
|
|
143
197
|
{category.name}
|
|
144
198
|
</p>
|
|
145
199
|
<p className="text-h6 text-ods-text-secondary">
|
|
146
|
-
{category.
|
|
200
|
+
{category.policies.length} Configurations
|
|
147
201
|
</p>
|
|
202
|
+
{editMode && (
|
|
203
|
+
<div className="md:hidden mt-[var(--spacing-system-xsf)]" onClick={(e) => e.stopPropagation()}>
|
|
204
|
+
<ApprovalLevelDropdown
|
|
205
|
+
value={bulkLevel}
|
|
206
|
+
onChange={handleBulkChange}
|
|
207
|
+
allowClear
|
|
208
|
+
triggerClassName="w-full"
|
|
209
|
+
contentClassName="w-[256px]"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
148
213
|
</div>
|
|
149
214
|
|
|
150
|
-
{/* Global Permission
|
|
215
|
+
{/* Global Permission column (desktop only) */}
|
|
151
216
|
{editMode ? (
|
|
152
|
-
<div onClick={(e) => e.stopPropagation()}>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<span className={cn(
|
|
161
|
-
"md:!text-sm font-medium",
|
|
162
|
-
category.globalPermission
|
|
163
|
-
? "text-ods-text-primary"
|
|
164
|
-
: "text-ods-text-secondary"
|
|
165
|
-
)}>
|
|
166
|
-
{getApprovalLevelLabel(category.globalPermission, editMode)}
|
|
167
|
-
</span>
|
|
168
|
-
</Button>
|
|
169
|
-
</DropdownMenuTrigger>
|
|
170
|
-
<DropdownMenuContent align="end" className="w-[256px]">
|
|
171
|
-
<DropdownMenuItem
|
|
172
|
-
onClick={() => onGlobalPermissionChange(category.id, undefined)}
|
|
173
|
-
className="md:!text-sm"
|
|
174
|
-
>
|
|
175
|
-
Clear Global Permission
|
|
176
|
-
</DropdownMenuItem>
|
|
177
|
-
{approvalLevelOptions.map(option => (
|
|
178
|
-
<DropdownMenuItem
|
|
179
|
-
key={option.value}
|
|
180
|
-
onClick={() => onGlobalPermissionChange(category.id, option.value)}
|
|
181
|
-
className="md:!text-sm"
|
|
182
|
-
>
|
|
183
|
-
{option.label}
|
|
184
|
-
</DropdownMenuItem>
|
|
185
|
-
))}
|
|
186
|
-
</DropdownMenuContent>
|
|
187
|
-
</DropdownMenu>
|
|
188
|
-
</div>
|
|
189
|
-
) : category.globalPermission ? (
|
|
190
|
-
<div
|
|
191
|
-
className="px-3 py-2 w-[256px]"
|
|
192
|
-
onClick={(e) => e.stopPropagation()}
|
|
193
|
-
>
|
|
194
|
-
<span className="md:!text-sm font-medium text-ods-text-primary">
|
|
195
|
-
{getApprovalLevelLabel(category.globalPermission, editMode)}
|
|
196
|
-
</span>
|
|
217
|
+
<div className="hidden md:block shrink-0" onClick={(e) => e.stopPropagation()}>
|
|
218
|
+
<ApprovalLevelDropdown
|
|
219
|
+
value={bulkLevel}
|
|
220
|
+
onChange={handleBulkChange}
|
|
221
|
+
allowClear
|
|
222
|
+
triggerClassName="w-[256px]"
|
|
223
|
+
contentClassName="w-[256px]"
|
|
224
|
+
/>
|
|
197
225
|
</div>
|
|
198
226
|
) : (
|
|
199
|
-
<div
|
|
200
|
-
className="px-
|
|
227
|
+
<div
|
|
228
|
+
className="hidden md:block px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] w-[256px] shrink-0"
|
|
201
229
|
onClick={(e) => e.stopPropagation()}
|
|
202
230
|
/>
|
|
203
231
|
)}
|
|
@@ -208,32 +236,32 @@ const CategorySection: React.FC<{
|
|
|
208
236
|
variant="transparent"
|
|
209
237
|
onClick={(e) => {
|
|
210
238
|
e.stopPropagation();
|
|
211
|
-
|
|
239
|
+
onToggle(category.id);
|
|
212
240
|
}}
|
|
213
|
-
leftIcon={<ChevronDown
|
|
241
|
+
leftIcon={<ChevronDown
|
|
214
242
|
className={cn(
|
|
215
243
|
"h-6 w-6 text-ods-text-primary transition-transform duration-300",
|
|
216
|
-
|
|
217
|
-
)}
|
|
244
|
+
isExpanded && "rotate-180"
|
|
245
|
+
)}
|
|
218
246
|
/>}
|
|
219
|
-
className="rounded-md h-auto w-auto"
|
|
220
|
-
aria-label={
|
|
247
|
+
className="rounded-md h-auto w-auto shrink-0"
|
|
248
|
+
aria-label={isExpanded ? "Collapse" : "Expand"}
|
|
221
249
|
/>
|
|
222
250
|
</div>
|
|
223
251
|
|
|
224
252
|
{/* Policies List */}
|
|
225
|
-
<div
|
|
253
|
+
<div
|
|
226
254
|
ref={contentRef}
|
|
227
|
-
style={{
|
|
255
|
+
style={{
|
|
228
256
|
height: height === 'auto' ? 'auto' : `${height}px`,
|
|
229
257
|
transition: 'height 300ms ease-in-out, opacity 300ms ease-in-out'
|
|
230
258
|
}}
|
|
231
259
|
className={cn(
|
|
232
260
|
"overflow-hidden",
|
|
233
|
-
|
|
261
|
+
isExpanded ? "opacity-100" : "opacity-0"
|
|
234
262
|
)}
|
|
235
263
|
>
|
|
236
|
-
<div className="px-
|
|
264
|
+
<div className="px-[var(--spacing-system-mf)] pb-[var(--spacing-system-mf)] pt-0">
|
|
237
265
|
<div className="border border-ods-border rounded-md overflow-hidden">
|
|
238
266
|
{category.policies.map((policy) => (
|
|
239
267
|
<PolicyRow
|
|
@@ -251,14 +279,47 @@ const CategorySection: React.FC<{
|
|
|
251
279
|
)
|
|
252
280
|
}
|
|
253
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Grouped list of permission categories with per-policy approval dropdowns.
|
|
284
|
+
* Presentation state — which categories are expanded and the last bulk level
|
|
285
|
+
* picked per category — lives inside the panel, so callers pass pure data and
|
|
286
|
+
* background data refreshes never collapse the user's view. Bulk selections
|
|
287
|
+
* reset when `editMode` changes.
|
|
288
|
+
*
|
|
289
|
+
* Responsive: below `md` the approval level / dropdowns stack inside the text
|
|
290
|
+
* column (per the ODS mobile design); at `md+` they render as fixed-width
|
|
291
|
+
* right-hand columns.
|
|
292
|
+
*/
|
|
254
293
|
export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> = ({
|
|
255
294
|
categories,
|
|
256
295
|
editMode,
|
|
257
|
-
onCategoryToggle,
|
|
258
|
-
onGlobalPermissionChange,
|
|
259
296
|
onPolicyPermissionChange,
|
|
297
|
+
onCategoryPermissionChange,
|
|
260
298
|
className
|
|
261
299
|
}) => {
|
|
300
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set())
|
|
301
|
+
const [bulkLevels, setBulkLevels] = useState<Record<string, ApprovalLevel | undefined>>({})
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
setBulkLevels({})
|
|
305
|
+
}, [editMode])
|
|
306
|
+
|
|
307
|
+
const handleToggle = (categoryId: string) => {
|
|
308
|
+
setExpandedIds(prev => {
|
|
309
|
+
const next = new Set(prev)
|
|
310
|
+
if (next.has(categoryId)) next.delete(categoryId)
|
|
311
|
+
else next.add(categoryId)
|
|
312
|
+
return next
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const handleBulkLevelChange = (categoryId: string, level: ApprovalLevel | undefined) => {
|
|
317
|
+
setBulkLevels(prev => ({ ...prev, [categoryId]: level }))
|
|
318
|
+
if (level) {
|
|
319
|
+
onCategoryPermissionChange(categoryId, level)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
262
323
|
return (
|
|
263
324
|
<div className={cn(
|
|
264
325
|
"bg-ods-card border border-ods-border rounded-md overflow-hidden",
|
|
@@ -269,8 +330,10 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
|
|
|
269
330
|
key={category.id}
|
|
270
331
|
category={category}
|
|
271
332
|
editMode={editMode}
|
|
272
|
-
|
|
273
|
-
|
|
333
|
+
isExpanded={expandedIds.has(category.id)}
|
|
334
|
+
bulkLevel={bulkLevels[category.id]}
|
|
335
|
+
onToggle={handleToggle}
|
|
336
|
+
onBulkLevelChange={handleBulkLevelChange}
|
|
274
337
|
onPolicyPermissionChange={onPolicyPermissionChange}
|
|
275
338
|
/>
|
|
276
339
|
))}
|
|
@@ -278,4 +341,4 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
|
|
|
278
341
|
)
|
|
279
342
|
}
|
|
280
343
|
|
|
281
|
-
PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
|
|
344
|
+
PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { fn } from 'storybook/test'
|
|
4
|
+
import { PolicyConfigurationPanel } from '../components/features/policy-configuration-panel'
|
|
5
|
+
import type { ApprovalLevel, PermissionCategory } from '../types/permissions'
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Features/PolicyConfigurationPanel',
|
|
9
|
+
component: PolicyConfigurationPanel,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'Grouped list of AI guardrails permission categories with per-policy approval dropdowns. ' +
|
|
16
|
+
'Categories are pure data — the panel owns its presentation state internally: which categories ' +
|
|
17
|
+
'are expanded and the last bulk level picked per category (bulk selections reset when `editMode` ' +
|
|
18
|
+
'changes). In edit mode each category header offers a bulk "global permission" dropdown that ' +
|
|
19
|
+
'fires `onCategoryPermissionChange`; each policy row fires `onPolicyPermissionChange`. ' +
|
|
20
|
+
'Used by the OpenFrame AI Settings → Guardrails tab.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
tags: ['autodocs'],
|
|
25
|
+
argTypes: {
|
|
26
|
+
editMode: { control: 'boolean' },
|
|
27
|
+
},
|
|
28
|
+
decorators: [
|
|
29
|
+
(Story) => (
|
|
30
|
+
<div style={{ maxWidth: '960px' }}>
|
|
31
|
+
<Story />
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
} satisfies Meta<typeof PolicyConfigurationPanel>
|
|
36
|
+
|
|
37
|
+
export default meta
|
|
38
|
+
type Story = StoryObj<typeof meta>
|
|
39
|
+
|
|
40
|
+
const deviceCategories: PermissionCategory[] = [
|
|
41
|
+
{
|
|
42
|
+
id: 'device-actions',
|
|
43
|
+
name: 'Device Actions',
|
|
44
|
+
policies: [
|
|
45
|
+
{
|
|
46
|
+
id: 'OPENFRAME_RMM:DEVICE_REBOOT',
|
|
47
|
+
naturalKey: 'OPENFRAME_RMM:DEVICE_REBOOT',
|
|
48
|
+
name: 'Reboot Device',
|
|
49
|
+
commandPattern: 'device.reboot --id {deviceId}',
|
|
50
|
+
toolName: 'OPENFRAME_RMM',
|
|
51
|
+
approvalLevel: 'ASK_USER',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'FLEET_MDM:DEVICE_LOCK',
|
|
55
|
+
naturalKey: 'FLEET_MDM:DEVICE_LOCK',
|
|
56
|
+
name: 'Lock Device',
|
|
57
|
+
commandPattern: 'fleet mdm lock --host {hostId}',
|
|
58
|
+
toolName: 'FLEET_MDM',
|
|
59
|
+
approvalLevel: 'ASK_TECHNICIAN',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'FLEET_MDM:DEVICE_WIPE',
|
|
63
|
+
naturalKey: 'FLEET_MDM:DEVICE_WIPE',
|
|
64
|
+
name: 'Wipe Device',
|
|
65
|
+
commandPattern: 'fleet mdm wipe --host {hostId}',
|
|
66
|
+
toolName: 'FLEET_MDM',
|
|
67
|
+
approvalLevel: 'DENY',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'remote-access',
|
|
73
|
+
name: 'Remote Access',
|
|
74
|
+
policies: [
|
|
75
|
+
{
|
|
76
|
+
id: 'MESHCENTRAL:REMOTE_SHELL',
|
|
77
|
+
naturalKey: 'MESHCENTRAL:REMOTE_SHELL',
|
|
78
|
+
name: 'Open Remote Shell',
|
|
79
|
+
commandPattern: 'mesh.shell --node {nodeId}',
|
|
80
|
+
toolName: 'MESHCENTRAL',
|
|
81
|
+
approvalLevel: 'ASK_TECHNICIAN',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'MESHCENTRAL:FILE_TRANSFER',
|
|
85
|
+
naturalKey: 'MESHCENTRAL:FILE_TRANSFER',
|
|
86
|
+
name: 'Transfer Files',
|
|
87
|
+
commandPattern: 'mesh.files --node {nodeId} --upload|--download',
|
|
88
|
+
toolName: 'MESHCENTRAL',
|
|
89
|
+
approvalLevel: 'DENY',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'scripts-automation',
|
|
95
|
+
name: 'Scripts & Automation',
|
|
96
|
+
policies: [
|
|
97
|
+
{
|
|
98
|
+
id: 'OPENFRAME_RMM:RUN_POWERSHELL',
|
|
99
|
+
naturalKey: 'OPENFRAME_RMM:RUN_POWERSHELL',
|
|
100
|
+
name: 'Run PowerShell Script',
|
|
101
|
+
commandPattern: 'powershell -ExecutionPolicy Bypass -File {script}',
|
|
102
|
+
toolName: 'OPENFRAME_RMM',
|
|
103
|
+
approvalLevel: 'ASK_USER',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'OPENFRAME_RMM:RUN_BASH',
|
|
107
|
+
naturalKey: 'OPENFRAME_RMM:RUN_BASH',
|
|
108
|
+
name: 'Run Bash Script',
|
|
109
|
+
commandPattern: 'bash -c {script}',
|
|
110
|
+
toolName: 'OPENFRAME_RMM',
|
|
111
|
+
approvalLevel: 'ASK_USER',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
const dataCategories: PermissionCategory[] = [
|
|
118
|
+
{
|
|
119
|
+
id: 'data-read',
|
|
120
|
+
name: 'Read Access',
|
|
121
|
+
policies: [
|
|
122
|
+
{
|
|
123
|
+
id: 'OSQUERY:QUERY_INVENTORY',
|
|
124
|
+
naturalKey: 'OSQUERY:QUERY_INVENTORY',
|
|
125
|
+
name: 'Query Device Inventory',
|
|
126
|
+
commandPattern: 'SELECT * FROM system_info;',
|
|
127
|
+
toolName: 'OSQUERY',
|
|
128
|
+
approvalLevel: 'ALLOW',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'SYSTEM:READ_LOGS',
|
|
132
|
+
naturalKey: 'SYSTEM:READ_LOGS',
|
|
133
|
+
name: 'Read System Logs',
|
|
134
|
+
commandPattern: 'logs.read --stream {stream} --since {ts}',
|
|
135
|
+
toolName: 'SYSTEM',
|
|
136
|
+
approvalLevel: 'ALLOW',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
/** Applies a level to one policy; mirrors what the guardrails editor hook does. */
|
|
143
|
+
const applyPolicyLevel = (
|
|
144
|
+
categories: PermissionCategory[],
|
|
145
|
+
categoryId: string,
|
|
146
|
+
policyId: string,
|
|
147
|
+
level: ApprovalLevel,
|
|
148
|
+
): PermissionCategory[] =>
|
|
149
|
+
categories.map(category =>
|
|
150
|
+
category.id !== categoryId
|
|
151
|
+
? category
|
|
152
|
+
: {
|
|
153
|
+
...category,
|
|
154
|
+
policies: category.policies.map(policy =>
|
|
155
|
+
policy.id === policyId ? { ...policy, approvalLevel: level } : policy,
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
/** Bulk-applies a level to every policy in a category. */
|
|
161
|
+
const applyCategoryLevel = (
|
|
162
|
+
categories: PermissionCategory[],
|
|
163
|
+
categoryId: string,
|
|
164
|
+
level: ApprovalLevel,
|
|
165
|
+
): PermissionCategory[] =>
|
|
166
|
+
categories.map(category =>
|
|
167
|
+
category.id !== categoryId
|
|
168
|
+
? category
|
|
169
|
+
: { ...category, policies: category.policies.map(policy => ({ ...policy, approvalLevel: level })) },
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
/** Read-only view: approval levels render as text, no bulk dropdowns. */
|
|
173
|
+
export const ReadOnly: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
categories: deviceCategories,
|
|
176
|
+
editMode: false,
|
|
177
|
+
onPolicyPermissionChange: fn(),
|
|
178
|
+
onCategoryPermissionChange: fn(),
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Interactive edit mode: per-policy dropdowns plus a bulk "global permission"
|
|
184
|
+
* dropdown per category header. The wrapper applies changes back into the
|
|
185
|
+
* category data the same way the OpenFrame guardrails editor does.
|
|
186
|
+
*/
|
|
187
|
+
export const EditMode: Story = {
|
|
188
|
+
args: {
|
|
189
|
+
categories: deviceCategories,
|
|
190
|
+
editMode: true,
|
|
191
|
+
onPolicyPermissionChange: fn(),
|
|
192
|
+
onCategoryPermissionChange: fn(),
|
|
193
|
+
},
|
|
194
|
+
render: args => {
|
|
195
|
+
const [categories, setCategories] = useState(args.categories)
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<PolicyConfigurationPanel
|
|
199
|
+
{...args}
|
|
200
|
+
categories={categories}
|
|
201
|
+
onPolicyPermissionChange={(categoryId, policyId, level) => {
|
|
202
|
+
args.onPolicyPermissionChange(categoryId, policyId, level)
|
|
203
|
+
setCategories(prev => applyPolicyLevel(prev, categoryId, policyId, level))
|
|
204
|
+
}}
|
|
205
|
+
onCategoryPermissionChange={(categoryId, level) => {
|
|
206
|
+
args.onCategoryPermissionChange(categoryId, level)
|
|
207
|
+
setCategories(prev => applyCategoryLevel(prev, categoryId, level))
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* How the guardrails tab composes the panel: one panel per policy group, each
|
|
216
|
+
* under a small secondary heading.
|
|
217
|
+
*/
|
|
218
|
+
export const GroupedPolicySections: Story = {
|
|
219
|
+
args: {
|
|
220
|
+
categories: deviceCategories,
|
|
221
|
+
editMode: true,
|
|
222
|
+
onPolicyPermissionChange: fn(),
|
|
223
|
+
onCategoryPermissionChange: fn(),
|
|
224
|
+
},
|
|
225
|
+
render: args => {
|
|
226
|
+
const [groups, setGroups] = useState<Record<string, PermissionCategory[]>>({
|
|
227
|
+
'Device Management': deviceCategories,
|
|
228
|
+
'Data Access': dataCategories,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const updateGroup = (groupName: string, next: (categories: PermissionCategory[]) => PermissionCategory[]) =>
|
|
232
|
+
setGroups(prev => ({ ...prev, [groupName]: next(prev[groupName]) }))
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="flex flex-col gap-[var(--spacing-system-l)]">
|
|
236
|
+
{Object.entries(groups).map(([groupName, categories]) => (
|
|
237
|
+
<div key={groupName} className="flex flex-col gap-[var(--spacing-system-xxs)]">
|
|
238
|
+
<p className="text-h5 text-ods-text-secondary truncate">{groupName}</p>
|
|
239
|
+
<PolicyConfigurationPanel
|
|
240
|
+
categories={categories}
|
|
241
|
+
editMode={args.editMode}
|
|
242
|
+
onPolicyPermissionChange={(categoryId, policyId, level) =>
|
|
243
|
+
updateGroup(groupName, categories => applyPolicyLevel(categories, categoryId, policyId, level))
|
|
244
|
+
}
|
|
245
|
+
onCategoryPermissionChange={(categoryId, level) =>
|
|
246
|
+
updateGroup(groupName, categories => applyCategoryLevel(categories, categoryId, level))
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
},
|
|
254
|
+
}
|