From 7536330ecfc7d4e2b539a92c2863d500a971a72d Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 29 Oct 2026 12:28:20 +0900 Subject: [PATCH] #434 --- backend/app/services/counts_condition.rb | 10 ++++ backend/app/services/counts_population.rb | 46 +++++++++++-------- backend/app/services/query_state.rb | 5 +- .../counts/CountsAdditionalConditions.tsx | 34 +++++++++++++- .../counts/CountsSummaryResults.tsx | 8 ++-- frontend/src/lib/counts.ts | 18 +++++++- frontend/src/lib/countsDefinitions.ts | 2 +- frontend/src/lib/queryState.ts | 8 ++-- 8 files changed, 99 insertions(+), 32 deletions(-) diff --git a/backend/app/services/counts_condition.rb b/backend/app/services/counts_condition.rb index 63cbe87..2f44dce 100644 --- a/backend/app/services/counts_condition.rb +++ b/backend/app/services/counts_condition.rb @@ -82,6 +82,16 @@ class CountsCondition def apply(scope, entry) raise ArgumentError unless entry.is_a?(Hash) + if entry['operator'] == 'axis_eq' + raise ArgumentError unless (entry.keys - ['field', 'operator', 'grain', 'value']).empty? + value = entry['value'] + unless value.nil? || value.is_a?(String) || value.is_a?(Numeric) + raise ArgumentError + end + raise ArgumentError if value.is_a?(String) && value.bytesize > 8192 + + return @population.axis_condition(scope, entry) + end allowed = ['field', 'operator', 'value', 'end', 'blank', 'input', 'values'] raise ArgumentError if (entry.keys - allowed).any? diff --git a/backend/app/services/counts_population.rb b/backend/app/services/counts_population.rb index 97611db..cb3493a 100644 --- a/backend/app/services/counts_population.rb +++ b/backend/app/services/counts_population.rb @@ -62,6 +62,29 @@ class CountsPopulation @authorisation_flg ? HistoryAccess.sql(model, row) : 'TRUE' end + def axis_condition(scope, entry) + key = entry.fetch('field') + if key == 'tags' + scope = scope.where(snapshot_membership(entry.fetch('value'))) + elsif key == 'changed_field' + raise ArgumentError unless history && definition.changes.include?(entry['value']) + + scope = scope.where(Arel.sql(changed_sql(entry['value']))) + elsif definition.fields[key] == :datetime && entry['grain'] + scope = scope.where(definition.expression(key) => nil) if entry['value'].nil? + if entry['value'] + expression = CountsAxes.time_expression(definition.expression(key), + entry['grain'], timezone) + scope = scope.where("#{ expression } = ?", entry['value']) + end + else + raise ArgumentError unless definition.axes(history).include?(key) + + scope = scope.where(definition.expression(key) => entry['value']) + end + scope + end + private def make_population @@ -69,7 +92,8 @@ class CountsPopulation @scope = @scope.from("#{ table } v").select('v.*') previous = ['rows', 'cols'].any? do |key| QueryState.axes(params.fetch(key, '')).any? { _1['field'] == 'changed_field' } - end || QueryState.records(params, 'drill').any? { _1['field'] == 'changed_field' } + end || (QueryState.records(params, 'drill') + QueryState.records(params, 'conditions')) + .any? { _1['field'] == 'changed_field' } if previous keys = definition.keys.map { "previous.#{ _1 } = v.#{ _1 }" }.join(' AND ') @scope = @scope.joins("LEFT JOIN #{ table } previous ON #{ keys } " + @@ -127,25 +151,7 @@ class CountsPopulation raise ArgumentError unless drills.is_a?(Array) && drills.length <= 100 drills.each do |entry| - key = entry.fetch('field') - if key == 'tags' - @scope = @scope.where(snapshot_membership(entry.fetch('value'))) - elsif key == 'changed_field' - raise ArgumentError unless history && definition.changes.include?(entry['value']) - - @scope = @scope.where(Arel.sql(changed_sql(entry['value']))) - elsif definition.fields[key] == :datetime && entry['grain'] - @scope = @scope.where(definition.expression(key) => nil) if entry['value'].nil? - if entry['value'] - expression = CountsAxes.time_expression(definition.expression(key), - entry['grain'], timezone) - @scope = @scope.where("#{ expression } = ?", entry['value']) - end - else - raise ArgumentError unless definition.axes(history).include?(key) - - @scope = @scope.where(definition.expression(key) => entry['value']) - end + @scope = axis_condition(@scope, entry) end end diff --git a/backend/app/services/query_state.rb b/backend/app/services/query_state.rb index 5002e30..b508b81 100644 --- a/backend/app/services/query_state.rb +++ b/backend/app/services/query_state.rb @@ -1,7 +1,7 @@ # Public query grammar shared with frontend/src/lib/queryState.ts. class QueryState SCHEMAS = { - 'conditions' => ['field', 'operator', 'value', 'end', 'blank', 'input', 'values'], + 'conditions' => ['field', 'operator', 'value', 'end', 'blank', 'input', 'values', 'grain', 'value_type'], 'condition_values' => ['condition', 'value', 'value_type'], 'drill' => ['field', 'grain', 'value', 'value_type'], 'tag_refs' => ['name', 'tag_id', 'external_tag_id', 'negative', 'missing'], @@ -57,7 +57,8 @@ class QueryState records.sort.map do |_index, record| type = record.delete('value_type') - if ['drill', 'measures', 'column_order', 'condition_values'].include?(key) + if ['drill', 'measures', 'column_order', 'condition_values'].include?(key) || + (key == 'conditions' && record['operator'] == 'axis_eq') raise ArgumentError unless ['null', 'string', 'number', 'boolean'].include?(type) raise ArgumentError unless type == 'null' || record.key?('value') diff --git a/frontend/src/components/counts/CountsAdditionalConditions.tsx b/frontend/src/components/counts/CountsAdditionalConditions.tsx index 23a1d11..e2e9049 100644 --- a/frontend/src/components/counts/CountsAdditionalConditions.tsx +++ b/frontend/src/components/counts/CountsAdditionalConditions.tsx @@ -7,6 +7,9 @@ import { countsConditionInputs, countsEventTypes, countsFieldLabel, countsFilter countsSetOperators, useCountsDefinition } from '@/lib/countsDefinitions' import useDialogue from '@/lib/dialogues/useDialogue' +import { countsSummaryValue } from '@/lib/countsDisplay' +import { countsAxisLabel } from '@/lib/countsDefinitions' +import { useLocale } from '@/lib/useLocale' import { inputClass } from '@/lib/utils' import { useUITranslation } from '@/lib/useUITranslation' @@ -97,6 +100,7 @@ const CountsAdditionalConditions = ({ params, update }: { const { label, message } = useUITranslation ('counts') const definition = useCountsDefinition () const dialogue = useDialogue () + const locale = useLocale () const conditions = readCountState (params, 'conditions', []) const metadata = definition.filterFields ?? definition.fields const fields = metadata.filter (field => field.operators.length > 0 && !field.legacy) @@ -153,7 +157,35 @@ const CountsAdditionalConditions = ({ params, update }: { {label ('additionalConditions')} - {conditions.map ((condition, index) => ( + {params.get ('tags') && params.get ('match') &&
+ match = {params.get ('match')} + +
} + {readCountState[]> ( + params, 'tag_refs', []).map ((reference, index, references) => ( +
+ {Object.entries (reference).map (([key, value]) => + `${key}=${value}`).join (' ')} + +
))} + {conditions.map ((condition, index) => condition.operator === 'axis_eq' ? ( +
+ {countsAxisLabel (condition, definition, locale).text} + {label ('operatorSymbols.eq')} + {condition.value == null ? 'NULL' : (condition.field === 'tags' + ? condition.value : countsSummaryValue ( + condition.value, condition, 0, { count: 0, page: 1, limit: 100 }, locale, + params.get ('timezone') ?? 'UTC'))} + +
) : (