Merge branch '13703-data-explorer-and-contents-api'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 6 Jul 2018 10:57:38 +0000 (12:57 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 6 Jul 2018 10:57:38 +0000 (12:57 +0200)
refs #13703

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

28 files changed:
src/common/api/common-resource-service.ts
src/common/api/filter-builder.test.ts [new file with mode: 0644]
src/common/api/filter-builder.ts
src/common/api/order-builder.test.ts
src/common/api/order-builder.ts
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-column.ts
src/components/data-table/data-table.test.tsx
src/components/data-table/data-table.tsx
src/models/collection.ts
src/models/container-request.ts [new file with mode: 0644]
src/models/group.ts [new file with mode: 0644]
src/models/kinds.ts [new file with mode: 0644]
src/models/process.ts [new file with mode: 0644]
src/models/project.ts
src/models/workflow.ts [new file with mode: 0644]
src/services/collection-service/collection-service.ts
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/project-panel/project-panel-middleware.ts [new file with mode: 0644]
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-selectors.ts [deleted file]
src/views/project-panel/project-panel.tsx

index 3e147b224772257b75967d33e4ccf9286037399a..fe6c752c8ee1d4b2b42cc8d5bc64640ca8e93171 100644 (file)
@@ -90,8 +90,8 @@ export default class CommonResourceService<T extends Resource> {
         const { filters, order, ...other } = args;
         const params = {
             ...other,
-            filters: filters ? filters.get() : undefined,
-            order: order ? order.get() : undefined
+            filters: filters ? filters.serialize() : undefined,
+            order: order ? order.getOrder() : undefined
         };
         return this.serverApi
             .get(this.resourceType, {
diff --git a/src/common/api/filter-builder.test.ts b/src/common/api/filter-builder.test.ts
new file mode 100644 (file)
index 0000000..3424393
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import FilterBuilder from "./filter-builder";
+
+describe("FilterBuilder", () => {
+
+    let filters: FilterBuilder;
+
+    beforeEach(() => {
+        filters = FilterBuilder.create();
+    });
+
+    it("should add 'equal' rule", () => {
+        expect(
+            filters.addEqual("etag", "etagValue").serialize()
+        ).toEqual(`[["etag","=","etagValue"]]`);
+    });
+
+    it("should add 'like' rule", () => {
+        expect(
+            filters.addLike("etag", "etagValue").serialize()
+        ).toEqual(`[["etag","like","%etagValue%"]]`);
+    });
+
+    it("should add 'ilike' rule", () => {
+        expect(
+            filters.addILike("etag", "etagValue").serialize()
+        ).toEqual(`[["etag","ilike","%etagValue%"]]`);
+    });
+
+    it("should add 'is_a' rule", () => {
+        expect(
+            filters.addIsA("etag", "etagValue").serialize()
+        ).toEqual(`[["etag","is_a","etagValue"]]`);
+    });
+
+    it("should add 'is_a' rule for set", () => {
+        expect(
+            filters.addIsA("etag", ["etagValue1", "etagValue2"]).serialize()
+        ).toEqual(`[["etag","is_a",["etagValue1","etagValue2"]]]`);
+    });
+
+    it("should add 'in' rule", () => {
+        expect(
+            filters.addIn("etag", "etagValue").serialize()
+        ).toEqual(`[["etag","in","etagValue"]]`);
+    });
+
+    it("should add 'in' rule for set", () => {
+        expect(
+            filters.addIn("etag", ["etagValue1", "etagValue2"]).serialize()
+        ).toEqual(`[["etag","in",["etagValue1","etagValue2"]]]`);
+    });
+
+    it("should add multiple rules", () => {
+        expect(
+            filters
+                .addIn("etag", ["etagValue1", "etagValue2"])
+                .addEqual("href", "hrefValue")
+                .serialize()
+        ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
+    });
+
+    it("should concatenate multiple builders", () => {
+        expect(
+            filters
+                .concat(FilterBuilder.create().addIn("etag", ["etagValue1", "etagValue2"]))
+                .concat(FilterBuilder.create().addEqual("href", "hrefValue"))
+                .serialize()
+        ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
+    });
+
+    it("should add attribute prefix", () => {
+        expect(FilterBuilder
+            .create("myPrefix")
+            .addIn("etag", ["etagValue1", "etagValue2"])
+            .serialize())
+            .toEqual(`[["my_prefix.etag","in",["etagValue1","etagValue2"]]]`);
+    });
+
+
+
+
+});
index 443c763913635c066ffb3f5426e424f997d68a09..47772e2475baa8c325d112d73804096f0a02e1de 100644 (file)
@@ -2,34 +2,64 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-export enum FilterField {
-    UUID = "uuid",
-    OWNER_UUID = "owner_uuid"
-}
+import * as _ from "lodash";
+import { Resource } from "./common-resource-service";
 
-export default class FilterBuilder {
-    private filters = "";
+export default class FilterBuilder<T extends Resource = Resource> {
 
-    private addCondition(field: FilterField, cond: string, value?: string, prefix: string = "", postfix: string = "") {
-        if (value) {
-            this.filters += `["${field}","${cond}","${prefix}${value}${postfix}"]`;
-        }
-        return this;
+    static create<T extends Resource = Resource>(resourcePrefix = "") {
+        return new FilterBuilder<T>(resourcePrefix);
     }
 
-    public addEqual(field: FilterField, value?: string) {
+    constructor(
+        private resourcePrefix = "",
+        private filters = "") { }
+
+    public addEqual(field: keyof T, value?: string) {
         return this.addCondition(field, "=", value);
     }
 
-    public addLike(field: FilterField, value?: string) {
-        return this.addCondition(field, "like", value, "", "%");
+    public addLike(field: keyof T, value?: string) {
+        return this.addCondition(field, "like", value, "%", "%");
+    }
+
+    public addILike(field: keyof T, value?: string) {
+        return this.addCondition(field, "ilike", value, "%", "%");
+    }
+
+    public addIsA(field: keyof T, value?: string | string[]) {
+        return this.addCondition(field, "is_a", value);
+    }
+
+    public addIn(field: keyof T, value?: string | string[]) {
+        return this.addCondition(field, "in", value);
     }
 
-    public addILike(field: FilterField, value?: string) {
-        return this.addCondition(field, "ilike", value, "", "%");
+    public concat<O extends Resource>(filterBuilder: FilterBuilder<O>) {
+        return new FilterBuilder(this.resourcePrefix, this.filters + (this.filters && filterBuilder.filters ? "," : "") + filterBuilder.getFilters());
     }
 
-    public get() {
+    public getFilters() {
+        return this.filters;
+    }
+
+    public serialize() {
         return "[" + this.filters + "]";
     }
+
+    private addCondition(field: keyof T, cond: string, value?: string | string[], prefix: string = "", postfix: string = "") {
+        if (value) {
+            value = typeof value === "string"
+                ? `"${prefix}${value}${postfix}"`
+                : `["${value.join(`","`)}"]`;
+
+            const resourcePrefix = this.resourcePrefix
+                ? _.snakeCase(this.resourcePrefix) + "."
+                : "";
+
+            this.filters += `${this.filters ? "," : ""}["${resourcePrefix}${_.snakeCase(field.toString())}","${cond}",${value}]`;
+        }
+        return this;
+    }
+
 }
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..316379145eceed2e11f62541fa258544d4f7c1ef 100644 (file)
@@ -2,21 +2,41 @@
 //
 // 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 addRule (direction: string, attribute: keyof T) {
+        const prefix = this.prefix ? this.prefix + "." : "";
+        const order = [...this.order, `${prefix}${_.snakeCase(attribute.toString())} ${direction}`];
+        return new OrderBuilder<T>(order, prefix);
+    }
+
+    addAsc(attribute: keyof T) {
+        return this.addRule("asc", attribute);
+    }
+
+    addDesc(attribute: keyof T) {
+        return this.addRule("desc", attribute);
+    }
+
+    concat(orderBuilder: OrderBuilder){
+        return new OrderBuilder<T>(
+            this.order.concat(orderBuilder.getOrder()),
+            this.prefix
+        );
     }
 
-    get() {
-        return this.order;
+    getOrder() {
+        return this.order.slice();
     }
 }
index 94c7be6dab933ff6991edda0a56ebf4b1edc0d2d..33899c00c79aa242c902c7c4d6af7cc11c741900 100644 (file)
@@ -12,6 +12,7 @@ import ColumnSelector from "../column-selector/column-selector";
 import DataTable from "../data-table/data-table";
 import SearchInput from "../search-input/search-input";
 import { TablePagination } from "@material-ui/core";
+import { MockItem } from "../data-table/data-table.test";
 
 configure({ adapter: new Adapter() });
 
@@ -23,7 +24,7 @@ describe("<DataExplorer />", () => {
             {...mockDataExplorerProps()}
             contextActions={[]}
             onContextAction={onContextAction}
-            items={["Item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             columns={[{ name: "Column 1", render: jest.fn(), selected: true }]} />);
         expect(dataExplorer.find(ContextMenu).prop("actions")).toEqual([]);
         dataExplorer.find(DataTable).prop("onRowContextMenu")({
@@ -38,7 +39,7 @@ describe("<DataExplorer />", () => {
         const onSearch = jest.fn();
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
-            items={["item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             searchValue="search value"
             onSearch={onSearch} />);
         expect(dataExplorer.find(SearchInput).prop("value")).toEqual("search value");
@@ -54,7 +55,7 @@ describe("<DataExplorer />", () => {
             columns={columns}
             onColumnToggle={onColumnToggle}
             contextActions={[]}
-            items={["Item 1"]} />);
+            items={[{ key: "1", name: "item 1" }] as MockItem[]} />);
         expect(dataExplorer.find(ColumnSelector).prop("columns")).toBe(columns);
         dataExplorer.find(ColumnSelector).prop("onColumnToggle")("columns");
         expect(onColumnToggle).toHaveBeenCalledWith("columns");
@@ -65,7 +66,7 @@ describe("<DataExplorer />", () => {
         const onSortToggle = jest.fn();
         const onRowClick = jest.fn();
         const columns = [{ name: "Column 1", render: jest.fn(), selected: true }];
-        const items = ["Item 1"];
+        const items = [{ key: "1", name: "item 1" }] as MockItem[];
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
             columns={columns}
@@ -83,12 +84,11 @@ describe("<DataExplorer />", () => {
         expect(onRowClick).toHaveBeenCalledWith("rowClick");
     });
 
-    it("does not render <SearchInput/>, <ColumnSelector/> and <TablePagination/> if there is no items", () => {
+    it("does not render <TablePagination/> if there is no items", () => {
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
             items={[]}
         />);
-        expect(dataExplorer.find(SearchInput)).toHaveLength(0);
         expect(dataExplorer.find(TablePagination)).toHaveLength(0);
     });
 
@@ -97,7 +97,7 @@ describe("<DataExplorer />", () => {
         const onChangeRowsPerPage = jest.fn();
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
-            items={["Item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             page={10}
             rowsPerPage={50}
             onChangePage={onChangePage}
@@ -115,6 +115,7 @@ describe("<DataExplorer />", () => {
 const mockDataExplorerProps = () => ({
     columns: [],
     items: [],
+    itemsAvailable: 0,
     contextActions: [],
     searchValue: "",
     page: 0,
index ff51c71c796fece83395d1b99a627dec1eb05d33..09a327268b76073b3bc92f90db91e4ea9ab8e5d9 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: {}
     };
@@ -53,9 +54,9 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
             <Toolbar className={this.props.classes.toolbar}>
                 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                     <div className={this.props.classes.searchBox}>
-                        {this.props.items.length > 0 && <SearchInput
+                        <SearchInput
                             value={this.props.searchValue}
-                            onSearch={this.props.onSearch} />}
+                            onSearch={this.props.onSearch} />
                     </div>
                     <ColumnSelector
                         columns={this.props.columns}
@@ -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 bbfea7acd5c6db1e43d224e68d455e5b4e52c491..06744c3a21bce90b9246ed74c06c984f9020cdf7 100644 (file)
@@ -4,19 +4,23 @@
 
 import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
 
-export interface DataColumn<T> {
+export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
     name: string;
     selected: boolean;
     configurable?: boolean;
     key?: React.Key;
     sortDirection?: SortDirection;
-    filters?: DataTableFilterItem[];
+    filters?: F[];
     render: (item: T) => React.ReactElement<void>;
     renderHeader?: () => React.ReactElement<void> | null;
     width?: string;
 }
 
-export type SortDirection = "asc" | "desc" | "none";
+export enum SortDirection {
+    Asc = "asc",
+    Desc = "desc",
+    None = "none"
+}
 
 export const isColumnConfigurable = <T>(column: DataColumn<T>) => {
     return column.configurable === undefined || column.configurable;
@@ -24,12 +28,12 @@ export const isColumnConfigurable = <T>(column: DataColumn<T>) => {
 
 export const toggleSortDirection = <T>(column: DataColumn<T>): DataColumn<T> => {
     return column.sortDirection
-        ? column.sortDirection === "asc"
-            ? { ...column, sortDirection: "desc" }
-            : { ...column, sortDirection: "asc" }
+        ? column.sortDirection === SortDirection.Asc
+            ? { ...column, sortDirection: SortDirection.Desc }
+            : { ...column, sortDirection: SortDirection.Asc }
         : column;
 };
 
 export const resetSortDirection = <T>(column: DataColumn<T>): DataColumn<T> => {
-    return column.sortDirection ? { ...column, sortDirection: "none" } : column;
+    return column.sortDirection ? { ...column, sortDirection: SortDirection.None } : column;
 };
index b9d112520acc33ec0083d05a6e022161711c08fb..6dbccb5e7d662222119103ff6155b57ffbcb6ae8 100644 (file)
@@ -6,14 +6,19 @@ import * as React from "react";
 import { mount, configure } from "enzyme";
 import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
 import * as Adapter from "enzyme-adapter-react-16";
-import DataTable, { DataColumns } from "./data-table";
+import DataTable, { DataColumns, DataItem } from "./data-table";
 import DataTableFilters from "../data-table-filters/data-table-filters";
+import { SortDirection } from "./data-column";
 
 configure({ adapter: new Adapter() });
 
+export interface MockItem extends DataItem {
+    name: string;
+}
+
 describe("<DataTable />", () => {
     it("shows only selected columns", () => {
-        const columns: DataColumns<string> = [
+        const columns: DataColumns<MockItem> = [
             {
                 name: "Column 1",
                 render: () => <span />,
@@ -32,7 +37,7 @@ describe("<DataTable />", () => {
         ];
         const dataTable = mount(<DataTable
             columns={columns}
-            items={["item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
@@ -41,7 +46,7 @@ describe("<DataTable />", () => {
     });
 
     it("renders column name", () => {
-        const columns: DataColumns<string> = [
+        const columns: DataColumns<MockItem> = [
             {
                 name: "Column 1",
                 render: () => <span />,
@@ -50,7 +55,7 @@ describe("<DataTable />", () => {
         ];
         const dataTable = mount(<DataTable
             columns={columns}
-            items={["item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
@@ -59,7 +64,7 @@ describe("<DataTable />", () => {
     });
 
     it("uses renderHeader instead of name prop", () => {
-        const columns: DataColumns<string> = [
+        const columns: DataColumns<MockItem> = [
             {
                 name: "Column 1",
                 renderHeader: () => <span>Column Header</span>,
@@ -69,7 +74,7 @@ describe("<DataTable />", () => {
         ];
         const dataTable = mount(<DataTable
             columns={columns}
-            items={["item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
@@ -78,7 +83,7 @@ describe("<DataTable />", () => {
     });
 
     it("passes column key prop to corresponding cells", () => {
-        const columns: DataColumns<string> = [
+        const columns: DataColumns<MockItem> = [
             {
                 name: "Column 1",
                 key: "column-1-key",
@@ -88,7 +93,7 @@ describe("<DataTable />", () => {
         ];
         const dataTable = mount(<DataTable
             columns={columns}
-            items={["item 1"]}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
@@ -98,21 +103,21 @@ describe("<DataTable />", () => {
     });
 
     it("renders items", () => {
-        const columns: DataColumns<string> = [
+        const columns: DataColumns<MockItem> = [
             {
                 name: "Column 1",
-                render: (item) => <Typography>{item}</Typography>,
+                render: (item) => <Typography>{item.name}</Typography>,
                 selected: true
             },
             {
                 name: "Column 2",
-                render: (item) => <Button>{item}</Button>,
+                render: (item) => <Button>{item.name}</Button>,
                 selected: true
             }
         ];
-        const dataTable = mount(<DataTable 
-            columns={columns} 
-            items={["item 1"]}
+        const dataTable = mount(<DataTable
+            columns={columns}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
@@ -122,41 +127,41 @@ describe("<DataTable />", () => {
     });
 
     it("passes sorting props to <TableSortLabel />", () => {
-        const columns: DataColumns<string> = [{
+        const columns: DataColumns<MockItem> = [{
             name: "Column 1",
-            sortDirection: "asc",
+            sortDirection: SortDirection.Asc,
             selected: true,
-            render: (item) => <Typography>{item}</Typography>
+            render: (item) => <Typography>{item.name}</Typography>
         }];
         const onSortToggle = jest.fn();
-        const dataTable = mount(<DataTable 
-            columns={columns} 
-            items={["item 1"]} 
+        const dataTable = mount(<DataTable
+            columns={columns}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
-            onSortToggle={onSortToggle}/>);
+            onSortToggle={onSortToggle} />);
         expect(dataTable.find(TableSortLabel).prop("active")).toBeTruthy();
         dataTable.find(TableSortLabel).at(0).simulate("click");
         expect(onSortToggle).toHaveBeenCalledWith(columns[0]);
     });
 
     it("passes filter props to <DataTableFilter />", () => {
-        const columns: DataColumns<string> = [{
+        const columns: DataColumns<MockItem> = [{
             name: "Column 1",
-            sortDirection: "asc",
+            sortDirection: SortDirection.Asc,
             selected: true,
-            filters: [{name: "Filter 1", selected: true}],
-            render: (item) => <Typography>{item}</Typography>
+            filters: [{ name: "Filter 1", selected: true }],
+            render: (item) => <Typography>{item.name}</Typography>
         }];
         const onFiltersChange = jest.fn();
-        const dataTable = mount(<DataTable 
-            columns={columns} 
-            items={["item 1"]} 
+        const dataTable = mount(<DataTable
+            columns={columns}
+            items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={onFiltersChange}
             onRowClick={jest.fn()}
             onRowContextMenu={jest.fn()}
-            onSortToggle={jest.fn()}/>);
+            onSortToggle={jest.fn()} />);
         expect(dataTable.find(DataTableFilters).prop("filters")).toBe(columns[0].filters);
         dataTable.find(DataTableFilters).prop("onChange")([]);
         expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
index e8a5b24e02c2fe22488236f1ae703f36ef482c8d..5372128f316613d41c9aa7758ba8b978dc97c5dc 100644 (file)
@@ -7,8 +7,10 @@ import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, Style
 import { DataColumn, SortDirection } from './data-column';
 import DataTableFilters, { DataTableFilterItem } from "../data-table-filters/data-table-filters";
 
-export type DataColumns<T> = Array<DataColumn<T>>;
-
+export type DataColumns<T, F extends DataTableFilterItem = DataTableFilterItem> = Array<DataColumn<T, F>>;
+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}>
@@ -52,8 +54,8 @@ class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRul
                     </DataTableFilters>
                     : sortDirection
                         ? <TableSortLabel
-                            active={sortDirection !== "none"}
-                            direction={sortDirection !== "none" ? sortDirection : undefined}
+                            active={sortDirection !== SortDirection.None}
+                            direction={sortDirection !== SortDirection.None ? sortDirection : undefined}
                             onClick={() =>
                                 onSortToggle &&
                                 onSortToggle(column)}>
@@ -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 316b1fac59f6271ddc4fb2a0e6844669327f8c39..cf5b4e68ce0d2d36331715461a4879b593cd5dcb 100644 (file)
@@ -2,7 +2,24 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Resource } from "./resource";
+import { Resource as R } from "./resource";
+import { Resource } from "../common/api/common-resource-service";
+import { ResourceKind } from "./kinds";
 
-export interface Collection extends Resource {
+export interface Collection extends R {
+}
+
+export interface CollectionResource extends Resource {
+    kind: ResourceKind.Collection;
+    name: string;
+    description: string;
+    properties: any;
+    portableDataHash: string;
+    manifestText: string;
+    replicationDesired: number;
+    replicationConfirmed: number;
+    replicationConfirmedAt: string;
+    trashAt: string;
+    deleteAt: string;
+    isTrashed: boolean;
 }
diff --git a/src/models/container-request.ts b/src/models/container-request.ts
new file mode 100644 (file)
index 0000000..4a10414
--- /dev/null
@@ -0,0 +1,40 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "../common/api/common-resource-service";
+import { ResourceKind } from "./kinds";
+
+export enum ContainerRequestState {
+    Uncommitted = "Uncommitted",
+    Committed = "Committed",
+    Final = "Final"
+}
+
+export interface ContainerRequestResource extends Resource {
+    kind: ResourceKind.ContainerRequest;
+    name: string;
+    description: string;
+    properties: any;
+    state: ContainerRequestState;
+    requestingContainerUuid: string;
+    containerUuid: string;
+    containerCountMax: number;
+    mounts: any;
+    runtimeConstraints: any;
+    schedulingParameters: any;
+    containerImage: string;
+    environment: any;
+    cwd: string;
+    command: string[];
+    outputPath: string;
+    outputName: string;
+    outputTtl: number;
+    priority: number;
+    expiresAt: string;
+    useExisting: boolean;
+    logUuid: string;
+    outputUuid: string;
+    filters: string;
+
+}
\ No newline at end of file
diff --git a/src/models/group.ts b/src/models/group.ts
new file mode 100644 (file)
index 0000000..dae516b
--- /dev/null
@@ -0,0 +1,18 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "../common/api/common-resource-service";
+import { ResourceKind } from "./kinds";
+
+export interface GroupResource extends Resource {
+    kind: ResourceKind.Group;
+    name: string;
+    groupClass: string;
+    description: string;
+    properties: string;
+    writeableBy: string[];
+    trashAt: string;
+    deleteAt: string;
+    isTrashed: boolean;
+}
\ No newline at end of file
diff --git a/src/models/kinds.ts b/src/models/kinds.ts
new file mode 100644 (file)
index 0000000..15bd622
--- /dev/null
@@ -0,0 +1,12 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum ResourceKind {
+    Collection = "arvados#collection",
+    ContainerRequest = "arvados#containerRequest",
+    Group = "arvados#group",
+    Process = "arvados#containerRequest",
+    Project = "arvados#group",
+    Workflow = "arvados#workflow"
+}
\ No newline at end of file
diff --git a/src/models/process.ts b/src/models/process.ts
new file mode 100644 (file)
index 0000000..1e04cb1
--- /dev/null
@@ -0,0 +1,7 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContainerRequestResource } from "./container-request";
+
+export type ProcessResource = ContainerRequestResource;
index 7d29de872974c62e5c7f9ce6fec6c3b591a1d5e6..beb9810cc0282a8830719887194bb90de259cab8 100644 (file)
@@ -2,7 +2,12 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Resource } from "./resource";
+import { Resource as R } from "./resource";
+import { GroupResource } from "./group";
 
-export interface Project extends Resource {
+export interface Project extends R {
+}
+
+export interface ProjectResource extends GroupResource {
+    groupClass: "project";
 }
diff --git a/src/models/workflow.ts b/src/models/workflow.ts
new file mode 100644 (file)
index 0000000..2054550
--- /dev/null
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "../common/api/common-resource-service";
+import { ResourceKind } from "./kinds";
+
+export interface WorkflowResource extends Resource {
+    kind: ResourceKind.Workflow;
+    name: string;
+    description: string;
+    definition: string;
+}
\ No newline at end of file
index bc9128171254eebe3b9a42c5cfad5ea86ec56856..8412dea1e5b74becf0eb58f8a45b25e1ba2e8b04 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { serverApi } from "../../common/api/server-api";
-import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
+import FilterBuilder from "../../common/api/filter-builder";
 import { ArvadosResource } from "../response";
 import { Collection } from "../../models/collection";
 import { getResourceKind } from "../../models/resource";
@@ -32,9 +32,9 @@ export default class CollectionService {
     public getCollectionList = (parentUuid?: string): Promise<Collection[]> => {
         if (parentUuid) {
             const fb = new FilterBuilder();
-            fb.addLike(FilterField.OWNER_UUID, parentUuid);
+            fb.addLike("ownerUuid", parentUuid);
             return serverApi.get<CollectionsResponse>('/collections', { params: {
-                filters: fb.get()
+                filters: fb.serialize()
             }}).then(resp => {
                 const collections = resp.data.items.map(g => ({
                     name: g.name,
index f230c70fa0bdd94ca3867f283fd2e7babe9303e2..ed61297dabe1fb82f78a50a08d6df05917f070a4 100644 (file)
@@ -7,17 +7,10 @@ import CommonResourceService, { Resource, ListResults } from "../../common/api/c
 import FilterBuilder from "../../common/api/filter-builder";
 import OrderBuilder from "../../common/api/order-builder";
 import { AxiosInstance } from "axios";
-
-interface GroupResource extends Resource {
-    name: string;
-    groupClass: string;
-    description: string;
-    properties: string;
-    writeableBy: string[];
-    trashAt: string;
-    deleteAt: string;
-    isTrashed: boolean;
-}
+import { GroupResource } from "../../models/group";
+import { CollectionResource } from "../../models/collection";
+import { ProjectResource } from "../../models/project";
+import { ProcessResource } from "../../models/process";
 
 interface ContensArguments {
     limit?: number;
@@ -27,18 +20,23 @@ interface ContensArguments {
     recursive?: boolean;
 }
 
+export type GroupContentsResource =
+    CollectionResource |
+    ProjectResource |
+    ProcessResource;
+
 export default class GroupsService extends CommonResourceService<GroupResource> {
 
     constructor(serverApi: AxiosInstance) {
         super(serverApi, "groups");
     }
 
-    contents (uuid: string, args: ContensArguments = {}): Promise<ListResults<Resource>> {
+    contents(uuid: string, args: ContensArguments = {}): Promise<ListResults<GroupContentsResource>> {
         const { filters, order, ...other } = args;
         const params = {
             ...other,
-            filters: filters ? filters.get() : undefined,
-            order: order ? order.get() : undefined
+            filters: filters ? filters.serialize() : undefined,
+            order: order ? order.getOrder() : undefined
         };
         return this.serverApi
             .get(this.resourceType + `${uuid}/contents/`, {
@@ -46,4 +44,10 @@ export default class GroupsService extends CommonResourceService<GroupResource>
             })
             .then(CommonResourceService.mapResponseKeys);
     }
+}
+
+export enum GroupContentsResourcePrefix {
+    Collection = "collections",
+    Project = "groups",
+    Process = "container_requests"
 }
\ No newline at end of file
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..01126170720c7593d9110eb74caffa3052558fd3 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 })),
@@ -44,6 +49,9 @@ const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorer
         SET_ROWS_PER_PAGE: ({ id, rowsPerPage }) =>
             update(state, id, explorer => ({ ...explorer, rowsPerPage })),
 
+        SET_SEARCH_VALUE: ({ id, searchValue }) =>
+            update(state, id, explorer => ({ ...explorer, searchValue })),
+
         TOGGLE_SORT: ({ id, columnName }) =>
             update(state, id, mapColumns(toggleSort(columnName))),
 
index daeb26fd2a9ee0d9e0b4ab7f8dd5441e93b77412..5fb6b7296997a578162f8d6e28f1fadd79354a1b 100644 (file)
@@ -6,13 +6,11 @@ import { Dispatch } from "redux";
 import projectActions, { getProjectList } from "../project/project-action";
 import { push } from "react-router-redux";
 import { TreeItemStatus } from "../../components/tree/tree";
-import { getCollectionList } from "../collection/collection-action";
 import { findTreeItem } from "../project/project-reducer";
 import { Resource, ResourceKind } from "../../models/resource";
 import sidePanelActions from "../side-panel/side-panel-action";
 import dataExplorerActions from "../data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
-import { projectPanelItems } from "../../views/project-panel/project-panel-selectors";
 import { RootState } from "../store";
 import { sidePanelData } from "../side-panel/side-panel-reducer";
 
@@ -61,17 +59,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}));
                 }));
 
         }
diff --git a/src/store/project-panel/project-panel-middleware.ts b/src/store/project-panel/project-panel-middleware.ts
new file mode 100644 (file)
index 0000000..e72b6c1
--- /dev/null
@@ -0,0 +1,128 @@
+// 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, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel";
+import { groupsService } from "../../services/services";
+import { RootState } from "../../store/store";
+import { getDataExplorer, DataExplorerState } from "../../store/data-explorer/data-explorer-reducer";
+import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item";
+import FilterBuilder from "../../common/api/filter-builder";
+import { DataColumns } from "../../components/data-table/data-table";
+import { ProcessResource } from "../../models/process";
+import { CollectionResource } from "../../models/collection";
+import OrderBuilder from "../../common/api/order-builder";
+import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
+import { SortDirection } from "../../components/data-table/data-column";
+
+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 }));
+            }),
+            SET_FILTERS: handleProjectPanelAction(() => {
+                store.dispatch(actions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
+                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+            }),
+            TOGGLE_SORT: handleProjectPanelAction(() => {
+                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+            }),
+            SET_SEARCH_VALUE: handleProjectPanelAction(() => {
+                store.dispatch(actions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
+                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);
+                const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
+                const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.Type);
+                const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.Status);
+                const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
+                const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.Asc ? SortDirection.Asc : SortDirection.Desc;
+                if (typeFilters.length > 0) {
+                    groupsService
+                        .contents(state.projects.currentItemId, {
+                            limit: dataExplorer.rowsPerPage,
+                            offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                            order: sortColumn
+                                ? sortColumn.name === ProjectPanelColumnNames.Name
+                                    ? getOrder("name", sortDirection)
+                                    : getOrder("createdAt", sortDirection)
+                                : OrderBuilder.create(),
+                            filters: FilterBuilder
+                                .create()
+                                .concat(FilterBuilder
+                                    .create()
+                                    .addIsA("uuid", typeFilters.map(f => f.type)))
+                                .concat(FilterBuilder
+                                    .create<ProcessResource>(GroupContentsResourcePrefix.Process)
+                                    .addIn("state", statusFilters.map(f => f.type)))
+                                .concat(getSearchFilter(dataExplorer.searchValue))
+                        })
+                        .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
+                            }));
+                        });
+                } else {
+                    store.dispatch(actions.SET_ITEMS({
+                        id: PROJECT_PANEL_ID,
+                        items: [],
+                        itemsAvailable: 0,
+                        page: 0,
+                        rowsPerPage: dataExplorer.rowsPerPage
+                    }));
+                }
+            }),
+            default: () => next(action)
+        });
+    };
+};
+
+const getColumnFilters = (columns: DataColumns<ProjectPanelItem, ProjectPanelFilter>, columnName: string) => {
+    const column = columns.find(c => c.name === columnName);
+    return column && column.filters ? column.filters.filter(f => f.selected) : [];
+};
+
+const getOrder = (attribute: "name" | "createdAt", direction: SortDirection) =>
+    [
+        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Collection),
+        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Process),
+        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Project)
+    ].reduce((acc, b) =>
+        acc.concat(direction === SortDirection.Asc
+            ? b.addAsc(attribute)
+            : b.addDesc(attribute)), OrderBuilder.create());
+
+const getSearchFilter = (searchValue: string) =>
+    searchValue
+        ? [
+            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Collection),
+            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Process),
+            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Project)]
+            .reduce((acc, b) =>
+                acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
+        : FilterBuilder.create();
+
+
index 68c5d8238c74894857e08f7d34f8e0df90bcfb9b..00c2ad75e422b074e53ab7cba5f90742c8a781be 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 '../store/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..9a9f845f57e975cb720a84c09124c6197f88de1d 100644 (file)
@@ -2,14 +2,14 @@
 //
 // 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 { DataItem } from "../../components/data-table/data-table";
+import { ResourceKind } from "../../models/kinds";
+import { GroupContentsResource } from "../../services/groups-service/groups-service";
 
-export interface ProjectPanelItem {
+export interface ProjectPanelItem extends DataItem {
     uuid: string;
     name: string;
-    kind: ResourceKind;
+    kind: string;
     url: string;
     owner: string;
     lastModified: string;
@@ -17,13 +17,17 @@ export interface ProjectPanelItem {
     status?: string;
 }
 
-function resourceToDataItem(r: Resource, kind?: ResourceKind) {
+
+export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
     return {
+        key: r.uuid,
         uuid: r.uuid,
         name: r.name,
-        kind: kind ? kind : getResourceKind(r.kind),
+        kind: r.kind,
+        url: "",
         owner: r.ownerUuid,
-        lastModified: r.modifiedAt
+        lastModified: r.modifiedAt,
+        status:  r.kind === ResourceKind.Process ? r.state : undefined
     };
 }
 
diff --git a/src/views/project-panel/project-panel-selectors.ts b/src/views/project-panel/project-panel-selectors.ts
deleted file mode 100644 (file)
index ee039a8..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { TreeItem } from "../../components/tree/tree";
-import { Project } from "../../models/project";
-import { findTreeItem } from "../../store/project/project-reducer";
-import { ResourceKind } from "../../models/resource";
-import { Collection } from "../../models/collection";
-import { getResourceUrl } from "../../store/navigation/navigation-action";
-import { ProjectPanelItem } from "./project-panel-item";
-
-export const projectPanelItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): ProjectPanelItem[] => {
-    const dataItems: ProjectPanelItem[] = [];
-
-    const treeItem = findTreeItem(projects, treeItemId);
-    if (treeItem) {
-        if (treeItem.items) {
-            treeItem.items.forEach(p => {
-                const item = {
-                    name: p.data.name,
-                    kind: ResourceKind.PROJECT,
-                    url: getResourceUrl(treeItem.data),
-                    owner: p.data.ownerUuid,
-                    uuid: p.data.uuid,
-                    lastModified: p.data.modifiedAt
-                } as ProjectPanelItem;
-
-                dataItems.push(item);
-            });
-        }
-    }
-
-    collections.forEach(c => {
-        const item = {
-            name: c.name,
-            kind: ResourceKind.COLLECTION,
-            url: getResourceUrl(c),
-            owner: c.ownerUuid,
-            uuid: c.uuid,
-            lastModified: c.modifiedAt
-        } as ProjectPanelItem;
-
-        dataItems.push(item);
-    });
-
-    return dataItems;
-};
-
index 6bfa61e0322de57f6040207448a5c8855759ad19..c1d66603405d9de7774c0aaca8056fd3f8e855bf 100644 (file)
@@ -7,18 +7,22 @@ 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";
 import { DataColumns } from '../../components/data-table/data-table';
-import { ResourceKind } from "../../models/resource";
 import { RouteComponentProps } from 'react-router';
 import { RootState } from '../../store/store';
+import { ResourceKind } from '../../models/kinds';
+import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
+import { ContainerRequestState } from '../../models/container-request';
+import { SortDirection } from '../../components/data-table/data-column';
 
 export const PROJECT_PANEL_ID = "projectPanel";
 
+export interface ProjectPanelFilter extends DataTableFilterItem {
+    type: ResourceKind | ContainerRequestState;
+}
+
 type ProjectPanelProps = {
     currentItemId: string,
     onItemClick: (item: ProjectPanelItem) => void,
@@ -44,55 +48,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";
@@ -126,10 +96,12 @@ const renderName = (item: ProjectPanelItem) =>
 
 const renderIcon = (item: ProjectPanelItem) => {
     switch (item.kind) {
-        case ResourceKind.PROJECT:
+        case ResourceKind.Project:
             return <i className="fas fa-folder fa-lg" />;
-        case ResourceKind.COLLECTION:
-            return <i className="fas fa-th fa-lg" />;
+        case ResourceKind.Collection:
+            return <i className="fas fa-archive fa-lg" />;
+        case ResourceKind.Process:
+            return <i className="fas fa-cogs fa-lg" />;
         default:
             return <i />;
     }
@@ -150,53 +122,98 @@ const renderOwner = (owner: string) =>
         {owner}
     </Typography>;
 
-const renderType = (type: string) =>
-    <Typography noWrap>
-        {type}
+
+
+const typeToLabel = (type: string) => {
+    switch (type) {
+        case ResourceKind.Collection:
+            return "Data collection";
+        case ResourceKind.Project:
+            return "Project";
+        case ResourceKind.Process:
+            return "Process";
+        default:
+            return "Unknown";
+    }
+};
+
+const renderType = (type: string) => {
+    return <Typography noWrap>
+        {typeToLabel(type)}
     </Typography>;
+};
 
 const renderStatus = (item: ProjectPanelItem) =>
     <Typography noWrap align="center">
         {item.status || "-"}
     </Typography>;
 
-const columns: DataColumns<ProjectPanelItem> = [{
-    name: "Name",
+export enum ProjectPanelColumnNames {
+    Name = "Name",
+    Status = "Status",
+    Type = "Type",
+    Owner = "Owner",
+    FileSize = "File size",
+    LastModified = "Last modified"
+
+}
+
+export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [{
+    name: ProjectPanelColumnNames.Name,
     selected: true,
-    sortDirection: "desc",
+    sortDirection: SortDirection.Asc,
     render: renderName,
     width: "450px"
 }, {
     name: "Status",
     selected: true,
+    filters: [{
+        name: ContainerRequestState.Committed,
+        selected: true,
+        type: ContainerRequestState.Committed
+    }, {
+        name: ContainerRequestState.Final,
+        selected: true,
+        type: ContainerRequestState.Final
+    }, {
+        name: ContainerRequestState.Uncommitted,
+        selected: true,
+        type: ContainerRequestState.Uncommitted
+    }],
     render: renderStatus,
     width: "75px"
 }, {
-    name: "Type",
+    name: ProjectPanelColumnNames.Type,
     selected: true,
     filters: [{
-        name: "Collection",
-        selected: true
+        name: typeToLabel(ResourceKind.Collection),
+        selected: true,
+        type: ResourceKind.Collection
+    }, {
+        name: typeToLabel(ResourceKind.Process),
+        selected: true,
+        type: ResourceKind.Process
     }, {
-        name: "Project",
-        selected: true
+        name: typeToLabel(ResourceKind.Project),
+        selected: true,
+        type: ResourceKind.Project
     }],
     render: item => renderType(item.kind),
     width: "125px"
 }, {
-    name: "Owner",
+    name: ProjectPanelColumnNames.Owner,
     selected: true,
     render: item => renderOwner(item.owner),
     width: "200px"
 }, {
-    name: "File size",
+    name: ProjectPanelColumnNames.FileSize,
     selected: true,
     render: item => renderFileSize(item.fileSize),
     width: "50px"
 }, {
-    name: "Last modified",
+    name: ProjectPanelColumnNames.LastModified,
     selected: true,
-    sortDirection: "none",
+    sortDirection: SortDirection.None,
     render: item => renderDate(item.lastModified),
     width: "150px"
 }];