@kopexa/grc 0.0.2
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/LICENSE +201 -0
- package/dist/asset/index.d.mts +2 -0
- package/dist/asset/index.d.ts +2 -0
- package/dist/asset/index.js +19 -0
- package/dist/asset/index.mjs +1 -0
- package/dist/chunk-7754RETD.mjs +57 -0
- package/dist/chunk-B47KDUYY.mjs +26 -0
- package/dist/chunk-BFZPRJQT.mjs +1 -0
- package/dist/chunk-GF3WJZVI.mjs +141 -0
- package/dist/chunk-GFABGXAO.mjs +1 -0
- package/dist/chunk-KNGEZZFI.mjs +157 -0
- package/dist/chunk-TICWEZUI.mjs +1 -0
- package/dist/chunk-TW3S4OE2.mjs +251 -0
- package/dist/common/compliance/compliance-badge.d.mts +33 -0
- package/dist/common/compliance/compliance-badge.d.ts +33 -0
- package/dist/common/compliance/compliance-badge.js +103 -0
- package/dist/common/compliance/compliance-badge.mjs +13 -0
- package/dist/common/compliance/index.d.mts +2 -0
- package/dist/common/compliance/index.d.ts +2 -0
- package/dist/common/compliance/index.js +104 -0
- package/dist/common/compliance/index.mjs +13 -0
- package/dist/common/compliance/messages.d.mts +20 -0
- package/dist/common/compliance/messages.d.ts +20 -0
- package/dist/common/compliance/messages.js +49 -0
- package/dist/common/compliance/messages.mjs +7 -0
- package/dist/common/impact/impact-card.d.mts +35 -0
- package/dist/common/impact/impact-card.d.ts +35 -0
- package/dist/common/impact/impact-card.js +551 -0
- package/dist/common/impact/impact-card.mjs +10 -0
- package/dist/common/impact/index.d.mts +5 -0
- package/dist/common/impact/index.d.ts +5 -0
- package/dist/common/impact/index.js +564 -0
- package/dist/common/impact/index.mjs +24 -0
- package/dist/common/impact/messages.d.mts +128 -0
- package/dist/common/impact/messages.d.ts +128 -0
- package/dist/common/impact/messages.js +164 -0
- package/dist/common/impact/messages.mjs +7 -0
- package/dist/common/impact/scales.d.mts +46 -0
- package/dist/common/impact/scales.d.ts +46 -0
- package/dist/common/impact/scales.js +319 -0
- package/dist/common/impact/scales.mjs +16 -0
- package/dist/common/index.d.mts +6 -0
- package/dist/common/index.d.ts +6 -0
- package/dist/common/index.js +640 -0
- package/dist/common/index.mjs +35 -0
- package/dist/control/index.d.mts +2 -0
- package/dist/control/index.d.ts +2 -0
- package/dist/control/index.js +19 -0
- package/dist/control/index.mjs +1 -0
- package/dist/incident/index.d.mts +2 -0
- package/dist/incident/index.d.ts +2 -0
- package/dist/incident/index.js +19 -0
- package/dist/incident/index.mjs +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +640 -0
- package/dist/index.mjs +35 -0
- package/dist/risk/index.d.mts +2 -0
- package/dist/risk/index.d.ts +2 -0
- package/dist/risk/index.js +19 -0
- package/dist/risk/index.mjs +1 -0
- package/dist/vendor/index.d.mts +2 -0
- package/dist/vendor/index.d.ts +2 -0
- package/dist/vendor/index.js +19 -0
- package/dist/vendor/index.mjs +1 -0
- package/package.json +66 -0
- package/src/asset/index.ts +4 -0
- package/src/common/compliance/compliance-badge.tsx +110 -0
- package/src/common/compliance/index.ts +8 -0
- package/src/common/compliance/messages.ts +20 -0
- package/src/common/impact/impact-card.tsx +367 -0
- package/src/common/impact/index.ts +14 -0
- package/src/common/impact/messages.ts +141 -0
- package/src/common/impact/scales.ts +191 -0
- package/src/common/index.ts +4 -0
- package/src/control/index.ts +4 -0
- package/src/incident/index.ts +4 -0
- package/src/index.ts +20 -0
- package/src/risk/index.ts +4 -0
- package/src/vendor/index.ts +4 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __copyProps = (to, from, except, desc) => {
|
|
8
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
9
|
+
for (let key of __getOwnPropNames(from))
|
|
10
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
11
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
12
|
+
}
|
|
13
|
+
return to;
|
|
14
|
+
};
|
|
15
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
16
|
+
|
|
17
|
+
// src/vendor/index.ts
|
|
18
|
+
var vendor_exports = {};
|
|
19
|
+
module.exports = __toCommonJS(vendor_exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kopexa/grc",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "GRC (Governance, Risk, Compliance) components for Kopexa",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/kopexa-grc/sight.git",
|
|
24
|
+
"directory": "packages/grc"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"kopexa",
|
|
28
|
+
"grc",
|
|
29
|
+
"governance",
|
|
30
|
+
"risk",
|
|
31
|
+
"compliance",
|
|
32
|
+
"react",
|
|
33
|
+
"components"
|
|
34
|
+
],
|
|
35
|
+
"author": "Kopexa GmbH",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/kopexa-grc/sight/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/kopexa-grc/sight#readme",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=19.0.0-rc.0",
|
|
43
|
+
"react-dom": ">=19.0.0-rc.0",
|
|
44
|
+
"react-intl": "^7.1.14",
|
|
45
|
+
"@kopexa/theme": "17.6.0",
|
|
46
|
+
"@kopexa/sight": "17.1.0"
|
|
47
|
+
},
|
|
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"
|
|
53
|
+
},
|
|
54
|
+
"clean-package": "../../clean-package.config.json",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup src --dts",
|
|
60
|
+
"build:fast": "tsup src",
|
|
61
|
+
"dev": "pnpm build:fast --watch",
|
|
62
|
+
"clean": "rimraf dist .turbo",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"i18n:extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.test.*\" \"**/*.stories.*\" --format simple --out-file ../core/i18n/tmp/$npm_package_name.en.json"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSafeIntl } from "@kopexa/i18n";
|
|
4
|
+
import { DORAIcon, NIS2Icon } from "@kopexa/icons";
|
|
5
|
+
import { Tooltip } from "@kopexa/sight";
|
|
6
|
+
import { complianceBadge } from "@kopexa/theme";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
|
|
9
|
+
// ============================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================
|
|
12
|
+
|
|
13
|
+
export interface DoraBadgeProps {
|
|
14
|
+
/** Custom tooltip content */
|
|
15
|
+
tooltip?: string;
|
|
16
|
+
/** Icon size */
|
|
17
|
+
iconSize?: number;
|
|
18
|
+
/** Additional class names */
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Nis2BadgeProps {
|
|
23
|
+
/** Custom tooltip content */
|
|
24
|
+
tooltip?: string;
|
|
25
|
+
/** Icon size */
|
|
26
|
+
iconSize?: number;
|
|
27
|
+
/** Additional class names */
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ComplianceBadgesProps {
|
|
32
|
+
/** Show DORA Critical badge */
|
|
33
|
+
doraCritical?: boolean;
|
|
34
|
+
/** Show NIS2 Significant badge */
|
|
35
|
+
nis2Significant?: boolean;
|
|
36
|
+
/** Icon size for badges */
|
|
37
|
+
iconSize?: number;
|
|
38
|
+
/** Additional class names for container */
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// DORA Badge Component
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
export function DoraBadge({
|
|
47
|
+
tooltip,
|
|
48
|
+
iconSize = 12,
|
|
49
|
+
className,
|
|
50
|
+
}: DoraBadgeProps) {
|
|
51
|
+
const intl = useSafeIntl();
|
|
52
|
+
const styles = complianceBadge({ color: "dora" });
|
|
53
|
+
|
|
54
|
+
const tooltipContent = tooltip ?? intl.formatMessage(messages.dora_tooltip);
|
|
55
|
+
const label = intl.formatMessage(messages.dora_label);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Tooltip content={tooltipContent}>
|
|
59
|
+
<span className={styles.root({ className })}>
|
|
60
|
+
<DORAIcon size={iconSize} className={styles.icon()} />
|
|
61
|
+
<span className={styles.label()}>{label}</span>
|
|
62
|
+
</span>
|
|
63
|
+
</Tooltip>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================
|
|
68
|
+
// NIS2 Badge Component
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
export function Nis2Badge({
|
|
72
|
+
tooltip,
|
|
73
|
+
iconSize = 12,
|
|
74
|
+
className,
|
|
75
|
+
}: Nis2BadgeProps) {
|
|
76
|
+
const intl = useSafeIntl();
|
|
77
|
+
const styles = complianceBadge({ color: "nis2" });
|
|
78
|
+
|
|
79
|
+
const tooltipContent = tooltip ?? intl.formatMessage(messages.nis2_tooltip);
|
|
80
|
+
const label = intl.formatMessage(messages.nis2_label);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Tooltip content={tooltipContent}>
|
|
84
|
+
<span className={styles.root({ className })}>
|
|
85
|
+
<NIS2Icon size={iconSize} className={styles.icon()} />
|
|
86
|
+
<span className={styles.label()}>{label}</span>
|
|
87
|
+
</span>
|
|
88
|
+
</Tooltip>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================
|
|
93
|
+
// Combined Compliance Badges Component
|
|
94
|
+
// ============================================
|
|
95
|
+
|
|
96
|
+
export function ComplianceBadges({
|
|
97
|
+
doraCritical,
|
|
98
|
+
nis2Significant,
|
|
99
|
+
iconSize = 12,
|
|
100
|
+
className,
|
|
101
|
+
}: ComplianceBadgesProps) {
|
|
102
|
+
if (!doraCritical && !nis2Significant) return null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className={className ?? "flex items-center gap-1"}>
|
|
106
|
+
{doraCritical && <DoraBadge iconSize={iconSize} />}
|
|
107
|
+
{nis2Significant && <Nis2Badge iconSize={iconSize} />}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineMessages } from "@kopexa/i18n";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
dora_label: {
|
|
5
|
+
id: "grc.compliance.dora.label",
|
|
6
|
+
defaultMessage: "DORA",
|
|
7
|
+
},
|
|
8
|
+
dora_tooltip: {
|
|
9
|
+
id: "grc.compliance.dora.tooltip",
|
|
10
|
+
defaultMessage: "DORA Critical - Affects critical ICT functions",
|
|
11
|
+
},
|
|
12
|
+
nis2_label: {
|
|
13
|
+
id: "grc.compliance.nis2.label",
|
|
14
|
+
defaultMessage: "NIS2",
|
|
15
|
+
},
|
|
16
|
+
nis2_tooltip: {
|
|
17
|
+
id: "grc.compliance.nis2.tooltip",
|
|
18
|
+
defaultMessage: "NIS2 Significant - All-hazards risk",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSafeIntl } from "@kopexa/i18n";
|
|
4
|
+
import { EditIcon } from "@kopexa/icons";
|
|
5
|
+
import { Button, Card, Chip, Heading, Select, Textarea } from "@kopexa/sight";
|
|
6
|
+
import { impactCard } from "@kopexa/theme";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { messages } from "./messages";
|
|
9
|
+
import {
|
|
10
|
+
getScale,
|
|
11
|
+
type ImpactLevel,
|
|
12
|
+
type ImpactScaleConfig,
|
|
13
|
+
type ImpactScalePreset,
|
|
14
|
+
impactLevels,
|
|
15
|
+
} from "./scales";
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// Types (aligned with Go ImpactMixin schema)
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export interface ImpactValue {
|
|
22
|
+
/** Rating 0-5. Vertraulichkeit (Confidentiality). */
|
|
23
|
+
impactConfidentiality: ImpactLevel;
|
|
24
|
+
/** Rating 0-5. Integrität (Integrity). */
|
|
25
|
+
impactIntegrity: ImpactLevel;
|
|
26
|
+
/** Rating 0-5. Verfügbarkeit (Availability) - DORA Critical! */
|
|
27
|
+
impactAvailability: ImpactLevel;
|
|
28
|
+
/** Rating 0-5. Authentizität (Authenticity/Accountability). */
|
|
29
|
+
impactAuthenticity?: ImpactLevel;
|
|
30
|
+
/** Human-readable explanation for the chosen impact scores. Focus on the highest score. */
|
|
31
|
+
impactJustification?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// Impact Item Row Component
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
interface ImpactItemRowProps {
|
|
39
|
+
label: string;
|
|
40
|
+
shortLabel: string;
|
|
41
|
+
value: ImpactLevel;
|
|
42
|
+
isEditing: boolean;
|
|
43
|
+
scale: ImpactScaleConfig;
|
|
44
|
+
formatLabel: (level: ImpactLevel) => string;
|
|
45
|
+
onLevelChange: (level: ImpactLevel) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ImpactItemRow({
|
|
49
|
+
label,
|
|
50
|
+
shortLabel,
|
|
51
|
+
value,
|
|
52
|
+
isEditing,
|
|
53
|
+
scale,
|
|
54
|
+
formatLabel,
|
|
55
|
+
onLevelChange,
|
|
56
|
+
}: ImpactItemRowProps) {
|
|
57
|
+
const config = scale[value];
|
|
58
|
+
const isUnrated = value === 0;
|
|
59
|
+
const percentage = isUnrated ? 0 : (value / 5) * 100;
|
|
60
|
+
const styles = impactCard({ unrated: isUnrated });
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={styles.row()}>
|
|
64
|
+
<div className={styles.rowContent()}>
|
|
65
|
+
{/* Icon */}
|
|
66
|
+
<span className={styles.rowIcon()}>{shortLabel}</span>
|
|
67
|
+
|
|
68
|
+
{/* Content */}
|
|
69
|
+
<div className={styles.rowBody()}>
|
|
70
|
+
<div className={styles.rowHeader()}>
|
|
71
|
+
<span className={styles.rowLabel()}>{label}</span>
|
|
72
|
+
{isEditing ? (
|
|
73
|
+
<Select
|
|
74
|
+
value={String(value)}
|
|
75
|
+
onValueChange={(val) =>
|
|
76
|
+
onLevelChange(Number(val) as ImpactLevel)
|
|
77
|
+
}
|
|
78
|
+
size="sm"
|
|
79
|
+
>
|
|
80
|
+
<Select.Trigger className="w-36">
|
|
81
|
+
<Select.Value />
|
|
82
|
+
</Select.Trigger>
|
|
83
|
+
<Select.Content>
|
|
84
|
+
{impactLevels.map((level) => (
|
|
85
|
+
<Select.Item key={level} value={String(level)}>
|
|
86
|
+
<span className="flex items-center gap-2">
|
|
87
|
+
<span className="text-xs text-muted-foreground w-3">
|
|
88
|
+
{level}
|
|
89
|
+
</span>
|
|
90
|
+
{formatLabel(level)}
|
|
91
|
+
</span>
|
|
92
|
+
</Select.Item>
|
|
93
|
+
))}
|
|
94
|
+
</Select.Content>
|
|
95
|
+
</Select>
|
|
96
|
+
) : (
|
|
97
|
+
<div className={styles.rowValue()}>
|
|
98
|
+
{!isUnrated && (
|
|
99
|
+
<span className={styles.rowValueNumber()}>{value}</span>
|
|
100
|
+
)}
|
|
101
|
+
<span
|
|
102
|
+
className={styles.rowValueBadge({
|
|
103
|
+
className: isUnrated
|
|
104
|
+
? undefined
|
|
105
|
+
: `${config.color} ${config.bgColor}`,
|
|
106
|
+
})}
|
|
107
|
+
>
|
|
108
|
+
{formatLabel(value)}
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Progress Bar */}
|
|
115
|
+
{!isUnrated && !isEditing && (
|
|
116
|
+
<div className={styles.progressContainer()}>
|
|
117
|
+
<div
|
|
118
|
+
className={styles.progressBar({ className: config.barColor })}
|
|
119
|
+
style={{ width: `${percentage}%` }}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================
|
|
130
|
+
// Impact Card Component
|
|
131
|
+
// ============================================
|
|
132
|
+
|
|
133
|
+
export interface ImpactCardProps {
|
|
134
|
+
/** The impact data */
|
|
135
|
+
value?: ImpactValue;
|
|
136
|
+
/** Callback when the impact changes */
|
|
137
|
+
onChange?: (impact: ImpactValue) => void;
|
|
138
|
+
/** Show justification field */
|
|
139
|
+
showJustification?: boolean;
|
|
140
|
+
/** Show authenticity as 4th dimension (CIAA) */
|
|
141
|
+
showAuthenticity?: boolean;
|
|
142
|
+
/** Make the component read-only */
|
|
143
|
+
readOnly?: boolean;
|
|
144
|
+
/** Scale preset or custom scale config */
|
|
145
|
+
scale?: ImpactScalePreset | ImpactScaleConfig;
|
|
146
|
+
/** Custom title for the card */
|
|
147
|
+
title?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const defaultImpact: ImpactValue = {
|
|
151
|
+
impactConfidentiality: 0,
|
|
152
|
+
impactIntegrity: 0,
|
|
153
|
+
impactAvailability: 0,
|
|
154
|
+
impactAuthenticity: 0,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export function ImpactCard({
|
|
158
|
+
value,
|
|
159
|
+
onChange,
|
|
160
|
+
showJustification = false,
|
|
161
|
+
showAuthenticity = false,
|
|
162
|
+
readOnly = false,
|
|
163
|
+
scale = "risk",
|
|
164
|
+
title,
|
|
165
|
+
}: ImpactCardProps) {
|
|
166
|
+
const intl = useSafeIntl();
|
|
167
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
168
|
+
const [editValues, setEditValues] = useState<ImpactValue>(
|
|
169
|
+
value || defaultImpact,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const styles = impactCard({ editing: isEditing });
|
|
173
|
+
|
|
174
|
+
// Resolve scale config
|
|
175
|
+
const scaleConfig: ImpactScaleConfig =
|
|
176
|
+
typeof scale === "string" ? getScale(scale) : scale;
|
|
177
|
+
|
|
178
|
+
// i18n helper for scale labels
|
|
179
|
+
const formatLabel = (level: ImpactLevel): string => {
|
|
180
|
+
const config = scaleConfig[level];
|
|
181
|
+
return intl.formatMessage(config.message);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// i18n labels
|
|
185
|
+
const t = {
|
|
186
|
+
titleCia: intl.formatMessage(messages.title_cia),
|
|
187
|
+
titleCiaa: intl.formatMessage(messages.title_ciaa),
|
|
188
|
+
confidentiality: intl.formatMessage(messages.confidentiality),
|
|
189
|
+
integrity: intl.formatMessage(messages.integrity),
|
|
190
|
+
availability: intl.formatMessage(messages.availability),
|
|
191
|
+
authenticity: intl.formatMessage(messages.authenticity),
|
|
192
|
+
justification: intl.formatMessage(messages.justification),
|
|
193
|
+
justificationPlaceholder: intl.formatMessage(
|
|
194
|
+
messages.justification_placeholder,
|
|
195
|
+
),
|
|
196
|
+
noJustification: intl.formatMessage(messages.no_justification),
|
|
197
|
+
edit: intl.formatMessage(messages.edit),
|
|
198
|
+
cancel: intl.formatMessage(messages.cancel),
|
|
199
|
+
save: intl.formatMessage(messages.save),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Derive default title based on authenticity
|
|
203
|
+
const defaultTitle = showAuthenticity ? t.titleCiaa : t.titleCia;
|
|
204
|
+
const cardTitle = title ?? defaultTitle;
|
|
205
|
+
|
|
206
|
+
const handleSave = () => {
|
|
207
|
+
onChange?.(editValues);
|
|
208
|
+
setIsEditing(false);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleCancel = () => {
|
|
212
|
+
setEditValues(value || defaultImpact);
|
|
213
|
+
setIsEditing(false);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handleStartEdit = () => {
|
|
217
|
+
setEditValues(value || defaultImpact);
|
|
218
|
+
setIsEditing(true);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const currentImpact = isEditing ? editValues : value || defaultImpact;
|
|
222
|
+
|
|
223
|
+
const handleLevelChange =
|
|
224
|
+
(
|
|
225
|
+
key:
|
|
226
|
+
| "impactConfidentiality"
|
|
227
|
+
| "impactIntegrity"
|
|
228
|
+
| "impactAvailability"
|
|
229
|
+
| "impactAuthenticity",
|
|
230
|
+
) =>
|
|
231
|
+
(level: ImpactLevel) => {
|
|
232
|
+
setEditValues((prev) => ({
|
|
233
|
+
...prev,
|
|
234
|
+
[key]: level,
|
|
235
|
+
}));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleJustificationChange = (justification: string) => {
|
|
239
|
+
setEditValues((prev) => ({
|
|
240
|
+
...prev,
|
|
241
|
+
impactJustification: justification || undefined,
|
|
242
|
+
}));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Calculate highest impact for justification hint
|
|
246
|
+
const highestImpact = Math.max(
|
|
247
|
+
currentImpact.impactConfidentiality,
|
|
248
|
+
currentImpact.impactIntegrity,
|
|
249
|
+
currentImpact.impactAvailability,
|
|
250
|
+
currentImpact.impactAuthenticity ?? 0,
|
|
251
|
+
) as ImpactLevel;
|
|
252
|
+
|
|
253
|
+
const highestLabel = formatLabel(highestImpact);
|
|
254
|
+
const justificationHint = intl.formatMessage(messages.justification_hint, {
|
|
255
|
+
level: highestLabel,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<Card.Root className={styles.root()}>
|
|
260
|
+
<Card.Header className="flex flex-row items-center justify-between">
|
|
261
|
+
<div className="flex items-center gap-2">
|
|
262
|
+
<Heading level="h4" className="text-sm font-medium">
|
|
263
|
+
{cardTitle}
|
|
264
|
+
</Heading>
|
|
265
|
+
{isEditing && (
|
|
266
|
+
<Chip size="sm" color="primary">
|
|
267
|
+
{t.edit}
|
|
268
|
+
</Chip>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
{!readOnly &&
|
|
272
|
+
(!isEditing ? (
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={handleStartEdit}
|
|
276
|
+
className={styles.editButton()}
|
|
277
|
+
aria-label={t.edit}
|
|
278
|
+
>
|
|
279
|
+
<EditIcon className="size-4" />
|
|
280
|
+
</button>
|
|
281
|
+
) : (
|
|
282
|
+
<div className="flex items-center gap-2">
|
|
283
|
+
<Button variant="ghost" size="sm" onClick={handleCancel}>
|
|
284
|
+
{t.cancel}
|
|
285
|
+
</Button>
|
|
286
|
+
<Button size="sm" onClick={handleSave}>
|
|
287
|
+
{t.save}
|
|
288
|
+
</Button>
|
|
289
|
+
</div>
|
|
290
|
+
))}
|
|
291
|
+
</Card.Header>
|
|
292
|
+
<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
|
+
)}
|
|
364
|
+
</Card.Body>
|
|
365
|
+
</Card.Root>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { ImpactCardProps, ImpactValue } from "./impact-card";
|
|
2
|
+
export { ImpactCard } from "./impact-card";
|
|
3
|
+
export { messages as impactMessages } from "./messages";
|
|
4
|
+
export {
|
|
5
|
+
assetScale,
|
|
6
|
+
getScale,
|
|
7
|
+
type ImpactLevel,
|
|
8
|
+
type ImpactLevelConfig,
|
|
9
|
+
type ImpactScaleConfig,
|
|
10
|
+
type ImpactScalePreset,
|
|
11
|
+
impactLevels,
|
|
12
|
+
processScale,
|
|
13
|
+
riskScale,
|
|
14
|
+
} from "./scales";
|