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,359 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
import { browser } from '$app/environment';
|
|
5
|
+
import { writable } from 'svelte/store';
|
|
6
|
+
import type { Control, Mapping } from './types';
|
|
7
|
+
|
|
8
|
+
export interface WSMessage {
|
|
9
|
+
type:
|
|
10
|
+
| 'state-update'
|
|
11
|
+
| 'connected'
|
|
12
|
+
| 'error'
|
|
13
|
+
| 'metadata-update'
|
|
14
|
+
| 'controls-update'
|
|
15
|
+
| 'mappings-update'
|
|
16
|
+
| 'control-details'
|
|
17
|
+
| 'control-sets-list'
|
|
18
|
+
| 'control-updated'
|
|
19
|
+
| 'mapping-created'
|
|
20
|
+
| 'mapping-updated'
|
|
21
|
+
| 'mapping-deleted';
|
|
22
|
+
payload?: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AppState {
|
|
26
|
+
// Control set properties are spread at root level
|
|
27
|
+
id?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
version?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
fieldSchema?: any;
|
|
33
|
+
field_schema?: any;
|
|
34
|
+
control_id_field?: string;
|
|
35
|
+
project?: {
|
|
36
|
+
framework?: {
|
|
37
|
+
baseline?: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
// State properties
|
|
41
|
+
currentPath: string;
|
|
42
|
+
controls: Control[];
|
|
43
|
+
mappings: Mapping[];
|
|
44
|
+
families: string[];
|
|
45
|
+
totalControls: number;
|
|
46
|
+
totalMappings: number;
|
|
47
|
+
isConnected: boolean;
|
|
48
|
+
isSwitchingControlSet?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create a writable store for the complete app state
|
|
52
|
+
export const appState = writable<AppState>({
|
|
53
|
+
id: 'unknown',
|
|
54
|
+
name: 'Unknown Control Set',
|
|
55
|
+
currentPath: '',
|
|
56
|
+
controls: [],
|
|
57
|
+
mappings: [],
|
|
58
|
+
families: [],
|
|
59
|
+
totalControls: 0,
|
|
60
|
+
totalMappings: 0,
|
|
61
|
+
isConnected: false
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
class WebSocketClient {
|
|
65
|
+
private ws: WebSocket | null = null;
|
|
66
|
+
private reconnectTimer: number | null = null;
|
|
67
|
+
private reconnectAttempts = 0;
|
|
68
|
+
private maxReconnectAttempts = 5;
|
|
69
|
+
private reconnectDelay = 1000; // Start with 1 second
|
|
70
|
+
|
|
71
|
+
connect() {
|
|
72
|
+
if (!browser) return;
|
|
73
|
+
|
|
74
|
+
// Don't create a new connection if one already exists
|
|
75
|
+
if (
|
|
76
|
+
this.ws &&
|
|
77
|
+
(this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)
|
|
78
|
+
) {
|
|
79
|
+
console.log('WebSocket already connected or connecting');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
84
|
+
// In development, WebSocket is on port 3000, not the dev server port
|
|
85
|
+
const host =
|
|
86
|
+
browser && window.location.hostname === 'localhost' ? 'localhost:3000' : window.location.host;
|
|
87
|
+
const wsUrl = `${protocol}//${host}/ws`;
|
|
88
|
+
|
|
89
|
+
console.log('Connecting to WebSocket:', wsUrl);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
this.ws = new WebSocket(wsUrl);
|
|
93
|
+
|
|
94
|
+
this.ws.onopen = () => {
|
|
95
|
+
console.log('WebSocket connected');
|
|
96
|
+
this.reconnectAttempts = 0;
|
|
97
|
+
this.reconnectDelay = 1000;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.ws.onmessage = (event) => {
|
|
101
|
+
try {
|
|
102
|
+
const message: WSMessage = JSON.parse(event.data);
|
|
103
|
+
this.handleMessage(message);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Failed to parse WebSocket message:', error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.ws.onerror = (error) => {
|
|
110
|
+
console.error('WebSocket error:', error);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.ws.onclose = () => {
|
|
114
|
+
console.log('WebSocket disconnected');
|
|
115
|
+
appState.update((state) => ({ ...state, isConnected: false }));
|
|
116
|
+
this.ws = null;
|
|
117
|
+
this.scheduleReconnect();
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Failed to create WebSocket:', error);
|
|
121
|
+
this.scheduleReconnect();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private handleMessage(message: WSMessage) {
|
|
126
|
+
console.log('WebSocket message received:', message);
|
|
127
|
+
|
|
128
|
+
switch (message.type) {
|
|
129
|
+
case 'connected':
|
|
130
|
+
console.log('WebSocket connection confirmed');
|
|
131
|
+
appState.update((state) => ({ ...state, isConnected: true }));
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'state-update':
|
|
135
|
+
console.log('State update received');
|
|
136
|
+
// Update the entire app state with the payload
|
|
137
|
+
if (message.payload) {
|
|
138
|
+
appState.set({
|
|
139
|
+
...message.payload,
|
|
140
|
+
isConnected: true,
|
|
141
|
+
isSwitchingControlSet: false // Clear switching flag after state update
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'metadata-update':
|
|
147
|
+
console.log('Metadata update received');
|
|
148
|
+
// Update metadata and control set info
|
|
149
|
+
if (message.payload) {
|
|
150
|
+
appState.update((state) => ({
|
|
151
|
+
...state,
|
|
152
|
+
...message.payload,
|
|
153
|
+
isConnected: true,
|
|
154
|
+
isSwitchingControlSet: false // Clear switching flag
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 'controls-update':
|
|
160
|
+
console.log('Controls update received');
|
|
161
|
+
// Update just the controls array
|
|
162
|
+
if (message.payload) {
|
|
163
|
+
appState.update((state) => ({
|
|
164
|
+
...state,
|
|
165
|
+
controls: message.payload
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'mappings-update':
|
|
171
|
+
console.log('Mappings update received');
|
|
172
|
+
// Update just the mappings array
|
|
173
|
+
if (message.payload) {
|
|
174
|
+
appState.update((state) => ({
|
|
175
|
+
...state,
|
|
176
|
+
mappings: message.payload
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'control-details':
|
|
182
|
+
console.log('Control details received:', message.payload);
|
|
183
|
+
// Emit a custom event for control details
|
|
184
|
+
if (message.payload) {
|
|
185
|
+
window.dispatchEvent(
|
|
186
|
+
new CustomEvent('control-details', {
|
|
187
|
+
detail: message.payload
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case 'control-sets-list':
|
|
194
|
+
console.log('Control sets list received');
|
|
195
|
+
// Emit a custom event for control sets list
|
|
196
|
+
if (message.payload) {
|
|
197
|
+
window.dispatchEvent(
|
|
198
|
+
new CustomEvent('control-sets-list', {
|
|
199
|
+
detail: message.payload
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'control-updated':
|
|
206
|
+
console.log('Control updated successfully:', message.payload);
|
|
207
|
+
// Don't trigger any state updates - the component already has the updated data
|
|
208
|
+
// This just confirms the save was successful
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
case 'mapping-created':
|
|
212
|
+
case 'mapping-updated':
|
|
213
|
+
case 'mapping-deleted':
|
|
214
|
+
console.log(`Mapping operation successful: ${message.type}`, message.payload);
|
|
215
|
+
// Emit an event so the control details panel can refresh its mappings
|
|
216
|
+
window.dispatchEvent(
|
|
217
|
+
new CustomEvent('mappings-changed', {
|
|
218
|
+
detail: message.payload
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'error':
|
|
224
|
+
console.error('WebSocket error:', message.payload);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
default:
|
|
228
|
+
console.warn('Unknown WebSocket message type:', message.type);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private scheduleReconnect() {
|
|
233
|
+
if (this.reconnectTimer) {
|
|
234
|
+
clearTimeout(this.reconnectTimer);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
238
|
+
console.error('Max reconnection attempts reached');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.reconnectAttempts++;
|
|
243
|
+
console.log(
|
|
244
|
+
`Scheduling reconnect attempt ${this.reconnectAttempts} in ${this.reconnectDelay}ms`
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
this.reconnectTimer = window.setTimeout(() => {
|
|
248
|
+
this.connect();
|
|
249
|
+
}, this.reconnectDelay);
|
|
250
|
+
|
|
251
|
+
// Exponential backoff
|
|
252
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000); // Max 30 seconds
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
disconnect() {
|
|
256
|
+
if (this.reconnectTimer) {
|
|
257
|
+
clearTimeout(this.reconnectTimer);
|
|
258
|
+
this.reconnectTimer = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this.ws) {
|
|
262
|
+
this.ws.close();
|
|
263
|
+
this.ws = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
appState.update((state) => ({ ...state, isConnected: false }));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check if WebSocket is connected without subscribing to store
|
|
270
|
+
isConnected(): boolean {
|
|
271
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Send a command to the backend
|
|
275
|
+
async sendCommand(type: string, payload?: any) {
|
|
276
|
+
console.log(`Sending WebSocket command: ${type}`, payload);
|
|
277
|
+
|
|
278
|
+
// Wait for connection if not ready
|
|
279
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
280
|
+
console.log('WebSocket not ready, waiting for connection...');
|
|
281
|
+
try {
|
|
282
|
+
await this.waitForConnection();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Failed to connect:', error);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
290
|
+
const message = JSON.stringify({ type, payload });
|
|
291
|
+
console.log('Sending message:', message);
|
|
292
|
+
this.ws.send(message);
|
|
293
|
+
} else {
|
|
294
|
+
const error = new Error('WebSocket not connected after waiting');
|
|
295
|
+
console.error(error);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Wait for WebSocket to be connected
|
|
301
|
+
private waitForConnection(timeout = 5000): Promise<void> {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const startTime = Date.now();
|
|
304
|
+
|
|
305
|
+
const checkConnection = () => {
|
|
306
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
307
|
+
resolve();
|
|
308
|
+
} else if (Date.now() - startTime > timeout) {
|
|
309
|
+
reject(new Error('WebSocket connection timeout'));
|
|
310
|
+
} else {
|
|
311
|
+
setTimeout(checkConnection, 100);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
checkConnection();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// High-level command methods
|
|
320
|
+
async updateControl(control: Control) {
|
|
321
|
+
return this.sendCommand('update-control', control);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async createMapping(mapping: Mapping) {
|
|
325
|
+
return this.sendCommand('create-mapping', mapping);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async updateMapping(mapping: Mapping) {
|
|
329
|
+
return this.sendCommand('update-mapping', mapping);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async deleteMapping(uuid: string) {
|
|
333
|
+
return this.sendCommand('delete-mapping', { uuid });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async switchControlSet(path: string) {
|
|
337
|
+
// Set switching flag to prevent redirect
|
|
338
|
+
appState.update((state) => ({ ...state, isSwitchingControlSet: true }));
|
|
339
|
+
return this.sendCommand('switch-control-set', { path });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getControlDetails(id: string) {
|
|
343
|
+
return this.sendCommand('get-control', { id });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async scanControlSets() {
|
|
347
|
+
return this.sendCommand('scan-control-sets');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
send(message: any) {
|
|
351
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
352
|
+
this.ws.send(JSON.stringify(message));
|
|
353
|
+
} else {
|
|
354
|
+
console.warn('WebSocket not connected, cannot send message');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export const wsClient = new WebSocketClient();
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { goto } from '$app/navigation';
|
|
6
|
+
import { page } from '$app/stores';
|
|
7
|
+
import { ControlSetInfo } from '$components/control-sets';
|
|
8
|
+
import { Dropdown } from '$components/ui';
|
|
9
|
+
import { appState, wsClient } from '$lib/websocket';
|
|
10
|
+
import { Code, DocumentExport, Download, LogoGithub } from 'carbon-icons-svelte';
|
|
11
|
+
import { onDestroy, onMount } from 'svelte';
|
|
12
|
+
import '../app.css';
|
|
13
|
+
|
|
14
|
+
let { children } = $props();
|
|
15
|
+
|
|
16
|
+
let hasCheckedInitialRedirect = false;
|
|
17
|
+
|
|
18
|
+
// Export controls function
|
|
19
|
+
async function exportControls(format: string) {
|
|
20
|
+
try {
|
|
21
|
+
// Build the export URL
|
|
22
|
+
const exportUrl = `/api/export-controls?format=${format}`;
|
|
23
|
+
|
|
24
|
+
// Create a temporary link and trigger download
|
|
25
|
+
const link = document.createElement('a');
|
|
26
|
+
link.href = exportUrl;
|
|
27
|
+
link.download = ''; // Let the server set the filename
|
|
28
|
+
document.body.appendChild(link);
|
|
29
|
+
link.click();
|
|
30
|
+
document.body.removeChild(link);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Export failed:', error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onMount(() => {
|
|
37
|
+
// Connect WebSocket - this will get the initial state
|
|
38
|
+
wsClient.connect();
|
|
39
|
+
|
|
40
|
+
// Only check for redirect once on initial mount
|
|
41
|
+
// We need to wait for the WebSocket to connect and send initial state
|
|
42
|
+
let checkTimeoutId: number | null = null;
|
|
43
|
+
|
|
44
|
+
const checkForRedirect = async () => {
|
|
45
|
+
// Skip if we've already checked, we're on the setup page, or switching control sets
|
|
46
|
+
if (
|
|
47
|
+
hasCheckedInitialRedirect ||
|
|
48
|
+
$page.url.pathname === '/setup' ||
|
|
49
|
+
$appState.isSwitchingControlSet
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const state = $appState;
|
|
55
|
+
|
|
56
|
+
// If connected and we have the initial state
|
|
57
|
+
if (state.isConnected) {
|
|
58
|
+
hasCheckedInitialRedirect = true;
|
|
59
|
+
|
|
60
|
+
// Check if we have a valid control set
|
|
61
|
+
if (
|
|
62
|
+
!state.name ||
|
|
63
|
+
state.name === 'Unknown Control Set' ||
|
|
64
|
+
state.id === 'unknown' ||
|
|
65
|
+
state.id === 'default'
|
|
66
|
+
) {
|
|
67
|
+
// Only redirect if we don't have controls (not just switching)
|
|
68
|
+
if (!state.controls || state.controls.length === 0) {
|
|
69
|
+
// Scan for available control sets
|
|
70
|
+
await wsClient.scanControlSets();
|
|
71
|
+
|
|
72
|
+
// Wait for the control sets list to arrive
|
|
73
|
+
const controlSets = await new Promise((resolve) => {
|
|
74
|
+
const handler = (event: CustomEvent) => {
|
|
75
|
+
window.removeEventListener('control-sets-list', handler as EventListener);
|
|
76
|
+
resolve(event.detail);
|
|
77
|
+
};
|
|
78
|
+
window.addEventListener('control-sets-list', handler as EventListener);
|
|
79
|
+
|
|
80
|
+
// Timeout after 2 seconds
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
window.removeEventListener('control-sets-list', handler as EventListener);
|
|
83
|
+
resolve(null);
|
|
84
|
+
}, 2000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (controlSets && Array.isArray(controlSets) && controlSets.length === 1) {
|
|
88
|
+
// Only one control set available - auto-load it
|
|
89
|
+
console.log('Auto-loading single control set:', controlSets[0].path);
|
|
90
|
+
await wsClient.switchControlSet(controlSets[0].path);
|
|
91
|
+
} else {
|
|
92
|
+
// Multiple control sets or none - show setup
|
|
93
|
+
goto('/setup');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Not connected yet, check again in a moment
|
|
99
|
+
checkTimeoutId = window.setTimeout(checkForRedirect, 500);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Start checking after a short delay to let WebSocket connect
|
|
104
|
+
checkTimeoutId = window.setTimeout(checkForRedirect, 100);
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
if (checkTimeoutId) {
|
|
108
|
+
clearTimeout(checkTimeoutId);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
onDestroy(() => {
|
|
114
|
+
// Disconnect WebSocket when component is destroyed
|
|
115
|
+
wsClient.disconnect();
|
|
116
|
+
});
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<div class="h-screen flex flex-col">
|
|
120
|
+
{#if $page.url.pathname !== '/setup'}
|
|
121
|
+
<!-- Fixed Header -->
|
|
122
|
+
<header
|
|
123
|
+
class="bg-white dark:bg-gray-900 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-shrink-0"
|
|
124
|
+
>
|
|
125
|
+
<div class="w-full px-6 lg:px-8">
|
|
126
|
+
<div class="flex justify-between items-center h-16">
|
|
127
|
+
<!-- Left: Logo and Brand -->
|
|
128
|
+
<div class="flex items-center">
|
|
129
|
+
<a href="/" class="flex items-center space-x-3 hover:opacity-80 transition-opacity">
|
|
130
|
+
<img src="/lula.png" class="h-8 w-8" alt="Lula Logo" />
|
|
131
|
+
<div class="flex flex-col">
|
|
132
|
+
<span class="text-xl font-bold text-gray-900 dark:text-white"> Lula </span>
|
|
133
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 -mt-1">
|
|
134
|
+
Gitops for Compliance
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
</a>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Right: Control Set Info and Actions -->
|
|
141
|
+
<div class="flex items-center space-x-4">
|
|
142
|
+
<!-- Control Set Info Badge -->
|
|
143
|
+
<ControlSetInfo />
|
|
144
|
+
|
|
145
|
+
{#if $appState.isConnected && $appState.controls && $appState.controls.length > 0}
|
|
146
|
+
<!-- Export Dropdown -->
|
|
147
|
+
<Dropdown
|
|
148
|
+
buttonLabel="Export"
|
|
149
|
+
buttonIcon={Download}
|
|
150
|
+
buttonClass="inline-flex items-center 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-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
|
151
|
+
dropdownClass="w-48"
|
|
152
|
+
>
|
|
153
|
+
{#snippet children()}
|
|
154
|
+
<div class="space-y-1 p-1">
|
|
155
|
+
<button
|
|
156
|
+
onclick={() => exportControls('csv')}
|
|
157
|
+
class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
158
|
+
>
|
|
159
|
+
<div class="flex items-center gap-2">
|
|
160
|
+
<DocumentExport class="w-4 h-4" />
|
|
161
|
+
<span>Export as CSV</span>
|
|
162
|
+
</div>
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
onclick={() => exportControls('excel')}
|
|
166
|
+
class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
167
|
+
>
|
|
168
|
+
<div class="flex items-center gap-2">
|
|
169
|
+
<Download class="w-4 h-4" />
|
|
170
|
+
<span>Export as Excel</span>
|
|
171
|
+
</div>
|
|
172
|
+
</button>
|
|
173
|
+
<button
|
|
174
|
+
onclick={() => exportControls('json')}
|
|
175
|
+
class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
176
|
+
>
|
|
177
|
+
<div class="flex items-center gap-2">
|
|
178
|
+
<Code class="w-4 h-4" />
|
|
179
|
+
<span>Export as JSON</span>
|
|
180
|
+
</div>
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
{/snippet}
|
|
184
|
+
</Dropdown>
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
187
|
+
<!-- Github -->
|
|
188
|
+
<a
|
|
189
|
+
class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
|
190
|
+
title="Github"
|
|
191
|
+
href="https://github.com/defenseunicorns/lula"
|
|
192
|
+
target="_blank"
|
|
193
|
+
>
|
|
194
|
+
<LogoGithub class="w-5 h-5" />
|
|
195
|
+
</a>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</header>
|
|
200
|
+
|
|
201
|
+
<!-- Split Pane Layout with Cards -->
|
|
202
|
+
<div class="flex-1 flex gap-6 p-6 overflow-hidden">
|
|
203
|
+
{#if $appState.isSwitchingControlSet}
|
|
204
|
+
<!-- Show loading during control set switch -->
|
|
205
|
+
<div class="flex-1 flex justify-center items-center">
|
|
206
|
+
<div class="text-center">
|
|
207
|
+
<div
|
|
208
|
+
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"
|
|
209
|
+
></div>
|
|
210
|
+
<p class="text-gray-500 dark:text-gray-400">Switching control set...</p>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
{:else if !$appState.isConnected || !$appState.controls || $appState.controls.length === 0 || !$appState.fieldSchema}
|
|
214
|
+
<div class="flex-1 flex justify-center items-center">
|
|
215
|
+
<div class="text-center">
|
|
216
|
+
<div
|
|
217
|
+
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"
|
|
218
|
+
></div>
|
|
219
|
+
<p class="text-gray-500 dark:text-gray-400">
|
|
220
|
+
{!$appState.isConnected
|
|
221
|
+
? 'Connecting...'
|
|
222
|
+
: !$appState.fieldSchema
|
|
223
|
+
? 'Loading schema...'
|
|
224
|
+
: 'Loading controls...'}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
{:else}
|
|
229
|
+
{@render children()}
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
{:else}
|
|
233
|
+
<!-- Setup page gets full screen -->
|
|
234
|
+
{@render children()}
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { ControlDetailsPanel, ControlsList } from '$components/controls';
|
|
6
|
+
import { selectedControl } from '$stores/compliance';
|
|
7
|
+
import { Document } from 'carbon-icons-svelte';
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!-- Left Pane: Controls List Card -->
|
|
11
|
+
<div class="w-1/2 flex flex-col">
|
|
12
|
+
<div
|
|
13
|
+
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm h-full flex flex-col"
|
|
14
|
+
>
|
|
15
|
+
<ControlsList />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Right Pane: Control Details -->
|
|
20
|
+
<div class="w-1/2 flex flex-col">
|
|
21
|
+
{#if $selectedControl}
|
|
22
|
+
<ControlDetailsPanel control={$selectedControl} />
|
|
23
|
+
{:else}
|
|
24
|
+
<div class=" h-full flex flex-col">
|
|
25
|
+
<div class="flex-1 flex items-center justify-center p-8">
|
|
26
|
+
<div class="text-center text-gray-500 dark:text-gray-400">
|
|
27
|
+
<Document class="mx-auto h-16 w-16 mb-4" />
|
|
28
|
+
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
|
29
|
+
No Control Selected
|
|
30
|
+
</h3>
|
|
31
|
+
<p class="text-gray-600 dark:text-gray-400">
|
|
32
|
+
Select a control from the list to view and edit its details
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
{/if}
|
|
38
|
+
</div>
|