Skip to content

Commit

Permalink
US 2 // Allow users to search resources by project
Browse files Browse the repository at this point in the history
Signed-off-by: Dinika Saxena <[email protected]>
  • Loading branch information
Dinika committed Jun 30, 2023
1 parent af6ed0a commit 7cd09eb
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 31 deletions.
95 changes: 86 additions & 9 deletions src/__mocks__/handlers/DataExplorer/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { rest } from 'msw';
import { deltaPath } from '__mocks__/handlers/handlers';
import { Resource } from '@bbp/nexus-sdk';
import { Project, Resource } from '@bbp/nexus-sdk';

export const dataExplorerPageHandler = (mockReources: Resource[]) =>
rest.get(deltaPath(`/resources`), (req, res, ctx) => {
Expand All @@ -18,9 +18,87 @@ export const dataExplorerPageHandler = (mockReources: Resource[]) =>
return res(ctx.status(200), ctx.json(mockResponse));
});

export const filterByProjectHandler = (mockReources: Resource[]) =>
rest.get(deltaPath(`/resources/:org/:project`), (req, res, ctx) => {
const { project } = req.params;

const responseBody = project
? mockReources.filter(
res =>
res._project.slice(res._project.lastIndexOf('/') + 1) === project
)
: mockReources;
const mockResponse = {
'@context': [
'https://bluebrain.github.io/nexus/contexts/metadata.json',
'https://bluebrain.github.io/nexus/contexts/search.json',
'https://bluebrain.github.io/nexus/contexts/search-metadata.json',
],
_total: 300,
_results: responseBody,
_next:
'https://bbp.epfl.ch/nexus/v1/resources?size=50&sort=@id&after=%5B1687269183553,%22https://bbp.epfl.ch/neurosciencegraph/data/31e22529-2c36-44f0-9158-193eb50526cd%22%5D',
};
return res(ctx.status(200), ctx.json(mockResponse));
});

export const getProjectHandler = () =>
rest.get(deltaPath(`/projects`), (req, res, ctx) => {
const projectResponse = {
'@context': [
'https://bluebrain.github.io/nexus/contexts/metadata.json',
'https://bluebrain.github.io/nexus/contexts/search.json',
'https://bluebrain.github.io/nexus/contexts/projects.json',
],
_next:
'https://bbp.epfl.ch/nexus/v1/projects?from=10&label=&size=10&sort=_label',
_total: 10,
_results: [
getMockProject('something-brainy', 'bbp'),

getMockProject('smarty', 'bbp'),

getMockProject('unhcr', 'un'),
getMockProject('unicef', 'un'),
getMockProject('tellytubbies', 'bbc'),
],
};
return res(ctx.status(200), ctx.json(projectResponse));
});

const getMockProject = (project: string, org: string = 'bbp'): Project => {
return {
'@id': `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`,
'@type': 'Project',
apiMappings: [
{
namespace: 'https://neuroshapes.org/dash/',
prefix: 'datashapes',
},
],
'@context': ['mockcontext'],
base: 'https://bbp.epfl.ch/neurosciencegraph/data/',
description: 'This is such a dumb mock project. dumb dumb dumb.',
vocab:
'https://bbp.epfl.ch/nexus/v1/resources/bbp/Blue-Brain-Ketogenic-Project-(BBK)/_/',
_constrainedBy: 'https://bluebrain.github.io/nexus/schemas/projects.json',
_createdAt: '2021-03-04T21:27:18.900Z',
_createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/harikris',
_deprecated: true,
_label: `${project}`,
_organizationLabel: org,
_organizationUuid: 'a605b71a-377d-4df3-95f8-923149d04106',
_rev: 2,
_self: `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`,
_updatedAt: '2021-03-15T09:05:05.882Z',
_updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/harikris',
};
};

export const getMockResource = (
selfSuffix: string,
extra: { [key: string]: any }
extra: { [key: string]: any },
project: string = 'hippocampus'
): Resource => ({
'@id': `https://bbp.epfl.ch/neurosciencegraph/data/${selfSuffix}`,
'@type':
Expand All @@ -30,27 +108,26 @@ export const getMockResource = (
_createdAt: '2023-06-21T09:39:47.217Z',
_createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel',
_deprecated: false,
_incoming: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}/incoming`,
_outgoing: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}/outgoing`,
_project:
'https://bbp.epfl.ch/nexus/v1/projects/bbp/mmb-point-neuron-framework-model',
_incoming: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}/incoming`,
_outgoing: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}/outgoing`,
_project: `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`,
_rev: 2,
_self: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}`,
_self: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}`,
_updatedAt: '2023-06-21T09:39:47.844Z',
_updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel',
...extra,
});

export const defaultMockResult: Resource[] = [
getMockResource('self1', {}),
getMockResource('self2', { specialProperty: 'superSpecialValue' }),
getMockResource('self2', { specialProperty: 'superSpecialValue' }, 'unhcr'),
getMockResource('self3', { specialProperty: ['superSpecialValue'] }),
getMockResource('self4', { specialProperty: '' }),
getMockResource('self5', { specialProperty: [] }),
getMockResource('self6', {
specialProperty: ['superSpecialValue', 'so'],
}),
getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }),
getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }, 'unhcr'),
getMockResource('self8', { specialProperty: null }),
getMockResource('self9', { specialProperty: {} }),
getMockResource('self10', {}),
Expand Down
7 changes: 1 addition & 6 deletions src/pages/DataExplorerPage/DataExplorerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { DataExplorer } from '../../subapps/dataExplorer/DataExplorer';

const DataExplorerPage = () => {
return (
<div
className="view-container"
style={{
padding: '0 1em',
}}
>
<div className={'view-container data-explorer-container'}>
<DataExplorer />
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/shared/App.less
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
background-color: @background-color-subtle;
}
}
&.data-explorer-container {
width: fit-content;
padding-right: 0;
margin-right: 1rem;
}
}

.graph-wrapper-container {
Expand Down
1 change: 1 addition & 0 deletions src/shared/layouts/FusionMainLayout.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
background: @fusion-main-bg !important;
height: 100%;
min-height: 100vh !important;
width: fit-content;
&.wall {
.fusion-main-layout__content {
margin-top: 0;
Expand Down
79 changes: 76 additions & 3 deletions src/subapps/dataExplorer/DataExplorer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import {
dataExplorerPageHandler,
defaultMockResult,
filterByProjectHandler,
getMockResource,
getProjectHandler,
} from '__mocks__/handlers/DataExplorer/handlers';
import { deltaPath } from '__mocks__/handlers/handlers';
import { setupServer } from 'msw/node';
import { QueryClient, QueryClientProvider } from 'react-query';
import { render, screen, waitFor } from '../../utils/testUtil';
import { DataExplorer } from './DataExplorer';
import { AllProjects } from './ProjectSelector';
import { getColumnTitle } from './DataExplorerTable';

describe('DataExplorer', () => {
const server = setupServer(dataExplorerPageHandler(defaultMockResult));
const server = setupServer(
dataExplorerPageHandler(defaultMockResult),
filterByProjectHandler(defaultMockResult),
getProjectHandler()
);

let container: HTMLElement;
let user: UserEvent;
let component: RenderResult;
Expand Down Expand Up @@ -61,7 +70,7 @@ describe('DataExplorer', () => {
};

const expectColumHeaderToExist = async (name: string) => {
const nameReg = new RegExp(`^${name}`, 'i');
const nameReg = new RegExp(getColumnTitle(name), 'i');
const header = await screen.getByText(nameReg, {
selector: 'th',
exact: false,
Expand Down Expand Up @@ -92,14 +101,54 @@ describe('DataExplorer', () => {
const allCellsForRow = Array.from(selfCell[0].parentElement!.childNodes);
const colIndex = Array.from(
container.querySelectorAll('th')
).findIndex(header => header.innerHTML.match(new RegExp(colName, 'i')));
).findIndex(header =>
header.innerHTML.match(new RegExp(getColumnTitle(colName), 'i'))
);
return allCellsForRow[colIndex].textContent;
};

const openProjectAutocomplete = async () => {
const projectAutocomplete = await getProjectAutocomplete();
await userEvent.click(projectAutocomplete);
return projectAutocomplete;
};

const selectProject = async (projectName: string) => {
await openProjectAutocomplete();
const unhcrProject = await getProjectOption(projectName);
await userEvent.click(unhcrProject, { pointerEventsCheck: 0 });
};

const searchForProject = async (searchTerm: string) => {
const projectAutocomplete = await openProjectAutocomplete();
await userEvent.clear(projectAutocomplete);
await userEvent.type(projectAutocomplete, searchTerm);
return projectAutocomplete;
};

const expectProjectOptionsToMatch = async (searchTerm: string) => {
const projectOptions = await screen.getAllByRole('option');
expect(projectOptions.length).toBeGreaterThan(0);
projectOptions.forEach(option => {
expect(option.innerHTML).toMatch(new RegExp(searchTerm, 'i'));
});
};

const projectFromRow = (row: Element) => {
const projectColumn = row.querySelector('td'); // first column is the project column
return projectColumn?.textContent;
};

const visibleTableRows = () => {
return container.querySelectorAll('table tbody tr.data-explorer-row');
};

const getProjectAutocomplete = async () => {
return await screen.getByLabelText('project-filter', {
selector: 'input',
});
};

it('shows rows for all fetched resources', async () => {
await expectRowCountToBe(10);
});
Expand Down Expand Up @@ -260,4 +309,28 @@ describe('DataExplorer', () => {
expect(textForSpecialProperty).not.toMatch(/No data/i);
expect(textForSpecialProperty).toEqual('{}');
});

const getProjectOption = async (projectName: string) =>
await screen.getByText(new RegExp(projectName, 'i'), {
selector: 'div.ant-select-item-option-content',
});

it('shows resources filtered by the selected project', async () => {
await selectProject('unhcr');

visibleTableRows().forEach(row =>
expect(projectFromRow(row)).toMatch(/unhcr/i)
);

await selectProject(AllProjects);
await expectRowCountToBe(10);
});

it('shows autocomplete options for project filter', async () => {
await searchForProject('bbp');
await expectProjectOptionsToMatch('bbp');

await searchForProject('bbc');
await expectProjectOptionsToMatch('bbc');
});
});
26 changes: 22 additions & 4 deletions src/subapps/dataExplorer/DataExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,33 @@ import { useQuery } from 'react-query';
import { getResourceLabel } from '../../shared/utils';
import { DataExplorerTable } from './DataExplorerTable';
import './styles.less';
import { ProjectSelector } from './ProjectSelector';

export interface DataExplorerConfiguration {
pageSize: number;
offset: number;
orgAndProject?: [string, string];
}

export const DataExplorer: React.FC<{}> = () => {
const nexus = useNexusContext();

const [{ pageSize, offset }, updateTableConfiguration] = useReducer(
const [
{ pageSize, offset, orgAndProject },
updateTableConfiguration,
] = useReducer(
(
previous: DataExplorerConfiguration,
next: Partial<DataExplorerConfiguration>
) => ({ ...previous, ...next }),
{ pageSize: 50, offset: 0 }
{ pageSize: 50, offset: 0, orgAndProject: undefined }
);

const { data: resources, isLoading } = useQuery({
queryKey: ['data-explorer', { pageSize, offset }],
queryKey: ['data-explorer', { pageSize, offset, orgAndProject }],
retry: false,
queryFn: () => {
return nexus.Resource.list(undefined, undefined, {
return nexus.Resource.list(orgAndProject?.[0], orgAndProject?.[1], {
from: offset,
size: pageSize,
});
Expand Down Expand Up @@ -60,6 +65,19 @@ export const DataExplorer: React.FC<{}> = () => {

return (
<div className="container">
<div className="data-explorer-header">
<ProjectSelector
onSelect={(orgLabel?: string, projectLabel?: string) => {
if (orgLabel && projectLabel) {
updateTableConfiguration({
orgAndProject: [orgLabel, projectLabel],
});
} else {
updateTableConfiguration({ orgAndProject: undefined });
}
}}
/>
</div>
<DataExplorerTable
isLoading={isLoading}
dataSource={dataSource}
Expand Down
11 changes: 5 additions & 6 deletions src/subapps/dataExplorer/DataExplorerTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Resource } from '@bbp/nexus-sdk';
import { Empty, Table, Tooltip } from 'antd';
import { ColumnType, TablePaginationConfig } from 'antd/lib/table';
import { isArray, isString } from 'lodash';
import { isArray, isString, startCase } from 'lodash';
import React from 'react';
import { makeOrgProjectTuple } from '../../shared/molecules/MyDataTable/MyDataTable';
import isValidUrl from '../../utils/validUrl';
Expand Down Expand Up @@ -49,10 +49,6 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({
return (
<>
<Table<Resource>
sticky={{
offsetHeader: 50,
getContainer: () => window,
}}
columns={dynamicColumnsForDataSource(dataSource)}
dataSource={dataSource}
rowKey={record => record._self}
Expand Down Expand Up @@ -93,10 +89,13 @@ const dynamicColumnsForDataSource = (
return Array.from(colNameToConfig.values());
};

export const getColumnTitle = (colName: string) =>
startCase(colName).toUpperCase();

const defaultColumnConfig = (colName: string): ColumnType<Resource> => {
return {
key: colName,
title: colName.toUpperCase(),
title: getColumnTitle(colName),
dataIndex: colName,
className: `data-explorer-column data-explorer-column-${colName}`,
sorter: false,
Expand Down
3 changes: 1 addition & 2 deletions src/subapps/dataExplorer/NoDataCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const NoDataCell: React.FC<{}> = () => {
backgroundColor: '#ffd9d9',
fontWeight: 600,
lineHeight: '17.5px',
padding: '5px',
paddingLeft: '10px',
padding: '5px 10px',
}}
>
No data
Expand Down
Loading

0 comments on commit 7cd09eb

Please sign in to comment.