Skip to content

Commit

Permalink
workspace: add file search
Browse files Browse the repository at this point in the history
- Extract out common `Search` component
- Add search param to workspace endpoint

closes reanahub#211
  • Loading branch information
mvidalgarcia committed Jan 12, 2022
1 parent b33f367 commit 2a07ff4
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 110 deletions.
5 changes: 3 additions & 2 deletions reana-ui/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,12 @@ export function fetchWorkflowLogs(id) {
};
}

export function fetchWorkflowFiles(id, pagination) {
export function fetchWorkflowFiles(id, pagination, search) {
return async (dispatch) => {
dispatch({ type: WORKFLOW_FILES_FETCH });
const nameSearch = search ? JSON.stringify({ name: [search] }) : search;
return await client
.getWorkflowFiles(id, pagination)
.getWorkflowFiles(id, pagination, nameSearch)
.then((resp) =>
dispatch({
type: WORKFLOW_FILES_RECEIVED,
Expand Down
8 changes: 4 additions & 4 deletions reana-ui/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const WORKFLOWS_URL = (params) =>
export const WORKFLOW_LOGS_URL = (id) => `${api}/api/workflows/${id}/logs`;
export const WORKFLOW_SPECIFICATION_URL = (id) =>
`${api}/api/workflows/${id}/specification`;
export const WORKFLOW_FILES_URL = (id, pagination) =>
`${api}/api/workflows/${id}/workspace?${stringifyQueryParams(pagination)}`;
export const WORKFLOW_FILES_URL = (id, params) =>
`${api}/api/workflows/${id}/workspace?${stringifyQueryParams(params)}`;
export const WORKFLOW_FILE_URL = (id, filename, preview = true) =>
`${api}/api/workflows/${id}/workspace/${filename}?${stringifyQueryParams(
preview
Expand Down Expand Up @@ -126,8 +126,8 @@ class Client {
return this._request(WORKFLOW_LOGS_URL(id));
}

getWorkflowFiles(id, pagination) {
return this._request(WORKFLOW_FILES_URL(id, pagination));
getWorkflowFiles(id, pagination, search) {
return this._request(WORKFLOW_FILES_URL(id, { ...pagination, search }));
}

getWorkflowFile(id, filename) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020 CERN.
Copyright (C) 2020, 2021, 2022 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
*/

import PropTypes from "prop-types";

import { unstable_batchedUpdates } from "react-dom";
import { Input } from "semantic-ui-react";
import debounce from "lodash/debounce";

const TYPING_DELAY = 1000;

export default function WorkflowSearch({ search }) {
export default function Search({ search }) {
const handleChange = debounce(search, TYPING_DELAY);
return (
<Input
Expand All @@ -27,6 +27,15 @@ export default function WorkflowSearch({ search }) {
);
}

WorkflowSearch.propTypes = {
Search.propTypes = {
search: PropTypes.func.isRequired,
};

export const applyFilter = (filter, pagination, setPagination) => (value) => {
// FIXME: refactor once implemented by default in future versions of React
// https://github.com/facebook/react/issues/16387#issuecomment-521623662c
unstable_batchedUpdates(() => {
filter(value);
setPagination({ ...pagination, page: 1 });
});
};
1 change: 1 addition & 0 deletions reana-ui/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { default as JupyterNotebookIcon } from "./JupyterNotebookIcon";
export { default as WorkflowDeleteModal } from "./WorkflowDeleteModal";
export { default as WorkflowActionsPopup } from "./WorkflowActionsPopup";
export { default as PieChart } from "./PieChart";
export { default as Search } from "./Search";
173 changes: 90 additions & 83 deletions reana-ui/src/pages/workflowDetails/components/WorkflowFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ import {
loadingDetails,
} from "~/selectors";
import { fetchWorkflowFiles } from "~/actions";
import client, { WORKFLOW_FILE_URL } from "~/client";
import { getMimeType } from "~/util";
import { Pagination } from "~/components";
import { Pagination, Search } from "~/components";
import { applyFilter } from "~/components/Search";

import styles from "./WorkflowFiles.module.scss";
import client, { WORKFLOW_FILE_URL } from "~/client";

const FILE_SIZE_LIMIT = 5 * 1024 ** 2; // 5MB
const PAGE_SIZE = 15;

const PREVIEW_MIME_PREFIX_WHITELIST = {
"image/": {
Expand Down Expand Up @@ -74,9 +78,6 @@ const PREVIEW_MIME_PREFIX_WHITELIST = {
},
};

const FILE_SIZE_LIMIT = 5 * 1024 ** 2; // 5MB
const PAGE_SIZE = 15;

export default function WorkflowFiles({ id }) {
const dispatch = useDispatch();
const loading = useSelector(loadingDetails);
Expand All @@ -88,10 +89,11 @@ export default function WorkflowFiles({ id }) {
const [sorting, setSorting] = useState({ column: null, direction: null });
const [displayContent, setDisplayContent] = useState(() => () => null);
const [pagination, setPagination] = useState({ page: 1, size: PAGE_SIZE });
const [searchFilter, setSearchFilter] = useState();

useEffect(() => {
dispatch(fetchWorkflowFiles(id, pagination));
}, [dispatch, id, pagination]);
dispatch(fetchWorkflowFiles(id, pagination, searchFilter));
}, [dispatch, id, pagination, searchFilter]);

useEffect(() => {
setFiles(_files);
Expand Down Expand Up @@ -197,91 +199,96 @@ export default function WorkflowFiles({ id }) {
/>
);

if (loading) {
return <Loader active inline="centered" />;
}

return !files ? (
<Message
icon="info circle"
content="The workflow workspace was deleted."
info
/>
) : (
<Segment>
<Table fixed compact basic="very">
<Table.Header className={styles["table-header"]}>
<Table.Row>
<Table.HeaderCell
sorted={sorting.column === "name" ? sorting.direction : null}
onClick={() => handleSort("name")}
>
Name {headerIcon("name")}
</Table.HeaderCell>
<Table.HeaderCell
sorted={
sorting.column === "lastModified" ? sorting.direction : null
}
onClick={() => handleSort("lastModified")}
>
Modified {headerIcon("lastModified")}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sorting.column === "size" ? sorting.direction : null}
onClick={() => handleSort("size")}
>
Size {headerIcon("size")}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<>
<Search
search={applyFilter(setSearchFilter, pagination, setPagination)}
/>
{loading ? (
<Loader active inline="centered" />
) : (
<Segment>
<Table fixed compact basic="very">
<Table.Header className={styles["table-header"]}>
<Table.Row>
<Table.HeaderCell
sorted={sorting.column === "name" ? sorting.direction : null}
onClick={() => handleSort("name")}
>
Name {headerIcon("name")}
</Table.HeaderCell>
<Table.HeaderCell
sorted={
sorting.column === "lastModified" ? sorting.direction : null
}
onClick={() => handleSort("lastModified")}
>
Modified {headerIcon("lastModified")}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sorting.column === "size" ? sorting.direction : null}
onClick={() => handleSort("size")}
>
Size {headerIcon("size")}
</Table.HeaderCell>
</Table.Row>
</Table.Header>

<Table.Body>
{files &&
files.map(({ name, lastModified, size }) => (
<Modal
key={name}
onOpen={() => getFile(name, size.raw)}
closeIcon
trigger={
<Table.Row className={styles["files-row"]}>
<Table.Cell>
<Icon name="file" />
{name}
</Table.Cell>
<Table.Cell>{lastModified}</Table.Cell>
<Table.Cell>{size.raw}</Table.Cell>
</Table.Row>
}
>
<Modal.Header>{name}</Modal.Header>
<Modal.Content scrolling>
{displayContent &&
modalContent &&
displayContent(modalContent, name)}
</Modal.Content>
<Modal.Actions>
<Button primary as="a" href={getFileURL(name, false)}>
<Icon name="download" /> Download
</Button>
</Modal.Actions>
</Modal>
))}
</Table.Body>
</Table>
{filesCount > PAGE_SIZE && (
<div className={styles["pagination-wrapper"]}>
<Pagination
activePage={pagination.page}
totalPages={Math.ceil(filesCount / PAGE_SIZE)}
onPageChange={(_, { activePage }) => {
setPagination({ ...pagination, page: activePage });
resetSort();
}}
size="mini"
/>
</div>
<Table.Body>
{files &&
files.map(({ name, lastModified, size }) => (
<Modal
key={name}
onOpen={() => getFile(name, size.raw)}
closeIcon
trigger={
<Table.Row className={styles["files-row"]}>
<Table.Cell>
<Icon name="file" />
{name}
</Table.Cell>
<Table.Cell>{lastModified}</Table.Cell>
<Table.Cell>{size.raw}</Table.Cell>
</Table.Row>
}
>
<Modal.Header>{name}</Modal.Header>
<Modal.Content scrolling>
{displayContent &&
modalContent &&
displayContent(modalContent, name)}
</Modal.Content>
<Modal.Actions>
<Button primary as="a" href={getFileURL(name, false)}>
<Icon name="download" /> Download
</Button>
</Modal.Actions>
</Modal>
))}
</Table.Body>
</Table>
{filesCount > PAGE_SIZE && (
<div className={styles["pagination-wrapper"]}>
<Pagination
activePage={pagination.page}
totalPages={Math.ceil(filesCount / PAGE_SIZE)}
onPageChange={(_, { activePage }) => {
setPagination({ ...pagination, page: activePage });
resetSort();
}}
size="mini"
/>
</div>
)}
</Segment>
)}
</Segment>
</>
);
}

Expand Down
30 changes: 13 additions & 17 deletions reana-ui/src/pages/workflowList/WorkflowList.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import moment from "moment";
import { useEffect, useRef, useState } from "react";
import { unstable_batchedUpdates } from "react-dom";
import { useDispatch, useSelector } from "react-redux";
import { Container, Dimmer, Loader } from "semantic-ui-react";

Expand All @@ -25,15 +24,15 @@ import {
userHasWorkflows,
getWorkflowRefresh,
} from "~/selectors";
import BasePage from "../BasePage";
import { Title } from "~/components";
import { Pagination, Search } from "~/components";
import { applyFilter } from "~/components/Search";
import BasePage from "../BasePage";
import Welcome from "./components/Welcome";
import WorkflowFilters from "./components/WorkflowFilters";
import WorkflowList from "./components/WorkflowList";
import { Pagination } from "~/components";

import styles from "./WorkflowList.module.scss";
import WorkflowFilters from "./components/WorkflowFilters";
import WorkflowSearch from "./components/WorkflowSearch";

const PAGE_SIZE = 5;

Expand Down Expand Up @@ -107,15 +106,6 @@ function Workflows() {
interval.current = null;
};

const applyFilter = (filter) => (value) => {
// FIXME: refactor once implemented by default in future versions of React
// https://github.com/facebook/react/issues/16387#issuecomment-521623662
unstable_batchedUpdates(() => {
filter(value);
setPagination({ ...pagination, page: 1 });
});
};

if (hideWelcomePage) {
return (
loading && (
Expand All @@ -142,12 +132,18 @@ function Workflows() {
<span>Your workflows</span>
<span className={styles.refresh}>Refreshed at {refreshedAt}</span>
</Title>
<WorkflowSearch search={applyFilter(setSearchFilter)} />
<Search
search={applyFilter(setSearchFilter, pagination, setPagination)}
/>
<WorkflowFilters
statusFilter={statusFilter}
setStatusFilter={applyFilter(setStatusFilter)}
setStatusFilter={applyFilter(
setStatusFilter,
pagination,
setPagination
)}
sortDir={sortDir}
setSortDir={applyFilter(setSortDir)}
setSortDir={applyFilter(setSortDir, pagination, setPagination)}
/>
<WorkflowList workflows={workflowArray} loading={loading} />
</Container>
Expand Down

0 comments on commit 2a07ff4

Please sign in to comment.