lula2 0.5.1-nightly.5 → 0.6.0

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.
@@ -9,7 +9,8 @@
9
9
  type FilterValue,
10
10
  activeFilters,
11
11
  FILTER_OPERATORS,
12
- getOperatorLabel
12
+ getOperatorLabel,
13
+ MAPPING_STATUS_OPTIONS
13
14
  } from '$stores/compliance';
14
15
  import { appState } from '$lib/websocket';
15
16
  import { Filter, Add, TrashCan, ChevronDown, ChevronUp } from 'carbon-icons-svelte';
@@ -37,13 +38,18 @@
37
38
 
38
39
  // Get field type for the selected field
39
40
  $: selectedFieldSchema = $fieldSchema[newFilterField] || null;
40
- $: selectedFieldType = selectedFieldSchema?.type || 'string';
41
- $: selectedFieldUiType = selectedFieldSchema?.ui_type || 'short_text';
41
+ $: selectedFieldType =
42
+ getMappingFieldType(newFilterField) || selectedFieldSchema?.type || 'string';
43
+ $: selectedFieldUiType =
44
+ getMappingFieldUiType(newFilterField) || selectedFieldSchema?.ui_type || 'short_text';
42
45
  $: isSelectField = selectedFieldUiType === 'select';
43
- $: fieldOptions = isSelectField ? selectedFieldSchema?.options || [] : [];
46
+ $: fieldOptions =
47
+ getMappingFieldOptions(newFilterField) ||
48
+ (isSelectField ? selectedFieldSchema?.options || [] : []);
44
49
 
45
- // Force equals operator for select fields
46
- $: if (isSelectField) {
50
+ // Force equals operator for select fields (but allow operators for mapping fields)
51
+ // Also force equals operator for has_mappings field, but allow operators for mapping_status
52
+ $: if (isSelectField && newFilterField !== 'mapping_status') {
47
53
  newFilterOperator = 'equals';
48
54
  }
49
55
 
@@ -51,6 +57,7 @@
51
57
  $: fieldsByTab = {
52
58
  overview: [] as string[],
53
59
  implementation: [] as string[],
60
+ mappings: [] as string[],
54
61
  custom: [] as string[]
55
62
  };
56
63
 
@@ -58,19 +65,25 @@
58
65
  // Reset arrays before populating
59
66
  fieldsByTab.overview = [];
60
67
  fieldsByTab.implementation = [];
68
+ fieldsByTab.mappings = [];
61
69
  fieldsByTab.custom = [];
62
70
 
63
71
  // Group fields by tab
64
72
  availableFields.forEach((field) => {
65
- const schema = $fieldSchema[field];
66
- if (schema) {
67
- const tab = schema.tab || getDefaultTabForCategory(schema.category);
68
- if (tab === 'overview') fieldsByTab.overview.push(field);
69
- else if (tab === 'implementation') fieldsByTab.implementation.push(field);
70
- else fieldsByTab.custom.push(field);
73
+ // Handle mapping-related fields specially
74
+ if (field === 'has_mappings' || field === 'mapping_status') {
75
+ fieldsByTab.mappings.push(field);
71
76
  } else {
72
- // If no schema, default to custom
73
- fieldsByTab.custom.push(field);
77
+ const schema = $fieldSchema[field];
78
+ if (schema) {
79
+ const tab = schema.tab || getDefaultTabForCategory(schema.category);
80
+ if (tab === 'overview') fieldsByTab.overview.push(field);
81
+ else if (tab === 'implementation') fieldsByTab.implementation.push(field);
82
+ else fieldsByTab.custom.push(field);
83
+ } else {
84
+ // If no schema, default to custom
85
+ fieldsByTab.custom.push(field);
86
+ }
74
87
  }
75
88
  });
76
89
  }
@@ -81,12 +94,23 @@
81
94
  // Create a new array from the readonly constant to make it mutable for Svelte
82
95
  const operatorOptions = FILTER_OPERATORS.map((op) => ({ value: op.value, label: op.label }));
83
96
 
97
+ // Limited operators for mapping status field
98
+ const mappingStatusOperatorOptions = [
99
+ { value: 'equals' as const, label: 'Equals' },
100
+ { value: 'not_equals' as const, label: 'Not equals' }
101
+ ];
102
+
84
103
  // Add a new filter
85
104
  function addFilter() {
86
105
  if (!newFilterField) return;
87
106
 
88
107
  let value = newFilterValue;
89
108
 
109
+ // Extract value from dropdown objects if necessary
110
+ if (typeof value === 'object' && value !== null && 'value' in value) {
111
+ value = (value as any).value;
112
+ }
113
+
90
114
  // Convert value based on field type
91
115
  let processedValue: FilterValue = value;
92
116
  if (selectedFieldType === 'boolean' && typeof value === 'string') {
@@ -125,8 +149,54 @@
125
149
  }
126
150
  }
127
151
 
152
+ // Centralized mapping field metadata
153
+ const mappingFieldConfig: Record<
154
+ string,
155
+ {
156
+ type: string;
157
+ ui_type: string;
158
+ options?: Array<{ value: string; label: string }>;
159
+ }
160
+ > = {
161
+ has_mappings: {
162
+ type: 'boolean',
163
+ ui_type: 'select',
164
+ options: [
165
+ { value: 'true', label: 'Yes' },
166
+ { value: 'false', label: 'No' }
167
+ ]
168
+ },
169
+ mapping_status: {
170
+ type: 'string',
171
+ ui_type: 'select',
172
+ options: MAPPING_STATUS_OPTIONS
173
+ }
174
+ };
175
+
176
+ function getMappingFieldType(fieldName: string): string | null {
177
+ return mappingFieldConfig[fieldName]?.type ?? null;
178
+ }
179
+
180
+ function getMappingFieldUiType(fieldName: string): string | null {
181
+ return mappingFieldConfig[fieldName]?.ui_type ?? null;
182
+ }
183
+
184
+ function getMappingFieldOptions(
185
+ fieldName: string
186
+ ): Array<{ value: string; label: string }> | null {
187
+ return mappingFieldConfig[fieldName]?.options ?? null;
188
+ }
189
+
128
190
  // Get display name for a field
129
191
  function getFieldDisplayName(fieldName: string): string {
192
+ // Handle mapping fields specially
193
+ switch (fieldName) {
194
+ case 'has_mappings':
195
+ return 'Has Mappings';
196
+ case 'mapping_status':
197
+ return 'Mapping Status';
198
+ }
199
+
130
200
  const schema = $fieldSchema[fieldName];
131
201
 
132
202
  // Use schema names if available
@@ -145,9 +215,27 @@
145
215
  if (filter.operator === 'exists') return 'exists';
146
216
  if (filter.operator === 'not_exists') return 'does not exist';
147
217
 
218
+ // Convert value to string, handling objects and arrays properly
219
+ let displayValue = '';
220
+ if (filter.value === null || filter.value === undefined) {
221
+ displayValue = '';
222
+ } else if (typeof filter.value === 'object') {
223
+ // Handle objects by converting to JSON or getting a meaningful representation
224
+ if (Array.isArray(filter.value)) {
225
+ displayValue = filter.value.join(', ');
226
+ } else if ('value' in filter.value) {
227
+ // Handle dropdown option objects
228
+ displayValue = String((filter.value as any).value);
229
+ } else {
230
+ displayValue = JSON.stringify(filter.value);
231
+ }
232
+ } else {
233
+ displayValue = String(filter.value);
234
+ }
235
+
148
236
  // Use the shared getOperatorLabel function
149
237
  const operatorText = getOperatorLabel(filter.operator).toLowerCase();
150
- return `${operatorText} "${filter.value}"`;
238
+ return `${operatorText} "${displayValue}"`;
151
239
  }
152
240
 
153
241
  // Handle click outside to close the panel
@@ -306,6 +394,33 @@
306
394
  {/each}
307
395
  {/if}
308
396
 
397
+ <!-- Mapping fields -->
398
+ {#if fieldsByTab.mappings.length > 0}
399
+ <!-- Divider -->
400
+ <div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
401
+ <div
402
+ class="px-3 py-1 text-xs font-semibold text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/80"
403
+ >
404
+ Mapping Fields
405
+ </div>
406
+ {#each fieldsByTab.mappings as field (field)}
407
+ <button
408
+ class={twMerge(
409
+ 'w-full text-left px-3 py-2 text-sm',
410
+ newFilterField === field
411
+ ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
412
+ : 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
413
+ )}
414
+ onclick={() => {
415
+ newFilterField = field;
416
+ showFieldDropdown = false;
417
+ }}
418
+ >
419
+ {getFieldDisplayName(field)}
420
+ </button>
421
+ {/each}
422
+ {/if}
423
+
309
424
  <!-- Custom fields -->
310
425
  {#if fieldsByTab.custom.length > 0}
311
426
  <!-- Divider -->
@@ -343,8 +458,8 @@
343
458
  >Operator</label
344
459
  >
345
460
 
346
- {#if isSelectField}
347
- <!-- Disabled dropdown for select fields (always equals) -->
461
+ {#if (isSelectField && !['has_mappings', 'mapping_status'].includes(newFilterField)) || newFilterField === 'has_mappings'}
462
+ <!-- Disabled dropdown for select fields and has_mappings (always equals) -->
348
463
  <div
349
464
  class="px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
350
465
  >
@@ -354,7 +469,9 @@
354
469
  <!-- Custom Operator Dropdown -->
355
470
  <CustomDropdown
356
471
  bind:value={newFilterOperator}
357
- options={operatorOptions}
472
+ options={newFilterField === 'mapping_status'
473
+ ? mappingStatusOperatorOptions
474
+ : operatorOptions}
358
475
  getDisplayValue={getOperatorLabel}
359
476
  labelId="filter-operator"
360
477
  />
@@ -367,7 +484,30 @@
367
484
  <label for="filter-value" class="block text-xs text-gray-600 dark:text-gray-400 mb-1"
368
485
  >Value</label
369
486
  >
370
- {#if isSelectField}
487
+ {#if newFilterField === 'mapping_status'}
488
+ <!-- Special dropdown for mapping status -->
489
+ <select
490
+ id="filter-value"
491
+ bind:value={newFilterValue}
492
+ class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
493
+ >
494
+ <option value="">Select status...</option>
495
+ {#each MAPPING_STATUS_OPTIONS as status}
496
+ <option value={status.value}>{status.label}</option>
497
+ {/each}
498
+ </select>
499
+ {:else if newFilterField === 'has_mappings'}
500
+ <!-- Special dropdown for has_mappings -->
501
+ <select
502
+ id="filter-value"
503
+ bind:value={newFilterValue}
504
+ class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
505
+ >
506
+ <option value="">Select...</option>
507
+ <option value="true">Yes</option>
508
+ <option value="false">No</option>
509
+ </select>
510
+ {:else if isSelectField}
371
511
  <!-- Custom dropdown for select fields -->
372
512
  <CustomDropdown
373
513
  bind:value={newFilterValue}
@@ -377,15 +517,15 @@
377
517
  />
378
518
  {:else if selectedFieldType === 'boolean'}
379
519
  <!-- Boolean field with CustomDropdown -->
380
- <CustomDropdown
520
+ <select
521
+ id="filter-value"
381
522
  bind:value={newFilterValue}
382
- options={[
383
- { value: 'true', label: 'Yes' },
384
- { value: 'false', label: 'No' }
385
- ]}
386
- placeholder="Select yes/no"
387
- labelId="filter-value"
388
- />
523
+ class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
524
+ >
525
+ <option value="">Select...</option>
526
+ <option value="true">Yes</option>
527
+ <option value="false">No</option>
528
+ </select>
389
529
  {:else}
390
530
  <!-- Text input for other fields -->
391
531
  <input
package/src/lib/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
3
4
  export interface Control {
4
5
  id: string;
5
6
  title: string;
@@ -5,6 +5,15 @@ import type { Control, Mapping } from '$lib/types';
5
5
  import { appState } from '$lib/websocket';
6
6
  import { get, writable } from 'svelte/store';
7
7
 
8
+ /**
9
+ * Shared mapping status options used across the application
10
+ */
11
+ export const MAPPING_STATUS_OPTIONS = [
12
+ { value: 'planned', label: 'Planned' },
13
+ { value: 'implemented', label: 'Implemented' },
14
+ { value: 'verified', label: 'Verified' }
15
+ ];
16
+
8
17
  /**
9
18
  * Shared filter operator options used across the application
10
19
  */
@@ -133,6 +142,10 @@ export const complianceStore = {
133
142
  });
134
143
  }
135
144
 
145
+ // Add mapping-related fields for filtering
146
+ fieldSet.add('has_mappings');
147
+ fieldSet.add('mapping_status');
148
+
136
149
  return Array.from(fieldSet).sort();
137
150
  }
138
151
  };