Merge branch 'master' into 13703-data-explorer-and-contents-api
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 4 Jul 2018 11:38:22 +0000 (13:38 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 4 Jul 2018 11:38:22 +0000 (13:38 +0200)
refs #13703

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

14 files changed:
src/common/api/common-resource-service.ts
src/common/api/order-builder.test.ts
src/common/api/order-builder.ts
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/services/groups-service/groups-service.ts
src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/navigation/navigation-action.ts
src/store/store.ts
src/views-components/data-explorer/data-explorer.tsx
src/views/project-panel/project-panel-item.ts
src/views/project-panel/project-panel-middleware.ts [new file with mode: 0644]
src/views/project-panel/project-panel.tsx

index 3e147b224772257b75967d33e4ccf9286037399a..58bcaa5ff3c70bac428f61ae93db9f6dc60debd7 100644 (file)
@@ -91,7 +91,7 @@ export default class CommonResourceService<T extends Resource> {
         const params = {
             ...other,
             filters: filters ? filters.get() : undefined,
-            order: order ? order.get() : undefined
+            order: order ? order.getOrder() : undefined
         };
         return this.serverApi
             .get(this.resourceType, {
index c184ebce0036abe300a93ad63a857ae767984cdd..b80756d408ef55770727727f9f2f3b3095e4a03e 100644 (file)
@@ -6,11 +6,22 @@ import OrderBuilder from "./order-builder";
 
 describe("OrderBuilder", () => {
     it("should build correct order query", () => {
-        const orderBuilder = new OrderBuilder();
-        const order = orderBuilder
-            .addAsc("name")
-            .addDesc("modified_at")
-            .get();
-        expect(order).toEqual(["name asc","modified_at desc"]);
+        const order = OrderBuilder
+            .create()
+            .addAsc("kind")
+            .addDesc("modifiedAt")
+            .getOrder();
+        expect(order).toEqual(["kind asc", "modified_at desc"]);
+    });
+
+    it("should combine results with other builder", () => {
+        const order = OrderBuilder
+            .create()
+            .addAsc("kind")
+            .concat(OrderBuilder
+                .create("properties")
+                .addDesc("modifiedAt"))
+            .getOrder();
+        expect(order).toEqual(["kind asc", "properties.modified_at desc"]);
     });
 });
index cc3eadbb904ffb947c34121f8852a35fab256b18..08d17b18e9d5b3ed7adba85fd36e8074d5abaaae 100644 (file)
@@ -2,21 +2,46 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import * as _ from "lodash";
+import { Resource } from "./common-resource-service";
 
-export default class OrderBuilder {
-    private order: string[] = [];
+export default class OrderBuilder<T extends Resource = Resource> {
 
-    addAsc(attribute: string) {
-        this.order.push(`${attribute} asc`);
-        return this;
+    static create<T extends Resource = Resource>(prefix?: string){
+        return new OrderBuilder<T>([], prefix);
     }
 
-    addDesc(attribute: string) {
-        this.order.push(`${attribute} desc`);
-        return this;
+    private constructor(
+        private order: string[] = [], 
+        private prefix = ""){}
+
+    private getRule (direction: string, attribute: keyof T) {
+        const prefix = this.prefix ? this.prefix + "." : "";
+        return `${prefix}${_.snakeCase(attribute.toString())} ${direction}`;
+    }
+
+    addAsc(attribute: keyof T) {
+        return new OrderBuilder<T>(
+            [...this.order, this.getRule("asc", attribute)],
+            this.prefix
+        );
+    }
+
+    addDesc(attribute: keyof T) {
+        return new OrderBuilder<T>(
+            [...this.order, this.getRule("desc", attribute)],
+            this.prefix
+        );
+    }
+
+    concat(orderBuilder: OrderBuilder){
+        return new OrderBuilder<T>(
+            this.order.concat(orderBuilder.getOrder()),
+            this.prefix
+        );
     }
 
-    get() {
-        return this.order;
+    getOrder() {
+        return this.order.slice();
     }
 }
index ff51c71c796fece83395d1b99a627dec1eb05d33..a0ddc28a89eebe781fd128e8b3f4c92454341946 100644 (file)
@@ -7,7 +7,7 @@ import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles
 import MoreVertIcon from "@material-ui/icons/MoreVert";
 import ContextMenu, { ContextMenuActionGroup, ContextMenuAction } from "../../components/context-menu/context-menu";
 import ColumnSelector from "../../components/column-selector/column-selector";
-import DataTable, { DataColumns } from "../../components/data-table/data-table";
+import DataTable, { DataColumns, DataItem } from "../../components/data-table/data-table";
 import { mockAnchorFromMouseEvent } from "../../components/popover/helpers";
 import { DataColumn } from "../../components/data-table/data-column";
 import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
@@ -15,6 +15,7 @@ import SearchInput from '../search-input/search-input';
 
 interface DataExplorerProps<T> {
     items: T[];
+    itemsAvailable: number;
     columns: DataColumns<T>;
     contextActions: ContextMenuActionGroup[];
     searchValue: string;
@@ -38,7 +39,7 @@ interface DataExplorerState<T> {
     };
 }
 
-class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>, DataExplorerState<T>> {
+class DataExplorer<T extends DataItem> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>, DataExplorerState<T>> {
     state: DataExplorerState<T> = {
         contextMenu: {}
     };
@@ -73,7 +74,7 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
                 {this.props.items.length > 0 &&
                     <Grid container justify="flex-end">
                         <TablePagination
-                            count={this.props.items.length}
+                            count={this.props.itemsAvailable}
                             rowsPerPage={this.props.rowsPerPage}
                             rowsPerPageOptions={this.props.rowsPerPageOptions}
                             page={this.props.page}
index e8a5b24e02c2fe22488236f1ae703f36ef482c8d..e96839e5d7891b4fff3ffca1baf3f4daa7421023 100644 (file)
@@ -8,7 +8,9 @@ import { DataColumn, SortDirection } from './data-column';
 import DataTableFilters, { DataTableFilterItem } from "../data-table-filters/data-table-filters";
 
 export type DataColumns<T> = Array<DataColumn<T>>;
-
+export interface DataItem {
+    key: React.Key;
+}
 export interface DataTableProps<T> {
     items: T[];
     columns: DataColumns<T>;
@@ -18,7 +20,7 @@ export interface DataTableProps<T> {
     onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
 }
 
-class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRules>> {
+class DataTable<T extends DataItem> extends React.Component<DataTableProps<T> & WithStyles<CssRules>> {
     render() {
         const { items, classes } = this.props;
         return <div className={classes.tableContainer}>
@@ -66,10 +68,10 @@ class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRul
     }
 
     renderBodyRow = (item: T, index: number) => {
-        const { columns, onRowClick, onRowContextMenu } = this.props;
+        const { onRowClick, onRowContextMenu } = this.props;
         return <TableRow
             hover
-            key={index}
+            key={item.key}
             onClick={event => onRowClick && onRowClick(event, item)}
             onContextMenu={event => onRowContextMenu && onRowContextMenu(event, item)}>
             {this.mapVisibleColumns((column, index) => (
index f230c70fa0bdd94ca3867f283fd2e7babe9303e2..1e5318530853fbbe4b947b45311261550a9f7235 100644 (file)
@@ -38,7 +38,7 @@ export default class GroupsService extends CommonResourceService<GroupResource>
         const params = {
             ...other,
             filters: filters ? filters.get() : undefined,
-            order: order ? order.get() : undefined
+            order: order ? order.getOrder() : undefined
         };
         return this.serverApi
             .get(this.resourceType + `${uuid}/contents/`, {
index fd3a7afe04dd5caaf35003098cc398dee78147e8..2c161c2cbbfd666e5b90beaa9b763025a8a2c056 100644 (file)
@@ -4,17 +4,19 @@
 
 import { default as unionize, ofType, UnionOf } from "unionize";
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
-import { DataColumns } from "../../components/data-table/data-table";
+import { DataColumns, DataItem } from "../../components/data-table/data-table";
 
 const actions = unionize({
-    SET_COLUMNS: ofType<{id: string, columns: DataColumns<any> }>(),
-    SET_FILTERS: ofType<{id: string,columnName: string, filters: DataTableFilterItem[]}>(),
-    SET_ITEMS: ofType<{id: string,items: any[]}>(),
-    SET_PAGE: ofType<{id: string,page: number}>(),
-    SET_ROWS_PER_PAGE: ofType<{id: string,rowsPerPage: number}>(),
-    TOGGLE_COLUMN: ofType<{id: string, columnName: string }>(),
-    TOGGLE_SORT: ofType<{id: string, columnName: string }>(),
-    SET_SEARCH_VALUE: ofType<{id: string,searchValue: string}>()
+    RESET_PAGINATION: ofType<{ id: string }>(),
+    REQUEST_ITEMS: ofType<{ id: string }>(),
+    SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
+    SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilterItem[] }>(),
+    SET_ITEMS: ofType<{ id: string, items: DataItem[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
+    SET_PAGE: ofType<{ id: string, page: number }>(),
+    SET_ROWS_PER_PAGE: ofType<{ id: string, rowsPerPage: number }>(),
+    TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
+    TOGGLE_SORT: ofType<{ id: string, columnName: string }>(),
+    SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
 }, { tag: "type", value: "payload" });
 
 export type DataExplorerAction = UnionOf<typeof actions>;
index efb45da8322b6e0c83f3f36ace56e421883da9ed..0622f0ff2ee990964f1a13e440f00f5b6a925322 100644 (file)
@@ -10,6 +10,7 @@ import { DataColumns } from "../../components/data-table/data-table";
 interface DataExplorer {
     columns: DataColumns<any>;
     items: any[];
+    itemsAvailable: number;
     page: number;
     rowsPerPage: number;
     rowsPerPageOptions?: number[];
@@ -19,6 +20,7 @@ interface DataExplorer {
 export const initialDataExplorer: DataExplorer = {
     columns: [],
     items: [],
+    itemsAvailable: 0,
     page: 0,
     rowsPerPage: 10,
     rowsPerPageOptions: [5, 10, 25, 50],
@@ -29,14 +31,17 @@ export type DataExplorerState = Record<string, DataExplorer | undefined>;
 
 const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
     actions.match(action, {
+        RESET_PAGINATION: ({ id }) =>
+            update(state, id, explorer => ({ ...explorer, page: 0 })),
+
         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_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
+            update(state, id, explorer => ({ ...explorer, items, itemsAvailable, page, rowsPerPage })),
 
         SET_PAGE: ({ id, page }) =>
             update(state, id, explorer => ({ ...explorer, page })),
index daeb26fd2a9ee0d9e0b4ab7f8dd5441e93b77412..ec6e9bb50cf36339f3e8b3e0ba2e14822dbf50af 100644 (file)
@@ -61,17 +61,9 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
                 : dispatch<any>(getProjectList(itemId));
 
             promise
-                .then(() => dispatch<any>(getCollectionList(itemId)))
                 .then(() => dispatch<any>(() => {
-                    const { projects, collections } = getState();
-                    dispatch(dataExplorerActions.SET_ITEMS({
-                        id: PROJECT_PANEL_ID,
-                        items: projectPanelItems(
-                            projects.items,
-                            treeItem.data.uuid,
-                            collections
-                        )
-                    }));
+                    dispatch(dataExplorerActions.RESET_PAGINATION({id: PROJECT_PANEL_ID}));
+                    dispatch(dataExplorerActions.REQUEST_ITEMS({id: PROJECT_PANEL_ID}));
                 }));
 
         }
index 68c5d8238c74894857e08f7d34f8e0df90bcfb9b..0c32e6538621dfdc0106173e025fd19bff700675 100644 (file)
@@ -12,6 +12,7 @@ import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reduce
 import authReducer, { AuthState } from "./auth/auth-reducer";
 import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
 import collectionsReducer, { CollectionState } from "./collection/collection-reducer";
+import { projectPanelMiddleware } from '../views/project-panel/project-panel-middleware';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -40,7 +41,8 @@ const rootReducer = combineReducers({
 export default function configureStore(history: History) {
     const middlewares: Middleware[] = [
         routerMiddleware(history),
-        thunkMiddleware
+        thunkMiddleware,
+        projectPanelMiddleware
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
index d9d1fc4acd86275d7ffc8f7f515f39b1b5c95be0..f89bc65e9bd37db0a7f087bea34e27d6085f4a30 100644 (file)
@@ -6,7 +6,53 @@ import { connect } from "react-redux";
 import { RootState } from "../../store/store";
 import DataExplorer from "../../components/data-explorer/data-explorer";
 import { getDataExplorer } from "../../store/data-explorer/data-explorer-reducer";
+import { Dispatch } from "redux";
+import actions from "../../store/data-explorer/data-explorer-action";
+import { DataColumn } from "../../components/data-table/data-column";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+import { ContextMenuAction, ContextMenuActionGroup } from "../../components/context-menu/context-menu";
+
+interface Props {
+    id: string;
+    contextActions: ContextMenuActionGroup[];
+    onRowClick: (item: any) => void;
+    onContextAction: (action: ContextMenuAction, item: any) => void;
+}
+
+const mapStateToProps = (state: RootState, { id, contextActions }: Props) =>
+    getDataExplorer(state.dataExplorer, id);
+
+const mapDispatchToProps = (dispatch: Dispatch, { id, contextActions, onRowClick, onContextAction }: Props) => ({
+    onSearch: (searchValue: string) => {
+        dispatch(actions.SET_SEARCH_VALUE({ id, searchValue }));
+    },
+
+    onColumnToggle: (column: DataColumn<any>) => {
+        dispatch(actions.TOGGLE_COLUMN({ id, columnName: column.name }));
+    },
+
+    onSortToggle: (column: DataColumn<any>) => {
+        dispatch(actions.TOGGLE_SORT({ id, columnName: column.name }));
+    },
+
+    onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+        dispatch(actions.SET_FILTERS({ id, columnName: column.name, filters }));
+    },
+
+    onChangePage: (page: number) => {
+        dispatch(actions.SET_PAGE({ id, page }));
+    },
+
+    onChangeRowsPerPage: (rowsPerPage: number) => {
+        dispatch(actions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+    },
+
+    contextActions,
+
+    onRowClick,
+
+    onContextAction
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(DataExplorer);
 
-export default connect((state: RootState, props: { id: string }) =>
-    getDataExplorer(state.dataExplorer, props.id)
-)(DataExplorer);
index e0eb84f05ad4c16c810dd6a8b9e477b89ae11df7..cf77aaf14e2c84ba6be3f749c004eb83532ae24f 100644 (file)
@@ -2,14 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { TreeItem } from "../../components/tree/tree";
-import { Project } from "../../models/project";
-import { getResourceKind, Resource, ResourceKind } from "../../models/resource";
+import { Resource } from "../../common/api/common-resource-service";
+import { DataItem } from "../../components/data-table/data-table";
 
-export interface ProjectPanelItem {
+export interface ProjectPanelItem extends DataItem {
     uuid: string;
     name: string;
-    kind: ResourceKind;
+    kind: string;
     url: string;
     owner: string;
     lastModified: string;
@@ -17,11 +16,13 @@ export interface ProjectPanelItem {
     status?: string;
 }
 
-function resourceToDataItem(r: Resource, kind?: ResourceKind) {
+export function resourceToDataItem(r: Resource): ProjectPanelItem {
     return {
+        key: r.uuid,
         uuid: r.uuid,
-        name: r.name,
-        kind: kind ? kind : getResourceKind(r.kind),
+        name: r.uuid,
+        kind: r.kind,
+        url: "",
         owner: r.ownerUuid,
         lastModified: r.modifiedAt
     };
diff --git a/src/views/project-panel/project-panel-middleware.ts b/src/views/project-panel/project-panel-middleware.ts
new file mode 100644 (file)
index 0000000..9be5981
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Middleware } from "redux";
+import actions from "../../store/data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID, columns } from "./project-panel";
+import { groupsService } from "../../services/services";
+import { RootState } from "../../store/store";
+import { getDataExplorer } from "../../store/data-explorer/data-explorer-reducer";
+import { resourceToDataItem } from "./project-panel-item";
+
+export const projectPanelMiddleware: Middleware = store => next => {
+    next(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
+
+    return action => {
+
+        const handleProjectPanelAction = <T extends { id: string }>(handler: (data: T) => void) =>
+            (data: T) => {
+                next(action);
+                if (data.id === PROJECT_PANEL_ID) {
+                    handler(data);
+                }
+            };
+
+        actions.match(action, {
+            SET_PAGE: handleProjectPanelAction(() => {
+                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+            }),
+            SET_ROWS_PER_PAGE: handleProjectPanelAction(() => {
+                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+            }),
+            REQUEST_ITEMS: handleProjectPanelAction(() => {
+                const state = store.getState() as RootState;
+                const dataExplorer = getDataExplorer(state.dataExplorer, PROJECT_PANEL_ID);
+                groupsService
+                    .contents(state.projects.currentItemId, {
+                        limit: dataExplorer.rowsPerPage,
+                        offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    })
+                    .then(response => {
+                        store.dispatch(actions.SET_ITEMS({
+                            id: PROJECT_PANEL_ID,
+                            items: response.items.map(resourceToDataItem),
+                            itemsAvailable: response.itemsAvailable,
+                            page: Math.floor(response.offset / response.limit),
+                            rowsPerPage: response.limit
+                        }));
+                    });
+
+            }),
+            default: () => next(action)
+        });
+    };
+};
index 6bfa61e0322de57f6040207448a5c8855759ad19..ff3dd9c8dd0876c1eed17d1490130e7e01d4ecc8 100644 (file)
@@ -7,8 +7,6 @@ import { ProjectPanelItem } from './project-panel-item';
 import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { formatDate, formatFileSize } from '../../common/formatters';
 import DataExplorer from "../../views-components/data-explorer/data-explorer";
-import { DataColumn, toggleSortDirection } 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";
@@ -44,55 +42,21 @@ class ProjectPanel extends React.Component<ProjectPanelProps> {
             <DataExplorer
                 id={PROJECT_PANEL_ID}
                 contextActions={contextMenuActions}
-                onColumnToggle={this.toggleColumn}
-                onFiltersChange={this.changeFilters}
                 onRowClick={this.props.onItemClick}
-                onSortToggle={this.toggleSort}
-                onSearch={this.search}
-                onContextAction={this.executeAction}
-                onChangePage={this.changePage}
-                onChangeRowsPerPage={this.changeRowsPerPage} />;
+                onContextAction={this.executeAction} />;
         </div>;
     }
 
-    componentDidMount() {
-        this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
-    }
-
     componentWillReceiveProps({ match, currentItemId }: ProjectPanelProps) {
         if (match.params.id !== currentItemId) {
             this.props.onItemRouteChange(match.params.id);
         }
     }
 
-    toggleColumn = (toggledColumn: DataColumn<ProjectPanelItem>) => {
-        this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_PANEL_ID, columnName: toggledColumn.name }));
-    }
-
-    toggleSort = (column: DataColumn<ProjectPanelItem>) => {
-        this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_PANEL_ID, columnName: column.name }));
-    }
-
-    changeFilters = (filters: DataTableFilterItem[], column: DataColumn<ProjectPanelItem>) => {
-        this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_PANEL_ID, columnName: column.name, filters }));
-    }
-
     executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
         alert(`Executing ${action.name} on ${item.name}`);
     }
 
-    search = (searchValue: string) => {
-        this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_PANEL_ID, searchValue }));
-    }
-
-    changePage = (page: number) => {
-        this.props.dispatch(actions.SET_PAGE({ id: PROJECT_PANEL_ID, page }));
-    }
-
-    changeRowsPerPage = (rowsPerPage: number) => {
-        this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_PANEL_ID, rowsPerPage }));
-    }
-
 }
 
 type CssRules = "toolbar" | "button";
@@ -160,7 +124,7 @@ const renderStatus = (item: ProjectPanelItem) =>
         {item.status || "-"}
     </Typography>;
 
-const columns: DataColumns<ProjectPanelItem> = [{
+export const columns: DataColumns<ProjectPanelItem> = [{
     name: "Name",
     selected: true,
     sortDirection: "desc",