@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.
Files changed (61) hide show
  1. package/dist/chunk-5TBN3JQA.mjs +66 -0
  2. package/dist/{chunk-TW3S4OE2.mjs → chunk-AGASJJ7X.mjs} +106 -82
  3. package/dist/chunk-DC44K745.mjs +46 -0
  4. package/dist/chunk-HI7F2CF4.mjs +1 -0
  5. package/dist/chunk-HJUSN7FD.mjs +1 -0
  6. package/dist/chunk-QDYL5ABK.mjs +118 -0
  7. package/dist/chunk-QS5S6V26.mjs +22 -0
  8. package/dist/chunk-VFX3DASQ.mjs +57 -0
  9. package/dist/common/control/index.d.mts +3 -0
  10. package/dist/common/control/index.d.ts +3 -0
  11. package/dist/common/control/index.js +160 -0
  12. package/dist/common/control/index.mjs +11 -0
  13. package/dist/common/control/mapped-controls.d.mts +33 -0
  14. package/dist/common/control/mapped-controls.d.ts +33 -0
  15. package/dist/common/control/mapped-controls.js +159 -0
  16. package/dist/common/control/mapped-controls.mjs +11 -0
  17. package/dist/common/control/messages.d.mts +16 -0
  18. package/dist/common/control/messages.d.ts +16 -0
  19. package/dist/common/control/messages.js +45 -0
  20. package/dist/common/control/messages.mjs +7 -0
  21. package/dist/common/impact/impact-card.d.mts +7 -1
  22. package/dist/common/impact/impact-card.d.ts +7 -1
  23. package/dist/common/impact/impact-card.js +105 -81
  24. package/dist/common/impact/impact-card.mjs +1 -1
  25. package/dist/common/impact/index.js +105 -81
  26. package/dist/common/impact/index.mjs +1 -1
  27. package/dist/common/index.d.mts +5 -0
  28. package/dist/common/index.d.ts +5 -0
  29. package/dist/common/index.js +458 -145
  30. package/dist/common/index.mjs +27 -2
  31. package/dist/common/risk/index.d.mts +4 -0
  32. package/dist/common/risk/index.d.ts +4 -0
  33. package/dist/common/risk/index.js +185 -0
  34. package/dist/common/risk/index.mjs +20 -0
  35. package/dist/common/risk/messages.d.mts +40 -0
  36. package/dist/common/risk/messages.d.ts +40 -0
  37. package/dist/common/risk/messages.js +69 -0
  38. package/dist/common/risk/messages.mjs +7 -0
  39. package/dist/common/risk/risk-rating-display.d.mts +21 -0
  40. package/dist/common/risk/risk-rating-display.d.ts +21 -0
  41. package/dist/common/risk/risk-rating-display.js +139 -0
  42. package/dist/common/risk/risk-rating-display.mjs +10 -0
  43. package/dist/common/risk/types.d.mts +37 -0
  44. package/dist/common/risk/types.d.ts +37 -0
  45. package/dist/common/risk/types.js +82 -0
  46. package/dist/common/risk/types.mjs +11 -0
  47. package/dist/index.d.mts +5 -0
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.js +458 -145
  50. package/dist/index.mjs +27 -2
  51. package/package.json +8 -7
  52. package/src/common/control/index.ts +6 -0
  53. package/src/common/control/mapped-controls.tsx +192 -0
  54. package/src/common/control/messages.ts +16 -0
  55. package/src/common/impact/impact-card.tsx +132 -79
  56. package/src/common/index.ts +2 -0
  57. package/src/common/risk/index.ts +12 -0
  58. package/src/common/risk/messages.ts +40 -0
  59. package/src/common/risk/risk-rating-display.tsx +86 -0
  60. package/src/common/risk/types.ts +91 -0
  61. /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-BFZPRJQT.mjs";
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-TW3S4OE2.mjs";
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.2",
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.6.0",
46
- "@kopexa/sight": "17.1.0"
45
+ "@kopexa/theme": "17.8.0",
46
+ "@kopexa/sight": "17.2.0"
47
47
  },
48
48
  "dependencies": {
49
- "@kopexa/react-utils": "17.0.12",
50
- "@kopexa/shared-utils": "17.0.12",
51
- "@kopexa/icons": "17.2.0",
52
- "@kopexa/i18n": "17.1.0"
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,6 @@
1
+ export {
2
+ ControlChip,
3
+ type ControlChipProps,
4
+ MappedControls,
5
+ type MappedControlsProps,
6
+ } from "./mapped-controls";
@@ -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
- const styles = impactCard({ editing: isEditing });
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
- const currentImpact = isEditing ? editValues : value || defaultImpact;
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
- setEditValues((prev) => ({
233
- ...prev,
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
- setEditValues((prev) => ({
240
- ...prev,
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
- <ImpactItemRow
294
- label={t.confidentiality}
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
  );
@@ -1,4 +1,6 @@
1
1
  // Common GRC components shared across domains
2
2
 
3
3
  export * from "./compliance";
4
+ export * from "./control";
4
5
  export * from "./impact";
6
+ export * from "./risk";
@@ -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
+ });