Skip to content

Commit

Permalink
feat(ws) add filter to workspaces table
Browse files Browse the repository at this point in the history
Signed-off-by: Elay Aharoni (EXT-Nokia) <elay.aharoni.ext@nokia.com>
  • Loading branch information
Elay Aharoni (EXT-Nokia) committed Jan 13, 2025
1 parent ac26178 commit a80378b
Show file tree
Hide file tree
Showing 8 changed files with 511 additions and 236 deletions.
309 changes: 122 additions & 187 deletions workspaces/frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion workspaces/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"core-js": "^3.39.0",
"css-loader": "^6.11.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"cypress": "^13.15.0",
"cypress": "^13.16.1",
"cypress-axe": "^1.5.0",
"cypress-high-resolution": "^1.0.0",
"cypress-mochawesome-reporter": "^3.8.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default defineConfig({
},
defaultCommandTimeout: 10000,
e2e: {
baseUrl: BASE_URL,
baseUrl: env.CY_MOCK ? BASE_URL : 'http://localhost:9000',
specPattern: env.CY_MOCK ? `cypress/tests/mocked/**/*.cy.ts` : `cypress/tests/e2e/**/*.cy.ts`,
experimentalInteractiveRunEvents: true,
setupNodeEvents(on, config) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { home } from '~/__tests__/cypress/cypress/pages/home';

const useFilter = (filterName: string, searchValue: string) => {
cy.get("[id$='filter-workspaces-dropdown']").click();
cy.get(`[id$='filter-workspaces-dropdown-${filterName}']`).click();
cy.get("[id$='filter-workspaces-search-input']").type(searchValue);
cy.get("[class$='pf-v6-c-toolbar__group']").contains(filterName);
cy.get("[class$='pf-v6-c-toolbar__group']").contains(searchValue);
};

describe('Application', () => {
it('filter rows with single filter', () => {
home.visit();
useFilter('Name', 'My');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Other Jupyter Notebook');
});

it('filter rows with multiple filters', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
});

it('filter rows with multiple filters and remove one', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[class$='pf-v6-c-label-group__close']").eq(1).click();
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Other Jupyter Notebook');
});

it('filter rows with multiple filters and remove all', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get('*').contains('Clear all filters').click();
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Name');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { home } from '~/__tests__/cypress/cypress/pages/home';

const useFilter = (filterName: string, searchValue: string) => {
cy.get("[id$='filter-workspaces-dropdown']").click();
cy.get(`[id$='filter-workspaces-dropdown-${filterName}']`).click();
cy.get("[id$='filter-workspaces-search-input']").type(searchValue);
cy.get("[class$='pf-v6-c-toolbar__group']").contains(filterName);
cy.get("[class$='pf-v6-c-toolbar__group']").contains(searchValue);
};

describe('Application', () => {
it('filter rows with single filter', () => {
home.visit();
useFilter('Name', 'My');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Other Jupyter Notebook');
});

it('filter rows with multiple filters', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
});

it('filter rows with multiple filters and remove one', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[class$='pf-v6-c-label-group__close']").eq(1).click();
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Other Jupyter Notebook');
});

it('filter rows with multiple filters and remove all', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get('*').contains('Clear all filters').click();
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Name');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
});
});
98 changes: 51 additions & 47 deletions workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@ import {
IActions,
} from '@patternfly/react-table';
import { useState } from 'react';
import Filter from '~/app/Generic components/Filter';
import { FilterIcon } from '@patternfly/react-icons';
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';

import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
import { formatRam } from 'shared/utilities/WorkspaceResources';
import Filter from 'shared/components/Filter';

export const Workspaces: React.FunctionComponent = () => {
/* Mocked workspaces, to be removed after fetching info from backend */
const workspaces: Workspace[] = [
const mockWorkspaces: Workspace[] = [
{
name: 'My Jupyter Notebook',
namespace: 'namespace1',
Expand Down Expand Up @@ -132,7 +131,6 @@ export const Workspaces: React.FunctionComponent = () => {
lastActivity: 'Last Activity',
};

export const Workspaces: React.FunctionComponent = () => {
// change when fetch workspaces is implemented
const initialWorkspaces = mockWorkspaces;
const [workspaces, setWorkspaces] = useState(initialWorkspaces);
Expand All @@ -150,34 +148,44 @@ export const Workspaces: React.FunctionComponent = () => {
expandedWorkspacesNames.includes(workspace.name);

// filter function to pass to the filter component
const onFilter = (activeAttributeMenu: string, searchValue: string) => {
const onFilter = (filters: { filterName: string; value: string }[]) => {
// Search name with search value
let searchValueInput: RegExp;
try {
searchValueInput = new RegExp(searchValue, 'i');
} catch {
searchValueInput = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}
const filteredWorkspaces = initialWorkspaces.filter(
(workspace) =>
searchValue === '' ||
(activeAttributeMenu === 'Name' && workspace.name.search(searchValueInput) >= 0) ||
(activeAttributeMenu === 'Kind' && workspace.kind.search(searchValueInput) >= 0) ||
(activeAttributeMenu === 'Image' &&
workspace.options.imageConfig.search(searchValueInput) >= 0) ||
(activeAttributeMenu === 'Pod Config' &&
workspace.options.podConfig.search(searchValueInput) >= 0) ||
(activeAttributeMenu === 'State' &&
WorkspaceState[workspace.status.state].search(searchValueInput) >= 0) ||
(activeAttributeMenu === 'Home Vol' &&
workspace.podTemplate.volumes.home.search(searchValueInput) >= 0) ||
(activeAttributeMenu === 'Data Vol' &&
workspace.podTemplate.volumes.data.some(
(dataVol) =>
dataVol.pvcName.search(searchValueInput) >= 0 ||
dataVol.mountPath.search(searchValueInput) >= 0,
)),
);
let filteredWorkspaces = initialWorkspaces;
filters.forEach((filter) => {
let searchValueInput: RegExp;
try {
searchValueInput = new RegExp(filter.value, 'i');
} catch {
searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line array-callback-return
filteredWorkspaces = filteredWorkspaces.filter((workspace) => {
if (filter.value === '') {
return true;
}
switch (filter.filterName) {
case 'Name':
return workspace.name.search(searchValueInput) >= 0;
case 'Kind':
return workspace.kind.search(searchValueInput) >= 0;
case 'Image':
return workspace.options.imageConfig.search(searchValueInput) >= 0;
case 'Pod Config':
return workspace.options.podConfig.search(searchValueInput) >= 0;
case 'State':
return WorkspaceState[workspace.status.state].search(searchValueInput) >= 0;
case 'Home Vol':
return workspace.podTemplate.volumes.home.search(searchValueInput) >= 0;
case 'CPU':
return workspace.cpu.toString().search(searchValueInput) >= 0;
case 'Memory':
return workspace.ram.toString().search(searchValueInput) >= 0;
default:
}
});
});
setWorkspaces(filteredWorkspaces);
};

Expand Down Expand Up @@ -298,30 +306,26 @@ export const Workspaces: React.FunctionComponent = () => {
<PageSection>
<Title headingLevel="h1">Kubeflow Workspaces</Title>
<p>View your existing workspaces or create new workspaces.</p>
<Filter onFilter={onFilter} columnNames={columnNames} />
<Filter id="filter-workspaces" onFilter={onFilter} columnNames={columnNames} />
<Table aria-label="Sortable table" ouiaId="SortableTable">
<Thead>
<Tr>
<Th />
<Th sort={getSortParams(0)}>{columnNames.name}</Th>
<Th sort={getSortParams(1)}>{columnNames.kind}</Th>
<Th sort={getSortParams(2)}>{columnNames.image}</Th>
<Th sort={getSortParams(3)}>{columnNames.podConfig}</Th>
<Th sort={getSortParams(4)}>{columnNames.state}</Th>
<Th sort={getSortParams(5)}>{columnNames.homeVol}</Th>
<Th sort={getSortParams(6)} info={{ tooltip: 'Workspace CPU usage' }}>
{columnNames.cpu}
</Th>
<Th sort={getSortParams(7)} info={{ tooltip: 'Workspace memory usage' }}>
{columnNames.ram}
</Th>
<Th sort={getSortParams(8)}>{columnNames.lastActivity}</Th>
{Object.values(columnNames).map((columnName, index) => (
<Th key={`${columnName}-col-name`} sort={getSortParams(index)}>
{columnName}
</Th>
))}
<Th screenReaderText="Primary action" />
</Tr>
</Thead>
{sortedWorkspaces.map((workspace, rowIndex) => (
<Tbody key={rowIndex} isExpanded={isWorkspaceExpanded(workspace)}>
<Tr>
<Tbody
id="workspaces-table-content"
key={rowIndex}
isExpanded={isWorkspaceExpanded(workspace)}
>
<Tr id={`workspaces-table-row-${rowIndex + 1}`}>
<Td
expand={{
rowIndex,
Expand Down
Loading

0 comments on commit a80378b

Please sign in to comment.