const history = createBrowserHistory();
-const store = configureStore({
- projects: [
- ],
- router: {
- location: null
- },
- auth: {
- user: undefined
- },
- sidePanel: []
-}, history);
+const store = configureStore(history);
store.dispatch(authActions.INIT());
const rootUuid = authService.getRootUuid();
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { SortDirection, DataColumn } from "../../components/data-table/data-column";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+
+type WithId<T> = T & { id: string };
+
+const actions = unionize({
+ SET_COLUMNS: ofType<WithId<{ columns: Array<DataColumn<any>> }>>(),
+ SET_FILTERS: ofType<WithId<{columnName: string, filters: DataTableFilterItem[]}>>(),
+ SET_ITEMS: ofType<WithId<{items: any[]}>>(),
+ SET_PAGE: ofType<WithId<{page: number}>>(),
+ SET_ROWS_PER_PAGE: ofType<WithId<{rowsPerPage: number}>>(),
+ TOGGLE_COLUMN: ofType<WithId<{ columnName: string }>>(),
+ TOGGLE_SORT: ofType<WithId<{ columnName: string }>>(),
+ SET_SEARCH_VALUE: ofType<WithId<{searchValue: string}>>()
+}, { tag: "type", value: "payload" });
+
+export type DataExplorerAction = UnionOf<typeof actions>;
+
+export default actions;
+
+
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import dataExplorerReducer, { initialDataExplorer } from "./data-explorer-reducer";
+import actions from "./data-explorer-action";
+import { DataColumn } from "../../components/data-table/data-column";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+
+describe('data-explorer-reducer', () => {
+ it('should set columns', () => {
+ const columns: Array<DataColumn<any>> = [{
+ name: "Column 1",
+ render: jest.fn(),
+ selected: true
+ }];
+ const state = dataExplorerReducer(undefined,
+ actions.SET_COLUMNS({ id: "Data explorer", columns }));
+ expect(state["Data explorer"].columns).toEqual(columns);
+ });
+
+ it('should toggle sorting', () => {
+ const columns: Array<DataColumn<any>> = [{
+ name: "Column 1",
+ render: jest.fn(),
+ selected: true,
+ sortDirection: "asc"
+ }, {
+ name: "Column 2",
+ render: jest.fn(),
+ selected: true,
+ sortDirection: "none",
+ }];
+ const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
+ actions.TOGGLE_SORT({ id: "Data explorer", columnName: "Column 2" }));
+ expect(state["Data explorer"].columns[0].sortDirection).toEqual("none");
+ expect(state["Data explorer"].columns[1].sortDirection).toEqual("asc");
+ });
+
+ it('should set filters', () => {
+ const columns: Array<DataColumn<any>> = [{
+ name: "Column 1",
+ render: jest.fn(),
+ selected: true,
+ }];
+
+ const filters: DataTableFilterItem[] = [{
+ name: "Filter 1",
+ selected: true
+ }];
+ const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
+ actions.SET_FILTERS({ id: "Data explorer", columnName: "Column 1", filters }));
+ expect(state["Data explorer"].columns[0].filters).toEqual(filters);
+ });
+
+ it('should set items', () => {
+ const state = dataExplorerReducer({ "Data explorer": undefined },
+ actions.SET_ITEMS({ id: "Data explorer", items: ["Item 1", "Item 2"] }));
+ expect(state["Data explorer"].items).toEqual(["Item 1", "Item 2"]);
+ });
+
+ it('should set page', () => {
+ const state = dataExplorerReducer({ "Data explorer": undefined },
+ actions.SET_PAGE({ id: "Data explorer", page: 2 }));
+ expect(state["Data explorer"].page).toEqual(2);
+ });
+
+ it('should set rows per page', () => {
+ const state = dataExplorerReducer({ "Data explorer": undefined },
+ actions.SET_ROWS_PER_PAGE({ id: "Data explorer", rowsPerPage: 5 }));
+ expect(state["Data explorer"].rowsPerPage).toEqual(5);
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataColumn, toggleSortDirection, resetSortDirection } from "../../components/data-table/data-column";
+import actions, { DataExplorerAction } from "./data-explorer-action";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+
+interface DataExplorer {
+ columns: Array<DataColumn<any>>;
+ items: any[];
+ page: number;
+ rowsPerPage: number;
+ searchValue: string;
+}
+
+export const initialDataExplorer: DataExplorer = {
+ columns: [],
+ items: [],
+ page: 0,
+ rowsPerPage: 0,
+ searchValue: ""
+};
+
+export type DataExplorerState = Record<string, DataExplorer | undefined>;
+
+const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
+ actions.match(action, {
+ SET_COLUMNS: ({ id, columns }) => update(state, id, setColumns(columns)),
+ SET_FILTERS: ({ id, columnName, filters }) => update(state, id, mapColumns(setFilters(columnName, filters))),
+ SET_ITEMS: ({ id, items }) => update(state, id, explorer => ({ ...explorer, items })),
+ SET_PAGE: ({ id, page }) => update(state, id, explorer => ({ ...explorer, page })),
+ SET_ROWS_PER_PAGE: ({ id, rowsPerPage }) => update(state, id, explorer => ({ ...explorer, rowsPerPage })),
+ TOGGLE_SORT: ({ id, columnName }) => update(state, id, mapColumns(toggleSort(columnName))),
+ TOGGLE_COLUMN: ({ id, columnName }) => update(state, id, mapColumns(toggleColumn(columnName))),
+ default: () => state
+ });
+
+export default dataExplorerReducer;
+
+export const get = (state: DataExplorerState, id: string) => state[id] || initialDataExplorer;
+
+const update = (state: DataExplorerState, id: string, updateFn: (dataExplorer: DataExplorer) => DataExplorer) =>
+ ({ ...state, [id]: updateFn(get(state, id)) });
+
+const setColumns = (columns: Array<DataColumn<any>>) =>
+ (dataExplorer: DataExplorer) =>
+ ({ ...dataExplorer, columns });
+
+const mapColumns = (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
+ (dataExplorer: DataExplorer) =>
+ ({ ...dataExplorer, columns: dataExplorer.columns.map(mapFn) });
+
+const toggleSort = (columnName: string) =>
+ (column: DataColumn<any>) => column.name === columnName
+ ? toggleSortDirection(column)
+ : resetSortDirection(column);
+
+const toggleColumn = (columnName: string) =>
+ (column: DataColumn<any>) => column.name === columnName
+ ? { ...column, selected: !column.selected }
+ : column;
+
+const setFilters = (columnName: string, filters: DataTableFilterItem[]) =>
+ (column: DataColumn<any>) => column.name === columnName
+ ? { ...column, filters }
+ : column;
import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
import authReducer, { AuthState } from "./auth/auth-reducer";
import collectionsReducer from "./collection/collection-reducer";
+import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
auth: AuthState;
projects: ProjectState;
router: RouterState;
+ dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
}
projects: projectsReducer,
collections: collectionsReducer,
router: routerReducer,
+ dataExplorer: dataExplorerReducer,
sidePanel: sidePanelReducer
});
-export default function configureStore(initialState: RootState, history: History) {
+export default function configureStore(history: History) {
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
- return createStore(rootReducer, initialState!, enhancer);
+ return createStore(rootReducer, enhancer);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../store/store";
+import DataExplorer from "../../components/data-explorer/data-explorer";
+import { get } from "../../store/data-explorer/data-explorer-reducer";
+
+export default connect((state: RootState, props: { id: string }) => get(state.dataExplorer, props.id))(DataExplorer);
import { ProjectExplorerItem } from './project-explorer-item';
import { Grid, Typography } from '@material-ui/core';
import { formatDate, formatFileSize } from '../../common/formatters';
-import DataExplorer from '../../components/data-explorer/data-explorer';
+import DataExplorer from '../data-explorer/data-explorer';
import { DataColumn, toggleSortDirection, resetSortDirection } from '../../components/data-table/data-column';
import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
import { ContextMenuAction } from '../../components/context-menu/context-menu';
+import { DispatchProp, connect } from 'react-redux';
+import actions from "../../store/data-explorer/data-explorer-action";
-export interface ProjectExplorerContextActions {
- onAddToFavourite: (item: ProjectExplorerItem) => void;
- onCopy: (item: ProjectExplorerItem) => void;
- onDownload: (item: ProjectExplorerItem) => void;
- onMoveTo: (item: ProjectExplorerItem) => void;
- onRemove: (item: ProjectExplorerItem) => void;
- onRename: (item: ProjectExplorerItem) => void;
- onShare: (item: ProjectExplorerItem) => void;
-}
-
-interface ProjectExplorerProps {
- items: ProjectExplorerItem[];
-}
-
-interface ProjectExplorerState {
- columns: Array<DataColumn<ProjectExplorerItem>>;
- searchValue: string;
- page: number;
- rowsPerPage: number;
-}
-
-class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplorerState> {
- state: ProjectExplorerState = {
- searchValue: "",
- page: 0,
- rowsPerPage: 10,
- columns: [{
- name: "Name",
- selected: true,
- sortDirection: "asc",
- render: renderName
- }, {
- name: "Status",
- selected: true,
- filters: [{
- name: "In progress",
- selected: true
- }, {
- name: "Complete",
- selected: true
- }],
- render: renderStatus
- }, {
- name: "Type",
- selected: true,
- filters: [{
- name: "Collection",
- selected: true
- }, {
- name: "Group",
- selected: true
- }],
- render: item => renderType(item.type)
- }, {
- name: "Owner",
- selected: true,
- render: item => renderOwner(item.owner)
- }, {
- name: "File size",
- selected: true,
- sortDirection: "none",
- render: item => renderFileSize(item.fileSize)
- }, {
- name: "Last modified",
- selected: true,
- render: item => renderDate(item.lastModified)
- }]
- };
+export const PROJECT_EXPLORER_ID = "projectExplorer";
+class ProjectExplorer extends React.Component<DispatchProp> {
contextMenuActions = [[{
icon: "fas fa-users fa-fw",
render() {
return <DataExplorer
- items={this.props.items}
- columns={this.state.columns}
+ id={PROJECT_EXPLORER_ID}
contextActions={this.contextMenuActions}
- searchValue={this.state.searchValue}
- page={this.state.page}
- rowsPerPage={this.state.rowsPerPage}
onColumnToggle={this.toggleColumn}
onFiltersChange={this.changeFilters}
onRowClick={console.log}
onChangeRowsPerPage={this.changeRowsPerPage} />;
}
+ componentDidMount() {
+ this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_EXPLORER_ID, columns }));
+ }
+
toggleColumn = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
- this.setState({
- columns: this.state.columns.map(column =>
- column.name === toggledColumn.name
- ? { ...column, selected: !column.selected }
- : column
- )
- });
+ this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name }));
}
toggleSort = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
- this.setState({
- columns: this.state.columns.map(column =>
- column.name === toggledColumn.name
- ? toggleSortDirection(column)
- : resetSortDirection(column)
- )
- });
+ this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name }));
}
changeFilters = (filters: DataTableFilterItem[], updatedColumn: DataColumn<ProjectExplorerItem>) => {
- this.setState({
- columns: this.state.columns.map(column =>
- column.name === updatedColumn.name
- ? { ...column, filters }
- : column
- )
- });
+ this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_EXPLORER_ID, columnName: updatedColumn.name, filters }));
}
executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
}
search = (searchValue: string) => {
- this.setState({ searchValue });
+ this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_EXPLORER_ID, searchValue }));
}
changePage = (page: number) => {
- this.setState({ page });
+ this.props.dispatch(actions.SET_PAGE({ id: PROJECT_EXPLORER_ID, page }));
}
changeRowsPerPage = (rowsPerPage: number) => {
- this.setState({ rowsPerPage });
+ this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_EXPLORER_ID, rowsPerPage }));
}
}
{item.status || "-"}
</Typography>;
-export default ProjectExplorer;
+const columns: Array<DataColumn<ProjectExplorerItem>> = [{
+ name: "Name",
+ selected: true,
+ sortDirection: "asc",
+ render: renderName
+}, {
+ name: "Status",
+ selected: true,
+ filters: [{
+ name: "In progress",
+ selected: true
+ }, {
+ name: "Complete",
+ selected: true
+ }],
+ render: renderStatus
+}, {
+ name: "Type",
+ selected: true,
+ filters: [{
+ name: "Collection",
+ selected: true
+ }, {
+ name: "Group",
+ selected: true
+ }],
+ render: item => renderType(item.type)
+}, {
+ name: "Owner",
+ selected: true,
+ render: item => renderOwner(item.owner)
+}, {
+ name: "File size",
+ selected: true,
+ sortDirection: "none",
+ render: item => renderFileSize(item.fileSize)
+}, {
+ name: "Last modified",
+ selected: true,
+ render: item => renderDate(item.lastModified)
+}];
+
+export default connect()(ProjectExplorer);
import { RouteComponentProps } from 'react-router-dom';
import { DispatchProp, connect } from 'react-redux';
import { ProjectState, findTreeItem } from '../../store/project/project-reducer';
-import ProjectExplorer from '../../views-components/project-explorer/project-explorer';
import { RootState } from '../../store/store';
-import { mapProjectTreeItem } from './project-panel-selectors';
+import ProjectExplorer from '../../views-components/project-explorer/project-explorer';
interface ProjectPanelDataProps {
projects: ProjectState;
class ProjectPanel extends React.Component<ProjectPanelProps> {
render() {
- const project = findTreeItem(this.props.projects, this.props.match.params.name);
- const projectItems = project && project.items || [];
return (
- <ProjectExplorer items={projectItems.map(mapProjectTreeItem)} />
+ <ProjectExplorer />
);
}
}
import { connect, DispatchProp } from "react-redux";
import { Route, Switch } from "react-router";
import authActions from "../../store/auth/auth-action";
+import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
import { User } from "../../models/user";
import { RootState } from "../../store/store";
import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
import ProjectTree from '../../views-components/project-tree/project-tree';
import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
import { Project } from "../../models/project";
-import { getTreePath } from '../../store/project/project-reducer';
+import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
import ProjectPanel from '../project-panel/project-panel';
+import { PROJECT_EXPLORER_ID } from '../../views-components/project-explorer/project-explorer';
+import { ProjectExplorerItem } from '../../views-components/project-explorer/project-explorer-item';
import sidePanelActions from '../../store/side-panel/side-panel-action';
import { projectService } from '../../services/services';
import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
});
this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
this.props.dispatch(push(`/project/${itemId}`));
+
+ const project = findTreeItem(this.props.projects, itemId);
+ const items: ProjectExplorerItem[] = project && project.items
+ ? project.items.map(({ data }) => ({
+ uuid: data.uuid,
+ name: data.name,
+ type: data.kind,
+ owner: data.ownerUuid,
+ lastModified: data.modifiedAt
+ }))
+ : [];
+ this.props.dispatch(dataExplorerActions.SET_ITEMS({ id: PROJECT_EXPLORER_ID, items }));
}
render() {