#
# SPDX-License-Identifier: AGPL-3.0
+REACT_APP_ARVADOS_CONFIG_URL=/config.json
REACT_APP_ARVADOS_API_HOST=https://qr1hi.arvadosapi.com
\ No newline at end of file
yarn build
</pre>
-### Configuration
+### Build time configuration
You can customize project global variables using env variables. Default values are placed in the `.env` file.
Example:
```
-REACT_APP_ARVADOS_API_HOST=localhost:8000 yarn start
+REACT_APP_ARVADOS_CONFIG_URL=config.json yarn build
+```
+
+### Run time configuration
+The app will fetch runtime configuration when starting. By default it will try to fetch `/config.json`. You can customize this url using build time configuration.
+
+Currently this configuration schema is supported:
+```
+{
+ "API_HOST": "string"
+}
```
### Licensing
export function removeServerApiAuthorizationHeader() {
delete serverApi.defaults.headers.common.Authorization;
}
+
+export const setBaseUrl = (url: string) => {
+ serverApi.defaults.baseURL = url + "/arvados/v1";
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import Axios from "../../node_modules/axios";
+
+export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
+
+export interface Config {
+ API_HOST: string;
+}
+
+const defaultConfig: Config = {
+ API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || ""
+};
+
+export const fetchConfig = () => {
+ return Axios
+ .get<Config>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
+ .then(response => response.data)
+ .catch(() => Promise.resolve(defaultConfig));
+};
+
import * as React from 'react';
import Typography from '@material-ui/core/Typography';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
+import { ArvadosTheme } from '../../common/custom-theme';
interface AttributeDataProps {
label: string;
</Typography>
</Button>
</Tooltip>
- {
- !isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />
- }
+ {!isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />}
</React.Fragment>
);
})
type CssRules = "item" | "currentItem" | "label";
const styles: StyleRulesCallback<CssRules> = theme => {
- const { unit } = theme.spacing;
return {
item: {
opacity: 0.6
import * as Adapter from "enzyme-adapter-react-16";
import ContextMenu from "./context-menu";
import { ListItem } from "@material-ui/core";
+import { IconTypes } from "../icon/icon";
configure({ adapter: new Adapter() });
describe("<ContextMenu />", () => {
- const actions = [[{
- icon: "",
+ const items = [[{
+ icon: IconTypes.ANNOUNCEMENT,
name: "Action 1.1"
}, {
- icon: "",
+ icon: IconTypes.ANNOUNCEMENT,
name: "Action 1.2"
},], [{
- icon: "",
+ icon: IconTypes.ANNOUNCEMENT,
name: "Action 2.1"
}]];
- it("calls onActionClick with clicked action", () => {
- const onActionClick = jest.fn();
+ it("calls onItemClick with clicked action", () => {
+ const onItemClick = jest.fn();
const contextMenu = mount(<ContextMenu
anchorEl={document.createElement("div")}
onClose={jest.fn()}
- onActionClick={onActionClick}
- actions={actions} />);
+ onItemClick={onItemClick}
+ items={items} />);
contextMenu.find(ListItem).at(2).simulate("click");
- expect(onActionClick).toHaveBeenCalledWith(actions[1][0]);
+ expect(onItemClick).toHaveBeenCalledWith(items[1][0]);
});
});
\ No newline at end of file
import * as React from "react";
import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core";
import { DefaultTransformOrigin } from "../popover/helpers";
+import IconBase, { IconTypes } from "../icon/icon";
-export interface ContextMenuAction {
+export interface ContextMenuItem {
name: string;
- icon: string;
- openCreateDialog?: boolean;
+ icon: IconTypes;
}
-export type ContextMenuActionGroup = ContextMenuAction[];
+export type ContextMenuItemGroup = ContextMenuItem[];
-export interface ContextMenuProps<T> {
+export interface ContextMenuProps {
anchorEl?: HTMLElement;
- actions: ContextMenuActionGroup[];
- onActionClick: (action: ContextMenuAction) => void;
+ items: ContextMenuItemGroup[];
+ onItemClick: (action: ContextMenuItem) => void;
onClose: () => void;
}
-export default class ContextMenu<T> extends React.PureComponent<ContextMenuProps<T>> {
+export default class ContextMenu extends React.PureComponent<ContextMenuProps> {
render() {
- const { anchorEl, actions, onClose, onActionClick } = this.props;
+ const { anchorEl, items, onClose, onItemClick } = this.props;
return <Popover
anchorEl={anchorEl}
open={!!anchorEl}
anchorOrigin={DefaultTransformOrigin}
onContextMenu={this.handleContextMenu}>
<List dense>
- {actions.map((group, groupIndex) =>
+ {items.map((group, groupIndex) =>
<React.Fragment key={groupIndex}>
- {group.map((action, actionIndex) =>
+ {group.map((item, actionIndex) =>
<ListItem
button
key={actionIndex}
- onClick={() => onActionClick(action)}>
+ onClick={() => onItemClick(item)}>
<ListItemIcon>
- <i className={action.icon} />
+ <IconBase icon={item.icon} />
</ListItemIcon>
<ListItemText>
- {action.name}
+ {item.name}
</ListItemText>
</ListItem>)}
- {groupIndex < actions.length - 1 && <Divider />}
+ {groupIndex < items.length - 1 && <Divider />}
</React.Fragment>)}
</List>
</Popover>;
import * as React from 'react';
import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, IconButton } from '@material-ui/core';
import MoreVertIcon from "@material-ui/icons/MoreVert";
-import ColumnSelector from "../../components/column-selector/column-selector";
-import DataTable, { DataColumns } from "../../components/data-table/data-table";
-import { DataColumn } from "../../components/data-table/data-column";
-import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
+import ColumnSelector from "../column-selector/column-selector";
+import DataTable, { DataColumns } from "../data-table/data-table";
+import { DataColumn } from "../data-table/data-column";
+import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
import SearchInput from '../search-input/search-input';
interface DataExplorerProps<T> {
import { PopoverOrigin } from "@material-ui/core/Popover";
-export const mockAnchorFromMouseEvent = (event: React.MouseEvent<HTMLElement>) => {
+export const createAnchorAt = (position: {x: number, y: number}) => {
const el = document.createElement('div');
const clientRect = {
- left: event.clientX,
- right: event.clientX,
- top: event.clientY,
- bottom: event.clientY,
+ left: position.x,
+ right: position.x,
+ top: position.y,
+ bottom: position.y,
width: 0,
height: 0
};
import { getProjectList } from "./store/project/project-action";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from './common/custom-theme';
-import CommonResourceService from './common/api/common-resource-service';
-import { CollectionResource } from './models/collection';
-import { serverApi } from './common/api/server-api';
-import { ProcessResource } from './models/process';
-
-const history = createBrowserHistory();
-
-const store = configureStore(history);
-
-store.dispatch(authActions.INIT());
-store.dispatch<any>(getProjectList(authService.getUuid()));
-
-// const service = new CommonResourceService<CollectionResource>(serverApi, "collections");
-// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 1 short title"});
-// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 2 long long long title"});
-
-// const processService = new CommonResourceService<ProcessResource>(serverApi, "container_requests");
-// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 1 short title"});
-// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 2 long long long title" });
-
-const App = () =>
- <MuiThemeProvider theme={CustomTheme}>
- <Provider store={store}>
- <ConnectedRouter history={history}>
- <div>
- <Route path="/" component={Workbench} />
- <Route path="/token" component={ApiToken} />
- </div>
- </ConnectedRouter>
- </Provider>
- </MuiThemeProvider>;
-
-ReactDOM.render(
- <App />,
- document.getElementById('root') as HTMLElement
-);
+import { fetchConfig } from './common/config';
+import { setBaseUrl } from './common/api/server-api';
+
+fetchConfig()
+ .then(config => {
+
+ setBaseUrl(config.API_HOST);
+
+ const history = createBrowserHistory();
+ const store = configureStore(history);
+
+ store.dispatch(authActions.INIT());
+ store.dispatch<any>(getProjectList(authService.getUuid()));
+
+ const App = () =>
+ <MuiThemeProvider theme={CustomTheme}>
+ <Provider store={store}>
+ <ConnectedRouter history={history}>
+ <div>
+ <Route path="/" component={Workbench} />
+ <Route path="/token" component={ApiToken} />
+ </div>
+ </ConnectedRouter>
+ </Provider>
+ </MuiThemeProvider>;
+
+ ReactDOM.render(
+ <App />,
+ document.getElementById('root') as HTMLElement
+ );
+ });
+
+
//
// SPDX-License-Identifier: AGPL-3.0
-import { API_HOST, serverApi } from "../../common/api/server-api";
+import { API_HOST } from "../../common/api/server-api";
import { User } from "../../models/user";
+import { AxiosInstance } from "../../../node_modules/axios";
export const API_TOKEN_KEY = 'apiToken';
export const USER_EMAIL_KEY = 'userEmail';
export default class AuthService {
+ constructor(protected serverApi: AxiosInstance) { }
+
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
}
}
public getUserDetails = (): Promise<User> => {
- return serverApi
+ return this.serverApi
.get<UserDetailsResponse>('/users/current')
.then(resp => ({
email: resp.data.email,
// SPDX-License-Identifier: AGPL-3.0
import axios from "axios";
-import MockAdapter from "axios-mock-adapter";
+import MockAdapter from "axios-mock-adapter/types";
import ProjectService from "./project-service";
import FilterBuilder from "../../common/api/filter-builder";
import { ProjectResource } from "../../models/project";
import { serverApi } from "../common/api/server-api";
import ProjectService from "./project-service/project-service";
-export const authService = new AuthService();
+export const authService = new AuthService(serverApi);
export const groupsService = new GroupsService(serverApi);
export const projectService = new ProjectService(serverApi);
//
// SPDX-License-Identifier: AGPL-3.0
-// import { default as unionize, ofType, UnionOf } from "unionize";
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
-// const actions = unionize({
-// OPEN_CONTEXT_MENU: ofType<{position: {x: number, y: number}}>()
-// }, {
-// tag: 'type',
-// value: 'payload'
-// });
+const actions = unionize({
+ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
+ CLOSE_CONTEXT_MENU: ofType<{}>()
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
-// export type ContextMenuAction = UnionOf<typeof actions>;
-// export default actions;
\ No newline at end of file
+export type ContextMenuAction = UnionOf<typeof actions>;
+export default actions;
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-// import actions, { DetailsPanelAction } from "./details-panel-action";
-// import { Resource, ResourceKind } from "../../models/resource";
+import { ResourceKind } from "../../models/resource";
+import actions, { ContextMenuAction } from "./context-menu-actions";
-// export interface ContextMenuState {
-// position: {
-// x: number;
-// y: number;
-// },
-// resource: {
-// uuid: string;
-// kind: ResourceKind.
-// }
-// }
+export interface ContextMenuState {
+ position: ContextMenuPosition;
+ resource?: ContextMenuResource;
+}
-// const initialState = {
-// item: null,
-// isOpened: false
-// };
+export interface ContextMenuPosition {
+ x: number;
+ y: number;
+}
-// const reducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
-// actions.match(action, {
-// default: () => state,
-// LOAD_DETAILS: () => state,
-// LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
-// TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
-// });
+export interface ContextMenuResource {
+ uuid: string;
+ kind: string;
+}
-// export default reducer;
+const initialState = {
+ position: { x: 0, y: 0 }
+};
+
+const reducer = (state: ContextMenuState = initialState, action: ContextMenuAction) =>
+ actions.match(action, {
+ default: () => state,
+ OPEN_CONTEXT_MENU: ({resource, position}) => ({ resource, position }),
+ CLOSE_CONTEXT_MENU: () => ({ position: state.position })
+ });
+
+export default reducer;
// SPDX-License-Identifier: AGPL-3.0
import { Middleware } from "redux";
-import actions from "../../store/data-explorer/data-explorer-action";
+import actions 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/store";
-import { getDataExplorer, DataExplorerState } from "../../store/data-explorer/data-explorer-reducer";
+import { RootState } from "../store";
+import { getDataExplorer, DataExplorerState } 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 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 '../store/project-panel/project-panel-middleware';
+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';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
detailsPanel: DetailsPanelState;
+ contextMenu: ContextMenuState;
}
const rootReducer = combineReducers({
router: routerReducer,
dataExplorer: dataExplorerReducer,
sidePanel: sidePanelReducer,
- detailsPanel: detailsPanelReducer
+ detailsPanel: detailsPanelReducer,
+ contextMenu: contextMenuReducer
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import actions from "../../../store/project/project-action";
+import { IconTypes } from "../../../components/icon/icon";
+
+export const projectActionSet: ContextMenuActionSet = [[{
+ icon: IconTypes.FOLDER,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch(actions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+ }
+}, {
+ icon: IconTypes.ANNOUNCEMENT,
+ name: "Share",
+ execute: () => { return; }
+}]];
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import actions from "../../../store/project/project-action";
+import { IconTypes } from "../../../components/icon/icon";
+
+export const rootProjectActionSet: ContextMenuActionSet = [[{
+ icon: IconTypes.FOLDER,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch(actions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+ }
+}]];
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { ContextMenuItem } from "../../components/context-menu/context-menu";
+import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+
+export interface ContextMenuAction extends ContextMenuItem {
+ execute(dispatch: Dispatch, resource: ContextMenuResource): void;
+}
+
+export type ContextMenuActionSet = Array<Array<ContextMenuAction>>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect, Dispatch, DispatchProp } from "react-redux";
+import { RootState } from "../../store/store";
+import actions from "../../store/context-menu/context-menu-actions";
+import ContextMenu, { ContextMenuProps, ContextMenuItem } from "../../components/context-menu/context-menu";
+import { createAnchorAt } from "../../components/popover/helpers";
+import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
+
+type DataProps = Pick<ContextMenuProps, "anchorEl" | "items"> & { resource?: ContextMenuResource };
+const mapStateToProps = (state: RootState): DataProps => {
+ const { position, resource } = state.contextMenu;
+ return {
+ anchorEl: resource ? createAnchorAt(position) : undefined,
+ items: getMenuActionSet(resource),
+ resource
+ };
+};
+
+type ActionProps = Pick<ContextMenuProps, "onClose"> & { onItemClick: (item: ContextMenuItem, resource?: ContextMenuResource) => void };
+const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({
+ onClose: () => {
+ dispatch(actions.CLOSE_CONTEXT_MENU());
+ },
+ onItemClick: (action: ContextMenuAction, resource?: ContextMenuResource) => {
+ dispatch(actions.CLOSE_CONTEXT_MENU());
+ if (resource) {
+ action.execute(dispatch, resource);
+ }
+ }
+});
+
+const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({
+ ...dataProps,
+ ...actionProps,
+ onItemClick: item => {
+ actionProps.onItemClick(item, resource);
+ }
+});
+
+export const ContextMenuHOC = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenu);
+
+const menuActionSets = new Map<string, ContextMenuActionSet>();
+
+export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => {
+ menuActionSets.set(name, itemSet);
+};
+
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
+ return resource ? menuActionSets.get(resource.kind) || [] : [];
+};
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuHOC, addMenuActionSet } from "./context-menu";
+import { projectActionSet } from "./action-sets/project-action-set";
+import { rootProjectActionSet } from "./action-sets/root-project-action-set";
+
+export default ContextMenuHOC;
+
+export enum ContextMenuKind {
+ RootProject = "RootProject",
+ Project = "Project"
+}
+
+addMenuActionSet(ContextMenuKind.RootProject, rootProjectActionSet);
+addMenuActionSet(ContextMenuKind.Project, projectActionSet);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { Dispatch } from "../../../node_modules/redux";
+import { Dispatch } from "redux";
import { RootState } from "../../store/store";
import DialogProjectCreate from "../dialog-create/dialog-project-create";
import actions, { createProject, getProjectList } from "../../store/project/project-action";
});
}
- handleProjectName(e: any) {
+ handleProjectName(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({
name: e.target.value,
});
}
- handleDescriptionValue(e: any) {
+ handleDescriptionValue(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({
description: e.target.value,
});
const mainAppBar = mount(
<MainAppBar
user={user}
+ onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
onContextMenu={jest.fn()}
{...{ searchText: "", breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
<MainAppBar
searchText="search text"
searchDebounce={2000}
+ onContextMenu={jest.fn()}
onSearch={onSearch}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
const mainAppBar = mount(
<MainAppBar
breadcrumbs={items}
+ onContextMenu={jest.fn()}
onBreadcrumbClick={onBreadcrumbClick}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
const mainAppBar = mount(
<MainAppBar
menuItems={menuItems}
+ onContextMenu={jest.fn()}
onMenuItemClick={onMenuItemClick}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { Theme } from "@material-ui/core";
-import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
-import Paper from "@material-ui/core/Paper/Paper";
-import Table from "@material-ui/core/Table/Table";
-import TableHead from "@material-ui/core/TableHead/TableHead";
-import TableRow from "@material-ui/core/TableRow/TableRow";
-import TableCell from "@material-ui/core/TableCell/TableCell";
-import TableBody from "@material-ui/core/TableBody/TableBody";
-
-type CssRules = 'root' | 'table';
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- root: {
- width: '100%',
- marginTop: theme.spacing.unit * 3,
- overflowX: 'auto',
- },
- table: {
- minWidth: 700,
- },
-});
-
-interface ProjectListProps {
-}
-
-class ProjectList extends React.Component<ProjectListProps & WithStyles<CssRules>, {}> {
- render() {
- const {classes} = this.props;
- return <Paper className={classes.root}>
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>Name</TableCell>
- <TableCell>Status</TableCell>
- <TableCell>Type</TableCell>
- <TableCell>Shared by</TableCell>
- <TableCell>File size</TableCell>
- <TableCell>Last modified</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- <TableRow>
- <TableCell>Project 1</TableCell>
- <TableCell>Complete</TableCell>
- <TableCell>Project</TableCell>
- <TableCell>John Doe</TableCell>
- <TableCell>1.5 GB</TableCell>
- <TableCell>9:22 PM</TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </Paper>;
- }
-}
-
-export default withStyles(styles)(ProjectList);
class ProjectPanel extends React.Component<ProjectPanelProps> {
render() {
+ const { classes, currentItemId, onItemClick, onItemDoubleClick, onContextMenu, onDialogOpen } = this.props;
return <div>
- <div className={this.props.classes.toolbar}>
- <Button color="primary" variant="raised" className={this.props.classes.button}>
+ <div className={classes.toolbar}>
+ <Button color="primary" variant="raised" className={classes.button}>
Create a collection
</Button>
- <Button color="primary" variant="raised" className={this.props.classes.button}>
+ <Button color="primary" variant="raised" className={classes.button}>
Run a process
</Button>
- <Button color="primary" onClick={() => this.props.onDialogOpen(this.props.currentItemId)} variant="raised" className={this.props.classes.button}>
+ <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
New project
</Button>
</div>
extractKey={(item: ProjectPanelItem) => item.uuid} />
</div>;
}
-
- componentWillReceiveProps({ match, currentItemId }: ProjectPanelProps) {
+
+ handleNewProjectClick = () => {
+ this.props.onDialogOpen(this.props.currentItemId);
+ }
+ componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
if (match.params.id !== currentItemId) {
- this.props.onItemRouteChange(match.params.id);
+ onItemRouteChange(match.params.id);
}
}
}
import ProjectPanel from "../project-panel/project-panel";
import DetailsPanel from '../../views-components/details-panel/details-panel';
import { ArvadosTheme } from '../../common/custom-theme';
-import ContextMenu, { ContextMenuAction } from '../../components/context-menu/context-menu';
-import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
+import ContextMenu, { ContextMenuKind } from "../../views-components/context-menu";
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 { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
import { ProjectResource } from '../../models/project';
import { ResourceKind } from '../../models/resource';
-const drawerWidth = 240;
-const appBarHeight = 100;
-
-type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- flexGrow: 1,
- zIndex: 1,
- overflow: 'hidden',
- position: 'relative',
- display: 'flex',
- width: '100vw',
- height: '100vh'
- },
- appBar: {
- zIndex: theme.zIndex.drawer + 1,
- position: "absolute",
- width: "100%"
- },
- drawerPaper: {
- position: 'relative',
- width: drawerWidth,
- display: 'flex',
- flexDirection: 'column',
- },
- contentWrapper: {
- backgroundColor: theme.palette.background.default,
- display: "flex",
- flexGrow: 1,
- minWidth: 0,
- paddingTop: appBarHeight
- },
- content: {
- padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
- overflowY: "auto",
- flexGrow: 1
- },
- toolbar: theme.mixins.toolbar
-});
-
interface WorkbenchDataProps {
projects: Array<TreeItem<ProjectResource>>;
currentProjectId: string;
}
interface WorkbenchState {
- contextMenu: {
- anchorEl?: HTMLElement;
- itemUuid?: string;
- };
anchorEl: any;
searchText: string;
menuItems: {
class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
state = {
- contextMenu: {
- anchorEl: undefined,
- itemUuid: undefined
- },
isCreationDialogOpen: false,
anchorEl: null,
searchText: "",
}
};
- mainAppBarActions: MainAppBarActionProps = {
- onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
- },
- onSearch: searchText => {
- this.setState({ searchText });
- this.props.dispatch(push(`/search?q=${searchText}`));
- },
- onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
- onDetailsPanelToggle: () => {
- this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
- },
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
- this.openContextMenu(event, breadcrumb.itemId);
- }
- };
-
- toggleSidePanelOpen = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
- }
-
- toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
- this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
- this.props.dispatch(push("/"));
- }
-
- handleCreationDialogOpen = (itemUuid: string) => {
- this.closeContextMenu();
- this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
- }
-
-
- openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string) => {
- event.preventDefault();
- this.setState({
- contextMenu: {
- anchorEl: mockAnchorFromMouseEvent(event),
- itemUuid
- }
- });
- }
-
- closeContextMenu = () => {
- this.setState({ contextMenu: {} });
- }
-
- openCreateDialog = (item: ContextMenuAction) => {
- const { itemUuid } = this.state.contextMenu;
- if (item.openCreateDialog && itemUuid) {
- this.handleCreationDialogOpen(itemUuid);
- }
- }
-
render() {
const path = getTreePath(this.props.projects, this.props.currentProjectId);
const breadcrumbs = path.map(item => ({
toggleOpen={this.toggleSidePanelOpen}
toggleActive={this.toggleSidePanelActive}
sidePanelItems={this.props.sidePanelItems}
- onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "")}>
+ onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}>
<ProjectTree
projects={this.props.projects}
toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
- onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid)}
+ onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)}
toggleActive={itemId => {
this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
</div>
<DetailsPanel />
</main>
- <ContextMenu
- anchorEl={this.state.contextMenu.anchorEl}
- actions={contextMenuActions}
- onActionClick={this.openCreateDialog}
- onClose={this.closeContextMenu} />
+ <ContextMenu />
<CreateProjectDialog />
</div>
);
renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
- onContextMenu={(event, item) => this.openContextMenu(event, item.uuid)}
+ onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)}
onDialogOpen={this.handleCreationDialogOpen}
onItemClick={item => {
this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
}}
{...props} />
-}
-const contextMenuActions = [[{
- icon: "fas fa-plus fa-fw",
- name: "New project",
- openCreateDialog: true
-}, {
- icon: "fas fa-users fa-fw",
- name: "Share"
-}, {
- icon: "fas fa-sign-out-alt fa-fw",
- name: "Move to"
-}, {
- icon: "fas fa-star fa-fw",
- name: "Add to favourite"
-}, {
- icon: "fas fa-edit fa-fw",
- name: "Rename"
-}, {
- icon: "fas fa-copy fa-fw",
- name: "Make a copy"
-}, {
- icon: "fas fa-download fa-fw",
- name: "Download"
-}], [{
- icon: "fas fa-trash-alt fa-fw",
- name: "Remove"
+ mainAppBarActions: MainAppBarActionProps = {
+ onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
+ this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
+ this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
+ },
+ onSearch: searchText => {
+ this.setState({ searchText });
+ this.props.dispatch(push(`/search?q=${searchText}`));
+ },
+ onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
+ onDetailsPanelToggle: () => {
+ this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ },
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
+ this.openContextMenu(event, breadcrumb.itemId, ContextMenuKind.Project);
+ }
+ };
+
+ toggleSidePanelOpen = (itemId: string) => {
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
+ }
+
+ toggleSidePanelActive = (itemId: string) => {
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
+ this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
+ this.props.dispatch(push("/"));
+ }
+
+ handleCreationDialogOpen = (itemUuid: string) => {
+ this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
+ }
+
+ openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string, kind: ContextMenuKind) => {
+ event.preventDefault();
+ this.props.dispatch(
+ contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource: { uuid: itemUuid, kind }
+ })
+ );
+ }
+
+
}
-]];
+
+const drawerWidth = 240;
+const appBarHeight = 100;
+
+type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ flexGrow: 1,
+ zIndex: 1,
+ overflow: 'hidden',
+ position: 'relative',
+ display: 'flex',
+ width: '100vw',
+ height: '100vh'
+ },
+ appBar: {
+ zIndex: theme.zIndex.drawer + 1,
+ position: "absolute",
+ width: "100%"
+ },
+ drawerPaper: {
+ position: 'relative',
+ width: drawerWidth,
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ contentWrapper: {
+ backgroundColor: theme.palette.background.default,
+ display: "flex",
+ flexGrow: 1,
+ minWidth: 0,
+ paddingTop: appBarHeight
+ },
+ content: {
+ padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
+ overflowY: "auto",
+ flexGrow: 1
+ },
+ toolbar: theme.mixins.toolbar
+});
export default connect<WorkbenchDataProps>(
(state: RootState) => ({