export const formatDate = (isoDate: string) => {
const date = new Date(isoDate);
- return date.toLocaleString();
+ const text = date.toLocaleString();
+ return text === 'Invalid Date' ? "" : text;
};
export const formatFileSize = (size?: number) => {
columns={[{ name: "Column 1", render: jest.fn(), selected: true }]} />);
expect(dataExplorer.find(ContextMenu).prop("actions")).toEqual([]);
dataExplorer.find(DataTable).prop("onRowContextMenu")({
- preventDefault: jest.fn()
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn()
}, "Item 1");
dataExplorer.find(ContextMenu).prop("onActionClick")({ name: "Action 1", icon: "" });
expect(onContextAction).toHaveBeenCalledWith({ name: "Action 1", icon: "" }, "Item 1");
items={[]}
/>);
expect(dataExplorer.find(SearchInput)).toHaveLength(0);
- expect(dataExplorer.find(ColumnSelector)).toHaveLength(0);
expect(dataExplorer.find(TablePagination)).toHaveLength(0);
});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, Table, IconButton } from '@material-ui/core';
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, IconButton } from '@material-ui/core';
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";
onActionClick={this.callAction}
onClose={this.closeContextMenu} />
<Toolbar className={this.props.classes.toolbar}>
- {this.props.items.length > 0 &&
- <Grid container justify="space-between" wrap="nowrap" alignItems="center">
- <div className={this.props.classes.searchBox}>
- <SearchInput
- value={this.props.searchValue}
- onSearch={this.props.onSearch} />
- </div>
- <ColumnSelector
- columns={this.props.columns}
- onColumnToggle={this.props.onColumnToggle} />
- </Grid>}
-
+ <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+ <div className={this.props.classes.searchBox}>
+ {this.props.items.length > 0 && <SearchInput
+ value={this.props.searchValue}
+ onSearch={this.props.onSearch} />}
+ </div>
+ <ColumnSelector
+ columns={this.props.columns}
+ onColumnToggle={this.props.onColumnToggle} />
+ </Grid>
</Toolbar>
<DataTable
columns={[
openContextMenu = (event: React.MouseEvent<HTMLElement>, item: T) => {
event.preventDefault();
+ event.stopPropagation();
this.setState({
contextMenu: {
anchorEl: mockAnchorFromMouseEvent(event),
selected: true,
key: "context-actions",
renderHeader: () => null,
- render: this.renderContextMenuTrigger
+ render: this.renderContextMenuTrigger,
+ width: "auto"
};
}
filters?: DataTableFilterItem[];
render: (item: T) => React.ReactElement<void>;
renderHeader?: () => React.ReactElement<void> | null;
+ width?: string;
}
export type SortDirection = "asc" | "desc" | "none";
expect(dataTable.find(TableBody).find(TableCell).key()).toBe("column-1-key");
});
- it("shows information that items array is empty", () => {
- const columns: DataColumns<string> = [
- {
- name: "Column 1",
- render: () => <span />,
- selected: true
- }
- ];
- const dataTable = mount(<DataTable
- columns={columns}
- items={[]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowContextMenu={jest.fn()}
- onSortToggle={jest.fn()} />);
- expect(dataTable.find(Typography).text()).toBe("No items");
- });
-
it("renders items", () => {
const columns: DataColumns<string> = [
{
render() {
const { items, classes } = this.props;
return <div className={classes.tableContainer}>
- {items.length > 0 ?
- <Table>
- <TableHead>
- <TableRow>
- {this.mapVisibleColumns(this.renderHeadCell)}
- </TableRow>
- </TableHead>
- <TableBody className={classes.tableBody}>
- {items.map(this.renderBodyRow)}
- </TableBody>
- </Table> : <Typography
- className={classes.noItemsInfo}
- variant="body2"
- gutterBottom>
- No items
- </Typography>}
+ <Table>
+ <TableHead>
+ <TableRow>
+ {this.mapVisibleColumns(this.renderHeadCell)}
+ </TableRow>
+ </TableHead>
+ <TableBody className={classes.tableBody}>
+ {items.map(this.renderBodyRow)}
+ </TableBody>
+ </Table>
</div>;
}
renderHeadCell = (column: DataColumn<T>, index: number) => {
const { name, key, renderHeader, filters, sortDirection } = column;
const { onSortToggle, onFiltersChange } = this.props;
- return <TableCell key={key || index}>
+ return <TableCell key={key || index} style={{width: column.width, minWidth: column.width}}>
{renderHeader ?
renderHeader() :
filters
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
tableContainer: {
- overflowX: 'auto'
+ overflowX: 'auto',
+ overflowY: 'hidden'
},
tableBody: {
background: theme.palette.background.paper
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
export interface Resource {
name: string;
createdAt: string;
uuid: string;
ownerUuid: string;
href: string;
- kind: string;
+ kind: ResourceKind;
+}
+
+export enum ResourceKind {
+ PROJECT = "project",
+ COLLECTION = "collection",
+ PIPELINE = "pipeline",
+ LEVEL_UP = "",
+ UNKNOWN = "unknown"
+}
+
+export function getResourceKind(itemKind: string) {
+ switch (itemKind) {
+ case "arvados#project": return ResourceKind.PROJECT;
+ case "arvados#collection": return ResourceKind.COLLECTION;
+ case "arvados#pipeline": return ResourceKind.PIPELINE;
+ default:
+ return ResourceKind.UNKNOWN;
+ }
}
return localStorage.getItem(API_TOKEN_KEY) || undefined;
}
+ public getUuid() {
+ return localStorage.getItem(USER_UUID_KEY) || undefined;
+ }
+
public getOwnerUuid() {
return localStorage.getItem(USER_OWNER_UUID_KEY) || undefined;
}
import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
import { ArvadosResource } from "../response";
import { Collection } from "../../models/collection";
+import { getResourceKind } from "../../models/resource";
interface CollectionResource extends ArvadosResource {
name: string;
href: g.href,
uuid: g.uuid,
ownerUuid: g.owner_uuid,
- kind: g.kind
+ kind: getResourceKind(g.kind)
} as Collection));
return collections;
});
import { Project } from "../../models/project";
import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
import { ArvadosResource } from "../response";
+import { getResourceKind } from "../../models/resource";
interface GroupResource extends ArvadosResource {
name: string;
href: g.href,
uuid: g.uuid,
ownerUuid: g.owner_uuid,
- kind: g.kind
+ kind: getResourceKind(g.kind)
} as Project));
return projects;
});
import collectionsReducer from "./collection-reducer";
import actions from "./collection-action";
+import { ResourceKind } from "../../models/resource";
describe('collection-reducer', () => {
it('should add new collection to the list', () => {
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: ""
+ kind: ResourceKind.COLLECTION
};
const state = collectionsReducer(initialState, actions.CREATE_COLLECTION(collection));
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: ""
+ kind: ResourceKind.COLLECTION
};
const collections = [collection, collection];
CREATE_COLLECTION: collection => [...state, collection],
REMOVE_COLLECTION: () => state,
COLLECTIONS_REQUEST: () => {
- return state;
+ return [];
},
COLLECTIONS_SUCCESS: ({ collections }) => {
return collections;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+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";
+
+export const getResourceUrl = (resource: Resource): string => {
+ switch (resource.kind) {
+ case ResourceKind.LEVEL_UP: return `/projects/${resource.ownerUuid}`;
+ case ResourceKind.PROJECT: return `/projects/${resource.uuid}`;
+ case ResourceKind.COLLECTION: return `/collections/${resource.uuid}`;
+ default:
+ return "#";
+ }
+};
+
+export enum ItemMode {
+ BOTH,
+ OPEN,
+ ACTIVE
+}
+
+export const setProjectItem = (itemId: string, itemKind = ResourceKind.PROJECT, itemMode = ItemMode.OPEN) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { projects } = getState();
+
+ let treeItem = findTreeItem(projects.items, itemId);
+ if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
+ treeItem = findTreeItem(projects.items, treeItem.data.ownerUuid);
+ }
+
+ if (treeItem) {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+
+ if (treeItem.status === TreeItemStatus.Loaded) {
+ dispatch<any>(openProjectItem(treeItem.data, itemKind, itemMode));
+ } else {
+ dispatch<any>(getProjectList(itemId))
+ .then(() => dispatch<any>(openProjectItem(treeItem!.data, itemKind, itemMode)));
+ }
+ if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
+ dispatch<any>(getCollectionList(itemId));
+ }
+ }
+ };
+
+const openProjectItem = (resource: Resource, itemKind: ResourceKind, itemMode: ItemMode) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+
+ const { collections, projects } = getState();
+
+ if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
+ }
+
+ if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
+ dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
+ }
+
+ dispatch(push(getResourceUrl({ ...resource, kind: itemKind })));
+ dispatch(dataExplorerActions.SET_ITEMS({
+ id: PROJECT_PANEL_ID,
+ items: projectPanelItems(
+ projects.items,
+ resource.uuid,
+ collections
+ )
+ }));
+ };
import projectsReducer, { getTreePath } from "./project-reducer";
import actions from "./project-action";
import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { ResourceKind } from "../../models/resource";
describe('project-reducer', () => {
it('should add new project to the list', () => {
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: ""
+ kind: ResourceKind.PROJECT
};
const state = projectsReducer(initialState, actions.CREATE_PROJECT(project));
- expect(state).toEqual([project]);
+ expect(state.items[0].data).toEqual(project);
});
it('should load projects', () => {
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: ""
+ kind: ResourceKind.PROJECT
};
const projects = [project, project];
const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
- expect(state).toEqual([{
- active: false,
- open: false,
- id: "test123",
- items: [],
- data: project,
- status: 0
- }, {
- active: false,
- open: false,
- id: "test123",
- items: [],
- data: project,
- status: 0
- }
- ]);
+ expect(state).toEqual({
+ items: [{
+ active: false,
+ open: false,
+ id: "test123",
+ items: [],
+ data: project,
+ status: 0
+ }, {
+ active: false,
+ open: false,
+ id: "test123",
+ items: [],
+ data: project,
+ status: 0
+ }
+ ],
+ currentItemId: ""
+ });
});
it('should remove activity on projects list', () => {
- const initialState = [
- {
+ const initialState = {
+ items: [{
data: {
name: 'test',
href: 'href',
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "1",
open: true,
active: true,
status: 1
- }
- ];
- const project = [
- {
+ }],
+ currentItemId: "1"
+ };
+ const project = {
+ items: [{
data: {
name: 'test',
href: 'href',
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "1",
open: true,
active: false,
status: 1
- }
- ];
+ }],
+ currentItemId: ""
+ };
- const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState[0].id));
+ const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
expect(state).toEqual(project);
});
it('should toggle project tree item activity', () => {
- const initialState = [
- {
+ const initialState = {
+ items: [{
data: {
name: 'test',
href: 'href',
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "1",
open: true,
active: false,
status: 1
- }
- ];
- const project = [
- {
+ }],
+ currentItemId: "1"
+ };
+ const project = {
+ items: [{
data: {
name: 'test',
href: 'href',
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "1",
open: true,
active: true,
status: 1
- }
- ];
+ }],
+ currentItemId: "1"
+ };
- const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState[0].id));
+ const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
expect(state).toEqual(project);
});
it('should close project tree item ', () => {
- const initialState = [
- {
+ const initialState = {
+ items: [{
data: {
name: 'test',
href: 'href',
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "1",
open: true,
active: false,
status: 1,
toggled: false,
- }
- ];
- const project = [
- {
+ }],
+ currentItemId: "1"
+ };
+ const project = {
+ items: [{
data: {
name: 'test',
href: 'href',
modifiedAt: '2018-01-01',
ownerUuid: 'owner-test123',
uuid: 'test123',
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "1",
open: false,
active: false,
status: 1,
toggled: true
- }
- ];
+ }],
+ currentItemId: "1"
+ };
- const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState[0].id));
+ const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
expect(state).toEqual(project);
});
});
describe("findTreeBranch", () => {
-
const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
id,
items,
import actions, { ProjectAction } from "./project-action";
import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
-export type ProjectState = Array<TreeItem<Project>>;
+export type ProjectState = {
+ items: Array<TreeItem<Project>>,
+ currentItemId: string
+};
export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
let item;
return item;
}
+export function getActiveTreeItem<T>(tree: Array<TreeItem<T>>): TreeItem<T> | undefined {
+ let item;
+ for (const t of tree) {
+ item = t.active
+ ? t
+ : getActiveTreeItem(t.items ? t.items : []);
+ if (item) {
+ break;
+ }
+ }
+ return item;
+}
+
export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<TreeItem<T>> {
- for(const item of tree){
+ for (const item of tree){
if(item.id === itemId){
return [item];
} else {
treeItem.status = TreeItemStatus.Loaded;
}
}
- const items = projects.map((p, idx) => ({
+ const items = projects.map(p => ({
id: p.uuid,
open: false,
active: false,
return items;
}
-const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
+const projectsReducer = (state: ProjectState = { items: [], currentItemId: "" }, action: ProjectAction) => {
return actions.match(action, {
- CREATE_PROJECT: project => [...state, project],
+ CREATE_PROJECT: project => ({
+ ...state,
+ items: state.items.concat({
+ id: project.uuid,
+ open: false,
+ active: false,
+ status: TreeItemStatus.Loaded,
+ toggled: false,
+ items: [],
+ data: project
+ })
+ }),
REMOVE_PROJECT: () => state,
PROJECTS_REQUEST: itemId => {
- const tree = _.cloneDeep(state);
- const item = findTreeItem(tree, itemId);
+ const items = _.cloneDeep(state.items);
+ const item = findTreeItem(items, itemId);
if (item) {
item.status = TreeItemStatus.Pending;
+ state.items = items;
}
- return tree;
+ return state;
},
PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
- return updateProjectTree(state, projects, parentItemId);
+ return {
+ ...state,
+ items: updateProjectTree(state.items, projects, parentItemId)
+ };
},
TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => {
- const tree = _.cloneDeep(state);
- const item = findTreeItem(tree, itemId);
+ const items = _.cloneDeep(state.items);
+ const item = findTreeItem(items, itemId);
if (item) {
item.toggled = true;
item.open = !item.open;
}
- return tree;
+ return {
+ items,
+ currentItemId: itemId
+ };
},
TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => {
- const tree = _.cloneDeep(state);
- resetTreeActivity(tree);
- const item = findTreeItem(tree, itemId);
+ const items = _.cloneDeep(state.items);
+ resetTreeActivity(items);
+ const item = findTreeItem(items, itemId);
if (item) {
item.active = true;
}
- return tree;
+ return {
+ items,
+ currentItemId: itemId
+ };
},
RESET_PROJECT_TREE_ACTIVITY: () => {
- const tree = _.cloneDeep(state);
- resetTreeActivity(tree);
- return tree;
+ const items = _.cloneDeep(state.items);
+ resetTreeActivity(items);
+ return {
+ items,
+ currentItemId: ""
+ };
},
default: () => state
});
import projectsReducer, { ProjectState } from "./project/project-reducer";
import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
import authReducer, { AuthState } from "./auth/auth-reducer";
-import collectionsReducer from "./collection/collection-reducer";
import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
+import collectionsReducer, { CollectionState } from "./collection/collection-reducer";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
export interface RootState {
auth: AuthState;
projects: ProjectState;
+ collections: CollectionState;
router: RouterState;
dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
+++ /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";
-
-export interface ProjectExplorerItem {
- uuid: string;
- name: string;
- type: string;
- owner: string;
- lastModified: string;
- fileSize?: number;
- status?: string;
-}
-
-export const mapProjectTreeItem = (item: TreeItem<Project>): ProjectExplorerItem => ({
- name: item.data.name,
- type: item.data.kind,
- owner: item.data.ownerUuid,
- lastModified: item.data.modifiedAt,
- uuid: item.data.uuid
-});
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { mount } from 'enzyme';
import * as Enzyme from 'enzyme';
+import { mount } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import { Collapse } from '@material-ui/core';
import ProjectTree from './project-tree';
import { TreeItem } from '../../components/tree/tree';
import { Project } from '../../models/project';
+import { ResourceKind } from "../../models/resource";
+
Enzyme.configure({ adapter: new Adapter() });
describe("ProjectTree component", () => {
uuid: "uuid",
ownerUuid: "ownerUuid",
href: "href",
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "3",
open: true,
uuid: "uuid",
ownerUuid: "ownerUuid",
href: "href",
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "3",
open: false,
uuid: "uuid",
ownerUuid: "ownerUuid",
href: "href",
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "3",
open: false,
uuid: "uuid",
ownerUuid: "ownerUuid",
href: "href",
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "3",
open: true,
uuid: "uuid",
ownerUuid: "ownerUuid",
href: "href",
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "3",
open: true,
uuid: "uuid",
ownerUuid: "ownerUuid",
href: "href",
- kind: 'example'
+ kind: ResourceKind.PROJECT
},
id: "3",
open: false,
--- /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 { getResourceKind, Resource, ResourceKind } from "../../models/resource";
+
+export interface ProjectPanelItem {
+ uuid: string;
+ name: string;
+ kind: ResourceKind;
+ url: string;
+ owner: string;
+ lastModified: string;
+ fileSize?: number;
+ status?: string;
+}
+
+function resourceToDataItem(r: Resource, kind?: ResourceKind) {
+ return {
+ uuid: r.uuid,
+ name: r.name,
+ kind: kind ? kind : getResourceKind(r.kind),
+ owner: r.ownerUuid,
+ lastModified: r.modifiedAt
+ };
+}
+
--- /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) {
+ dataItems.push({
+ name: "..",
+ url: getResourceUrl(treeItem.data),
+ kind: ResourceKind.LEVEL_UP,
+ owner: "",
+ uuid: treeItem.data.uuid,
+ lastModified: ""
+ });
+
+ 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;
+};
+
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ProjectExplorerItem } from './project-explorer-item';
+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 '../data-explorer/data-explorer';
-import { DataColumn } from '../../components/data-table/data-column';
+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 { setProjectItem } from "../../store/navigation/navigation-action";
import { DataColumns } from '../../components/data-table/data-table';
+import { ResourceKind } from "../../models/resource";
-export const PROJECT_EXPLORER_ID = "projectExplorer";
-class ProjectExplorer extends React.Component<DispatchProp & WithStyles<CssRules>> {
+export const PROJECT_PANEL_ID = "projectPanel";
+class ProjectPanel extends React.Component<DispatchProp & WithStyles<CssRules>> {
render() {
return <div>
<div className={this.props.classes.toolbar}>
</Button>
</div>
<DataExplorer
- id={PROJECT_EXPLORER_ID}
+ id={PROJECT_PANEL_ID}
contextActions={contextMenuActions}
onColumnToggle={this.toggleColumn}
onFiltersChange={this.changeFilters}
- onRowClick={console.log}
+ onRowClick={this.openProject}
onSortToggle={this.toggleSort}
onSearch={this.search}
onContextAction={this.executeAction}
}
componentDidMount() {
- this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_EXPLORER_ID, columns }));
+ this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
}
- toggleColumn = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
- this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name }));
+ toggleColumn = (toggledColumn: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_PANEL_ID, columnName: toggledColumn.name }));
}
- toggleSort = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
- this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name }));
+ toggleSort = (column: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_PANEL_ID, columnName: column.name }));
}
- changeFilters = (filters: DataTableFilterItem[], updatedColumn: DataColumn<ProjectExplorerItem>) => {
- this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_EXPLORER_ID, columnName: updatedColumn.name, filters }));
+ changeFilters = (filters: DataTableFilterItem[], column: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_PANEL_ID, columnName: column.name, filters }));
}
- executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
+ executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
alert(`Executing ${action.name} on ${item.name}`);
}
search = (searchValue: string) => {
- this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_EXPLORER_ID, searchValue }));
+ this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_PANEL_ID, searchValue }));
}
changePage = (page: number) => {
- this.props.dispatch(actions.SET_PAGE({ id: PROJECT_EXPLORER_ID, page }));
+ this.props.dispatch(actions.SET_PAGE({ id: PROJECT_PANEL_ID, page }));
}
changeRowsPerPage = (rowsPerPage: number) => {
- this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_EXPLORER_ID, rowsPerPage }));
+ this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_PANEL_ID, rowsPerPage }));
+ }
+
+ openProject = (item: ProjectPanelItem) => {
+ this.props.dispatch<any>(setProjectItem(item.uuid));
}
}
}
});
-const renderName = (item: ProjectExplorerItem) =>
+const renderName = (item: ProjectPanelItem) =>
<Grid
container
alignItems="center"
</Grid>
</Grid>;
-const renderIcon = (item: ProjectExplorerItem) => {
- switch (item.type) {
- case "arvados#group":
+
+const renderIcon = (item: ProjectPanelItem) => {
+ switch (item.kind) {
+ case ResourceKind.LEVEL_UP:
+ return <i className="icon-level-up" style={{ fontSize: "1rem" }} />;
+ case ResourceKind.PROJECT:
return <i className="fas fa-folder fa-lg" />;
- case "arvados#groupList":
+ case ResourceKind.COLLECTION:
return <i className="fas fa-th fa-lg" />;
default:
return <i />;
{type}
</Typography>;
-const renderStatus = (item: ProjectExplorerItem) =>
+const renderStatus = (item: ProjectPanelItem) =>
<Typography noWrap align="center">
{item.status || "-"}
</Typography>;
-const columns: DataColumns<ProjectExplorerItem> = [{
+const columns: DataColumns<ProjectPanelItem> = [{
name: "Name",
selected: true,
- sortDirection: "asc",
- render: renderName
+ sortDirection: "desc",
+ render: renderName,
+ width: "450px"
}, {
name: "Status",
selected: true,
- filters: [{
- name: "In progress",
- selected: true
- }, {
- name: "Complete",
- selected: true
- }],
- render: renderStatus
+ render: renderStatus,
+ width: "75px"
}, {
name: "Type",
selected: true,
name: "Collection",
selected: true
}, {
- name: "Group",
+ name: "Project",
selected: true
}],
- render: item => renderType(item.type)
+ render: item => renderType(item.kind),
+ width: "125px"
}, {
name: "Owner",
selected: true,
- render: item => renderOwner(item.owner)
+ render: item => renderOwner(item.owner),
+ width: "200px"
}, {
name: "File size",
selected: true,
- sortDirection: "none",
- render: item => renderFileSize(item.fileSize)
+ render: item => renderFileSize(item.fileSize),
+ width: "50px"
}, {
name: "Last modified",
selected: true,
- render: item => renderDate(item.lastModified)
+ sortDirection: "none",
+ render: item => renderDate(item.lastModified),
+ width: "150px"
}];
const contextMenuActions = [[{
}
]];
-export default withStyles(styles)(connect()(ProjectExplorer));
+export default withStyles(styles)(connect()(ProjectPanel));
it('renders without crashing', () => {
const div = document.createElement('div');
- const store = configureStore({ projects: [], router: { location: null }, auth: {}, sidePanel: [] }, createBrowserHistory());
+ const store = configureStore({
+ projects: {
+ items: [],
+ currentItemId: ""
+ },
+ collections: [],
+ router: { location: null },
+ auth: {},
+ sidePanel: []
+ }, createBrowserHistory());
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
import { User } from "../../models/user";
import { RootState } from "../../store/store";
-import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
+import MainAppBar, {
+ MainAppBarActionProps,
+ MainAppBarMenuItem
+} from '../../views-components/main-app-bar/main-app-bar';
import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
import { push } from 'react-router-redux';
-import projectActions, { getProjectList } from "../../store/project/project-action";
import ProjectTree from '../../views-components/project-tree/project-tree';
-import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { TreeItem } from "../../components/tree/tree";
import { Project } from "../../models/project";
import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
-import ProjectExplorer, { PROJECT_EXPLORER_ID } from '../../views-components/project-explorer/project-explorer';
-import { ProjectExplorerItem, mapProjectTreeItem } from '../../views-components/project-explorer/project-explorer-item';
import sidePanelActions from '../../store/side-panel/side-panel-action';
import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
+import { ResourceKind } from "../../models/resource";
+import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
+import projectActions from "../../store/project/project-action";
+import ProjectPanel from "../project-panel/project-panel";
const drawerWidth = 240;
const appBarHeight = 102;
interface WorkbenchDataProps {
projects: Array<TreeItem<Project>>;
+ currentProjectId: string;
user?: User;
sidePanelItems: SidePanelItem[];
}
interface NavBreadcrumb extends Breadcrumb {
itemId: string;
- status: TreeItemStatus;
}
interface NavMenuItem extends MainAppBarMenuItem {
interface WorkbenchState {
anchorEl: any;
- breadcrumbs: NavBreadcrumb[];
searchText: string;
menuItems: {
accountMenu: NavMenuItem[],
}
};
-
mainAppBarActions: MainAppBarActionProps = {
- onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
- this.toggleProjectTreeItemOpen(itemId, status);
+ onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
+ this.props.dispatch<any>(setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.BOTH));
},
onSearch: searchText => {
this.setState({ searchText });
onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
};
- toggleProjectTreeItemOpen = (itemId: string, status: TreeItemStatus) => {
- if (status === TreeItemStatus.Loaded) {
- this.openProjectItem(itemId);
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
- } else {
- this.props.dispatch<any>(getProjectList(itemId))
- .then(() => {
- this.openProjectItem(itemId);
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
- });
- }
- }
-
- toggleProjectTreeItemActive = (itemId: string, status: TreeItemStatus) => {
- if (status === TreeItemStatus.Loaded) {
- this.openProjectItem(itemId);
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
- this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
- } else {
- this.props.dispatch<any>(getProjectList(itemId))
- .then(() => {
- this.openProjectItem(itemId);
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
- this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
- });
- }
- }
-
toggleSidePanelOpen = (itemId: string) => {
this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
}
this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
}
- openProjectItem = (itemId: string) => {
- const branch = getTreePath(this.props.projects, itemId);
- this.setState({
- breadcrumbs: branch.map(item => ({
- label: item.data.name,
- itemId: item.data.uuid,
- status: item.status
- }))
- });
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
- this.props.dispatch(push(`/project/${itemId}`));
-
- const project = findTreeItem(this.props.projects, itemId);
- const items: ProjectExplorerItem[] = project && project.items
- ? project.items.map(mapProjectTreeItem)
- : [];
- this.props.dispatch(dataExplorerActions.SET_ITEMS({ id: PROJECT_EXPLORER_ID, items }));
- }
-
render() {
- const { classes, user, projects, sidePanelItems } = this.props;
+ const branch = getTreePath(this.props.projects, this.props.currentProjectId);
+ const breadcrumbs = branch.map(item => ({
+ label: item.data.name,
+ itemId: item.data.uuid,
+ status: item.status
+ }));
+
+ const { classes, user } = this.props;
return (
<div className={classes.root}>
<div className={classes.appBar}>
<MainAppBar
- breadcrumbs={this.state.breadcrumbs}
+ breadcrumbs={breadcrumbs}
searchText={this.state.searchText}
user={this.props.user}
menuItems={this.state.menuItems}
<SidePanel
toggleOpen={this.toggleSidePanelOpen}
toggleActive={this.toggleSidePanelActive}
- sidePanelItems={sidePanelItems}>
+ sidePanelItems={this.props.sidePanelItems}>
<ProjectTree
- projects={projects}
- toggleOpen={this.toggleProjectTreeItemOpen}
- toggleActive={this.toggleProjectTreeItemActive} />
+ projects={this.props.projects}
+ toggleOpen={itemId =>
+ this.props.dispatch<any>(
+ setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.OPEN)
+ )}
+ toggleActive={itemId =>
+ this.props.dispatch<any>(
+ setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.ACTIVE)
+ )}
+ />
</SidePanel>
</Drawer>}
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path="/project/:name" component={ProjectExplorer} />
+ <Route path="/projects/:name" component={ProjectPanel} />
</Switch>
</div>
</main>
export default connect<WorkbenchDataProps>(
(state: RootState) => ({
- projects: state.projects,
+ projects: state.projects.items,
+ currentProjectId: state.projects.currentItemId,
user: state.auth.user,
- sidePanelItems: state.sidePanel,
+ sidePanelItems: state.sidePanel
})
)(
withStyles(styles)(Workbench)
"no-debugger": false,
"no-console": false,
"no-shadowed-variable": false,
- "semicolon": true
+ "semicolon": true,
+ "array-type": false,
+ "interface-over-type-literal": false
},
"linterOptions": {
"exclude": [