"version": "0.1.0",
"private": true,
"dependencies": {
- "@material-ui/core": "1.4.0",
- "@material-ui/icons": "1.1.0",
- "@types/lodash": "4.14.112",
- "@types/redux-form": "7.4.1",
+ "@material-ui/core": "1.4.2",
+ "@material-ui/icons": "2.0.0",
+ "@types/lodash": "4.14.116",
+ "@types/react-copy-to-clipboard": "4.2.5",
+ "@types/redux-form": "7.4.4",
"axios": "0.18.0",
"classnames": "2.2.6",
"lodash": "4.17.10",
- "react": "16.4.1",
- "react-dom": "16.4.1",
+ "react": "16.4.2",
+ "react-copy-to-clipboard": "5.0.1",
+ "react-dom": "16.4.2",
"react-redux": "5.0.7",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"@types/classnames": "^2.2.4",
"@types/enzyme": "3.1.12",
"@types/enzyme-adapter-react-16": "1.0.2",
- "@types/jest": "23.3.0",
- "@types/node": "10.5.2",
+ "@types/jest": "23.3.1",
+ "@types/node": "10.5.5",
"@types/react": "16.4",
"@types/react-dom": "16.0.6",
- "@types/react-redux": "6.0.4",
+ "@types/react-redux": "6.0.6",
"@types/react-router": "4.0.29",
- "@types/react-router-dom": "4.2.7",
+ "@types/react-router-dom": "4.3.0",
"@types/react-router-redux": "5.0.15",
"@types/redux-devtools": "3.0.44",
- "@types/redux-form": "7.4.1",
+ "@types/redux-form": "7.4.4",
"axios-mock-adapter": "1.15.0",
"enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1",
"jest-localstorage-mock": "2.2.0",
"redux-devtools": "3.4.1",
"redux-form": "7.4.2",
- "typescript": "2.9.2"
+ "typescript": "3.0.1"
},
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/src/$1"
}));
}
- update(uuid: string) {
- throw new Error("Not implemented");
+ update(uuid: string, data: any) {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .put<T>(this.resourceType + uuid, data));
+
}
}
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import Axios, { AxiosInstance } from "axios";
-
-export const API_HOST = process.env.REACT_APP_ARVADOS_API_HOST;
-
-export const authClient: AxiosInstance = Axios.create();
-export const apiClient: AxiosInstance = Axios.create();
-
-export function setServerApiAuthorizationHeader(token: string) {
- [authClient, apiClient].forEach(client => {
- client.defaults.headers.common = {
- Authorization: `OAuth2 ${token}`
- };
- });
-}
-
-export function removeServerApiAuthorizationHeader() {
- [authClient, apiClient].forEach(client => {
- delete client.defaults.headers.common.Authorization;
- });
-}
-
-export const setBaseUrl = (url: string) => {
- authClient.defaults.baseURL = url;
- apiClient.defaults.baseURL = url + "/arvados/v1";
-};
}
export interface ArvadosTheme extends Theme {
- customs: any;
+ customs: {
+ colors: Colors
+ };
+}
+
+interface Colors {
+ green700: string;
+ yellow700: string;
}
const red900 = red["900"];
-const yellow700 = yellow["700"];
const purple800 = purple["800"];
const grey200 = grey["200"];
const grey300 = grey["300"];
const themeOptions: ArvadosThemeOptions = {
customs: {
colors: {
- green700: green["700"]
+ green700: green["700"],
+ yellow700: yellow["700"]
}
},
overrides: {
root: {
fontSize: '1.25rem'
}
+ },
+ MuiCardHeader: {
+ avatar: {
+ display: 'flex',
+ alignItems: 'center'
+ },
+ title: {
+ color: grey700,
+ fontSize: '1.25rem'
+ }
+ },
+ MuiMenuItem: {
+ root: {
+ padding: '8px 16px'
+ }
+ },
+ MuiInput: {
+ underline: {
+ '&:after': {
+ borderBottomColor: purple800
+ },
+ '&:hover:not($disabled):not($focused):not($error):before': {
+ borderBottom: '1px solid inherit'
+ }
+ }
+ },
+ MuiFormLabel: {
+ focused: {
+ "&$focused:not($error)": {
+ color: purple800
+ }
+ }
}
},
mixins: {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { TreeItem, TreeItemStatus } from '../tree/tree';
+import { FileTreeData } from '../file-tree/file-tree-data';
+import { FileTree } from '../file-tree/file-tree';
+import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, CardContent, Card, Button } from '@material-ui/core';
+import { CustomizeTableIcon } from '../icon/icon';
+
+export interface CollectionPanelFilesProps {
+ items: Array<TreeItem<FileTreeData>>;
+ onUploadDataClick: () => void;
+ onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
+ onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+}
+
+type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ root: {
+ paddingBottom: theme.spacing.unit
+ },
+ cardSubheader: {
+ paddingTop: 0,
+ paddingBottom: 0
+ },
+ nameHeader: {
+ marginLeft: '75px'
+ },
+ fileSizeHeader: {
+ marginRight: '65px'
+ }
+});
+
+export const CollectionPanelFiles = withStyles(styles)(
+ ({ onItemMenuOpen, onOptionsMenuOpen, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
+ <Card className={classes.root}>
+ <CardHeader
+ title="Files"
+ action={
+ <Button
+ variant='raised'
+ color='primary'
+ size='small'>
+ Upload data
+ </Button>
+ } />
+ <CardHeader
+ className={classes.cardSubheader}
+ action={
+ <IconButton onClick={onOptionsMenuOpen}>
+ <CustomizeTableIcon />
+ </IconButton>
+ } />
+ <Grid container justify="space-between">
+ <Typography variant="caption" className={classes.nameHeader}>
+ Name
+ </Typography>
+ <Typography variant="caption" className={classes.fileSizeHeader}>
+ File size
+ </Typography>
+ </Grid>
+ <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
+ </Card>);
{
name: "Column 1",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
},
{
name: "Column 2",
{
name: "Column 1",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
},
{
name: "Column 2",
render: () => <span />,
- selected: false
+ selected: false,
+ configurable: true
},
{
name: "Column 3",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
}
];
const columnsConfigurator = mount(<ColumnSelector columns={columns} onColumnToggle={jest.fn()} />);
{
name: "Column 1",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
}
];
const onColumnToggle = jest.fn();
import * as React from 'react';
import { WithStyles, StyleRulesCallback, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
import MenuIcon from "@material-ui/icons/Menu";
-import { DataColumn, isColumnConfigurable } from '../data-table/data-column';
+import { DataColumn } from '../data-table/data-column';
import { Popover } from "../popover/popover";
import { IconButtonProps } from '@material-ui/core/IconButton';
import { DataColumns } from '../data-table/data-table';
<Paper>
<List dense>
{columns
- .filter(isColumnConfigurable)
- .map((column, index) => (
+ .filter(column => column.configurable)
+ .map((column, index) =>
<ListItem
button
key={index}
{column.name}
</ListItemText>
</ListItem>
- ))}
+ )}
</List>
</Paper>
</Popover>
columns: DataColumns<T>;
searchValue: string;
rowsPerPage: number;
- rowsPerPageOptions?: number[];
+ rowsPerPageOptions: number[];
page: number;
onSearch: (value: string) => void;
onRowClick: (item: T) => void;
contextMenuColumn = {
name: "Actions",
selected: true,
+ configurable: false,
key: "context-actions",
- renderHeader: () => null,
render: this.renderContextMenuTrigger,
width: "auto"
};
export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
name: string;
selected: boolean;
- configurable?: boolean;
+ configurable: boolean;
key?: React.Key;
sortDirection?: SortDirection;
filters?: F[];
- render: (item: T) => React.ReactElement<void>;
- renderHeader?: () => React.ReactElement<void> | null;
+ render: (item: T) => React.ReactElement<any>;
+ renderHeader?: () => React.ReactElement<any>;
width?: string;
}
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 === SortDirection.ASC
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface FileTreeData {
+ name: string;
+ type: string;
+ size?: number;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { TreeItem } from "../tree/tree";
+import { ProjectIcon, MoreOptionsIcon, DefaultIcon, CollectionIcon } from "../icon/icon";
+import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
+import { formatFileSize } from "../../common/formatters";
+import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
+import { FileTreeData } from "./file-tree-data";
+
+type CssRules = "root" | "spacer" | "sizeInfo" | "button";
+
+const fileTreeItemStyle: StyleRulesCallback<CssRules> = theme => ({
+ root: {
+ display: "flex",
+ alignItems: "center",
+ paddingRight: `${theme.spacing.unit * 1.5}px`
+ },
+ spacer: {
+ flex: "1"
+ },
+ sizeInfo: {
+ width: `${theme.spacing.unit * 8}px`
+ },
+ button: {
+ width: theme.spacing.unit * 3,
+ height: theme.spacing.unit * 3,
+ marginRight: theme.spacing.unit
+ }
+});
+
+export interface FileTreeItemProps {
+ item: TreeItem<FileTreeData>;
+ onMoreClick: (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => void;
+}
+export const FileTreeItem = withStyles(fileTreeItemStyle)(
+ class extends React.Component<FileTreeItemProps & WithStyles<CssRules>> {
+ render() {
+ const { classes, item } = this.props;
+ return <div className={classes.root}>
+ <ListItemTextIcon
+ icon={getIcon(item)}
+ name={item.data.name} />
+ <div className={classes.spacer} />
+ <Typography
+ className={classes.sizeInfo}
+ variant="caption">{formatFileSize(item.data.size)}</Typography>
+ <IconButton
+ className={classes.button}
+ onClick={this.handleClick}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </div >;
+ }
+
+ handleClick = (event: React.MouseEvent<any>) => {
+ this.props.onMoreClick(event, this.props.item);
+ }
+ });
+
+const getIcon = (item: TreeItem<FileTreeData>) => {
+ switch(item.data.type){
+ case 'directory':
+ return ProjectIcon;
+ case 'file':
+ return CollectionIcon;
+ default:
+ return DefaultIcon;
+ }
+};
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Tree, TreeItem, TreeItemStatus } from "../tree/tree";
+import { FileTreeData } from "./file-tree-data";
+import { FileTreeItem } from "./file-tree-item";
+
+export interface FileTreeProps {
+ items: Array<TreeItem<FileTreeData>>;
+ onMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+}
+
+export class FileTree extends React.Component<FileTreeProps> {
+ render() {
+ return <Tree
+ showSelection={true}
+ items={this.props.items}
+ disableRipple={true}
+ render={this.renderItem}
+ onContextMenu={this.handleContextMenu}
+ toggleItemActive={this.handleToggleActive}
+ toggleItemOpen={this.handleToggle}
+ onSelectionChange={this.handleSelectionChange} />;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => {
+ event.stopPropagation();
+ this.props.onMenuOpen(event, item);
+ }
+
+ handleToggle = (id: string, status: TreeItemStatus) => {
+ this.props.onCollapseToggle(id, status);
+ }
+
+ handleToggleActive = () => { return; };
+
+ handleSelectionChange = (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
+ event.stopPropagation();
+ this.props.onSelectionToggle(event, item);
+ }
+
+ renderItem = (item: TreeItem<FileTreeData>) =>
+ <FileTreeItem
+ item={item}
+ onMoreClick={this.handleContextMenu} />
+
+}
import ChevronLeft from '@material-ui/icons/ChevronLeft';
import ChevronRight from '@material-ui/icons/ChevronRight';
import Close from '@material-ui/icons/Close';
-import ContentCopy from '@material-ui/icons/ContentCopy';
+import ContentCopy from '@material-ui/icons/FileCopyOutlined';
import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
import Delete from '@material-ui/icons/Delete';
+import DeviceHub from '@material-ui/icons/DeviceHub';
import Edit from '@material-ui/icons/Edit';
import Folder from '@material-ui/icons/Folder';
import GetApp from '@material-ui/icons/GetApp';
import Inbox from '@material-ui/icons/Inbox';
import Info from '@material-ui/icons/Info';
import Input from '@material-ui/icons/Input';
+import LibraryBooks from '@material-ui/icons/LibraryBooks';
import Menu from '@material-ui/icons/Menu';
import MoreVert from '@material-ui/icons/MoreVert';
import Notifications from '@material-ui/icons/Notifications';
import PlayArrow from '@material-ui/icons/PlayArrow';
import RateReview from '@material-ui/icons/RateReview';
import Search from '@material-ui/icons/Search';
+import SettingsApplications from '@material-ui/icons/SettingsApplications';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
export type IconType = React.SFC<{ className?: string }>;
export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
-export const AdvancedIcon: IconType = (props) => <Folder {...props} />;
+export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
-export const CollectionIcon: IconType = (props) => <Folder {...props} />;
+export const CollectionIcon: IconType = (props) => <LibraryBooks {...props} />;
export const CloseIcon: IconType = (props) => <Close {...props} />;
export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
export const DetailsIcon: IconType = (props) => <Info {...props} />;
export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
export const ProjectIcon: IconType = (props) => <Folder {...props} />;
export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
-export const ProvenanceGraphIcon: IconType = (props) => <Folder {...props} />;
+export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
export const RemoveIcon: IconType = (props) => <Delete {...props} />;
export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
export const TrashIcon: IconType = (props) => <Delete {...props} />;
export const UserPanelIcon: IconType = (props) => <Person {...props} />;
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
-export const WorkflowIcon: IconType = (props) => <Code {...props} />;
\ No newline at end of file
+export const WorkflowIcon: IconType = (props) => <Code {...props} />;
color: theme.palette.primary.main,
},
hasMargin: {
- marginLeft: '18px',
+ marginLeft: `${theme.spacing.unit}px`,
},
});
toggableIconContainer: {
color: theme.palette.grey["700"],
height: '14px',
- position: 'absolute'
+ width: '14px'
},
toggableIcon: {
fontSize: '14px'
import { Tree, TreeItem } from './tree';
import { ProjectResource } from '../../models/project';
import { mockProjectResource } from '../../models/test-utils';
+import { Checkbox } from '@material-ui/core';
Enzyme.configure({ adapter: new Adapter() });
items={[project]} />);
expect(wrapper.find('i')).toHaveLength(1);
});
+
+ it("should render checkbox", () => {
+ const project: TreeItem<ProjectResource> = {
+ data: mockProjectResource(),
+ id: "3",
+ open: true,
+ active: true,
+ status: 1,
+ };
+ const wrapper = mount(<Tree
+ showSelection={true}
+ render={() => <div />}
+ toggleItemOpen={jest.fn()}
+ toggleItemActive={jest.fn()}
+ onContextMenu={jest.fn()}
+ items={[project]} />);
+ expect(wrapper.find(Checkbox)).toHaveLength(1);
+ });
+
+ it("call onSelectionChanged with associated item", () => {
+ const project: TreeItem<ProjectResource> = {
+ data: mockProjectResource(),
+ id: "3",
+ open: true,
+ active: true,
+ status: 1,
+ };
+ const spy = jest.fn();
+ const onSelectionChanged = (event: any, item: TreeItem<any>) => spy(item);
+ const wrapper = mount(<Tree
+ showSelection={true}
+ render={() => <div />}
+ toggleItemOpen={jest.fn()}
+ toggleItemActive={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSelectionChange={onSelectionChanged}
+ items={[project]} />);
+ wrapper.find(Checkbox).prop('onClick')();
+ expect(spy).toHaveBeenLastCalledWith({
+ data: mockProjectResource(),
+ id: "3",
+ open: true,
+ active: true,
+ status: 1,
+ });
+ });
+
});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
+import { List, ListItem, ListItemIcon, Collapse, Checkbox } from "@material-ui/core";
import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
import { ReactElement } from "react";
import CircularProgress from '@material-ui/core/CircularProgress';
import { ArvadosTheme } from '../../common/custom-theme';
import { SidePanelRightArrowIcon } from '../icon/icon';
-type CssRules = 'list' | 'active' | 'loader' | 'toggableIconContainer' | 'iconClose' | 'iconOpen' | 'toggableIcon';
+type CssRules = 'list'
+ | 'listItem'
+ | 'active'
+ | 'loader'
+ | 'toggableIconContainer'
+ | 'iconClose'
+ | 'renderContainer'
+ | 'iconOpen'
+ | 'toggableIcon'
+ | 'checkbox';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
list: {
padding: '3px 0px'
},
+ listItem: {
+ padding: '3px 0px',
+ },
loader: {
position: 'absolute',
transform: 'translate(0px)',
toggableIconContainer: {
color: theme.palette.grey["700"],
height: '14px',
- position: 'absolute'
+ width: '14px',
},
toggableIcon: {
fontSize: '14px'
},
+ renderContainer: {
+ flex: 1
+ },
active: {
color: theme.palette.primary.main,
},
iconOpen: {
transition: 'all 0.1s ease',
transform: 'rotate(90deg)',
+ },
+ checkbox: {
+ width: theme.spacing.unit * 3,
+ height: theme.spacing.unit * 3,
+ margin: `0 ${theme.spacing.unit}px`,
+ color: theme.palette.grey["500"]
}
});
id: string;
open: boolean;
active: boolean;
+ selected?: boolean;
status: TreeItemStatus;
items?: Array<TreeItem<T>>;
}
toggleItemActive: (id: string, status: TreeItemStatus) => void;
level?: number;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+ showSelection?: boolean;
+ onSelectionChange?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+ disableRipple?: boolean;
}
export const Tree = withStyles(styles)(
class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
render(): ReactElement<any> {
const level = this.props.level ? this.props.level : 0;
- const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
- const { list, loader, toggableIconContainer } = classes;
+ const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu, disableRipple } = this.props;
+ const { list, listItem, loader, toggableIconContainer, renderContainer } = classes;
return <List component="div" className={list}>
{items && items.map((it: TreeItem<T>, idx: number) =>
<div key={`item/${level}/${idx}`}>
- <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
+ <ListItem button className={listItem} style={{ paddingLeft: (level + 1) * 20 }}
+ disableRipple={disableRipple}
onClick={() => toggleItemActive(it.id, it.status)}
onContextMenu={this.handleRowContextMenu(it)}>
{it.status === TreeItemStatus.PENDING ?
{it.status !== TreeItemStatus.INITIAL && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
</ListItemIcon>
</i>
- {render(it, level)}
+ {this.props.showSelection &&
+ <Checkbox
+ checked={it.selected}
+ className={classes.checkbox}
+ color="primary"
+ onClick={this.handleCheckboxChange(it)} />}
+ <div className={renderContainer}>
+ {render(it, level)}
+ </div>
</ListItem>
{it.items && it.items.length > 0 &&
<Collapse in={it.open} timeout="auto" unmountOnExit>
<Tree
+ showSelection={this.props.showSelection}
items={it.items}
render={render}
+ disableRipple={disableRipple}
toggleItemOpen={toggleItemOpen}
toggleItemActive={toggleItemActive}
level={level + 1}
- onContextMenu={onContextMenu} />
+ onContextMenu={onContextMenu}
+ onSelectionChange={this.props.onSelectionChange} />
</Collapse>}
</div>)}
</List>;
handleRowContextMenu = (item: TreeItem<T>) =>
(event: React.MouseEvent<HTMLElement>) =>
this.props.onContextMenu(event, item)
+
+ handleCheckboxChange = (item: TreeItem<T>) => {
+ const { onSelectionChange } = this.props;
+ return onSelectionChange
+ ? (event: React.MouseEvent<HTMLElement>) => {
+ onSelectionChange(event, item);
+ }
+ : undefined;
+ }
}
);
import { configureStore } from "./store/store";
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "./views-components/api-token/api-token";
-import { authActions } from "./store/auth/auth-action";
-import { authService } from "./services/services";
+import { initAuth } from "./store/auth/auth-action";
+import { createServices } from "./services/services";
import { getProjectList } from "./store/project/project-action";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from './common/custom-theme';
import { fetchConfig } from './common/config';
-import { setBaseUrl } from './common/api/server-api';
import { addMenuActionSet, ContextMenuKind } from "./views-components/context-menu/context-menu";
import { rootProjectActionSet } from "./views-components/context-menu/action-sets/root-project-action-set";
import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
import { resourceActionSet } from './views-components/context-menu/action-sets/resource-action-set';
import { favoriteActionSet } from "./views-components/context-menu/action-sets/favorite-action-set";
+import { collectionFilesActionSet } from './views-components/context-menu/action-sets/collection-files-action-set';
+import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
+import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
fetchConfig()
.then(config => {
-
- setBaseUrl(config.API_HOST);
-
const history = createBrowserHistory();
- const store = configureStore(history);
+ const services = createServices(config.API_HOST);
+ const store = configureStore(history, services);
+
+ store.dispatch(initAuth());
+ store.dispatch(getProjectList(services.authService.getUuid()));
- store.dispatch(authActions.INIT());
- store.dispatch<any>(getProjectList(authService.getUuid()));
+ const Token = (props: any) => <ApiToken authService={services.authService} {...props}/>;
const App = () =>
<MuiThemeProvider theme={CustomTheme}>
<ConnectedRouter history={history}>
<div>
<Route path="/" component={Workbench} />
- <Route path="/token" component={ApiToken} />
+ <Route path="/token" component={Token} />
</div>
</ConnectedRouter>
</Provider>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Tree } from './tree';
+
+export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
+
+export enum CollectionFileType {
+ DIRECTORY = 'directory',
+ FILE = 'file'
+}
+
+export interface CollectionDirectory {
+ parentId: string;
+ id: string;
+ name: string;
+ type: CollectionFileType.DIRECTORY;
+}
+
+export interface CollectionFile {
+ parentId: string;
+ id: string;
+ name: string;
+ size: number;
+ type: CollectionFileType.FILE;
+}
+
+export const createCollectionDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
+ id: '',
+ name: '',
+ parentId: '',
+ type: CollectionFileType.DIRECTORY,
+ ...data
+});
+
+export const createCollectionFile = (data: Partial<CollectionFile>): CollectionFile => ({
+ id: '',
+ name: '',
+ parentId: '',
+ size: 0,
+ type: CollectionFileType.FILE,
+ ...data
+});
\ No newline at end of file
deleteAt: string;
isTrashed: boolean;
}
+
+export const getCollectionUrl = (uuid: string) => {
+ return `/collections/${uuid}`;
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type KeepManifest = KeepManifestStream[];
+
+export interface KeepManifestStream {
+ name: string;
+ locators: string[];
+ files: Array<KeepManifestStreamFile>;
+}
+
+export interface KeepManifestStreamFile {
+ name: string;
+ position: string;
+ size: number;
+}
export interface ProjectResource extends GroupResource {
groupClass: GroupClass.PROJECT;
}
+
+export const getProjectUrl = (uuid: string) => {
+ return `/projects/${uuid}`;
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as Tree from './tree';
+
+describe('Tree', () => {
+ let tree: Tree.Tree<string>;
+
+ beforeEach(() => {
+ tree = Tree.createTree();
+ });
+
+ it('sets new node', () => {
+ const newTree = Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' })(tree);
+ expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: [], id: 'Node 1', parent: '', value: 'Value 1' });
+ });
+
+ it('adds new node reference to parent children', () => {
+ const [newTree] = [tree]
+ .map(Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' }))
+ .map(Tree.setNode({ children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' }));
+
+ expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: ['Node 2'], id: 'Node 1', parent: '', value: 'Value 1' });
+ });
+
+ it('gets node ancestors', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 2', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeAncestors('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']);
+ });
+
+ it('gets node descendants', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeDescendants('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+ });
+
+ it('gets root descendants', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeDescendants('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+ });
+
+ it('gets node children', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeChildren('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']);
+ });
+
+ it('gets root children', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeChildren('')(newTree)).toEqual(['Node 1', 'Node 3']);
+ });
+
+ it('maps tree', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' },
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ const mappedTree = Tree.mapTreeValues<string, number>(value => parseInt(value.split(' ')[1], 10))(newTree);
+ expect(Tree.getNode('Node 2')(mappedTree)).toEqual({ children: [], id: 'Node 2', parent: 'Node 1', value: 2 }, );
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type Tree<T> = Record<string, TreeNode<T>>;
+
+export const TREE_ROOT_ID = '';
+
+export interface TreeNode<T> {
+ children: string[];
+ value: T;
+ id: string;
+ parent: string;
+}
+
+export const createTree = <T>(): Tree<T> => ({});
+
+export const getNode = (id: string) => <T>(tree: Tree<T>): TreeNode<T> | undefined => tree[id];
+
+export const setNode = <T>(node: TreeNode<T>) => (tree: Tree<T>): Tree<T> => {
+ const [newTree] = [tree]
+ .map(tree => getNode(node.id)(tree) === node
+ ? tree
+ : {...tree, [node.id]: node})
+ .map(addChild(node.parent, node.id));
+ return newTree;
+};
+
+export const getNodeValue = (id: string) => <T>(tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node ? node.value : undefined;
+};
+
+export const setNodeValue = (id: string) => <T>(value: T) => (tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node
+ ? setNode(mapNodeValue(() => value)(node))(tree)
+ : tree;
+};
+
+export const setNodeValueWith = <T>(mapFn: (value: T) => T) => (id: string) => (tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node
+ ? setNode(mapNodeValue(mapFn)(node))(tree)
+ : tree;
+};
+
+export const mapTreeValues = <T, R>(mapFn: (value: T) => R) => (tree: Tree<T>): Tree<R> =>
+ getNodeDescendants('')(tree)
+ .map(id => getNode(id)(tree))
+ .map(mapNodeValue(mapFn))
+ .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const mapTree = <T, R>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
+ getNodeDescendants('')(tree)
+ .map(id => getNode(id)(tree))
+ .map(mapFn)
+ .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>): string[] => {
+ const node = getNode(id)(tree);
+ return node && node.parent
+ ? [...getNodeAncestors(node.parent)(tree), node.parent]
+ : [];
+};
+
+export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
+ const node = getNode(id)(tree);
+ const children = node ? node.children :
+ id === TREE_ROOT_ID
+ ? getRootNodeChildren(tree)
+ : [];
+
+ return children
+ .concat(limit < 1
+ ? []
+ : children
+ .map(id => getNodeDescendants(id, limit - 1)(tree))
+ .reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], []));
+};
+
+export const getNodeChildren = (id: string) => <T>(tree: Tree<T>): string[] =>
+ getNodeDescendants(id, 0)(tree);
+
+const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
+ ({ ...node, value: mapFn(node.value) });
+
+const getRootNodeChildren = <T>(tree: Tree<T>) =>
+ Object
+ .keys(tree)
+ .filter(id => getNode(id)(tree)!.parent === TREE_ROOT_ID);
+
+const addChild = (parentId: string, childId: string) => <T>(tree: Tree<T>): Tree<T> => {
+ const node = getNode(parentId)(tree);
+ if (node) {
+ const children = node.children.some(id => id === childId)
+ ? node.children
+ : [...node.children, childId];
+
+ const newNode = children === node.children
+ ? node
+ : { ...node, children };
+
+ return setNode(newNode)(tree);
+ }
+ return tree;
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { API_HOST } from "../../common/api/server-api";
import { User } from "../../models/user";
import { AxiosInstance } from "../../../node_modules/axios";
export class AuthService {
constructor(
- protected authClient: AxiosInstance,
- protected apiClient: AxiosInstance) { }
+ protected apiClient: AxiosInstance,
+ protected baseUrl: string) { }
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
public login() {
const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
- window.location.assign(`${this.authClient.defaults.baseURL || ""}/login?return_to=${currentUrl}`);
+ window.location.assign(`${this.baseUrl || ""}/login?return_to=${currentUrl}`);
}
public logout() {
const currentUrl = `${window.location.protocol}//${window.location.host}`;
- window.location.assign(`${this.authClient.defaults.baseURL || ""}/logout?return_to=${currentUrl}`);
+ window.location.assign(`${this.baseUrl || ""}/logout?return_to=${currentUrl}`);
}
public getUserDetails = (): Promise<User> => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionService } from "../collection-service/collection-service";
+import { parseKeepManifestText } from "./collection-manifest-parser";
+import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
+
+export class CollectionFilesService {
+
+ constructor(private collectionService: CollectionService) { }
+
+ getFiles(collectionUuid: string) {
+ return this.collectionService
+ .get(collectionUuid)
+ .then(collection =>
+ mapManifestToCollectionFilesTree(
+ parseKeepManifestText(
+ collection.manifestText
+ )
+ )
+ );
+ }
+
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { parseKeepManifestText } from "./collection-manifest-parser";
+import { mapManifestToFiles, mapManifestToDirectories } from "./collection-manifest-mapper";
+
+test('mapManifestToFiles', () => {
+ const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d`;
+ const manifest = parseKeepManifestText(manifestText);
+ const files = mapManifestToFiles(manifest);
+ expect(files).toEqual([{
+ parentId: '',
+ id: '/a',
+ name: 'a',
+ size: 0,
+ type: 'file'
+ }, {
+ parentId: '',
+ id: '/b',
+ name: 'b',
+ size: 0,
+ type: 'file'
+ }, {
+ parentId: '',
+ id: '/output.txt',
+ name: 'output.txt',
+ size: 33,
+ type: 'file'
+ }, {
+ parentId: '/c',
+ id: '/c/d',
+ name: 'd',
+ size: 0,
+ type: 'file'
+ },]);
+});
+
+test('mapManifestToDirectories', () => {
+ const manifestText = `./c/user/results 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n`;
+ const manifest = parseKeepManifestText(manifestText);
+ const directories = mapManifestToDirectories(manifest);
+ expect(directories).toEqual([{
+ parentId: "",
+ id: '/c',
+ name: 'c',
+ type: 'directory'
+ }, {
+ parentId: '/c',
+ id: '/c/user',
+ name: 'user',
+ type: 'directory'
+ }, {
+ parentId: '/c/user',
+ id: '/c/user/results',
+ name: 'results',
+ type: 'directory'
+ },]);
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { uniqBy } from 'lodash';
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../models/keep-manifest";
+import { TreeNode, setNode, createTree } from '../../models/tree';
+import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile } from '../../models/collection-file';
+
+export const mapManifestToCollectionFilesTree = (manifest: KeepManifest): CollectionFilesTree =>
+ manifestToCollectionFiles(manifest)
+ .map(mapCollectionFileToTreeNode)
+ .reduce((tree, node) => setNode(node)(tree), createTree<CollectionFile>());
+
+
+export const mapCollectionFileToTreeNode = (file: CollectionFile): TreeNode<CollectionFile> => ({
+ children: [],
+ id: file.id,
+ parent: file.parentId,
+ value: file
+});
+
+export const manifestToCollectionFiles = (manifest: KeepManifest): Array<CollectionDirectory | CollectionFile> => ([
+ ...mapManifestToDirectories(manifest),
+ ...mapManifestToFiles(manifest)
+]);
+
+export const mapManifestToDirectories = (manifest: KeepManifest): CollectionDirectory[] =>
+ uniqBy(
+ manifest
+ .map(mapStreamDirectory)
+ .map(splitDirectory)
+ .reduce((all, splitted) => ([...all, ...splitted]), []),
+ directory => directory.id);
+
+export const mapManifestToFiles = (manifest: KeepManifest): CollectionFile[] =>
+ manifest
+ .map(stream => stream.files.map(mapStreamFile(stream)))
+ .reduce((all, current) => ([...all, ...current]), []);
+
+const splitDirectory = (directory: CollectionDirectory): CollectionDirectory[] => {
+ return directory.name
+ .split('/')
+ .slice(1)
+ .map(mapPathComponentToDirectory);
+};
+
+const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionDirectory =>
+ createCollectionDirectory({
+ parentId: index === 0 ? '' : joinPathComponents(components, index),
+ id: joinPathComponents(components, index + 1),
+ name: component,
+ });
+
+const joinPathComponents = (components: string[], index: number) =>
+ `/${components.slice(0, index).join('/')}`;
+
+const mapStreamDirectory = (stream: KeepManifestStream): CollectionDirectory =>
+ createCollectionDirectory({
+ parentId: '',
+ id: stream.name,
+ name: stream.name,
+ });
+
+const mapStreamFile = (stream: KeepManifestStream) =>
+ (file: KeepManifestStreamFile): CollectionFile =>
+ createCollectionFile({
+ parentId: stream.name,
+ id: `${stream.name}/${file.name}`,
+ name: file.name,
+ size: file.size,
+ });
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { parseKeepManifestText, parseKeepManifestStream } from "./collection-manifest-parser";
+
+describe('parseKeepManifestText', () => {
+ it('should parse text into streams', () => {
+ const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d\n`;
+ const manifest = parseKeepManifestText(manifestText);
+ expect(manifest[0].name).toBe('');
+ expect(manifest[1].name).toBe('/c');
+ expect(manifest.length).toBe(2);
+ });
+});
+
+describe('parseKeepManifestStream', () => {
+ const streamText = './c 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt';
+ const stream = parseKeepManifestStream(streamText);
+
+ it('should parse stream name', () => {
+ expect(stream.name).toBe('/c');
+ });
+ it('should parse stream locators', () => {
+ expect(stream.locators).toEqual(['930625b054ce894ac40596c3f5a0d947+33']);
+ });
+ it('should parse stream files', () => {
+ expect(stream.files).toEqual([
+ {name: 'a', position: '0', size: 0},
+ {name: 'b', position: '0', size: 0},
+ {name: 'output.txt', position: '0', size: 33},
+ ]);
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { KeepManifestStream, KeepManifestStreamFile } from "../../models/keep-manifest";
+
+/**
+ * Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
+ */
+export const parseKeepManifestText = (text: string) =>
+ text
+ .split(/\n/)
+ .filter(streamText => streamText.length > 0)
+ .map(parseKeepManifestStream);
+
+/**
+ * Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
+ */
+export const parseKeepManifestStream = (stream: string): KeepManifestStream => {
+ const tokens = stream.split(' ');
+ return {
+ name: streamName(tokens),
+ locators: locators(tokens),
+ files: files(tokens)
+ };
+};
+
+const FILE_LOCATOR_REGEXP = /^([0-9a-f]{32})\+([0-9]+)(\+[A-Z][-A-Za-z0-9@_]*)*$/;
+
+const FILE_REGEXP = /([0-9]+):([0-9]+):(.*)/;
+
+const streamName = (tokens: string[]) => tokens[0].slice(1);
+
+const locators = (tokens: string[]) => tokens.filter(isFileLocator);
+
+const files = (tokens: string[]) => tokens.filter(isFile).map(parseFile);
+
+const isFileLocator = (token: string) => FILE_LOCATOR_REGEXP.test(token);
+
+const isFile = (token: string) => FILE_REGEXP.test(token);
+
+const parseFile = (token: string): KeepManifestStreamFile => {
+ const match = FILE_REGEXP.exec(token);
+ const [position, size, name] = match!.slice(1);
+ return { name, position, size: parseInt(size, 10) };
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CommonResourceService } from "../../common/api/common-resource-service";
+import { CollectionResource } from "../../models/collection";
+import { AxiosInstance } from "axios";
+
+export class CollectionService extends CommonResourceService<CollectionResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "collections");
+ }
+}
\ No newline at end of file
import { AuthService } from "./auth-service/auth-service";
import { GroupsService } from "./groups-service/groups-service";
-import { authClient, apiClient } from "../common/api/server-api";
import { ProjectService } from "./project-service/project-service";
import { LinkService } from "./link-service/link-service";
import { FavoriteService } from "./favorite-service/favorite-service";
+import { AxiosInstance } from "axios";
+import { CollectionService } from "./collection-service/collection-service";
+import Axios from "axios";
+import { CollectionFilesService } from "./collection-files-service/collection-files-service";
-export const authService = new AuthService(authClient, apiClient);
-export const groupsService = new GroupsService(apiClient);
-export const projectService = new ProjectService(apiClient);
-export const linkService = new LinkService(apiClient);
-export const favoriteService = new FavoriteService(linkService, groupsService);
+export interface ServiceRepository {
+ apiClient: AxiosInstance;
+
+ authService: AuthService;
+ groupsService: GroupsService;
+ projectService: ProjectService;
+ linkService: LinkService;
+ favoriteService: FavoriteService;
+ collectionService: CollectionService;
+ collectionFilesService: CollectionFilesService;
+}
+
+export const createServices = (baseUrl: string): ServiceRepository => {
+ const apiClient = Axios.create();
+ apiClient.defaults.baseURL = `${baseUrl}/arvados/v1`;
+
+ const authService = new AuthService(apiClient, baseUrl);
+ const groupsService = new GroupsService(apiClient);
+ const projectService = new ProjectService(apiClient);
+ const linkService = new LinkService(apiClient);
+ const favoriteService = new FavoriteService(linkService, groupsService);
+ const collectionService = new CollectionService(apiClient);
+ const collectionFilesService = new CollectionFilesService(collectionService);
+
+ return {
+ apiClient,
+ authService,
+ groupsService,
+ projectService,
+ linkService,
+ favoriteService,
+ collectionService,
+ collectionFilesService
+ };
+};
import { ofType, default as unionize, UnionOf } from "unionize";
import { Dispatch } from "redux";
-import { authService } from "../../services/services";
import { User } from "../../models/user";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
+import { AxiosInstance } from "axios";
export const authActions = unionize({
SAVE_API_TOKEN: ofType<string>(),
LOGIN: {},
LOGOUT: {},
- INIT: {},
+ INIT: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
}, {
value: 'payload'
});
-export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
+function setAuthorizationHeader(client: AxiosInstance, token: string) {
+ client.defaults.headers.common = {
+ Authorization: `OAuth2 ${token}`
+ };
+}
+
+function removeAuthorizationHeader(client: AxiosInstance) {
+ delete client.defaults.headers.common.Authorization;
+}
+
+export const initAuth = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const user = services.authService.getUser();
+ const token = services.authService.getApiToken();
+ if (token) {
+ setAuthorizationHeader(services.apiClient, token);
+ }
+ if (token && user) {
+ dispatch(authActions.INIT({ user, token }));
+ }
+};
+
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.saveApiToken(token);
+ setAuthorizationHeader(services.apiClient, token);
+ dispatch(authActions.SAVE_API_TOKEN(token));
+};
+
+export const login = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.login();
+ dispatch(authActions.LOGIN());
+};
+
+export const logout = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.removeApiToken();
+ services.authService.removeUser();
+ removeAuthorizationHeader(services.apiClient);
+ services.authService.logout();
+ dispatch(authActions.LOGOUT());
+};
+
+export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
dispatch(authActions.USER_DETAILS_REQUEST());
- return authService.getUserDetails().then(details => {
- dispatch(authActions.USER_DETAILS_SUCCESS(details));
- return details;
+ return services.authService.getUserDetails().then(user => {
+ services.authService.saveUser(user);
+ dispatch(authActions.USER_DETAILS_SUCCESS(user));
+ return user;
});
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, initAuth } from "./auth-action";
+import {
+ API_TOKEN_KEY,
+ USER_EMAIL_KEY,
+ USER_FIRST_NAME_KEY,
+ USER_LAST_NAME_KEY,
+ USER_OWNER_UUID_KEY,
+ USER_UUID_KEY
+} from "../../services/auth-service/auth-service";
+
+import 'jest-localstorage-mock';
+import { createServices } from "../../services/services";
+import { configureStore, RootStore } from "../store";
+import createBrowserHistory from "history/createBrowserHistory";
+
+describe('auth-actions', () => {
+ let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+ let store: RootStore;
+
+ beforeEach(() => {
+ store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
+ localStorage.clear();
+ reducer = authReducer(createServices("/arvados/v1"));
+ });
+
+ it('should initialise state with user and api token from local storage', () => {
+
+ localStorage.setItem(API_TOKEN_KEY, "token");
+ localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
+ localStorage.setItem(USER_FIRST_NAME_KEY, "John");
+ localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
+ localStorage.setItem(USER_UUID_KEY, "uuid");
+ localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
+
+ store.dispatch(initAuth());
+
+ expect(store.getState().auth).toEqual({
+ apiToken: "token",
+ user: {
+ email: "test@test.com",
+ firstName: "John",
+ lastName: "Doe",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid"
+ }
+ });
+ });
+
+ // TODO: Add remaining action tests
+ /*
+ it('should fire external url to login', () => {
+ const initialState = undefined;
+ window.location.assign = jest.fn();
+ reducer(initialState, authActions.LOGIN());
+ expect(window.location.assign).toBeCalledWith(
+ `/login?return_to=${window.location.protocol}//${window.location.host}/token`
+ );
+ });
+
+ it('should fire external url to logout', () => {
+ const initialState = undefined;
+ window.location.assign = jest.fn();
+ reducer(initialState, authActions.LOGOUT());
+ expect(window.location.assign).toBeCalledWith(
+ `/logout?return_to=${location.protocol}//${location.host}`
+ );
+ });
+ */
+});
//
// SPDX-License-Identifier: AGPL-3.0
-import { authReducer } from "./auth-reducer";
-import { authActions } from "./auth-action";
-import {
- API_TOKEN_KEY,
- USER_EMAIL_KEY,
- USER_FIRST_NAME_KEY,
- USER_LAST_NAME_KEY,
- USER_OWNER_UUID_KEY,
- USER_UUID_KEY
-} from "../../services/auth-service/auth-service";
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, authActions } from "./auth-action";
import 'jest-localstorage-mock';
+import { createServices } from "../../services/services";
describe('auth-reducer', () => {
+ let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+
beforeAll(() => {
localStorage.clear();
+ reducer = authReducer(createServices("/arvados/v1"));
});
- it('should return default state on initialisation', () => {
- const initialState = undefined;
- const state = authReducer(initialState, authActions.INIT());
- expect(state).toEqual({
- apiToken: undefined,
- user: undefined
- });
- });
-
- it('should read user and api token from local storage on init if they are there', () => {
+ it('should correctly initialise state', () => {
const initialState = undefined;
-
- localStorage.setItem(API_TOKEN_KEY, "token");
- localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
- localStorage.setItem(USER_FIRST_NAME_KEY, "John");
- localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
- localStorage.setItem(USER_UUID_KEY, "uuid");
- localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
-
- const state = authReducer(initialState, authActions.INIT());
+ const user = {
+ email: "test@test.com",
+ firstName: "John",
+ lastName: "Doe",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid"
+ };
+ const state = reducer(initialState, authActions.INIT({user, token: "token"}));
expect(state).toEqual({
apiToken: "token",
- user: {
- email: "test@test.com",
- firstName: "John",
- lastName: "Doe",
- uuid: "uuid",
- ownerUuid: "ownerUuid"
- }
+ user
});
});
- it('should store token in local storage', () => {
+ it('should save api token', () => {
const initialState = undefined;
- const state = authReducer(initialState, authActions.SAVE_API_TOKEN("token"));
+ const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
expect(state).toEqual({
apiToken: "token",
user: undefined
});
-
- expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
});
it('should set user details on success fetch', () => {
ownerUuid: "ownerUuid"
};
- const state = authReducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
+ const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
expect(state).toEqual({
apiToken: undefined,
user: {
ownerUuid: "ownerUuid",
}
});
-
- expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
- });
-
- it('should fire external url to login', () => {
- const initialState = undefined;
- window.location.assign = jest.fn();
- authReducer(initialState, authActions.LOGIN());
- expect(window.location.assign).toBeCalledWith(
- `/login?return_to=${window.location.protocol}//${window.location.host}/token`
- );
- });
-
- it('should fire external url to logout', () => {
- const initialState = undefined;
- window.location.assign = jest.fn();
- authReducer(initialState, authActions.LOGOUT());
- expect(window.location.assign).toBeCalledWith(
- `/logout?return_to=${location.protocol}//${location.host}`
- );
});
});
import { authActions, AuthAction } from "./auth-action";
import { User } from "../../models/user";
-import { authService } from "../../services/services";
-import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
+import { ServiceRepository } from "../../services/services";
export interface AuthState {
user?: User;
apiToken?: string;
}
-export const authReducer = (state: AuthState = {}, action: AuthAction) => {
+export const authReducer = (services: ServiceRepository) => (state: AuthState = {}, action: AuthAction) => {
return authActions.match(action, {
SAVE_API_TOKEN: (token: string) => {
- authService.saveApiToken(token);
- setServerApiAuthorizationHeader(token);
return {...state, apiToken: token};
},
- INIT: () => {
- const user = authService.getUser();
- const token = authService.getApiToken();
- if (token) {
- setServerApiAuthorizationHeader(token);
- }
- return {user, apiToken: token};
+ INIT: ({ user, token }) => {
+ return { user, apiToken: token };
},
LOGIN: () => {
- authService.login();
return state;
},
LOGOUT: () => {
- authService.removeApiToken();
- authService.removeUser();
- removeServerApiAuthorizationHeader();
- authService.logout();
return {...state, apiToken: undefined};
},
USER_DETAILS_SUCCESS: (user: User) => {
- authService.saveUser(user);
return {...state, user};
},
default: () => state
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "unionize";
+import { Dispatch } from "redux";
+import { ResourceKind } from "../../models/resource";
+import { CollectionResource } from "../../models/collection";
+import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
+import { createTree } from "../../models/tree";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
+
+export const collectionPanelActions = unionize({
+ LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
+ LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>()
+}, { tag: 'type', value: 'payload' });
+
+export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
+
+export const loadCollection = (uuid: string, kind: ResourceKind) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
+ return services.collectionService
+ .get(uuid)
+ .then(item => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
+ return services.collectionFilesService.getFiles(item.uuid);
+ })
+ .then(files => {
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+ });
+ };
+
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { CollectionFilesTree } from "../../../models/collection-file";
+
+export const collectionPanelFilesAction = unionize({
+ SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
+ TOGGLE_COLLECTION_FILE_COLLAPSE: ofType<{ id: string }>(),
+ TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
+ SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+ UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+}, { tag: 'type', value: 'payload' });
+
+export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionPanelFilesReducer } from "./collection-panel-files-reducer";
+import { collectionPanelFilesAction } from "./collection-panel-files-actions";
+import { CollectionFile, CollectionDirectory, createCollectionFile, createCollectionDirectory } from "../../../models/collection-file";
+import { createTree, setNode, getNodeValue, mapTreeValues, Tree } from "../../../models/tree";
+import { CollectionPanelFile, CollectionPanelDirectory } from "./collection-panel-files-state";
+
+describe('CollectionPanelFilesReducer', () => {
+
+ const files: Array<CollectionFile | CollectionDirectory> = [
+ createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+ createCollectionDirectory({ id: 'Directory 2', name: 'Directory 2', parentId: 'Directory 1' }),
+ createCollectionDirectory({ id: 'Directory 3', name: 'Directory 3', parentId: '' }),
+ createCollectionDirectory({ id: 'Directory 4', name: 'Directory 4', parentId: 'Directory 3' }),
+ createCollectionFile({ id: 'file1.txt', name: 'file1.txt', parentId: 'Directory 2' }),
+ createCollectionFile({ id: 'file2.txt', name: 'file2.txt', parentId: 'Directory 2' }),
+ createCollectionFile({ id: 'file3.txt', name: 'file3.txt', parentId: 'Directory 3' }),
+ createCollectionFile({ id: 'file4.txt', name: 'file4.txt', parentId: 'Directory 3' }),
+ createCollectionFile({ id: 'file5.txt', name: 'file5.txt', parentId: 'Directory 4' }),
+ ];
+
+ const collectionFilesTree = files.reduce((tree, file) => setNode({
+ children: [],
+ id: file.id,
+ parent: file.parentId,
+ value: file
+ })(tree), createTree<CollectionFile | CollectionDirectory>());
+
+ const collectionPanelFilesTree = collectionPanelFilesReducer(
+ createTree<CollectionPanelFile | CollectionPanelDirectory>(),
+ collectionPanelFilesAction.SET_COLLECTION_FILES(collectionFilesTree));
+
+ it('SET_COLLECTION_FILES', () => {
+ expect(getNodeValue('Directory 1')(collectionPanelFilesTree)).toEqual({
+ ...createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+ collapsed: true,
+ selected: false
+ });
+ });
+
+ it('TOGGLE_COLLECTION_FILE_COLLAPSE', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id: 'Directory 3' }));
+
+ const value = getNodeValue('Directory 3')(newTree)! as CollectionPanelDirectory;
+ expect(value.collapsed).toBe(false);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 3' }));
+
+ const value = getNodeValue('Directory 3')(newTree);
+ expect(value!.selected).toBe(true);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION ancestors', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' }));
+
+ const value = getNodeValue('Directory 1')(newTree);
+ expect(value!.selected).toBe(true);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION descendants', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' }));
+ expect(getNodeValue('file1.txt')(newTree)!.selected).toBe(true);
+ expect(getNodeValue('file2.txt')(newTree)!.selected).toBe(true);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION unselect ancestors', () => {
+ const [newTree] = [collectionPanelFilesTree]
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' })))
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'file1.txt' })));
+
+ expect(getNodeValue('Directory 2')(newTree)!.selected).toBe(false);
+ });
+
+ it('SELECT_ALL_COLLECTION_FILES', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
+
+ mapTreeValues((v: CollectionPanelFile | CollectionPanelDirectory) => {
+ expect(v.selected).toEqual(true);
+ return v;
+ })(newTree);
+ });
+
+ it('SELECT_ALL_COLLECTION_FILES', () => {
+ const [newTree] = [collectionPanelFilesTree]
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES()))
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES()));
+
+ mapTreeValues((v: CollectionPanelFile | CollectionPanelDirectory) => {
+ expect(v.selected).toEqual(false);
+ return v;
+ })(newTree);
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } from "./collection-panel-files-state";
+import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
+import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "../../../models/tree";
+import { CollectionFileType } from "../../../models/collection-file";
+
+export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
+ return collectionPanelFilesAction.match(action, {
+ SET_COLLECTION_FILES: files =>
+ mapTree(mapCollectionFileToCollectionPanelFile)(files),
+
+ TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
+ toggleCollapse(data.id)(state),
+
+ TOGGLE_COLLECTION_FILE_SELECTION: data => [state]
+ .map(toggleSelected(data.id))
+ .map(toggleAncestors(data.id))
+ .map(toggleDescendants(data.id))[0],
+
+ SELECT_ALL_COLLECTION_FILES: () =>
+ mapTreeValues(v => ({ ...v, selected: true }))(state),
+
+ UNSELECT_ALL_COLLECTION_FILES: () =>
+ mapTreeValues(v => ({ ...v, selected: false }))(state),
+
+ default: () => state
+ }) as CollectionPanelFilesState;
+};
+
+const toggleCollapse = (id: string) => (tree: CollectionPanelFilesState) =>
+ setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) =>
+ v.type === CollectionFileType.DIRECTORY
+ ? { ...v, collapsed: !v.collapsed }
+ : v)(id)(tree);
+
+
+const toggleSelected = (id: string) => (tree: CollectionPanelFilesState) =>
+ setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) => ({ ...v, selected: !v.selected }))(id)(tree);
+
+
+const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
+ const node = getNode(id)(tree);
+ if (node && node.value.type === CollectionFileType.DIRECTORY) {
+ return getNodeDescendants(id)(tree)
+ .reduce((newTree, id) =>
+ setNodeValueWith(v => ({ ...v, selected: node.value.selected }))(id)(newTree), tree);
+ }
+ return tree;
+};
+
+const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
+ const ancestors = getNodeAncestors(id)(tree).reverse();
+ return ancestors.reduce((newTree, parent) => parent ? toggleParentNode(parent)(newTree) : newTree, tree);
+};
+
+const toggleParentNode = (id: string) => (tree: CollectionPanelFilesState) => {
+ const node = getNode(id)(tree);
+ if (node) {
+ const parentNode = getNode(node.id)(tree);
+ if (parentNode) {
+ const selected = parentNode.children
+ .map(id => getNode(id)(tree))
+ .every(node => node !== undefined && node.value.selected);
+ return setNodeValueWith(v => ({ ...v, selected }))(parentNode.id)(tree);
+ }
+ return setNode(node)(tree);
+ }
+ return tree;
+};
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '../../../models/collection-file';
+import { Tree, TreeNode } from '../../../models/tree';
+
+export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
+
+export interface CollectionPanelDirectory extends CollectionDirectory {
+ collapsed: boolean;
+ selected: boolean;
+}
+
+export interface CollectionPanelFile extends CollectionFile {
+ selected: boolean;
+}
+
+export const mapCollectionFileToCollectionPanelFile = (node: TreeNode<CollectionDirectory | CollectionFile>): TreeNode<CollectionPanelDirectory | CollectionPanelFile> => {
+ return {
+ ...node,
+ value: node.value.type === CollectionFileType.DIRECTORY
+ ? { ...node.value, selected: false, collapsed: true }
+ : { ...node.value, selected: false }
+ };
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionPanelActions, CollectionPanelAction } from "./collection-panel-action";
+import { CollectionResource } from "../../models/collection";
+
+export interface CollectionPanelState {
+ item: CollectionResource | null;
+}
+
+const initialState = {
+ item: null
+};
+
+export const collectionPanelReducer = (state: CollectionPanelState = initialState, action: CollectionPanelAction) =>
+ collectionPanelActions.match(action, {
+ default: () => state,
+ LOAD_COLLECTION: () => state,
+ LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { combineReducers } from 'redux';
+import * as creator from "./creator/collection-creator-reducer";
+import * as updator from "./updator/collection-updator-reducer";
+
+export type CollectionsState = {
+ creator: creator.CollectionCreatorState;
+ updator: updator.CollectionUpdatorState;
+};
+
+export const collectionsReducer = combineReducers({
+ creator: creator.collectionCreationReducer,
+ updator: updator.collectionCreationReducer
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { Dispatch } from "redux";
+
+import { RootState } from "../../store";
+import { CollectionResource } from '../../../models/collection';
+import { ServiceRepository } from "../../../services/services";
+
+export const collectionCreateActions = unionize({
+ OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
+ CLOSE_COLLECTION_CREATOR: ofType<{}>(),
+ CREATE_COLLECTION: ofType<{}>(),
+ CREATE_COLLECTION_SUCCESS: ofType<{}>(),
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+export const createCollection = (collection: Partial<CollectionResource>) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { ownerUuid } = getState().collections.creator;
+ const collectiontData = { ownerUuid, ...collection };
+ dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
+ return services.collectionService
+ .create(collectiontData)
+ .then(collection => dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection)));
+ };
+
+export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionCreationReducer } from "./collection-creator-reducer";
+import { collectionCreateActions } from "./collection-creator-action";
+
+describe('collection-reducer', () => {
+
+ it('should open collection creator dialog', () => {
+ const initialState = { opened: false, ownerUuid: "" };
+ const collection = { opened: true, ownerUuid: "" };
+
+ const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
+ expect(state).toEqual(collection);
+ });
+
+ it('should close collection creator dialog', () => {
+ const initialState = { opened: true, ownerUuid: "" };
+ const collection = { opened: false, ownerUuid: "" };
+
+ const state = collectionCreationReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
+ expect(state).toEqual(collection);
+ });
+
+ it('should reset collection creator dialog props', () => {
+ const initialState = { opened: true, ownerUuid: "test" };
+ const collection = { opened: false, ownerUuid: "" };
+
+ const state = collectionCreationReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
+ expect(state).toEqual(collection);
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action';
+
+export type CollectionCreatorState = CollectionCreator;
+
+interface CollectionCreator {
+ opened: boolean;
+ ownerUuid: string;
+}
+
+const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreator>) => ({
+ ...state,
+ ...creator
+});
+
+const initialState: CollectionCreatorState = {
+ opened: false,
+ ownerUuid: ''
+};
+
+export const collectionCreationReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
+ return collectionCreateActions.match(action, {
+ OPEN_COLLECTION_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
+ CLOSE_COLLECTION_CREATOR: () => updateCreator(state, { opened: false }),
+ CREATE_COLLECTION: () => updateCreator(state),
+ CREATE_COLLECTION_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
+ default: () => state
+ });
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { Dispatch } from "redux";
+
+import { RootState } from "../../store";
+import { ServiceRepository } from "../../../services/services";
+import { CollectionResource } from '../../../models/collection';
+import { initialize } from 'redux-form';
+import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
+
+export const collectionUpdatorActions = unionize({
+ OPEN_COLLECTION_UPDATOR: ofType<{ uuid: string }>(),
+ CLOSE_COLLECTION_UPDATOR: ofType<{}>(),
+ UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+
+export const COLLECTION_FORM_NAME = 'collectionEditDialog';
+
+export const openUpdator = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(collectionUpdatorActions.OPEN_COLLECTION_UPDATOR({ uuid }));
+ const item = getState().collectionPanel.item;
+ if(item) {
+ dispatch(initialize(COLLECTION_FORM_NAME, { name: item.name, description: item.description }));
+ }
+ };
+
+export const updateCollection = (collection: Partial<CollectionResource>) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { uuid } = getState().collections.updator;
+ return services.collectionService
+ .update(uuid, collection)
+ .then(collection => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
+ dispatch(collectionUpdatorActions.UPDATE_COLLECTION_SUCCESS());
+ }
+ );
+ };
+
+export type CollectionUpdatorAction = UnionOf<typeof collectionUpdatorActions>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionUpdatorActions, CollectionUpdatorAction } from './collection-updator-action';
+
+export type CollectionUpdatorState = CollectionUpdator;
+
+interface CollectionUpdator {
+ opened: boolean;
+ uuid: string;
+}
+
+const updateCollection = (state: CollectionUpdatorState, updator?: Partial<CollectionUpdator>) => ({
+ ...state,
+ ...updator
+});
+
+const initialState: CollectionUpdatorState = {
+ opened: false,
+ uuid: ''
+};
+
+export const collectionCreationReducer = (state: CollectionUpdatorState = initialState, action: CollectionUpdatorAction) => {
+ return collectionUpdatorActions.match(action, {
+ OPEN_COLLECTION_UPDATOR: ({ uuid }) => updateCollection(state, { uuid, opened: true }),
+ CLOSE_COLLECTION_UPDATOR: () => updateCollection(state, { opened: false }),
+ UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, uuid: "" }),
+ default: () => state
+ });
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { ResourceKind } from "../../models/resource";
import { contextMenuActions, ContextMenuAction } from "./context-menu-actions";
export interface ContextMenuState {
}, { tag: "type", value: "payload" });
export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
+
+export const bindDataExplorerActions = (id: string) => ({
+ RESET_PAGINATION: () =>
+ dataExplorerActions.RESET_PAGINATION({ id }),
+ REQUEST_ITEMS: () =>
+ dataExplorerActions.REQUEST_ITEMS({ id }),
+ SET_COLUMNS: (payload: { columns: DataColumns<any> }) =>
+ dataExplorerActions.SET_COLUMNS({ ...payload, id }),
+ SET_FILTERS: (payload: { columnName: string, filters: DataTableFilterItem[] }) =>
+ dataExplorerActions.SET_FILTERS({ ...payload, id }),
+ SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
+ dataExplorerActions.SET_ITEMS({ ...payload, id }),
+ SET_PAGE: (payload: { page: number }) =>
+ dataExplorerActions.SET_PAGE({ ...payload, id }),
+ SET_ROWS_PER_PAGE: (payload: { rowsPerPage: number }) =>
+ dataExplorerActions.SET_ROWS_PER_PAGE({ ...payload, id }),
+ TOGGLE_COLUMN: (payload: { columnName: string }) =>
+ dataExplorerActions.TOGGLE_COLUMN({ ...payload, id }),
+ TOGGLE_SORT: (payload: { columnName: string }) =>
+ dataExplorerActions.TOGGLE_SORT({ ...payload, id }),
+ SET_SEARCH_VALUE: (payload: { searchValue: string }) =>
+ dataExplorerActions.SET_SEARCH_VALUE({ ...payload, id }),
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, MiddlewareAPI } from "redux";
+import { RootState } from "../store";
+
+export abstract class DataExplorerMiddlewareService {
+ protected readonly id: string;
+
+ protected constructor(id: string) {
+ this.id = id;
+ }
+
+ public getId() {
+ return this.id;
+ }
+
+ abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
+import { dataExplorerMiddleware } from "./data-explorer-middleware";
+import { MiddlewareAPI } from "redux";
+import { DataColumns } from "../../components/data-table/data-table";
+import { dataExplorerActions } from "./data-explorer-action";
+
+
+describe("DataExplorerMiddleware", () => {
+
+ it("handles only actions that are identified by service id", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [{
+ name: "Column",
+ selected: true,
+ configurable: false,
+ render: jest.fn()
+ }],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
+ middleware(dataExplorerActions.SET_PAGE({ id: "ServiceId", page: 0 }));
+ middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
+ expect(api.dispatch).toHaveBeenCalledWith(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId" }));
+ expect(api.dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles REQUEST_ITEMS action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [{
+ name: "Column",
+ selected: true,
+ configurable: false,
+ render: jest.fn()
+ }],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId" }));
+ expect(config.requestItems).toHaveBeenCalled();
+ });
+
+ it("handles SET_PAGE action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_PAGE({ id: service.getId(), page: 0 }));
+ expect(api.dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles SET_ROWS_PER_PAGE action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_ROWS_PER_PAGE({ id: service.getId(), rowsPerPage: 0 }));
+ expect(api.dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles SET_FILTERS action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_FILTERS({ id: service.getId(), columnName: "", filters: [] }));
+ expect(api.dispatch).toHaveBeenCalledTimes(2);
+ });
+
+ it("handles SET_ROWS_PER_PAGE action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_ROWS_PER_PAGE({ id: service.getId(), rowsPerPage: 0 }));
+ expect(api.dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles TOGGLE_SORT action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.TOGGLE_SORT({ id: service.getId(), columnName: "" }));
+ expect(api.dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles SET_SEARCH_VALUE action", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_SEARCH_VALUE({ id: service.getId(), searchValue: "" }));
+ expect(api.dispatch).toHaveBeenCalledTimes(2);
+ });
+
+ it("forwards other actions", () => {
+ const config = {
+ id: "ServiceId",
+ columns: [],
+ requestItems: jest.fn(),
+ setApi: jest.fn()
+ };
+ const service = new ServiceMock(config);
+ const api = {
+ getState: jest.fn(),
+ dispatch: jest.fn()
+ };
+ const next = jest.fn();
+ const middleware = dataExplorerMiddleware(service)(api)(next);
+ middleware(dataExplorerActions.SET_COLUMNS({ id: service.getId(), columns: [] }));
+ middleware(dataExplorerActions.SET_ITEMS({ id: service.getId(), items: [], rowsPerPage: 0, itemsAvailable: 0, page: 0 }));
+ middleware(dataExplorerActions.TOGGLE_COLUMN({ id: service.getId(), columnName: "" }));
+ expect(api.dispatch).toHaveBeenCalledTimes(0);
+ expect(next).toHaveBeenCalledTimes(3);
+ });
+
+});
+
+class ServiceMock extends DataExplorerMiddlewareService {
+ constructor(private config: {
+ id: string,
+ columns: DataColumns<any>,
+ requestItems: (api: MiddlewareAPI) => void
+ }) {
+ super(config.id);
+ }
+
+ getColumns() {
+ return this.config.columns;
+ }
+
+ requestItems(api: MiddlewareAPI) {
+ this.config.requestItems(api);
+ }
+}
--- /dev/null
+
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Middleware } from "redux";
+import { dataExplorerActions, bindDataExplorerActions } from "./data-explorer-action";
+import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
+
+export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService): Middleware => api => next => {
+ const handleAction = <T extends { id: string }>(handler: (data: T) => void) =>
+ (data: T) => {
+ if (data.id === service.getId()) {
+ handler(data);
+ }
+ };
+ const actions = bindDataExplorerActions(service.getId());
+
+ return action => {
+ dataExplorerActions.match(action, {
+ SET_PAGE: handleAction(() => {
+ api.dispatch(actions.REQUEST_ITEMS());
+ }),
+ SET_ROWS_PER_PAGE: handleAction(() => {
+ api.dispatch(actions.REQUEST_ITEMS());
+ }),
+ SET_FILTERS: handleAction(() => {
+ api.dispatch(actions.RESET_PAGINATION());
+ api.dispatch(actions.REQUEST_ITEMS());
+ }),
+ TOGGLE_SORT: handleAction(() => {
+ api.dispatch(actions.REQUEST_ITEMS());
+ }),
+ SET_SEARCH_VALUE: handleAction(() => {
+ api.dispatch(actions.RESET_PAGINATION());
+ api.dispatch(actions.REQUEST_ITEMS());
+ }),
+ REQUEST_ITEMS: handleAction(() => {
+ service.requestItems(api);
+ }),
+ default: () => next(action)
+ });
+ };
+};
import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
import { DataColumns } from "../../components/data-table/data-table";
-interface DataExplorer {
+export interface DataExplorer {
columns: DataColumns<any>;
items: any[];
itemsAvailable: number;
page: number;
rowsPerPage: number;
- rowsPerPageOptions?: number[];
+ rowsPerPageOptions: number[];
searchValue: string;
}
searchValue: ""
};
-export type DataExplorerState = Record<string, DataExplorer | undefined>;
+export type DataExplorerState = Record<string, DataExplorer>;
export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
dataExplorerActions.match(action, {
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "unionize";
-import { CommonResourceService } from "../../common/api/common-resource-service";
import { Dispatch } from "redux";
-import { apiClient } from "../../common/api/server-api";
import { Resource, ResourceKind } from "../../models/resource";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
export const loadDetails = (uuid: string, kind: ResourceKind) =>
- (dispatch: Dispatch) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
- getService(kind)
- .get(uuid)
- .then(project => {
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
- });
+ const item = await getService(services, kind).get(uuid);
+ dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
};
-const getService = (kind: ResourceKind) => {
+const getService = (services: ServiceRepository, kind: ResourceKind) => {
switch (kind) {
case ResourceKind.PROJECT:
- return new CommonResourceService(apiClient, "groups");
+ return services.projectService;
case ResourceKind.COLLECTION:
- return new CommonResourceService(apiClient, "collections");
+ return services.collectionService;
default:
- return new CommonResourceService(apiClient, "");
+ return services.projectService;
}
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+
+export const dialogActions = unionize({
+ OPEN_DIALOG: ofType<{ id: string, data: any }>(),
+ CLOSE_DIALOG: ofType<{ id: string }>()
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+export type DialogAction = UnionOf<typeof dialogActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogReducer } from "./dialog-reducer";
+import { dialogActions } from "./dialog-actions";
+
+describe('DialogReducer', () => {
+ it('OPEN_DIALOG', () => {
+ const id = 'test id';
+ const data = 'test data';
+ const state = dialogReducer({}, dialogActions.OPEN_DIALOG({ id, data }));
+ expect(state[id]).toEqual({ open: true, data });
+ });
+
+ it('CLOSE_DIALOG', () => {
+ const id = 'test id';
+ const state = dialogReducer({}, dialogActions.CLOSE_DIALOG({ id }));
+ expect(state[id]).toEqual({ open: false });
+ });
+
+ it('CLOSE_DIALOG persist data', () => {
+ const id = 'test id';
+ const [newState] = [{}]
+ .map(state => dialogReducer(state, dialogActions.OPEN_DIALOG({ id, data: 'test data' })))
+ .map(state => dialogReducer(state, dialogActions.CLOSE_DIALOG({ id })));
+
+ expect(newState[id]).toEqual({ open: false, data: 'test data' });
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DialogAction, dialogActions } from "./dialog-actions";
+
+export type DialogState = Record<string, Dialog>;
+
+export interface Dialog {
+ open: boolean;
+ data?: any;
+}
+
+export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
+ dialogActions.match(action, {
+ OPEN_DIALOG: ({ id, data }) => ({ ...state, [id]: { open: true, data } }),
+ CLOSE_DIALOG: ({ id }) => ({
+ ...state,
+ [id]: state[id] ? { ...state[id], open: false } : { open: false } }),
+ default: () => state,
+ });
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { DialogState } from './dialog-reducer';
+import { Dispatch } from 'redux';
+import { dialogActions } from './dialog-actions';
+
+export type WithDialog<T> = {
+ open: boolean;
+ data?: T;
+};
+
+export type WithDialogActions = {
+ closeDialog: () => void;
+};
+
+export const withDialog = (id: string) =>
+ <T>(component: React.ComponentType<WithDialog<T> & WithDialogActions>) =>
+ connect(mapStateToProps(id), mapDispatchToProps(id))(component);
+
+export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialog<T> => {
+ const dialog = state.dialog[id];
+ return dialog ? dialog : { open: false };
+};
+
+export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogActions => ({
+ closeDialog: () => {
+ dispatch(dialogActions.CLOSE_DIALOG({ id }));
+ }
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+
+export const FAVORITE_PANEL_ID = "favoritePanel";
+export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { FavoritePanelFilter, FavoritePanelColumnNames } from "../../views/favorite-panel/favorite-panel";
+import { RootState } from "../store";
+import { DataColumns } from "../../components/data-table/data-table";
+import { FavoritePanelItem, resourceToDataItem } from "../../views/favorite-panel/favorite-panel-item";
+import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
+import { ServiceRepository } from "../../services/services";
+import { SortDirection } from "../../components/data-table/data-column";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { LinkResource } from "../../models/link";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { favoritePanelActions } from "./favorite-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+
+export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const dataExplorer = api.getState().dataExplorer[this.getId()];
+ const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
+ const sortColumn = dataExplorer.columns.find(
+ ({ sortDirection }) => sortDirection !== undefined && sortDirection !== "none"
+ );
+ const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const order = FavoriteOrderBuilder.create();
+ if (typeFilters.length > 0) {
+ this.services.favoriteService
+ .list(this.services.authService.getUuid()!, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ order: sortColumn!.name === FavoritePanelColumnNames.NAME
+ ? sortColumn!.sortDirection === SortDirection.ASC
+ ? order.addDesc("name")
+ : order.addAsc("name")
+ : order,
+ filters: FilterBuilder
+ .create<LinkResource>()
+ .addIsA("headUuid", typeFilters.map(filter => filter.type))
+ .addILike("name", dataExplorer.searchValue)
+ })
+ .then(response => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ });
+ } else {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ }
+ }
+}
+
+const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
+ const column = columns.find(c => c.name === columnName);
+ return column && column.filters ? column.filters.filter(f => f.selected) : [];
+};
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Middleware } from "redux";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
-import { favoriteService } from "../../services/services";
-import { RootState } from "../store";
-import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { DataColumns } from "../../components/data-table/data-table";
-import {
- columns,
- FAVORITE_PANEL_ID,
- FavoritePanelColumnNames,
- FavoritePanelFilter
-} from "../../views/favorite-panel/favorite-panel";
-import { FavoritePanelItem, resourceToDataItem } from "../../views/favorite-panel/favorite-panel-item";
-import { LinkResource } from "../../models/link";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { OrderBuilder } from "../../common/api/order-builder";
-import { SortDirection } from "../../components/data-table/data-column";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
-import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
-
-export const favoritePanelMiddleware: Middleware = store => next => {
- next(dataExplorerActions.SET_COLUMNS({ id: FAVORITE_PANEL_ID, columns }));
-
- return action => {
-
- const handlePanelAction = <T extends { id: string }>(handler: (data: T) => void) =>
- (data: T) => {
- next(action);
- if (data.id === FAVORITE_PANEL_ID) {
- handler(data);
- }
- };
-
- dataExplorerActions.match(action, {
- SET_PAGE: handlePanelAction(() => {
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
- }),
- SET_ROWS_PER_PAGE: handlePanelAction(() => {
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
- }),
- SET_FILTERS: handlePanelAction(() => {
- store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: FAVORITE_PANEL_ID }));
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
- }),
- TOGGLE_SORT: handlePanelAction(() => {
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
- }),
- SET_SEARCH_VALUE: handlePanelAction(() => {
- store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: FAVORITE_PANEL_ID }));
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
- }),
- REQUEST_ITEMS: handlePanelAction(() => {
- const state = store.getState() as RootState;
- const dataExplorer = getDataExplorer(state.dataExplorer, FAVORITE_PANEL_ID);
- const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
- const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
- const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
- const order = FavoriteOrderBuilder.create();
- if (typeFilters.length > 0) {
- favoriteService
- .list(state.projects.currentItemId, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: sortColumn!.name === FavoritePanelColumnNames.NAME
- ? sortColumn!.sortDirection === SortDirection.ASC
- ? order.addDesc("name")
- : order.addAsc("name")
- : order,
- filters: FilterBuilder
- .create<LinkResource>()
- .addIsA("headUuid", typeFilters.map(filter => filter.type))
- .addILike("name", dataExplorer.searchValue)
- })
- .then(response => {
- store.dispatch(dataExplorerActions.SET_ITEMS({
- id: FAVORITE_PANEL_ID,
- items: response.items.map(resourceToDataItem),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- store.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- });
- } else {
- store.dispatch(dataExplorerActions.SET_ITEMS({
- id: FAVORITE_PANEL_ID,
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- }
- }),
- default: () => next(action)
- });
- };
-};
-
-const getOrder = (direction: SortDirection) => {
- const order = OrderBuilder.create<LinkResource>();
- const addRule = (builder: OrderBuilder<GroupContentsResource | LinkResource>, direction: SortDirection) =>
- direction === SortDirection.ASC
- ? builder.addAsc("name")
- : builder.addDesc("name");
-
- return [
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)
- ].reduce((acc, b) =>
- acc.concat(addRule(b, direction)), addRule(OrderBuilder.create(), direction));
-};
-
-const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
- const column = columns.find(c => c.name === columnName);
- return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
-
-
-
import { unionize, ofType, UnionOf } from "unionize";
import { Dispatch } from "redux";
-import { favoriteService } from "../../services/services";
import { RootState } from "../store";
import { checkFavorite } from "./favorites-reducer";
import { snackbarActions } from "../snackbar/snackbar-actions";
+import { ServiceRepository } from "../../services/services";
export const favoritesActions = unionize({
TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
export type FavoritesAction = UnionOf<typeof favoritesActions>;
export const toggleFavorite = (resource: { uuid: string; name: string }) =>
- (dispatch: Dispatch, getState: () => RootState): Promise<any> => {
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
const isFavorite = checkFavorite(resource.uuid, getState().favorites);
const promise: any = isFavorite
- ? favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
- : favoriteService.create({ userUuid, resource });
+ ? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
+ : services.favoriteService.create({ userUuid, resource });
return promise
.then(() => {
};
export const checkPresenceInFavorites = (resourceUuids: string[]) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
- favoriteService
+ services.favoriteService
.checkPresenceInFavorites(userUuid, resourceUuids)
- .then(results => {
+ .then((results: any) => {
dispatch(favoritesActions.UPDATE_FAVORITES(results));
});
};
import { push } from "react-router-redux";
import { TreeItemStatus } from "../../components/tree/tree";
import { findTreeItem } from "../project/project-reducer";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
import { RootState } from "../store";
import { Resource, ResourceKind } from "../../models/resource";
+import { projectPanelActions } from "../project-panel/project-panel-action";
+import { getCollectionUrl } from "../../models/collection";
+import { getProjectUrl } from "../../models/project";
export const getResourceUrl = <T extends Resource>(resource: T): string => {
switch (resource.kind) {
- case ResourceKind.PROJECT: return `/projects/${resource.uuid}`;
- case ResourceKind.COLLECTION: return `/collections/${resource.uuid}`;
+ case ResourceKind.PROJECT: return getProjectUrl(resource.uuid);
+ case ResourceKind.COLLECTION: return getCollectionUrl(resource.uuid);
default: return resource.href;
}
};
if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
}
- dispatch(dataExplorerActions.RESET_PAGINATION({id: PROJECT_PANEL_ID}));
- dispatch(dataExplorerActions.REQUEST_ITEMS({id: PROJECT_PANEL_ID}));
+ dispatch(projectPanelActions.RESET_PAGINATION());
+ dispatch(projectPanelActions.REQUEST_ITEMS());
}));
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+
+export const PROJECT_PANEL_ID = "projectPanel";
+export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { ProjectPanelColumnNames, ProjectPanelFilter } from "../../views/project-panel/project-panel";
+import { RootState } from "../store";
+import { DataColumns } from "../../components/data-table/data-table";
+import { ServiceRepository } from "../../services/services";
+import { ProjectPanelItem, resourceToDataItem } from "../../views/project-panel/project-panel-item";
+import { SortDirection } from "../../components/data-table/data-column";
+import { OrderBuilder } from "../../common/api/order-builder";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { ProcessResource } from "../../models/process";
+import { GroupContentsResourcePrefix, GroupContentsResource } from "../../services/groups-service/groups-service";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { projectPanelActions } from "./project-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+
+export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const state = api.getState();
+ const dataExplorer = state.dataExplorer[this.getId()];
+ 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) {
+ this.services.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 => {
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ });
+ } else {
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ }
+ }
+}
+
+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();
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Middleware } from "redux";
-import { dataExplorerActions } from "../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";
-import { getDataExplorer } from "../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 { OrderBuilder } from "../../common/api/order-builder";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
-import { SortDirection } from "../../components/data-table/data-column";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-
-export const projectPanelMiddleware: Middleware = store => next => {
- next(dataExplorerActions.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);
- }
- };
-
- dataExplorerActions.match(action, {
- SET_PAGE: handleProjectPanelAction(() => {
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
- }),
- SET_ROWS_PER_PAGE: handleProjectPanelAction(() => {
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
- }),
- SET_FILTERS: handleProjectPanelAction(() => {
- store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
- }),
- TOGGLE_SORT: handleProjectPanelAction(() => {
- store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
- }),
- SET_SEARCH_VALUE: handleProjectPanelAction(() => {
- store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
- store.dispatch(dataExplorerActions.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(dataExplorerActions.SET_ITEMS({
- id: PROJECT_PANEL_ID,
- items: response.items.map(resourceToDataItem),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- store.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- });
- } else {
- store.dispatch(dataExplorerActions.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 { default as unionize, ofType, UnionOf } from "unionize";
import { ProjectResource } from "../../models/project";
-import { projectService } from "../../services/services";
import { Dispatch } from "redux";
import { FilterBuilder } from "../../common/api/filter-builder";
import { RootState } from "../store";
import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { ServiceRepository } from "../../services/services";
export const projectActions = unionize({
OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
value: 'payload'
});
-export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState) => {
+export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
- return projectService.list({
+ return services.projectService.list({
filters: FilterBuilder
.create<ProjectResource>()
.addEqual("ownerUuid", parentUuid)
};
export const createProject = (project: Partial<ProjectResource>) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { ownerUuid } = getState().projects.creator;
const projectData = { ownerUuid, ...project };
dispatch(projectActions.CREATE_PROJECT(projectData));
- return projectService
+ return services.projectService
.create(projectData)
.then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
};
creator: {
opened: false,
ownerUuid: "",
- pending: false
}
});
});
status: TreeItemStatus.PENDING
}],
currentItemId: "1",
- creator: { opened: false, pending: false, ownerUuid: "" },
+ creator: { opened: false, ownerUuid: "" },
};
const project = {
items: [{
status: TreeItemStatus.PENDING
}],
currentItemId: "",
- creator: { opened: false, pending: false, ownerUuid: "" },
+ creator: { opened: false, ownerUuid: "" },
};
const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
status: TreeItemStatus.PENDING
}],
currentItemId: "1",
- creator: { opened: false, pending: false, ownerUuid: "" }
+ creator: { opened: false, ownerUuid: "" }
};
const project = {
items: [{
status: TreeItemStatus.PENDING,
}],
currentItemId: "1",
- creator: { opened: false, pending: false, ownerUuid: "" },
+ creator: { opened: false, ownerUuid: "" },
};
const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
status: TreeItemStatus.PENDING,
}],
currentItemId: "1",
- creator: { opened: false, pending: false, ownerUuid: "" }
+ creator: { opened: false, ownerUuid: "" }
};
const project = {
items: [{
status: TreeItemStatus.PENDING,
}],
currentItemId: "1",
- creator: { opened: false, pending: false, ownerUuid: "" },
+ creator: { opened: false, ownerUuid: "" },
};
const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
interface ProjectCreator {
opened: boolean;
- pending: boolean;
ownerUuid: string;
error?: string;
}
currentItemId: "",
creator: {
opened: false,
- pending: false,
ownerUuid: ""
}
};
export const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
return projectActions.match(action, {
- OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true, pending: false }),
+ OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
CREATE_PROJECT: () => updateCreator(state, { error: undefined }),
CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
import { sidePanelActions, SidePanelAction } from './side-panel-action';
import { SidePanelItem } from '../../components/side-panel/side-panel';
import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "../../components/icon/icon";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
import { Dispatch } from "redux";
-import { FAVORITE_PANEL_ID } from "../../views/favorite-panel/favorite-panel";
import { push } from "react-router-redux";
+import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
export type SidePanelState = SidePanelItem[];
active: false,
activeAction: (dispatch: Dispatch) => {
dispatch(push("/favorites"));
- dispatch(dataExplorerActions.RESET_PAGINATION({id: FAVORITE_PANEL_ID}));
- dispatch(dataExplorerActions.REQUEST_ITEMS({id: FAVORITE_PANEL_ID}));
+ dispatch(favoritePanelActions.RESET_PAGINATION());
+ dispatch(favoritePanelActions.REQUEST_ITEMS());
}
},
{
//
// SPDX-License-Identifier: AGPL-3.0
-import { createStore, applyMiddleware, compose, Middleware, combineReducers } from 'redux';
+import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
import { authReducer, AuthState } from "./auth/auth-reducer";
import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
-import { favoritePanelMiddleware } from "./favorite-panel/favorite-panel-middleware";
import { reducer as formReducer } from 'redux-form';
import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
+import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
+import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
+import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
+import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
+import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
+import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
+import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
+import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { DialogState, dialogReducer } from './dialog/dialog-reducer';
+import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
+import { ServiceRepository } from "../services/services";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
export interface RootState {
auth: AuthState;
projects: ProjectState;
+ collections: CollectionsState;
router: RouterState;
dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
+ collectionPanel: CollectionPanelState;
detailsPanel: DetailsPanelState;
contextMenu: ContextMenuState;
favorites: FavoritesState;
snackbar: SnackbarState;
+ collectionPanelFiles: CollectionPanelFilesState;
+ dialog: DialogState;
}
-const rootReducer = combineReducers({
- auth: authReducer,
- projects: projectsReducer,
- router: routerReducer,
- dataExplorer: dataExplorerReducer,
- sidePanel: sidePanelReducer,
- detailsPanel: detailsPanelReducer,
- contextMenu: contextMenuReducer,
- form: formReducer,
- favorites: favoritesReducer,
- snackbar: snackbarReducer,
-});
-
-
-export function configureStore(history: History) {
+export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
+
+export function configureStore(history: History, services: ServiceRepository): RootStore {
+ const rootReducer = combineReducers({
+ auth: authReducer(services),
+ projects: projectsReducer,
+ collections: collectionsReducer,
+ router: routerReducer,
+ dataExplorer: dataExplorerReducer,
+ sidePanel: sidePanelReducer,
+ collectionPanel: collectionPanelReducer,
+ detailsPanel: detailsPanelReducer,
+ contextMenu: contextMenuReducer,
+ form: formReducer,
+ favorites: favoritesReducer,
+ snackbar: snackbarReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ dialog: dialogReducer
+ });
+
+ const projectPanelMiddleware = dataExplorerMiddleware(
+ new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
+ );
+ const favoritePanelMiddleware = dataExplorerMiddleware(
+ new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
+ );
+
const middlewares: Middleware[] = [
routerMiddleware(history),
- thunkMiddleware,
+ thunkMiddleware.withExtraArgument(services),
projectPanelMiddleware,
favoritePanelMiddleware
];
export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
+export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
import { Redirect, RouteProps } from "react-router";
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
-import { authActions, getUserDetails } from "../../store/auth/auth-action";
-import { authService } from "../../services/services";
+import { getUserDetails, saveApiToken } from "../../store/auth/auth-action";
import { getProjectList } from "../../store/project/project-action";
import { getUrlParameter } from "../../common/url";
+import { AuthService } from "../../services/auth-service/auth-service";
interface ApiTokenProps {
+ authService: AuthService;
}
export const ApiToken = connect()(
componentDidMount() {
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
- this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+ this.props.dispatch(saveApiToken(apiToken));
this.props.dispatch<any>(getUserDetails()).then(() => {
- const rootUuid = authService.getRootUuid();
+ const rootUuid = this.props.authService.getRootUuid();
this.props.dispatch(getProjectList(rootUuid));
});
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "../../components/collection-panel-files/collection-panel-files";
+import { RootState } from "../../store/store";
+import { TreeItemStatus, TreeItem } from "../../components/tree/tree";
+import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "../../store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { FileTreeData } from "../../components/file-tree/file-tree-data";
+import { Dispatch } from "redux";
+import { collectionPanelFilesAction } from "../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
+import { ContextMenuKind } from "../context-menu/context-menu";
+import { Tree, getNodeChildren, getNode } from "../../models/tree";
+import { CollectionFileType } from "../../models/collection-file";
+
+const memoizedMapStateToProps = () => {
+ let prevState: CollectionPanelFilesState;
+ let prevTree: Array<TreeItem<FileTreeData>>;
+
+ return (state: RootState): Pick<CollectionPanelFilesProps, "items"> => {
+ if (prevState !== state.collectionPanelFiles) {
+ prevState = state.collectionPanelFiles;
+ prevTree = getNodeChildren('')(state.collectionPanelFiles)
+ .map(collectionItemToTreeItem(state.collectionPanelFiles));
+ }
+ return {
+ items: prevTree
+ };
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+ onUploadDataClick: () => { return; },
+ onCollapseToggle: (id) => {
+ dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
+ },
+ onSelectionToggle: (event, item) => {
+ dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
+ },
+ onItemMenuOpen: (event, item) => {
+ event.preventDefault();
+ dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource: { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }
+ }));
+ },
+ onOptionsMenuOpen: (event) =>
+ dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource: { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }
+ }))
+});
+
+
+export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
+
+const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
+ (id: string): TreeItem<FileTreeData> => {
+ const node = getNode(id)(tree) || {
+ id: '',
+ children: [],
+ parent: '',
+ value: {
+ name: 'Invalid node',
+ type: CollectionFileType.DIRECTORY,
+ selected: false,
+ collapsed: true
+ }
+ };
+ return {
+ active: false,
+ data: {
+ name: node.value.name,
+ size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
+ type: node.value.type
+ },
+ id: node.id,
+ items: getNodeChildren(node.id)(tree)
+ .map(collectionItemToTreeItem(tree)),
+ open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
+ selected: node.value.selected,
+ status: TreeItemStatus.LOADED
+ };
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { toggleFavorite } from "../../../store/favorites/favorites-actions";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "../../../components/icon/icon";
+import { openUpdator } from "../../../store/collections/updator/collection-updator-action";
+import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
+
+export const collectionActionSet: ContextMenuActionSet = [[
+ {
+ icon: RenameIcon,
+ name: "Edit collection",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openUpdator(resource.uuid));
+ }
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleFavorite(resource)).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ });
+ }
+ },
+ {
+ icon: CopyIcon,
+ name: "Copy to project",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: ProvenanceGraphIcon,
+ name: "Provenance graph",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: AdvancedIcon,
+ name: "Advanced",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: RemoveIcon,
+ name: "Remove",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ }
+]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { collectionPanelFilesAction } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+
+
+export const collectionFilesActionSet: ContextMenuActionSet = [[{
+ name: "Select all",
+ execute: (dispatch) => {
+ dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
+ }
+},{
+ name: "Unselect all",
+ execute: (dispatch) => {
+ dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES());
+ }
+},{
+ name: "Remove selected",
+ execute: (dispatch, resource) => {
+ dispatch(openRemoveDialog('selected files'));
+ }
+},{
+ name: "Download selected",
+ execute: (dispatch, resource) => {
+ return;
+ }
+},{
+ name: "Create a new collection with selected",
+ execute: (dispatch, resource) => {
+ return;
+ }
+}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { RenameIcon, DownloadIcon, RemoveIcon } from "../../../components/icon/icon";
+import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+import { openRenameDialog } from "../../rename-dialog/rename-dialog";
+
+
+export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
+ name: "Rename",
+ icon: RenameIcon,
+ execute: (dispatch, resource) => {
+ dispatch(openRenameDialog('the item'));
+ }
+},{
+ name: "Download",
+ icon: DownloadIcon,
+ execute: (dispatch, resource) => {
+ return;
+ }
+},{
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, resource) => {
+ dispatch(openRemoveDialog('selected file'));
+ }
+}]];
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "./favorite-action";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
-import { FAVORITE_PANEL_ID } from "../../../views/favorite-panel/favorite-panel";
+import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
export const favoriteActionSet: ContextMenuActionSet = [[{
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id : FAVORITE_PANEL_ID }));
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
});
}
}]];
import { ContextMenuActionSet } from "../context-menu-action-set";
import { projectActions } from "../../../store/project/project-action";
import { NewProjectIcon } from "../../../components/icon/icon";
-import { ToggleFavoriteAction } from "./favorite-action";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
-import { FAVORITE_PANEL_ID } from "../../../views/favorite-panel/favorite-panel";
+import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
export const projectActionSet: ContextMenuActionSet = [[{
icon: NewProjectIcon,
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id : FAVORITE_PANEL_ID }));
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
});
}
}]];
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "./favorite-action";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "../../../store/favorites/favorites-actions";
export const resourceActionSet: ContextMenuActionSet = [[{
import * as React from "react";
import { ListItemIcon, ListItemText } from "@material-ui/core";
-import { FavoriteIcon, AddFavoriteIcon, RemoveFavoriteIcon } from "../../../components/icon/icon";
+import { AddFavoriteIcon, RemoveFavoriteIcon } from "../../../components/icon/icon";
import { connect } from "react-redux";
import { RootState } from "../../../store/store";
const mapStateToProps = (state: RootState) => ({
- isFavorite: state.contextMenu.resource && state.favorites[state.contextMenu.resource.uuid] === true
+ isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
});
export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
ROOT_PROJECT = "RootProject",
PROJECT = "Project",
RESOURCE = "Resource",
- FAVORITE = "Favorite"
+ FAVORITE = "Favorite",
+ COLLECTION_FILES = "CollectionFiles",
+ COLLECTION_FILES_ITEM = "CollectionFilesItem",
+ COLLECTION = 'Collection'
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+
+import { RootState } from "../../store/store";
+import { DialogCollectionCreate } from "../dialog-create/dialog-collection-create";
+import { collectionCreateActions, createCollection } from "../../store/collections/creator/collection-creator-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+
+const mapStateToProps = (state: RootState) => ({
+ open: state.collections.creator.opened
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ handleClose: () => {
+ dispatch(collectionCreateActions.CLOSE_COLLECTION_CREATOR());
+ },
+ onSubmit: (data: { name: string, description: string }) => {
+ return dispatch<any>(addCollection(data))
+ .catch((e: any) => {
+ throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
+ });
+ }
+});
+
+const addCollection = (data: { name: string, description: string }) =>
+ (dispatch: Dispatch) => {
+ return dispatch<any>(createCollection(data)).then(() => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully created.",
+ hideDuration: 2000
+ }));
+ dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ });
+ };
+
+export const CreateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionCreate);
+
import { RootState } from "../../store/store";
import { DialogProjectCreate } from "../dialog-create/dialog-project-create";
import { projectActions, createProject, getProjectList } from "../../store/project/project-action";
-import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { projectPanelActions } from "../../store/project-panel/project-panel-action";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
const mapStateToProps = (state: RootState) => ({
open: state.projects.creator.opened
(dispatch: Dispatch, getState: () => RootState) => {
const { ownerUuid } = getState().projects.creator;
return dispatch<any>(createProject(data)).then(() => {
- dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Created a new project",
+ hideDuration: 2000
+ }));
+ dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch<any>(getProjectList(ownerUuid));
});
};
import { dataExplorerActions } 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 { DataColumns } from "../../components/data-table/data-table";
interface Props {
id: string;
+ columns: DataColumns<any>;
onRowClick: (item: any) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
onRowDoubleClick: (item: any) => void;
const mapStateToProps = (state: RootState, { id }: Props) =>
getDataExplorer(state.dataExplorer, id);
-const mapDispatchToProps = (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
- onSearch: (searchValue: string) => {
- dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
- },
+const mapDispatchToProps = () => {
+ let prevColumns: DataColumns<any>;
+ return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
+ if (columns !== prevColumns) {
+ prevColumns = columns;
+ dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
+ }
+ return {
+ onSearch: (searchValue: string) => {
+ dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
+ },
- onColumnToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
- },
+ onColumnToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
+ },
- onSortToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
- },
+ onSortToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
+ },
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
- dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
- },
+ onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
+ },
- onChangePage: (page: number) => {
- dispatch(dataExplorerActions.SET_PAGE({ id, page }));
- },
+ onChangePage: (page: number) => {
+ dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+ },
- onChangeRowsPerPage: (rowsPerPage: number) => {
- dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
- },
+ onChangeRowsPerPage: (rowsPerPage: number) => {
+ dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+ },
- onRowClick,
+ onRowClick,
- onRowDoubleClick,
+ onRowDoubleClick,
- onContextMenu,
-});
+ onContextMenu,
+ };
+ };
+};
-export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
+export const DataExplorer = connect(mapStateToProps, mapDispatchToProps())(DataExplorerComponent);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Grid, Typography } from '@material-ui/core';
+import { FavoriteStar } from '../favorite-star/favorite-star';
+import { ResourceKind } from '../../models/resource';
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
+import { formatDate, formatFileSize } from '../../common/formatters';
+import { resourceLabel } from '../../common/labels';
+
+
+export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+ <Grid container alignItems="center" wrap="nowrap" spacing={16}>
+ <Grid item>
+ {renderIcon(item)}
+ </Grid>
+ <Grid item>
+ <Typography color="primary">
+ {item.name}
+ </Typography>
+ </Grid>
+ <Grid item>
+ <Typography variant="caption">
+ <FavoriteStar resourceUuid={item.uuid} />
+ </Typography>
+ </Grid>
+ </Grid>;
+
+
+export const renderIcon = (item: {kind: string}) => {
+ switch (item.kind) {
+ case ResourceKind.PROJECT:
+ return <ProjectIcon />;
+ case ResourceKind.COLLECTION:
+ return <CollectionIcon />;
+ case ResourceKind.PROCESS:
+ return <ProcessIcon />;
+ default:
+ return <DefaultIcon />;
+ }
+};
+
+export const renderDate = (date: string) => {
+ return <Typography noWrap>{formatDate(date)}</Typography>;
+};
+
+export const renderFileSize = (fileSize?: number) =>
+ <Typography noWrap>
+ {formatFileSize(fileSize)}
+ </Typography>;
+
+export const renderOwner = (owner: string) =>
+ <Typography noWrap color="primary" >
+ {owner}
+ </Typography>;
+
+export const renderType = (type: string) =>
+ <Typography noWrap>
+ {resourceLabel(type)}
+ </Typography>;
+
+export const renderStatus = (item: {status?: string}) =>
+ <Typography noWrap align="center" >
+ {item.status || "-"}
+ </Typography>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
+import TextField from '@material-ui/core/TextField';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
+
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ button: {
+ marginLeft: theme.spacing.unit
+ },
+ lastButton: {
+ marginLeft: theme.spacing.unit,
+ marginRight: "20px",
+ },
+ formContainer: {
+ display: "flex",
+ flexDirection: "column",
+ },
+ textField: {
+ marginBottom: theme.spacing.unit * 3
+ },
+ createProgress: {
+ position: "absolute",
+ minWidth: "20px",
+ right: "110px"
+ },
+ dialogActions: {
+ marginBottom: theme.spacing.unit * 3
+ }
+});
+interface DialogCollectionCreateProps {
+ open: boolean;
+ handleClose: () => void;
+ onSubmit: (data: { name: string, description: string }) => void;
+ handleSubmit: any;
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface TextFieldProps {
+ label: string;
+ floatinglabeltext: string;
+ className?: string;
+ input?: string;
+ meta?: any;
+}
+
+export const DialogCollectionCreate = compose(
+ reduxForm({ form: 'collectionCreateDialog' }),
+ withStyles(styles))(
+ class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & WithStyles<CssRules>> {
+ render() {
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
+
+ return (
+ <Dialog
+ open={open}
+ onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}>
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
+ <DialogContent className={classes.formContainer}>
+ <Field name="name"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Collection Name"
+ validate={COLLECTION_NAME_VALIDATION}
+ className={classes.textField}
+ label="Collection Name"/>
+ <Field name="description"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Description - optional"
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ className={classes.textField}
+ label="Description - optional"/>
+ </DialogContent>
+ <DialogActions className={classes.dialogActions}>
+ <Button onClick={handleClose} className={classes.button} color="primary"
+ disabled={submitting}>CANCEL</Button>
+ <Button type="submit"
+ className={classes.lastButton}
+ color="primary"
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ CREATE A COLLECTION
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
+ </DialogActions>
+ </form>
+ </Dialog>
+ );
+ }
+
+ renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+ <TextField
+ helperText={touched && error}
+ label={label}
+ className={this.props.classes.textField}
+ error={touched && !!error}
+ autoComplete='off'
+ {...input}
+ {...custom}
+ />
+ )
+ }
+);
onSubmit: (data: { name: string, description: string }) => void;
handleSubmit: any;
submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
}
interface TextFieldProps {
withStyles(styles))(
class DialogProjectCreate extends React.Component<DialogProjectProps & WithStyles<CssRules>> {
render() {
- const { classes, open, handleClose, handleSubmit, onSubmit, submitting } = this.props;
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
return (
<Dialog
<Button type="submit"
className={classes.lastButton}
color="primary"
- disabled={submitting}
+ disabled={invalid || submitting || pristine}
variant="contained">
CREATE A PROJECT
</Button>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
+
+type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ content: {
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ actions: {
+ margin: 0,
+ padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px
+ ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
+ },
+ textField: {
+ marginBottom: theme.spacing.unit * 3
+ },
+ buttonWrapper: {
+ position: 'relative'
+ },
+ saveButton: {
+ boxShadow: 'none'
+ },
+ circularProgress: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ margin: 'auto'
+ }
+});
+
+interface DialogCollectionDataProps {
+ open: boolean;
+ handleSubmit: any;
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface DialogCollectionAction {
+ handleClose: () => void;
+ onSubmit: (data: { name: string, description: string }) => void;
+}
+
+type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
+
+interface TextFieldProps {
+ label: string;
+ floatinglabeltext: string;
+ className?: string;
+ input?: string;
+ meta?: any;
+}
+
+export const DialogCollectionUpdate = compose(
+ reduxForm({ form: COLLECTION_FORM_NAME }),
+ withStyles(styles))(
+
+ class DialogCollectionUpdate extends React.Component<DialogCollectionProps> {
+
+ render() {
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
+ return (
+ <Dialog open={open}
+ onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}>
+
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <DialogTitle>Edit Collection</DialogTitle>
+ <DialogContent className={classes.content}>
+ <Field name="name"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Collection Name"
+ validate={COLLECTION_NAME_VALIDATION}
+ className={classes.textField}
+ label="Collection Name" />
+ <Field name="description"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Description - optional"
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ className={classes.textField}
+ label="Description - optional" />
+ </DialogContent>
+ <DialogActions className={classes.actions}>
+ <Button onClick={handleClose} color="primary"
+ disabled={submitting}>CANCEL</Button>
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ SAVE
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </DialogActions>
+ </form>
+ </Dialog>
+ );
+ }
+
+ renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+ <TextField
+ helperText={touched && error}
+ label={label}
+ className={this.props.classes.textField}
+ error={touched && !!error}
+ autoComplete='off'
+ {...input}
+ {...custom}
+ />
+ )
+ }
+ );
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@material-ui/core";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+
+export const REMOVE_DIALOG = 'removeCollectionFilesDialog';
+
+export const RemoveDialog = withDialog(REMOVE_DIALOG)(
+ (props) =>
+ <Dialog open={props.open}>
+ <DialogTitle>{`Removing ${props.data}`}</DialogTitle>
+ <DialogContent>
+ {`Are you sure you want to remove ${props.data}?`}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button variant='raised' color='primary'>
+ Remove
+ </Button>
+ </DialogActions>
+ </Dialog>
+);
+
+export const openRemoveDialog = (removedDataName: string) =>
+ dialogActions.OPEN_DIALOG({ id: REMOVE_DIALOG, data: removedDataName });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography } from "@material-ui/core";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+
+export const RENAME_DIALOG = 'nameDialog';
+
+export const RenameDialog = withDialog(RENAME_DIALOG)(
+ (props) =>
+ <Dialog open={props.open}>
+ <DialogTitle>{`Rename`}</DialogTitle>
+ <DialogContent>
+ <Typography variant='body1' gutterBottom>
+ {`Please, enter a new name for ${props.data}`}
+ </Typography>
+ <TextField fullWidth={true} placeholder='New name' />
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button variant='raised' color='primary'>
+ Ok
+ </Button>
+ </DialogActions>
+ </Dialog>
+);
+
+export const openRenameDialog = (originalName: string, ) =>
+ dialogActions.OPEN_DIALOG({ id: RENAME_DIALOG, data: originalName });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+import { RootState } from "../../store/store";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { collectionUpdatorActions, updateCollection } from "../../store/collections/updator/collection-updator-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { DialogCollectionUpdate } from "../dialog-update/dialog-collection-update";
+
+const mapStateToProps = (state: RootState) => ({
+ open: state.collections.updator.opened
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ handleClose: () => {
+ dispatch(collectionUpdatorActions.CLOSE_COLLECTION_UPDATOR());
+ },
+ onSubmit: (data: { name: string, description: string }) => {
+ return dispatch<any>(editCollection(data))
+ .catch((e: any) => {
+ if(e.errors) {
+ throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
+ }
+ });
+ }
+});
+
+const editCollection = (data: { name: string, description: string }) =>
+ (dispatch: Dispatch) => {
+ return dispatch<any>(updateCollection(data)).then(() => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ });
+ };
+
+export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card,
+ CardHeader, IconButton, CardContent, Grid, Chip
+} from '@material-ui/core';
+import { connect } from 'react-redux';
+import { RouteComponentProps } from 'react-router';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { RootState } from '../../store/store';
+import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '../../components/icon/icon';
+import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
+import { CollectionResource } from '../../models/collection';
+import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ marginBottom: '20px'
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.yellow700
+ },
+ tag: {
+ marginRight: theme.spacing.unit
+ },
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ fontSize: '1.125rem',
+ cursor: 'pointer'
+ }
+});
+
+interface CollectionPanelDataProps {
+ item: CollectionResource;
+}
+
+interface CollectionPanelActionProps {
+ onItemRouteChange: (collectionId: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
+}
+
+type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
+
+export const CollectionPanel = withStyles(styles)(
+ connect((state: RootState) => ({ item: state.collectionPanel.item! }))(
+ class extends React.Component<CollectionPanelProps> {
+
+ render() {
+ const { classes, item, onContextMenu } = this.props;
+ return <div>
+ <Card className={classes.card}>
+ <CardHeader
+ avatar={ <CollectionIcon className={classes.iconHeader} /> }
+ action={
+ <IconButton
+ aria-label="More options"
+ onClick={event => onContextMenu(event, item)}>
+ <MoreOptionsIcon />
+ </IconButton>
+ }
+ title={item && item.name }
+ subheader={item && item.description} />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={6}>
+ <DetailsAttribute label='Collection UUID' value={item && item.uuid}>
+ <CopyToClipboard text={item && item.uuid}>
+ <CopyIcon className={classes.copyIcon} />
+ </CopyToClipboard>
+ </DetailsAttribute>
+ <DetailsAttribute label='Content size' value='54 MB' />
+ <DetailsAttribute label='Owner' value={item && item.ownerUuid} />
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+
+ <Card className={classes.card}>
+ <CardHeader title="Tags" />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={4}>
+ <Chip label="Tag 1" className={classes.tag}/>
+ <Chip label="Tag 2" className={classes.tag}/>
+ <Chip label="Tag 3" className={classes.tag}/>
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+ <div className={classes.card}>
+ <CollectionPanelFiles/>
+ </div>
+ </div>;
+ }
+
+ componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
+ if (!item || match.params.id !== item.uuid) {
+ onItemRouteChange(match.params.id);
+ }
+ }
+
+ }
+ )
+);
import * as React from 'react';
import { FavoritePanelItem } from './favorite-panel-item';
-import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { formatDate, formatFileSize } from '../../common/formatters';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { DataExplorer } from "../../views-components/data-explorer/data-explorer";
import { DispatchProp, connect } from 'react-redux';
import { DataColumns } from '../../components/data-table/data-table';
import { SortDirection } from '../../components/data-table/data-column';
import { ResourceKind } from '../../models/resource';
import { resourceLabel } from '../../common/labels';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
import { ArvadosTheme } from '../../common/custom-theme';
-import { FavoriteStar } from "../../views-components/favorite-star/favorite-star";
+import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '../../views-components/data-explorer/renderers';
+import { FAVORITE_PANEL_ID } from "../../store/favorite-panel/favorite-panel-action";
type CssRules = "toolbar" | "button";
},
});
-const renderName = (item: FavoritePanelItem) =>
- <Grid container alignItems="center" wrap="nowrap" spacing={16}>
- <Grid item>
- {renderIcon(item)}
- </Grid>
- <Grid item>
- <Typography color="primary">
- {item.name}
- </Typography>
- </Grid>
- <Grid item>
- <Typography variant="caption">
- <FavoriteStar resourceUuid={item.uuid} />
- </Typography>
- </Grid>
- </Grid>;
-
-
-const renderIcon = (item: FavoritePanelItem) => {
- switch (item.kind) {
- case ResourceKind.PROJECT:
- return <ProjectIcon />;
- case ResourceKind.COLLECTION:
- return <CollectionIcon />;
- case ResourceKind.PROCESS:
- return <ProcessIcon />;
- default:
- return <DefaultIcon />;
- }
-};
-
-const renderDate = (date: string) => {
- return <Typography noWrap>{formatDate(date)}</Typography>;
-};
-
-const renderFileSize = (fileSize?: number) =>
- <Typography noWrap>
- {formatFileSize(fileSize)}
- </Typography>;
-
-const renderOwner = (owner: string) =>
- <Typography noWrap color="primary" >
- {owner}
- </Typography>;
-
-const renderType = (type: string) =>
- <Typography noWrap>
- {resourceLabel(type)}
- </Typography>;
-
-const renderStatus = (item: FavoritePanelItem) =>
- <Typography noWrap align="center" >
- {item.status || "-"}
- </Typography>;
-
export enum FavoritePanelColumnNames {
NAME = "Name",
STATUS = "Status",
{
name: FavoritePanelColumnNames.NAME,
selected: true,
+ configurable: true,
sortDirection: SortDirection.ASC,
render: renderName,
width: "450px"
{
name: "Status",
selected: true,
+ configurable: true,
filters: [
{
name: ContainerRequestState.COMMITTED,
{
name: FavoritePanelColumnNames.TYPE,
selected: true,
+ configurable: true,
filters: [
{
name: resourceLabel(ResourceKind.COLLECTION),
{
name: FavoritePanelColumnNames.OWNER,
selected: true,
+ configurable: true,
render: item => renderOwner(item.owner),
width: "200px"
},
{
name: FavoritePanelColumnNames.FILE_SIZE,
selected: true,
+ configurable: true,
render: item => renderFileSize(item.fileSize),
width: "50px"
},
{
name: FavoritePanelColumnNames.LAST_MODIFIED,
selected: true,
+ configurable: true,
sortDirection: SortDirection.NONE,
render: item => renderDate(item.lastModified),
width: "150px"
}
];
-export const FAVORITE_PANEL_ID = "favoritePanel";
-
interface FavoritePanelDataProps {
currentItemId: string;
}
render() {
return <DataExplorer
id={FAVORITE_PANEL_ID}
+ columns={columns}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
import * as React from 'react';
import { ProjectPanelItem } from './project-panel-item';
-import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { formatDate, formatFileSize } from '../../common/formatters';
+import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { DataExplorer } from "../../views-components/data-explorer/data-explorer";
import { DispatchProp, connect } from 'react-redux';
import { DataColumns } from '../../components/data-table/data-table';
import { SortDirection } from '../../components/data-table/data-column';
import { ResourceKind } from '../../models/resource';
import { resourceLabel } from '../../common/labels';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, FavoriteIcon } from '../../components/icon/icon';
import { ArvadosTheme } from '../../common/custom-theme';
-import { FavoriteStar } from '../../views-components/favorite-star/favorite-star';
+import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '../../views-components/data-explorer/renderers';
type CssRules = "toolbar" | "button";
},
});
-const renderName = (item: ProjectPanelItem) =>
- <Grid container alignItems="center" wrap="nowrap" spacing={16}>
- <Grid item>
- {renderIcon(item)}
- </Grid>
- <Grid item>
- <Typography color="primary">
- {item.name}
- </Typography>
- </Grid>
- <Grid item>
- <Typography variant="caption">
- <FavoriteStar resourceUuid={item.uuid} />
- </Typography>
- </Grid>
- </Grid>;
-
-
-const renderIcon = (item: ProjectPanelItem) => {
- switch (item.kind) {
- case ResourceKind.PROJECT:
- return <ProjectIcon />;
- case ResourceKind.COLLECTION:
- return <CollectionIcon />;
- case ResourceKind.PROCESS:
- return <ProcessIcon />;
- default:
- return <DefaultIcon />;
- }
-};
-
-const renderDate = (date: string) => {
- return <Typography noWrap>{formatDate(date)}</Typography>;
-};
-
-const renderFileSize = (fileSize?: number) =>
- <Typography noWrap>
- {formatFileSize(fileSize)}
- </Typography>;
-
-const renderOwner = (owner: string) =>
- <Typography noWrap color="primary" >
- {owner}
- </Typography>;
-
-const renderType = (type: string) =>
- <Typography noWrap>
- {resourceLabel(type)}
- </Typography>;
-
-const renderStatus = (item: ProjectPanelItem) =>
- <Typography noWrap align="center" >
- {item.status || "-"}
- </Typography>;
-
export enum ProjectPanelColumnNames {
NAME = "Name",
STATUS = "Status",
{
name: ProjectPanelColumnNames.NAME,
selected: true,
+ configurable: true,
sortDirection: SortDirection.ASC,
render: renderName,
width: "450px"
{
name: "Status",
selected: true,
+ configurable: true,
filters: [
{
name: ContainerRequestState.COMMITTED,
{
name: ProjectPanelColumnNames.TYPE,
selected: true,
+ configurable: true,
filters: [
{
name: resourceLabel(ResourceKind.COLLECTION),
{
name: ProjectPanelColumnNames.OWNER,
selected: true,
+ configurable: true,
render: item => renderOwner(item.owner),
width: "200px"
},
{
name: ProjectPanelColumnNames.FILE_SIZE,
selected: true,
+ configurable: true,
render: item => renderFileSize(item.fileSize),
width: "50px"
},
{
name: ProjectPanelColumnNames.LAST_MODIFIED,
selected: true,
+ configurable: true,
sortDirection: SortDirection.NONE,
render: item => renderDate(item.lastModified),
width: "150px"
interface ProjectPanelActionProps {
onItemClick: (item: ProjectPanelItem) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
- onDialogOpen: (ownerUuid: string) => void;
+ onProjectCreationDialogOpen: (ownerUuid: string) => void;
+ onCollectionCreationDialogOpen: (ownerUuid: string) => void;
onItemDoubleClick: (item: ProjectPanelItem) => void;
onItemRouteChange: (itemId: string) => void;
}
const { classes } = this.props;
return <div>
<div className={classes.toolbar}>
- <Button color="primary" variant="raised" className={classes.button}>
+ <Button color="primary" onClick={this.handleNewCollectionClick} variant="raised" className={classes.button}>
Create a collection
</Button>
<Button color="primary" variant="raised" className={classes.button}>
</div>
<DataExplorer
id={PROJECT_PANEL_ID}
+ columns={columns}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
}
handleNewProjectClick = () => {
- this.props.onDialogOpen(this.props.currentItemId);
+ this.props.onProjectCreationDialogOpen(this.props.currentItemId);
+ }
+
+ handleNewCollectionClick = () => {
+ this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
}
componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
if (match.params.id !== currentItemId) {
}
}
)
-);
\ No newline at end of file
+);
import { ConnectedRouter } from "react-router-redux";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from '../../common/custom-theme';
+import { createServices } from "../../services/services";
const history = createBrowserHistory();
it('renders without crashing', () => {
const div = document.createElement('div');
- const store = configureStore(createBrowserHistory());
+ const store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
ReactDOM.render(
<MuiThemeProvider theme={CustomTheme}>
<Provider store={store}>
import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
import { Route, Switch, RouteComponentProps } from "react-router";
-import { authActions } from "../../store/auth/auth-action";
+import { login, logout } from "../../store/auth/auth-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 { SidePanel, SidePanelItem } from '../../components/side-panel/side-panel';
import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
import { projectActions } from "../../store/project/project-action";
+import { collectionCreateActions } from '../../store/collections/creator/collection-creator-action';
import { ProjectPanel } from "../project-panel/project-panel";
import { DetailsPanel } from '../../views-components/details-panel/details-panel';
import { ArvadosTheme } from '../../common/custom-theme';
import { CreateProjectDialog } from "../../views-components/create-project-dialog/create-project-dialog";
-import { authService } from '../../services/services';
import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action";
import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
-import { sidePanelData, SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
+import { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
import { ProjectResource } from '../../models/project';
import { ResourceKind } from '../../models/resource';
import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
-import { FavoritePanel, FAVORITE_PANEL_ID } from "../favorite-panel/favorite-panel";
+import { FavoritePanel } from "../favorite-panel/favorite-panel";
import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog';
import { dataExplorerActions } from '../../store/data-explorer/data-explorer-action';
import { Snackbar } from '../../views-components/snackbar/snackbar';
+import { favoritePanelActions } from '../../store/favorite-panel/favorite-panel-action';
+import { CreateCollectionDialog } from '../../views-components/create-collection-dialog/create-collection-dialog';
+import { CollectionPanel } from '../collection-panel/collection-panel';
+import { loadCollection } from '../../store/collection-panel/collection-panel-action';
+import { getCollectionUrl } from '../../models/collection';
+import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
+import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
+import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
+import { AuthService } from "../../services/auth-service/auth-service";
-const drawerWidth = 240;
-const appBarHeight = 100;
+const DRAWER_WITDH = 240;
+const APP_BAR_HEIGHT = 100;
type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
},
drawerPaper: {
position: 'relative',
- width: drawerWidth,
+ width: DRAWER_WITDH,
display: 'flex',
flexDirection: 'column',
},
display: "flex",
flexGrow: 1,
minWidth: 0,
- paddingTop: appBarHeight
+ paddingTop: APP_BAR_HEIGHT
},
content: {
padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
sidePanelItems: SidePanelItem[];
}
+interface WorkbenchServiceProps {
+ authService: AuthService;
+}
+
interface WorkbenchActionProps {
}
-type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
interface NavBreadcrumb extends Breadcrumb {
itemId: string;
},
{
label: "Logout",
- action: () => this.props.dispatch(authActions.LOGOUT())
+ action: () => this.props.dispatch(logout())
},
{
label: "My account",
anonymousMenu: [
{
label: "Sign in",
- action: () => this.props.dispatch(authActions.LOGIN())
+ action: () => this.props.dispatch(login())
}
]
}
toggleActive={this.toggleSidePanelActive}
sidePanelItems={this.props.sidePanelItems}
onContextMenu={(event) => this.openContextMenu(event, {
- uuid: authService.getUuid() || "",
+ uuid: this.props.authService.getUuid() || "",
name: "",
kind: ContextMenuKind.ROOT_PROJECT
})}>
<ProjectTree
projects={this.props.projects}
- toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
+ toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
onContextMenu={(event, item) => this.openContextMenu(event, {
uuid: item.data.uuid,
name: item.data.name,
kind: ContextMenuKind.PROJECT
})}
toggleActive={itemId => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.PROJECT));
- this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
+ this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
}} />
</SidePanel>
</Drawer>}
<Switch>
<Route path="/projects/:id" render={this.renderProjectPanel} />
<Route path="/favorites" render={this.renderFavoritePanel} />
+ <Route path="/collections/:id" render={this.renderCollectionPanel} />
</Switch>
</div>
{user && <DetailsPanel />}
<ContextMenu />
<Snackbar />
<CreateProjectDialog />
+ <CreateCollectionDialog />
+ <RemoveDialog />
+ <RenameDialog />
+ <UpdateCollectionDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
open={this.state.isCurrentTokenDialogOpen}
);
}
+ renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
+ onItemRouteChange={(collectionId) => this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION))}
+ onContextMenu={(event, item) => {
+ this.openContextMenu(event, {
+ uuid: item.uuid,
+ name: item.name,
+ kind: ContextMenuKind.COLLECTION
+ });
+ }}
+ {...props} />
+
renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
- onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
+ onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
onContextMenu={(event, item) => {
+
const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
this.openContextMenu(event, {
uuid: item.uuid,
kind
});
}}
- onDialogOpen={this.handleCreationDialogOpen}
+ onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
+ onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
onItemClick={item => {
- this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
}}
onItemDoubleClick={item => {
- this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.PROJECT));
+ switch (item.kind) {
+ case ResourceKind.COLLECTION:
+ this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(push(getCollectionUrl(item.uuid)));
+ default:
+ this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
+ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
+ }
+
}}
{...props} />
renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
- onItemRouteChange={() => this.props.dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }))}
+ onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
onContextMenu={(event, item) => {
const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
this.openContextMenu(event, {
kind,
});
}}
- onDialogOpen={this.handleCreationDialogOpen}
+ onDialogOpen={this.handleProjectCreationDialogOpen}
onItemClick={item => {
- this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
}}
onItemDoubleClick={item => {
- this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.PROJECT));
- this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ switch (item.kind) {
+ case ResourceKind.COLLECTION:
+ this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(push(getCollectionUrl(item.uuid)));
+ default:
+ this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
+ this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ }
+
}}
{...props} />
mainAppBarActions: MainAppBarActionProps = {
onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.PROJECT));
+ this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
+ this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
},
onSearch: searchText => {
this.setState({ searchText });
}
}
- handleCreationDialogOpen = (itemUuid: string) => {
+ handleProjectCreationDialogOpen = (itemUuid: string) => {
this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
}
+ handleCollectionCreationDialogOpen = (itemUuid: string) => {
+ this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: itemUuid }));
+ }
+
openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; kind: ContextMenuKind; }) => {
event.preventDefault();
this.props.dispatch(
core-js "^2.5.7"
regenerator-runtime "^0.12.0"
-"@material-ui/core@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.0.tgz#e535fef84576b096c46e1fb7d6c4c61895155fd3"
+"@material-ui/core@1.4.2":
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.2.tgz#8a1282e985d4922a4d2b4f7e287d8a716a2fc108"
dependencies:
"@babel/runtime" "^7.0.0-beta.42"
"@types/jss" "^9.5.3"
jss-vendor-prefixer "^7.0.0"
keycode "^2.1.9"
normalize-scroll-left "^0.1.2"
- popper.js "^1.0.0"
+ popper.js "^1.14.1"
prop-types "^15.6.0"
react-event-listener "^0.6.0"
react-jss "^8.1.0"
react-transition-group "^2.2.1"
recompose "^0.27.0"
- scroll "^2.0.3"
warning "^4.0.1"
-"@material-ui/icons@1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-1.1.0.tgz#4d025df7b0ba6ace8d6710079ed76013a4d26595"
+"@material-ui/icons@2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.0.tgz#f2c4e80d0cb4bbbd433127781da67d93393535f8"
dependencies:
- recompose "^0.26.0 || ^0.27.0"
+ "@babel/runtime" "^7.0.0-beta.42"
+ recompose "^0.27.0"
"@types/cheerio@*":
version "0.22.8"
version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
-"@types/jest@23.3.0":
- version "23.3.0"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.0.tgz#5dd70033b616a6228042244ebd992f6426808810"
+"@types/jest@23.3.1":
+ version "23.3.1"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.1.tgz#a4319aedb071d478e6f407d1c4578ec8156829cf"
"@types/jss@^9.5.3":
version "9.5.4"
csstype "^2.0.0"
indefinite-observable "^1.0.1"
-"@types/lodash@4.14.112":
- version "4.14.112"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.112.tgz#4a8d8e5716b97a1ac01fe1931ad1e4cba719de5a"
+"@types/lodash@4.14.116":
+ version "4.14.116"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
-"@types/node@*", "@types/node@10.5.2":
+"@types/node@*":
version "10.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
+"@types/node@10.5.5":
+ version "10.5.5"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.5.tgz#8e84d24e896cd77b0d4f73df274027e3149ec2ba"
+
+"@types/react-copy-to-clipboard@4.2.5":
+ version "4.2.5"
+ resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+ dependencies:
+ "@types/react" "*"
+
"@types/react-dom@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
"@types/node" "*"
"@types/react" "*"
-"@types/react-redux@6.0.4":
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.4.tgz#c1cfce0a0bd88983c75dbf393576f8dc59181586"
+"@types/react-redux@6.0.6":
+ version "6.0.6"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.6.tgz#87f1d0a6ea901b93fcaf95fa57641ff64079d277"
dependencies:
"@types/react" "*"
redux "^4.0.0"
-"@types/react-router-dom@4.2.7":
- version "4.2.7"
- resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.7.tgz#9d36bfe175f916dd8d7b6b0237feed6cce376b4c"
+"@types/react-router-dom@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.0.tgz#c91796d02deb3a5b24bc1c5db4a255df0d18b8b5"
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react" "*"
redux "^3.6.0"
-"@types/redux-form@7.4.1":
- version "7.4.1"
- resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.1.tgz#df84bbda5f06e4d517210797c3cfdc573c3bda36"
+"@types/redux-form@7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.4.tgz#2cf62b8eb1dc1b1df95b6b25c2763db196e5c190"
dependencies:
"@types/react" "*"
redux "^3.6.0 || ^4.0.0"
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+copy-to-clipboard@^3:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+ dependencies:
+ toggle-selection "^1.0.3"
+
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
dependencies:
urijs "^1.16.1"
-dom-walk@^0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
-
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
is-windows "^1.0.1"
which "^1.2.14"
-global@~4.3.0:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
- dependencies:
- min-document "^2.19.0"
- process "~0.5.1"
-
globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
-min-document@^2.19.0:
- version "2.19.0"
- resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
- dependencies:
- dom-walk "^0.1.0"
-
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
-popper.js@^1.0.0:
- version "1.14.3"
- resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
+popper.js@^1.14.1:
+ version "1.14.4"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6"
portfinder@^1.0.9:
version "1.0.13"
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
-process@~0.5.1:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
-
promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
dependencies:
performance-now "^2.1.0"
-rafl@~1.2.1:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/rafl/-/rafl-1.2.2.tgz#fe930f758211020d47e38815f5196a8be4150740"
- dependencies:
- global "~4.3.0"
-
railroad-diagrams@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-copy-to-clipboard@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
+ dependencies:
+ copy-to-clipboard "^3"
+ prop-types "^15.5.8"
+
react-dev-utils@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
strip-ansi "3.0.1"
text-table "0.2.0"
-react-dom@16.4.1:
- version "16.4.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+react-dom@16.4.2:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.2.tgz#4afed569689f2c561d2b8da0b819669c38a0bda4"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
-react@16.4.1:
- version "16.4.1"
- resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+react@16.4.2:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"
dependencies:
util.promisify "^1.0.0"
-"recompose@^0.26.0 || ^0.27.0", recompose@^0.27.0:
+recompose@^0.27.0:
version "0.27.1"
resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
dependencies:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
-scroll@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/scroll/-/scroll-2.0.3.tgz#0951b785544205fd17753bc3d294738ba16fc2ab"
- dependencies:
- rafl "~1.2.1"
-
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
regex-not "^1.0.2"
safe-regex "^1.1.0"
+toggle-selection@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
toposort@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-typescript@2.9.2:
- version "2.9.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
+typescript@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb"
ua-parser-js@^0.7.18:
version "0.7.18"