Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,13 @@ public void close() {

private void addLikeOrEqualsExpression(StringBuilder query, List<Object> args, List<Integer> argTypes,
String column, String searchValue) throws StorageException {
if (searchValue.startsWith("[[[") && searchValue.endsWith("]]]")) {
addExpression(query, column + " = ?");
searchValue = searchValue.substring(3, searchValue.length() - 3);
args.add(searchValue);
argTypes.add(Types.VARCHAR);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know how to do this better. Is this OK? Should we restrict the [[[ ]]] option to string-typed fields? Do you know a way to put the correct database type for other fields?

return;
}
if (!(searchValue.startsWith("[") && searchValue.endsWith("]"))
&& !(searchValue.startsWith("*") || searchValue.endsWith("*"))) {
searchValue = "*" + searchValue + "*";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020, 2022-2023, 2025 WeAreFrank!, 2018 Nationale-Nederlanden
Copyright 2020, 2022-2023, 2025-2026 WeAreFrank!, 2018 Nationale-Nederlanden

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -41,7 +41,8 @@ public static String getUserHelp() {
}

public static String getUserHelpWildcards() {
return "Search case insensitive using * as the wildcard character."
return "Search exactly and omit column from table when search value starts with [[[ and ends with ]]]."
+ " Search case insensitive using * as the wildcard character."
+ " Wildcards are automatically added at the beginning and the end unless the search value starts with [ and ends with ] or already starts or ends with the wildcard."
+ " The search is done case sensitive when the search value starts with [[ and ends with ]].";
}
Expand All @@ -58,6 +59,10 @@ public static String getUserHelpNullAndEmpty() {

public static boolean matches(Object value, String query) {
if (query != null && !"".equals(query)) {
if (query.startsWith("[[[") && query.endsWith("]]]")) {
query = query.substring(3, query.length() - 3);
return value != null && value.equals(query);
}
if (query.startsWith("(") && query.endsWith(")")) {
// Regex search
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ public void testSearchUtil() {
assertFalse(SearchUtil.matches(null, "(.+)"));
assertFalse(SearchUtil.matches("", "(.+)"));

// Exact match between [[[ ]]]
assertTrue(SearchUtil.matches("some text", "[[[some text]]]"));
assertFalse(SearchUtil.matches("SOME TEXT", "[[[some text]]]"));
assertFalse(SearchUtil.matches("some tex", "[[[some text]]]"));
assertFalse(SearchUtil.matches("some text ", "[[[some text]]]"));
assertFalse(SearchUtil.matches(null, "[[[]]]"));
assertTrue(SearchUtil.matches("", "[[[]]]"));
assertFalse(SearchUtil.matches(null, "[[[null]]]"));
assertTrue(SearchUtil.matches("null", "[[[null]]]"));
}

private void assertAllMatch(String[] values, String[] searchValues) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ Cypress.Commands.add('enterFilter', (field: string, filter: string) => {

Cypress.Commands.add('checkActiveFilterSphere', (field: string, value: string) => {
const expectedText = `${field}: ${value}`
cy.inIframeBody('app-active-filters').should('not.contain', '[');
cy.inIframeBody('app-active-filters').should('not.contain', ']');
return cy.inIframeBody('app-active-filters').contains(expectedText)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,52 @@ describe('Tests with views and filtering', () => {
interface ColumnAndName {
readonly name: string
readonly colNr: number
readonly testExactMatch: boolean
// TODO: Get rid of this when issue https://github.com/wearefrank/ladybug/issues/429 is fixed
readonly enabled: boolean
}

const isDatabaseStorage: boolean = Cypress.env('debugStorageName') === 'DatabaseDebugStorage'

const columnAndNameCombinations: ColumnAndName[] = [
{ name: 'Storage Id', colNr: 1, enabled: !isDatabaseStorage },
{ name: 'End Time', colNr: 2, enabled: !isDatabaseStorage },
{ name: 'Duration', colNr: 3, enabled: true },
{ name: 'Name', colNr: 4, enabled: true },
{ name: 'Correlation Id', colNr: 5, enabled: true },
{ name: 'Status', colNr: 6, enabled: true },
{ name: 'Checkpoints', colNr: 7, enabled: !isDatabaseStorage },
{ name: 'Memory', colNr: 8, enabled: true },
{ name: 'Size', colNr: 9, enabled: true },
{ name: 'Input', colNr: 10, enabled: true }
{ name: 'Storage Id', colNr: 1, testExactMatch: false, enabled: !isDatabaseStorage },
{ name: 'End Time', colNr: 2, testExactMatch: false, enabled: !isDatabaseStorage },
{ name: 'Duration', colNr: 3, testExactMatch: false, enabled: true },
{ name: 'Duration', colNr: 3, testExactMatch: true, enabled: true },
{ name: 'Name', colNr: 4, testExactMatch: false, enabled: true },
{ name: 'Name', colNr: 4, testExactMatch: true, enabled: true },
{ name: 'Correlation Id', colNr: 5, testExactMatch: false, enabled: true },
{ name: 'Status', colNr: 6, testExactMatch: false, enabled: true },
{ name: 'Checkpoints', colNr: 7, testExactMatch: false, enabled: !isDatabaseStorage },
{ name: 'Memory', colNr: 8, testExactMatch: false, enabled: true },
{ name: 'Size', colNr: 9, testExactMatch: false, enabled: true },
{ name: 'Input', colNr: 10, testExactMatch: false, enabled: true },
]

const testedColumnAndNameCombinations = columnAndNameCombinations.filter((testCase) => testCase.name !== 'Status')

for (const testCase of testedColumnAndNameCombinations.filter((c) => c.enabled)) {
it(`Filter on field ${testCase.name}, expected at column ${testCase.colNr}`, () => {
let exactPhrase = '';
if (testCase.testExactMatch) {
exactPhrase = ', with exact matching';
}
it(`Filter on field ${testCase.name}, expected at column ${testCase.colNr}${exactPhrase}`, () => {
cy.visit('')
// Enter Ladybug
cy.getNumLadybugReports().should('equal', 5)
cy.inIframeBody('[data-cy-change-view-dropdown]').select('White box view no application');
// Check the name and column number combination
cy.inIframeBody('[data-cy-debug="table"]').find(`th:eq(${testCase.colNr})`).contains(`${testCase.name}`)
cy.inIframeBody('[data-cy-debug="tableRow"]:eq(0)').find(`td:eq(${testCase.colNr})`).then((el: JQuery<HTMLElement>) => {
// TODO: It would be nice to get rid of this trim().
const firstRowFieldValue = el.text().trim()
cy.log(`Filtering on value: ${firstRowFieldValue}`)
cy.inIframeBody('[data-cy-debug="filter"]').click()
cy.enterFilter(testCase.name, firstRowFieldValue)
let filterValue = firstRowFieldValue;
if (testCase.testExactMatch) {
filterValue = `[[[${filterValue}]]]`;
}
cy.enterFilter(testCase.name, filterValue);
cy.inIframeBody('[data-cy-debug="tableRow"]').should('have.length.lessThan', 5)
cy.inIframeBody('[data-cy-debug="tableRow"]').should('have.length.greaterThan', 0)
cy.checkActiveFilterSphere(testCase.name, firstRowFieldValue).should('be.visible')
Expand All @@ -70,10 +82,26 @@ describe('Tests with views and filtering', () => {
})
}

it('Can manipulate filter on Application, even though column is not shown', () => {
cy.visit('')
cy.getNumLadybugReports().should('equal', 5)
cy.inIframeBody('[data-cy-change-view-dropdown]').select('White box view no application');
cy.inIframeBody('[data-cy-debug="table"]').find(`th:contains(Name)`).should('be.visible')
cy.inIframeBody('[data-cy-debug="table"]').find(`th:contains(Application)`).should('not.exist')
// Test that the FF! opens Ladybug so that we filter on Application by default
cy.checkActiveFilterSphere('Application', 'ladybug-ff-test-webapp').should('be.visible')
cy.inIframeBody('[data-cy-debug="filterLabel"]:contains(Application)')
.parent()
.contains('Clear')
.click()
cy.inIframeBody('app-active-filters').should('not.exist')
})

it('Filter on two criteria', () => {
cy.visit('')
// Enter Ladybug
cy.getNumLadybugReports().should('equal', 5)
cy.inIframeBody('[data-cy-change-view-dropdown]').select('White box view no application');
cy.inIframeBody('[data-cy-debug="filter"]').click()
cy.enterFilter('Name', 'Adapter')
cy.enterFilter('Input', 'yyy')
Expand All @@ -94,6 +122,7 @@ describe('Tests with views and filtering', () => {
cy.visit('')
// Enter Ladybug
cy.getNumLadybugReports().should('equal', 5)
cy.inIframeBody('[data-cy-change-view-dropdown]').select('White box view no application');
cy.inIframeBody('[data-cy-debug="filter"]').click()
cy.enterFilter('Input', 'yyy')
cy.inIframeBody('[data-cy-debug="close-filter-btn"]').click()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<value>estimatedMemoryUsage</value>
<value>storageSize</value>
<value>input</value>
<!-- We add this one to test that we can delete the default filter on it, even when not shown -->
<value>application</value>
</list>
</constructor-arg>
</bean>
Expand Down Expand Up @@ -87,6 +89,25 @@
</property>
</bean>

<bean name="whiteBoxViewNoApplication" parent="whiteBoxView">
<property name="name" value="White box view no application" />
<property name="metadataNames">
<list>
<value>storageId</value>
<value>endTime</value>
<value>duration</value>
<value>name</value>
<value>correlationId</value>
<value>status</value>
<value>numberOfCheckpoints</value>
<value>estimatedMemoryUsage</value>
<value>storageSize</value>
<value>input</value>
<!-- We omit "application" -->
</list>
</property>
</bean>

<bean name="views" class="org.wearefrank.ladybug.filter.Views" scope="prototype">
<property name="views">
<list>
Expand All @@ -96,6 +117,7 @@
<ref bean="whiteBoxViewNoName"/>
<ref bean="grayBoxView"/>
<ref bean="blackBoxView"/>
<ref bean="whiteBoxViewNoApplication"/>
</list>
</property>
</bean>
Expand Down
36 changes: 36 additions & 0 deletions ladybug-frontend/cypress/e2e/no-profile/debug/filterTable.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,42 @@ describe('Tests for table filter', () => {
cy.get('[data-cy-debug="filter"]').click();
cy.get('[data-cy-debug="tableFilter"').eq(1).should('not.contain.value', '1')
})

it('When exact filter is requested then column omitted from table', () => {
cy.assertDebugTableLength(2);
cy.get('[data-cy-debug="table"]').find('th:contains(Name)').should('be.visible');
cy.get('[data-cy-debug="filter"]').click();
cy.get('[data-cy-debug="tableFilter"]').eq(3).type('[[[Simple report]]]{enter}');
cy.assertDebugTableLength(1);
cy.get('[data-cy-debug="table"]').find('th:contains(Name)').should('not.exist');
// No [[[ ]]]
cy.get('[data-cy-active-filter]').should('contain.text', 'Name: Simple report');
cy.get('[data-cy-active-filter-exact]').should('be.visible');
cy.get('[data-cy-debug="tableFilter"]').eq(3).clear().type('{enter}');
cy.assertDebugTableLength(2);
cy.get('[data-cy-active-filter]').should('not.exist');
cy.get('[data-cy-active-filter-exact]').should('not.exist');
cy.get('[data-cy-debug="table"]').find('th:contains(Name)').should('be.visible');
})

it('When non-exact filter is requested then column not omitted from table', () => {
cy.assertDebugTableLength(2);
cy.get('[data-cy-debug="table"]').find('th:contains(Name)').should('be.visible');
cy.get('[data-cy-debug="filter"]').click();
// Filtering on simple report allows both "Simple report" and "Another simple report"
// if filtering is not exact.
cy.get('[data-cy-debug="tableFilter"]').eq(3).type('Another simple report{enter}');
cy.assertDebugTableLength(1);
cy.get('[data-cy-debug="table"]').find('th:contains(Name)').should('be.visible');
// No [[[ ]]]
cy.get('[data-cy-active-filter]').should('contain.text', 'Name: Another simple report');
cy.get('[data-cy-active-filter-exact]').should('not.exist');
cy.get('[data-cy-debug="tableFilter"]').eq(3).clear().type('{enter}');
cy.assertDebugTableLength(2);
cy.get('[data-cy-active-filter]').should('not.exist');
cy.get('[data-cy-active-filter-exact]').should('not.exist');
cy.get('[data-cy-debug="table"]').find('th:contains(Name)').should('be.visible');
})
});

describe('About URL filters and row filtering views', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
div:hover {
cursor: pointer;
}

.exact {
color: red;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
@if (activeFilters) {
<div class="d-flex flex-row">
@for (filter of activeFilters | dictionary; track filter; let isFirst = $first) {
@if (filter.value !== '') {
<span [attr.data-cy-active-filter]="filter.key" class="filter-container d-flex align-items-center rounded-pill px-3 py-2 text-white "
[title]="'Active filter for ' + filter.key">
{{ safeColumnNameToLabel(filter.key) | shortenedTableHeader | titlecase }}: {{ filter.value }}
</span>
<div class="d-flex flex-row gap-2">
@for (filter of activeFilters; track filter.metadataName; let isFirst = $first) {
<div class="filter-container d-flex align-items-center rounded-pill px-3 py-2 text-white gap-1">
<div class="d-flex flex-row"
[attr.data-cy-active-filter]="filter.metadataName"
[title]="'Active filter for ' + filter.metadataName + exactPhrase(filter)">
{{ safeColumnNameToLabel(filter.metadataName) | shortenedTableHeader | titlecase }}: {{ filter.shownValue }}
</div>
@if (filter.exact) {
<div class="exact" data-cy-active-filter-exact>
<i class="bi bi-eye-slash"></i>
</div>
}
</div>
}
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { catchError, Subscription } from 'rxjs';
import { ErrorHandling } from '../../shared/classes/error-handling.service';
import { FilterService } from '../../shared/services/filter.service';
import { ShortenedTableHeaderPipe } from '../../shared/pipes/shortened-table-header.pipe';
import { MetadataFilter } from '../../shared/services/tab.service';

@Component({
selector: 'app-active-filters',
Expand All @@ -15,7 +16,7 @@ import { ShortenedTableHeaderPipe } from '../../shared/pipes/shortened-table-hea
})
export class ActiveFiltersComponent implements OnInit, OnDestroy {
private columnNameToLabel: Map<string, string> = new Map<string, string>();
protected activeFilters: Map<string, string> = new Map<string, string>();
protected activeFilters: MetadataFilter[] = [];
private subscriptions: Subscription = new Subscription();

private filterService = inject(FilterService);
Expand All @@ -35,7 +36,7 @@ export class ActiveFiltersComponent implements OnInit, OnDestroy {
);
this.subscriptions.add(
this.filterService.userFilters$.subscribe({
next: (context: Map<string, string>) => this.changeFilter(context),
next: (metadataFilters: MetadataFilter[]) => this.changeFilter(metadataFilters),
error: () => catchError(this.errorHandler.handleError()),
}),
);
Expand All @@ -45,13 +46,17 @@ export class ActiveFiltersComponent implements OnInit, OnDestroy {
this.subscriptions.unsubscribe();
}

changeFilter(context: Map<string, string>): void {
changeFilter(metadataFilers: MetadataFilter[]): void {
// Use the column name as key of this map, even though the column label is the base of what is shown.
// We only know for sure that the column name is unique.
this.activeFilters = new Map<string, string>(context);
this.activeFilters = [...metadataFilers];
}

protected safeColumnNameToLabel(columnName: string): string {
return this.columnNameToLabel.get(columnName) ?? columnName;
}

protected exactPhrase(filter: MetadataFilter): string {
return filter.exact ? '. Is exact match, column omitted from table' : '.';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export class DebugTableGridComponent implements OnInit, OnDestroy {
this.subscriptions.add(tableDataSubscription);
}

// Argument is allowed to contain data about columns that are not shown.
// Only columns that are inTableData.columns are shown.
// Not called directly in production - available for Karma tests.
setTableData(argument: TableData): void {
this.data = {
Expand Down
Loading
Loading