import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
import ColumnSelector, { ColumnSelectorProps, ColumnSelectorTrigger } from "./column-selector";
-import { DataColumn } from "../data-column";
+import { DataColumn } from "../data-table/data-column";
import { ListItem, Checkbox } from "@material-ui/core";
configure({ adapter: new Adapter() });
columnsConfigurator.find(ListItem).simulate("click");
expect(onColumnToggle).toHaveBeenCalledWith(columns[0]);
});
-});
\ No newline at end of file
+});
import * as React from 'react';
import { WithStyles, StyleRulesCallback, Theme, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
import MenuIcon from "@material-ui/icons/Menu";
-import { DataColumn, isColumnConfigurable } from '../data-column';
-import Popover from "../../popover/popover";
+import { DataColumn, isColumnConfigurable } from '../data-table/data-column';
+import Popover from "../popover/popover";
import { IconButtonProps } from '@material-ui/core/IconButton';
export interface ColumnSelectorProps {
import { Table, TableBody, TableRow, TableCell, TableHead, StyleRulesCallback, Theme, WithStyles, withStyles, Typography } from '@material-ui/core';
import { DataColumn } from './data-column';
+export type DataColumns<T> = Array<DataColumn<T>>;
+
export interface DataTableProps<T> {
items: T[];
- columns: Array<DataColumn<T>>;
+ columns: DataColumns<T>;
onRowClick?: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
onRowContextMenu?: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
}
</TableRow>
)}
</TableBody>
- </Table> : <Typography
+ </Table> : <Typography
className={classes.noItemsInfo}
variant="body2"
gutterBottom>
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-export * from "./data-column";
-export * from "./column-selector/column-selector";
-export { default as ColumnSelector } from "./column-selector/column-selector";
-export * from "./data-table";
-export { default as DataTable } from "./data-table";
\ No newline at end of file
import * as ReactDOM from 'react-dom';
import { Provider } from "react-redux";
import Workbench from './views/workbench/workbench';
-import ProjectList from './components/project-list/project-list';
import './index.css';
import { Route } from "react-router";
import createBrowserHistory from "history/createBrowserHistory";
import configureStore from "./store/store";
import { ConnectedRouter } from "react-router-redux";
-import ApiToken from "./components/api-token/api-token";
+import ApiToken from "./views-components/api-token/api-token";
import authActions from "./store/auth/auth-action";
-import { authService, projectService } from "./services/services";
+import { authService } from "./services/services";
+import { getProjectList } from "./store/project/project-action";
const history = createBrowserHistory();
store.dispatch(authActions.INIT());
const rootUuid = authService.getRootUuid();
-store.dispatch<any>(projectService.getProjectList(rootUuid));
+store.dispatch<any>(getProjectList(rootUuid));
const App = () =>
<Provider store={store}>
//
// SPDX-License-Identifier: AGPL-3.0
-export { default as DataExplorer } from "./data-explorer";
-export * from "./data-item";
\ No newline at end of file
+import { Resource } from "./resource";
+
+export interface Collection extends Resource {
+}
//
// SPDX-License-Identifier: AGPL-3.0
-export interface Project {
- name: string;
- createdAt: string;
- modifiedAt: string;
- uuid: string;
- ownerUuid: string;
- href: string;
- kind: string;
+import { Resource } from "./resource";
+
+export interface Project extends Resource {
}
--- /dev/null
+export interface Resource {
+ name: string;
+ createdAt: string;
+ modifiedAt: string;
+ uuid: string;
+ ownerUuid: string;
+ href: string;
+ kind: string;
+}
import { API_HOST, serverApi } from "../../common/api/server-api";
import { User } from "../../models/user";
-import { Dispatch } from "redux";
-import actions from "../../store/auth/auth-action";
export const API_TOKEN_KEY = 'apiToken';
export const USER_EMAIL_KEY = 'userEmail';
window.location.assign(`${API_HOST}/logout?return_to=${currentUrl}`);
}
- public getUserDetails = () => (dispatch: Dispatch): Promise<void> => {
- dispatch(actions.USER_DETAILS_REQUEST());
+ public getUserDetails = (): Promise<User> => {
return serverApi
.get<UserDetailsResponse>('/users/current')
- .then(resp => {
- dispatch(actions.USER_DETAILS_SUCCESS(resp.data));
- });
+ .then(resp => ({
+ email: resp.data.email,
+ firstName: resp.data.first_name,
+ lastName: resp.data.last_name,
+ uuid: resp.data.uuid,
+ ownerUuid: resp.data.owner_uuid
+ }));
}
public getRootUuid() {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { serverApi } from "../../common/api/server-api";
+import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
+import { ArvadosResource } from "../response";
+import { Collection } from "../../models/collection";
+
+interface CollectionResource extends ArvadosResource {
+ name: string;
+ description: string;
+ properties: any;
+ portable_data_hash: string;
+ manifest_text: string;
+ replication_desired: number;
+ replication_confirmed: number;
+ replication_confirmed_at: string;
+ trash_at: string;
+ delete_at: string;
+ is_trashed: boolean;
+}
+
+interface CollectionsResponse {
+ offset: number;
+ limit: number;
+ items: CollectionResource[];
+}
+
+export default class CollectionService {
+ public getCollectionList = (parentUuid?: string): Promise<Collection[]> => {
+ if (parentUuid) {
+ const fb = new FilterBuilder();
+ fb.addLike(FilterField.OWNER_UUID, parentUuid);
+ return serverApi.get<CollectionsResponse>('/collections', { params: {
+ filters: fb.get()
+ }}).then(resp => {
+ const collections = resp.data.items.map(g => ({
+ name: g.name,
+ createdAt: g.created_at,
+ modifiedAt: g.modified_at,
+ href: g.href,
+ uuid: g.uuid,
+ ownerUuid: g.owner_uuid,
+ kind: g.kind
+ } as Collection));
+ return collections;
+ });
+ } else {
+ return Promise.resolve([]);
+ }
+ }
+}
import { serverApi } from "../../common/api/server-api";
import { Dispatch } from "redux";
-import actions from "../../store/project/project-action";
import { Project } from "../../models/project";
-import UrlBuilder from "../../common/api/url-builder";
import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
+import { ArvadosResource } from "../response";
+
+interface GroupResource extends ArvadosResource {
+ name: string;
+ group_class: string;
+ description: string;
+ writable_by: string[];
+ delete_at: string;
+ trash_at: string;
+ is_trashed: boolean;
+}
interface GroupsResponse {
offset: number;
limit: number;
- items: Array<{
- href: string;
- kind: string;
- etag: string;
- uuid: string;
- owner_uuid: string;
- created_at: string;
- modified_by_client_uuid: string;
- modified_by_user_uuid: string;
- modified_at: string;
- name: string;
- group_class: string;
- description: string;
- writable_by: string[];
- delete_at: string;
- trash_at: string;
- is_trashed: boolean;
- }>;
+ items: GroupResource[];
}
export default class ProjectService {
- public getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
- dispatch(actions.PROJECTS_REQUEST(parentUuid));
+ public getProjectList = (parentUuid?: string): Promise<Project[]> => {
if (parentUuid) {
const fb = new FilterBuilder();
fb.addLike(FilterField.OWNER_UUID, parentUuid);
return serverApi.get<GroupsResponse>('/groups', { params: {
filters: fb.get()
- }}).then(groups => {
- const projects = groups.data.items.map(g => ({
+ }}).then(resp => {
+ const projects = resp.data.items.map(g => ({
name: g.name,
createdAt: g.created_at,
modifiedAt: g.modified_at,
ownerUuid: g.owner_uuid,
kind: g.kind
} as Project));
- dispatch(actions.PROJECTS_SUCCESS({projects, parentItemId: parentUuid}));
return projects;
});
} else {
- dispatch(actions.PROJECTS_SUCCESS({projects: [], parentItemId: parentUuid}));
return Promise.resolve([]);
}
}
--- /dev/null
+export interface ArvadosResource {
+ uuid: string;
+ owner_uuid: string;
+ created_at: string;
+ modified_by_client_uuid: string;
+ modified_by_user_uuid: string;
+ modified_at: string;
+ href: string;
+ kind: string;
+ etag: string;
+}
import AuthService from "./auth-service/auth-service";
import ProjectService from "./project-service/project-service";
+import CollectionService from "./collection-service/collection-service";
export const authService = new AuthService();
export const projectService = new ProjectService();
+export const collectionService = new CollectionService();
// SPDX-License-Identifier: AGPL-3.0
import { ofType, default as unionize, UnionOf } from "unionize";
-import { UserDetailsResponse } from "../../services/auth-service/auth-service";
+import { Dispatch } from "redux";
+import { authService } from "../../services/services";
+import { User } from "../../models/user";
const actions = unionize({
SAVE_API_TOKEN: ofType<string>(),
LOGOUT: {},
INIT: {},
USER_DETAILS_REQUEST: {},
- USER_DETAILS_SUCCESS: ofType<UserDetailsResponse>()
+ USER_DETAILS_SUCCESS: ofType<User>()
}, {
tag: 'type',
value: 'payload'
});
+export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
+ dispatch(actions.USER_DETAILS_REQUEST());
+ return authService.getUserDetails().then(details => {
+ dispatch(actions.USER_DETAILS_SUCCESS(details));
+ return details;
+ });
+};
+
+
export type AuthAction = UnionOf<typeof actions>;
export default actions;
it('should set user details on success fetch', () => {
const initialState = undefined;
- const userDetails = {
+ const user = {
email: "test@test.com",
- first_name: "John",
- last_name: "Doe",
+ firstName: "John",
+ lastName: "Doe",
uuid: "uuid",
- owner_uuid: "ownerUuid",
- is_admin: true
+ ownerUuid: "ownerUuid"
};
- const state = authReducer(initialState, actions.USER_DETAILS_SUCCESS(userDetails));
+ const state = authReducer(initialState, actions.USER_DETAILS_SUCCESS(user));
expect(state).toEqual({
apiToken: undefined,
user: {
import { User } from "../../models/user";
import { authService } from "../../services/services";
import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
-import { UserDetailsResponse } from "../../services/auth-service/auth-service";
export interface AuthState {
user?: User;
authService.logout();
return {...state, apiToken: undefined};
},
- USER_DETAILS_SUCCESS: (ud: UserDetailsResponse) => {
- const user = {
- email: ud.email,
- firstName: ud.first_name,
- lastName: ud.last_name,
- uuid: ud.uuid,
- ownerUuid: ud.owner_uuid
- };
+ USER_DETAILS_SUCCESS: (user: User) => {
authService.saveUser(user);
return {...state, user};
},
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Collection } from "../../models/collection";
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { Dispatch } from "redux";
+import { collectionService } from "../../services/services";
+
+const actions = unionize({
+ CREATE_COLLECTION: ofType<Collection>(),
+ REMOVE_COLLECTION: ofType<string>(),
+ COLLECTIONS_REQUEST: ofType<any>(),
+ COLLECTIONS_SUCCESS: ofType<{ collections: Collection[] }>(),
+}, {
+ tag: 'type',
+ value: 'payload'
+});
+
+export const getCollectionList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Collection[]> => {
+ dispatch(actions.COLLECTIONS_REQUEST());
+ return collectionService.getCollectionList(parentUuid).then(collections => {
+ dispatch(actions.COLLECTIONS_SUCCESS({collections}));
+ return collections;
+ });
+};
+
+export type CollectionAction = UnionOf<typeof actions>;
+export default actions;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import collectionsReducer from "./collection-reducer";
+import actions from "./collection-action";
+
+describe('collection-reducer', () => {
+ it('should add new collection to the list', () => {
+ const initialState = undefined;
+ const collection = {
+ name: 'test',
+ href: 'href',
+ createdAt: '2018-01-01',
+ modifiedAt: '2018-01-01',
+ ownerUuid: 'owner-test123',
+ uuid: 'test123',
+ kind: ""
+ };
+
+ const state = collectionsReducer(initialState, actions.CREATE_COLLECTION(collection));
+ expect(state).toEqual([collection]);
+ });
+
+ it('should load collections', () => {
+ const initialState = undefined;
+ const collection = {
+ name: 'test',
+ href: 'href',
+ createdAt: '2018-01-01',
+ modifiedAt: '2018-01-01',
+ ownerUuid: 'owner-test123',
+ uuid: 'test123',
+ kind: ""
+ };
+
+ const collections = [collection, collection];
+ const state = collectionsReducer(initialState, actions.COLLECTIONS_SUCCESS({ collections }));
+ expect(state).toEqual([collection, collection]);
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import actions, { CollectionAction } from "./collection-action";
+import { Collection } from "../../models/collection";
+
+export type CollectionState = Collection[];
+
+
+const collectionsReducer = (state: CollectionState = [], action: CollectionAction) => {
+ return actions.match(action, {
+ CREATE_COLLECTION: collection => [...state, collection],
+ REMOVE_COLLECTION: () => state,
+ COLLECTIONS_REQUEST: () => {
+ return state;
+ },
+ COLLECTIONS_SUCCESS: ({ collections }) => {
+ return collections;
+ },
+ default: () => state
+ });
+};
+
+export default collectionsReducer;
import { Project } from "../../models/project";
import { default as unionize, ofType, UnionOf } from "unionize";
+import { projectService } from "../../services/services";
+import { Dispatch } from "redux";
const actions = unionize({
CREATE_PROJECT: ofType<Project>(),
value: 'payload'
});
+export const getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
+ dispatch(actions.PROJECTS_REQUEST());
+ return projectService.getProjectList(parentUuid).then(projects => {
+ dispatch(actions.PROJECTS_SUCCESS({projects, parentItemId: parentUuid}));
+ return projects;
+ });
+};
+
export type ProjectAction = UnionOf<typeof actions>;
export default actions;
import { History } from "history";
import projectsReducer, { ProjectState } from "./project/project-reducer";
import authReducer, { AuthState } from "./auth/auth-reducer";
+import collectionsReducer from "./collection/collection-reducer";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
const rootReducer = combineReducers({
auth: authReducer,
projects: projectsReducer,
+ collections: collectionsReducer,
router: routerReducer
});
import { Redirect, RouteProps } from "react-router";
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
-import authActions from "../../store/auth/auth-action";
-import { authService, projectService } from "../../services/services";
+import authActions, { getUserDetails } from "../../store/auth/auth-action";
+import { authService } from "../../services/services";
+import { getProjectList } from "../../store/project/project-action";
interface ApiTokenProps {
}
const search = this.props.location ? this.props.location.search : "";
const apiToken = ApiToken.getUrlParameter(search, 'api_token');
this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
- this.props.dispatch<any>(authService.getUserDetails()).then(() => {
+ this.props.dispatch<any>(getUserDetails()).then(() => {
const rootUuid = authService.getRootUuid();
- this.props.dispatch(projectService.getProjectList(rootUuid));
+ this.props.dispatch(getProjectList(rootUuid));
});
}
render() {
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { DataTable, DataColumn, ColumnSelector } from "../../components/data-table";
import { Typography, Grid, Paper, Toolbar } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import MoreVertIcon from "@material-ui/icons/MoreVert";
import { formatFileSize, formatDate } from '../../common/formatters';
import { DataItem } from './data-item';
-import { mockAnchorFromMouseEvent } from '../popover/helpers';
-import ContextMenu from '../context-menu/context-menu';
+import { DataColumns } from "../../components/data-table/data-table";
+import ContextMenu from "../../components/context-menu/context-menu";
+import ColumnSelector from "../../components/column-selector/column-selector";
+import DataTable from "../../components/data-table/data-table";
+import { mockAnchorFromMouseEvent } from "../../components/popover/helpers";
+import { DataColumn } from "../../components/data-table/data-column";
export interface DataExplorerContextActions {
onAddToFavourite: (dataIitem: DataItem) => void;
}
interface DataExplorerState {
- columns: Array<DataColumn<DataItem>>;
+ columns: DataColumns<DataItem>;
contextMenu: {
anchorEl?: HTMLElement;
item?: DataItem;
// SPDX-License-Identifier: AGPL-3.0
export interface DataItem {
+ uuid: string;
name: string;
type: string;
owner: string;
lastModified: string;
fileSize?: number;
status?: string;
-}
\ No newline at end of file
+}
import { mount, configure, ReactWrapper } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
import MainAppBar from "./main-app-bar";
-import SearchBar from "./search-bar/search-bar";
-import Breadcrumbs from "../breadcrumbs/breadcrumbs";
-import DropdownMenu from "./dropdown-menu/dropdown-menu";
+import SearchBar from "../../components/search-bar/search-bar";
+import Breadcrumbs from "../../components/breadcrumbs/breadcrumbs";
+import DropdownMenu from "../../components/dropdown-menu/dropdown-menu";
import { Button, MenuItem, IconButton } from "@material-ui/core";
import { User } from "../../models/user";
mainAppBar.find(DropdownMenu).at(0).find(MenuItem).at(1).simulate("click");
expect(onMenuItemClick).toBeCalledWith(menuItems.accountMenu[0]);
});
-});
\ No newline at end of file
+});
import NotificationsIcon from "@material-ui/icons/Notifications";
import PersonIcon from "@material-ui/icons/Person";
import HelpIcon from "@material-ui/icons/Help";
-import SearchBar from "./search-bar/search-bar";
-import Breadcrumbs, { Breadcrumb } from "../breadcrumbs/breadcrumbs";
-import DropdownMenu from "./dropdown-menu/dropdown-menu";
+import SearchBar from "../../components/search-bar/search-bar";
+import Breadcrumbs, { Breadcrumb } from "../../components/breadcrumbs/breadcrumbs";
+import DropdownMenu from "../../components/dropdown-menu/dropdown-menu";
import { User, getUserFullname } from "../../models/user";
export interface MainAppBarMenuItem {
}
});
-export default withStyles(styles)(MainAppBar);
\ No newline at end of file
+export default withStyles(styles)(MainAppBar);
import CircularProgress from '@material-ui/core/CircularProgress';
import ProjectTree from './project-tree';
-import { TreeItem } from '../tree/tree';
+import { TreeItem } from '../../components/tree/tree';
import { Project } from '../../models/project';
Enzyme.configure({ adapter: new Adapter() });
import ListItemIcon from '@material-ui/core/ListItemIcon';
import Typography from '@material-ui/core/Typography';
-import Tree, { TreeItem, TreeItemStatus } from '../tree/tree';
+import Tree, { TreeItem, TreeItemStatus } from '../../components/tree/tree';
import { Project } from '../../models/project';
type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
--- /dev/null
+import { TreeItem } from "../../components/tree/tree";
+import { Project } from "../../models/project";
+import { DataItem } from "../../views-components/data-explorer/data-item";
+
+export const mapProjectTreeItem = (item: TreeItem<Project>): DataItem => ({
+ name: item.data.name,
+ type: item.data.kind,
+ owner: item.data.ownerUuid,
+ lastModified: item.data.modifiedAt,
+ uuid: item.data.uuid
+});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { DataTableProps } from "../../components/data-table";
import { RouteComponentProps } from 'react-router';
import { Project } from '../../models/project';
import { ProjectState, findTreeItem } from '../../store/project/project-reducer';
import { connect, DispatchProp } from 'react-redux';
import { push } from 'react-router-redux';
import projectActions from "../../store/project/project-action";
-import { DataExplorer, DataItem } from '../../components/data-explorer';
-import { TreeItem } from '../../components/tree/tree';
-import { DataExplorerContextActions } from '../../components/data-explorer/data-explorer';
+import { DataColumns } from "../../components/data-table/data-table";
+import DataExplorer, { DataExplorerContextActions } from "../../views-components/data-explorer/data-explorer";
+import { mapProjectTreeItem } from "./data-explorer-selectors";
+import { DataItem } from "../../views-components/data-explorer/data-item";
interface DataExplorerViewDataProps {
projects: ProjectState;
}
type DataExplorerViewProps = DataExplorerViewDataProps & RouteComponentProps<{ name: string }> & DispatchProp;
-
-type DataExplorerViewState = Pick<DataTableProps<Project>, "columns">;
-
-interface MappedProjectItem extends DataItem {
- uuid: string;
-}
+type DataExplorerViewState = DataColumns<Project>;
class DataExplorerView extends React.Component<DataExplorerViewProps, DataExplorerViewState> {
const projectItems = project && project.items || [];
return (
<DataExplorer
- items={projectItems.map(mapTreeItem)}
+ items={projectItems.map(mapProjectTreeItem)}
onItemClick={this.goToProject}
contextActions={this.contextActions}
/>
onShare: console.log
};
- goToProject = (project: MappedProjectItem) => {
- this.props.dispatch(push(`/project/${project.uuid}`));
- this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(project.uuid));
+ goToProject = (item: DataItem) => {
+ this.props.dispatch(push(`/project/${item}`));
+ this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(item.uuid));
}
-
}
-const mapTreeItem = (item: TreeItem<Project>): MappedProjectItem => ({
- name: item.data.name,
- type: item.data.kind,
- owner: item.data.ownerUuid,
- lastModified: item.data.modifiedAt,
- uuid: item.data.uuid
-});
-
-
export default connect(
(state: RootState) => ({
projects: state.projects
import authActions from "../../store/auth/auth-action";
import { User } from "../../models/user";
import { RootState } from "../../store/store";
-import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../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 from "../../store/project/project-action";
-import ProjectTree from '../../components/project-tree/project-tree';
+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 { Project } from "../../models/project";
-import { projectService } from '../../services/services';
import { getTreePath } from '../../store/project/project-reducer';
import DataExplorer from '../data-explorer/data-explorer';
if (status === TreeItemStatus.Loaded) {
this.openProjectItem(itemId);
} else {
- this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => this.openProjectItem(itemId));
+ this.props.dispatch<any>(getProjectList(itemId))
+ .then(() => this.openProjectItem(itemId));
}
}