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,428 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { formatValue } from '$lib/formatUtils';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
yamlDiff: any; // YamlDiffResult
|
|
9
|
+
showToggle?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { yamlDiff, showToggle = true }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let showDetailedView = $state(false); // Default to summary/compact view
|
|
15
|
+
|
|
16
|
+
function getChangeIcon(type: string) {
|
|
17
|
+
switch (type) {
|
|
18
|
+
case 'added':
|
|
19
|
+
return '+';
|
|
20
|
+
case 'removed':
|
|
21
|
+
return '-';
|
|
22
|
+
case 'modified':
|
|
23
|
+
return '~';
|
|
24
|
+
default:
|
|
25
|
+
return '•';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getChangeColor(type: string) {
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'added':
|
|
32
|
+
return 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20';
|
|
33
|
+
case 'removed':
|
|
34
|
+
return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20';
|
|
35
|
+
case 'modified':
|
|
36
|
+
return 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20';
|
|
37
|
+
default:
|
|
38
|
+
return 'text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900/20';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Function to highlight character differences in text
|
|
43
|
+
function getTextDiff(oldText: string, newText: string) {
|
|
44
|
+
if (!oldText && !newText) return { oldHighlighted: '', newHighlighted: '' };
|
|
45
|
+
if (!oldText) return { oldHighlighted: '', newHighlighted: escapeHtml(newText) };
|
|
46
|
+
if (!newText) return { oldHighlighted: escapeHtml(oldText), newHighlighted: '' };
|
|
47
|
+
|
|
48
|
+
const oldStr = String(oldText);
|
|
49
|
+
const newStr = String(newText);
|
|
50
|
+
|
|
51
|
+
// Split on word boundaries, keeping separators
|
|
52
|
+
const oldTokens = oldStr.split(/(\s+)/);
|
|
53
|
+
const newTokens = newStr.split(/(\s+)/);
|
|
54
|
+
|
|
55
|
+
let oldHighlighted = '';
|
|
56
|
+
let newHighlighted = '';
|
|
57
|
+
|
|
58
|
+
// Simple LCS-style alignment
|
|
59
|
+
const oldLen = oldTokens.length;
|
|
60
|
+
const newLen = newTokens.length;
|
|
61
|
+
|
|
62
|
+
// Create a table to track matches
|
|
63
|
+
const dp = Array(oldLen + 1)
|
|
64
|
+
.fill(null)
|
|
65
|
+
.map(() => Array(newLen + 1).fill(0));
|
|
66
|
+
|
|
67
|
+
// Fill LCS table
|
|
68
|
+
for (let i = 1; i <= oldLen; i++) {
|
|
69
|
+
for (let j = 1; j <= newLen; j++) {
|
|
70
|
+
if (oldTokens[i - 1] === newTokens[j - 1]) {
|
|
71
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
72
|
+
} else {
|
|
73
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Trace back to find the actual differences
|
|
79
|
+
let i = oldLen,
|
|
80
|
+
j = newLen;
|
|
81
|
+
const oldChanges = [];
|
|
82
|
+
const newChanges = [];
|
|
83
|
+
|
|
84
|
+
while (i > 0 || j > 0) {
|
|
85
|
+
if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
|
|
86
|
+
// Tokens match
|
|
87
|
+
oldChanges.unshift({ token: oldTokens[i - 1], changed: false });
|
|
88
|
+
newChanges.unshift({ token: newTokens[j - 1], changed: false });
|
|
89
|
+
i--;
|
|
90
|
+
j--;
|
|
91
|
+
} else if (i > 0 && (j === 0 || dp[i - 1][j] >= dp[i][j - 1])) {
|
|
92
|
+
// Token was deleted
|
|
93
|
+
oldChanges.unshift({ token: oldTokens[i - 1], changed: true });
|
|
94
|
+
i--;
|
|
95
|
+
} else if (j > 0) {
|
|
96
|
+
// Token was added
|
|
97
|
+
newChanges.unshift({ token: newTokens[j - 1], changed: true });
|
|
98
|
+
j--;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build highlighted strings
|
|
103
|
+
oldHighlighted = oldChanges
|
|
104
|
+
.map(({ token, changed }) =>
|
|
105
|
+
changed
|
|
106
|
+
? `<mark class="bg-red-200 dark:bg-red-800 text-red-900 dark:text-red-100 px-0.5 rounded">${escapeHtml(token)}</mark>`
|
|
107
|
+
: escapeHtml(token)
|
|
108
|
+
)
|
|
109
|
+
.join('');
|
|
110
|
+
|
|
111
|
+
newHighlighted = newChanges
|
|
112
|
+
.map(({ token, changed }) =>
|
|
113
|
+
changed
|
|
114
|
+
? `<mark class="bg-green-200 dark:bg-green-800 text-green-900 dark:text-green-100 px-0.5 rounded">${escapeHtml(token)}</mark>`
|
|
115
|
+
: escapeHtml(token)
|
|
116
|
+
)
|
|
117
|
+
.join('');
|
|
118
|
+
|
|
119
|
+
return { oldHighlighted, newHighlighted };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Helper function to escape HTML
|
|
123
|
+
function escapeHtml(text: string): string {
|
|
124
|
+
const div = document.createElement('div');
|
|
125
|
+
div.textContent = text;
|
|
126
|
+
return div.innerHTML;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Helper function to truncate text while preserving highlights
|
|
130
|
+
function truncateWithHighlight(htmlText: string, maxLength: number): string {
|
|
131
|
+
if (!htmlText) return '';
|
|
132
|
+
|
|
133
|
+
// Remove HTML tags to check actual text length
|
|
134
|
+
const tempDiv = document.createElement('div');
|
|
135
|
+
tempDiv.innerHTML = htmlText;
|
|
136
|
+
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
137
|
+
|
|
138
|
+
if (textContent.length <= maxLength) {
|
|
139
|
+
return htmlText;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If we need to truncate and there are highlights, find the best section
|
|
143
|
+
if (htmlText.includes('<mark')) {
|
|
144
|
+
// Find all highlights and their positions
|
|
145
|
+
const markRegex = /<mark[^>]*>([^<]*)<\/mark>/g;
|
|
146
|
+
const highlights = [];
|
|
147
|
+
let match;
|
|
148
|
+
|
|
149
|
+
while ((match = markRegex.exec(htmlText)) !== null) {
|
|
150
|
+
highlights.push({
|
|
151
|
+
start: match.index,
|
|
152
|
+
end: match.index + match[0].length,
|
|
153
|
+
text: match[1],
|
|
154
|
+
fullMatch: match[0]
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (highlights.length > 0) {
|
|
159
|
+
// Find the first substantial highlight (not just whitespace)
|
|
160
|
+
const goodHighlight = highlights.find((h) => h.text.trim().length > 0) || highlights[0];
|
|
161
|
+
|
|
162
|
+
// Calculate context window around this highlight
|
|
163
|
+
const contextSize = Math.floor((maxLength - goodHighlight.text.length) / 2);
|
|
164
|
+
|
|
165
|
+
// Find text boundaries in the original HTML
|
|
166
|
+
const beforeText = htmlText.substring(0, goodHighlight.start);
|
|
167
|
+
const afterText = htmlText.substring(goodHighlight.end);
|
|
168
|
+
|
|
169
|
+
// Convert HTML positions to text positions for better calculation
|
|
170
|
+
const beforeDiv = document.createElement('div');
|
|
171
|
+
beforeDiv.innerHTML = beforeText;
|
|
172
|
+
const beforePlainText = beforeDiv.textContent || '';
|
|
173
|
+
|
|
174
|
+
const afterDiv = document.createElement('div');
|
|
175
|
+
afterDiv.innerHTML = afterText;
|
|
176
|
+
const afterPlainText = afterDiv.textContent || '';
|
|
177
|
+
|
|
178
|
+
// Calculate how much context to show
|
|
179
|
+
const beforeContextLength = Math.min(beforePlainText.length, contextSize);
|
|
180
|
+
const afterContextLength = Math.min(afterPlainText.length, contextSize);
|
|
181
|
+
|
|
182
|
+
// Find word boundaries for cleaner truncation
|
|
183
|
+
const beforeStart = Math.max(0, beforePlainText.length - beforeContextLength);
|
|
184
|
+
const afterEnd = Math.min(afterPlainText.length, afterContextLength);
|
|
185
|
+
|
|
186
|
+
// Find corresponding positions in HTML (simplified approach)
|
|
187
|
+
let beforeTruncated = '';
|
|
188
|
+
let afterTruncated = '';
|
|
189
|
+
|
|
190
|
+
if (beforeStart < beforePlainText.length) {
|
|
191
|
+
// Try to find a reasonable cut point in the HTML
|
|
192
|
+
const ratio = beforeStart / beforePlainText.length;
|
|
193
|
+
const htmlCutPoint = Math.floor(beforeText.length * ratio);
|
|
194
|
+
beforeTruncated = '...' + beforeText.substring(htmlCutPoint);
|
|
195
|
+
} else {
|
|
196
|
+
beforeTruncated = beforeText;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (afterEnd < afterPlainText.length) {
|
|
200
|
+
const ratio = afterEnd / afterPlainText.length;
|
|
201
|
+
const htmlCutPoint = Math.floor(afterText.length * ratio);
|
|
202
|
+
afterTruncated = afterText.substring(0, htmlCutPoint) + '...';
|
|
203
|
+
} else {
|
|
204
|
+
afterTruncated = afterText;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return beforeTruncated + goodHighlight.fullMatch + afterTruncated;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fallback: simple word-boundary truncation
|
|
212
|
+
const words = textContent.split(/\s+/);
|
|
213
|
+
let result = '';
|
|
214
|
+
let currentLength = 0;
|
|
215
|
+
|
|
216
|
+
for (const word of words) {
|
|
217
|
+
if (currentLength + word.length + (result ? 1 : 0) > maxLength) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (result) result += ' ';
|
|
221
|
+
result += word;
|
|
222
|
+
currentLength += word.length + (result.length > word.length ? 1 : 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return escapeHtml(result) + (currentLength < textContent.length ? '...' : '');
|
|
226
|
+
}
|
|
227
|
+
</script>
|
|
228
|
+
|
|
229
|
+
<div
|
|
230
|
+
class="yaml-diff-viewer bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden"
|
|
231
|
+
>
|
|
232
|
+
<div class="bg-gray-100 dark:bg-gray-900 px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
|
233
|
+
<div class="flex items-center justify-between">
|
|
234
|
+
<div class="flex items-center space-x-2">
|
|
235
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
236
|
+
<path
|
|
237
|
+
stroke-linecap="round"
|
|
238
|
+
stroke-linejoin="round"
|
|
239
|
+
stroke-width="2"
|
|
240
|
+
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
|
241
|
+
/>
|
|
242
|
+
</svg>
|
|
243
|
+
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Changes</span>
|
|
244
|
+
<span
|
|
245
|
+
class="text-xs text-gray-500 dark:text-gray-400 bg-gray-200 dark:bg-gray-700 px-2 py-0.5 rounded-full"
|
|
246
|
+
>
|
|
247
|
+
{yamlDiff?.summary || 'No changes'}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{#if showToggle}
|
|
252
|
+
<button
|
|
253
|
+
onclick={() => (showDetailedView = !showDetailedView)}
|
|
254
|
+
class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
|
255
|
+
>
|
|
256
|
+
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
257
|
+
<path
|
|
258
|
+
stroke-linecap="round"
|
|
259
|
+
stroke-linejoin="round"
|
|
260
|
+
stroke-width="2"
|
|
261
|
+
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
262
|
+
/>
|
|
263
|
+
</svg>
|
|
264
|
+
{showDetailedView ? 'Summary' : 'Details'}
|
|
265
|
+
</button>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="max-h-96 overflow-y-auto">
|
|
271
|
+
{#if yamlDiff?.changes && yamlDiff.changes.length > 0}
|
|
272
|
+
{#if showDetailedView}
|
|
273
|
+
<!-- Detailed view with full context -->
|
|
274
|
+
<div class="divide-y divide-gray-200 dark:divide-gray-600">
|
|
275
|
+
{#each yamlDiff.changes as change}
|
|
276
|
+
<div class="p-3 {getChangeColor(change.type)}">
|
|
277
|
+
<div class="flex items-start space-x-3">
|
|
278
|
+
<div
|
|
279
|
+
class="flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
|
280
|
+
>
|
|
281
|
+
{getChangeIcon(change.type)}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="flex-1 min-w-0">
|
|
285
|
+
<div class="flex items-center space-x-2 mb-1">
|
|
286
|
+
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
|
287
|
+
{change.description}
|
|
288
|
+
</span>
|
|
289
|
+
<span
|
|
290
|
+
class="text-xs text-gray-500 dark:text-gray-400 font-mono bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded"
|
|
291
|
+
>
|
|
292
|
+
{change.path}
|
|
293
|
+
</span>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{#if change.type === 'modified'}
|
|
297
|
+
<div class="text-sm space-y-1">
|
|
298
|
+
<div class="flex items-start space-x-2">
|
|
299
|
+
<span class="text-red-600 dark:text-red-400 font-mono text-xs">-</span>
|
|
300
|
+
<code
|
|
301
|
+
class="text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/30 px-1 py-0.5 rounded text-xs"
|
|
302
|
+
>
|
|
303
|
+
{formatValue(change.oldValue)}
|
|
304
|
+
</code>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="flex items-start space-x-2">
|
|
307
|
+
<span class="text-green-600 dark:text-green-400 font-mono text-xs">+</span>
|
|
308
|
+
<code
|
|
309
|
+
class="text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/30 px-1 py-0.5 rounded text-xs"
|
|
310
|
+
>
|
|
311
|
+
{formatValue(change.newValue)}
|
|
312
|
+
</code>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
{:else if change.type === 'added'}
|
|
316
|
+
<div class="text-sm">
|
|
317
|
+
<code
|
|
318
|
+
class="text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/30 px-1 py-0.5 rounded text-xs"
|
|
319
|
+
>
|
|
320
|
+
{formatValue(change.newValue)}
|
|
321
|
+
</code>
|
|
322
|
+
</div>
|
|
323
|
+
{:else if change.type === 'removed'}
|
|
324
|
+
<div class="text-sm">
|
|
325
|
+
<code
|
|
326
|
+
class="text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/30 px-1 py-0.5 rounded text-xs"
|
|
327
|
+
>
|
|
328
|
+
{formatValue(change.oldValue)}
|
|
329
|
+
</code>
|
|
330
|
+
</div>
|
|
331
|
+
{/if}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
{/each}
|
|
336
|
+
</div>
|
|
337
|
+
{:else}
|
|
338
|
+
<!-- Summary view - more compact -->
|
|
339
|
+
<div class="p-4 space-y-2">
|
|
340
|
+
{#each yamlDiff.changes as change}
|
|
341
|
+
<div class="flex items-center space-x-2 text-sm">
|
|
342
|
+
<span
|
|
343
|
+
class="w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold {getChangeColor(
|
|
344
|
+
change.type
|
|
345
|
+
)}"
|
|
346
|
+
>
|
|
347
|
+
{getChangeIcon(change.type)}
|
|
348
|
+
</span>
|
|
349
|
+
|
|
350
|
+
<span class="font-medium text-gray-900 dark:text-white">
|
|
351
|
+
{change.description}
|
|
352
|
+
</span>
|
|
353
|
+
|
|
354
|
+
{#if change.type === 'modified' && change.oldValue !== change.newValue}
|
|
355
|
+
{@const diff = getTextDiff(
|
|
356
|
+
formatValue(change.oldValue),
|
|
357
|
+
formatValue(change.newValue)
|
|
358
|
+
)}
|
|
359
|
+
{@const oldTruncated = truncateWithHighlight(
|
|
360
|
+
diff.oldHighlighted || escapeHtml(formatValue(change.oldValue)),
|
|
361
|
+
80
|
|
362
|
+
)}
|
|
363
|
+
{@const newTruncated = truncateWithHighlight(
|
|
364
|
+
diff.newHighlighted || escapeHtml(formatValue(change.newValue)),
|
|
365
|
+
80
|
|
366
|
+
)}
|
|
367
|
+
<div class="flex items-center space-x-2 text-xs ml-2 mt-1">
|
|
368
|
+
<span class="text-red-600 dark:text-red-400 font-mono">−</span>
|
|
369
|
+
<code
|
|
370
|
+
class="text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-2 py-1 rounded"
|
|
371
|
+
>
|
|
372
|
+
{@html oldTruncated}
|
|
373
|
+
</code>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="flex items-center space-x-2 text-xs ml-2 mt-1">
|
|
376
|
+
<span class="text-green-600 dark:text-green-400 font-mono">+</span>
|
|
377
|
+
<code
|
|
378
|
+
class="text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-2 py-1 rounded"
|
|
379
|
+
>
|
|
380
|
+
{@html newTruncated}
|
|
381
|
+
</code>
|
|
382
|
+
</div>
|
|
383
|
+
{:else if change.type === 'added' && change.newValue !== undefined}
|
|
384
|
+
<div class="flex items-center space-x-1 text-xs ml-2">
|
|
385
|
+
<code
|
|
386
|
+
class="text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-1 py-0.5 rounded"
|
|
387
|
+
>
|
|
388
|
+
{typeof change.newValue === 'string' && change.newValue.length > 20
|
|
389
|
+
? change.newValue.substring(0, 20) + '...'
|
|
390
|
+
: formatValue(change.newValue)}
|
|
391
|
+
</code>
|
|
392
|
+
</div>
|
|
393
|
+
{:else if change.type === 'removed' && change.oldValue !== undefined}
|
|
394
|
+
<div class="flex items-center space-x-1 text-xs ml-2">
|
|
395
|
+
<code
|
|
396
|
+
class="text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1 py-0.5 rounded"
|
|
397
|
+
>
|
|
398
|
+
{typeof change.oldValue === 'string' && change.oldValue.length > 20
|
|
399
|
+
? change.oldValue.substring(0, 20) + '...'
|
|
400
|
+
: formatValue(change.oldValue)}
|
|
401
|
+
</code>
|
|
402
|
+
</div>
|
|
403
|
+
{/if}
|
|
404
|
+
</div>
|
|
405
|
+
{/each}
|
|
406
|
+
</div>
|
|
407
|
+
{/if}
|
|
408
|
+
{:else}
|
|
409
|
+
<div class="p-6 text-center text-gray-500 dark:text-gray-400">
|
|
410
|
+
<svg class="mx-auto h-8 w-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
411
|
+
<path
|
|
412
|
+
stroke-linecap="round"
|
|
413
|
+
stroke-linejoin="round"
|
|
414
|
+
stroke-width="2"
|
|
415
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
416
|
+
/>
|
|
417
|
+
</svg>
|
|
418
|
+
<p class="text-sm">No changes detected</p>
|
|
419
|
+
</div>
|
|
420
|
+
{/if}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<style>
|
|
425
|
+
.yaml-diff-viewer {
|
|
426
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
427
|
+
}
|
|
428
|
+
</style>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
export { default as DiffViewer } from './DiffViewer.svelte';
|
|
5
|
+
export { default as TimelineItem } from './TimelineItem.svelte';
|
|
6
|
+
export { default as YamlDiffViewer } from './YamlDiffViewer.svelte';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
export interface FieldDefinition {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
type:
|
|
8
|
+
| 'text'
|
|
9
|
+
| 'textarea'
|
|
10
|
+
| 'select'
|
|
11
|
+
| 'boolean'
|
|
12
|
+
| 'date'
|
|
13
|
+
| 'number'
|
|
14
|
+
| 'string-array'
|
|
15
|
+
| 'object-array';
|
|
16
|
+
description?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
group?: string;
|
|
20
|
+
rows?: number; // for textarea
|
|
21
|
+
options?: string[]; // for select
|
|
22
|
+
validation?: ValidationRule[];
|
|
23
|
+
helpText?: string;
|
|
24
|
+
arrayItemType?: 'string' | 'object'; // for array types
|
|
25
|
+
arraySchema?: any; // schema for object array items
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ValidationRule {
|
|
29
|
+
type: 'minLength' | 'maxLength' | 'pattern';
|
|
30
|
+
value: number; // Made required to fix TypeScript errors
|
|
31
|
+
pattern?: string;
|
|
32
|
+
message?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ControlSchema {
|
|
36
|
+
name: string;
|
|
37
|
+
version: string;
|
|
38
|
+
fields: FieldDefinition[];
|
|
39
|
+
groups?: FieldGroup[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FieldGroup {
|
|
43
|
+
id: string;
|
|
44
|
+
label: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ValidationError {
|
|
49
|
+
field: string;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ValidationResult {
|
|
54
|
+
valid: boolean;
|
|
55
|
+
errors: ValidationError[];
|
|
56
|
+
warnings: ValidationError[];
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
import * as YAML from 'yaml';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a value for display in diffs
|
|
8
|
+
*/
|
|
9
|
+
export function formatValue(value: any): string {
|
|
10
|
+
if (value === null) return 'null';
|
|
11
|
+
if (value === undefined) return 'undefined';
|
|
12
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
13
|
+
if (typeof value === 'object') {
|
|
14
|
+
return YAML.stringify(value).trim();
|
|
15
|
+
}
|
|
16
|
+
return String(value);
|
|
17
|
+
}
|
package/src/lib/index.ts
ADDED
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
export interface Control {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
family: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
sort_id?: string;
|
|
9
|
+
statement?: string;
|
|
10
|
+
guidance?: string;
|
|
11
|
+
objectives?: string[];
|
|
12
|
+
assessment_methods?: string[];
|
|
13
|
+
properties?: { [key: string]: string };
|
|
14
|
+
links?: Array<{
|
|
15
|
+
href: string;
|
|
16
|
+
rel?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
}>;
|
|
19
|
+
parameters?: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
usage?: string;
|
|
23
|
+
values?: string[];
|
|
24
|
+
guidelines?: string[];
|
|
25
|
+
constraints?: string[];
|
|
26
|
+
}>;
|
|
27
|
+
enhancements?: Control[];
|
|
28
|
+
// Allow dynamic field access for form components
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Mapping {
|
|
33
|
+
uuid: string;
|
|
34
|
+
control_id: string;
|
|
35
|
+
justification: string;
|
|
36
|
+
source_entries: SourceEntry[];
|
|
37
|
+
status: 'planned' | 'implemented' | 'verified';
|
|
38
|
+
created_by?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SourceEntry {
|
|
42
|
+
location: string;
|
|
43
|
+
shasum?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ControlWithMappings extends Control {
|
|
47
|
+
mappings: Mapping[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SearchResult {
|
|
51
|
+
controls: Control[];
|
|
52
|
+
mappings: Mapping[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Stats {
|
|
56
|
+
controls: number;
|
|
57
|
+
mappings: number;
|
|
58
|
+
families: number;
|
|
59
|
+
familyList: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FieldSchema {
|
|
63
|
+
type: string;
|
|
64
|
+
ui_type:
|
|
65
|
+
| 'short_text'
|
|
66
|
+
| 'medium_text'
|
|
67
|
+
| 'textarea'
|
|
68
|
+
| 'select'
|
|
69
|
+
| 'date'
|
|
70
|
+
| 'number'
|
|
71
|
+
| 'boolean'
|
|
72
|
+
| 'long_text';
|
|
73
|
+
is_array: boolean;
|
|
74
|
+
max_length?: number;
|
|
75
|
+
usage_count?: number;
|
|
76
|
+
usage_percentage?: number;
|
|
77
|
+
required: boolean;
|
|
78
|
+
visible: boolean;
|
|
79
|
+
show_in_table?: boolean;
|
|
80
|
+
editable: boolean;
|
|
81
|
+
display_order: number;
|
|
82
|
+
category: 'core' | 'content' | 'metadata' | 'compliance' | 'custom';
|
|
83
|
+
tab?: 'overview' | 'implementation' | 'custom' | 'hidden';
|
|
84
|
+
examples?: any[];
|
|
85
|
+
options?: string[];
|
|
86
|
+
original_name?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ControlSet {
|
|
90
|
+
id: string;
|
|
91
|
+
name: string;
|
|
92
|
+
title?: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
version?: string;
|
|
95
|
+
lastModified?: string;
|
|
96
|
+
path?: string;
|
|
97
|
+
families?: Array<{
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
control_count?: number;
|
|
101
|
+
}>;
|
|
102
|
+
project?: {
|
|
103
|
+
framework?: {
|
|
104
|
+
baseline?: string;
|
|
105
|
+
name?: string;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
statistics?: {
|
|
109
|
+
total_controls?: number;
|
|
110
|
+
families?: number;
|
|
111
|
+
};
|
|
112
|
+
metadata?: {
|
|
113
|
+
source?: string;
|
|
114
|
+
baseline?: string;
|
|
115
|
+
revision?: string;
|
|
116
|
+
[key: string]: any;
|
|
117
|
+
};
|
|
118
|
+
fieldSchema?: {
|
|
119
|
+
fields: Record<string, FieldSchema>;
|
|
120
|
+
total_controls?: number;
|
|
121
|
+
analyzed_at?: string;
|
|
122
|
+
};
|
|
123
|
+
field_schema?: {
|
|
124
|
+
fields: Record<string, FieldSchema>;
|
|
125
|
+
total_controls?: number;
|
|
126
|
+
analyzed_at?: string;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ControlSetInfo {
|
|
131
|
+
currentSet: ControlSet;
|
|
132
|
+
availableSets: ControlSet[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface GitCommit {
|
|
136
|
+
hash: string;
|
|
137
|
+
shortHash: string;
|
|
138
|
+
author: string;
|
|
139
|
+
authorEmail: string;
|
|
140
|
+
date: string;
|
|
141
|
+
message: string;
|
|
142
|
+
changes: {
|
|
143
|
+
insertions: number;
|
|
144
|
+
deletions: number;
|
|
145
|
+
files: number;
|
|
146
|
+
};
|
|
147
|
+
diff?: string; // The actual file diff
|
|
148
|
+
yamlDiff?: any; // Intelligent YAML diff (YamlDiffResult)
|
|
149
|
+
type?: string; // 'control' or 'mapping' - added by unified endpoint
|
|
150
|
+
fileType?: string; // 'Control File' or 'Mappings' - added by unified endpoint
|
|
151
|
+
isPending?: boolean; // true for uncommitted changes
|
|
152
|
+
isStaged?: boolean; // true for staged changes (optional since user doesn't care about staged vs unstaged)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface GitFileHistory {
|
|
156
|
+
filePath: string;
|
|
157
|
+
commits: GitCommit[];
|
|
158
|
+
totalCommits: number;
|
|
159
|
+
firstCommit: GitCommit | null;
|
|
160
|
+
lastCommit: GitCommit | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ControlWithHistory extends Control {
|
|
164
|
+
history?: GitFileHistory;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface UnifiedHistory {
|
|
168
|
+
commits: GitCommit[];
|
|
169
|
+
totalCommits: number;
|
|
170
|
+
controlCommits: number;
|
|
171
|
+
mappingCommits: number;
|
|
172
|
+
controlFilePath?: string;
|
|
173
|
+
mappingFilePath?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ControlCompleteData {
|
|
177
|
+
control: Control;
|
|
178
|
+
mappings: Mapping[];
|
|
179
|
+
unifiedHistory: UnifiedHistory;
|
|
180
|
+
}
|