@kopexa/grc 0.0.2 → 0.0.3

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 (53) hide show
  1. package/dist/chunk-5TBN3JQA.mjs +66 -0
  2. package/dist/chunk-DC44K745.mjs +46 -0
  3. package/dist/chunk-HI7F2CF4.mjs +1 -0
  4. package/dist/chunk-HJUSN7FD.mjs +1 -0
  5. package/dist/chunk-QDYL5ABK.mjs +118 -0
  6. package/dist/chunk-QS5S6V26.mjs +22 -0
  7. package/dist/chunk-VFX3DASQ.mjs +57 -0
  8. package/dist/common/control/index.d.mts +3 -0
  9. package/dist/common/control/index.d.ts +3 -0
  10. package/dist/common/control/index.js +160 -0
  11. package/dist/common/control/index.mjs +11 -0
  12. package/dist/common/control/mapped-controls.d.mts +33 -0
  13. package/dist/common/control/mapped-controls.d.ts +33 -0
  14. package/dist/common/control/mapped-controls.js +159 -0
  15. package/dist/common/control/mapped-controls.mjs +11 -0
  16. package/dist/common/control/messages.d.mts +16 -0
  17. package/dist/common/control/messages.d.ts +16 -0
  18. package/dist/common/control/messages.js +45 -0
  19. package/dist/common/control/messages.mjs +7 -0
  20. package/dist/common/index.d.mts +5 -0
  21. package/dist/common/index.d.ts +5 -0
  22. package/dist/common/index.js +364 -75
  23. package/dist/common/index.mjs +26 -1
  24. package/dist/common/risk/index.d.mts +4 -0
  25. package/dist/common/risk/index.d.ts +4 -0
  26. package/dist/common/risk/index.js +185 -0
  27. package/dist/common/risk/index.mjs +20 -0
  28. package/dist/common/risk/messages.d.mts +40 -0
  29. package/dist/common/risk/messages.d.ts +40 -0
  30. package/dist/common/risk/messages.js +69 -0
  31. package/dist/common/risk/messages.mjs +7 -0
  32. package/dist/common/risk/risk-rating-display.d.mts +21 -0
  33. package/dist/common/risk/risk-rating-display.d.ts +21 -0
  34. package/dist/common/risk/risk-rating-display.js +139 -0
  35. package/dist/common/risk/risk-rating-display.mjs +10 -0
  36. package/dist/common/risk/types.d.mts +37 -0
  37. package/dist/common/risk/types.d.ts +37 -0
  38. package/dist/common/risk/types.js +82 -0
  39. package/dist/common/risk/types.mjs +11 -0
  40. package/dist/index.d.mts +5 -0
  41. package/dist/index.d.ts +5 -0
  42. package/dist/index.js +364 -75
  43. package/dist/index.mjs +26 -1
  44. package/package.json +8 -7
  45. package/src/common/control/index.ts +6 -0
  46. package/src/common/control/mapped-controls.tsx +192 -0
  47. package/src/common/control/messages.ts +16 -0
  48. package/src/common/index.ts +2 -0
  49. package/src/common/risk/index.ts +12 -0
  50. package/src/common/risk/messages.ts +40 -0
  51. package/src/common/risk/risk-rating-display.tsx +86 -0
  52. package/src/common/risk/types.ts +91 -0
  53. /package/dist/{chunk-BFZPRJQT.mjs → chunk-CND77GVC.mjs} +0 -0
package/dist/index.mjs CHANGED
@@ -1,6 +1,24 @@
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
@@ -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.3",
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.7.0",
46
+ "@kopexa/sight": "17.1.1"
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.13",
51
+ "@kopexa/icons": "17.2.1",
52
+ "@kopexa/i18n": "17.2.0",
53
+ "@kopexa/shared-utils": "17.0.13"
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
+ });
@@ -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
+ });
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { useSafeIntl } from "@kopexa/i18n";
4
+ import { Tooltip } from "@kopexa/sight";
5
+ import { riskRating } from "@kopexa/theme";
6
+ import { messages } from "./messages";
7
+ import {
8
+ getRiskLevelFromRating,
9
+ isRatingUnrated,
10
+ type RiskLevel,
11
+ type RiskRating,
12
+ } from "./types";
13
+
14
+ export interface RiskRatingDisplayProps {
15
+ /** The risk rating to display */
16
+ rating: RiskRating | null | undefined;
17
+ /** Show the rating value badge */
18
+ showBadge?: boolean;
19
+ /** Show the level label text */
20
+ showLabel?: boolean;
21
+ /** Size variant */
22
+ size?: "sm" | "md";
23
+ }
24
+
25
+ /**
26
+ * Display component for risk ratings.
27
+ * Shows a colored badge with the rating value and level label.
28
+ * Handles "not rated" state gracefully.
29
+ */
30
+ export function RiskRatingDisplay({
31
+ rating,
32
+ showBadge = true,
33
+ showLabel = true,
34
+ size = "md",
35
+ }: RiskRatingDisplayProps) {
36
+ const intl = useSafeIntl();
37
+
38
+ // Handle unrated state
39
+ if (isRatingUnrated(rating)) {
40
+ const styles = riskRating({ size, level: "unrated" });
41
+ const levelLabel = intl.formatMessage(messages.level_unrated);
42
+
43
+ return (
44
+ <Tooltip content={intl.formatMessage(messages.not_rated_hint)}>
45
+ <div className={styles.base()}>
46
+ {showBadge && <div className={styles.badge()}>—</div>}
47
+ {showLabel && <span className={styles.label()}>{levelLabel}</span>}
48
+ </div>
49
+ </Tooltip>
50
+ );
51
+ }
52
+
53
+ // At this point, rating is guaranteed to be non-null with valid values
54
+ const ratedValue = rating as RiskRating;
55
+ const level = getRiskLevelFromRating(ratedValue.rating);
56
+ const styles = riskRating({
57
+ size,
58
+ level: level as Exclude<RiskLevel, "unrated">,
59
+ });
60
+ const levelLabelKey = `level_${level}` as keyof typeof messages;
61
+ const levelLabel = intl.formatMessage(messages[levelLabelKey]);
62
+
63
+ return (
64
+ <Tooltip
65
+ content={
66
+ <div className="text-xs space-y-1">
67
+ <div>
68
+ {intl.formatMessage(messages.likelihood)}: {ratedValue.likelihood}/5
69
+ </div>
70
+ <div>
71
+ {intl.formatMessage(messages.consequence)}: {ratedValue.consequence}
72
+ /5
73
+ </div>
74
+ {ratedValue.comment && (
75
+ <div className="text-muted-foreground">{ratedValue.comment}</div>
76
+ )}
77
+ </div>
78
+ }
79
+ >
80
+ <div className={styles.base()}>
81
+ {showBadge && <div className={styles.badge()}>{ratedValue.rating}</div>}
82
+ {showLabel && <span className={styles.label()}>{levelLabel}</span>}
83
+ </div>
84
+ </Tooltip>
85
+ );
86
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * RiskRating represents the risk assessment details.
3
+ * Aligned with backend Go struct.
4
+ */
5
+ export interface RiskRating {
6
+ /** Likelihood of the risk occurring (1-5). 0 = not rated. */
7
+ likelihood: number;
8
+ /** Consequence/impact if the risk occurs (1-5). 0 = not rated. */
9
+ consequence: number;
10
+ /** Calculated rating: likelihood × consequence (0-25). 0 = not rated. */
11
+ rating: number;
12
+ /** Optional comment for context */
13
+ comment?: string;
14
+ }
15
+
16
+ export type RiskLevel = "unrated" | "low" | "medium" | "high" | "critical";
17
+
18
+ /**
19
+ * Get risk level from rating (0-25).
20
+ * - 0: Unrated
21
+ * - 1-4: Low
22
+ * - 5-9: Medium
23
+ * - 10-16: High
24
+ * - 17-25: Critical
25
+ */
26
+ export function getRiskLevelFromRating(rating: number): RiskLevel {
27
+ if (rating === 0) return "unrated";
28
+ if (rating <= 4) return "low";
29
+ if (rating <= 9) return "medium";
30
+ if (rating <= 16) return "high";
31
+ return "critical";
32
+ }
33
+
34
+ /**
35
+ * Check if a rating is considered "not rated"
36
+ */
37
+ export function isRatingUnrated(
38
+ rating: RiskRating | null | undefined,
39
+ ): boolean {
40
+ if (!rating) return true;
41
+ return (
42
+ rating.rating === 0 || rating.likelihood === 0 || rating.consequence === 0
43
+ );
44
+ }
45
+
46
+ export const riskLevelConfig: Record<
47
+ RiskLevel,
48
+ {
49
+ label: string;
50
+ bgColor: string;
51
+ textColor: string;
52
+ iconBgColor: string;
53
+ iconTextColor: string;
54
+ }
55
+ > = {
56
+ unrated: {
57
+ label: "Nicht bewertet",
58
+ bgColor: "bg-muted",
59
+ textColor: "text-muted-foreground",
60
+ iconBgColor: "bg-muted/50",
61
+ iconTextColor: "text-muted-foreground",
62
+ },
63
+ low: {
64
+ label: "Niedrig",
65
+ bgColor: "bg-success",
66
+ textColor: "text-success",
67
+ iconBgColor: "bg-success/10",
68
+ iconTextColor: "text-success",
69
+ },
70
+ medium: {
71
+ label: "Mittel",
72
+ bgColor: "bg-warning",
73
+ textColor: "text-warning",
74
+ iconBgColor: "bg-warning/10",
75
+ iconTextColor: "text-warning",
76
+ },
77
+ high: {
78
+ label: "Hoch",
79
+ bgColor: "bg-orange-500",
80
+ textColor: "text-orange-500",
81
+ iconBgColor: "bg-orange-500/10",
82
+ iconTextColor: "text-orange-500",
83
+ },
84
+ critical: {
85
+ label: "Kritisch",
86
+ bgColor: "bg-destructive",
87
+ textColor: "text-destructive",
88
+ iconBgColor: "bg-destructive/10",
89
+ iconTextColor: "text-destructive",
90
+ },
91
+ };
File without changes