@flamingo-stack/openframe-frontend-core 0.0.442-snapshot.20260717134227 → 0.0.442
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-MG4AMIFX.js → chunk-2BTDPDME.js} +1 -4
- package/dist/chunk-2BTDPDME.js.map +1 -0
- package/dist/{chunk-3ZE55IH6.js → chunk-63TSTDME.js} +5 -5
- package/dist/{chunk-FQASXKBK.js → chunk-7KDR5P23.js} +2 -2
- package/dist/{chunk-RQVR7I73.js → chunk-7KNOHGMI.js} +2 -2
- package/dist/{chunk-RYKANQE6.cjs → chunk-7QP65IE4.cjs} +5 -5
- package/dist/{chunk-RYKANQE6.cjs.map → chunk-7QP65IE4.cjs.map} +1 -1
- package/dist/{chunk-LGMRNYTX.js → chunk-AGV26PUR.js} +3 -3
- package/dist/{chunk-IBIGXER5.cjs → chunk-DUQQEQ7G.cjs} +153 -191
- package/dist/chunk-DUQQEQ7G.cjs.map +1 -0
- package/dist/{chunk-A7MG4SBL.js → chunk-FPE3SQ5D.js} +3 -3
- package/dist/{chunk-6IPNXMB7.js → chunk-IAZ5AN2E.js} +6 -6
- package/dist/{chunk-ZVAAIP5Y.cjs → chunk-IPEIOR77.cjs} +40 -40
- package/dist/{chunk-ZVAAIP5Y.cjs.map → chunk-IPEIOR77.cjs.map} +1 -1
- package/dist/{chunk-KNNSS3CZ.cjs → chunk-IZ6A6K6N.cjs} +15 -15
- package/dist/{chunk-KNNSS3CZ.cjs.map → chunk-IZ6A6K6N.cjs.map} +1 -1
- package/dist/{chunk-2CJ734N2.cjs → chunk-J5NO7PAG.cjs} +3 -3
- package/dist/{chunk-2CJ734N2.cjs.map → chunk-J5NO7PAG.cjs.map} +1 -1
- package/dist/{chunk-VACXEUGF.cjs → chunk-JNC4LVD6.cjs} +29 -29
- package/dist/{chunk-VACXEUGF.cjs.map → chunk-JNC4LVD6.cjs.map} +1 -1
- package/dist/{chunk-PRJFSF4W.cjs → chunk-JOH5D76X.cjs} +73 -73
- package/dist/{chunk-PRJFSF4W.cjs.map → chunk-JOH5D76X.cjs.map} +1 -1
- package/dist/{chunk-DOVBNECK.js → chunk-JYI4R25Q.js} +3 -3
- package/dist/{chunk-OHKKI6EF.cjs → chunk-KUXGQ2FQ.cjs} +32 -32
- package/dist/{chunk-OHKKI6EF.cjs.map → chunk-KUXGQ2FQ.cjs.map} +1 -1
- package/dist/{chunk-H4W3NCR2.cjs → chunk-LFWPWIAC.cjs} +7 -7
- package/dist/{chunk-H4W3NCR2.cjs.map → chunk-LFWPWIAC.cjs.map} +1 -1
- package/dist/{chunk-YPFQ2UIH.cjs → chunk-LUKRPHEW.cjs} +73 -73
- package/dist/{chunk-YPFQ2UIH.cjs.map → chunk-LUKRPHEW.cjs.map} +1 -1
- package/dist/{chunk-UXJ47OJF.js → chunk-QAG357PE.js} +2 -2
- package/dist/{chunk-7CZ4MJSI.cjs → chunk-QO6MCQYE.cjs} +10 -13
- package/dist/chunk-QO6MCQYE.cjs.map +1 -0
- package/dist/{chunk-AZQOX2RS.js → chunk-SM26C2L7.js} +2 -2
- package/dist/{chunk-M6XUXQ5Q.cjs → chunk-TAQMYVO6.cjs} +40 -40
- package/dist/{chunk-M6XUXQ5Q.cjs.map → chunk-TAQMYVO6.cjs.map} +1 -1
- package/dist/{chunk-323TCNF3.cjs → chunk-UFBUQO7Z.cjs} +18 -18
- package/dist/{chunk-323TCNF3.cjs.map → chunk-UFBUQO7Z.cjs.map} +1 -1
- package/dist/{chunk-PVMPBWKX.js → chunk-W3JZXLJ2.js} +6 -6
- package/dist/{chunk-6NO4YYU7.cjs → chunk-XMCVUYKC.cjs} +12 -12
- package/dist/{chunk-6NO4YYU7.cjs.map → chunk-XMCVUYKC.cjs.map} +1 -1
- package/dist/{chunk-5U6OZ5FD.js → chunk-Y3NFB6HR.js} +4 -4
- package/dist/{chunk-LYYVTI34.js → chunk-YZJUHEPZ.js} +5 -5
- package/dist/{chunk-NQODKJ2F.js → chunk-ZQYFDYWV.js} +83 -121
- package/dist/{chunk-NQODKJ2F.js.map → chunk-ZQYFDYWV.js.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 +0 -11
- 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 +6 -9
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.d.ts +3 -19
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -3
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +0 -3
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/features/policy-configuration-panel.tsx +122 -185
- package/src/types/permissions.ts +7 -10
- package/src/utils/__tests__/embed-authed-fetch.test.ts +0 -39
- package/src/utils/embed-authed-fetch.ts +3 -27
- package/dist/chunk-7CZ4MJSI.cjs.map +0 -1
- package/dist/chunk-IBIGXER5.cjs.map +0 -1
- package/dist/chunk-MG4AMIFX.js.map +0 -1
- package/src/stories/PolicyConfigurationPanel.stories.tsx +0 -254
- /package/dist/{chunk-3ZE55IH6.js.map → chunk-63TSTDME.js.map} +0 -0
- /package/dist/{chunk-FQASXKBK.js.map → chunk-7KDR5P23.js.map} +0 -0
- /package/dist/{chunk-RQVR7I73.js.map → chunk-7KNOHGMI.js.map} +0 -0
- /package/dist/{chunk-LGMRNYTX.js.map → chunk-AGV26PUR.js.map} +0 -0
- /package/dist/{chunk-A7MG4SBL.js.map → chunk-FPE3SQ5D.js.map} +0 -0
- /package/dist/{chunk-6IPNXMB7.js.map → chunk-IAZ5AN2E.js.map} +0 -0
- /package/dist/{chunk-DOVBNECK.js.map → chunk-JYI4R25Q.js.map} +0 -0
- /package/dist/{chunk-UXJ47OJF.js.map → chunk-QAG357PE.js.map} +0 -0
- /package/dist/{chunk-AZQOX2RS.js.map → chunk-SM26C2L7.js.map} +0 -0
- /package/dist/{chunk-PVMPBWKX.js.map → chunk-W3JZXLJ2.js.map} +0 -0
- /package/dist/{chunk-5U6OZ5FD.js.map → chunk-Y3NFB6HR.js.map} +0 -0
- /package/dist/{chunk-LYYVTI34.js.map → chunk-YZJUHEPZ.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
|
|
5
|
+
import { ChevronDown } 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,110 +34,62 @@ 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
|
-
|
|
78
37
|
const PolicyRow: React.FC<{
|
|
79
38
|
policy: PermissionPolicy;
|
|
80
39
|
categoryId: string;
|
|
81
40
|
editMode: boolean;
|
|
82
41
|
onPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
83
42
|
}> = ({ policy, categoryId, editMode, onPermissionChange }) => {
|
|
84
|
-
const handleChange = (level: ApprovalLevel | undefined) => {
|
|
85
|
-
if (level) onPermissionChange(categoryId, policy.id, level)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
43
|
return (
|
|
89
|
-
<div className="bg-ods-bg border-b
|
|
44
|
+
<div className="bg-ods-bg border-b border-ods-border flex gap-4 items-center px-4 py-3">
|
|
90
45
|
{/* Tool Icon */}
|
|
91
|
-
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8
|
|
46
|
+
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
|
|
92
47
|
<ToolIcon toolType={policy.toolName} size={16} />
|
|
93
48
|
</div>
|
|
94
49
|
|
|
95
|
-
{/* Policy Info
|
|
50
|
+
{/* Policy Info */}
|
|
96
51
|
<div className="flex-1 flex flex-col min-w-0">
|
|
97
|
-
<p className="text-
|
|
52
|
+
<p className="text-h6 text-ods-text-primary truncate" title={policy.name}>
|
|
98
53
|
{policy.name}
|
|
99
54
|
</p>
|
|
100
|
-
<p
|
|
101
|
-
className="text-h6 text-ods-text-secondary truncate md:whitespace-normal md:break-all"
|
|
102
|
-
title={policy.commandPattern}
|
|
103
|
-
>
|
|
55
|
+
<p className="text-code text-ods-text-secondary break-all">
|
|
104
56
|
{policy.commandPattern}
|
|
105
57
|
</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}
|
|
120
58
|
</div>
|
|
121
59
|
|
|
122
|
-
{/* Approval Level
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
60
|
+
{/* Approval Level Dropdown */}
|
|
61
|
+
{editMode ? (
|
|
62
|
+
<DropdownMenu>
|
|
63
|
+
<DropdownMenuTrigger asChild>
|
|
64
|
+
<Button
|
|
65
|
+
variant="outline"
|
|
66
|
+
rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
|
|
67
|
+
className="md:!text-sm bg-ods-card border border-ods-border rounded-md flex gap-2 items-center justify-between !px-2 py-2 w-[180px] hover:bg-ods-bg-hover transition-colors text-ods-text-primary h-auto"
|
|
68
|
+
>
|
|
69
|
+
{getApprovalLevelLabel(policy.approvalLevel, editMode)}
|
|
70
|
+
</Button>
|
|
71
|
+
</DropdownMenuTrigger>
|
|
72
|
+
<DropdownMenuContent align="end" className="w-[180px]">
|
|
73
|
+
{approvalLevelOptions.map(option => (
|
|
74
|
+
<DropdownMenuItem
|
|
75
|
+
key={option.value}
|
|
76
|
+
onClick={() => onPermissionChange(categoryId, policy.id, option.value)}
|
|
77
|
+
className="text-h6"
|
|
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
|
+
)}
|
|
141
93
|
</div>
|
|
142
94
|
)
|
|
143
95
|
}
|
|
@@ -145,7 +97,7 @@ const PolicyRow: React.FC<{
|
|
|
145
97
|
const useAnimatedHeight = (isExpanded: boolean) => {
|
|
146
98
|
const [height, setHeight] = useState<number | 'auto'>(0);
|
|
147
99
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
148
|
-
|
|
100
|
+
|
|
149
101
|
useEffect(() => {
|
|
150
102
|
if (contentRef.current) {
|
|
151
103
|
const contentHeight = contentRef.current.scrollHeight;
|
|
@@ -159,73 +111,93 @@ const useAnimatedHeight = (isExpanded: boolean) => {
|
|
|
159
111
|
const CategorySection: React.FC<{
|
|
160
112
|
category: PermissionCategory;
|
|
161
113
|
editMode: boolean;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
bulkLevel: ApprovalLevel | undefined;
|
|
165
|
-
onToggle: (categoryId: string) => void;
|
|
166
|
-
onBulkLevelChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
|
|
114
|
+
onCategoryToggle: (categoryId: string) => void;
|
|
115
|
+
onGlobalPermissionChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
|
|
167
116
|
onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
168
|
-
}> = ({
|
|
169
|
-
category,
|
|
170
|
-
editMode,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
onBulkLevelChange,
|
|
175
|
-
onPolicyPermissionChange
|
|
117
|
+
}> = ({
|
|
118
|
+
category,
|
|
119
|
+
editMode,
|
|
120
|
+
onCategoryToggle,
|
|
121
|
+
onGlobalPermissionChange,
|
|
122
|
+
onPolicyPermissionChange
|
|
176
123
|
}) => {
|
|
177
|
-
const { contentRef, height } = useAnimatedHeight(isExpanded);
|
|
178
|
-
|
|
179
|
-
|
|
124
|
+
const { contentRef, height } = useAnimatedHeight(category.isExpanded);
|
|
125
|
+
|
|
180
126
|
return (
|
|
181
127
|
<>
|
|
182
128
|
{/* Category Header */}
|
|
183
|
-
<div
|
|
184
|
-
className="bg-ods-card border-t
|
|
185
|
-
onClick={() =>
|
|
129
|
+
<div
|
|
130
|
+
className="bg-ods-card border-t border-ods-border flex gap-4 items-center pl-4 pr-2 py-3 cursor-pointer hover:bg-ods-bg-hover transition-colors"
|
|
131
|
+
onClick={() => onCategoryToggle(category.id)}
|
|
186
132
|
>
|
|
187
133
|
{/* Category Icon */}
|
|
188
|
-
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8
|
|
134
|
+
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
|
|
189
135
|
<div className="text-ods-text-secondary">
|
|
190
|
-
{category.icon
|
|
136
|
+
{category.icon}
|
|
191
137
|
</div>
|
|
192
138
|
</div>
|
|
193
139
|
|
|
194
|
-
{/* Category Info
|
|
195
|
-
<div className="flex-1 flex flex-col
|
|
196
|
-
<p className="text-
|
|
140
|
+
{/* Category Info */}
|
|
141
|
+
<div className="flex-1 flex flex-col">
|
|
142
|
+
<p className="md:!text-sm font-medium text-ods-text-primary">
|
|
197
143
|
{category.name}
|
|
198
144
|
</p>
|
|
199
145
|
<p className="text-h6 text-ods-text-secondary">
|
|
200
|
-
{category.
|
|
146
|
+
{category.configurationsCount} Configurations
|
|
201
147
|
</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
|
-
)}
|
|
213
148
|
</div>
|
|
214
149
|
|
|
215
|
-
{/* Global Permission
|
|
150
|
+
{/* Global Permission Dropdown */}
|
|
216
151
|
{editMode ? (
|
|
217
|
-
<div
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
152
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
153
|
+
<DropdownMenu>
|
|
154
|
+
<DropdownMenuTrigger asChild>
|
|
155
|
+
<Button
|
|
156
|
+
variant="outline"
|
|
157
|
+
rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
|
|
158
|
+
className="md:!text-sm bg-ods-card border border-ods-border rounded-md flex gap-2 items-center justify-between !px-2 py-2 w-[256px] hover:bg-ods-bg-hover transition-colors h-auto"
|
|
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>
|
|
225
197
|
</div>
|
|
226
198
|
) : (
|
|
227
|
-
<div
|
|
228
|
-
className="
|
|
199
|
+
<div
|
|
200
|
+
className="px-3 py-2 w-[256px]"
|
|
229
201
|
onClick={(e) => e.stopPropagation()}
|
|
230
202
|
/>
|
|
231
203
|
)}
|
|
@@ -236,32 +208,32 @@ const CategorySection: React.FC<{
|
|
|
236
208
|
variant="transparent"
|
|
237
209
|
onClick={(e) => {
|
|
238
210
|
e.stopPropagation();
|
|
239
|
-
|
|
211
|
+
onCategoryToggle(category.id);
|
|
240
212
|
}}
|
|
241
|
-
leftIcon={<ChevronDown
|
|
213
|
+
leftIcon={<ChevronDown
|
|
242
214
|
className={cn(
|
|
243
215
|
"h-6 w-6 text-ods-text-primary transition-transform duration-300",
|
|
244
|
-
isExpanded && "rotate-180"
|
|
245
|
-
)}
|
|
216
|
+
category.isExpanded && "rotate-180"
|
|
217
|
+
)}
|
|
246
218
|
/>}
|
|
247
|
-
className="rounded-md h-auto w-auto
|
|
248
|
-
aria-label={isExpanded ? "Collapse" : "Expand"}
|
|
219
|
+
className="rounded-md h-auto w-auto"
|
|
220
|
+
aria-label={category.isExpanded ? "Collapse" : "Expand"}
|
|
249
221
|
/>
|
|
250
222
|
</div>
|
|
251
223
|
|
|
252
224
|
{/* Policies List */}
|
|
253
|
-
<div
|
|
225
|
+
<div
|
|
254
226
|
ref={contentRef}
|
|
255
|
-
style={{
|
|
227
|
+
style={{
|
|
256
228
|
height: height === 'auto' ? 'auto' : `${height}px`,
|
|
257
229
|
transition: 'height 300ms ease-in-out, opacity 300ms ease-in-out'
|
|
258
230
|
}}
|
|
259
231
|
className={cn(
|
|
260
232
|
"overflow-hidden",
|
|
261
|
-
isExpanded ? "opacity-100" : "opacity-0"
|
|
233
|
+
category.isExpanded ? "opacity-100" : "opacity-0"
|
|
262
234
|
)}
|
|
263
235
|
>
|
|
264
|
-
<div className="px-
|
|
236
|
+
<div className="px-4 pb-4 pt-0">
|
|
265
237
|
<div className="border border-ods-border rounded-md overflow-hidden">
|
|
266
238
|
{category.policies.map((policy) => (
|
|
267
239
|
<PolicyRow
|
|
@@ -279,47 +251,14 @@ const CategorySection: React.FC<{
|
|
|
279
251
|
)
|
|
280
252
|
}
|
|
281
253
|
|
|
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
|
-
*/
|
|
293
254
|
export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> = ({
|
|
294
255
|
categories,
|
|
295
256
|
editMode,
|
|
257
|
+
onCategoryToggle,
|
|
258
|
+
onGlobalPermissionChange,
|
|
296
259
|
onPolicyPermissionChange,
|
|
297
|
-
onCategoryPermissionChange,
|
|
298
260
|
className
|
|
299
261
|
}) => {
|
|
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
|
-
|
|
323
262
|
return (
|
|
324
263
|
<div className={cn(
|
|
325
264
|
"bg-ods-card border border-ods-border rounded-md overflow-hidden",
|
|
@@ -330,10 +269,8 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
|
|
|
330
269
|
key={category.id}
|
|
331
270
|
category={category}
|
|
332
271
|
editMode={editMode}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
onToggle={handleToggle}
|
|
336
|
-
onBulkLevelChange={handleBulkLevelChange}
|
|
272
|
+
onCategoryToggle={onCategoryToggle}
|
|
273
|
+
onGlobalPermissionChange={onGlobalPermissionChange}
|
|
337
274
|
onPolicyPermissionChange={onPolicyPermissionChange}
|
|
338
275
|
/>
|
|
339
276
|
))}
|
|
@@ -341,4 +278,4 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
|
|
|
341
278
|
)
|
|
342
279
|
}
|
|
343
280
|
|
|
344
|
-
PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
|
|
281
|
+
PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
|
package/src/types/permissions.ts
CHANGED
|
@@ -11,24 +11,21 @@ export interface PermissionPolicy {
|
|
|
11
11
|
approvalLevel: ApprovalLevel;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Pure data describing one permission category. Presentation state (expansion,
|
|
16
|
-
* bulk-selection display) is owned by PolicyConfigurationPanel itself.
|
|
17
|
-
*/
|
|
18
14
|
export interface PermissionCategory {
|
|
19
15
|
id: string;
|
|
20
16
|
name: string;
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
icon: React.ReactNode;
|
|
18
|
+
configurationsCount: number;
|
|
19
|
+
globalPermission?: ApprovalLevel;
|
|
20
|
+
isExpanded: boolean;
|
|
23
21
|
policies: PermissionPolicy[];
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
export interface PolicyConfigurationPanelProps {
|
|
27
25
|
categories: PermissionCategory[];
|
|
28
26
|
editMode: boolean;
|
|
29
|
-
|
|
27
|
+
onCategoryToggle: (categoryId: string) => void;
|
|
28
|
+
onGlobalPermissionChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
|
|
30
29
|
onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
31
|
-
/** Bulk-apply an approval level to every policy in a category. */
|
|
32
|
-
onCategoryPermissionChange: (categoryId: string, level: ApprovalLevel) => void;
|
|
33
30
|
className?: string;
|
|
34
|
-
}
|
|
31
|
+
}
|
|
@@ -166,42 +166,3 @@ describe('embedAuthedFetch 401 self-heal (adapter.refresh)', () => {
|
|
|
166
166
|
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
|
-
|
|
170
|
-
describe('embedAuthedFetch adapter.allowedOrigins (native-shell cross-origin sanction)', () => {
|
|
171
|
-
const originalFetch = globalThis.fetch
|
|
172
|
-
|
|
173
|
-
beforeEach(() => {
|
|
174
|
-
globalThis.fetch = vi.fn(async () => new Response('ok')) as typeof fetch
|
|
175
|
-
// Production mode so these tests exercise the real guard, not the
|
|
176
|
-
// dev-mode warn-and-allow hatch.
|
|
177
|
-
vi.stubEnv('NODE_ENV', 'production')
|
|
178
|
-
})
|
|
179
|
-
afterEach(() => {
|
|
180
|
-
globalThis.fetch = originalFetch
|
|
181
|
-
setEmbedAuthAdapter(null)
|
|
182
|
-
vi.unstubAllEnvs()
|
|
183
|
-
vi.restoreAllMocks()
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('allows a cross-origin URL whose exact origin the adapter sanctions', async () => {
|
|
187
|
-
setEmbedAuthAdapter({ allowedOrigins: ['https://gateway.example'] })
|
|
188
|
-
await expect(embedAuthedFetch('https://gateway.example/content/api/faqs')).resolves.toBeDefined()
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('still rejects origins outside the allowlist', () => {
|
|
192
|
-
setEmbedAuthAdapter({ allowedOrigins: ['https://gateway.example'] })
|
|
193
|
-
expect(() => embedAuthedFetch('https://evil.com/x')).toThrow(/cross-origin/i)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('does not treat an allowlisted origin as a prefix (suffix-domain spoof rejected)', () => {
|
|
197
|
-
setEmbedAuthAdapter({ allowedOrigins: ['https://gateway.example'] })
|
|
198
|
-
expect(() => embedAuthedFetch('https://gateway.example.evil.com/x')).toThrow(/cross-origin/i)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('cannot allowlist non-http(s) schemes (protocol check runs first)', () => {
|
|
202
|
-
// `javascript:` URLs have origin 'null'; even a (misguided) 'null' entry
|
|
203
|
-
// must not get past the protocol gate.
|
|
204
|
-
setEmbedAuthAdapter({ allowedOrigins: ['null'] })
|
|
205
|
-
expect(() => embedAuthedFetch('javascript:alert(1)')).toThrow(/non-http\(s\)/i)
|
|
206
|
-
})
|
|
207
|
-
})
|
|
@@ -86,19 +86,6 @@ export interface EmbedAuthAdapter {
|
|
|
86
86
|
* host's own auth layer, not to this fetch wrapper.
|
|
87
87
|
*/
|
|
88
88
|
refresh?: () => Promise<boolean>
|
|
89
|
-
/**
|
|
90
|
-
* Exact extra origins (`scheme://host[:port]`, as produced by `URL.origin`)
|
|
91
|
-
* the same-origin guard additionally accepts. The guard normally rejects
|
|
92
|
-
* every cross-origin URL in production — but native-shell hosts serve the
|
|
93
|
-
* page from a local pseudo-origin (`capacitor://localhost`,
|
|
94
|
-
* `tauri://localhost`) with NO server behind it, so the gateway their
|
|
95
|
-
* bearer already belongs to is unavoidably cross-origin. Listing it here
|
|
96
|
-
* is the host's explicit sanction to send the adapter's credentials there.
|
|
97
|
-
* Compared AFTER the http(s)-protocol check, so non-http(s) schemes can
|
|
98
|
-
* never be allowlisted. Omit (or leave empty) everywhere else — same-origin
|
|
99
|
-
* remains the rule, this is the narrow exception.
|
|
100
|
-
*/
|
|
101
|
-
allowedOrigins?: string[]
|
|
102
89
|
}
|
|
103
90
|
|
|
104
91
|
/**
|
|
@@ -171,12 +158,9 @@ export function hasEmbedAuthAdapter(): boolean {
|
|
|
171
158
|
*
|
|
172
159
|
* **Cross-origin defense:** the wrapper assumes a same-origin `/api/…`
|
|
173
160
|
* relative URL. Absolute URLs are accepted only when their origin matches
|
|
174
|
-
* the current window's origin
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* This is a defense-in-depth guard for future call sites — outside the
|
|
178
|
-
* allowlisted-shell case there is no legitimate cross-origin use of this
|
|
179
|
-
* fetch wrapper.
|
|
161
|
+
* the current window's origin; cross-origin URLs throw before the bearer
|
|
162
|
+
* leaves the page. This is a defense-in-depth guard for future call sites
|
|
163
|
+
* — there is no legitimate cross-origin use of this fetch wrapper.
|
|
180
164
|
*
|
|
181
165
|
* **401 self-heal:** when a registered adapter supplies `refresh`, a `401`
|
|
182
166
|
* response triggers a single token refresh + retry of the same request
|
|
@@ -368,14 +352,6 @@ function assertSameOrigin(url: string): void {
|
|
|
368
352
|
)
|
|
369
353
|
}
|
|
370
354
|
if (target.origin !== pageOrigin) {
|
|
371
|
-
// Host-sanctioned cross-origin target (`EmbedAuthAdapter.allowedOrigins`).
|
|
372
|
-
// Native shells serve the page from a local pseudo-origin with no server
|
|
373
|
-
// behind it, so their gateway — the same backend the host already trusts
|
|
374
|
-
// with this bearer — is unavoidably cross-origin. Exact `URL.origin`
|
|
375
|
-
// match, and only reachable for http(s) targets (checked above).
|
|
376
|
-
if (getRegisteredAuthAdapter()?.allowedOrigins?.includes(target.origin)) {
|
|
377
|
-
return
|
|
378
|
-
}
|
|
379
355
|
// Dev-mode escape hatch — embedded apps (e.g. openframe-frontend)
|
|
380
356
|
// run on a different origin from their gateway during local dev,
|
|
381
357
|
// and forcing a Next.js `rewrites()` workaround is more error-prone
|