const params = {
...other,
filters: filters ? filters.get() : undefined,
- order: order ? order.get() : undefined
+ order: order ? order.getOrder() : undefined
};
return this.serverApi
.get(this.resourceType, {
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"]);
});
});
//
// 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();
}
}
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';
interface DataExplorerProps<T> {
items: T[];
+ itemsAvailable: number;
columns: DataColumns<T>;
contextActions: ContextMenuActionGroup[];
searchValue: string;
};
}
-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: {}
};
{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}
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>;
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}>
}
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) => (
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/`, {
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>;
interface DataExplorer {
columns: DataColumns<any>;
items: any[];
+ itemsAvailable: number;
page: number;
rowsPerPage: number;
rowsPerPageOptions?: number[];
export const initialDataExplorer: DataExplorer = {
columns: [],
items: [],
+ itemsAvailable: 0,
page: 0,
rowsPerPage: 10,
rowsPerPageOptions: [5, 10, 25, 50],
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 })),
: 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}));
}));
}
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' &&
export default function configureStore(history: History) {
const middlewares: Middleware[] = [
routerMiddleware(history),
- thunkMiddleware
+ thunkMiddleware,
+ projectPanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
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);
//
// 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;
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
};
--- /dev/null
+// 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)
+ });
+ };
+};
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";
<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";
{item.status || "-"}
</Typography>;
-const columns: DataColumns<ProjectPanelItem> = [{
+export const columns: DataColumns<ProjectPanelItem> = [{
name: "Name",
selected: true,
sortDirection: "desc",