diff --git a/dashboard/src/components/FilterQuery.tsx b/dashboard/src/components/FilterQuery.tsx
index 69ef98fd29d..20351c33a6f 100644
--- a/dashboard/src/components/FilterQuery.tsx
+++ b/dashboard/src/components/FilterQuery.tsx
@@ -22,8 +22,12 @@ import { attributeFilter } from "@utils/CommonViewFunction";
import {
queryBuilderDateRangeUIValueToAPI,
systemAttributes,
- filterQueryValue
+ filterQueryValue,
+ getDisplayOperator
} from "@utils/Enum";
+import { removeCriterionFromFilterUrl } from "@utils/filterUrlCriterionRemoval";
+import type { ParsedApiFilter } from "@utils/filterUrlCriterionRemoval";
+import { dateTimeFormat } from "@utils/Global";
import moment from "moment";
import { useLocation, useNavigate } from "react-router-dom";
import { globalSearchFilterInitialQuery } from "@utils/Utils";
@@ -45,13 +49,18 @@ export const FilterQuery = ({ value }: any) => {
);
} else {
- if (obj.type === "date" || obj.id == "createTime") {
+ const isDateAttr =
+ obj.type === "date" ||
+ obj.id === "createTime" ||
+ obj.id === "__timestamp" ||
+ obj.id === "__modificationTimestamp";
+ if (isDateAttr) {
if (queryBuilderDateRangeUIValueToAPI[obj.value]) {
obj.value = queryBuilderDateRangeUIValueToAPI[obj.value];
+ } else if (/^\d+$/.test(String(obj.value))) {
+ obj.value = `${moment(Number(obj.value)).format(dateTimeFormat)} (${moment.tz(moment.tz.guess()).zoneAbbr()})`;
} else {
- obj.value = `${obj.value} (${moment
- .tz(moment.tz.guess())
- .zoneAbbr()})`;
+ obj.value = `${obj.value} (${moment.tz(moment.tz.guess()).zoneAbbr()})`;
}
}
@@ -60,6 +69,7 @@ export const FilterQuery = ({ value }: any) => {
color="primary"
className="chip-items"
data-type={type}
+ data-rule-key={key}
data-id={`${obj.id}${key}`}
data-cy={`${obj.id}${key}`}
label={
@@ -71,7 +81,7 @@ export const FilterQuery = ({ value }: any) => {
: obj.id}
- {obj.operator}{" "}
+ {getDisplayOperator(obj.operator)}{" "}
{filterQueryValue[obj.id]
@@ -97,9 +107,138 @@ export const FilterQuery = ({ value }: any) => {
const clearQueryAttr = (e: any) => {
const searchParams = new URLSearchParams(location.search);
- const currentType: any = e?.target
- ?.closest("[data-type]")
- ?.getAttribute("data-type");
+ const chipEl = (e?.target as HTMLElement)?.closest?.(
+ "[data-type]"
+ ) as HTMLElement | null;
+ const currentType = chipEl?.getAttribute("data-type");
+ const ruleKeyRaw = chipEl?.getAttribute("data-rule-key");
+
+ const navigateAfterChange = (sp: URLSearchParams) => {
+ const meaningfulFilterParams = [
+ "type",
+ "tag",
+ "query",
+ "term",
+ "relationshipName",
+ "entityFilters",
+ "tagFilters",
+ "relationshipFilters",
+ "excludeST",
+ "excludeSC",
+ "includeDE"
+ ];
+ const hasMeaningfulFilters = meaningfulFilterParams.some((param) =>
+ sp.has(param)
+ );
+ if (!hasMeaningfulFilters) {
+ navigate({
+ pathname: "/search"
+ });
+ } else if ([...sp]?.length <= 1) {
+ navigate({
+ pathname: "/search"
+ });
+ } else {
+ navigate({
+ pathname: "/search/searchResult",
+ search: sp.toString()
+ });
+ }
+ };
+
+ const syncFilterParamGlobalState = (
+ paramKey: "entityFilters" | "tagFilters" | "relationshipFilters",
+ urlValue: string | null
+ ) => {
+ if (!urlValue) {
+ globalSearchFilterInitialQuery.setQuery({ [paramKey]: [] });
+ return;
+ }
+ const api = attributeFilter.extractUrl({
+ value: urlValue,
+ apiObj: true
+ }) as ParsedApiFilter | null;
+ if (!api?.criterion || !Array.isArray(api.criterion)) {
+ globalSearchFilterInitialQuery.setQuery({ [paramKey]: [] });
+ return;
+ }
+ globalSearchFilterInitialQuery.setQuery({
+ [paramKey]: {
+ combinator: String(api.condition || "AND").toLowerCase(),
+ rules: api.criterion.map((rule: any, i: number) => ({
+ id: `url-rule-${i}`,
+ field: rule.attributeName,
+ operator: getDisplayOperator(rule.operator) || rule.operator,
+ value: rule.attributeValue,
+ ...(rule.type ? { type: rule.type } : {})
+ }))
+ }
+ });
+ };
+
+ const isFilterCriterionChip =
+ ruleKeyRaw !== null &&
+ ruleKeyRaw !== "" &&
+ (currentType === "entityFilters" ||
+ currentType === "tagFilters" ||
+ currentType === "relationshipFilters");
+
+ if (isFilterCriterionChip) {
+ const idx = Number.parseInt(ruleKeyRaw as string, 10);
+ if (Number.isNaN(idx)) {
+ return;
+ }
+ const paramKey = currentType as
+ | "entityFilters"
+ | "tagFilters"
+ | "relationshipFilters";
+ const raw = searchParams.get(paramKey);
+ if (!raw) {
+ return;
+ }
+ const allowSimplify = paramKey === "entityFilters";
+ const result = removeCriterionFromFilterUrl(raw, idx, allowSimplify);
+ if (!result) {
+ return;
+ }
+ if (result.kind === "empty") {
+ searchParams.delete(paramKey);
+ globalSearchFilterInitialQuery.setQuery({ [paramKey]: [] });
+ navigateAfterChange(searchParams);
+ return;
+ }
+ if (result.kind === "singleTypeName") {
+ searchParams.set("type", result.typeName);
+ searchParams.delete("entityFilters");
+ if (searchParams.get("includeDE") === "true") {
+ const delUrl = attributeFilter.generateUrl({
+ value: {
+ condition: "AND",
+ criterion: [
+ {
+ attributeName: "__state",
+ operator: "eq",
+ attributeValue: "DELETED"
+ }
+ ]
+ }
+ });
+ if (delUrl) {
+ searchParams.set("entityFilters", delUrl);
+ }
+ }
+ syncFilterParamGlobalState(
+ "entityFilters",
+ searchParams.get("entityFilters")
+ );
+ navigateAfterChange(searchParams);
+ return;
+ }
+ searchParams.set(paramKey, result.url);
+ syncFilterParamGlobalState(paramKey, result.url);
+ navigateAfterChange(searchParams);
+ return;
+ }
if (currentType == "term") {
searchParams.delete("gtype");
@@ -127,43 +266,11 @@ export const FilterQuery = ({ value }: any) => {
globalSearchFilterInitialQuery.setQuery({ [currentType]: [] });
}
- searchParams.delete(currentType);
-
- // Check if there are any meaningful filters left after removal
- const meaningfulFilterParams = [
- "type",
- "tag",
- "query",
- "term",
- "relationshipName",
- "entityFilters",
- "tagFilters",
- "relationshipFilters",
- "excludeST",
- "excludeSC",
- "includeDE"
- ];
-
- const hasMeaningfulFilters = meaningfulFilterParams.some((param) =>
- searchParams.has(param)
- );
-
- // If no meaningful filters remain, navigate to clear all (like Clear button)
- if (!hasMeaningfulFilters) {
- navigate({
- pathname: "/search"
- });
- } else if ([...searchParams]?.length <= 1) {
- // Only searchType or other system params remain
- navigate({
- pathname: "/search"
- });
- } else {
- navigate({
- pathname: "/search/searchResult",
- search: searchParams.toString()
- });
+ if (currentType) {
+ searchParams.delete(currentType);
}
+
+ navigateAfterChange(searchParams);
};
if (value.type) {
diff --git a/dashboard/src/utils/CommonViewFunction.ts b/dashboard/src/utils/CommonViewFunction.ts
index 3760de5a538..d58357bfd42 100644
--- a/dashboard/src/utils/CommonViewFunction.ts
+++ b/dashboard/src/utils/CommonViewFunction.ts
@@ -193,13 +193,19 @@ export const attributeFilter = {
let temp = obj.split("::") || obj.split("|" + spliter + "|");
let rule = {};
if (apiObj) {
+ const attrName = temp[0];
+ const isDateAttr =
+ temp[3] === "date" ||
+ attrName === "__timestamp" ||
+ attrName === "__modificationTimestamp" ||
+ attrName === "createTime";
rule = {
- attributeName: temp[0],
+ attributeName: attrName,
operator: mapUiOperatorToAPI(temp[1]),
attributeValue: temp[2]?.trim()
};
rule.attributeValue =
- rule.type === "date" && formatDate && rule.attributeValue.length
+ isDateAttr && formatDate && rule.attributeValue?.length
? formatedDate({
date: parseInt(rule.attributeValue),
zone: false
@@ -231,9 +237,12 @@ export const attributeFilter = {
rule.value;
}
} else if (
- (rule.type === "date" || rule.attributeName == "createTime") &&
+ (rule.type === "date" ||
+ rule.attributeName === "createTime" ||
+ rule.id === "__timestamp" ||
+ rule.id === "__modificationTimestamp") &&
formatDate &&
- rule.value.length
+ rule.value?.length
) {
rule.value = formatedDate({
date: parseInt(rule.value),
diff --git a/dashboard/src/utils/filterUrlCriterionRemoval.ts b/dashboard/src/utils/filterUrlCriterionRemoval.ts
new file mode 100644
index 00000000000..746fe7998f2
--- /dev/null
+++ b/dashboard/src/utils/filterUrlCriterionRemoval.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { attributeFilter } from "./CommonViewFunction";
+
+export type RemoveCriterionResult =
+ | { kind: "empty" }
+ | { kind: "url"; url: string }
+ | { kind: "singleTypeName"; typeName: string };
+
+type ApiFilterCriterionRow = {
+ attributeName: string;
+ operator: string;
+ attributeValue: string;
+ type?: string;
+};
+
+/** Narrowing shape for `attributeFilter.extractUrl(..., { apiObj: true })`. */
+export type ParsedApiFilter = {
+ condition: string;
+ criterion: ApiFilterCriterionRow[];
+};
+
+/**
+ * Drops one criterion from a serialized filter URL (entity / tag / relationship).
+ * When allowTypeParamSimplification is true and exactly one __typeName eq remains,
+ * returns singleTypeName so the UI can use type= (matches sidebar search).
+ */
+export const removeCriterionFromFilterUrl = (
+ filterUrl: string,
+ index: number,
+ allowTypeParamSimplification = false
+): RemoveCriterionResult | null => {
+ if (!filterUrl || index < 0) {
+ return null;
+ }
+ const parsed = attributeFilter.extractUrl({
+ value: filterUrl,
+ apiObj: true
+ }) as ParsedApiFilter | null;
+ if (!parsed?.criterion || !Array.isArray(parsed.criterion)) {
+ return null;
+ }
+ const crit = [...parsed.criterion];
+ if (index >= crit.length) {
+ return null;
+ }
+ crit.splice(index, 1);
+ if (crit.length === 0) {
+ return { kind: "empty" };
+ }
+ if (
+ allowTypeParamSimplification &&
+ crit.length === 1 &&
+ crit[0].attributeName === "__typeName" &&
+ String(crit[0].operator).toLowerCase() === "eq"
+ ) {
+ return { kind: "singleTypeName", typeName: String(crit[0].attributeValue) };
+ }
+ const url = attributeFilter.generateUrl({
+ value: { condition: parsed.condition, criterion: crit }
+ });
+ if (!url) {
+ return { kind: "empty" };
+ }
+ return { kind: "url", url };
+};