lula2 0.5.1 → 0.6.1-nightly.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.
- package/dist/_app/immutable/chunks/{CWggGNM7.js → BMNSAUym.js} +1 -1
- package/dist/_app/immutable/chunks/{R1gz3SOr.js → BXUi170M.js} +1 -1
- package/dist/_app/immutable/chunks/BmwkkWK-.js +65 -0
- package/dist/_app/immutable/chunks/{CPEw6sZY.js → CRa0j_Fx.js} +1 -1
- package/dist/_app/immutable/chunks/{CXdZVXJf.js → Cq7PwVfU.js} +1 -1
- package/dist/_app/immutable/chunks/DTWPdvjs.js +2 -0
- package/dist/_app/immutable/chunks/{BNbzyHlf.js → DYjeAJVm.js} +2 -2
- package/dist/_app/immutable/chunks/{zYrdvxnm.js → WlyXjfrM.js} +1 -1
- package/dist/_app/immutable/chunks/{GToHgjp8.js → e7OeeeKP.js} +1 -1
- package/dist/_app/immutable/entry/{app.DMBX4AyV.js → app.BMX3XH9B.js} +2 -2
- package/dist/_app/immutable/entry/start.CikNPmTv.js +1 -0
- package/dist/_app/immutable/nodes/{0.ePBNOec_.js → 0.DFzEgAsn.js} +1 -1
- package/dist/_app/immutable/nodes/{1.BjVPQIpm.js → 1.f-frqYrl.js} +1 -1
- package/dist/_app/immutable/nodes/{2.DAjmdkWJ.js → 2.DjKBnAK5.js} +1 -1
- package/dist/_app/immutable/nodes/{3.C42QL2CH.js → 3.Cm8oQ-Ws.js} +1 -1
- package/dist/_app/immutable/nodes/{4.BKr3XX2Z.js → 4.DVhDnucO.js} +1 -1
- package/dist/_app/version.json +1 -1
- package/dist/cli/commands/crawl.js +53 -1
- package/dist/index.html +10 -10
- package/dist/index.js +52 -1
- package/package.json +20 -21
- package/src/lib/components/controls/ControlsList.svelte +82 -9
- package/src/lib/components/ui/FilterBuilder.svelte +167 -27
- package/src/lib/types.ts +1 -0
- package/src/stores/compliance.ts +13 -0
- package/dist/_app/immutable/chunks/Bvop-7hR.js +0 -2
- package/dist/_app/immutable/chunks/y6toZMLe.js +0 -65
- package/dist/_app/immutable/entry/start.CXGxkyju.js +0 -1
package/dist/index.html
CHANGED
|
@@ -6,28 +6,28 @@
|
|
|
6
6
|
<link rel="icon" href="/lula.png" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
8
|
|
|
9
|
-
<link rel="modulepreload" href="/_app/immutable/entry/start.
|
|
10
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
11
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
12
|
-
<link rel="modulepreload" href="/_app/immutable/entry/app.
|
|
9
|
+
<link rel="modulepreload" href="/_app/immutable/entry/start.CikNPmTv.js">
|
|
10
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/DYjeAJVm.js">
|
|
11
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/DTWPdvjs.js">
|
|
12
|
+
<link rel="modulepreload" href="/_app/immutable/entry/app.BMX3XH9B.js">
|
|
13
13
|
<link rel="modulepreload" href="/_app/immutable/chunks/DsnmJJEf.js">
|
|
14
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
15
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
16
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
14
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/BXUi170M.js">
|
|
15
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/WlyXjfrM.js">
|
|
16
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/CRa0j_Fx.js">
|
|
17
17
|
</head>
|
|
18
18
|
<body data-sveltekit-preload-data="hover">
|
|
19
19
|
<div style="display: contents">
|
|
20
20
|
<script>
|
|
21
21
|
{
|
|
22
|
-
|
|
22
|
+
__sveltekit_9ui1cm = {
|
|
23
23
|
base: ""
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const element = document.currentScript.parentElement;
|
|
27
27
|
|
|
28
28
|
Promise.all([
|
|
29
|
-
import("/_app/immutable/entry/start.
|
|
30
|
-
import("/_app/immutable/entry/app.
|
|
29
|
+
import("/_app/immutable/entry/start.CikNPmTv.js"),
|
|
30
|
+
import("/_app/immutable/entry/app.BMX3XH9B.js")
|
|
31
31
|
]).then(([kit, app]) => {
|
|
32
32
|
kit.start(app, element);
|
|
33
33
|
});
|
package/dist/index.js
CHANGED
|
@@ -5628,6 +5628,18 @@ function getChangedBlocks(oldText, newText) {
|
|
|
5628
5628
|
}
|
|
5629
5629
|
return changed;
|
|
5630
5630
|
}
|
|
5631
|
+
function getRemovedBlocks(oldText, newText) {
|
|
5632
|
+
const oldBlocks = extractMapBlocks(oldText);
|
|
5633
|
+
const newBlocks = extractMapBlocks(newText);
|
|
5634
|
+
const removed = [];
|
|
5635
|
+
for (const oldBlock of oldBlocks) {
|
|
5636
|
+
const newMatch = newBlocks.find((b) => b.uuid === oldBlock.uuid);
|
|
5637
|
+
if (!newMatch) {
|
|
5638
|
+
removed.push(oldBlock);
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
return removed;
|
|
5642
|
+
}
|
|
5631
5643
|
function containsLulaAnnotations(text) {
|
|
5632
5644
|
const lines = text.split("\n");
|
|
5633
5645
|
return lines.some((line) => line.includes("@lulaStart") || line.includes("@lulaEnd"));
|
|
@@ -5708,6 +5720,7 @@ Please review whether:
|
|
|
5708
5720
|
fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: prBranch })
|
|
5709
5721
|
]);
|
|
5710
5722
|
const changedBlocks = getChangedBlocks(oldText, newText);
|
|
5723
|
+
const removedBlocks = getRemovedBlocks(oldText, newText);
|
|
5711
5724
|
for (const block of changedBlocks) {
|
|
5712
5725
|
console.log(`Commenting regarding \`${file.filename}\`.`);
|
|
5713
5726
|
leavePost = true;
|
|
@@ -5723,6 +5736,43 @@ Please review whether:
|
|
|
5723
5736
|
> **uuid**-\`${block.uuid}\`
|
|
5724
5737
|
**sha256** \`${blockSha256}\`
|
|
5725
5738
|
|
|
5739
|
+
`;
|
|
5740
|
+
}
|
|
5741
|
+
if (removedBlocks.length > 0) {
|
|
5742
|
+
leavePost = true;
|
|
5743
|
+
console.log(`Found removed annotations in \`${file.filename}\`.`);
|
|
5744
|
+
commentBody += `
|
|
5745
|
+
|
|
5746
|
+
**Compliance Warning: Lula annotations were removed from \`${file.filename}\`**
|
|
5747
|
+
|
|
5748
|
+
`;
|
|
5749
|
+
commentBody += `The following compliance annotation blocks were present in the original file but are missing in the updated version:
|
|
5750
|
+
|
|
5751
|
+
`;
|
|
5752
|
+
commentBody += `| File | Original Lines | UUID |
|
|
5753
|
+
`;
|
|
5754
|
+
commentBody += `| ---- | -------------- | ---- |
|
|
5755
|
+
`;
|
|
5756
|
+
for (const block of removedBlocks) {
|
|
5757
|
+
const oldBlockText = oldText.split("\n").slice(block.startLine, block.endLine).join("\n");
|
|
5758
|
+
const blockSha256 = createHash2("sha256").update(oldBlockText).digest("hex");
|
|
5759
|
+
commentBody += `| \`${file.filename}\` | \`${block.startLine + 1}\u2013${block.endLine}\` | \`${block.uuid}\` |
|
|
5760
|
+
`;
|
|
5761
|
+
commentBody += `> **sha256** \`${blockSha256}\`
|
|
5762
|
+
|
|
5763
|
+
`;
|
|
5764
|
+
}
|
|
5765
|
+
commentBody += `Please review whether:
|
|
5766
|
+
`;
|
|
5767
|
+
commentBody += `- The removal of these compliance annotations is intentional
|
|
5768
|
+
`;
|
|
5769
|
+
commentBody += `- Alternative compliance measures have been implemented
|
|
5770
|
+
`;
|
|
5771
|
+
commentBody += `- The compliance coverage is still adequate
|
|
5772
|
+
|
|
5773
|
+
`;
|
|
5774
|
+
commentBody += `---
|
|
5775
|
+
|
|
5726
5776
|
`;
|
|
5727
5777
|
}
|
|
5728
5778
|
} catch (err) {
|
|
@@ -5826,7 +5876,8 @@ async function dismissOldReviews({
|
|
|
5826
5876
|
if (!reviews.length) break;
|
|
5827
5877
|
for (const r of reviews) {
|
|
5828
5878
|
const hasSignature = (r.body ?? "").includes(LULA_SIGNATURE);
|
|
5829
|
-
|
|
5879
|
+
const isAlreadyDismissed = r.state === "DISMISSED";
|
|
5880
|
+
if (hasSignature && !isAlreadyDismissed) {
|
|
5830
5881
|
await octokit.pulls.dismissReview({
|
|
5831
5882
|
owner,
|
|
5832
5883
|
repo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lula2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1-nightly.0",
|
|
4
4
|
"description": "A tool for managing compliance as code in your GitHub repositories.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lula2": "./dist/lula2"
|
|
@@ -33,25 +33,6 @@
|
|
|
33
33
|
"!dist/**/*.test.js*",
|
|
34
34
|
"!dist/**/*.test.d.ts*"
|
|
35
35
|
],
|
|
36
|
-
"scripts": {
|
|
37
|
-
"dev": "vite dev --port 5173",
|
|
38
|
-
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
39
|
-
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
40
|
-
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
41
|
-
"build:svelte": "vite build",
|
|
42
|
-
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
|
|
43
|
-
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
44
|
-
"preview": "vite preview",
|
|
45
|
-
"prepare": "svelte-kit sync || echo ''",
|
|
46
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
47
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
48
|
-
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
49
|
-
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
50
|
-
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
51
|
-
"test": "npm run test:unit -- --run --coverage",
|
|
52
|
-
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
53
|
-
"test:unit": "vitest"
|
|
54
|
-
},
|
|
55
36
|
"dependencies": {
|
|
56
37
|
"@octokit/rest": "^22.0.0",
|
|
57
38
|
"@types/ws": "^8.18.1",
|
|
@@ -124,5 +105,23 @@
|
|
|
124
105
|
"main",
|
|
125
106
|
"next"
|
|
126
107
|
]
|
|
108
|
+
},
|
|
109
|
+
"scripts": {
|
|
110
|
+
"dev": "vite dev --port 5173",
|
|
111
|
+
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
112
|
+
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
113
|
+
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
114
|
+
"build:svelte": "vite build",
|
|
115
|
+
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
|
|
116
|
+
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
117
|
+
"preview": "vite preview",
|
|
118
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
119
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
120
|
+
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
121
|
+
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
122
|
+
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
123
|
+
"test": "npm run test:unit -- --run --coverage",
|
|
124
|
+
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
125
|
+
"test:unit": "vitest"
|
|
127
126
|
}
|
|
128
|
-
}
|
|
127
|
+
}
|
|
@@ -119,15 +119,66 @@
|
|
|
119
119
|
results = results.filter((control) => {
|
|
120
120
|
// Control must match all filters
|
|
121
121
|
return $activeFilters.every(filter => {
|
|
122
|
+
// Handle mapping-related fields specially
|
|
123
|
+
if (filter.fieldName === 'has_mappings') {
|
|
124
|
+
const hasMappings = control.mappings && control.mappings.length > 0;
|
|
125
|
+
const filterValue = getFilterValue(filter.value);
|
|
126
|
+
switch (filter.operator) {
|
|
127
|
+
case 'equals':
|
|
128
|
+
return hasMappings === (filterValue === 'true' || filterValue === true);
|
|
129
|
+
case 'not_equals':
|
|
130
|
+
return hasMappings !== (filterValue === 'true' || filterValue === true);
|
|
131
|
+
case 'exists':
|
|
132
|
+
return hasMappings;
|
|
133
|
+
case 'not_exists':
|
|
134
|
+
return !hasMappings;
|
|
135
|
+
default:
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (filter.fieldName === 'mapping_status') {
|
|
141
|
+
if (!control.mappings || control.mappings.length === 0) {
|
|
142
|
+
return filter.operator === 'not_exists';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const filterValue = getFilterValue(filter.value);
|
|
146
|
+
const hasStatus = control.mappings.some(mapping => {
|
|
147
|
+
switch (filter.operator) {
|
|
148
|
+
case 'equals':
|
|
149
|
+
return mapping.status === filterValue;
|
|
150
|
+
case 'not_equals':
|
|
151
|
+
return mapping.status !== filterValue;
|
|
152
|
+
case 'includes':
|
|
153
|
+
return (typeof mapping.status === 'string' ? mapping.status.toLowerCase() : '').includes(String(filterValue).toLowerCase());
|
|
154
|
+
case 'not_includes':
|
|
155
|
+
return !(typeof mapping.status === 'string' ? mapping.status.toLowerCase() : '').includes(String(filterValue).toLowerCase());
|
|
156
|
+
default:
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
switch (filter.operator) {
|
|
162
|
+
case 'exists':
|
|
163
|
+
return control.mappings.some(m => m.status !== undefined && m.status !== null);
|
|
164
|
+
case 'not_exists':
|
|
165
|
+
return !control.mappings.some(m => m.status !== undefined && m.status !== null);
|
|
166
|
+
default:
|
|
167
|
+
return hasStatus;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle regular control fields
|
|
122
172
|
const dynamicControl = control as Record<string, unknown>;
|
|
123
173
|
const fieldValue = dynamicControl[filter.fieldName];
|
|
174
|
+
const filterValue = getFilterValue(filter.value);
|
|
124
175
|
|
|
125
176
|
switch (filter.operator) {
|
|
126
177
|
case 'equals':
|
|
127
|
-
return fieldValue ===
|
|
178
|
+
return fieldValue === filterValue;
|
|
128
179
|
|
|
129
180
|
case 'not_equals':
|
|
130
|
-
return fieldValue !==
|
|
181
|
+
return fieldValue !== filterValue;
|
|
131
182
|
|
|
132
183
|
case 'exists':
|
|
133
184
|
return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
|
|
@@ -137,20 +188,20 @@
|
|
|
137
188
|
|
|
138
189
|
case 'includes':
|
|
139
190
|
if (typeof fieldValue === 'string') {
|
|
140
|
-
return fieldValue.toLowerCase().includes(String(
|
|
191
|
+
return fieldValue.toLowerCase().includes(String(filterValue).toLowerCase());
|
|
141
192
|
} else if (Array.isArray(fieldValue)) {
|
|
142
193
|
return fieldValue.some(item =>
|
|
143
|
-
String(item).toLowerCase().includes(String(
|
|
194
|
+
String(item).toLowerCase().includes(String(filterValue).toLowerCase())
|
|
144
195
|
);
|
|
145
196
|
}
|
|
146
197
|
return false;
|
|
147
198
|
|
|
148
199
|
case 'not_includes':
|
|
149
200
|
if (typeof fieldValue === 'string') {
|
|
150
|
-
return !fieldValue.toLowerCase().includes(String(
|
|
201
|
+
return !fieldValue.toLowerCase().includes(String(filterValue).toLowerCase());
|
|
151
202
|
} else if (Array.isArray(fieldValue)) {
|
|
152
203
|
return !fieldValue.some(item =>
|
|
153
|
-
String(item).toLowerCase().includes(String(
|
|
204
|
+
String(item).toLowerCase().includes(String(filterValue).toLowerCase())
|
|
154
205
|
);
|
|
155
206
|
}
|
|
156
207
|
return true;
|
|
@@ -198,6 +249,28 @@
|
|
|
198
249
|
return 'No description available';
|
|
199
250
|
}
|
|
200
251
|
|
|
252
|
+
function formatFilterValue(value: any): string {
|
|
253
|
+
if (value === null || value === undefined) {
|
|
254
|
+
return '';
|
|
255
|
+
}
|
|
256
|
+
if (typeof value === 'object') {
|
|
257
|
+
if (Array.isArray(value)) {
|
|
258
|
+
return value.join(', ');
|
|
259
|
+
}
|
|
260
|
+
if ('value' in value) {
|
|
261
|
+
return String((value as any).value);
|
|
262
|
+
}
|
|
263
|
+
return JSON.stringify(value);
|
|
264
|
+
}
|
|
265
|
+
return String(value);
|
|
266
|
+
}
|
|
267
|
+
function getFilterValue(filterValue: any): any {
|
|
268
|
+
if (typeof filterValue === 'object' && filterValue !== null && 'value' in filterValue) {
|
|
269
|
+
return filterValue.value;
|
|
270
|
+
}
|
|
271
|
+
return filterValue;
|
|
272
|
+
}
|
|
273
|
+
|
|
201
274
|
// Get truncation length based on field type
|
|
202
275
|
function getTruncationLength(field: FieldSchema | undefined): number {
|
|
203
276
|
if (field?.ui_type === 'textarea' || field?.ui_type === 'long_text') {
|
|
@@ -258,11 +331,11 @@
|
|
|
258
331
|
{#if filter.operator === 'exists' || filter.operator === 'not_exists'}
|
|
259
332
|
<span>{getOperatorLabel(filter.operator).toLowerCase()}</span>
|
|
260
333
|
{:else if filter.operator === 'equals'}
|
|
261
|
-
<span>= {filter.value}</span>
|
|
334
|
+
<span>= {formatFilterValue(filter.value)}</span>
|
|
262
335
|
{:else if filter.operator === 'not_equals'}
|
|
263
|
-
<span>≠ {filter.value}</span>
|
|
336
|
+
<span>≠ {formatFilterValue(filter.value)}</span>
|
|
264
337
|
{:else}
|
|
265
|
-
<span>{getOperatorLabel(filter.operator).toLowerCase()} "{filter.value}"</span>
|
|
338
|
+
<span>{getOperatorLabel(filter.operator).toLowerCase()} "{formatFilterValue(filter.value)}"</span>
|
|
266
339
|
{/if}
|
|
267
340
|
<button
|
|
268
341
|
onclick={() => complianceStore.removeFilter(index)}
|
|
@@ -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 =
|
|
41
|
-
|
|
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 =
|
|
46
|
+
$: fieldOptions =
|
|
47
|
+
getMappingFieldOptions(newFilterField) ||
|
|
48
|
+
(isSelectField ? selectedFieldSchema?.options || [] : []);
|
|
44
49
|
|
|
45
|
-
// Force equals operator for select fields
|
|
46
|
-
|
|
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
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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} "${
|
|
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={
|
|
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
|
|
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
|
-
<
|
|
520
|
+
<select
|
|
521
|
+
id="filter-value"
|
|
381
522
|
bind:value={newFilterValue}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
package/src/stores/compliance.ts
CHANGED
|
@@ -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
|
};
|