diff --git a/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanel.tsx b/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanel.tsx index 09ada3ab9b..a51bcf86fb 100644 --- a/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanel.tsx +++ b/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanel.tsx @@ -3,6 +3,7 @@ import { ExtraFilter } from "../types"; import { escapeForLogsQLString } from "../../../utils/regexp"; import ExtraFiltersPanelItem from "./ExtraFiltersPanelItem"; import "./style.scss"; +import { isStreamFilter } from "../utils/isStreamFilter"; type Props = { extraFilters: ExtraFilter[]; @@ -13,7 +14,7 @@ const ExtraFiltersPanel: FC = ({ extraFilters, onRemove }) => { const getLabel = (filter: ExtraFilter) => { const escapedValue = escapeForLogsQLString(filter.value); const expr = `${filter.field}${filter.operator}"${escapedValue}"`; - return filter.isStream ? `{${expr}}` : expr; + return isStreamFilter(filter) ? `{${expr}}` : expr; }; const formattedFilters = useMemo(() => { diff --git a/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanelItem.tsx b/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanelItem.tsx index 3c528b91e0..45d332ec22 100644 --- a/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanelItem.tsx +++ b/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/ExtraFiltersPanelItem.tsx @@ -1,9 +1,11 @@ import { FC, useEffect, useRef, useState } from "preact/compat"; -import { CloseIcon } from "../../Main/Icons"; +import { CloseIcon, FieldIcon, FieldStreamIcon } from "../../Main/Icons"; import { ExtraFilter } from "../types"; import "./style.scss"; import { useCallback } from "react"; import Tooltip from "../../Main/Tooltip/Tooltip"; +import classNames from "classnames"; +import { isStreamFilter } from "../utils/isStreamFilter"; type Props = { filter: ExtraFilter & { label: string }; @@ -14,6 +16,8 @@ const ExtraFiltersPanelItem: FC = ({ filter, onRemove }) => { const labelRef = useRef(null); const [isOverflownLabel, setIsOverflownLabel] = useState(false); + const isStream = isStreamFilter(filter); + const handleRemove = useCallback(() => { onRemove(filter.field, filter.value); }, [filter, onRemove]); @@ -24,29 +28,36 @@ const ExtraFiltersPanelItem: FC = ({ filter, onRemove }) => { }, [filter.label, labelRef]); return ( - {filter.value}

} - disabled={!isOverflownLabel} +
-
+
+ {isStream ? : } +
+ + {filter.value}

} + disabled={!isOverflownLabel} > -
{filter.label}
-
- -
+
+
+
- +
); }; diff --git a/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/style.scss b/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/style.scss index 161a24a707..bee43d34b4 100644 --- a/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/style.scss +++ b/app/vmui/packages/vmui/src/components/ExtraFilters/ExtraFiltersPanel/style.scss @@ -16,16 +16,44 @@ $bg-color-dark: $color-background-body; font-size: $font-size-small; &-item { + position: relative; display: flex; align-items: center; justify-content: center; gap: calc($padding-small / 2); padding-block: 1px; - padding-inline: $padding-small 2px; + padding-inline: calc($padding-small / 2) 2px; max-width: clamp(180px, 50%, 360px); border-radius: $border-radius-button; color: $color-text-secondary; - background-color: $color-hover-black; + z-index: 1; + + &:after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: $border-radius-button; + background-color: currentColor; + opacity: 0.1; + z-index: -1; + } + + &_stream { + color: $color-dodger-blue; + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + min-height: 22px; + max-height: 22px; + aspect-ratio: 1; + } &__tooltip { max-width: calc(clamp(180px, 24vw, 360px) - ($padding-small * 2)); @@ -34,17 +62,18 @@ $bg-color-dark: $color-background-body; } &__label { + color: $color-text; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } &__remove { + color: $color-text-secondary; display: flex; align-items: center; justify-content: center; border-radius: calc($border-radius-medium - 2px); - color: $color-text-secondary; background-color: transparent; padding: calc($padding-small / 2); cursor: pointer; diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx index bb6db3d37c..ac3fd36610 100644 --- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx @@ -659,3 +659,34 @@ export const SidebarCloseIcon = () => ( /> ); + +export const FieldStreamIcon = () => ( + + + + + +); + +export const FieldIcon = () => ( + + + + +); diff --git a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRow.tsx b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRow.tsx index 3638d61432..a979f63cc7 100644 --- a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRow.tsx +++ b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRow.tsx @@ -2,14 +2,17 @@ import { FC, memo } from "preact/compat"; import classNames from "classnames"; import useDeviceDetect from "../../../hooks/useDeviceDetect"; import FieldRowActions from "./GroupLogsFieldRowMenu/FieldRowActions"; +import { FieldStreamIcon } from "../../Main/Icons"; +import Tooltip from "../../Main/Tooltip/Tooltip"; interface Props { field: string; value: string; + isStreamField: boolean; hideGroupButton?: boolean; } -const GroupLogsFieldRow: FC = ({ field, value, hideGroupButton = false }) => { +const GroupLogsFieldRow: FC = ({ field, value, isStreamField, hideGroupButton = false }) => { const { isMobile } = useDeviceDetect(); return ( @@ -19,12 +22,18 @@ const GroupLogsFieldRow: FC = ({ field, value, hideGroupButton = false }) "vm-group-logs-row-fields-item_mobile": isMobile })} > + + + {isStreamField ? : null} + + {field} {value} diff --git a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterExclude.tsx b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterExclude.tsx index 2a71488478..efd7a2bcf9 100644 --- a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterExclude.tsx +++ b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterExclude.tsx @@ -6,16 +6,17 @@ import { useExtraFilters } from "../../../ExtraFilters/hooks/useExtraFilters"; interface Props { field: string; value: string; + isStreamField: boolean; onClose: () => void; } -const FieldFilterExclude: FC = ({ field, value, onClose }) => { +const FieldFilterExclude: FC = ({ field, value, isStreamField, onClose }) => { const { extraFilters, addNewFilter, removeFilterByValue } = useExtraFilters(); const filtersByValue = extraFilters.filter(f => f.field === field && f.value === value); const isExcludeFilter = filtersByValue.some(f => f.operator === ExtraFilterOperator.NotEquals); const handleClickFilter = () => { - const newFilter = { field, value, operator: ExtraFilterOperator.NotEquals }; + const newFilter = { field, value, operator: ExtraFilterOperator.NotEquals, isStream: isStreamField }; if (isExcludeFilter) { // If the same filter already exists, we remove it by setting the value to an empty string removeFilterByValue(field, value); diff --git a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterInclude.tsx b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterInclude.tsx index 2c79b250d1..1fe6f8eb02 100644 --- a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterInclude.tsx +++ b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldFilterInclude.tsx @@ -8,15 +8,16 @@ import { useExtraFilters } from "../../../ExtraFilters/hooks/useExtraFilters"; interface Props { field: string; value: string; + isStreamField: boolean; } -const FieldFilterInclude: FC = ({ field, value }) => { +const FieldFilterInclude: FC = ({ field, value, isStreamField }) => { const { extraFilters, addNewFilter, removeFilterByValue } = useExtraFilters(); const filtersByValue = extraFilters.filter(f => f.field === field && f.value === value); const isIncludeFilter = filtersByValue.some(f => f.operator === ExtraFilterOperator.Equals); const handleClickFilter = () => { - const newFilter = { field, value, operator: ExtraFilterOperator.Equals }; + const newFilter = { field, value, operator: ExtraFilterOperator.Equals, isStream: isStreamField }; if (isIncludeFilter) { // If the same filter already exists, we remove it by setting the value to an empty string removeFilterByValue(field, value); diff --git a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldRowActions.tsx b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldRowActions.tsx index 7e78ca06be..3eab44c37d 100644 --- a/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldRowActions.tsx +++ b/app/vmui/packages/vmui/src/components/Views/GroupView/GroupLogsFieldRowMenu/FieldRowActions.tsx @@ -12,10 +12,11 @@ import Popper from "../../../Main/Popper/Popper"; type Props = { field: string; value: string; + isStreamField: boolean; hideGroupButton: boolean; } -const FieldRowActions: FC = ({ field, value, hideGroupButton }) => { +const FieldRowActions: FC = ({ field, value, isStreamField, hideGroupButton }) => { const { value: openContextMenu, setFalse: handleCloseContextMenu, @@ -40,6 +41,7 @@ const FieldRowActions: FC = ({ field, value, hideGroupButton }) => {
@@ -62,6 +64,7 @@ const FieldRowActions: FC = ({ field, value, hideGroupButton }) => { = ({ log, hideGroupButton }) => { return key.toLowerCase().includes(searchLower) || String(value).toLowerCase().includes(searchLower); }), [rawEntries, search]); + const streamFields = useMemo(() => { + if (!log._stream) return []; + return getStreamKeys(log._stream); + }, [log]); + const [disabledHovers] = useLocalStorageBoolean("LOGS_DISABLED_HOVERS"); return ( @@ -59,6 +65,7 @@ const GroupLogsFields: FC = ({ log, hideGroupButton }) => { key={key} field={key} value={value} + isStreamField={streamFields.includes(key) || key === "_stream"} hideGroupButton={hideGroupButton} /> ))} diff --git a/app/vmui/packages/vmui/src/components/Views/GroupView/style.scss b/app/vmui/packages/vmui/src/components/Views/GroupView/style.scss index 840adec671..d4205560d9 100644 --- a/app/vmui/packages/vmui/src/components/Views/GroupView/style.scss +++ b/app/vmui/packages/vmui/src/components/Views/GroupView/style.scss @@ -441,6 +441,18 @@ $actions-width: calc(($actions-buttons * 30px) + 10px); } } + &__icon { + display: flex; + align-items: center; + justify-content: center; + max-width: 20px; + min-width: 20px; + padding: 4px; + aspect-ratio: 1; + color: $color-text-secondary; + margin-left: calc($padding-small / 2); + } + &__key, &__value { vertical-align: top; diff --git a/docs/victorialogs/CHANGELOG.md b/docs/victorialogs/CHANGELOG.md index 52feb8f47b..aa5357f8a7 100644 --- a/docs/victorialogs/CHANGELOG.md +++ b/docs/victorialogs/CHANGELOG.md @@ -22,6 +22,8 @@ according to the following docs: ## tip +* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): visually distinguish stream fields in expanded log entries and active filters, and automatically use [stream filters](https://docs.victoriametrics.com/victorialogs/logsql/#stream-filter) for include and exclude actions on stream fields. See [#1607](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1607). + ## [v1.52.0](https://github.com/VictoriaMetrics/VictoriaLogs/releases/tag/v1.52.0) Released at 2026-07-16