@kopexa/grc 0.0.2 → 0.0.4
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-5TBN3JQA.mjs +66 -0
- package/dist/{chunk-TW3S4OE2.mjs → chunk-AGASJJ7X.mjs} +106 -82
- package/dist/chunk-DC44K745.mjs +46 -0
- package/dist/chunk-HI7F2CF4.mjs +1 -0
- package/dist/chunk-HJUSN7FD.mjs +1 -0
- package/dist/chunk-QDYL5ABK.mjs +118 -0
- package/dist/chunk-QS5S6V26.mjs +22 -0
- package/dist/chunk-VFX3DASQ.mjs +57 -0
- package/dist/common/control/index.d.mts +3 -0
- package/dist/common/control/index.d.ts +3 -0
- package/dist/common/control/index.js +160 -0
- package/dist/common/control/index.mjs +11 -0
- package/dist/common/control/mapped-controls.d.mts +33 -0
- package/dist/common/control/mapped-controls.d.ts +33 -0
- package/dist/common/control/mapped-controls.js +159 -0
- package/dist/common/control/mapped-controls.mjs +11 -0
- package/dist/common/control/messages.d.mts +16 -0
- package/dist/common/control/messages.d.ts +16 -0
- package/dist/common/control/messages.js +45 -0
- package/dist/common/control/messages.mjs +7 -0
- package/dist/common/impact/impact-card.d.mts +7 -1
- package/dist/common/impact/impact-card.d.ts +7 -1
- package/dist/common/impact/impact-card.js +105 -81
- package/dist/common/impact/impact-card.mjs +1 -1
- package/dist/common/impact/index.js +105 -81
- package/dist/common/impact/index.mjs +1 -1
- package/dist/common/index.d.mts +5 -0
- package/dist/common/index.d.ts +5 -0
- package/dist/common/index.js +458 -145
- package/dist/common/index.mjs +27 -2
- package/dist/common/risk/index.d.mts +4 -0
- package/dist/common/risk/index.d.ts +4 -0
- package/dist/common/risk/index.js +185 -0
- package/dist/common/risk/index.mjs +20 -0
- package/dist/common/risk/messages.d.mts +40 -0
- package/dist/common/risk/messages.d.ts +40 -0
- package/dist/common/risk/messages.js +69 -0
- package/dist/common/risk/messages.mjs +7 -0
- package/dist/common/risk/risk-rating-display.d.mts +21 -0
- package/dist/common/risk/risk-rating-display.d.ts +21 -0
- package/dist/common/risk/risk-rating-display.js +139 -0
- package/dist/common/risk/risk-rating-display.mjs +10 -0
- package/dist/common/risk/types.d.mts +37 -0
- package/dist/common/risk/types.d.ts +37 -0
- package/dist/common/risk/types.js +82 -0
- package/dist/common/risk/types.mjs +11 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +458 -145
- package/dist/index.mjs +27 -2
- package/package.json +8 -7
- package/src/common/control/index.ts +6 -0
- package/src/common/control/mapped-controls.tsx +192 -0
- package/src/common/control/messages.ts +16 -0
- package/src/common/impact/impact-card.tsx +132 -79
- package/src/common/index.ts +2 -0
- package/src/common/risk/index.ts +12 -0
- package/src/common/risk/messages.ts +40 -0
- package/src/common/risk/risk-rating-display.tsx +86 -0
- package/src/common/risk/types.ts +91 -0
- /package/dist/{chunk-BFZPRJQT.mjs → chunk-CND77GVC.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-HJUSN7FD.mjs";
|
|
3
|
+
import "./chunk-HI7F2CF4.mjs";
|
|
4
|
+
import {
|
|
5
|
+
RiskRatingDisplay
|
|
6
|
+
} from "./chunk-5TBN3JQA.mjs";
|
|
7
|
+
import {
|
|
8
|
+
messages as messages2
|
|
9
|
+
} from "./chunk-DC44K745.mjs";
|
|
10
|
+
import {
|
|
11
|
+
getRiskLevelFromRating,
|
|
12
|
+
isRatingUnrated,
|
|
13
|
+
riskLevelConfig
|
|
14
|
+
} from "./chunk-VFX3DASQ.mjs";
|
|
3
15
|
import "./chunk-TICWEZUI.mjs";
|
|
16
|
+
import "./chunk-CND77GVC.mjs";
|
|
17
|
+
import {
|
|
18
|
+
ControlChip,
|
|
19
|
+
MappedControls
|
|
20
|
+
} from "./chunk-QDYL5ABK.mjs";
|
|
21
|
+
import "./chunk-QS5S6V26.mjs";
|
|
4
22
|
import "./chunk-GFABGXAO.mjs";
|
|
5
23
|
import {
|
|
6
24
|
ImpactCard
|
|
7
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AGASJJ7X.mjs";
|
|
8
26
|
import {
|
|
9
27
|
assetScale,
|
|
10
28
|
getScale,
|
|
@@ -23,13 +41,20 @@ import {
|
|
|
23
41
|
import "./chunk-B47KDUYY.mjs";
|
|
24
42
|
export {
|
|
25
43
|
ComplianceBadges,
|
|
44
|
+
ControlChip,
|
|
26
45
|
DoraBadge,
|
|
27
46
|
ImpactCard,
|
|
47
|
+
MappedControls,
|
|
28
48
|
Nis2Badge,
|
|
49
|
+
RiskRatingDisplay,
|
|
29
50
|
assetScale,
|
|
51
|
+
getRiskLevelFromRating,
|
|
30
52
|
getScale,
|
|
31
53
|
impactLevels,
|
|
32
54
|
messages as impactMessages,
|
|
55
|
+
isRatingUnrated,
|
|
33
56
|
processScale,
|
|
57
|
+
riskLevelConfig,
|
|
58
|
+
messages2 as riskMessages,
|
|
34
59
|
riskScale
|
|
35
60
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kopexa/grc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "GRC (Governance, Risk, Compliance) components for Kopexa",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -42,14 +42,15 @@
|
|
|
42
42
|
"react": ">=19.0.0-rc.0",
|
|
43
43
|
"react-dom": ">=19.0.0-rc.0",
|
|
44
44
|
"react-intl": "^7.1.14",
|
|
45
|
-
"@kopexa/theme": "17.
|
|
46
|
-
"@kopexa/sight": "17.
|
|
45
|
+
"@kopexa/theme": "17.8.0",
|
|
46
|
+
"@kopexa/sight": "17.2.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@kopexa/
|
|
50
|
-
"@kopexa/
|
|
51
|
-
"@kopexa/
|
|
52
|
-
"@kopexa/
|
|
49
|
+
"@kopexa/krn": "^1.0.1",
|
|
50
|
+
"@kopexa/react-utils": "17.0.14",
|
|
51
|
+
"@kopexa/shared-utils": "17.0.14",
|
|
52
|
+
"@kopexa/icons": "17.2.2",
|
|
53
|
+
"@kopexa/i18n": "17.3.0"
|
|
53
54
|
},
|
|
54
55
|
"clean-package": "../../clean-package.config.json",
|
|
55
56
|
"publishConfig": {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSafeIntl } from "@kopexa/i18n";
|
|
4
|
+
import { KRN } from "@kopexa/krn";
|
|
5
|
+
import { Popover, Tooltip } from "@kopexa/sight";
|
|
6
|
+
import { relatedControlChip } from "@kopexa/theme";
|
|
7
|
+
import type { ComponentProps } from "react";
|
|
8
|
+
import { messages } from "./messages";
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
export interface MappedControlsProps extends ComponentProps<"div"> {
|
|
15
|
+
/** Array of control KRN strings */
|
|
16
|
+
controls: string[];
|
|
17
|
+
/** Maximum number of chips to display before showing overflow */
|
|
18
|
+
maxVisible?: number;
|
|
19
|
+
/** Size variant */
|
|
20
|
+
size?: "sm" | "md" | "lg";
|
|
21
|
+
/** Make chips interactive (clickable) */
|
|
22
|
+
interactive?: boolean;
|
|
23
|
+
/** Callback when a control chip is clicked */
|
|
24
|
+
onControlClick?: (krn: string) => void;
|
|
25
|
+
/** Show empty state when no controls */
|
|
26
|
+
showEmpty?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ControlChipProps {
|
|
30
|
+
/** The KRN string */
|
|
31
|
+
krn: string;
|
|
32
|
+
/** Size variant */
|
|
33
|
+
size?: "sm" | "md" | "lg";
|
|
34
|
+
/** Make chip interactive */
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
/** Click handler */
|
|
37
|
+
onClick?: () => void;
|
|
38
|
+
/** Additional class names */
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// Helper Functions
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a KRN string and extract framework + control ID.
|
|
48
|
+
* Returns formatted string like "iso27001:a-7-1" or the original string if parsing fails.
|
|
49
|
+
*/
|
|
50
|
+
function formatControlKrn(krnString: string): {
|
|
51
|
+
framework: string | null;
|
|
52
|
+
controlId: string;
|
|
53
|
+
full: string;
|
|
54
|
+
} {
|
|
55
|
+
const parsed = KRN.tryParse(krnString);
|
|
56
|
+
if (!parsed) {
|
|
57
|
+
return { framework: null, controlId: krnString, full: krnString };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const framework = parsed.tryResourceId("frameworks");
|
|
61
|
+
const control = parsed.tryResourceId("controls");
|
|
62
|
+
|
|
63
|
+
if (framework && control) {
|
|
64
|
+
return {
|
|
65
|
+
framework,
|
|
66
|
+
controlId: control,
|
|
67
|
+
full: `${framework}:${control}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const basename = parsed.basename();
|
|
72
|
+
return {
|
|
73
|
+
framework: null,
|
|
74
|
+
controlId: basename || krnString,
|
|
75
|
+
full: basename || krnString,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// Control Chip Component
|
|
81
|
+
// ============================================
|
|
82
|
+
|
|
83
|
+
export function ControlChip({
|
|
84
|
+
krn,
|
|
85
|
+
size = "md",
|
|
86
|
+
interactive = false,
|
|
87
|
+
onClick,
|
|
88
|
+
className,
|
|
89
|
+
}: ControlChipProps) {
|
|
90
|
+
const styles = relatedControlChip({ size, interactive });
|
|
91
|
+
const { controlId, full } = formatControlKrn(krn);
|
|
92
|
+
|
|
93
|
+
// Only show uppercase control ID in chip
|
|
94
|
+
const displayId = controlId.toUpperCase();
|
|
95
|
+
|
|
96
|
+
const content = <span className={styles.chipId()}>{displayId}</span>;
|
|
97
|
+
|
|
98
|
+
// Use button when interactive, span otherwise
|
|
99
|
+
const chip =
|
|
100
|
+
interactive && onClick ? (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className={styles.chip({ className })}
|
|
104
|
+
onClick={onClick}
|
|
105
|
+
>
|
|
106
|
+
{content}
|
|
107
|
+
</button>
|
|
108
|
+
) : (
|
|
109
|
+
<span className={styles.chip({ className })}>{content}</span>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Wrap in tooltip with full formatted KRN (e.g., "iso27001:a-7-1")
|
|
113
|
+
return <Tooltip content={full}>{chip}</Tooltip>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// Mapped Controls Component
|
|
118
|
+
// ============================================
|
|
119
|
+
|
|
120
|
+
export function MappedControls({
|
|
121
|
+
controls,
|
|
122
|
+
maxVisible = 2,
|
|
123
|
+
size = "md",
|
|
124
|
+
interactive = false,
|
|
125
|
+
onControlClick,
|
|
126
|
+
showEmpty = false,
|
|
127
|
+
className,
|
|
128
|
+
...rest
|
|
129
|
+
}: MappedControlsProps) {
|
|
130
|
+
const intl = useSafeIntl();
|
|
131
|
+
const styles = relatedControlChip({ size, interactive });
|
|
132
|
+
|
|
133
|
+
// Handle empty state
|
|
134
|
+
if (controls.length === 0) {
|
|
135
|
+
if (!showEmpty) return null;
|
|
136
|
+
return (
|
|
137
|
+
<span className="text-xs text-muted-foreground">
|
|
138
|
+
{intl.formatMessage(messages.no_controls)}
|
|
139
|
+
</span>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const visibleControls = controls.slice(0, maxVisible);
|
|
144
|
+
const hiddenCount = controls.length - maxVisible;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className={styles.root({ className })} {...rest}>
|
|
148
|
+
{visibleControls.map((krn) => (
|
|
149
|
+
<ControlChip
|
|
150
|
+
key={krn}
|
|
151
|
+
krn={krn}
|
|
152
|
+
size={size}
|
|
153
|
+
interactive={interactive}
|
|
154
|
+
onClick={onControlClick ? () => onControlClick(krn) : undefined}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
{hiddenCount > 0 && (
|
|
158
|
+
<Popover.Root>
|
|
159
|
+
<Popover.Trigger className={styles.overflow()}>
|
|
160
|
+
{intl.formatMessage(messages.more_controls, {
|
|
161
|
+
count: hiddenCount,
|
|
162
|
+
})}
|
|
163
|
+
</Popover.Trigger>
|
|
164
|
+
<Popover.Content
|
|
165
|
+
align="start"
|
|
166
|
+
className="p-2 max-w-xs"
|
|
167
|
+
showArrow={false}
|
|
168
|
+
>
|
|
169
|
+
<div className="text-xs font-medium text-muted-foreground mb-2">
|
|
170
|
+
{intl.formatMessage(messages.view_all, {
|
|
171
|
+
count: controls.length,
|
|
172
|
+
})}
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex flex-wrap gap-1">
|
|
175
|
+
{controls.map((krn) => (
|
|
176
|
+
<ControlChip
|
|
177
|
+
key={krn}
|
|
178
|
+
krn={krn}
|
|
179
|
+
size={size}
|
|
180
|
+
interactive={interactive}
|
|
181
|
+
onClick={
|
|
182
|
+
onControlClick ? () => onControlClick(krn) : undefined
|
|
183
|
+
}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</Popover.Content>
|
|
188
|
+
</Popover.Root>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineMessages } from "@kopexa/i18n";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
more_controls: {
|
|
5
|
+
id: "grc.control.mapped.more",
|
|
6
|
+
defaultMessage: "+{count} more",
|
|
7
|
+
},
|
|
8
|
+
view_all: {
|
|
9
|
+
id: "grc.control.mapped.view_all",
|
|
10
|
+
defaultMessage: "View all {count} controls",
|
|
11
|
+
},
|
|
12
|
+
no_controls: {
|
|
13
|
+
id: "grc.control.mapped.no_controls",
|
|
14
|
+
defaultMessage: "No mapped controls",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -145,6 +145,12 @@ export interface ImpactCardProps {
|
|
|
145
145
|
scale?: ImpactScalePreset | ImpactScaleConfig;
|
|
146
146
|
/** Custom title for the card */
|
|
147
147
|
title?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Display variant:
|
|
150
|
+
* - `card`: Wrapped in a Card with edit/save/cancel buttons (default)
|
|
151
|
+
* - `inline`: No card wrapper, always editable, changes propagate immediately
|
|
152
|
+
*/
|
|
153
|
+
variant?: "card" | "inline";
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
const defaultImpact: ImpactValue = {
|
|
@@ -162,14 +168,19 @@ export function ImpactCard({
|
|
|
162
168
|
readOnly = false,
|
|
163
169
|
scale = "risk",
|
|
164
170
|
title,
|
|
171
|
+
variant = "card",
|
|
165
172
|
}: ImpactCardProps) {
|
|
166
173
|
const intl = useSafeIntl();
|
|
174
|
+
const isInline = variant === "inline";
|
|
167
175
|
const [isEditing, setIsEditing] = useState(false);
|
|
168
176
|
const [editValues, setEditValues] = useState<ImpactValue>(
|
|
169
177
|
value || defaultImpact,
|
|
170
178
|
);
|
|
171
179
|
|
|
172
|
-
|
|
180
|
+
// For inline variant, always show as editing (form mode)
|
|
181
|
+
const effectiveIsEditing = isInline ? !readOnly : isEditing;
|
|
182
|
+
|
|
183
|
+
const styles = impactCard({ editing: !isInline && isEditing });
|
|
173
184
|
|
|
174
185
|
// Resolve scale config
|
|
175
186
|
const scaleConfig: ImpactScaleConfig =
|
|
@@ -218,7 +229,12 @@ export function ImpactCard({
|
|
|
218
229
|
setIsEditing(true);
|
|
219
230
|
};
|
|
220
231
|
|
|
221
|
-
|
|
232
|
+
// In inline mode, always use value; in card mode, use editValues when editing
|
|
233
|
+
const currentImpact = isInline
|
|
234
|
+
? value || defaultImpact
|
|
235
|
+
: isEditing
|
|
236
|
+
? editValues
|
|
237
|
+
: value || defaultImpact;
|
|
222
238
|
|
|
223
239
|
const handleLevelChange =
|
|
224
240
|
(
|
|
@@ -229,17 +245,29 @@ export function ImpactCard({
|
|
|
229
245
|
| "impactAuthenticity",
|
|
230
246
|
) =>
|
|
231
247
|
(level: ImpactLevel) => {
|
|
232
|
-
|
|
233
|
-
...
|
|
248
|
+
const newValues = {
|
|
249
|
+
...(isInline ? value || defaultImpact : editValues),
|
|
234
250
|
[key]: level,
|
|
235
|
-
}
|
|
251
|
+
};
|
|
252
|
+
if (isInline) {
|
|
253
|
+
// Inline mode: propagate changes immediately
|
|
254
|
+
onChange?.(newValues);
|
|
255
|
+
} else {
|
|
256
|
+
setEditValues(newValues);
|
|
257
|
+
}
|
|
236
258
|
};
|
|
237
259
|
|
|
238
260
|
const handleJustificationChange = (justification: string) => {
|
|
239
|
-
|
|
240
|
-
...
|
|
261
|
+
const newValues = {
|
|
262
|
+
...(isInline ? value || defaultImpact : editValues),
|
|
241
263
|
impactJustification: justification || undefined,
|
|
242
|
-
}
|
|
264
|
+
};
|
|
265
|
+
if (isInline) {
|
|
266
|
+
// Inline mode: propagate changes immediately
|
|
267
|
+
onChange?.(newValues);
|
|
268
|
+
} else {
|
|
269
|
+
setEditValues(newValues);
|
|
270
|
+
}
|
|
243
271
|
};
|
|
244
272
|
|
|
245
273
|
// Calculate highest impact for justification hint
|
|
@@ -255,6 +283,100 @@ export function ImpactCard({
|
|
|
255
283
|
level: highestLabel,
|
|
256
284
|
});
|
|
257
285
|
|
|
286
|
+
// Shared content for both variants
|
|
287
|
+
const impactRows = (
|
|
288
|
+
<>
|
|
289
|
+
<ImpactItemRow
|
|
290
|
+
label={t.confidentiality}
|
|
291
|
+
shortLabel="C"
|
|
292
|
+
value={currentImpact.impactConfidentiality}
|
|
293
|
+
isEditing={effectiveIsEditing}
|
|
294
|
+
scale={scaleConfig}
|
|
295
|
+
formatLabel={formatLabel}
|
|
296
|
+
onLevelChange={handleLevelChange("impactConfidentiality")}
|
|
297
|
+
/>
|
|
298
|
+
<ImpactItemRow
|
|
299
|
+
label={t.integrity}
|
|
300
|
+
shortLabel="I"
|
|
301
|
+
value={currentImpact.impactIntegrity}
|
|
302
|
+
isEditing={effectiveIsEditing}
|
|
303
|
+
scale={scaleConfig}
|
|
304
|
+
formatLabel={formatLabel}
|
|
305
|
+
onLevelChange={handleLevelChange("impactIntegrity")}
|
|
306
|
+
/>
|
|
307
|
+
<ImpactItemRow
|
|
308
|
+
label={t.availability}
|
|
309
|
+
shortLabel="A"
|
|
310
|
+
value={currentImpact.impactAvailability}
|
|
311
|
+
isEditing={effectiveIsEditing}
|
|
312
|
+
scale={scaleConfig}
|
|
313
|
+
formatLabel={formatLabel}
|
|
314
|
+
onLevelChange={handleLevelChange("impactAvailability")}
|
|
315
|
+
/>
|
|
316
|
+
{showAuthenticity && (
|
|
317
|
+
<ImpactItemRow
|
|
318
|
+
label={t.authenticity}
|
|
319
|
+
shortLabel="Au"
|
|
320
|
+
value={currentImpact.impactAuthenticity ?? 0}
|
|
321
|
+
isEditing={effectiveIsEditing}
|
|
322
|
+
scale={scaleConfig}
|
|
323
|
+
formatLabel={formatLabel}
|
|
324
|
+
onLevelChange={handleLevelChange("impactAuthenticity")}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
</>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const justificationContent = showJustification && (
|
|
331
|
+
<div className={styles.justificationSection()}>
|
|
332
|
+
<label
|
|
333
|
+
htmlFor="impact-justification"
|
|
334
|
+
className={styles.justificationLabel()}
|
|
335
|
+
>
|
|
336
|
+
{t.justification}
|
|
337
|
+
{highestImpact > 0 && (
|
|
338
|
+
<span className={styles.justificationHint()}>
|
|
339
|
+
{justificationHint}
|
|
340
|
+
</span>
|
|
341
|
+
)}
|
|
342
|
+
</label>
|
|
343
|
+
{effectiveIsEditing ? (
|
|
344
|
+
<Textarea
|
|
345
|
+
id="impact-justification"
|
|
346
|
+
value={currentImpact.impactJustification || ""}
|
|
347
|
+
onChange={(e) => handleJustificationChange(e.target.value)}
|
|
348
|
+
placeholder={t.justificationPlaceholder}
|
|
349
|
+
rows={3}
|
|
350
|
+
className="text-sm"
|
|
351
|
+
/>
|
|
352
|
+
) : currentImpact.impactJustification ? (
|
|
353
|
+
<p className={styles.justificationText()}>
|
|
354
|
+
{currentImpact.impactJustification}
|
|
355
|
+
</p>
|
|
356
|
+
) : (
|
|
357
|
+
<p className={styles.justificationEmpty()}>{t.noJustification}</p>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Inline variant: no card wrapper, always editable
|
|
363
|
+
if (isInline) {
|
|
364
|
+
return (
|
|
365
|
+
<div className={styles.wrapper()}>
|
|
366
|
+
{title && (
|
|
367
|
+
<div className={styles.inlineHeader()}>
|
|
368
|
+
<Heading level="h4" className="text-sm font-medium">
|
|
369
|
+
{title}
|
|
370
|
+
</Heading>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
{impactRows}
|
|
374
|
+
{justificationContent}
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Card variant: wrapped in Card with edit/save/cancel
|
|
258
380
|
return (
|
|
259
381
|
<Card.Root className={styles.root()}>
|
|
260
382
|
<Card.Header className="flex flex-row items-center justify-between">
|
|
@@ -290,77 +412,8 @@ export function ImpactCard({
|
|
|
290
412
|
))}
|
|
291
413
|
</Card.Header>
|
|
292
414
|
<Card.Body className="space-y-3">
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
shortLabel="C"
|
|
296
|
-
value={currentImpact.impactConfidentiality}
|
|
297
|
-
isEditing={isEditing}
|
|
298
|
-
scale={scaleConfig}
|
|
299
|
-
formatLabel={formatLabel}
|
|
300
|
-
onLevelChange={handleLevelChange("impactConfidentiality")}
|
|
301
|
-
/>
|
|
302
|
-
<ImpactItemRow
|
|
303
|
-
label={t.integrity}
|
|
304
|
-
shortLabel="I"
|
|
305
|
-
value={currentImpact.impactIntegrity}
|
|
306
|
-
isEditing={isEditing}
|
|
307
|
-
scale={scaleConfig}
|
|
308
|
-
formatLabel={formatLabel}
|
|
309
|
-
onLevelChange={handleLevelChange("impactIntegrity")}
|
|
310
|
-
/>
|
|
311
|
-
<ImpactItemRow
|
|
312
|
-
label={t.availability}
|
|
313
|
-
shortLabel="A"
|
|
314
|
-
value={currentImpact.impactAvailability}
|
|
315
|
-
isEditing={isEditing}
|
|
316
|
-
scale={scaleConfig}
|
|
317
|
-
formatLabel={formatLabel}
|
|
318
|
-
onLevelChange={handleLevelChange("impactAvailability")}
|
|
319
|
-
/>
|
|
320
|
-
{showAuthenticity && (
|
|
321
|
-
<ImpactItemRow
|
|
322
|
-
label={t.authenticity}
|
|
323
|
-
shortLabel="Au"
|
|
324
|
-
value={currentImpact.impactAuthenticity ?? 0}
|
|
325
|
-
isEditing={isEditing}
|
|
326
|
-
scale={scaleConfig}
|
|
327
|
-
formatLabel={formatLabel}
|
|
328
|
-
onLevelChange={handleLevelChange("impactAuthenticity")}
|
|
329
|
-
/>
|
|
330
|
-
)}
|
|
331
|
-
|
|
332
|
-
{/* Justification - single field for all dimensions */}
|
|
333
|
-
{showJustification && (
|
|
334
|
-
<div className={styles.justificationSection()}>
|
|
335
|
-
<label
|
|
336
|
-
htmlFor="impact-justification"
|
|
337
|
-
className={styles.justificationLabel()}
|
|
338
|
-
>
|
|
339
|
-
{t.justification}
|
|
340
|
-
{highestImpact > 0 && (
|
|
341
|
-
<span className={styles.justificationHint()}>
|
|
342
|
-
{justificationHint}
|
|
343
|
-
</span>
|
|
344
|
-
)}
|
|
345
|
-
</label>
|
|
346
|
-
{isEditing ? (
|
|
347
|
-
<Textarea
|
|
348
|
-
id="impact-justification"
|
|
349
|
-
value={currentImpact.impactJustification || ""}
|
|
350
|
-
onChange={(e) => handleJustificationChange(e.target.value)}
|
|
351
|
-
placeholder={t.justificationPlaceholder}
|
|
352
|
-
rows={3}
|
|
353
|
-
className="text-sm"
|
|
354
|
-
/>
|
|
355
|
-
) : currentImpact.impactJustification ? (
|
|
356
|
-
<p className={styles.justificationText()}>
|
|
357
|
-
{currentImpact.impactJustification}
|
|
358
|
-
</p>
|
|
359
|
-
) : (
|
|
360
|
-
<p className={styles.justificationEmpty()}>{t.noJustification}</p>
|
|
361
|
-
)}
|
|
362
|
-
</div>
|
|
363
|
-
)}
|
|
415
|
+
{impactRows}
|
|
416
|
+
{justificationContent}
|
|
364
417
|
</Card.Body>
|
|
365
418
|
</Card.Root>
|
|
366
419
|
);
|
package/src/common/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { messages as riskMessages } from "./messages";
|
|
2
|
+
export {
|
|
3
|
+
RiskRatingDisplay,
|
|
4
|
+
type RiskRatingDisplayProps,
|
|
5
|
+
} from "./risk-rating-display";
|
|
6
|
+
export {
|
|
7
|
+
getRiskLevelFromRating,
|
|
8
|
+
isRatingUnrated,
|
|
9
|
+
type RiskLevel,
|
|
10
|
+
type RiskRating,
|
|
11
|
+
riskLevelConfig,
|
|
12
|
+
} from "./types";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineMessages } from "@kopexa/i18n";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
level_unrated: {
|
|
5
|
+
id: "grc.risk.level.unrated",
|
|
6
|
+
defaultMessage: "Not rated",
|
|
7
|
+
},
|
|
8
|
+
level_low: {
|
|
9
|
+
id: "grc.risk.level.low",
|
|
10
|
+
defaultMessage: "Low",
|
|
11
|
+
},
|
|
12
|
+
level_medium: {
|
|
13
|
+
id: "grc.risk.level.medium",
|
|
14
|
+
defaultMessage: "Medium",
|
|
15
|
+
},
|
|
16
|
+
level_high: {
|
|
17
|
+
id: "grc.risk.level.high",
|
|
18
|
+
defaultMessage: "High",
|
|
19
|
+
},
|
|
20
|
+
level_critical: {
|
|
21
|
+
id: "grc.risk.level.critical",
|
|
22
|
+
defaultMessage: "Critical",
|
|
23
|
+
},
|
|
24
|
+
likelihood: {
|
|
25
|
+
id: "grc.risk.likelihood",
|
|
26
|
+
defaultMessage: "Likelihood",
|
|
27
|
+
},
|
|
28
|
+
consequence: {
|
|
29
|
+
id: "grc.risk.consequence",
|
|
30
|
+
defaultMessage: "Consequence",
|
|
31
|
+
},
|
|
32
|
+
rating: {
|
|
33
|
+
id: "grc.risk.rating",
|
|
34
|
+
defaultMessage: "Rating",
|
|
35
|
+
},
|
|
36
|
+
not_rated_hint: {
|
|
37
|
+
id: "grc.risk.not_rated_hint",
|
|
38
|
+
defaultMessage: "Risk has not been assessed yet",
|
|
39
|
+
},
|
|
40
|
+
});
|