feat(grid): add begins-with and ends-with filter modes

Closes #7282
This commit is contained in:
t8y2
2026-08-26 16:40:17 +08:00
parent f12ac21453
commit d85816c3ed
14 changed files with 124 additions and 3 deletions
@@ -1032,6 +1032,8 @@ const allFilterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [
{ value: "not-equals", labelKey: "grid.filterBuilderNotEquals" },
{ value: "like", labelKey: "grid.filterBuilderContains" },
{ value: "not-like", labelKey: "grid.filterBuilderNotContains" },
{ value: "begins-with", labelKey: "grid.filterBuilderBeginsWith" },
{ value: "ends-with", labelKey: "grid.filterBuilderEndsWith" },
{ value: "greater-than", labelKey: "grid.filterBuilderGreaterThan" },
{
value: "greater-than-or-equal",
+2
View File
@@ -1711,6 +1711,8 @@ export default {
filterBuilderNotEquals: "Does not equal",
filterBuilderContains: "Contains",
filterBuilderNotContains: "Does not contain",
filterBuilderBeginsWith: "Starts with",
filterBuilderEndsWith: "Ends with",
filterBuilderGreaterThan: "Greater than",
filterBuilderGreaterThanOrEqual: "Greater than or equal to",
filterBuilderLessThan: "Less than",
+2
View File
@@ -1594,6 +1594,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "Distinto de",
filterBuilderContains: "Contiene",
filterBuilderNotContains: "No contiene",
filterBuilderBeginsWith: "Empieza por",
filterBuilderEndsWith: "Termina por",
filterBuilderGreaterThan: "Mayor que",
filterBuilderGreaterThanOrEqual: "Mayor o igual que",
filterBuilderLessThan: "Menor que",
+2
View File
@@ -1592,6 +1592,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "Diverso da",
filterBuilderContains: "Contiene",
filterBuilderNotContains: "Non contiene",
filterBuilderBeginsWith: "Inizia con",
filterBuilderEndsWith: "Termina con",
filterBuilderGreaterThan: "Maggiore di",
filterBuilderGreaterThanOrEqual: "Maggiore o uguale a",
filterBuilderLessThan: "Minore di",
+2
View File
@@ -1610,6 +1610,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "等しくない",
filterBuilderContains: "含む",
filterBuilderNotContains: "含まない",
filterBuilderBeginsWith: "で始まる",
filterBuilderEndsWith: "で終わる",
filterBuilderGreaterThan: "より大きい",
filterBuilderGreaterThanOrEqual: "以上",
filterBuilderLessThan: "より小さい",
+2
View File
@@ -1587,6 +1587,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "같지 않음",
filterBuilderContains: "포함",
filterBuilderNotContains: "포함하지 않음",
filterBuilderBeginsWith: "…으로 시작",
filterBuilderEndsWith: "…으로 끝",
filterBuilderGreaterThan: "보다 큼",
filterBuilderGreaterThanOrEqual: "크거나 같음",
filterBuilderLessThan: "보다 작음",
+2
View File
@@ -1594,6 +1594,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "Diferente de",
filterBuilderContains: "Contém",
filterBuilderNotContains: "Não contém",
filterBuilderBeginsWith: "Começa com",
filterBuilderEndsWith: "Termina com",
filterBuilderGreaterThan: "Maior que",
filterBuilderGreaterThanOrEqual: "Maior ou igual a",
filterBuilderLessThan: "Menor que",
+2
View File
@@ -1636,6 +1636,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "不等于",
filterBuilderContains: "包含",
filterBuilderNotContains: "不包含",
filterBuilderBeginsWith: "以…开始",
filterBuilderEndsWith: "以…结束",
filterBuilderGreaterThan: "大于",
filterBuilderGreaterThanOrEqual: "大于等于",
filterBuilderLessThan: "小于",
+2
View File
@@ -1593,6 +1593,8 @@ export default withEnglishFallback({
filterBuilderNotEquals: "不等於",
filterBuilderContains: "包含",
filterBuilderNotContains: "不包含",
filterBuilderBeginsWith: "以…開始",
filterBuilderEndsWith: "以…結束",
filterBuilderGreaterThan: "大於",
filterBuilderGreaterThanOrEqual: "大於等於",
filterBuilderLessThan: "小於",
@@ -4,7 +4,7 @@ import type { DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
describe("VictoriaMetrics data grid filters", () => {
it("does not expose SQL filter modes for MetricsQL results", () => {
const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "less-than", "greater-than", "in", "not-in", "between", "not-between"];
const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "begins-with", "ends-with", "less-than", "greater-than", "in", "not-in", "between", "not-between"];
expect(modes.every((mode) => !filterModeIsSupportedForDatabase(mode, "victoriametrics"))).toBe(true);
expect(filterModeIsSupportedForDatabase("equals", "postgres")).toBe(true);
@@ -190,7 +190,26 @@ export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig
return undefined;
}
const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set<DataGridContextFilterMode>(["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "less-than", "less-than-or-equal", "greater-than", "greater-than-or-equal", "in", "not-in", "between", "not-between"]);
const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set<DataGridContextFilterMode>([
"equals",
"not-equals",
"is-null",
"is-not-null",
"is-blank",
"is-not-blank",
"like",
"not-like",
"begins-with",
"ends-with",
"less-than",
"less-than-or-equal",
"greater-than",
"greater-than-or-equal",
"in",
"not-in",
"between",
"not-between",
]);
function normalizeForeignKeyDisplayFilter(value: unknown): ForeignKeyDisplayFilterConfig | undefined {
if (!value || typeof value !== "object") return undefined;
+1 -1
View File
@@ -58,7 +58,7 @@ export interface DataGridCopyInsertStatementOptions {
insertMode?: DataGridCopyInsertMode;
}
export type DataGridContextFilterMode = "equals" | "not-equals" | "is-null" | "is-not-null" | "is-blank" | "is-not-blank" | "like" | "not-like" | "less-than" | "less-than-or-equal" | "greater-than" | "greater-than-or-equal" | "in" | "not-in" | "between" | "not-between";
export type DataGridContextFilterMode = "equals" | "not-equals" | "is-null" | "is-not-null" | "is-blank" | "is-not-blank" | "like" | "not-like" | "begins-with" | "ends-with" | "less-than" | "less-than-or-equal" | "greater-than" | "greater-than-or-equal" | "in" | "not-in" | "between" | "not-between";
export interface DataGridContextFilterConditionOptions {
databaseType?: DatabaseType;
+60
View File
@@ -153,6 +153,8 @@ pub enum DataGridContextFilterMode {
IsNotBlank,
Like,
NotLike,
BeginsWith,
EndsWith,
LessThan,
LessThanOrEqual,
GreaterThan,
@@ -578,6 +580,22 @@ pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterCo
None
)
)),
DataGridContextFilterMode::BeginsWith => Some(format!(
"{like_column} LIKE {}",
format_grid_sql_literal(
&Value::String(format!("{}%", value_to_filter_text(value))),
options.database_type,
None
)
)),
DataGridContextFilterMode::EndsWith => Some(format!(
"{like_column} LIKE {}",
format_grid_sql_literal(
&Value::String(format!("%{}", value_to_filter_text(value))),
options.database_type,
None
)
)),
DataGridContextFilterMode::LessThan => Some(format!(
"{column} < {}",
format_data_grid_context_filter_literal(
@@ -4153,6 +4171,48 @@ mod tests {
.as_deref(),
Some("\"created_at\"::text NOT LIKE '%2026%'")
);
assert_eq!(
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
column_name: "file_name".to_string(),
mode: DataGridContextFilterMode::BeginsWith,
value: json!("FN"),
values: Vec::new(),
end_value: None,
column_info: Some(column("file_name", "varchar", false, None)),
})
.as_deref(),
Some("`file_name` LIKE 'FN%'")
);
assert_eq!(
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
column_name: "file_name".to_string(),
mode: DataGridContextFilterMode::EndsWith,
value: json!(".sql"),
values: Vec::new(),
end_value: None,
column_info: Some(column("file_name", "varchar", false, None)),
})
.as_deref(),
Some("`file_name` LIKE '%.sql'")
);
assert_eq!(
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
column_name: "update_date".to_string(),
mode: DataGridContextFilterMode::BeginsWith,
value: json!("128"),
values: Vec::new(),
end_value: None,
column_info: Some(column("update_date", "bigint", false, None)),
})
.as_deref(),
Some("\"update_date\"::text LIKE '128%'")
);
assert_eq!(
build_data_grid_column_value_filter_condition(DataGridColumnValueFilterConditionOptions {
database_type: Some(DatabaseType::SqlServer),
+24
View File
@@ -168,6 +168,30 @@ test("passes value-less blank modes through the shared context filter API", asyn
}
});
test("passes begins-with and ends-with modes through the shared context filter API", async () => {
installFilterFetchMock();
for (const mode of ["begins-with", "ends-with"] as const) {
await buildDataGridContextFilterCondition({
databaseType: "mysql",
columnName: "file_name",
mode,
value: "FN",
});
assert.deepEqual(lastContextFilterOptions, {
databaseType: "mysql",
columnName: "file_name",
mode,
value: "FN",
});
}
assert.equal(filterModeIsSupportedForDatabase("begins-with", "mysql"), true);
assert.equal(filterModeIsSupportedForDatabase("ends-with", "postgres"), true);
assert.equal(filterModeIsSupportedForDatabase("begins-with", "cassandra"), true);
assert.equal(filterModeHasCompleteValue("begins-with", "FN"), true);
assert.equal(filterModeHasCompleteValue("begins-with", " "), false);
assert.equal(filterModeHasCompleteValue("ends-with", ".sql"), true);
});
test("passes list and range values through the shared context filter API", async () => {
installFilterFetchMock();
await buildDataGridContextFilterCondition({