--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export * from 'unionize';
+import { unionize as originalUnionize, SingleValueRec } from 'unionize';
+
+export function unionize<Record extends SingleValueRec>(record: Record) {
+ return originalUnionize(record, {
+ tag: 'type',
+ value: 'payload'
+ });
+}
+
}
});
-interface BreadcrumbsProps {
+export interface BreadcrumbsProps {
items: Breadcrumb[];
onClick: (breadcrumb: Breadcrumb) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
+++ /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 } from '@material-ui/core/styles';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
-import { SidePanelRightArrowIcon, IconType } from '../icon/icon';
-import * as classnames from "classnames";
-import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
-import { Dispatch } from "redux";
-import { RouteComponentProps, withRouter } from "react-router";
-
-type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- overflowY: 'auto',
- minWidth: '240px',
- whiteSpace: 'nowrap',
- marginTop: '52px',
- display: 'flex',
- flexGrow: 1,
- },
- list: {
- padding: '5px 0px 5px 14px',
- minWidth: '240px',
- },
- row: {
- display: 'flex',
- alignItems: 'center',
- },
- toggableIconContainer: {
- color: theme.palette.grey["700"],
- height: '14px',
- width: '14px'
- },
- toggableIcon: {
- fontSize: '14px'
- },
- active: {
- color: theme.palette.primary.main,
- },
- iconClose: {
- transition: 'all 0.1s ease',
- },
- iconOpen: {
- transition: 'all 0.1s ease',
- transform: 'rotate(90deg)',
- }
-});
-
-export interface SidePanelItem {
- id: string;
- name: string;
- url: string;
- icon: IconType;
- open?: boolean;
- margin?: boolean;
- openAble?: boolean;
- activeAction?: (dispatch: Dispatch, uuid?: string) => void;
-}
-
-interface SidePanelDataProps {
- toggleOpen: (id: string) => void;
- toggleActive: (id: string) => void;
- sidePanelItems: SidePanelItem[];
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
-}
-
-type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
-
-export const SidePanel = withStyles(styles)(withRouter(
- class extends React.Component<SidePanelProps> {
- render() {
- const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
- const { root, row, list, toggableIconContainer } = classes;
-
- const path = this.props.location.pathname.split('/');
- const activeUrl = path.length > 1 ? "/" + path[1] : "/";
- return (
- <div className={root}>
- <List>
- {sidePanelItems.map(it => {
- const active = it.url === activeUrl;
- return <span key={it.name}>
- <ListItem button className={list} onClick={() => toggleActive(it.id)}
- onContextMenu={this.handleRowContextMenu(it)}>
- <span className={row}>
- {it.openAble ? (
- <i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
- <ListItemIcon
- className={this.getToggableIconClassNames(it.open, active)}>
- < SidePanelRightArrowIcon/>
- </ListItemIcon>
- </i>
- ) : null}
- <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
- hasMargin={it.margin}/>
- </span>
- </ListItem>
- {it.openAble ? (
- <Collapse in={it.open} timeout="auto" unmountOnExit>
- {children}
- </Collapse>
- ) : null}
- </span>;
- })}
- </List>
- </div>
- );
- }
-
- getToggableIconClassNames = (isOpen?: boolean, isActive ?: boolean) => {
- const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
- return classnames(toggableIcon, {
- [iconOpen]: isOpen,
- [iconClose]: !isOpen,
- [active]: isActive
- });
- }
-
- handleRowContextMenu = (item: SidePanelItem) =>
- (event: React.MouseEvent<HTMLElement>) =>
- item.openAble ? this.props.onContextMenu(event, item) : null
- }
-));
onContextMenu={this.handleRowContextMenu(it)}>
{it.status === TreeItemStatus.PENDING ?
<CircularProgress size={10} className={loader} /> : null}
- <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
+ <i onClick={this.handleToggleItemOpen(it.id, it.status)}
className={toggableIconContainer}>
<ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
{this.getProperArrowAnimation(it.status, it.items!)}
}
: undefined;
}
+
+ handleToggleItemOpen = (id: string, status: TreeItemStatus) => (event: React.MouseEvent<HTMLElement>) => {
+ event.stopPropagation();
+ this.props.toggleItemOpen(id, status);
+ }
}
);
import { Provider } from "react-redux";
import { Workbench } from './views/workbench/workbench';
import './index.css';
-import { Route } from "react-router";
+import { Route } from 'react-router';
import createBrowserHistory from "history/createBrowserHistory";
-import { configureStore } from "./store/store";
+import { History } from "history";
+import { configureStore, RootStore } from './store/store';
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "./views-components/api-token/api-token";
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 { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
import { processActionSet } from './views-components/context-menu/action-sets/process-action-set';
+import { addRouteChangeHandlers } from './routes/routes';
+import { loadWorkbench } from './store/workbench/workbench-actions';
+import { Routes } from '~/routes/routes';
const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
fetchConfig()
- .then(config => {
+ .then((config) => {
const history = createBrowserHistory();
const services = createServices(config);
const store = configureStore(history, services);
+ store.subscribe(initListener(history, store));
+
store.dispatch(initAuth());
- store.dispatch(getProjectList(services.authService.getUuid()));
- const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
- const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
+ const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
+ const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props} />;
const App = () =>
<MuiThemeProvider theme={CustomTheme}>
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
- <Route path="/" component={WorkbenchComponent} />
- <Route path="/token" component={TokenComponent} />
+ <Route path={Routes.TOKEN} component={TokenComponent} />
+ <Route path={Routes.ROOT} component={WorkbenchComponent} />
</div>
</ConnectedRouter>
</Provider>
<App />,
document.getElementById('root') as HTMLElement
);
+
+
});
+const initListener = (history: History, store: RootStore) => {
+ let initialized = false;
+ return async () => {
+ const { router, auth } = store.getState();
+ if (router.location && auth.user && !initialized) {
+ initialized = true;
+ await store.dispatch(loadWorkbench());
+ addRouteChangeHandlers(history, store);
+ }
+ };
+};
+
GROUP = "arvados#group",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
- WORKFLOW = "arvados#workflow"
+ WORKFLOW = "arvados#workflow",
+ USER = "arvados#user",
}
+
+export enum ResourceObjectType {
+ USER = 'tpzed',
+ GROUP = 'j7d0g',
+ COLLECTION = '4zz18'
+}
+
+export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
+export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
+
+export const isResourceUuid = (uuid: string) =>
+ RESOURCE_UUID_REGEX.test(uuid);
+
+export const extractUuidObjectType = (uuid: string) => {
+ const match = RESOURCE_UUID_REGEX.exec(uuid);
+ return match
+ ? match[0].split('-')[1]
+ : undefined;
+};
+
+export const extractUuidKind = (uuid: string = '') => {
+ const objectType = extractUuidObjectType(uuid);
+ switch (objectType) {
+ case ResourceObjectType.USER:
+ return ResourceKind.USER;
+ case ResourceObjectType.GROUP:
+ return ResourceKind.GROUP;
+ case ResourceObjectType.COLLECTION:
+ return ResourceKind.COLLECTION;
+ default:
+ return undefined;
+ }
+};
//
// SPDX-License-Identifier: AGPL-3.0
+import { Resource, ResourceKind } from '~/models/resource';
+
export interface User {
email: string;
firstName: string;
export const getUserFullname = (user?: User) => {
return user ? `${user.firstName} ${user.lastName}` : "";
-};
\ No newline at end of file
+};
+
+export interface UserResource extends Resource {
+ kind: ResourceKind.USER;
+ email: string;
+ username: string;
+ firstName: string;
+ lastName: string;
+ identityUrl: string;
+ isAdmin: boolean;
+ prefs: string;
+ defaultOwnerUuid: string;
+ isActive: boolean;
+ writableBy: string[];
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { History, Location } from 'history';
+import { RootStore } from '../store/store';
+import { matchPath } from 'react-router';
+import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
+import { getProjectUrl } from '../models/project';
+import { getCollectionUrl } from '~/models/collection';
+import { loadProject, loadFavorites, loadCollection } from '../store/workbench/workbench-actions';
+
+export const Routes = {
+ ROOT: '/',
+ TOKEN: '/token',
+ PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
+ COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
+ PROCESS: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
+ FAVORITES: '/favorites',
+};
+
+export const getResourceUrl = (uuid: string) => {
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ return getProjectUrl(uuid);
+ case ResourceKind.COLLECTION:
+ return getCollectionUrl(uuid);
+ default:
+ return undefined;
+ }
+};
+
+export const addRouteChangeHandlers = (history: History, store: RootStore) => {
+ const handler = handleLocationChange(store);
+ handler(history.location);
+ history.listen(handler);
+};
+
+export const matchRootRoute = (route: string) =>
+ matchPath(route, { path: Routes.ROOT, exact: true });
+
+export const matchFavoritesRoute = (route: string) =>
+ matchPath(route, { path: Routes.FAVORITES });
+
+export interface ProjectRouteParams {
+ id: string;
+}
+
+export const matchProjectRoute = (route: string) =>
+ matchPath<ProjectRouteParams>(route, { path: Routes.PROJECTS });
+
+export interface CollectionRouteParams {
+ id: string;
+}
+
+export const matchCollectionRoute = (route: string) =>
+ matchPath<CollectionRouteParams>(route, { path: Routes.COLLECTIONS });
+
+
+const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
+ const projectMatch = matchProjectRoute(pathname);
+ const collectionMatch = matchCollectionRoute(pathname);
+ const favoriteMatch = matchFavoritesRoute(pathname);
+ if (projectMatch) {
+ store.dispatch(loadProject(projectMatch.params.id));
+ } else if (collectionMatch) {
+ store.dispatch(loadCollection(collectionMatch.params.id));
+ } else if (favoriteMatch) {
+ store.dispatch(loadFavorites());
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { GroupsService } from "~/services/groups-service/groups-service";
+import { UserService } from '../user-service/user-service';
+import { GroupResource } from '~/models/group';
+import { UserResource } from '~/models/user';
+import { extractUuidObjectType, ResourceObjectType } from "~/models/resource";
+
+export class AncestorService {
+ constructor(
+ private groupsService: GroupsService,
+ private userService: UserService
+ ) { }
+
+ async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource>> {
+ const service = this.getService(extractUuidObjectType(uuid));
+ if (service) {
+ const resource = await service.get(uuid);
+ if (uuid === rootUuid) {
+ return [resource];
+ } else {
+ return [
+ ...await this.ancestors(resource.ownerUuid, rootUuid),
+ resource
+ ];
+ }
+ } else {
+ return [];
+ }
+ }
+
+ private getService = (objectType?: string) => {
+ switch (objectType) {
+ case ResourceObjectType.GROUP:
+ return this.groupsService;
+ case ResourceObjectType.USER:
+ return this.userService;
+ default:
+ return undefined;
+ }
+ }
+}
\ No newline at end of file
import { KeepService } from "./keep-service/keep-service";
import { WebDAV } from "../common/webdav";
import { Config } from "../common/config";
+import { UserService } from './user-service/user-service';
+import { AncestorService } from "~/services/ancestors-service/ancestors-service";
+import { ResourceKind } from "~/models/resource";
export type ServiceRepository = ReturnType<typeof createServices>;
const collectionService = new CollectionService(apiClient, webdavClient, authService);
const tagService = new TagService(linkService);
const collectionFilesService = new CollectionFilesService(collectionService);
+ const userService = new UserService(apiClient);
+ const ancestorsService = new AncestorService(groupsService, userService);
return {
apiClient,
favoriteService,
collectionService,
tagService,
- collectionFilesService
+ collectionFilesService,
+ userService,
+ ancestorsService,
};
};
+
+export const getResourceService = (kind?: ResourceKind) => (serviceRepository: ServiceRepository) => {
+ switch (kind) {
+ case ResourceKind.USER:
+ return serviceRepository.userService;
+ case ResourceKind.GROUP:
+ return serviceRepository.groupsService;
+ case ResourceKind.COLLECTION:
+ return serviceRepository.collectionService;
+ default:
+ return undefined;
+ }
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "~/common/api/common-resource-service";
+import { UserResource } from "~/models/user";
+
+export class UserService extends CommonResourceService<UserResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "users");
+ }
+}
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { ofType, default as unionize, UnionOf } from "unionize";
+import { ofType, unionize, UnionOf } from '~/common/unionize';
import { Dispatch } from "redux";
import { User } from "~/models/user";
import { RootState } from "../store";
INIT: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
-}, {
- tag: 'type',
- value: 'payload'
});
function setAuthorizationHeader(services: ServiceRepository, token: string) {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
+import { getResource } from '~/store/resources/resources';
+import { TreePicker } from '../tree-picker/tree-picker';
+import { getSidePanelTreeBranch } from '../side-panel-tree/side-panel-tree-actions';
+import { propertiesActions } from '../properties/properties-actions';
+
+export const BREADCRUMBS = 'breadcrumbs';
+
+export interface ResourceBreadcrumb extends Breadcrumb {
+ uuid: string;
+}
+
+export const setBreadcrumbs = (breadcrumbs: Breadcrumb[]) =>
+ propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
+
+const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): ResourceBreadcrumb[] => {
+ const nodes = getSidePanelTreeBranch(uuid)(treePicker);
+ return nodes.map(node =>
+ typeof node.value === 'string'
+ ? { label: node.value, uuid: node.nodeId }
+ : { label: node.value.name, uuid: node.value.uuid });
+};
+
+export const setSidePanelBreadcrumbs = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { treePicker } = getState();
+ const breadcrumbs = getSidePanelTreeBreadcrumbs(uuid)(treePicker);
+ dispatch(setBreadcrumbs(breadcrumbs));
+ };
+
+export const setProjectBreadcrumbs = setSidePanelBreadcrumbs;
+
+export const setCollectionBreadcrumbs = (collectionUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { resources } = getState();
+ const collection = getResource(collectionUuid)(resources);
+ if (collection) {
+ dispatch<any>(setProjectBreadcrumbs(collection.ownerUuid));
+ }
+ };
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
import { Dispatch } from "redux";
import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
-import { CollectionResource } from "~/models/collection";
+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";
import { TagResource, TagProperty } from "~/models/tag";
import { snackbarActions } from "../snackbar/snackbar-actions";
+import { resourcesActions } from "~/store/resources/resources-actions";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
export const collectionPanelActions = unionize({
LOAD_COLLECTION: ofType<{ uuid: string }>(),
CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(),
DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(),
DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>()
-}, { tag: 'type', value: 'payload' });
+});
export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
-export const loadCollection = (uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const loadCollectionPanel = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
- return services.collectionService
- .get(uuid)
- .then(item => {
- dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
- dispatch<any>(loadCollectionFiles(uuid));
- });
+ const collection = await services.collectionService.get(uuid);
+ dispatch(resourcesActions.SET_RESOURCES([collection]));
+ dispatch<any>(loadCollectionFiles(collection.uuid));
+ dispatch<any>(loadCollectionTags(collection.uuid));
+ return collection;
};
export const loadCollectionTags = (uuid: string) =>
});
};
-
export const createCollectionTag = (data: TagProperty) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
import { ServiceRepository } from "~/services/services";
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>;
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { projectPanelActions } from '~/store/project-panel/project-panel-action';
export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
const uuidKey = 'uuid';
delete collection[uuidKey];
await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
- dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied', hideDuration: 2000 }));
+ return collection;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(COLLECTION_COPY_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' }));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not copy the collection', hideDuration: 2000 }));
+ throw new Error('Could not copy the collection.');
}
+ return ;
}
};
\ No newline at end of file
import { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
import { RootState } from '~/store/store';
import { uploadCollectionFiles } from '~/store/collections/uploader/collection-uploader-actions';
-import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
import { dialogActions } from "~/store/dialog/dialog-actions";
import { CollectionResource } from '~/models/collection';
import { ServiceRepository } from '~/services/services';
dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_CREATE_FORM_NAME, data: { ownerUuid } }));
};
-export const addCollection = (data: CollectionCreateFormDialogData) =>
- async (dispatch: Dispatch) => {
- await dispatch<any>(createCollection(data));
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Collection has been successfully created.",
- hideDuration: 2000
- }));
- };
-
export const createCollection = (collection: Partial<CollectionResource>) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME));
try {
const newCollection = await services.collectionService.create(collection);
await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
- dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
dispatch(reset(COLLECTION_CREATE_FORM_NAME));
+ return newCollection;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
}
+ return ;
}
};
\ No newline at end of file
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved', hideDuration: 2000 }));
+ return collection;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000 }));
}
+ return ;
}
};
import { initialize, startSubmit, stopSubmit } from 'redux-form';
import { RootState } from "~/store/store";
import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
-import { updateDetails } from "~/store/details-panel/details-panel-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
import { dialogActions } from "~/store/dialog/dialog-actions";
import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action";
import { snackbarActions } from "~/store/snackbar/snackbar-actions";
dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME, data: {} }));
};
-export const editCollection = (data: CollectionUpdateFormDialogData) =>
- async (dispatch: Dispatch) => {
- await dispatch<any>(updateCollection(data));
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Collection has been successfully updated.",
- hideDuration: 2000
- }));
- };
-
export const updateCollection = (collection: Partial<CollectionResource>) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const uuid = collection.uuid || '';
try {
const updatedCollection = await services.collectionService.update(uuid, collection);
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
- dispatch<any>(updateDetails(updatedCollection));
- dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
- } catch(e) {
+ return updatedCollection;
+ } catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
}
+ return;
}
};
\ No newline at end of file
//\r
// SPDX-License-Identifier: AGPL-3.0\r
\r
-import { default as unionize, ofType, UnionOf } from "unionize";\r
+import { unionize, ofType, UnionOf } from "~/common/unionize";\r
import { Dispatch } from 'redux';\r
import { RootState } from '~/store/store';\r
import { ServiceRepository } from '~/services/services';\r
START_UPLOAD: ofType(),\r
SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),\r
CLEAR_UPLOAD: ofType()\r
-}, {\r
- tag: 'type',\r
- value: 'payload'\r
});\r
\r
export type CollectionUploaderAction = UnionOf<typeof collectionUploaderActions>;\r
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../resources/resources';
+import { ProjectResource } from '~/models/project';
+import { UserResource } from '../../models/user';
+import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
CLOSE_CONTEXT_MENU: ofType<{}>()
-}, {
- tag: 'type',
- value: 'payload'
- });
+});
export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
+
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) =>
+ (dispatch: Dispatch) => {
+ event.preventDefault();
+ dispatch(
+ contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource
+ })
+ );
+ };
+
+export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const userResource = getResource<UserResource>(projectUuid)(getState().resources);
+ if (userResource) {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: userResource.uuid,
+ kind: ContextMenuKind.ROOT_PROJECT
+ }));
+ }
+ };
+
+export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const projectResource = getResource<ProjectResource>(projectUuid)(getState().resources);
+ if (projectResource) {
+ dispatch<any>(openContextMenu(event, {
+ name: projectResource.name,
+ uuid: projectResource.uuid,
+ kind: ContextMenuKind.PROJECT
+ }));
+ }
+ };
+
+export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ if (!isSidePanelTreeCategory(id)) {
+ const kind = extractUuidKind(id);
+ if (kind === ResourceKind.USER) {
+ dispatch<any>(openRootProjectContextMenu(event, id));
+ } else if (kind === ResourceKind.PROJECT) {
+ dispatch<any>(openProjectContextMenu(event, id));
+ }
+ }
+ };
+
+export const resourceKindToContextMenuKind = (uuid: string) => {
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ return ContextMenuKind.PROJECT;
+ case ResourceKind.COLLECTION:
+ return ContextMenuKind.COLLECTION_RESOURCE;
+ case ResourceKind.USER:
+ return ContextMenuKind.ROOT_PROJECT;
+ default:
+ return;
+ }
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
import { DataColumns } from "~/components/data-table/data-table";
TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
TOGGLE_SORT: ofType<{ id: string, columnName: string }>(),
SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
-}, { tag: "type", value: "payload" });
+});
export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataExplorer } from './data-explorer-reducer';
+import { ListArguments, ListResults } from '~/common/api/common-resource-service';
export abstract class DataExplorerMiddlewareService {
protected readonly id: string;
abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
}
+
+export const getDataExplorerColumnFilters = <T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] => {
+ const column = columns.find(c => c.name === columnName);
+ return column ? column.filters.filter(f => f.selected) : [];
+};
+
+export const dataExplorerToListParams = <R>(dataExplorer: DataExplorer) => ({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+});
+
+export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
+ itemsAvailable,
+ page: Math.floor(offset / limit),
+ rowsPerPage: limit
+});
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
-import { Dispatch } from "redux";
-import { Resource, ResourceKind } from "~/models/resource";
-import { RootState } from "../store";
-import { ServiceRepository } from "~/services/services";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
- LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
- LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
- UPDATE_DETAILS: ofType<{ item: Resource }>()
-}, { tag: 'type', value: 'payload' });
+ LOAD_DETAILS_PANEL: ofType<string>()
+});
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
-export const loadDetails = (uuid: string, kind: ResourceKind) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
- const item = await getService(services, kind).get(uuid);
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
- };
-
-export const updateDetails = (item: Resource) =>
- async (dispatch: Dispatch, getState: () => RootState) => {
- const currentItem = getState().detailsPanel.item;
- if (currentItem && (currentItem.uuid === item.uuid)) {
- dispatch(detailsPanelActions.UPDATE_DETAILS({ item }));
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
- }
- };
-
-
-const getService = (services: ServiceRepository, kind: ResourceKind) => {
- switch (kind) {
- case ResourceKind.PROJECT:
- return services.projectService;
- case ResourceKind.COLLECTION:
- return services.collectionService;
- default:
- return services.projectService;
- }
-};
+export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+
// SPDX-License-Identifier: AGPL-3.0
import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
-import { Resource } from "~/models/resource";
export interface DetailsPanelState {
- item: Resource | null;
+ resourceUuid: string;
isOpened: boolean;
}
const initialState = {
- item: null,
+ resourceUuid: '',
isOpened: false
};
export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
detailsPanelActions.match(action, {
default: () => state,
- LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
+ LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/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>;
export const FAVORITE_PANEL_ID = "favoritePanel";
export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID);
+
+export const loadFavoritePanel = () => favoritePanelActions.REQUEST_ITEMS();
import { FavoritePanelColumnNames, FavoritePanelFilter } 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 { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { updateFavorites } from "../favorites/favorites-actions";
import { favoritePanelActions } from "./favorite-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
import { LinkResource } from "~/models/link";
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
+import { resourcesActions } from "~/store/resources/resources-actions";
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
}
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(c => c.sortDirection !== SortDirection.NONE);
- const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+ if (!dataExplorer) {
+ api.dispatch(favoritesPanelDataExplorerIsNotSet());
+ } else {
- const linkOrder = new OrderBuilder<LinkResource>();
- const contentOrder = new OrderBuilder<GroupContentsResource>();
+ const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
- if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
- const direction = sortColumn.sortDirection === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+ const linkOrder = new OrderBuilder<LinkResource>();
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
- linkOrder.addOrder(direction, "name");
- contentOrder
- .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
- .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
- .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
- }
+ if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+ const direction = sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ linkOrder.addOrder(direction, "name");
+ contentOrder
+ .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
+ }
- this.services.favoriteService
- .list(this.services.authService.getUuid()!, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- linkOrder: linkOrder.getOrder(),
- contentOrder: contentOrder.getOrder(),
- filters: new FilterBuilder()
- .addIsA("headUuid", typeFilters.map(filter => filter.type))
- .addILike("name", dataExplorer.searchValue)
- .getFilters()
- })
- .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)));
- })
- .catch(() => {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- });
+ this.services.favoriteService
+ .list(this.services.authService.getUuid()!, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ linkOrder: linkOrder.getOrder(),
+ contentOrder: contentOrder.getOrder(),
+ filters: new FilterBuilder()
+ .addIsA("headUuid", typeFilters.map(filter => filter.type))
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters()
+ })
+ .then(response => {
+ api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: response.items.map(resource => resource.uuid),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ })
+ .catch(() => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ });
+ }
}
}
+
+const favoritesPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Favorites panel is not ready.'
+ });
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
import { RootState } from "../store";
import { checkFavorite } from "./favorites-reducer";
TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
CHECK_PRESENCE_IN_FAVORITES: ofType<string[]>(),
UPDATE_FAVORITES: ofType<Record<string, boolean>>()
-}, { tag: 'type', value: 'payload' });
+});
export type FavoritesAction = UnionOf<typeof favoritesActions>;
});
};
-export const checkPresenceInFavorites = (resourceUuids: string[]) =>
+export const updateFavorites = (resourceUuids: string[]) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
dispatch(favoritesActions.UPDATE_FAVORITES(results));
});
};
-
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from "redux";
-import { getProjectList, projectActions } from "../project/project-action";
+import { Dispatch, compose } from 'redux';
import { push } from "react-router-redux";
-import { TreeItemStatus } from "~/components/tree/tree";
-import { findTreeItem } from "../project/project-reducer";
-import { RootState } from "../store";
-import { ResourceKind } from "~/models/resource";
-import { projectPanelActions } from "../project-panel/project-panel-action";
+import { ResourceKind, extractUuidKind } from '~/models/resource';
import { getCollectionUrl } from "~/models/collection";
-import { getProjectUrl, ProjectResource } from "~/models/project";
-import { ProjectService } from "~/services/project-service/project-service";
-import { ServiceRepository } from "~/services/services";
-import { sidePanelActions } from "../side-panel/side-panel-action";
-import { SidePanelId } from "../side-panel/side-panel-reducer";
-import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
-
-export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
- switch (resourceKind) {
- case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
- case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
- default:
- return '';
- }
-};
-
-export enum ItemMode {
- BOTH,
- OPEN,
- ACTIVE
-}
-
-export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { projects, router } = getState();
- const treeItem = findTreeItem(projects.items, itemId);
-
- if (treeItem) {
- const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
-
- if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- if (router.location && !router.location.pathname.includes(resourceUrl)) {
- dispatch(push(resourceUrl));
- }
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
- }
-
- const promise = treeItem.status === TreeItemStatus.LOADED
- ? Promise.resolve()
- : dispatch<any>(getProjectList(itemId));
-
- promise
- .then(() => dispatch<any>(() => {
- if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
- }
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }));
- } else {
- const uuid = services.authService.getUuid();
- if (itemId === uuid) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }
+import { getProjectUrl } from "~/models/project";
+
+import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
+import { Routes } from '~/routes/routes';
+
+export const navigateTo = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ const kind = extractUuidKind(uuid);
+ if (kind === ResourceKind.PROJECT || kind === ResourceKind.USER) {
+ dispatch<any>(navigateToProject(uuid));
+ } else if (kind === ResourceKind.COLLECTION) {
+ dispatch<any>(navigateToCollection(uuid));
+ }
+ if (uuid === SidePanelTreeCategory.FAVORITES) {
+ dispatch<any>(navigateToFavorites);
}
};
-export const restoreBranch = (itemId: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const ancestors = await loadProjectAncestors(itemId, services.projectService);
- const uuids = ancestors.map(ancestor => ancestor.uuid);
- await loadBranch(uuids, dispatch);
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
- uuids.forEach(uuid => {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
- });
- };
+export const navigateToFavorites = push(Routes.FAVORITES);
-export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
- if (getUuidObjectType(uuid) === ObjectTypes.USER) {
- return [];
- } else {
- const currentProject = await projectService.get(uuid);
- const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
- return [...ancestors, currentProject];
- }
-};
+export const navigateToProject = compose(push, getProjectUrl);
-const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
- const [uuid, ...rest] = uuids;
- if (uuid) {
- await dispatch<any>(getProjectList(uuid));
- return loadBranch(rest, dispatch);
- }
-};
+export const navigateToCollection = compose(push, getCollectionUrl);
// SPDX-License-Identifier: AGPL-3.0
import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
-
+import { propertiesActions } from "~/store/properties/properties-actions";
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { RootState } from '~/store/store';
+import { getProperty } from "~/store/properties/properties";
export const PROJECT_PANEL_ID = "projectPanel";
+export const PROJECT_PANEL_CURRENT_UUID = "projectPanelCurrentUuid";
export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
+
+export const openProjectPanel = (projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ };
+
+export const getProjectPanelCurrentUuid = (state: RootState) => getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { DataExplorerMiddlewareService, getDataExplorerColumnFilters, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } 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, OrderDirection } from "~/common/api/order-builder";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { projectPanelActions } from "./project-panel-action";
+import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
+import { updateFavorites } from "../favorites/favorites-actions";
+import { projectPanelActions, PROJECT_PANEL_CURRENT_UUID } from './project-panel-action';
import { Dispatch, MiddlewareAPI } from "redux";
import { ProjectResource } from "~/models/project";
+import { updateResources } from "~/store/resources/resources-actions";
+import { getProperty } from "~/store/properties/properties";
+import { snackbarActions } from '../snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
+import { ListResults } from '~/common/api/common-resource-service';
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
super(id);
}
- requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async 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 = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
- const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
- const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ const projectUuid = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+ if (!projectUuid) {
+ api.dispatch(projectPanelCurrentUuidIsNotSet());
+ } else if (!dataExplorer) {
+ api.dispatch(projectPanelDataExplorerIsNotSet());
+ } else {
+ try {
+ const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ api.dispatch(updateResources(response.items));
+ api.dispatch(setItems(response));
+ } catch (e) {
+ api.dispatch(couldNotFetchProjectContents());
+ }
+ }
+ }
+}
- const order = new OrderBuilder<ProjectResource>();
+const setItems = (listResults: ListResults<GroupContentsResource>) =>
+ projectPanelActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
- if (sortColumn) {
- const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer),
+ filters: getFilters(dataExplorer),
+});
- const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
- order
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
- }
+const getFilters = (dataExplorer: DataExplorer) => {
+ const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
+ const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+ const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+ return new FilterBuilder()
+ .addIsA("uuid", typeFilters.map(f => f.type))
+ .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .getFilters();
+};
+
+const getOrder = (dataExplorer: DataExplorer) => {
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const order = new OrderBuilder<ProjectResource>();
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
- this.services.groupsService
- .contents(state.projects.currentItemId, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: order.getOrder(),
- filters: new FilterBuilder()
- .addIsA("uuid", typeFilters.map(f => f.type))
- .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
- .getFilters()
- })
- .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)));
- })
- .catch(() => {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- });
+ const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+ return order
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT)
+ .getOrder();
+ } else {
+ return order.getOrder();
}
-}
+};
+
+const projectPanelCurrentUuidIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Project panel is not opened.'
+ });
+
+const couldNotFetchProjectContents = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch project contents.'
+ });
+
+const projectPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Project panel is not ready.'
+ });
+++ /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 { ProjectResource } from "~/models/project";
-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({
- REMOVE_PROJECT: ofType<string>(),
- PROJECTS_REQUEST: ofType<string>(),
- PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
- TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<string>(),
- TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
- RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
-}, {
- tag: 'type',
- value: 'payload'
-});
-
-export const getProjectList = (parentUuid: string = '') =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
- return services.projectService.list({
- filters: new FilterBuilder()
- .addEqual("ownerUuid", parentUuid)
- .getFilters()
- }).then(({ items: projects }) => {
- dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
- dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
- return projects;
- });
- };
-
-export type ProjectAction = UnionOf<typeof projectActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { projectsReducer, getTreePath } from "./project-reducer";
-import { projectActions } from "./project-action";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { mockProjectResource } from "~/models/test-utils";
-
-describe('project-reducer', () => {
-
- it('should load projects', () => {
- const initialState = undefined;
-
- const projects = [mockProjectResource({ uuid: "1" }), mockProjectResource({ uuid: "2" })];
- const state = projectsReducer(initialState, projectActions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
- expect(state).toEqual({
- items: [{
- active: false,
- open: false,
- id: "1",
- items: [],
- data: mockProjectResource({ uuid: "1" }),
- status: TreeItemStatus.INITIAL
- }, {
- active: false,
- open: false,
- id: "2",
- items: [],
- data: mockProjectResource({ uuid: "2" }),
- status: TreeItemStatus.INITIAL
- }
- ],
- currentItemId: ""
- });
- });
-
- it('should remove activity on projects list', () => {
- const initialState = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: true,
- status: TreeItemStatus.PENDING
- }],
- currentItemId: "1"
- };
- const project = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: false,
- status: TreeItemStatus.PENDING
- }],
- currentItemId: ""
- };
-
- const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
- expect(state).toEqual(project);
- });
-
- it('should toggle project tree item activity', () => {
- const initialState = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: false,
- status: TreeItemStatus.PENDING
- }],
- currentItemId: "1"
- };
- const project = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: true,
- status: TreeItemStatus.PENDING,
- }],
- currentItemId: "1"
- };
-
- const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
- expect(state).toEqual(project);
- });
-
-
- it('should close project tree item ', () => {
- const initialState = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: false,
- status: TreeItemStatus.PENDING,
- }],
- currentItemId: "1"
- };
- const project = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: false,
- active: false,
- status: TreeItemStatus.PENDING,
- }],
- currentItemId: "1"
- };
-
- const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
- expect(state).toEqual(project);
- });
-});
-
-describe("findTreeBranch", () => {
- const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
- id,
- items,
- active: false,
- data: "",
- open: false,
- status: TreeItemStatus.INITIAL
- });
-
- it("should return an array that matches path to the given item", () => {
- const tree: Array<TreeItem<string>> = [
- createTreeItem("1", [
- createTreeItem("1.1", [
- createTreeItem("1.1.1"),
- createTreeItem("1.1.2")
- ])
- ]),
- createTreeItem("2", [
- createTreeItem("2.1", [
- createTreeItem("2.1.1"),
- createTreeItem("2.1.2")
- ])
- ])
- ];
- const branch = getTreePath(tree, "2.1.1");
- expect(branch.map(item => item.id)).toEqual(["2", "2.1", "2.1.1"]);
- });
-
- it("should return empty array if item is not found", () => {
- const tree: Array<TreeItem<string>> = [
- createTreeItem("1", [
- createTreeItem("1.1", [
- createTreeItem("1.1.1"),
- createTreeItem("1.1.2")
- ])
- ]),
- createTreeItem("2", [
- createTreeItem("2.1", [
- createTreeItem("2.1.1"),
- createTreeItem("2.1.2")
- ])
- ])
- ];
- expect(getTreePath(tree, "3")).toHaveLength(0);
- });
-
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as _ from "lodash";
-
-import { projectActions, ProjectAction } from "./project-action";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { ProjectResource } from "~/models/project";
-
-export type ProjectState = {
- items: Array<TreeItem<ProjectResource>>,
- currentItemId: string
-};
-
-interface ProjectUpdater {
- opened: boolean;
- uuid: string;
-}
-
-export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
- let item;
- for (const t of tree) {
- item = t.id === itemId
- ? t
- : findTreeItem(t.items ? t.items : [], itemId);
- if (item) {
- break;
- }
- }
- return item;
-}
-
-export function getActiveTreeItem<T>(tree: Array<TreeItem<T>>): TreeItem<T> | undefined {
- let item;
- for (const t of tree) {
- item = t.active
- ? t
- : getActiveTreeItem(t.items ? t.items : []);
- if (item) {
- break;
- }
- }
- return item;
-}
-
-export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<TreeItem<T>> {
- for (const item of tree) {
- if (item.id === itemId) {
- return [item];
- } else {
- const branch = getTreePath(item.items || [], itemId);
- if (branch.length > 0) {
- return [item, ...branch];
- }
- }
- }
- return [];
-}
-
-function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
- for (const t of tree) {
- t.active = false;
- resetTreeActivity(t.items ? t.items : []);
- }
-}
-
-function updateProjectTree(tree: Array<TreeItem<ProjectResource>>, projects: ProjectResource[], parentItemId?: string): Array<TreeItem<ProjectResource>> {
- let treeItem;
- if (parentItemId) {
- treeItem = findTreeItem(tree, parentItemId);
- if (treeItem) {
- treeItem.status = TreeItemStatus.LOADED;
- }
- }
- const items = projects.map(p => ({
- id: p.uuid,
- open: false,
- active: false,
- status: TreeItemStatus.INITIAL,
- data: p,
- items: []
- } as TreeItem<ProjectResource>));
-
- if (treeItem) {
- treeItem.items = items;
- return tree;
- }
-
- return items;
-}
-
-const initialState: ProjectState = {
- items: [],
- currentItemId: ""
-};
-
-
-export const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
- return projectActions.match(action, {
- REMOVE_PROJECT: () => state,
- PROJECTS_REQUEST: itemId => {
- const items = _.cloneDeep(state.items);
- const item = findTreeItem(items, itemId);
- if (item) {
- item.status = TreeItemStatus.PENDING;
- state.items = items;
- }
- return { ...state, items };
- },
- PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
- const items = _.cloneDeep(state.items);
- return {
- ...state,
- items: updateProjectTree(items, projects, parentItemId)
- };
- },
- TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => {
- const items = _.cloneDeep(state.items);
- const item = findTreeItem(items, itemId);
- if (item) {
- item.open = !item.open;
- }
- return {
- ...state,
- items,
- currentItemId: itemId
- };
- },
- TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => {
- const items = _.cloneDeep(state.items);
- resetTreeActivity(items);
- const item = findTreeItem(items, itemId);
- if (item) {
- item.active = true;
- }
- return {
- ...state,
- items,
- currentItemId: itemId
- };
- },
- RESET_PROJECT_TREE_ACTIVITY: () => {
- const items = _.cloneDeep(state.items);
- resetTreeActivity(items);
- return {
- ...state,
- items,
- currentItemId: ""
- };
- },
- default: () => state
- });
-};
import { Dispatch } from "redux";
import { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
import { RootState } from '~/store/store';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { projectPanelActions } from '~/store/project-panel/project-panel-action';
-import { getProjectList } from '~/store/project/project-action';
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
import { ProjectResource } from '~/models/project';
import { ServiceRepository } from '~/services/services';
-
export interface ProjectCreateFormDialogData {
ownerUuid: string;
name: string;
dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_CREATE_FORM_NAME, data: {} }));
};
-export const addProject = (data: ProjectCreateFormDialogData) =>
- async (dispatch: Dispatch) => {
- await dispatch<any>(createProject(data));
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Project has been successfully created.",
- hideDuration: 2000
- }));
- };
-
-
-const createProject = (project: Partial<ProjectResource>) =>
+export const createProject = (project: Partial<ProjectResource>) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(PROJECT_CREATE_FORM_NAME));
try {
const newProject = await services.projectService.create(project);
- dispatch<any>(getProjectList(newProject.ownerUuid));
- dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
dispatch(reset(PROJECT_CREATE_FORM_NAME));
+ return newProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' }));
}
+ return undefined;
}
- };
\ No newline at end of file
+ };
import { ServiceRepository } from '~/services/services';
import { RootState } from '~/store/store';
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { projectPanelActions } from '~/store/project-panel/project-panel-action';
-import { getProjectList } from '~/store/project/project-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
-import { findTreeItem } from '~/store/project/project-reducer';
export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
try {
const project = await services.projectService.get(resource.uuid);
- await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
- dispatch(projectPanelActions.REQUEST_ITEMS());
- dispatch<any>(getProjectList(project.ownerUuid));
- const { projects } = getState();
- if (findTreeItem(projects.items, resource.ownerUuid)) {
- dispatch<any>(getProjectList(resource.ownerUuid));
- }
+ const newProject = await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000 }));
+ return newProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' }));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the project.', hideDuration: 2000 }));
+ throw new Error('Could not move the project.');
}
+ return;
}
};
import { Dispatch } from "redux";
import { initialize, startSubmit, stopSubmit } from 'redux-form';
import { RootState } from "~/store/store";
-import { updateDetails } from "~/store/details-panel/details-panel-action";
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
import { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
import { ServiceRepository } from "~/services/services";
import { ProjectResource } from '~/models/project';
-import { getProjectList } from '~/store/project/project-action';
-import { projectPanelActions } from '~/store/project-panel/project-panel-action';
export interface ProjectUpdateFormDialogData {
uuid: string;
dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {} }));
};
-export const editProject = (data: ProjectUpdateFormDialogData) =>
- async (dispatch: Dispatch) => {
- await dispatch<any>(updateProject(data));
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Project has been successfully updated.",
- hideDuration: 2000
- }));
- };
-
export const updateProject = (project: Partial<ProjectResource>) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const uuid = project.uuid || '';
dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
try {
const updatedProject = await services.projectService.update(uuid, project);
- dispatch(projectPanelActions.REQUEST_ITEMS());
- dispatch<any>(getProjectList(updatedProject.ownerUuid));
- dispatch<any>(updateDetails(updatedProject));
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
+ return updatedProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' }));
}
+ return ;
}
};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+
+export const propertiesActions = unionize({
+ SET_PROPERTY: ofType<{ key: string, value: any }>(),
+ DELETE_PROPERTY: ofType<string>(),
+});
+
+export type PropertiesAction = UnionOf<typeof propertiesActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PropertiesState, setProperty, deleteProperty } from './properties';
+import { PropertiesAction, propertiesActions } from './properties-actions';
+
+
+export const propertiesReducer = (state: PropertiesState = {}, action: PropertiesAction) =>
+ propertiesActions.match(action, {
+ SET_PROPERTY: ({ key, value }) => setProperty(key, value)(state),
+ DELETE_PROPERTY: key => deleteProperty(key)(state),
+ default: () => state,
+ });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type PropertiesState = { [key: string]: any };
+
+export const getProperty = <T>(id: string) =>
+ (state: PropertiesState): T | undefined =>
+ state[id];
+
+export const setProperty = <T>(id: string, data: T) =>
+ (state: PropertiesState) => ({
+ ...state,
+ [id]: data
+ });
+
+export const deleteProperty = (id: string) =>
+ (state: PropertiesState) => {
+ const newState = { ...state };
+ delete newState[id];
+ return newState;
+ };
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { Resource, extractUuidKind } from '~/models/resource';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { getResourceService } from '~/services/services';
+
+export const resourcesActions = unionize({
+ SET_RESOURCES: ofType<Resource[]>(),
+ DELETE_RESOURCES: ofType<string[]>()
+});
+
+export type ResourcesAction = UnionOf<typeof resourcesActions>;
+
+export const updateResources = (resources: Resource[]) => resourcesActions.SET_RESOURCES(resources);
+
+export const loadResource = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const kind = extractUuidKind(uuid);
+ const service = getResourceService(kind)(services);
+ if (service) {
+ const resource = await service.get(uuid);
+ dispatch<any>(updateResources([resource]));
+ return resource;
+ }
+ return undefined;
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourcesState, setResource, deleteResource } from './resources';
+import { ResourcesAction, resourcesActions } from './resources-actions';
+
+export const resourcesReducer = (state: ResourcesState = {}, action: ResourcesAction) =>
+ resourcesActions.match(action, {
+ SET_RESOURCES: resources => resources.reduce((state, resource) => setResource(resource.uuid, resource)(state), state),
+ DELETE_RESOURCES: ids => ids.reduce((state, id) => deleteResource(id)(state), state),
+ default: () => state,
+ });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "~/models/resource";
+import { ResourceKind } from '../../models/resource';
+
+export type ResourcesState = { [key: string]: Resource };
+
+export const getResource = <T extends Resource = Resource>(id: string) =>
+ (state: ResourcesState): T | undefined =>
+ state[id] as T;
+
+export const setResource = <T extends Resource>(id: string, data: T) =>
+ (state: ResourcesState) => ({
+ ...state,
+ [id]: data
+ });
+
+export const deleteResource = (id: string) =>
+ (state: ResourcesState) => {
+ const newState = {...state};
+ delete newState[id];
+ return newState;
+ };
+
+export const filterResources = (filter: (resource: Resource) => boolean) =>
+ (state: ResourcesState) =>
+ Object
+ .keys(state)
+ .map(id => getResource(id)(state))
+ .filter(filter);
+
+export const filterResourcesByKind = (kind: ResourceKind) =>
+ (state: ResourcesState) =>
+ filterResources(resource => resource.kind === kind)(state);
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
+import { createTreePickerNode, TreePickerNode } from '~/store/tree-picker/tree-picker';
+import { RootState } from '../store';
+import { ServiceRepository } from '~/services/services';
+import { FilterBuilder } from '~/common/api/filter-builder';
+import { resourcesActions } from '../resources/resources-actions';
+import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
+import { TreeItemStatus } from "~/components/tree/tree";
+import { getNodeAncestors, getNodeValue, getNodeAncestorsIds, getNode } from '~/models/tree';
+import { ProjectResource } from '~/models/project';
+
+export enum SidePanelTreeCategory {
+ PROJECTS = 'Projects',
+ SHARED_WITH_ME = 'Shared with me',
+ WORKFLOWS = 'Workflows',
+ RECENT_OPEN = 'Recent open',
+ FAVORITES = 'Favorites',
+ TRASH = 'Trash'
+}
+
+export const SIDE_PANEL_TREE = 'sidePanelTree';
+
+export const getSidePanelTree = (treePicker: TreePicker) =>
+ getTreePicker<ProjectResource | string>(SIDE_PANEL_TREE)(treePicker);
+
+export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array<TreePickerNode<ProjectResource | string>> => {
+ const tree = getSidePanelTree(treePicker);
+ if (tree) {
+ const ancestors = getNodeAncestors(uuid)(tree).map(node => node.value);
+ const node = getNodeValue(uuid)(tree);
+ if (node) {
+ return [...ancestors, node];
+ }
+ }
+ return [];
+};
+
+const SIDE_PANEL_CATEGORIES = [
+ SidePanelTreeCategory.SHARED_WITH_ME,
+ SidePanelTreeCategory.WORKFLOWS,
+ SidePanelTreeCategory.RECENT_OPEN,
+ SidePanelTreeCategory.FAVORITES,
+ SidePanelTreeCategory.TRASH,
+];
+
+export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
+
+export const initSidePanelTree = () =>
+ (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
+ const rootProjectUuid = authService.getUuid() || '';
+ const nodes = SIDE_PANEL_CATEGORIES.map(nodeId => createTreePickerNode({ nodeId, value: nodeId }));
+ const projectsNode = createTreePickerNode({ nodeId: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: '',
+ pickerId: SIDE_PANEL_TREE,
+ nodes: [projectsNode, ...nodes]
+ }));
+ SIDE_PANEL_CATEGORIES.forEach(category => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: category,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: []
+ }));
+ });
+ };
+
+export const loadSidePanelTreeProjects = (projectUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const treePicker = getTreePicker(SIDE_PANEL_TREE)(getState().treePicker);
+ const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
+ if (node || projectUuid === '') {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: projectUuid, pickerId: SIDE_PANEL_TREE }));
+ const params = {
+ filters: new FilterBuilder()
+ .addEqual('ownerUuid', projectUuid)
+ .getFilters()
+ };
+ const { items } = await services.projectService.list(params);
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: projectUuid,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: items.map(item => createTreePickerNode({ nodeId: item.uuid, value: item })),
+ }));
+ dispatch(resourcesActions.SET_RESOURCES(items));
+ }
+ };
+
+export const activateSidePanelTreeItem = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && !node.selected) {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ }
+ if (!isSidePanelTreeCategory(nodeId)) {
+ await dispatch<any>(activateSidePanelTreeProject(nodeId));
+ }
+ };
+
+export const activateSidePanelTreeProject = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { treePicker } = getState();
+ const node = getSidePanelTreeNode(nodeId)(treePicker);
+ if (node && node.status !== TreeItemStatus.LOADED) {
+ await dispatch<any>(loadSidePanelTreeProjects(nodeId));
+ } else if (node === undefined) {
+ await dispatch<any>(activateSidePanelTreeBranch(nodeId));
+ }
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+ nodeIds: getSidePanelTreeNodeAncestorsIds(nodeId)(treePicker),
+ pickerId: SIDE_PANEL_TREE
+ }));
+ dispatch<any>(expandSidePanelTreeItem(nodeId));
+ };
+
+export const activateSidePanelTreeBranch = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const ancestors = await services.ancestorsService.ancestors(nodeId, services.authService.getUuid() || '');
+ for (const ancestor of ancestors) {
+ await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
+ }
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+ nodeIds: ancestors.map(ancestor => ancestor.uuid),
+ pickerId: SIDE_PANEL_TREE
+ }));
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ };
+
+export const toggleSidePanelTreeItemCollapse = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && node.status === TreeItemStatus.INITIAL) {
+ await dispatch<any>(loadSidePanelTreeProjects(node.nodeId));
+ }
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ };
+
+export const expandSidePanelTreeItem = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && node.collapsed) {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ }
+ };
+
+const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
+ const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
+ return sidePanelTree
+ ? getNodeValue(nodeId)(sidePanelTree)
+ : undefined;
+};
+
+const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
+ const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
+ return sidePanelTree
+ ? getNodeAncestorsIds(nodeId)(sidePanelTree)
+ : [];
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
-import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+import { Dispatch } from 'redux';
+import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { navigateToFavorites, navigateTo } from '../navigation/navigation-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-export const sidePanelActions = unionize({
- TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
-}, {
- tag: 'type',
- value: 'payload'
-});
+export const navigateFromSidePanel = (id: string) =>
+ (dispatch: Dispatch) => {
+ if (isSidePanelTreeCategory(id)) {
+ dispatch<any>(getSidePanelTreeCategoryAction(id));
+ } else {
+ dispatch<any>(navigateTo(id));
+ }
+ };
-export type SidePanelAction = UnionOf<typeof sidePanelActions>;
+const getSidePanelTreeCategoryAction = (id: string) => {
+ switch (id) {
+ case SidePanelTreeCategory.FAVORITES:
+ return navigateToFavorites;
+ default:
+ return sidePanelTreeCategoryNotAvailable(id);
+ }
+};
+
+const sidePanelTreeCategoryNotAvailable = (id: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `${id} not available`,
+ hideDuration: 3000,
+ });
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelReducer } from "./side-panel-reducer";
-import { sidePanelActions } from "./side-panel-action";
-import { ProjectsIcon } from "~/components/icon/icon";
-
-describe('side-panel-reducer', () => {
- it('should open side-panel item', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- url: "/projects",
- icon: ProjectsIcon,
- open: false
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: true,
- url: "/projects"
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
- expect(state).toEqual(project);
- });
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-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 { Dispatch } from "redux";
-import { push } from "react-router-redux";
-import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
-import { projectPanelActions } from "../project-panel/project-panel-action";
-import { projectActions } from "../project/project-action";
-import { getProjectUrl } from "../../models/project";
-import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
-import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
-
-export type SidePanelState = SidePanelItem[];
-
-export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
- return sidePanelActions.match(action, {
- TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
- state.map(it => ({...it, open: itemId === it.id && it.open === false})),
- default: () => state
- });
-};
-
-export enum SidePanelId {
- PROJECTS = "Projects",
- SHARED_WITH_ME = "SharedWithMe",
- WORKFLOWS = "Workflows",
- RECENT_OPEN = "RecentOpen",
- FAVORITES = "Favourites",
- TRASH = "Trash"
-}
-
-export const sidePanelItems = [
- {
- id: SidePanelId.PROJECTS,
- name: "Projects",
- url: "/projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- margin: true,
- openAble: true,
- activeAction: (dispatch: Dispatch, uuid: string) => {
- dispatch(push(getProjectUrl(uuid)));
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
- dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }
- },
- {
- id: SidePanelId.SHARED_WITH_ME,
- name: "Shared with me",
- url: "/shared",
- icon: ShareMeIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/shared"));
- }
- },
- {
- id: SidePanelId.WORKFLOWS,
- name: "Workflows",
- url: "/workflows",
- icon: WorkflowIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/workflows"));
- }
- },
- {
- id: SidePanelId.RECENT_OPEN,
- name: "Recent open",
- url: "/recent",
- icon: RecentIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/recent"));
- }
- },
- {
- id: SidePanelId.FAVORITES,
- name: "Favorites",
- url: "/favorites",
- icon: FavoriteIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/favorites"));
- dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
- dispatch(favoritePanelActions.RESET_PAGINATION());
- dispatch(favoritePanelActions.REQUEST_ITEMS());
- }
- },
- {
- id: SidePanelId.TRASH,
- name: "Trash",
- url: "/trash",
- icon: TrashIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/trash"));
- }
- }
-];
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
export const snackbarActions = unionize({
OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
CLOSE_SNACKBAR: ofType<{}>()
-}, { tag: 'type', value: 'payload' });
+});
export type SnackbarAction = UnionOf<typeof snackbarActions>;
export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
return snackbarActions.match(action, {
- OPEN_SNACKBAR: data => ({ ...data, open: true }),
+ OPEN_SNACKBAR: data => ({ ...initialState, ...data, open: true }),
CLOSE_SNACKBAR: () => initialState,
default: () => state,
});
// SPDX-License-Identifier: AGPL-3.0
import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
-import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
+import { routerMiddleware, routerReducer } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
-import { projectsReducer, ProjectState } from "./project/project-reducer";
-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 { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
-import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
+import { authReducer } from "./auth/auth-reducer";
+import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
+import { detailsPanelReducer } from './details-panel/details-panel-reducer';
+import { contextMenuReducer } from './context-menu/context-menu-reducer';
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 { favoritesReducer } from './favorites/favorites-reducer';
+import { snackbarReducer } from './snackbar/snackbar-reducer';
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 { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { dialogReducer } from './dialog/dialog-reducer';
+import { collectionsReducer } from './collections/collections-reducer';
import { ServiceRepository } from "~/services/services";
import { treePickerReducer } from './tree-picker/tree-picker-reducer';
-import { TreePicker } from './tree-picker/tree-picker';
+import { resourcesReducer } from '~/store/resources/resources-reducer';
+import { propertiesReducer } from './properties/properties-reducer';
+import { RootState } from './store';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
-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;
- treePicker: TreePicker;
-}
+export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
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,
- treePicker: treePickerReducer,
- });
+ const rootReducer = createRootReducer(services);
const projectPanelMiddleware = dataExplorerMiddleware(
new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
}
+
+const createRootReducer = (services: ServiceRepository) => combineReducers({
+ auth: authReducer(services),
+ collections: collectionsReducer,
+ router: routerReducer,
+ dataExplorer: dataExplorerReducer,
+ collectionPanel: collectionPanelReducer,
+ detailsPanel: detailsPanelReducer,
+ contextMenu: contextMenuReducer,
+ form: formReducer,
+ favorites: favoritesReducer,
+ snackbar: snackbarReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ dialog: dialogReducer,
+ treePicker: treePickerReducer,
+ resources: resourcesReducer,
+ properties: propertiesReducer,
+});
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { TreePickerNode } from "./tree-picker";
LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ nodeId: string, nodes: Array<TreePickerNode>, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ nodeId: string, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ nodeId: string, pickerId: string }>(),
+ EXPAND_TREE_PICKER_NODES: ofType<{ nodeIds: string[], pickerId: string }>(),
RESET_TREE_PICKER: ofType<{ pickerId: string }>()
-}, {
- tag: 'type',
- value: 'payload'
- });
+});
export type TreePickerAction = UnionOf<typeof treePickerActions>;
import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
import { TreeItemStatus } from "~/components/tree/tree";
import { compose } from "redux";
+import { getNode } from '../../models/tree';
export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) =>
treePickerActions.match(action, {
updateOrCreatePicker(state, pickerId, setNodeValueWith(toggleCollapse)(nodeId)),
TOGGLE_TREE_PICKER_NODE_SELECT: ({ nodeId, pickerId }) =>
updateOrCreatePicker(state, pickerId, mapTreeValues(toggleSelect(nodeId))),
- RESET_TREE_PICKER: ({ pickerId }) =>
+ RESET_TREE_PICKER: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, createTree),
+ EXPAND_TREE_PICKER_NODES: ({ pickerId, nodeIds }) =>
+ updateOrCreatePicker(state, pickerId, mapTreeValues(expand(nodeIds))),
default: () => state
});
return { ...state, [pickerId]: updatedPicker };
};
+const expand = (ids: string[]) => (node: TreePickerNode): TreePickerNode =>
+ ids.some(id => id === node.nodeId)
+ ? { ...node, collapsed: false }
+ : node;
+
const setPending = (value: TreePickerNode): TreePickerNode =>
({ ...value, status: TreeItemStatus.PENDING });
? ({ ...value, selected: !value.selected })
: ({ ...value, selected: false });
-const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: Tree<TreePickerNode>) =>
- nodes.reduce((tree, node) =>
- setNode(
- createTreeNode(parent)(node)
- )(tree), state);
+const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: Tree<TreePickerNode>) => {
+ const parentNode = getNode(parent)(state);
+ let newState = state;
+ if (parentNode) {
+ newState = setNode({ ...parentNode, children: [] })(state);
+ }
+ return nodes.reduce((tree, node) => {
+ const oldNode = getNode(node.nodeId)(state) || { value: {} };
+ const newNode = createTreeNode(parent)(node);
+ const value = { ...oldNode.value, ...newNode.value };
+ return setNode({ ...newNode, value })(tree);
+ }, newState);
+};
const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode<TreePickerNode> => ({
children: [],
export type TreePicker = { [key: string]: Tree<TreePickerNode> };
-export interface TreePickerNode {
+export interface TreePickerNode<Value = any> {
nodeId: string;
- value: any;
+ value: Value;
selected: boolean;
collapsed: boolean;
status: TreeItemStatus;
collapsed: true,
status: TreeItemStatus.INITIAL
});
+
+export const getTreePicker = <Value = {}>(id: string) => (state: TreePicker): Tree<TreePickerNode<Value>> | undefined => state[id];
\ 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 { RootState } from "../store";
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { loadCollectionPanel } from '~/store/collection-panel/collection-panel-action';
+import { snackbarActions } from '../snackbar/snackbar-actions';
+import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action';
+import { openProjectPanel, projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
+import { loadResource, updateResources } from '../resources/resources-actions';
+import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
+import { projectPanelColumns } from '~/views/project-panel/project-panel';
+import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
+import { matchRootRoute } from '~/routes/routes';
+import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { navigateToProject } from '../navigation/navigation-action';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { ServiceRepository } from '~/services/services';
+import { getResource } from '../resources/resources';
+import { getProjectPanelCurrentUuid } from '../project-panel/project-panel-action';
+import * as projectCreateActions from '~/store/projects/project-create-actions';
+import * as projectMoveActions from '~/store/projects/project-move-actions';
+import * as projectUpdateActions from '~/store/projects/project-update-actions';
+import * as collectionCreateActions from '~/store/collections/collection-create-actions';
+import * as collectionCopyActions from '~/store/collections/collection-copy-actions';
+import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
+import * as collectionMoveActions from '~/store/collections/collection-move-actions';
+
+
+export const loadWorkbench = () =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const { auth, router } = getState();
+ const { user } = auth;
+ if (user) {
+ const userResource = await dispatch<any>(loadResource(user.uuid));
+ if (userResource) {
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+ dispatch<any>(initSidePanelTree());
+ if (router.location) {
+ const match = matchRootRoute(router.location.pathname);
+ if (match) {
+ dispatch(navigateToProject(userResource.uuid));
+ }
+ }
+ } else {
+ dispatch(userIsNotAuthenticated);
+ }
+ } else {
+ dispatch(userIsNotAuthenticated);
+ }
+ };
+
+export const loadFavorites = () =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+ dispatch<any>(loadFavoritePanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
+ };
+
+
+export const loadProject = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ await dispatch<any>(activateSidePanelTreeItem(uuid));
+ dispatch<any>(setProjectBreadcrumbs(uuid));
+ dispatch<any>(openProjectPanel(uuid));
+ dispatch(loadDetailsPanel(uuid));
+ };
+
+export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ const newProject = await dispatch<any>(projectCreateActions.createProject(data));
+ if (newProject) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully created.",
+ hideDuration: 2000
+ }));
+ await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
+ }
+ };
+
+export const moveProject = (data: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const oldProject = getResource(data.uuid)(getState().resources);
+ const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
+ const movedProject = await dispatch<any>(projectMoveActions.moveProject(data));
+ if (movedProject) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000 }));
+ if (oldProject) {
+ await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
+ }
+ dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
+ }
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
+ if (updatedProject) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully updated.",
+ hideDuration: 2000
+ }));
+ await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
+ }
+ };
+
+export const loadCollection = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(loadCollectionPanel(uuid));
+ await dispatch<any>(activateSidePanelTreeItem(collection.ownerUuid));
+ dispatch<any>(setCollectionBreadcrumbs(collection.uuid));
+ dispatch(loadDetailsPanel(uuid));
+ };
+
+export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
+ if (collection) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully created.",
+ hideDuration: 2000
+ }));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ }
+ };
+
+export const updateCollection = (data: collectionUpdateActions.CollectionUpdateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
+ if (collection) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ }
+ };
+
+export const copyCollection = (data: collectionCopyActions.CollectionCopyFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const moveCollection = (data: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const collection = await dispatch<any>(collectionMoveActions.moveCollection(data));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const resourceIsNotLoaded = (uuid: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `Resource identified by ${uuid} is not loaded.`
+ });
+
+export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
+ message: 'User is not authenticated'
+});
+
+export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not load user'
+});
+
+const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
+ if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
+ dispatch<any>(loadProject(currentProjectPanelUuid));
+ }
+ };
\ No newline at end of file
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
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";
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
this.props.dispatch(saveApiToken(apiToken));
- this.props.dispatch<any>(getUserDetails()).then(() => {
- const rootUuid = this.props.authService.getRootUuid();
- this.props.dispatch(getProjectList(rootUuid));
- });
+ this.props.dispatch<any>(getUserDetails());
}
render() {
return <Redirect to="/"/>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Breadcrumbs as BreadcrumbsComponent, BreadcrumbsProps } from '~/components/breadcrumbs/breadcrumbs';
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { getProperty } from '../../store/properties/properties';
+import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
+import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
+
+type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items'>;
+type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
+
+const mapStateToProps = () => ({ properties }: RootState): BreadcrumbsDataProps => ({
+ items: getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
+ onClick: ({ uuid }: ResourceBreadcrumb) => {
+ dispatch<any>(navigateTo(uuid));
+ },
+ onContextMenu: (event, breadcrumb: ResourceBreadcrumb) => {
+ dispatch<any>(openSidePanelContextMenu(event, breadcrumb.uuid));
+ }
+});
+
+export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
import { Tree, getNodeChildrenIds, getNode } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
import { openUploadCollectionFilesDialog } from '~/store/collections/uploader/collection-uploader-actions';
+import { openContextMenu } from '../../store/context-menu/context-menu-actions';
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
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 }
- }));
+ dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }));
+ },
+ onOptionsMenuOpen: (event) => {
+ dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }));
},
- onOptionsMenuOpen: (event) =>
- dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
- position: { x: event.clientX, y: event.clientY },
- resource: { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }
- }))
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { reset, initialize } from "redux-form";
-
import { ContextMenuActionSet } from "../context-menu-action-set";
import { NewProjectIcon, RenameIcon, CopyIcon, MoveToIcon } from "~/components/icon/icon";
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { openMoveProjectDialog } from '~/store/projects/project-move-actions';
-import { PROJECT_CREATE_FORM_NAME, openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
export const projectActionSet: ContextMenuActionSet = [[
icon: NewProjectIcon,
name: "New project",
execute: (dispatch, resource) => {
- dispatch(reset(PROJECT_CREATE_FORM_NAME));
dispatch<any>(openProjectCreateDialog(resource.uuid));
}
},
//
// SPDX-License-Identifier: AGPL-3.0
-import { reset } from "redux-form";
-
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions } from "~/store/project/project-action";
-import { COLLECTION_CREATE_FORM_NAME, openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
import { NewProjectIcon, CollectionIcon } from "~/components/icon/icon";
-import { PROJECT_CREATE_FORM_NAME, openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
export const rootProjectActionSet: ContextMenuActionSet = [[
{
icon: NewProjectIcon,
name: "New project",
execute: (dispatch, resource) => {
- dispatch(reset(PROJECT_CREATE_FORM_NAME));
dispatch<any>(openProjectCreateDialog(resource.uuid));
}
},
icon: CollectionIcon,
name: "New Collection",
execute: (dispatch, resource) => {
- dispatch(reset(COLLECTION_CREATE_FORM_NAME));
dispatch<any>(openCollectionCreateDialog(resource.uuid));
}
}
interface Props {
id: string;
- columns: DataColumns<any>;
onRowClick: (item: any) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
onRowDoubleClick: (item: any) => void;
extractKey?: (item: any) => React.Key;
}
-const mapStateToProps = (state: RootState, { id, columns }: Props) => {
- const s = getDataExplorer(state.dataExplorer, id);
- if (s.columns.length === 0) {
- s.columns = columns;
- }
- return s;
+const mapStateToProps = (state: RootState, { id }: Props) => {
+ return getDataExplorer(state.dataExplorer, id);
};
const mapDispatchToProps = () => {
- return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+ return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
onSetColumns: (columns: DataColumns<any>) => {
dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
},
import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize } from '~/common/formatters';
import { resourceLabel } from '~/common/labels';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../../store/resources/resources';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { ProcessResource } from '~/models/process';
-export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+export const renderName = (item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
<Grid item>
{renderIcon(item)}
</Grid>
</Grid>;
+export const ResourceName = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return resource || { name: '', uuid: '', kind: '' };
+ })(renderName);
-export const renderIcon = (item: {kind: string}) => {
+export const renderIcon = (item: { kind: string }) => {
switch (item.kind) {
case ResourceKind.PROJECT:
return <ProjectIcon />;
return <Typography noWrap>{formatDate(date)}</Typography>;
};
+export const ResourceLastModifiedDate = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { date: resource ? resource.modifiedAt : '' };
+ })((props: { date: string }) => renderDate(props.date));
+
export const renderFileSize = (fileSize?: number) =>
<Typography noWrap>
{formatFileSize(fileSize)}
</Typography>;
+export const ResourceFileSize = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return {};
+ })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
+
export const renderOwner = (owner: string) =>
<Typography noWrap color="primary" >
{owner}
</Typography>;
+export const ResourceOwner = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { owner: resource ? resource.ownerUuid : '' };
+ })((props: { owner: string }) => renderOwner(props.owner));
+
export const renderType = (type: string) =>
<Typography noWrap>
{resourceLabel(type)}
</Typography>;
-export const renderStatus = (item: {status?: string}) =>
+export const ResourceType = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { type: resource ? resource.kind : '' };
+ })((props: { type: string }) => renderType(props.type));
+
+export const renderStatus = (item: { status?: string }) =>
<Typography noWrap align="center" >
{item.status || "-"}
</Typography>;
+
+export const ProcessStatus = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+ return { status: resource ? resource.state : '-' };
+ })((props: { status: string }) => renderType(props.status));
import { EmptyDetails } from "./empty-details";
import { DetailsData } from "./details-data";
import { DetailsResource } from "~/models/details";
+import { getResource } from '../../store/resources/resources';
type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
}
};
-const mapStateToProps = ({ detailsPanel }: RootState) => ({
- isOpened: detailsPanel.isOpened,
- item: getItem(detailsPanel.item as DetailsResource)
-});
+const mapStateToProps = ({ detailsPanel, resources }: RootState) => {
+ const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+ return {
+ isOpened: detailsPanel.isOpened,
+ item: getItem(resource)
+ };
+};
const mapDispatchToProps = (dispatch: Dispatch) => ({
onCloseDrawer: () => {
const { tabsValue } = this.state;
return (
<Typography component="div"
- className={classnames([classes.container, { [classes.opened]: isOpened }])}>
+ className={classnames([classes.container, { [classes.opened]: isOpened }])}>
<Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
<Typography component="div" className={classes.headerContainer}>
<Grid container alignItems='center' justify='space-around'>
</Grid>
<Grid item>
<IconButton color="inherit" onClick={onCloseDrawer}>
- {<CloseIcon/>}
+ {<CloseIcon />}
</IconButton>
</Grid>
</Grid>
</Typography>
<Tabs value={tabsValue} onChange={this.handleChange}>
- <Tab disableRipple label="Details"/>
- <Tab disableRipple label="Activity" disabled/>
+ <Tab disableRipple label="Details" />
+ <Tab disableRipple label="Activity" disabled />
</Tabs>
{tabsValue === 0 && this.renderTabContainer(
<Grid container direction="column">
</Grid>
)}
{tabsValue === 1 && this.renderTabContainer(
- <Grid container direction="column"/>
+ <Grid container direction="column" />
)}
</Drawer>
</Typography>
import { compose } from "redux";
import { withDialog } from "~/store/dialog/with-dialog";
import { reduxForm } from 'redux-form';
-import { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData, copyCollection } from '~/store/collections/collection-copy-actions';
+import { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData } from '~/store/collections/collection-copy-actions';
import { DialogCollectionCopy } from "~/views-components/dialog-copy/dialog-collection-copy";
+import { copyCollection } from '~/store/workbench/workbench-actions';
export const CopyCollectionDialog = compose(
withDialog(COLLECTION_COPY_FORM_NAME),
import { compose } from "redux";
import { reduxForm } from 'redux-form';
import { withDialog } from "~/store/dialog/with-dialog";
-import { addCollection, COLLECTION_CREATE_FORM_NAME, CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
-import { UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
+import { COLLECTION_CREATE_FORM_NAME, CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
import { DialogCollectionCreate } from "~/views-components/dialog-create/dialog-collection-create";
+import { createCollection } from "~/store/workbench/workbench-actions";
export const CreateCollectionDialog = compose(
withDialog(COLLECTION_CREATE_FORM_NAME),
reduxForm<CollectionCreateFormDialogData>({
form: COLLECTION_CREATE_FORM_NAME,
onSubmit: (data, dispatch) => {
- dispatch(addCollection(data));
+ dispatch(createCollection(data));
}
})
)(DialogCollectionCreate);
import { compose } from "redux";
import { reduxForm } from 'redux-form';
import { withDialog } from "~/store/dialog/with-dialog";
-import { addProject, PROJECT_CREATE_FORM_NAME, ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
+import { PROJECT_CREATE_FORM_NAME, ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
import { DialogProjectCreate } from '~/views-components/dialog-create/dialog-project-create';
+import { createProject } from "~/store/workbench/workbench-actions";
export const CreateProjectDialog = compose(
withDialog(PROJECT_CREATE_FORM_NAME),
reduxForm<ProjectCreateFormDialogData>({
form: PROJECT_CREATE_FORM_NAME,
onSubmit: (data, dispatch) => {
- dispatch(addProject(data));
+ dispatch(createProject(data));
}
})
)(DialogProjectCreate);
\ No newline at end of file
import { withDialog } from "~/store/dialog/with-dialog";
import { reduxForm } from 'redux-form';
import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
-import { COLLECTION_MOVE_FORM_NAME, moveCollection } from '~/store/collections/collection-move-actions';
+import { COLLECTION_MOVE_FORM_NAME } from '~/store/collections/collection-move-actions';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { moveCollection } from '~/store/workbench/workbench-actions';
export const MoveCollectionDialog = compose(
withDialog(COLLECTION_MOVE_FORM_NAME),
import { withDialog } from "~/store/dialog/with-dialog";
import { reduxForm } from 'redux-form';
import { PROJECT_MOVE_FORM_NAME } from '~/store/projects/project-move-actions';
-import { moveProject } from '~/store/projects/project-move-actions';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
+import { moveProject } from '~/store/workbench/workbench-actions';
export const MoveProjectDialog = compose(
withDialog(PROJECT_MOVE_FORM_NAME),
import { reduxForm } from 'redux-form';
import { withDialog } from "~/store/dialog/with-dialog";
import { DialogCollectionUpdate } from '~/views-components/dialog-update/dialog-collection-update';
-import { editCollection, COLLECTION_UPDATE_FORM_NAME, CollectionUpdateFormDialogData } from '~/store/collections/collection-update-actions';
+import { COLLECTION_UPDATE_FORM_NAME, CollectionUpdateFormDialogData } from '~/store/collections/collection-update-actions';
+import { updateCollection } from "~/store/workbench/workbench-actions";
export const UpdateCollectionDialog = compose(
withDialog(COLLECTION_UPDATE_FORM_NAME),
reduxForm<CollectionUpdateFormDialogData>({
form: COLLECTION_UPDATE_FORM_NAME,
onSubmit: (data, dispatch) => {
- dispatch(editCollection(data));
+ dispatch(updateCollection(data));
}
})
)(DialogCollectionUpdate);
\ No newline at end of file
import { reduxForm } from 'redux-form';
import { withDialog } from "~/store/dialog/with-dialog";
import { DialogProjectUpdate } from '~/views-components/dialog-update/dialog-project-update';
-import { editProject, PROJECT_UPDATE_FORM_NAME, ProjectUpdateFormDialogData } from '~/store/projects/project-update-actions';
+import { PROJECT_UPDATE_FORM_NAME, ProjectUpdateFormDialogData } from '~/store/projects/project-update-actions';
+import { updateProject } from '~/store/workbench/workbench-actions';
export const UpdateProjectDialog = compose(
withDialog(PROJECT_UPDATE_FORM_NAME),
reduxForm<ProjectUpdateFormDialogData>({
form: PROJECT_UPDATE_FORM_NAME,
onSubmit: (data, dispatch) => {
- dispatch(editProject(data));
+ dispatch(updateProject(data));
}
})
)(DialogProjectUpdate);
\ No newline at end of file
import * as React from "react";
import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
-import { MainAppBar } from "./main-app-bar";
+import { MainAppBar, MainAppBarProps } from './main-app-bar';
import { SearchBar } from "~/components/search-bar/search-bar";
import { Breadcrumbs } from "~/components/breadcrumbs/breadcrumbs";
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
it("renders all components and the menu for authenticated user if user prop has value", () => {
const mainAppBar = mount(
<MainAppBar
- user={user}
- onContextMenu={jest.fn()}
- onDetailsPanelToggle={jest.fn()}
- {...{ searchText: "", breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
+ {...mockMainAppBarProps({ user })}
/>
);
expect(mainAppBar.find(SearchBar)).toHaveLength(1);
const menuItems = { accountMenu: [], helpMenu: [], anonymousMenu: [{ label: 'Sign in' }] };
const mainAppBar = mount(
<MainAppBar
- menuItems={menuItems}
- onDetailsPanelToggle={jest.fn()}
- onContextMenu={jest.fn()}
- {...{ searchText: "", breadcrumbs: [], onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
+ {...mockMainAppBarProps({ user: undefined, menuItems })}
/>
);
expect(mainAppBar.find(SearchBar)).toHaveLength(0);
const onSearch = jest.fn();
const mainAppBar = mount(
<MainAppBar
- searchText="search text"
- searchDebounce={2000}
- onContextMenu={jest.fn()}
- onSearch={onSearch}
- onDetailsPanelToggle={jest.fn()}
- {...{ user, breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
+ {...mockMainAppBarProps({ searchText: 'search text', searchDebounce: 2000, onSearch, user })}
/>
);
const searchBar = mainAppBar.find(SearchBar);
expect(onSearch).toBeCalledWith("new search text");
});
- it("communicates with <Breadcrumbs />", () => {
- const items = [{ label: "breadcrumb 1" }];
- const onBreadcrumbClick = jest.fn();
- const mainAppBar = mount(
- <MainAppBar
- breadcrumbs={items}
- onContextMenu={jest.fn()}
- onBreadcrumbClick={onBreadcrumbClick}
- onDetailsPanelToggle={jest.fn()}
- {...{ user, searchText: "", menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onMenuItemClick: jest.fn() }}
- />
- );
- const breadcrumbs = mainAppBar.find(Breadcrumbs);
- expect(breadcrumbs.prop("items")).toBe(items);
- breadcrumbs.prop("onClick")(items[0]);
- expect(onBreadcrumbClick).toBeCalledWith(items[0]);
- });
-
it("communicates with menu", () => {
const onMenuItemClick = jest.fn();
- const menuItems = { accountMenu: [{label: "log out"}], helpMenu: [], anonymousMenu: [] };
+ const menuItems = { accountMenu: [{ label: "log out" }], helpMenu: [], anonymousMenu: [] };
const mainAppBar = mount(
<MainAppBar
- menuItems={menuItems}
- onContextMenu={jest.fn()}
- onMenuItemClick={onMenuItemClick}
- onDetailsPanelToggle={jest.fn()}
- {...{ user, searchText: "", breadcrumbs: [], onSearch: jest.fn(), onBreadcrumbClick: jest.fn() }}
+ {...mockMainAppBarProps({ menuItems, onMenuItemClick, user })}
/>
);
expect(onMenuItemClick).toBeCalledWith(menuItems.accountMenu[0]);
});
});
+
+const Breadcrumbs = () => <span>Breadcrumbs</span>;
+
+const mockMainAppBarProps = (props: Partial<MainAppBarProps>): MainAppBarProps => ({
+ searchText: '',
+ breadcrumbs: Breadcrumbs,
+ menuItems: {
+ accountMenu: [],
+ helpMenu: [],
+ anonymousMenu: [],
+ },
+ buildInfo: '',
+ onSearch: jest.fn(),
+ onMenuItemClick: jest.fn(),
+ onDetailsPanelToggle: jest.fn(),
+ ...props,
+});
import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Button, MenuItem } from "@material-ui/core";
import { User, getUserFullname } from "~/models/user";
import { SearchBar } from "~/components/search-bar/search-bar";
-import { Breadcrumbs, Breadcrumb } from "~/components/breadcrumbs/breadcrumbs";
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
import { DetailsIcon, NotificationIcon, UserPanelIcon, HelpIcon } from "~/components/icon/icon";
interface MainAppBarDataProps {
searchText: string;
searchDebounce?: number;
- breadcrumbs: Breadcrumb[];
+ breadcrumbs: React.ComponentType<any>;
user?: User;
menuItems: MainAppBarMenuItems;
buildInfo: string;
export interface MainAppBarActionProps {
onSearch: (searchText: string) => void;
- onBreadcrumbClick: (breadcrumb: Breadcrumb) => void;
onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
onDetailsPanelToggle: () => void;
}
-type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps;
+export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps;
export const MainAppBar: React.SFC<MainAppBarProps> = (props) => {
return <AppBar position="static">
</Grid>
</Toolbar>
<Toolbar >
- {
- props.user && <Breadcrumbs
- items={props.breadcrumbs}
- onClick={props.onBreadcrumbClick}
- onContextMenu={props.onContextMenu} />
- }
- { props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
- <DetailsIcon />
- </IconButton>
+ {props.user && <props.breadcrumbs />}
+ {props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+ <DetailsIcon />
+ </IconButton>
}
</Toolbar>
</AppBar>;
import { FilterBuilder } from "~/common/api/filter-builder";
import { WrappedFieldProps } from 'redux-form';
-type ProjectTreePickerProps = Pick<TreePickerProps, 'toggleItemActive' | 'toggleItemOpen'>;
+type ProjectTreePickerProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
+ onContextMenu: () => { return; },
toggleItemActive: (nodeId, status, pickerId) => {
getNotSelectedTreePickerKind(pickerId)
.forEach(pickerId => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId })));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
+import { TreeItem } from "~/components/tree/tree";
+import { ProjectResource } from "~/models/project";
+import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon';
+import { RecentIcon, WorkflowIcon } from '~/components/icon/icon';
+import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
+
+export interface SidePanelTreeProps {
+ onItemActivation: (id: string) => void;
+}
+
+type SidePanelTreeActionProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({
+ onContextMenu: (event, id) => {
+ dispatch<any>(openSidePanelContextMenu(event, id));
+ },
+ toggleItemActive: (nodeId) => {
+ dispatch<any>(activateSidePanelTreeItem(nodeId));
+ props.onItemActivation(nodeId);
+ },
+ toggleItemOpen: (nodeId) => {
+ dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
+ }
+});
+
+export const SidePanelTree = connect(undefined, mapDispatchToProps)(
+ (props: SidePanelTreeActionProps) =>
+ <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
+
+const renderSidePanelItem = (item: TreeItem<ProjectResource>) =>
+ <ListItemTextIcon
+ icon={getProjectPickerIcon(item)}
+ name={typeof item.data === 'string' ? item.data : item.data.name}
+ isActive={item.active}
+ hasMargin={true} />;
+
+const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
+ typeof item.data === 'string'
+ ? getSidePanelIcon(item.data)
+ : ProjectIcon;
+
+const getSidePanelIcon = (category: string) => {
+ switch (category) {
+ case SidePanelTreeCategory.FAVORITES:
+ return FavoriteIcon;
+ case SidePanelTreeCategory.PROJECTS:
+ return ProjectsIcon;
+ case SidePanelTreeCategory.RECENT_OPEN:
+ return RecentIcon;
+ case SidePanelTreeCategory.SHARED_WITH_ME:
+ return ShareMeIcon;
+ case SidePanelTreeCategory.TRASH:
+ return TrashIcon;
+ case SidePanelTreeCategory.WORKFLOWS:
+ return WorkflowIcon;
+ default:
+ return ProjectIcon;
+ }
+};
--- /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 } from '@material-ui/core/styles';
+import Drawer from '@material-ui/core/Drawer';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
+import { compose, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+
+const DRAWER_WITDH = 240;
+
+type CssRules = 'drawerPaper' | 'toolbar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ drawerPaper: {
+ position: 'relative',
+ width: DRAWER_WITDH,
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: 58,
+ overflow: 'auto',
+ },
+ toolbar: theme.mixins.toolbar
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
+ onItemActivation: id => {
+ dispatch<any>(navigateFromSidePanel(id));
+ }
+});
+
+export const SidePanel = compose(
+ withStyles(styles),
+ connect(undefined, mapDispatchToProps)
+)(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
+ <Drawer
+ variant="permanent"
+ classes={{ paper: classes.drawerPaper }}>
+ <div className={classes.toolbar} />
+ <SidePanelTree {...props} />
+ </Drawer>);
export interface TreePickerProps {
pickerId: string;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, nodeId: string, pickerId: string) => void;
toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
}
-const mapStateToProps = (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
- const tree = state.treePicker[props.pickerId] || createTree();
- return {
- items: getNodeChildrenIds('')(tree)
- .map(treePickerToTreeItems(tree))
+const memoizedMapStateToProps = () => {
+ let prevTree: Ttree<TreePickerNode>;
+ let mappedProps: Pick<TreeProps<any>, 'items'>;
+ return (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
+ const tree = state.treePicker[props.pickerId] || createTree();
+ if(tree !== prevTree){
+ prevTree = tree;
+ mappedProps = {
+ items: getNodeChildrenIds('')(tree)
+ .map(treePickerToTreeItems(tree))
+ };
+ }
+ return mappedProps;
};
};
const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive'> => ({
- onContextMenu: () => { return; },
+ onContextMenu: (event, item) => props.onContextMenu(event, item.id, props.pickerId),
toggleItemActive: (id, status) => props.toggleItemActive(id, status, props.pickerId),
toggleItemOpen: (id, status) => props.toggleItemOpen(id, status, props.pickerId)
});
-export const TreePicker = connect(mapStateToProps, mapDispatchToProps)(Tree);
+export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
const treePickerToTreeItems = (tree: Ttree<TreePickerNode>) =>
(id: string): TreeItem<any> => {
import { CollectionTagForm } from './collection-tag-form';
import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { getResource } from '~/store/resources/resources';
+import { contextMenuActions, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
tags: TagResource[];
}
-interface CollectionPanelActionProps {
- onItemRouteChange: (collectionId: string) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
-}
-
-type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState) => ({
- item: state.collectionPanel.item,
- tags: state.collectionPanel.tags
- }))(
+ connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+ const collection = getResource(props.match.params.id)(state.resources);
+ return {
+ item: collection,
+ tags: state.collectionPanel.tags
+ };
+ })(
class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, tags, onContextMenu } = this.props;
+ const { classes, item, tags } = 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 classLabel={classes.label} classValue={classes.value}
- label='Collection UUID'
- value={item && item.uuid}>
- <Tooltip title="Copy uuid">
- <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy() }>
- <CopyIcon className={classes.copyIcon} />
- </CopyToClipboard>
- </Tooltip>
- </DetailsAttribute>
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Number of files' value='14' />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Content size' value='54 MB' />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Owner' value={item && item.ownerUuid} />
- </Grid>
+ <Card className={classes.card}>
+ <CardHeader
+ avatar={<CollectionIcon className={classes.iconHeader} />}
+ action={
+ <IconButton
+ aria-label="More options"
+ onClick={this.handleContextMenu}>
+ <MoreOptionsIcon />
+ </IconButton>
+ }
+ title={item && item.name}
+ subheader={item && item.description} />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={6}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Collection UUID'
+ value={item && item.uuid}>
+ <Tooltip title="Copy uuid">
+ <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy()}>
+ <CopyIcon className={classes.copyIcon} />
+ </CopyToClipboard>
+ </Tooltip>
+ </DetailsAttribute>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Number of files' value='14' />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Content size' value='54 MB' />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Owner' value={item && item.ownerUuid} />
</Grid>
- </CardContent>
- </Card>
+ </Grid>
+ </CardContent>
+ </Card>
- <Card className={classes.card}>
- <CardHeader title="Properties" />
- <CardContent>
- <Grid container direction="column">
- <Grid item xs={12}><CollectionTagForm /></Grid>
- <Grid item xs={12}>
- {
- tags.map(tag => {
- return <Chip key={tag.etag} className={classes.tag}
- onDelete={this.handleDelete(tag.uuid)}
- label={renderTagLabel(tag)} />;
- })
- }
- </Grid>
+ <Card className={classes.card}>
+ <CardHeader title="Properties" />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={12}><CollectionTagForm /></Grid>
+ <Grid item xs={12}>
+ {
+ tags.map(tag => {
+ return <Chip key={tag.etag} className={classes.tag}
+ onDelete={this.handleDelete(tag.uuid)}
+ label={renderTagLabel(tag)} />;
+ })
+ }
</Grid>
- </CardContent>
- </Card>
- <div className={classes.card}>
- <CollectionPanelFiles/>
- </div>
- </div>;
+ </Grid>
+ </CardContent>
+ </Card>
+ <div className={classes.card}>
+ <CollectionPanelFiles />
+ </div>
+ </div>;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<any>) => {
+ const { uuid, name, description } = this.props.item;
+ const resource = {
+ uuid,
+ name,
+ description,
+ kind: ContextMenuKind.COLLECTION
+ };
+ this.props.dispatch<any>(openContextMenu(event, resource));
}
handleDelete = (uuid: string) => () => {
}));
}
- componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
- if (!item || match.params.id !== item.uuid) {
- onItemRouteChange(match.params.id);
- }
- }
-
}
)
);
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface FavoritePanelItem {
- uuid: string;
- name: string;
- kind: string;
- url: string;
- owner: string;
- lastModified: string;
- fileSize?: number;
- status?: string;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem {
- return {
- uuid: r.uuid,
- name: r.name,
- kind: r.kind,
- url: "",
- owner: r.ownerUuid,
- lastModified: r.modifiedAt,
- status: r.kind === ResourceKind.PROCESS ? r.state : undefined
- };
-}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { FavoritePanelItem } from './favorite-panel-item';
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 { RouteComponentProps } from 'react-router';
-import { RootState } from '~/store/store';
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
import { ContainerRequestState } from '~/models/container-request';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
import { FavoriteIcon } from '~/components/icon/icon';
+import { Dispatch } from 'redux';
+import { contextMenuActions, openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
type CssRules = "toolbar" | "button";
type: ResourceKind | ContainerRequestState;
}
-export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
{
name: FavoritePanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: renderName,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
type: ContainerRequestState.UNCOMMITTED
}
],
- render: renderStatus,
+ render: uuid => <ProcessStatus uuid={uuid} />,
width: "75px"
},
{
type: ResourceKind.PROJECT
}
],
- render: item => renderType(item.kind),
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderOwner(item.owner),
+ render: uuid => <ResourceOwner uuid={uuid} />,
width: "200px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderFileSize(item.fileSize),
+ render: uuid => <ResourceFileSize uuid={uuid} />,
width: "50px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderDate(item.lastModified),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
width: "150px"
}
];
}
interface FavoritePanelActionProps {
- onItemClick: (item: FavoritePanelItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: FavoritePanelItem) => void;
+ onItemClick: (item: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
onDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: FavoritePanelItem) => void;
- onItemRouteChange: (itemId: string) => void;
+ onItemDoubleClick: (item: string) => void;
}
+const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
+ onContextMenu: (event, resourceUuid) => {
+ const kind = resourceKindToContextMenuKind(resourceUuid);
+ if (kind) {
+ dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+ }
+ },
+ onDialogOpen: (ownerUuid: string) => { return; },
+ onItemClick: (resourceUuid: string) => {
+ dispatch<any>(loadDetailsPanel(resourceUuid));
+ },
+ onItemDoubleClick: uuid => {
+ dispatch<any>(navigateTo(uuid));
+ }
+});
+
type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const FavoritePanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ connect(undefined, mapDispatchToProps)(
class extends React.Component<FavoritePanelProps> {
render() {
return <DataExplorer
id={FAVORITE_PANEL_ID}
- columns={columns}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
- extractKey={(item: FavoritePanelItem) => item.uuid}
defaultIcon={FavoriteIcon}
- defaultMessages={['Your favorites list is empty.']}/>
- ;
- }
-
- componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) {
- if (match.params.id !== currentItemId) {
- onItemRouteChange(match.params.id);
- }
+ defaultMessages={['Your favorites list is empty.']} />;
}
}
)
import { MoreOptionsIcon, ProcessIcon } from '~/components/icon/icon';
import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
import { RootState } from '~/store/store';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'content' | 'chip' | 'headerText' | 'link';
}))(
class extends React.Component<ProcessPanelProps> {
render() {
- const { classes, onContextMenu, item } = this.props;
+ const { classes } = this.props;
return <div>
<Card className={classes.card}>
action={
<IconButton
aria-label="More options"
- onClick={event => onContextMenu(event, item)}>
+ onClick={this.handleContextMenu}>
<MoreOptionsIcon />
</IconButton>
}
- title="Pipeline template that generates a config file from a template"
- />
+ title="Pipeline template that generates a config file from a template" />
<CardContent className={classes.content}>
<Grid container direction="column">
<Grid item xs={8}>
</Card>
</div>;
}
+
+ handleContextMenu = (event: React.MouseEvent<any>) => {
+ // const { uuid, name, description } = this.props.item;
+ const resource = {
+ uuid: '',
+ name: '',
+ description: '',
+ kind: ContextMenuKind.PROCESS
+ };
+ this.props.dispatch<any>(openContextMenu(event, resource));
+ }
}
)
);
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface ProjectPanelItem {
- uuid: string;
- name: string;
- description?: string;
- kind: string;
- url: string;
- owner: string;
- lastModified: string;
- fileSize?: number;
- status?: string;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
- return {
- uuid: r.uuid,
- name: r.name,
- description: r.description,
- kind: r.kind,
- url: "",
- owner: r.ownerUuid,
- lastModified: r.modifiedAt,
- status: r.kind === ResourceKind.PROCESS ? r.state : undefined
- };
-}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ProjectPanelItem } from './project-panel-item';
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 { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
-import { restoreBranch } from '~/store/navigation/navigation-action';
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
import { ProjectIcon } from '~/components/icon/icon';
+import { ResourceName } from '~/views-components/data-explorer/renderers';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { contextMenuActions, resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { CollectionResource } from '~/models/collection';
+import { ProjectResource } from '~/models/project';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { getProperty } from '~/store/properties/properties';
+import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
+import { openCollectionCreateDialog } from '../../store/collections/collection-create-actions';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
type CssRules = 'root' | "toolbar" | "button";
type: ResourceKind | ContainerRequestState;
}
-export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
{
name: ProjectPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: renderName,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
type: ContainerRequestState.UNCOMMITTED
}
],
- render: renderStatus,
+ render: uuid => <ProcessStatus uuid={uuid} />,
width: "75px"
},
{
type: ResourceKind.PROJECT
}
],
- render: item => renderType(item.kind),
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderOwner(item.owner),
+ render: uuid => <ResourceOwner uuid={uuid} />,
width: "200px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderFileSize(item.fileSize),
+ render: uuid => <ResourceFileSize uuid={uuid} />,
width: "50px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderDate(item.lastModified),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
width: "150px"
}
];
interface ProjectPanelDataProps {
currentItemId: string;
+ resources: ResourcesState;
}
-interface ProjectPanelActionProps {
- onItemClick: (item: ProjectPanelItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
- onProjectCreationDialogOpen: (ownerUuid: string) => void;
- onCollectionCreationDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: ProjectPanelItem) => void;
- onItemRouteChange: (itemId: string) => void;
-}
-
-type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const ProjectPanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ resources: state.resources
+ }))(
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
</div>
<DataExplorer
id={PROJECT_PANEL_ID}
- columns={columns}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
- extractKey={(item: ProjectPanelItem) => item.uuid}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
defaultIcon={ProjectIcon}
defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
</div>;
}
handleNewProjectClick = () => {
- this.props.onProjectCreationDialogOpen(this.props.currentItemId);
+ this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
}
handleNewCollectionClick = () => {
- this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
+ this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
}
- componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
- if (match.params.id !== currentItemId) {
- onItemRouteChange(match.params.id);
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const kind = resourceKindToContextMenuKind(resourceUuid);
+ if (kind) {
+ this.props.dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
}
}
- componentDidMount() {
- if (this.props.match.params.id && this.props.currentItemId === '') {
- this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
- }
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
}
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+
}
)
);
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
-import { Route, RouteComponentProps, Switch, Redirect } from "react-router";
+import { Route, Switch } from "react-router";
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 { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
import { push } from 'react-router-redux';
-import { reset } from 'redux-form';
-import { ProjectTree } from '~/views-components/project-tree/project-tree';
-import { TreeItem } from "~/components/tree/tree";
-import { getTreePath } from '~/store/project/project-reducer';
-import { sidePanelActions } from '~/store/side-panel/side-panel-action';
-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 { ProjectPanel } from "~/views/project-panel/project-panel";
import { DetailsPanel } from '~/views-components/details-panel/details-panel';
import { ArvadosTheme } from '~/common/custom-theme';
-
-import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
-import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
-import { ProjectResource } from '~/models/project';
-import { ResourceKind } from '~/models/resource';
-import { ProcessPanel } from '~/views/process-panel/process-panel';
-import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
+import { ContextMenu } from "~/views-components/context-menu/context-menu";
import { FavoritePanel } from "../favorite-panel/favorite-panel";
import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog';
import { Snackbar } from '~/views-components/snackbar/snackbar';
-import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
import { CollectionPanel } from '../collection-panel/collection-panel';
-import { loadCollection, loadCollectionTags } from '~/store/collection-panel/collection-panel-action';
-import { getCollectionUrl } from '~/models/collection';
-
-import { PROJECT_CREATE_FORM_NAME, openProjectCreateDialog } from '~/store/projects/project-create-actions';
-import { COLLECTION_CREATE_FORM_NAME, openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
-import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
-import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
-import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
-import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
-
-import { ProjectPanel } from "~/views/project-panel/project-panel";
import { AuthService } from "~/services/auth-service/auth-service";
import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-file-dialog';
import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog/multiple-files-remove-dialog';
import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog';
import { CollectionPartialCopyDialog } from '~/views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
+import { SidePanel } from '~/views-components/side-panel/side-panel';
+import { Routes } from '~/routes/routes';
+import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
+import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
+import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
+import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
-import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+import { ProcessPanel } from '~/views/process-panel/process-panel';
-const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
-type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
+type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
position: "absolute",
width: "100%"
},
- drawerPaper: {
- position: 'relative',
- width: DRAWER_WITDH,
- display: 'flex',
- flexDirection: 'column',
- },
contentWrapper: {
backgroundColor: theme.palette.background.default,
display: "flex",
flexGrow: 1,
position: 'relative'
},
- toolbar: theme.mixins.toolbar
});
interface WorkbenchDataProps {
- projects: Array<TreeItem<ProjectResource>>;
- currentProjectId: string;
user?: User;
currentToken?: string;
- sidePanelItems: SidePanelItem[];
}
interface WorkbenchGeneralProps {
type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
-interface NavBreadcrumb extends Breadcrumb {
- itemId: string;
-}
-
interface NavMenuItem extends MainAppBarMenuItem {
action: () => void;
}
export const Workbench = withStyles(styles)(
connect<WorkbenchDataProps>(
(state: RootState) => ({
- projects: state.projects.items,
- currentProjectId: state.projects.currentItemId,
user: state.auth.user,
currentToken: state.auth.apiToken,
- sidePanelItems: state.sidePanel
})
)(
class extends React.Component<WorkbenchProps, WorkbenchState> {
state = {
- isCreationDialogOpen: false,
isCurrentTokenDialogOpen: false,
anchorEl: null,
searchText: "",
};
render() {
- const path = getTreePath(this.props.projects, this.props.currentProjectId);
- const breadcrumbs = path.map(item => ({
- label: item.data.name,
- itemId: item.data.uuid,
- status: item.status
- }));
-
const { classes, user } = this.props;
return (
<div className={classes.root}>
<div className={classes.appBar}>
<MainAppBar
- breadcrumbs={breadcrumbs}
+ breadcrumbs={Breadcrumbs}
searchText={this.state.searchText}
user={this.props.user}
menuItems={this.state.menuItems}
buildInfo={this.props.buildInfo}
{...this.mainAppBarActions} />
</div>
- {user &&
- <Drawer
- variant="permanent"
- classes={{
- paper: classes.drawerPaper,
- }}>
- <div className={classes.toolbar} />
- <SidePanel
- toggleOpen={this.toggleSidePanelOpen}
- toggleActive={this.toggleSidePanelActive}
- sidePanelItems={this.props.sidePanelItems}
- onContextMenu={(event) => this.openContextMenu(event, {
- uuid: this.props.authService.getUuid() || "",
- name: "",
- kind: ContextMenuKind.ROOT_PROJECT
- })}>
- <ProjectTree
- projects={this.props.projects}
- 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(setProjectItem(itemId, ItemMode.ACTIVE));
- this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- }} />
- </SidePanel>
- </Drawer>}
+ {user && <SidePanel />}
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
- <Route path="/projects/:id" render={this.renderProjectPanel} />
- <Route path="/favorites" render={this.renderFavoritePanel} />
- <Route path="/collections/:id" render={this.renderCollectionPanel} />
- <Route path="/process/:id" render={this.renderProcessPanel} />
+ <Route path={Routes.PROJECTS} component={ProjectPanel} />
+ <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+ <Route path={Routes.FAVORITES} component={FavoritePanel} />
+ <Route path={Routes.PROCESS} component={ProcessPanel} />
</Switch>
</div>
{user && <DetailsPanel />}
);
}
- renderProcessPanel = (props: RouteComponentProps<{ id: string }>) => <ProcessPanel
- onContextMenu={(event, item) => {
- this.openContextMenu(event, {
- uuid: 'item.uuid',
- name: 'item.name',
- description: 'item.description',
- kind: ContextMenuKind.PROCESS
- });
- }}
- {...props} />
-
- renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
- onItemRouteChange={(collectionId) => {
- this.props.dispatch<any>(loadCollection(collectionId));
- this.props.dispatch<any>(loadCollectionTags(collectionId));
- }}
- onContextMenu={(event, item) => {
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- description: item.description,
- kind: ContextMenuKind.COLLECTION
- });
- }}
- {...props} />
-
- renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
- onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
- onContextMenu={(event, item) => {
- let kind: ContextMenuKind;
-
- if (item.kind === ResourceKind.PROJECT) {
- kind = ContextMenuKind.PROJECT;
- } else if (item.kind === ResourceKind.COLLECTION) {
- kind = ContextMenuKind.COLLECTION_RESOURCE;
- } else {
- kind = ContextMenuKind.RESOURCE;
- }
-
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- description: item.description,
- kind
- });
- }}
- onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
- onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- switch (item.kind) {
- case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid));
- 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(favoritePanelActions.REQUEST_ITEMS())}
- onContextMenu={(event, item) => {
- const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- kind,
- });
- }}
- onDialogOpen={this.handleProjectCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- switch (item.kind) {
- case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid));
- this.props.dispatch(push(getCollectionUrl(item.uuid)));
- default:
- this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- }
-
- }}
- {...props} />
-
mainAppBarActions: MainAppBarActionProps = {
- onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- },
onSearch: searchText => {
this.setState({ searchText });
this.props.dispatch(push(`/search?q=${searchText}`));
onDetailsPanelToggle: () => {
this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
},
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
- this.openContextMenu(event, {
- uuid: breadcrumb.itemId,
- name: breadcrumb.label,
- kind: ContextMenuKind.PROJECT
- });
- }
};
- toggleSidePanelOpen = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
- }
-
- toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
-
- const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
- if (panelItem && panelItem.activeAction) {
- panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
- }
- }
-
- handleProjectCreationDialogOpen = (itemUuid: string) => {
- this.props.dispatch(reset(PROJECT_CREATE_FORM_NAME));
- this.props.dispatch<any>(openProjectCreateDialog(itemUuid));
- }
-
- handleCollectionCreationDialogOpen = (itemUuid: string) => {
- this.props.dispatch(reset(COLLECTION_CREATE_FORM_NAME));
- this.props.dispatch<any>(openCollectionCreateDialog(itemUuid));
- }
-
- openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => {
- event.preventDefault();
- this.props.dispatch(
- contextMenuActions.OPEN_CONTEXT_MENU({
- position: { x: event.clientX, y: event.clientY },
- resource
- })
- );
- }
-
toggleCurrentTokenModal = () => {
this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
}