lula2 0.0.5 → 0.0.6
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/README.md +291 -8
- package/dist/_app/env.js +1 -0
- package/dist/_app/immutable/assets/0.DtiRW3lO.css +1 -0
- package/dist/_app/immutable/assets/DynamicControlEditor.BkVTzFZ-.css +1 -0
- package/dist/_app/immutable/chunks/7x_q-1ab.js +1 -0
- package/dist/_app/immutable/chunks/B19gt6-g.js +2 -0
- package/dist/_app/immutable/chunks/BR-0Dorr.js +1 -0
- package/dist/_app/immutable/chunks/B_3ksxz5.js +2 -0
- package/dist/_app/immutable/chunks/Bg_R1qWi.js +3 -0
- package/dist/_app/immutable/chunks/D3aNP_lg.js +1 -0
- package/dist/_app/immutable/chunks/D4Q_ObIy.js +1 -0
- package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
- package/dist/_app/immutable/chunks/XY2j_owG.js +66 -0
- package/dist/_app/immutable/chunks/rzN25oDf.js +1 -0
- package/dist/_app/immutable/entry/app.r0uOd9qg.js +2 -0
- package/dist/_app/immutable/entry/start.DvoqR0rc.js +1 -0
- package/dist/_app/immutable/nodes/0.Ct6FAss_.js +1 -0
- package/dist/_app/immutable/nodes/1.DLoKuy8Q.js +1 -0
- package/dist/_app/immutable/nodes/2.IRkwSmiB.js +1 -0
- package/dist/_app/immutable/nodes/3.BrTg-ZHv.js +1 -0
- package/dist/_app/immutable/nodes/4.Blq-4WQS.js +9 -0
- package/dist/_app/version.json +1 -0
- package/dist/cli/commands/crawl.js +128 -0
- package/dist/cli/commands/ui.js +2769 -0
- package/dist/cli/commands/version.js +30 -0
- package/dist/cli/server/index.js +2713 -0
- package/dist/cli/server/server.js +2702 -0
- package/dist/cli/server/serverState.js +1199 -0
- package/dist/cli/server/spreadsheetRoutes.js +788 -0
- package/dist/cli/server/types.js +0 -0
- package/dist/cli/server/websocketServer.js +2625 -0
- package/dist/cli/utils/debug.js +24 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +38 -0
- package/dist/index.js +2924 -37
- package/dist/lula.png +0 -0
- package/dist/lula2 +2 -0
- package/package.json +120 -72
- package/src/app.css +192 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +13 -0
- package/src/lib/actions/fadeWhenScrollable.ts +39 -0
- package/src/lib/actions/modal.ts +230 -0
- package/src/lib/actions/tooltip.ts +82 -0
- package/src/lib/components/control-sets/ControlSetInfo.svelte +20 -0
- package/src/lib/components/control-sets/ControlSetSelector.svelte +46 -0
- package/src/lib/components/control-sets/index.ts +5 -0
- package/src/lib/components/controls/ControlDetailsPanel.svelte +235 -0
- package/src/lib/components/controls/ControlsList.svelte +608 -0
- package/src/lib/components/controls/DynamicControlEditor.svelte +298 -0
- package/src/lib/components/controls/MappingCard.svelte +105 -0
- package/src/lib/components/controls/MappingForm.svelte +188 -0
- package/src/lib/components/controls/index.ts +9 -0
- package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +103 -0
- package/src/lib/components/controls/renderers/FieldRenderer.svelte +49 -0
- package/src/lib/components/controls/renderers/index.ts +5 -0
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +130 -0
- package/src/lib/components/controls/tabs/ImplementationTab.svelte +127 -0
- package/src/lib/components/controls/tabs/MappingsTab.svelte +182 -0
- package/src/lib/components/controls/tabs/OverviewTab.svelte +151 -0
- package/src/lib/components/controls/tabs/TimelineTab.svelte +41 -0
- package/src/lib/components/controls/tabs/index.ts +8 -0
- package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +63 -0
- package/src/lib/components/controls/utils/textProcessor.ts +164 -0
- package/src/lib/components/forms/DynamicControlForm.svelte +340 -0
- package/src/lib/components/forms/DynamicField.svelte +494 -0
- package/src/lib/components/forms/FormField.svelte +107 -0
- package/src/lib/components/forms/index.ts +6 -0
- package/src/lib/components/setup/ExistingControlSets.svelte +284 -0
- package/src/lib/components/setup/SpreadsheetImport.svelte +968 -0
- package/src/lib/components/setup/index.ts +5 -0
- package/src/lib/components/ui/Dropdown.svelte +107 -0
- package/src/lib/components/ui/EmptyState.svelte +80 -0
- package/src/lib/components/ui/FeatureToggle.svelte +50 -0
- package/src/lib/components/ui/SearchBar.svelte +73 -0
- package/src/lib/components/ui/StatusBadge.svelte +79 -0
- package/src/lib/components/ui/TabNavigation.svelte +48 -0
- package/src/lib/components/ui/Tooltip.svelte +120 -0
- package/src/lib/components/ui/index.ts +10 -0
- package/src/lib/components/version-control/DiffViewer.svelte +292 -0
- package/src/lib/components/version-control/TimelineItem.svelte +107 -0
- package/src/lib/components/version-control/YamlDiffViewer.svelte +428 -0
- package/src/lib/components/version-control/index.ts +6 -0
- package/src/lib/form-types.ts +57 -0
- package/src/lib/formatUtils.ts +17 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/types.ts +180 -0
- package/src/lib/websocket.ts +359 -0
- package/src/routes/+layout.svelte +236 -0
- package/src/routes/+page.svelte +38 -0
- package/src/routes/control/[id]/+page.svelte +112 -0
- package/src/routes/setup/+page.svelte +241 -0
- package/src/stores/compliance.ts +95 -0
- package/src/styles/highlightjs.css +20 -0
- package/src/styles/modal.css +58 -0
- package/src/styles/tables.css +111 -0
- package/src/styles/tooltip.css +65 -0
- package/dist/controls/index.d.ts +0 -18
- package/dist/controls/index.d.ts.map +0 -1
- package/dist/controls/index.js +0 -18
- package/dist/crawl.d.ts +0 -62
- package/dist/crawl.d.ts.map +0 -1
- package/dist/crawl.js +0 -172
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/src/controls/index.ts +0 -19
- package/src/crawl.ts +0 -227
- package/src/index.ts +0 -46
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { DynamicControlForm } from '$components/forms';
|
|
6
|
+
import type { ControlSchema, ValidationResult } from '$lib/form-types';
|
|
7
|
+
import type { Control } from '$lib/types';
|
|
8
|
+
import { mappings } from '$stores/compliance';
|
|
9
|
+
import { Close } from 'carbon-icons-svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
control: Control;
|
|
13
|
+
schema: ControlSchema;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
onSave: (control: Control) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { control, schema, onClose, onSave }: Props = $props();
|
|
19
|
+
|
|
20
|
+
let editedControl = $state({ ...control });
|
|
21
|
+
let activeTab = $state('details');
|
|
22
|
+
let validationResult = $state<ValidationResult | null>(null);
|
|
23
|
+
|
|
24
|
+
function handleSave() {
|
|
25
|
+
// Validate before saving if we have validation results
|
|
26
|
+
if (!validationResult?.valid) {
|
|
27
|
+
// Show validation errors
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
onSave(editedControl);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleValidation(result: ValidationResult) {
|
|
34
|
+
validationResult = result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseCCIsFromNarrative(narrative: string): string[] {
|
|
38
|
+
const cciPattern = /CCI-(\d{6})/g;
|
|
39
|
+
const matches = narrative.match(cciPattern);
|
|
40
|
+
return matches ? [...new Set(matches)] : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let ccisInNarrative = $derived(
|
|
44
|
+
parseCCIsFromNarrative(editedControl['control-implementation-narrative'] || '')
|
|
45
|
+
);
|
|
46
|
+
let associatedMappings = $derived($mappings.filter((m) => m.control_id === control.id));
|
|
47
|
+
|
|
48
|
+
// Group fields by section for tabbed interface
|
|
49
|
+
const detailsFields = $derived(
|
|
50
|
+
schema.fields.filter((f) => f.group === 'identification' || f.group === 'description')
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const implementationFields = $derived(schema.fields.filter((f) => f.group === 'implementation'));
|
|
54
|
+
|
|
55
|
+
const complianceFields = $derived(schema.fields.filter((f) => f.group === 'compliance'));
|
|
56
|
+
|
|
57
|
+
// Count validation errors per tab
|
|
58
|
+
const detailsErrors = $derived(
|
|
59
|
+
validationResult?.errors.filter((e) => detailsFields.some((f) => f.id === e.field)).length || 0
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const implementationErrors = $derived(
|
|
63
|
+
validationResult?.errors.filter((e) => implementationFields.some((f) => f.id === e.field))
|
|
64
|
+
.length || 0
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const complianceErrors = $derived(
|
|
68
|
+
validationResult?.errors.filter((e) => complianceFields.some((f) => f.id === e.field)).length ||
|
|
69
|
+
0
|
|
70
|
+
);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<!-- Modal Backdrop -->
|
|
74
|
+
<div
|
|
75
|
+
class="fixed inset-0 bg-gray-700 dark:bg-gray-900 bg-opacity-50 dark:bg-opacity-75 overflow-y-auto h-full w-full z-50"
|
|
76
|
+
role="dialog"
|
|
77
|
+
aria-modal="true"
|
|
78
|
+
tabindex="-1"
|
|
79
|
+
onclick={onClose}
|
|
80
|
+
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
|
81
|
+
>
|
|
82
|
+
<!-- Modal -->
|
|
83
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
84
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
85
|
+
<div
|
|
86
|
+
class="relative min-h-screen w-full p-6 bg-white dark:bg-gray-900"
|
|
87
|
+
role="document"
|
|
88
|
+
onclick={(e) => e.stopPropagation()}
|
|
89
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
90
|
+
>
|
|
91
|
+
<!-- Header -->
|
|
92
|
+
<div
|
|
93
|
+
class="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-gray-700"
|
|
94
|
+
>
|
|
95
|
+
<div>
|
|
96
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
97
|
+
Edit Control: {control['control-acronym']}
|
|
98
|
+
</h3>
|
|
99
|
+
{#if !validationResult?.valid && validationResult?.errors.length}
|
|
100
|
+
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
|
|
101
|
+
{validationResult.errors.length} validation error{validationResult.errors.length === 1
|
|
102
|
+
? ''
|
|
103
|
+
: 's'} found
|
|
104
|
+
</p>
|
|
105
|
+
{/if}
|
|
106
|
+
</div>
|
|
107
|
+
<button
|
|
108
|
+
onclick={onClose}
|
|
109
|
+
class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
|
110
|
+
aria-label="Close modal"
|
|
111
|
+
>
|
|
112
|
+
<Close class="w-6 h-6" />
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Tabs -->
|
|
117
|
+
<div class="mt-4">
|
|
118
|
+
<nav class="flex space-x-8">
|
|
119
|
+
<button
|
|
120
|
+
onclick={() => (activeTab = 'details')}
|
|
121
|
+
class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'details'
|
|
122
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
123
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
124
|
+
>
|
|
125
|
+
Details
|
|
126
|
+
{#if detailsErrors > 0}
|
|
127
|
+
<span class="ml-1 bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
|
128
|
+
{detailsErrors}
|
|
129
|
+
</span>
|
|
130
|
+
{/if}
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
onclick={() => (activeTab = 'implementation')}
|
|
134
|
+
class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'implementation'
|
|
135
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
136
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
137
|
+
>
|
|
138
|
+
Implementation
|
|
139
|
+
{#if implementationErrors > 0}
|
|
140
|
+
<span class="ml-1 bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
|
141
|
+
{implementationErrors}
|
|
142
|
+
</span>
|
|
143
|
+
{/if}
|
|
144
|
+
</button>
|
|
145
|
+
<button
|
|
146
|
+
onclick={() => (activeTab = 'compliance')}
|
|
147
|
+
class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'compliance'
|
|
148
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
149
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
150
|
+
>
|
|
151
|
+
Compliance
|
|
152
|
+
{#if complianceErrors > 0}
|
|
153
|
+
<span class="ml-1 bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
|
154
|
+
{complianceErrors}
|
|
155
|
+
</span>
|
|
156
|
+
{/if}
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
onclick={() => (activeTab = 'mappings')}
|
|
160
|
+
class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'mappings'
|
|
161
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
162
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
163
|
+
>
|
|
164
|
+
Mappings ({associatedMappings.length})
|
|
165
|
+
</button>
|
|
166
|
+
</nav>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Tab Content -->
|
|
170
|
+
<div class="mt-6 max-h-screen-3/4 overflow-y-auto">
|
|
171
|
+
{#if activeTab === 'details'}
|
|
172
|
+
<DynamicControlForm
|
|
173
|
+
bind:control={editedControl}
|
|
174
|
+
schema={{
|
|
175
|
+
...schema,
|
|
176
|
+
fields: detailsFields
|
|
177
|
+
}}
|
|
178
|
+
onValidation={handleValidation}
|
|
179
|
+
/>
|
|
180
|
+
{:else if activeTab === 'implementation'}
|
|
181
|
+
<DynamicControlForm
|
|
182
|
+
bind:control={editedControl}
|
|
183
|
+
schema={{
|
|
184
|
+
...schema,
|
|
185
|
+
fields: implementationFields
|
|
186
|
+
}}
|
|
187
|
+
onValidation={handleValidation}
|
|
188
|
+
/>
|
|
189
|
+
|
|
190
|
+
{#if ccisInNarrative.length > 0}
|
|
191
|
+
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
192
|
+
<div class="block text-sm font-medium text-blue-700 dark:text-blue-300 mb-2">
|
|
193
|
+
CCIs Found in Narrative
|
|
194
|
+
</div>
|
|
195
|
+
<div class="flex flex-wrap gap-2">
|
|
196
|
+
{#each ccisInNarrative as cci}
|
|
197
|
+
<span
|
|
198
|
+
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200"
|
|
199
|
+
>
|
|
200
|
+
{cci}
|
|
201
|
+
</span>
|
|
202
|
+
{/each}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
206
|
+
{:else if activeTab === 'compliance'}
|
|
207
|
+
<DynamicControlForm
|
|
208
|
+
bind:control={editedControl}
|
|
209
|
+
schema={{
|
|
210
|
+
...schema,
|
|
211
|
+
fields: complianceFields
|
|
212
|
+
}}
|
|
213
|
+
onValidation={handleValidation}
|
|
214
|
+
/>
|
|
215
|
+
{:else if activeTab === 'mappings'}
|
|
216
|
+
<div class="space-y-4">
|
|
217
|
+
{#if associatedMappings.length > 0}
|
|
218
|
+
{#each associatedMappings as mapping}
|
|
219
|
+
<div
|
|
220
|
+
class="border border-gray-200 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-gray-800"
|
|
221
|
+
>
|
|
222
|
+
<div class="flex justify-between items-start mb-2">
|
|
223
|
+
<span class="text-sm font-medium text-gray-900 dark:text-white"
|
|
224
|
+
>@lula {mapping.uuid}</span
|
|
225
|
+
>
|
|
226
|
+
<button
|
|
227
|
+
onclick={() => navigator.clipboard.writeText(`@lula ${mapping.uuid}`)}
|
|
228
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
|
|
229
|
+
>
|
|
230
|
+
Copy UUID
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">{mapping.justification}</p>
|
|
234
|
+
<div
|
|
235
|
+
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
|
|
236
|
+
>
|
|
237
|
+
<span>Status: {mapping.status}</span>
|
|
238
|
+
<span>By: {mapping.created_by}</span>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
{/each}
|
|
242
|
+
{:else}
|
|
243
|
+
<div class="text-center py-8">
|
|
244
|
+
<svg
|
|
245
|
+
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
|
|
246
|
+
fill="none"
|
|
247
|
+
viewBox="0 0 24 24"
|
|
248
|
+
stroke="currentColor"
|
|
249
|
+
>
|
|
250
|
+
<path
|
|
251
|
+
stroke-linecap="round"
|
|
252
|
+
stroke-linejoin="round"
|
|
253
|
+
stroke-width="2"
|
|
254
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
255
|
+
/>
|
|
256
|
+
</svg>
|
|
257
|
+
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No mappings</h3>
|
|
258
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
259
|
+
This control has no associated mappings yet.
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
264
|
+
{/if}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- Footer -->
|
|
268
|
+
<div
|
|
269
|
+
class="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700 mt-6"
|
|
270
|
+
>
|
|
271
|
+
<!-- Schema Info -->
|
|
272
|
+
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
273
|
+
Schema: {schema.name} v{schema.version}
|
|
274
|
+
<span class="ml-2 text-green-600 dark:text-green-400">Dynamic Forms</span>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<!-- Action Buttons -->
|
|
278
|
+
<div class="flex space-x-3">
|
|
279
|
+
<button
|
|
280
|
+
onclick={onClose}
|
|
281
|
+
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
282
|
+
>
|
|
283
|
+
Cancel
|
|
284
|
+
</button>
|
|
285
|
+
<button
|
|
286
|
+
onclick={handleSave}
|
|
287
|
+
disabled={!validationResult?.valid}
|
|
288
|
+
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
289
|
+
>
|
|
290
|
+
Save Changes
|
|
291
|
+
{#if !validationResult?.valid && validationResult?.errors.length}
|
|
292
|
+
<span class="ml-1">({validationResult.errors.length} errors)</span>
|
|
293
|
+
{/if}
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Mapping } from '$lib/types';
|
|
6
|
+
import { StatusBadge } from '../ui';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
mapping: Mapping;
|
|
10
|
+
onEdit?: (mapping: Mapping) => void;
|
|
11
|
+
onDelete?: (uuid: string) => void;
|
|
12
|
+
showActions?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { mapping, onEdit, onDelete, showActions = false }: Props = $props();
|
|
16
|
+
|
|
17
|
+
function handleCopyUuid() {
|
|
18
|
+
navigator.clipboard.writeText(`@lula ${mapping.uuid}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleEdit() {
|
|
22
|
+
onEdit?.(mapping);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleDelete() {
|
|
26
|
+
if (confirm('Are you sure you want to delete this mapping?')) {
|
|
27
|
+
onDelete?.(mapping.uuid);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div
|
|
33
|
+
class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200"
|
|
34
|
+
>
|
|
35
|
+
<!-- Header -->
|
|
36
|
+
<div
|
|
37
|
+
class="bg-gray-100 dark:bg-gray-900 rounded-t-xl px-6 py-4 border-b border-gray-200 dark:border-gray-700"
|
|
38
|
+
>
|
|
39
|
+
<div class="flex justify-between items-center">
|
|
40
|
+
<span
|
|
41
|
+
class="text-xs font-mono font-medium text-gray-600 dark:text-gray-300"
|
|
42
|
+
>
|
|
43
|
+
{mapping.uuid}
|
|
44
|
+
</span>
|
|
45
|
+
<div class="flex items-center space-x-2">
|
|
46
|
+
<button
|
|
47
|
+
onclick={handleCopyUuid}
|
|
48
|
+
class="inline-flex items-center px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-200 shadow-sm hover:shadow"
|
|
49
|
+
title="Copy UUID to clipboard"
|
|
50
|
+
>
|
|
51
|
+
Copy UUID
|
|
52
|
+
</button>
|
|
53
|
+
{#if showActions}
|
|
54
|
+
<button
|
|
55
|
+
onclick={handleEdit}
|
|
56
|
+
class="inline-flex items-center px-3 py-2 text-xs font-medium text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-100 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-all duration-200 shadow-sm hover:shadow"
|
|
57
|
+
title="Edit mapping"
|
|
58
|
+
>
|
|
59
|
+
Edit
|
|
60
|
+
</button>
|
|
61
|
+
<button
|
|
62
|
+
onclick={handleDelete}
|
|
63
|
+
class="inline-flex items-center px-3 py-2 text-xs font-medium text-red-600 dark:text-red-300 hover:text-red-800 dark:hover:text-red-100 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/50 transition-all duration-200 shadow-sm hover:shadow"
|
|
64
|
+
title="Delete mapping"
|
|
65
|
+
>
|
|
66
|
+
Delete
|
|
67
|
+
</button>
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Content -->
|
|
74
|
+
<div class="px-6 py-4">
|
|
75
|
+
<p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
|
76
|
+
{mapping.justification}
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
{#if mapping.source_entries && mapping.source_entries.length > 0}
|
|
80
|
+
<div class="mb-4">
|
|
81
|
+
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">Source References</h4>
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
{#each mapping.source_entries as entry}
|
|
84
|
+
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
85
|
+
<div class="flex items-start justify-between">
|
|
86
|
+
<span class="text-xs font-mono text-blue-600 dark:text-blue-400 break-all">
|
|
87
|
+
{entry.location}
|
|
88
|
+
</span>
|
|
89
|
+
{#if entry.shasum}
|
|
90
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2" title="SHA checksum">
|
|
91
|
+
{entry.shasum.substring(0, 8)}...
|
|
92
|
+
</span>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
{/each}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<div class="flex items-center justify-start">
|
|
102
|
+
<StatusBadge status={mapping.status} type="mapping" />
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { SourceEntry } from '$lib/types';
|
|
6
|
+
import { Add, TrashCan } from 'carbon-icons-svelte';
|
|
7
|
+
import FormField from '../forms/FormField.svelte';
|
|
8
|
+
|
|
9
|
+
interface MappingFormData {
|
|
10
|
+
justification: string;
|
|
11
|
+
status: 'planned' | 'implemented' | 'verified';
|
|
12
|
+
source_entries: SourceEntry[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
initialData?: Partial<MappingFormData>;
|
|
17
|
+
onSubmit: (data: MappingFormData) => void;
|
|
18
|
+
onCancel: () => void;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
submitLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
initialData = {},
|
|
25
|
+
onSubmit,
|
|
26
|
+
onCancel,
|
|
27
|
+
loading = false,
|
|
28
|
+
submitLabel = 'Create Mapping'
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
let formData = $state<MappingFormData>({
|
|
32
|
+
justification: initialData.justification || '',
|
|
33
|
+
status: initialData.status || 'planned',
|
|
34
|
+
source_entries: initialData.source_entries || []
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let newLocation = $state('');
|
|
38
|
+
let newShasum = $state('');
|
|
39
|
+
|
|
40
|
+
const statusOptions = ['planned', 'implemented', 'verified'];
|
|
41
|
+
|
|
42
|
+
const isValid = $derived(formData.justification.trim().length > 0);
|
|
43
|
+
|
|
44
|
+
function handleSubmit() {
|
|
45
|
+
if (!isValid || loading) return;
|
|
46
|
+
onSubmit(formData);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleCancel() {
|
|
50
|
+
// Reset form
|
|
51
|
+
formData = {
|
|
52
|
+
justification: initialData.justification || '',
|
|
53
|
+
status: initialData.status || 'planned',
|
|
54
|
+
source_entries: initialData.source_entries || []
|
|
55
|
+
};
|
|
56
|
+
newLocation = '';
|
|
57
|
+
newShasum = '';
|
|
58
|
+
onCancel();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addSourceEntry() {
|
|
62
|
+
if (newLocation.trim()) {
|
|
63
|
+
const entry: SourceEntry = {
|
|
64
|
+
location: newLocation.trim()
|
|
65
|
+
};
|
|
66
|
+
if (newShasum.trim()) {
|
|
67
|
+
entry.shasum = newShasum.trim();
|
|
68
|
+
}
|
|
69
|
+
formData.source_entries = [...formData.source_entries, entry];
|
|
70
|
+
newLocation = '';
|
|
71
|
+
newShasum = '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function removeSourceEntry(index: number) {
|
|
76
|
+
formData.source_entries = formData.source_entries.filter((_, i) => i !== index);
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
|
|
81
|
+
<div class="space-y-8">
|
|
82
|
+
<!-- Main form fields -->
|
|
83
|
+
<div class="grid grid-cols-1 gap-8">
|
|
84
|
+
<FormField
|
|
85
|
+
id="mapping-justification"
|
|
86
|
+
label="Justification"
|
|
87
|
+
type="textarea"
|
|
88
|
+
bind:value={formData.justification}
|
|
89
|
+
rows={4}
|
|
90
|
+
placeholder="Explain how this compliance artifact satisfies the control requirements..."
|
|
91
|
+
required
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<FormField
|
|
95
|
+
id="mapping-status"
|
|
96
|
+
label="Implementation Status"
|
|
97
|
+
type="select"
|
|
98
|
+
bind:value={formData.status}
|
|
99
|
+
options={statusOptions}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Source References -->
|
|
104
|
+
<div class="space-y-4">
|
|
105
|
+
<div class="flex items-center justify-between">
|
|
106
|
+
<div class="text-sm font-medium text-gray-900 dark:text-white">Source References</div>
|
|
107
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">Optional</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Existing entries -->
|
|
111
|
+
{#if formData.source_entries.length > 0}
|
|
112
|
+
<div class="space-y-3">
|
|
113
|
+
{#each formData.source_entries as entry, index}
|
|
114
|
+
<div class="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
|
|
115
|
+
<div class="flex-1 min-w-0">
|
|
116
|
+
<div class="text-sm font-mono text-gray-900 dark:text-white break-all">
|
|
117
|
+
{entry.location}
|
|
118
|
+
</div>
|
|
119
|
+
{#if entry.shasum}
|
|
120
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
121
|
+
SHA: {entry.shasum.substring(0, 8)}...
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
</div>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onclick={() => removeSourceEntry(index)}
|
|
128
|
+
class="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors"
|
|
129
|
+
title="Remove reference"
|
|
130
|
+
>
|
|
131
|
+
<TrashCan size={16} />
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
{/each}
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
|
|
138
|
+
<!-- Add new entry -->
|
|
139
|
+
<div class="grid grid-cols-1 gap-4">
|
|
140
|
+
<input
|
|
141
|
+
type="text"
|
|
142
|
+
bind:value={newLocation}
|
|
143
|
+
placeholder="File path or URI (e.g., src/auth/handler.ts:42)"
|
|
144
|
+
class="w-full px-4 py-3 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
145
|
+
/>
|
|
146
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
bind:value={newShasum}
|
|
150
|
+
placeholder="SHA checksum (optional)"
|
|
151
|
+
class="px-4 py-3 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
152
|
+
/>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onclick={addSourceEntry}
|
|
156
|
+
disabled={!newLocation.trim()}
|
|
157
|
+
class="px-4 py-3 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
|
|
158
|
+
>
|
|
159
|
+
<Add size={16} />
|
|
160
|
+
Add Reference
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Action buttons -->
|
|
167
|
+
<div class="flex justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onclick={handleCancel}
|
|
171
|
+
disabled={loading}
|
|
172
|
+
class="px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 disabled:opacity-50 transition-all duration-200"
|
|
173
|
+
>
|
|
174
|
+
Cancel
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onclick={handleSubmit}
|
|
179
|
+
disabled={!isValid || loading}
|
|
180
|
+
class="px-6 py-2.5 text-sm font-medium rounded-lg transition-all duration-200 {isValid && !loading
|
|
181
|
+
? 'text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2'
|
|
182
|
+
: 'text-gray-400 bg-gray-200 dark:bg-gray-700 cursor-not-allowed'}"
|
|
183
|
+
>
|
|
184
|
+
{loading ? 'Saving...' : submitLabel}
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
export { default as ControlDetailsPanel } from './ControlDetailsPanel.svelte';
|
|
5
|
+
export { default as ControlsList } from './ControlsList.svelte';
|
|
6
|
+
export { default as DynamicControlEditor } from './DynamicControlEditor.svelte';
|
|
7
|
+
export { default as MappingCard } from './MappingCard.svelte';
|
|
8
|
+
export { default as MappingForm } from './MappingForm.svelte';
|
|
9
|
+
|