ファイル
btrc-hub/frontend/src/components/counts/CountsAdditionalConditions.tsx
T
2026-10-29 12:38:03 +09:00

214 行
10 KiB
TypeScript
Raw Blame 履歴

このファイルには曖昧(ambiguous)なUnicode文字が含まれています
このファイルには、他の文字と見間違える可能性があるUnicode文字が含まれています。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 それらの文字を表示するにはエスケープボタンを使用します。
import { useEffect, useState } from 'react'
import CountsConditionValues from '@/components/counts/CountsConditionValues'
import DateTimeField from '@/components/common/DateTimeField'
import { Button } from '@/components/ui/button'
import { countsConditionInputs, countsEventTypes, countsFieldLabel, countsFilterFields,
countsSetOperators, useCountsDefinition }
from '@/lib/countsDefinitions'
import useDialogue from '@/lib/dialogues/useDialogue'
import { countsConditionValue } from '@/lib/countsDisplay'
import { countsAxisLabel } from '@/lib/countsDefinitions'
import { useLocale } from '@/lib/useLocale'
import { inputClass } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import { readCountState, encodeCountState } from '@/lib/queryState'
import type { CountsResult } from '@/lib/counts'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
import type { CountsCondition, CountsDefinition } from '@/lib/countsDefinitions'
const ConditionEditor = (
{ definition, initial, controls, save }: {
definition: CountsDefinition; initial?: CountsCondition
controls: DialogueFormControls
save: (condition: CountsCondition) => void },
) => {
const { label, message } = useUITranslation ('counts')
const fields = definition.filterFields ?? definition.fields
const fieldName = (key: string) => (fields.find (item => item.key === key)?.labels
?? [countsFieldLabel (key)]).map (label => message (
label, fields.find (item => item.key === key)?.labelValues).text).join (' › ')
const [condition, setCondition] = useState<CountsCondition> (() => {
const next = { ...initial, field: initial?.field ?? 'resource_id',
operator: initial?.operator ?? 'eq' }
if (next.operator !== 'range') delete next.end
return next
})
const { field, operator } = condition
const operators = fields.find (item => item.key === field)?.operators ?? []
const inputs = countsConditionInputs (fields.find (item => item.key === field), operator)
useEffect (() => {
controls.setActions ([{ label: label (initial == null ? 'add' : 'shared:actions.edit'),
onSelect: () => save (condition) }])
}, [controls, condition, initial, label, save])
return <div className="space-y-2">
<select value={field} aria-label={message ('field').text}
className={inputClass (false, 'h-11 w-full')}
onChange={event => {
const field = event.target.value
const operator = fields.find (item => item.key === field)?.operators[0]
if (operator != null) setCondition ({ field, operator })
}}>
{fields.filter (item => item.operators.length > 0
&& (!item.legacy || item.key === field)).map (field => (
<option key={field.key} value={field.key}>{fieldName (field.key)}</option>))}
</select>
<select value={operator} aria-label={message ('operator').text}
className={inputClass (false, 'h-11 w-full')}
onChange={event => setCondition (previous => {
const operator = event.target.value
const next = { ...previous, operator }
if (operator !== 'range') delete next.end
if ((operator === 'like') !== (previous.operator === 'like')) delete next.value
const choices = countsConditionInputs (fields.find (item => item.key === field), operator)
if (countsSetOperators.includes (operator))
{
delete next.value
delete next.end
next.input = choices.includes (previous.input ?? 'single') ? previous.input : choices[0]
}
else
{
delete next.values
delete next.input
}
return next
})}>
{operators.map (key => <option key={key} value={key}>
{message (`operators.${key}`).text}</option>)}
</select>
{inputs.length > 1 && <label className="block space-y-2 text-sm">
<span>{label ('counts:pattern.input')}</span>
<select value={condition.input ?? inputs[0]}
className={inputClass (false, 'h-11 w-full')}
onChange={event => setCondition (previous => ({ ...previous,
input: event.target.value as CountsCondition['input'] }))}>
{inputs.map (input => <option key={input} value={input}>
{message (`counts:pattern.${input}`).text}</option>)}
</select>
</label>}
</div>
}
const CountsAdditionalConditions = ({ params, update, conditionLabels }: {
conditionLabels?: CountsResult['conditionLabels']
params: URLSearchParams
update: (key: string, value: string) => void },
) => {
const { label, message } = useUITranslation ('counts')
const definition = useCountsDefinition ()
const dialogue = useDialogue ()
const locale = useLocale ()
const conditions = readCountState<CountsCondition[]> (params, 'conditions', [])
const metadata = definition.filterFields ?? definition.fields
const fields = metadata.filter (field => field.operators.length > 0 && !field.legacy)
const fieldName = (key: string) => (metadata.find (item => item.key === key)?.labels
?? [countsFieldLabel (key)]).map (label => message (
label, metadata.find (item => item.key === key)?.labelValues).text).join (' › ')
const write = (next: CountsCondition[]) => update (
'conditions', encodeCountState ('conditions', next))
const replace = (index: number, condition: CountsCondition) => write (
conditions.map ((item, position) => position === index ? condition : item))
const patch = (index: number, change: Partial<CountsCondition>) =>
replace (index, { ...conditions[index], ...change })
const edit = (index?: number) => {
const initial = index == null ? undefined : conditions[index]
const save = (condition: CountsCondition) => {
if (index == null) write ([...conditions, condition])
else replace (index, condition)
}
void dialogue.form ({ title: label ('additionalConditions'), footerLayout: 'inline',
body: controls => <ConditionEditor definition={definition}
initial={initial} controls={controls} save={save}/> })
}
const valueInput = (condition: CountsCondition, index: number, key: 'value' | 'end') => {
const type = countsFilterFields (definition)[condition.field]
const aria = message (key === 'end' ? 'through'
: (condition.operator === 'range' ? 'from' : 'value'))
if (type === 'boolean' || condition.field === 'event_type')
{
const options: string[] = type === 'boolean' ? ['true', 'false'] : [...countsEventTypes]
const namespace = type === 'boolean' ? 'truthValues' : 'events'
return <select value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
aria-label={aria.text}
lang={aria.language}
onChange={event => patch (index, { [key]: event.target.value })}>
<option value=""/>
{options.map (value => <option key={value} value={value}
lang={message (`${namespace}.${value}`).language}>
{message (`${namespace}.${value}`).text}</option>)}
</select>
}
if (type === 'datetime')
return <DateTimeField precision="second" className="h-10 w-48 min-w-0 max-w-full py-1"
value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
aria-label={aria.text} lang={aria.language}
onChange={value => patch (index, { [key]: value ?? '' })}/>
return <input value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
type={(type === 'integer' || type === 'interval') ? 'number' : 'text'}
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
aria-label={aria.text} lang={aria.language}
onChange={event => patch (index, { [key]: event.target.value })}/>
}
return <fieldset className="min-w-0 space-y-1">
<legend className="flex items-center gap-2">{label ('additionalConditions')}
<Button type="button" variant="outline" size="sm" disabled={fields.length === 0}
onClick={() => edit ()}>{label ('add')}</Button></legend>
{conditions.map ((condition, index) => condition.operator === 'axis_eq' ? (
<div key={index} className="flex flex-wrap items-center gap-2 border-b py-2">
<span>{countsAxisLabel (condition, definition, locale).text}</span>
<span>{label ('operatorSymbols.eq')}</span>
<span>{countsConditionValue (condition.value, condition, locale,
params.get ('timezone') ?? 'UTC', conditionLabels)}</span>
<Button type="button" variant="ghost" size="sm"
aria-label={message ('remove').text}
onClick={() => write (conditions.filter ((_, position) => position !== index))}>
×</Button>
</div>) : (
<div key={index} className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-2
border-b border-slate-200 py-2 dark:border-slate-700">
<Button type="button" variant="ghost" size="sm" onClick={() => edit (index)}
aria-label={`${fieldName (condition.field)} ${message (
`operators.${condition.operator}`).text}`}
className="min-w-0 max-w-full justify-start gap-2 whitespace-normal px-1 text-left">
<span className="min-w-0 break-words">{fieldName (condition.field)}</span>
{condition.operator !== 'range' && <span aria-hidden="true" className="shrink-0">
{label (`operatorSymbols.${condition.operator}`)}</span>}
</Button>
{countsSetOperators.includes (condition.operator) ? (
<CountsConditionValues key={`${condition.field}:${condition.operator}`}
condition={condition}
field={metadata.find (field => field.key === condition.field)!}
definition={definition} params={params}
maximum={Math.max (0, 100 - conditions.reduce ((sum, item, position) =>
sum + (position === index ? 0 : item.values?.length ?? 0), 0))}
change={values => patch (index, { values })}/>) : (
<div className="inline-flex min-w-0 max-w-full items-center gap-1">
{valueInput (condition, index, 'value')}
{condition.operator === 'range' && <>
<span aria-hidden="true" className="shrink-0">{label ('operatorSymbols.range')}</span>
{valueInput (condition, index, 'end')}
</>}
</div>)}
<div className="inline-flex shrink-0 items-center gap-1">
<label className="inline-flex items-center gap-1 text-sm">
<input type="checkbox" checked={condition.blank ?? false}
onChange={event => patch (index, { blank: event.target.checked })}/>
{label ('blank')}</label>
<Button type="button" variant="ghost" size="sm" className="px-1"
aria-label={message ('remove').text}
onClick={() => write (conditions.filter ((_, position) => index !== position))}>
×</Button>
</div>
</div>))}
</fieldset>
}
export default CountsAdditionalConditions