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