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, {
--- /dev/null
+// 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"]]]`);
+ });
+
+
+
+
+});
//
// 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;
+ }
+
}
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 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();
}
}
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() });
{...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")({
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");
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");
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}
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);
});
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}
const mockDataExplorerProps = () => ({
columns: [],
items: [],
+ itemsAvailable: 0,
contextActions: [],
searchValue: "",
page: 0,
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: {}
};
<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}
{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 { 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;
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;
};
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 />,
];
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()}
});
it("renders column name", () => {
- const columns: DataColumns<string> = [
+ const columns: DataColumns<MockItem> = [
{
name: "Column 1",
render: () => <span />,
];
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()}
});
it("uses renderHeader instead of name prop", () => {
- const columns: DataColumns<string> = [
+ const columns: DataColumns<MockItem> = [
{
name: "Column 1",
renderHeader: () => <span>Column Header</span>,
];
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()}
});
it("passes column key prop to corresponding cells", () => {
- const columns: DataColumns<string> = [
+ const columns: DataColumns<MockItem> = [
{
name: "Column 1",
key: "column-1-key",
];
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()}
});
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()}
});
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]);
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>;
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}>
</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)}>
}
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) => (
//
// 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;
}
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContainerRequestResource } from "./container-request";
+
+export type ProcessResource = ContainerRequestResource;
//
// 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";
}
--- /dev/null
+// 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
// 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";
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,
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;
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/`, {
})
.then(CommonResourceService.mapResponseKeys);
}
+}
+
+export enum GroupContentsResourcePrefix {
+ Collection = "collections",
+ Project = "groups",
+ Process = "container_requests"
}
\ No newline at end of file
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 })),
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))),
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";
: 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}));
}));
}
--- /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, 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();
+
+
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' &&
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 { 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;
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
};
}
+++ /dev/null
-// 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;
-};
-
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,
<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";
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 />;
}
{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"
}];