Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Projects view filter bar #3179

Merged
Merged
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
23 changes: 20 additions & 3 deletions frontend/src/__tests__/cypress/cypress/pages/projects.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal';
import { appChrome } from '~/__tests__/cypress/cypress/pages/appChrome';
import { DeleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal';
import { Contextual } from '~/__tests__/cypress/cypress/pages/components/Contextual';
import { TableRow } from './components/table';
import { TableToolbar } from './components/TableToolbar';

class ProjectListToolbar extends TableToolbar {}
class ProjectListToolbar extends Contextual<HTMLElement> {
findToggleButton(id: string) {
return this.find().pfSwitch(id).click();
}

findFilterMenuOption(id: string, name: string): Cypress.Chainable<JQuery<HTMLElement>> {
return this.findToggleButton(id).parents().findByRole('menuitem', { name });
}

findFilterInput(name: string): Cypress.Chainable<JQuery<HTMLElement>> {
return this.find().findByLabelText(`Filter by ${name}`);
}

findSearchInput(): Cypress.Chainable<JQuery<HTMLElement>> {
return this.find().findByLabelText('Search input');
}
}

class NotebookRow extends TableRow {
findNotebookImageAvailability() {
return cy.findByTestId('notebook-image-availability');
Expand Down Expand Up @@ -93,7 +110,7 @@ class ProjectListPage {
}

getTableToolbar() {
return new ProjectListToolbar(() => cy.findByTestId('dashboard-table-toolbar'));
return new ProjectListToolbar(() => cy.findByTestId('projects-table-toolbar'));
}

findCreateWorkbenchButton() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ describe('Data science projects details', () => {

// Select the "Name" filter
const projectListToolbar = projectListPage.getTableToolbar();
projectListToolbar.findFilterMenuOption('filter-dropdown-select', 'Name').click();
projectListToolbar.findSearchInput().type('Test Project');
projectListToolbar.findFilterMenuOption('filter-toolbar-dropdown', 'Name').click();
projectListToolbar.findFilterInput('name').type('Test Project');
// Verify only rows with the typed run name exist
projectListPage.getProjectRow('Test Project').find().should('exist');
});
Expand All @@ -210,8 +210,8 @@ describe('Data science projects details', () => {

// Select the "User" filter
const projectListToolbar = projectListPage.getTableToolbar();
projectListToolbar.findFilterMenuOption('filter-dropdown-select', 'User').click();
projectListToolbar.findSearchInput().type('test-user');
projectListToolbar.findFilterMenuOption('filter-toolbar-dropdown', 'User').click();
projectListToolbar.findFilterInput('user').type('test-user');
// Verify only rows with the typed run user exist
projectListPage.getProjectRow('Test Project').find().should('exist');
});
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/PopoverListContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import {
Text,
TextContent,
TextContentProps,
TextList,
TextListItem,
} from '@patternfly/react-core';

type PopoverListContentProps = TextContentProps & {
leadText?: React.ReactNode;
listHeading?: React.ReactNode;
listItems: React.ReactNode[];
};

const ContentText: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<Text component="small" style={{ color: 'var(--Text---pf-v5-global--Color--100)' }}>
{children}
</Text>
);

const PopoverListContent: React.FC<PopoverListContentProps> = ({
leadText,
listHeading,
listItems,

Check warning on line 25 in frontend/src/components/PopoverListContent.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/components/PopoverListContent.tsx#L23-L25

Added lines #L23 - L25 were not covered by tests
...props
}) => (

Check warning on line 27 in frontend/src/components/PopoverListContent.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/components/PopoverListContent.tsx#L27

Added line #L27 was not covered by tests
<TextContent {...props}>
{leadText ? <ContentText>{leadText}</ContentText> : null}
{listHeading ? <Text component="h4">{listHeading}</Text> : null}
<TextList>
{listItems.map((item, index) => (
<TextListItem key={index}>
<ContentText>{item}</ContentText>
</TextListItem>
))}
</TextList>
</TextContent>
);

export default PopoverListContent;
21 changes: 6 additions & 15 deletions frontend/src/pages/projects/screens/projects/EmptyProjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
Popover,
Button,
Icon,
TextContent,
TextList,
TextListItem,
} from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import PopoverListContent from '~/components/PopoverListContent';
import projectsEmptyStateImg from '~/images/empty-state-projects-color.svg';
import { FindAdministratorOptions } from '~/pages/projects/screens/projects/const';
import NewProjectButton from './NewProjectButton';

type EmptyProjectsProps = {
Expand Down Expand Up @@ -54,18 +53,10 @@ const EmptyProjects: React.FC<EmptyProjectsProps> = ({ allowCreate }) => {
minWidth="400px"
headerContent="Your administrator might be:"
bodyContent={
<TextContent data-testid="projects-empty-admin-help-content">
<TextList>
<TextListItem>The person who gave you your username</TextListItem>
<TextListItem>
Someone in your IT department or Help desk (at a company or school)
</TextListItem>
<TextListItem>
The person who manages your email service or web site (in a small business or
club)
</TextListItem>
</TextList>
</TextContent>
<PopoverListContent
data-testid="projects-empty-admin-help-content"
listItems={FindAdministratorOptions}
/>
}
>
<Button
Expand Down
96 changes: 43 additions & 53 deletions frontend/src/pages/projects/screens/projects/ProjectListView.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as React from 'react';
import { Button, ToolbarGroup, ToolbarItem } from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { Table } from '~/components/table';
import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField';
import { ProjectKind } from '~/k8sTypes';
import { getProjectOwner } from '~/concepts/projects/utils';
import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import ProjectTableRow from '~/pages/projects/screens/projects/ProjectTableRow';
import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils';
import NewProjectButton from './NewProjectButton';
import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView';
import ProjectsToolbar from '~/pages/projects/screens/projects/ProjectsToolbar';
import {
initialProjectsFilterData,
ProjectsFilterDataType,
} from '~/pages/projects/screens/projects/const';
import { columns, subColumns } from './tableData';
import DeleteProjectModal from './DeleteProjectModal';
import ManageProjectModal from './ManageProjectModal';
Expand All @@ -20,27 +23,41 @@
const ProjectListView: React.FC<ProjectListViewProps> = ({ allowCreate }) => {
const { projects } = React.useContext(ProjectsContext);
const navigate = useNavigate();
const [searchType, setSearchType] = React.useState<SearchType>(SearchType.NAME);
const [search, setSearch] = React.useState('');
const filteredProjects = projects.filter((project) => {
if (!search) {
return true;
}
const [filterData, setFilterData] =
React.useState<ProjectsFilterDataType>(initialProjectsFilterData);
const onClearFilters = React.useCallback(
() => setFilterData(initialProjectsFilterData),
[setFilterData],
);

const filteredProjects = React.useMemo(
() =>
projects.filter((project) => {
const nameFilter = filterData.Name?.toLowerCase();
const userFilter = filterData.User?.toLowerCase();

switch (searchType) {
case SearchType.NAME:
return getDisplayNameFromK8sResource(project).toLowerCase().includes(search.toLowerCase());
case SearchType.USER:
return getProjectOwner(project).toLowerCase().includes(search.toLowerCase());
default:
return true;
}
});
if (
nameFilter &&
!getDisplayNameFromK8sResource(project).toLowerCase().includes(nameFilter)
) {
return false;

Check warning on line 43 in frontend/src/pages/projects/screens/projects/ProjectListView.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/projects/screens/projects/ProjectListView.tsx#L43

Added line #L43 was not covered by tests
}

return !userFilter || getProjectOwner(project).toLowerCase().includes(userFilter);
}),
[projects, filterData],
);

const resetFilters = () => {
setSearch('');
setFilterData(initialProjectsFilterData);

Check warning on line 52 in frontend/src/pages/projects/screens/projects/ProjectListView.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/projects/screens/projects/ProjectListView.tsx#L52

Added line #L52 was not covered by tests
};

const onFilterUpdate = React.useCallback(
(key: string, value: string | { label: string; value: string } | undefined) =>
setFilterData((prevValues) => ({ ...prevValues, [key]: value })),
[setFilterData],
);

const [deleteData, setDeleteData] = React.useState<ProjectKind | undefined>();
const [editData, setEditData] = React.useState<ProjectKind | undefined>();
const [refreshIds, setRefreshIds] = React.useState<string[]>([]);
Expand All @@ -55,14 +72,7 @@
hasNestedHeader
columns={columns}
subColumns={subColumns}
emptyTableView={
<>
No projects match your filters.{' '}
<Button variant="link" isInline onClick={resetFilters}>
Clear filters
</Button>
</>
}
emptyTableView={<DashboardEmptyTableView onClearFilters={resetFilters} />}
data-testid="project-view-table"
rowRenderer={(project) => (
<ProjectTableRow
Expand All @@ -74,32 +84,12 @@
/>
)}
toolbarContent={
<>
<ToolbarGroup>
<ToolbarItem>
<DashboardSearchField
types={[SearchType.NAME, SearchType.USER]}
searchType={searchType}
searchValue={search}
onSearchTypeChange={(newSearchType: SearchType) => {
setSearchType(newSearchType);
}}
onSearchValueChange={(searchValue: string) => {
setSearch(searchValue);
}}
/>
</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup align={{ default: 'alignRight' }}>
{allowCreate && (
<ToolbarItem>
<NewProjectButton
onProjectCreated={(projectName) => navigate(`/projects/${projectName}`)}
/>
</ToolbarItem>
)}
</ToolbarGroup>
</>
<ProjectsToolbar
allowCreate={allowCreate}
filterData={filterData}
onFilterUpdate={onFilterUpdate}
onClearFilters={onClearFilters}
/>
}
/>
<ManageProjectModal
Expand Down
96 changes: 96 additions & 0 deletions frontend/src/pages/projects/screens/projects/ProjectsToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from 'react';
import {
Button,
Icon,
Popover,
SearchInput,
ToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import PopoverListContent from '~/components/PopoverListContent';
import FilterToolbar from '~/components/FilterToolbar';
import {
FindAdministratorOptions,
ProjectsFilterDataType,
projectsFilterOptions,
ProjectsFilterOptions,
} from '~/pages/projects/screens/projects/const';
import NewProjectButton from './NewProjectButton';

type ProjectsToolbarProps = {
allowCreate: boolean;
filterData: ProjectsFilterDataType;
onFilterUpdate: (key: string, value?: string | { label: string; value: string }) => void;
onClearFilters: () => void;
};

const ProjectsToolbar: React.FC<ProjectsToolbarProps> = ({
allowCreate,
filterData,
onFilterUpdate,
onClearFilters,
}) => {
const navigate = useNavigate();

return (
<FilterToolbar<keyof typeof projectsFilterOptions>
data-testid="projects-table-toolbar"
filterOptions={projectsFilterOptions}
filterOptionRenders={{
[ProjectsFilterOptions.name]: ({ onChange, ...props }) => (
<SearchInput
{...props}
aria-label="Filter by name"
placeholder="Filter by name"
onChange={(_event, value) => onChange(value)}
/>
),
[ProjectsFilterOptions.user]: ({ onChange, ...props }) => (
<SearchInput
{...props}
aria-label="Filter by user"
placeholder="Filter by user"
onChange={(_event, value) => onChange(value)}
/>
),
}}
filterData={filterData}
onClearFilters={onClearFilters}
onFilterUpdate={onFilterUpdate}
>
<ToolbarGroup>
<ToolbarItem>
{allowCreate ? (
<NewProjectButton
onProjectCreated={(projectName) => navigate(`/projects/${projectName}`)}
/>
) : (
<Popover
minWidth="400px"
headerContent="Need another project?"
bodyContent={
<PopoverListContent
data-testid="projects-admin-help-content"
leadText="To request a new project, contact your administrator."
listHeading="Your administrator might be:"
listItems={FindAdministratorOptions}
/>
}
>
<Button data-testid="projects-empty-admin-help" variant="link">
<Icon isInline aria-label="More info">
<OutlinedQuestionCircleIcon />
</Icon>
<span className="pf-v5-u-ml-xs">Need another project?</span>
</Button>
</Popover>
)}
</ToolbarItem>
</ToolbarGroup>
</FilterToolbar>
);
};

export default ProjectsToolbar;
22 changes: 22 additions & 0 deletions frontend/src/pages/projects/screens/projects/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export enum ProjectsFilterOptions {
name = 'Name',
user = 'User',
}

export const projectsFilterOptions = {
[ProjectsFilterOptions.name]: 'Name',
[ProjectsFilterOptions.user]: 'User',
};

export type ProjectsFilterDataType = Record<ProjectsFilterOptions, string | undefined>;

export const initialProjectsFilterData: ProjectsFilterDataType = {
[ProjectsFilterOptions.name]: '',
[ProjectsFilterOptions.user]: '',
};

export const FindAdministratorOptions = [
'The person who gave you your username',
'Someone in your IT department or Help desk (at a company or school)',
'The person who manages your email service or web site (in a small business or club)',
];
Loading