|
1 | 1 | <script> |
2 | 2 | import Select from '$lib/components/Select.svelte' |
| 3 | + import TagInput from '$lib/components/TagInput.svelte' |
3 | 4 | import { |
4 | 5 | fields, |
5 | 6 | getField, |
|
23 | 24 | let name = $state('') |
24 | 25 | let operator = $state('equals') |
25 | 26 | let value = $state('') |
26 | | - let values = $state('') |
| 27 | + /** @type {string[]} */ |
| 28 | + let valuesList = $state([]) |
| 29 | + let tls = $state(true) |
27 | 30 | let combine = $state(/** @type {'and' | 'or' | 'replace'} */ ('and')) |
28 | 31 |
|
29 | 32 | const fieldMeta = $derived(getField(field)) |
30 | 33 | const fieldType = $derived(fieldMeta?.type ?? 'string') |
31 | 34 | const needsName = $derived(!!fieldMeta?.hasName) |
32 | 35 | const operators = $derived(operatorsForType(fieldType)) |
33 | 36 | const multi = $derived(isMultiOperator(operator)) |
| 37 | + const isTls = $derived(fieldType === 'tls') |
| 38 | + // Free-text combobox (datalist) for equals/not_equals on suggestion-backed fields. |
| 39 | + const useCombobox = $derived( |
| 40 | + !!fieldMeta?.suggestions && (operator === 'equals' || operator === 'not_equals') |
| 41 | + ) |
34 | 42 |
|
35 | 43 | const fieldOptions = fields.map((f) => ({ value: f.value, label: f.label })) |
36 | 44 | const operatorOptions = $derived(operators.map((o) => ({ value: o.value, label: o.label }))) |
|
45 | 53 | name, |
46 | 54 | operator, |
47 | 55 | value, |
48 | | - values |
| 56 | + // `buildExpression`/`parseList` consume the raw multi-value text contract; |
| 57 | + // feed the chips joined by newlines so the output format is unchanged. |
| 58 | + values: valuesList.join('\n'), |
| 59 | + tls |
49 | 60 | }) |
50 | 61 |
|
51 | 62 | // Live preview of the single condition the builder controls describe. |
52 | 63 | const preview = $derived(buildExpression(spec)) |
53 | | - const canAdd = $derived(preview !== '') |
| 64 | + // A TLS condition is always complete; otherwise rely on a non-empty preview. |
| 65 | + const canAdd = $derived(isTls || preview !== '') |
54 | 66 |
|
55 | 67 | // Keep the operator valid when the field type changes (e.g. switching from a |
56 | 68 | // string field to Remote IP must not leave "contains any of" selected). |
| 69 | + // Guard against an empty operator list (the `'tls'` field has none). |
57 | 70 | $effect(() => { |
| 71 | + if (operators.length === 0) return |
58 | 72 | const valid = operators.some((o) => o.value === operator) |
59 | 73 | if (!valid) operator = operators[0]?.value ?? '' |
60 | 74 | }) |
|
64 | 78 | name = '' |
65 | 79 | operator = 'equals' |
66 | 80 | value = '' |
67 | | - values = '' |
| 81 | + valuesList = [] |
| 82 | + tls = true |
68 | 83 | combine = 'and' |
69 | 84 | } |
70 | 85 |
|
|
98 | 113 | <code class="font-mono">.referer</code>, |
99 | 114 | <code class="font-mono">.remote_ip</code>, |
100 | 115 | <code class="font-mono">.content_length</code>, |
101 | | - <code class="font-mono">.body</code>, |
102 | 116 | <code class="font-mono">.headers[…]</code>, |
103 | 117 | <code class="font-mono">.args[…]</code>, |
104 | 118 | <code class="font-mono">.cookies[…]</code>. |
|
140 | 154 | {/if} |
141 | 155 | </div> |
142 | 156 |
|
143 | | - <div class="grid gap-4 sm:grid-cols-2"> |
144 | | - <div class="field"> |
145 | | - <label for="waf-operator">Operator</label> |
146 | | - <Select id="waf-operator" bind:value={operator} options={operatorOptions} /> |
147 | | - </div> |
148 | | - </div> |
149 | | -
|
150 | | - {#if multi} |
| 157 | + {#if isTls} |
151 | 158 | <div class="field"> |
152 | | - <label for="waf-values">Values</label> |
153 | | - <div class="textarea"> |
154 | | - <textarea id="waf-values" rows="4" bind:value={values} |
155 | | - placeholder="One value per line (commas also accepted)"></textarea> |
156 | | - </div> |
| 159 | + <label class="checkbox" for="waf-tls"> |
| 160 | + <input id="waf-tls" type="checkbox" bind:checked={tls}> |
| 161 | + <span>TLS</span> |
| 162 | + </label> |
| 163 | + <p class="helper">When on, the request must be served over HTTPS.</p> |
157 | 164 | </div> |
158 | 165 | {:else} |
159 | | - <div class="field"> |
160 | | - <label for="waf-value"> |
161 | | - {#if fieldType === 'numeric'}Number |
162 | | - {:else if fieldType === 'ip' && operator === 'in_cidr'}CIDR |
163 | | - {:else if operator === 'matches_regex'}Pattern |
164 | | - {:else}Value{/if} |
165 | | - </label> |
166 | | - <div class="input"> |
167 | | - <input id="waf-value" class="font-mono" bind:value |
168 | | - inputmode={fieldType === 'numeric' ? 'numeric' : undefined} |
169 | | - placeholder={fieldType === 'numeric' |
170 | | - ? 'e.g. 1048576' |
171 | | - : fieldType === 'ip' && operator === 'in_cidr' |
172 | | - ? 'e.g. 10.0.0.0/8' |
173 | | - : operator === 'matches_regex' |
174 | | - ? 'e.g. ^/api/v[0-9]+/' |
175 | | - : 'Value'}> |
| 166 | + <div class="grid gap-4 sm:grid-cols-2"> |
| 167 | + <div class="field"> |
| 168 | + <label for="waf-operator">Operator</label> |
| 169 | + <Select id="waf-operator" bind:value={operator} options={operatorOptions} /> |
176 | 170 | </div> |
177 | 171 | </div> |
| 172 | +
|
| 173 | + {#if multi} |
| 174 | + <div class="field"> |
| 175 | + <label for="waf-values">Values</label> |
| 176 | + <TagInput id="waf-values" bind:tags={valuesList} |
| 177 | + placeholder="Type a value, press Enter to add" /> |
| 178 | + </div> |
| 179 | + {:else if useCombobox} |
| 180 | + <div class="field"> |
| 181 | + <label for="waf-value">Value</label> |
| 182 | + <div class="input"> |
| 183 | + <input id="waf-value" class="font-mono" bind:value |
| 184 | + list="waf-value-suggestions" placeholder="e.g. GET"> |
| 185 | + </div> |
| 186 | + <datalist id="waf-value-suggestions"> |
| 187 | + {#each fieldMeta?.suggestions ?? [] as s (s)} |
| 188 | + <option value={s}></option> |
| 189 | + {/each} |
| 190 | + </datalist> |
| 191 | + </div> |
| 192 | + {:else} |
| 193 | + <div class="field"> |
| 194 | + <label for="waf-value"> |
| 195 | + {#if fieldType === 'numeric'}Number |
| 196 | + {:else if fieldType === 'ip' && operator === 'in_cidr'}CIDR |
| 197 | + {:else if operator === 'matches_regex'}Pattern |
| 198 | + {:else}Value{/if} |
| 199 | + </label> |
| 200 | + <div class="input"> |
| 201 | + <input id="waf-value" class="font-mono" bind:value |
| 202 | + inputmode={fieldType === 'numeric' ? 'numeric' : undefined} |
| 203 | + placeholder={fieldType === 'numeric' |
| 204 | + ? 'e.g. 1048576' |
| 205 | + : fieldType === 'ip' && operator === 'in_cidr' |
| 206 | + ? 'e.g. 10.0.0.0/8' |
| 207 | + : operator === 'matches_regex' |
| 208 | + ? 'e.g. ^/api/v[0-9]+/' |
| 209 | + : 'Value'}> |
| 210 | + </div> |
| 211 | + </div> |
| 212 | + {/if} |
178 | 213 | {/if} |
179 | 214 |
|
180 | 215 | {#if expression.trim()} |
|
0 commit comments