Files
breakpilot-compliance/admin-compliance/app/sdk/template-rule-editor/_components/ConditionBuilder.tsx
T
Benjamin Admin 02879a2c3a
CI / detect-changes (push) Successful in 7s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Failing after 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m19s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 29s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
refactor: split cookie_screenshot_ocr.py (642 → 290 + 353 LOC)
CI hard-cap 500 LOC. cookie_screenshot_ocr.py war auf 642 gewachsen,
also gesplittet:

  - cookie_screenshot_ocr_engines.py (353 LOC, NEU)
    OCR-Engine-Funktionen: _slice_screenshot, Vision-LLM (qwen2.5vl),
    PaddleOCR, Tesseract, parse_ocr_cookie_table, parse_vision_response,
    Konstanten VISION_MODEL/OLLAMA_URL/VISION_PROMPT.

  - cookie_screenshot_ocr.py (290 LOC, REWRITE)
    Orchestration: capture_cookie_evidence_slices, _ocr_one_slice,
    ocr_slices_extract_cookies, capture_cookie_screenshot,
    extract_cookies_via_vision, cookies_to_vendor_records.
    Re-Exports der Engine-Funktionen für Backward-Kompat.

Einziger externer Importer (_phase_d1_vendors_raw.py) braucht keinen
Code-Change — Public-API stabil.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 23:35:33 +02:00

233 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* Strukturierter Editor fuer JSONB-Conditions:
* { kind: 'all'|'any', clauses: [{field, op, value}] }
*
* Wird im RuleEditor verwendet. Reine Praesentations-Komponente — Parent
* verwaltet State.
*/
import type {
ClauseOperator, RuleClause, RuleCondition,
} from '../_types'
import { OPERATOR_LABELS, PROFILE_FIELDS } from '../_types'
interface Props {
value: RuleCondition
onChange: (next: RuleCondition) => void
readOnly?: boolean
}
export default function ConditionBuilder({ value, onChange, readOnly }: Props) {
const setKind = (kind: 'all' | 'any') => onChange({ ...value, kind })
const setClause = (idx: number, clause: RuleClause) => {
const next = [...value.clauses]
next[idx] = clause
onChange({ ...value, clauses: next })
}
const addClause = () =>
onChange({
...value,
clauses: [
...value.clauses,
{ field: PROFILE_FIELDS[0].key, op: 'eq', value: '' },
],
})
const removeClause = (idx: number) =>
onChange({ ...value, clauses: value.clauses.filter((_, i) => i !== idx) })
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600">Bedingung:</span>
<select
className="text-xs px-2 py-1 border border-gray-300 rounded"
value={value.kind}
disabled={readOnly}
onChange={(e) => setKind(e.target.value as 'all' | 'any')}
>
<option value="all">ALLE Klauseln müssen zutreffen (AND)</option>
<option value="any">MIND. EINE Klausel trifft zu (OR)</option>
</select>
</div>
{value.clauses.length === 0 && (
<div className="text-xs text-gray-500 italic px-1">
Keine Klauseln Regel gilt für jedes Profil.
</div>
)}
<ul className="space-y-1">
{value.clauses.map((clause, idx) => (
<li key={idx} className="flex items-start gap-1 p-1.5 bg-gray-50 rounded border border-gray-200">
<ClauseRow
clause={clause}
onChange={(c) => setClause(idx, c)}
readOnly={!!readOnly}
/>
{!readOnly && (
<button
className="text-xs px-1.5 py-0.5 text-rose-700 hover:bg-rose-50 rounded"
onClick={() => removeClause(idx)}
title="Klausel entfernen"
>
×
</button>
)}
</li>
))}
</ul>
{!readOnly && (
<button
className="text-xs px-2 py-1 border border-gray-300 rounded text-gray-700 hover:bg-gray-50"
onClick={addClause}
>
+ Klausel hinzufügen
</button>
)}
</div>
)
}
function ClauseRow({
clause, onChange, readOnly,
}: {
clause: RuleClause
onChange: (c: RuleClause) => void
readOnly: boolean
}) {
const field = PROFILE_FIELDS.find((f) => f.key === clause.field) || PROFILE_FIELDS[0]
const operators: ClauseOperator[] =
field.type === 'enum'
? ['eq', 'neq', 'in', 'not_in', 'exists', 'truthy', 'falsy']
: field.type === 'boolean'
? ['truthy', 'falsy', 'eq', 'neq']
: field.type === 'number'
? ['eq', 'neq', 'gt', 'gte', 'lt', 'lte']
: ['eq', 'neq', 'in', 'not_in', 'exists']
const requiresValue = !['exists', 'truthy', 'falsy'].includes(clause.op)
const multiValue = clause.op === 'in' || clause.op === 'not_in'
return (
<div className="flex-1 grid grid-cols-12 gap-1 items-center text-xs">
<select
className="col-span-4 px-1 py-0.5 border border-gray-300 rounded bg-white truncate"
value={clause.field}
disabled={readOnly}
onChange={(e) => onChange({ ...clause, field: e.target.value })}
>
{PROFILE_FIELDS.map((f) => (
<option key={f.key} value={f.key}>{f.label} ({f.key})</option>
))}
</select>
<select
className="col-span-3 px-1 py-0.5 border border-gray-300 rounded bg-white"
value={clause.op}
disabled={readOnly}
onChange={(e) => onChange({ ...clause, op: e.target.value as ClauseOperator })}
>
{operators.map((op) => (
<option key={op} value={op}>{OPERATOR_LABELS[op]}</option>
))}
</select>
<div className="col-span-5">
{requiresValue && (
<ValueInput
field={field}
multi={multiValue}
value={clause.value}
onChange={(v) => onChange({ ...clause, value: v })}
readOnly={readOnly}
/>
)}
</div>
</div>
)
}
function ValueInput({
field, multi, value, onChange, readOnly,
}: {
field: typeof PROFILE_FIELDS[number]
multi: boolean
value: unknown
onChange: (v: unknown) => void
readOnly: boolean
}) {
if (field.type === 'enum' && field.options) {
if (multi) {
const selected = Array.isArray(value) ? (value as string[]) : []
return (
<select
multiple
className="w-full px-1 py-0.5 border border-gray-300 rounded bg-white h-16"
value={selected}
disabled={readOnly}
onChange={(e) => {
const opts = Array.from(e.target.selectedOptions, (o) => o.value)
onChange(opts)
}}
>
{field.options.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
)
}
return (
<select
className="w-full px-1 py-0.5 border border-gray-300 rounded bg-white"
value={typeof value === 'string' ? value : ''}
disabled={readOnly}
onChange={(e) => onChange(e.target.value)}
>
<option value=""> wählen </option>
{field.options.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
)
}
if (field.type === 'number') {
return (
<input
type="number"
className="w-full px-1 py-0.5 border border-gray-300 rounded"
value={typeof value === 'number' ? value : 0}
disabled={readOnly}
onChange={(e) => onChange(Number(e.target.value))}
/>
)
}
if (field.type === 'boolean') {
return (
<select
className="w-full px-1 py-0.5 border border-gray-300 rounded bg-white"
value={value ? 'true' : 'false'}
disabled={readOnly}
onChange={(e) => onChange(e.target.value === 'true')}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
)
}
return (
<input
type="text"
className="w-full px-1 py-0.5 border border-gray-300 rounded"
value={typeof value === 'string' ? value : ''}
disabled={readOnly}
onChange={(e) => onChange(e.target.value)}
/>
)
}