Skip to content

Commit 9db4cbf

Browse files
committed
workspace: add file search
- Extract out common `Search` component - Add search param to workspace endpoint closes reanahub#211
1 parent b33f367 commit 9db4cbf

File tree

7 files changed

+126
-110
lines changed

7 files changed

+126
-110
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Changes
44
Version 0.8.1 (UNRELEASED)
55
---------------------------
66

7+
- Adds support for HTML preview of workspace files.
8+
- Adds search by name in workflow file list page.
79
- Changes cluster health status page to represent availability instead of usage.
810

911
Version 0.8.0 (2021-11-22)

reana-ui/src/actions.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,12 @@ export function fetchWorkflowLogs(id) {
298298
};
299299
}
300300

301-
export function fetchWorkflowFiles(id, pagination) {
301+
export function fetchWorkflowFiles(id, pagination, search) {
302302
return async (dispatch) => {
303303
dispatch({ type: WORKFLOW_FILES_FETCH });
304+
const nameSearch = search ? JSON.stringify({ name: [search] }) : search;
304305
return await client
305-
.getWorkflowFiles(id, pagination)
306+
.getWorkflowFiles(id, pagination, nameSearch)
306307
.then((resp) =>
307308
dispatch({
308309
type: WORKFLOW_FILES_RECEIVED,

reana-ui/src/client.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export const WORKFLOWS_URL = (params) =>
3131
export const WORKFLOW_LOGS_URL = (id) => `${api}/api/workflows/${id}/logs`;
3232
export const WORKFLOW_SPECIFICATION_URL = (id) =>
3333
`${api}/api/workflows/${id}/specification`;
34-
export const WORKFLOW_FILES_URL = (id, pagination) =>
35-
`${api}/api/workflows/${id}/workspace?${stringifyQueryParams(pagination)}`;
34+
export const WORKFLOW_FILES_URL = (id, params) =>
35+
`${api}/api/workflows/${id}/workspace?${stringifyQueryParams(params)}`;
3636
export const WORKFLOW_FILE_URL = (id, filename, preview = true) =>
3737
`${api}/api/workflows/${id}/workspace/${filename}?${stringifyQueryParams(
3838
preview
@@ -126,8 +126,8 @@ class Client {
126126
return this._request(WORKFLOW_LOGS_URL(id));
127127
}
128128

129-
getWorkflowFiles(id, pagination) {
130-
return this._request(WORKFLOW_FILES_URL(id, pagination));
129+
getWorkflowFiles(id, pagination, search) {
130+
return this._request(WORKFLOW_FILES_URL(id, { ...pagination, search }));
131131
}
132132

133133
getWorkflowFile(id, filename) {

reana-ui/src/pages/workflowList/components/WorkflowSearch.js renamed to reana-ui/src/components/Search.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
-*- coding: utf-8 -*-
33
44
This file is part of REANA.
5-
Copyright (C) 2020 CERN.
5+
Copyright (C) 2020, 2021, 2022 CERN.
66
77
REANA is free software; you can redistribute it and/or modify it
88
under the terms of the MIT License; see LICENSE file for more details.
99
*/
1010

1111
import PropTypes from "prop-types";
12-
12+
import { unstable_batchedUpdates } from "react-dom";
1313
import { Input } from "semantic-ui-react";
1414
import debounce from "lodash/debounce";
1515

1616
const TYPING_DELAY = 1000;
1717

18-
export default function WorkflowSearch({ search }) {
18+
export default function Search({ search }) {
1919
const handleChange = debounce(search, TYPING_DELAY);
2020
return (
2121
<Input
@@ -27,6 +27,15 @@ export default function WorkflowSearch({ search }) {
2727
);
2828
}
2929

30-
WorkflowSearch.propTypes = {
30+
Search.propTypes = {
3131
search: PropTypes.func.isRequired,
3232
};
33+
34+
export const applyFilter = (filter, pagination, setPagination) => (value) => {
35+
// FIXME: refactor once implemented by default in future versions of React
36+
// https://github.com/facebook/react/issues/16387#issuecomment-521623662c
37+
unstable_batchedUpdates(() => {
38+
filter(value);
39+
setPagination({ ...pagination, page: 1 });
40+
});
41+
};

reana-ui/src/components/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export { default as JupyterNotebookIcon } from "./JupyterNotebookIcon";
2020
export { default as WorkflowDeleteModal } from "./WorkflowDeleteModal";
2121
export { default as WorkflowActionsPopup } from "./WorkflowActionsPopup";
2222
export { default as PieChart } from "./PieChart";
23+
export { default as Search } from "./Search";

reana-ui/src/pages/workflowDetails/components/WorkflowFiles.js

Lines changed: 90 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ import {
2828
loadingDetails,
2929
} from "~/selectors";
3030
import { fetchWorkflowFiles } from "~/actions";
31+
import client, { WORKFLOW_FILE_URL } from "~/client";
3132
import { getMimeType } from "~/util";
32-
import { Pagination } from "~/components";
33+
import { Pagination, Search } from "~/components";
34+
import { applyFilter } from "~/components/Search";
3335

3436
import styles from "./WorkflowFiles.module.scss";
35-
import client, { WORKFLOW_FILE_URL } from "~/client";
37+
38+
const FILE_SIZE_LIMIT = 5 * 1024 ** 2; // 5MB
39+
const PAGE_SIZE = 15;
3640

3741
const PREVIEW_MIME_PREFIX_WHITELIST = {
3842
"image/": {
@@ -74,9 +78,6 @@ const PREVIEW_MIME_PREFIX_WHITELIST = {
7478
},
7579
};
7680

77-
const FILE_SIZE_LIMIT = 5 * 1024 ** 2; // 5MB
78-
const PAGE_SIZE = 15;
79-
8081
export default function WorkflowFiles({ id }) {
8182
const dispatch = useDispatch();
8283
const loading = useSelector(loadingDetails);
@@ -88,10 +89,11 @@ export default function WorkflowFiles({ id }) {
8889
const [sorting, setSorting] = useState({ column: null, direction: null });
8990
const [displayContent, setDisplayContent] = useState(() => () => null);
9091
const [pagination, setPagination] = useState({ page: 1, size: PAGE_SIZE });
92+
const [searchFilter, setSearchFilter] = useState();
9193

9294
useEffect(() => {
93-
dispatch(fetchWorkflowFiles(id, pagination));
94-
}, [dispatch, id, pagination]);
95+
dispatch(fetchWorkflowFiles(id, pagination, searchFilter));
96+
}, [dispatch, id, pagination, searchFilter]);
9597

9698
useEffect(() => {
9799
setFiles(_files);
@@ -197,91 +199,96 @@ export default function WorkflowFiles({ id }) {
197199
/>
198200
);
199201

200-
if (loading) {
201-
return <Loader active inline="centered" />;
202-
}
203-
204202
return !files ? (
205203
<Message
206204
icon="info circle"
207205
content="The workflow workspace was deleted."
208206
info
209207
/>
210208
) : (
211-
<Segment>
212-
<Table fixed compact basic="very">
213-
<Table.Header className={styles["table-header"]}>
214-
<Table.Row>
215-
<Table.HeaderCell
216-
sorted={sorting.column === "name" ? sorting.direction : null}
217-
onClick={() => handleSort("name")}
218-
>
219-
Name {headerIcon("name")}
220-
</Table.HeaderCell>
221-
<Table.HeaderCell
222-
sorted={
223-
sorting.column === "lastModified" ? sorting.direction : null
224-
}
225-
onClick={() => handleSort("lastModified")}
226-
>
227-
Modified {headerIcon("lastModified")}
228-
</Table.HeaderCell>
229-
<Table.HeaderCell
230-
sorted={sorting.column === "size" ? sorting.direction : null}
231-
onClick={() => handleSort("size")}
232-
>
233-
Size {headerIcon("size")}
234-
</Table.HeaderCell>
235-
</Table.Row>
236-
</Table.Header>
209+
<>
210+
<Search
211+
search={applyFilter(setSearchFilter, pagination, setPagination)}
212+
/>
213+
{loading ? (
214+
<Loader active inline="centered" />
215+
) : (
216+
<Segment>
217+
<Table fixed compact basic="very">
218+
<Table.Header className={styles["table-header"]}>
219+
<Table.Row>
220+
<Table.HeaderCell
221+
sorted={sorting.column === "name" ? sorting.direction : null}
222+
onClick={() => handleSort("name")}
223+
>
224+
Name {headerIcon("name")}
225+
</Table.HeaderCell>
226+
<Table.HeaderCell
227+
sorted={
228+
sorting.column === "lastModified" ? sorting.direction : null
229+
}
230+
onClick={() => handleSort("lastModified")}
231+
>
232+
Modified {headerIcon("lastModified")}
233+
</Table.HeaderCell>
234+
<Table.HeaderCell
235+
sorted={sorting.column === "size" ? sorting.direction : null}
236+
onClick={() => handleSort("size")}
237+
>
238+
Size {headerIcon("size")}
239+
</Table.HeaderCell>
240+
</Table.Row>
241+
</Table.Header>
237242

238-
<Table.Body>
239-
{files &&
240-
files.map(({ name, lastModified, size }) => (
241-
<Modal
242-
key={name}
243-
onOpen={() => getFile(name, size.raw)}
244-
closeIcon
245-
trigger={
246-
<Table.Row className={styles["files-row"]}>
247-
<Table.Cell>
248-
<Icon name="file" />
249-
{name}
250-
</Table.Cell>
251-
<Table.Cell>{lastModified}</Table.Cell>
252-
<Table.Cell>{size.raw}</Table.Cell>
253-
</Table.Row>
254-
}
255-
>
256-
<Modal.Header>{name}</Modal.Header>
257-
<Modal.Content scrolling>
258-
{displayContent &&
259-
modalContent &&
260-
displayContent(modalContent, name)}
261-
</Modal.Content>
262-
<Modal.Actions>
263-
<Button primary as="a" href={getFileURL(name, false)}>
264-
<Icon name="download" /> Download
265-
</Button>
266-
</Modal.Actions>
267-
</Modal>
268-
))}
269-
</Table.Body>
270-
</Table>
271-
{filesCount > PAGE_SIZE && (
272-
<div className={styles["pagination-wrapper"]}>
273-
<Pagination
274-
activePage={pagination.page}
275-
totalPages={Math.ceil(filesCount / PAGE_SIZE)}
276-
onPageChange={(_, { activePage }) => {
277-
setPagination({ ...pagination, page: activePage });
278-
resetSort();
279-
}}
280-
size="mini"
281-
/>
282-
</div>
243+
<Table.Body>
244+
{files &&
245+
files.map(({ name, lastModified, size }) => (
246+
<Modal
247+
key={name}
248+
onOpen={() => getFile(name, size.raw)}
249+
closeIcon
250+
trigger={
251+
<Table.Row className={styles["files-row"]}>
252+
<Table.Cell>
253+
<Icon name="file" />
254+
{name}
255+
</Table.Cell>
256+
<Table.Cell>{lastModified}</Table.Cell>
257+
<Table.Cell>{size.raw}</Table.Cell>
258+
</Table.Row>
259+
}
260+
>
261+
<Modal.Header>{name}</Modal.Header>
262+
<Modal.Content scrolling>
263+
{displayContent &&
264+
modalContent &&
265+
displayContent(modalContent, name)}
266+
</Modal.Content>
267+
<Modal.Actions>
268+
<Button primary as="a" href={getFileURL(name, false)}>
269+
<Icon name="download" /> Download
270+
</Button>
271+
</Modal.Actions>
272+
</Modal>
273+
))}
274+
</Table.Body>
275+
</Table>
276+
{filesCount > PAGE_SIZE && (
277+
<div className={styles["pagination-wrapper"]}>
278+
<Pagination
279+
activePage={pagination.page}
280+
totalPages={Math.ceil(filesCount / PAGE_SIZE)}
281+
onPageChange={(_, { activePage }) => {
282+
setPagination({ ...pagination, page: activePage });
283+
resetSort();
284+
}}
285+
size="mini"
286+
/>
287+
</div>
288+
)}
289+
</Segment>
283290
)}
284-
</Segment>
291+
</>
285292
);
286293
}
287294

reana-ui/src/pages/workflowList/WorkflowList.js

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import moment from "moment";
1212
import { useEffect, useRef, useState } from "react";
13-
import { unstable_batchedUpdates } from "react-dom";
1413
import { useDispatch, useSelector } from "react-redux";
1514
import { Container, Dimmer, Loader } from "semantic-ui-react";
1615

@@ -25,15 +24,15 @@ import {
2524
userHasWorkflows,
2625
getWorkflowRefresh,
2726
} from "~/selectors";
28-
import BasePage from "../BasePage";
2927
import { Title } from "~/components";
28+
import { Pagination, Search } from "~/components";
29+
import { applyFilter } from "~/components/Search";
30+
import BasePage from "../BasePage";
3031
import Welcome from "./components/Welcome";
32+
import WorkflowFilters from "./components/WorkflowFilters";
3133
import WorkflowList from "./components/WorkflowList";
32-
import { Pagination } from "~/components";
3334

3435
import styles from "./WorkflowList.module.scss";
35-
import WorkflowFilters from "./components/WorkflowFilters";
36-
import WorkflowSearch from "./components/WorkflowSearch";
3736

3837
const PAGE_SIZE = 5;
3938

@@ -107,15 +106,6 @@ function Workflows() {
107106
interval.current = null;
108107
};
109108

110-
const applyFilter = (filter) => (value) => {
111-
// FIXME: refactor once implemented by default in future versions of React
112-
// https://github.com/facebook/react/issues/16387#issuecomment-521623662
113-
unstable_batchedUpdates(() => {
114-
filter(value);
115-
setPagination({ ...pagination, page: 1 });
116-
});
117-
};
118-
119109
if (hideWelcomePage) {
120110
return (
121111
loading && (
@@ -142,12 +132,18 @@ function Workflows() {
142132
<span>Your workflows</span>
143133
<span className={styles.refresh}>Refreshed at {refreshedAt}</span>
144134
</Title>
145-
<WorkflowSearch search={applyFilter(setSearchFilter)} />
135+
<Search
136+
search={applyFilter(setSearchFilter, pagination, setPagination)}
137+
/>
146138
<WorkflowFilters
147139
statusFilter={statusFilter}
148-
setStatusFilter={applyFilter(setStatusFilter)}
140+
setStatusFilter={applyFilter(
141+
setStatusFilter,
142+
pagination,
143+
setPagination
144+
)}
149145
sortDir={sortDir}
150-
setSortDir={applyFilter(setSortDir)}
146+
setSortDir={applyFilter(setSortDir, pagination, setPagination)}
151147
/>
152148
<WorkflowList workflows={workflowArray} loading={loading} />
153149
</Container>

0 commit comments

Comments
 (0)