[ARVADOS-WORKBENCH2] created: 1.1.4-359-gab3e261
Git user
git at public.curoverse.com
Mon Jul 23 18:18:47 EDT 2018
at ab3e261d28ff83fa214002a372a055817a931cd1 (commit)
commit ab3e261d28ff83fa214002a372a055817a931cd1
Author: Daniel Kos <daniel.kos at contractors.roche.com>
Date: Tue Jul 24 00:18:41 2018 +0200
Add favorite panel
Feature #13753
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos at contractors.roche.com>
diff --git a/src/components/side-panel/side-panel.tsx b/src/components/side-panel/side-panel.tsx
index 4240b1b..0d27584 100644
--- a/src/components/side-panel/side-panel.tsx
+++ b/src/components/side-panel/side-panel.tsx
@@ -58,6 +58,7 @@ export interface SidePanelItem {
open?: boolean;
margin?: boolean;
openAble?: boolean;
+ path?: string;
}
interface SidePanelDataProps {
diff --git a/src/services/favorite-service/favorite-service.ts b/src/services/favorite-service/favorite-service.ts
index d075b79..fe7c787 100644
--- a/src/services/favorite-service/favorite-service.ts
+++ b/src/services/favorite-service/favorite-service.ts
@@ -10,9 +10,12 @@ import { ListArguments, ListResults } from "../../common/api/common-resource-ser
import { OrderBuilder } from "../../common/api/order-builder";
export interface FavoriteListArguments extends ListArguments {
+ limit?: number;
+ offset?: number;
filters?: FilterBuilder<LinkResource>;
order?: OrderBuilder<LinkResource>;
}
+
export class FavoriteService {
constructor(
private linkService: LinkService,
@@ -63,6 +66,4 @@ export class FavoriteService {
});
});
}
-
-
-}
\ No newline at end of file
+}
diff --git a/src/services/project-service/project-service.ts b/src/services/project-service/project-service.ts
index f759547..7b1640e 100644
--- a/src/services/project-service/project-service.ts
+++ b/src/services/project-service/project-service.ts
@@ -32,5 +32,4 @@ export class ProjectService extends GroupsService<ProjectResource> {
.create<ProjectResource>()
.addEqual("groupClass", GroupClass.Project));
}
-
}
diff --git a/src/store/favorite-panel/favorite-panel-middleware.ts b/src/store/favorite-panel/favorite-panel-middleware.ts
new file mode 100644
index 0000000..e2743c2
--- /dev/null
+++ b/src/store/favorite-panel/favorite-panel-middleware.ts
@@ -0,0 +1,132 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Middleware } from "redux";
+import { dataExplorerActions } from "../data-explorer/data-explorer-action";
+import { favoriteService, groupsService } from "../../services/services";
+import { RootState } from "../store";
+import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { DataColumns } from "../../components/data-table/data-table";
+import { ProcessResource } from "../../models/process";
+import { OrderBuilder } from "../../common/api/order-builder";
+import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
+import { SortDirection } from "../../components/data-table/data-column";
+import {
+ columns,
+ FAVORITE_PANEL_ID,
+ FavoritePanelColumnNames,
+ FavoritePanelFilter
+} from "../../views/favorite-panel/favorite-panel";
+import { FavoritePanelItem, resourceToDataItem } from "../../views/favorite-panel/favorite-panel-item";
+
+export const favoritePanelMiddleware: Middleware = store => next => {
+ next(dataExplorerActions.SET_COLUMNS({ id: FAVORITE_PANEL_ID, columns }));
+
+ return action => {
+
+ const handleProjectPanelAction = <T extends { id: string }>(handler: (data: T) => void) =>
+ (data: T) => {
+ next(action);
+ if (data.id === FAVORITE_PANEL_ID) {
+ handler(data);
+ }
+ };
+
+ dataExplorerActions.match(action, {
+ SET_PAGE: handleProjectPanelAction(() => {
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
+ }),
+ SET_ROWS_PER_PAGE: handleProjectPanelAction(() => {
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
+ }),
+ SET_FILTERS: handleProjectPanelAction(() => {
+ store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: FAVORITE_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
+ }),
+ TOGGLE_SORT: handleProjectPanelAction(() => {
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
+ }),
+ SET_SEARCH_VALUE: handleProjectPanelAction(() => {
+ store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: FAVORITE_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
+ }),
+ REQUEST_ITEMS: handleProjectPanelAction(() => {
+ const state = store.getState() as RootState;
+ const dataExplorer = getDataExplorer(state.dataExplorer, FAVORITE_PANEL_ID);
+ const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
+ const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const statusFilters = getColumnFilters(columns, FavoritePanelColumnNames.STATUS);
+ const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.Asc ? SortDirection.Asc : SortDirection.Desc;
+ if (typeFilters.length > 0) {
+ favoriteService
+ .list(state.projects.currentItemId, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ order: /*sortColumn
+ ? sortColumn.name === FavoritePanelColumnNames.NAME
+ ? getOrder("name", sortDirection)
+ : getOrder("createdAt", sortDirection)
+ : */OrderBuilder.create(),
+ filters: FilterBuilder
+ .create()
+ .concat(FilterBuilder
+ .create()
+ .addIsA("uuid", typeFilters.map(f => f.type)))
+ .concat(FilterBuilder
+ .create<ProcessResource>(GroupContentsResourcePrefix.Process)
+ .addIn("state", statusFilters.map(f => f.type)))
+ .concat(getSearchFilter(dataExplorer.searchValue))
+ })
+ .then(response => {
+ store.dispatch(dataExplorerActions.SET_ITEMS({
+ id: FAVORITE_PANEL_ID,
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ });
+ } else {
+ store.dispatch(dataExplorerActions.SET_ITEMS({
+ id: FAVORITE_PANEL_ID,
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ }
+ }),
+ default: () => next(action)
+ });
+ };
+};
+
+const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
+ const column = columns.find(c => c.name === columnName);
+ return column && column.filters ? column.filters.filter(f => f.selected) : [];
+};
+
+const getOrder = (attribute: "name" | "createdAt", direction: SortDirection) =>
+ [
+ OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Collection),
+ OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Process),
+ OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Project)
+ ].reduce((acc, b) =>
+ acc.concat(direction === SortDirection.Asc
+ ? b.addAsc(attribute)
+ : b.addDesc(attribute)), OrderBuilder.create());
+
+const getSearchFilter = (searchValue: string) =>
+ searchValue
+ ? [
+ FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Collection),
+ FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Process),
+ FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.Project)]
+ .reduce((acc, b) =>
+ acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
+ : FilterBuilder.create();
+
+
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 3920b5a..7f78243 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -58,3 +58,8 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
}
};
+
+export const setFavoriteItem = (itemId: string, itemMode: ItemMode) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const a = 1;
+ };
diff --git a/src/store/side-panel/side-panel-reducer.ts b/src/store/side-panel/side-panel-reducer.ts
index 2bbd6a1..5dd5c01 100644
--- a/src/store/side-panel/side-panel-reducer.ts
+++ b/src/store/side-panel/side-panel-reducer.ts
@@ -14,11 +14,12 @@ export const sidePanelReducer = (state: SidePanelState = sidePanelData, action:
return sidePanelData;
} else {
return sidePanelActions.match(action, {
- TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId => state.map(it => itemId === it.id && it.open === false ? {...it, open: true} : {...it, open: false}),
+ TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
+ state.map(it => ({...it, open: itemId === it.id && it.open === false})),
TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => {
const sidePanel = _.cloneDeep(state);
resetSidePanelActivity(sidePanel);
- sidePanel.map(it => {
+ sidePanel.forEach(it => {
if (it.id === itemId) {
it.active = true;
}
@@ -77,6 +78,7 @@ export const sidePanelData = [
name: "Favorites",
icon: FavoriteIcon,
active: false,
+ path: '/favorites'
},
{
id: SidePanelIdentifiers.Trash,
diff --git a/src/store/store.ts b/src/store/store.ts
index adb7ddd..fbb5ad6 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -14,6 +14,7 @@ import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-exp
import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
+import { favoritePanelMiddleware } from "./favorite-panel/favorite-panel-middleware";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
@@ -45,7 +46,8 @@ export function configureStore(history: History) {
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware,
- projectPanelMiddleware
+ projectPanelMiddleware,
+ favoritePanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index cc2fcb3..fe6ebd8 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -56,5 +56,6 @@ const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet
export enum ContextMenuKind {
RootProject = "RootProject",
- Project = "Project"
+ Project = "Project",
+ Favorite = "Favorite"
}
diff --git a/src/views/favorite-panel/favorite-panel-item.ts b/src/views/favorite-panel/favorite-panel-item.ts
new file mode 100644
index 0000000..28f7f88
--- /dev/null
+++ b/src/views/favorite-panel/favorite-panel-item.ts
@@ -0,0 +1,31 @@
+// 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
+ };
+}
+
diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx
new file mode 100644
index 0000000..4c5b5bd
--- /dev/null
+++ b/src/views/favorite-panel/favorite-panel.tsx
@@ -0,0 +1,225 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FavoritePanelItem } from './favorite-panel-item';
+import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { formatDate, formatFileSize } from '../../common/formatters';
+import { 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 { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
+import { ArvadosTheme } from '../../common/custom-theme';
+
+type CssRules = "toolbar" | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ toolbar: {
+ paddingBottom: theme.spacing.unit * 3,
+ textAlign: "right"
+ },
+ button: {
+ marginLeft: theme.spacing.unit
+ },
+});
+
+const renderName = (item: FavoritePanelItem) =>
+ <Grid container alignItems="center" wrap="nowrap" spacing={16}>
+ <Grid item>
+ {renderIcon(item)}
+ </Grid>
+ <Grid item>
+ <Typography color="primary">
+ {item.name}
+ </Typography>
+ </Grid>
+ </Grid>;
+
+
+const renderIcon = (item: FavoritePanelItem) => {
+ switch (item.kind) {
+ case ResourceKind.Project:
+ return <ProjectIcon />;
+ case ResourceKind.Collection:
+ return <CollectionIcon />;
+ case ResourceKind.Process:
+ return <ProcessIcon />;
+ default:
+ return <DefaultIcon />;
+ }
+};
+
+const renderDate = (date: string) => {
+ return <Typography noWrap>{formatDate(date)}</Typography>;
+};
+
+const renderFileSize = (fileSize?: number) =>
+ <Typography noWrap>
+ {formatFileSize(fileSize)}
+ </Typography>;
+
+const renderOwner = (owner: string) =>
+ <Typography noWrap color="primary" >
+ {owner}
+ </Typography>;
+
+const renderType = (type: string) =>
+ <Typography noWrap>
+ {resourceLabel(type)}
+ </Typography>;
+
+const renderStatus = (item: FavoritePanelItem) =>
+ <Typography noWrap align="center" >
+ {item.status || "-"}
+ </Typography>;
+
+export enum FavoritePanelColumnNames {
+ NAME = "Name",
+ STATUS = "Status",
+ TYPE = "Type",
+ OWNER = "Owner",
+ FILE_SIZE = "File size",
+ LAST_MODIFIED = "Last modified"
+}
+
+export interface FavoritePanelFilter extends DataTableFilterItem {
+ type: ResourceKind | ContainerRequestState;
+}
+
+export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
+ {
+ name: FavoritePanelColumnNames.NAME,
+ selected: true,
+ sortDirection: SortDirection.Asc,
+ render: renderName,
+ width: "450px"
+ },
+ {
+ name: "Status",
+ selected: true,
+ filters: [
+ {
+ name: ContainerRequestState.Committed,
+ selected: true,
+ type: ContainerRequestState.Committed
+ },
+ {
+ name: ContainerRequestState.Final,
+ selected: true,
+ type: ContainerRequestState.Final
+ },
+ {
+ name: ContainerRequestState.Uncommitted,
+ selected: true,
+ type: ContainerRequestState.Uncommitted
+ }
+ ],
+ render: renderStatus,
+ width: "75px"
+ },
+ {
+ name: FavoritePanelColumnNames.TYPE,
+ selected: true,
+ filters: [
+ {
+ name: resourceLabel(ResourceKind.Collection),
+ selected: true,
+ type: ResourceKind.Collection
+ },
+ {
+ name: resourceLabel(ResourceKind.Process),
+ selected: true,
+ type: ResourceKind.Process
+ },
+ {
+ name: resourceLabel(ResourceKind.Project),
+ selected: true,
+ type: ResourceKind.Project
+ }
+ ],
+ render: item => renderType(item.kind),
+ width: "125px"
+ },
+ {
+ name: FavoritePanelColumnNames.OWNER,
+ selected: true,
+ render: item => renderOwner(item.owner),
+ width: "200px"
+ },
+ {
+ name: FavoritePanelColumnNames.FILE_SIZE,
+ selected: true,
+ render: item => renderFileSize(item.fileSize),
+ width: "50px"
+ },
+ {
+ name: FavoritePanelColumnNames.LAST_MODIFIED,
+ selected: true,
+ sortDirection: SortDirection.None,
+ render: item => renderDate(item.lastModified),
+ width: "150px"
+ }
+];
+
+export const FAVORITE_PANEL_ID = "favoritePanel";
+
+interface FavoritePanelDataProps {
+ currentItemId: string;
+}
+
+interface FavoritePanelActionProps {
+ onItemClick: (item: FavoritePanelItem) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: FavoritePanelItem) => void;
+ onDialogOpen: (ownerUuid: string) => void;
+ onItemDoubleClick: (item: FavoritePanelItem) => void;
+ onItemRouteChange: (itemId: string) => void;
+}
+
+type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
+export const FavoritePanel = withStyles(styles)(
+ connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ class extends React.Component<FavoritePanelProps> {
+ render() {
+ const { classes } = this.props;
+ return <div>
+ <div className={classes.toolbar}>
+ <Button color="primary" variant="raised" className={classes.button}>
+ Create a collection
+ </Button>
+ <Button color="primary" variant="raised" className={classes.button}>
+ Run a process
+ </Button>
+ <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
+ New project
+ </Button>
+ </div>
+ <DataExplorer
+ id={FAVORITE_PANEL_ID}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ extractKey={(item: FavoritePanelItem) => item.uuid} />
+ </div>;
+ }
+
+ handleNewProjectClick = () => {
+ this.props.onDialogOpen(this.props.currentItemId);
+ }
+ componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) {
+ if (match.params.id !== currentItemId) {
+ onItemRouteChange(match.params.id);
+ }
+ }
+ }
+ )
+);
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index a62b713..3fec6d6 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -18,7 +18,7 @@ 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 { ItemMode, setFavoriteItem, setProjectItem } from "../../store/navigation/navigation-action";
import { projectActions } from "../../store/project/project-action";
import { ProjectPanel } from "../project-panel/project-panel";
import { DetailsPanel } from '../../views-components/details-panel/details-panel';
@@ -28,10 +28,11 @@ import { authService } from '../../services/services';
import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action";
import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
-import { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
+import { sidePanelData, SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
import { ProjectResource } from '../../models/project';
import { ResourceKind } from '../../models/resource';
import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
+import { FavoritePanel } from "../favorite-panel/favorite-panel";
const drawerWidth = 240;
const appBarHeight = 100;
@@ -191,6 +192,7 @@ export const Workbench = withStyles(styles)(
<div className={classes.content}>
<Switch>
<Route path="/projects/:id" render={this.renderProjectPanel} />
+ <Route path="/favorites" render={this.renderFavoritePanel} />
</Switch>
</div>
{ user && <DetailsPanel /> }
@@ -214,6 +216,19 @@ export const Workbench = withStyles(styles)(
}}
{...props} />
+ renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
+ onItemRouteChange={itemId => this.props.dispatch<any>(setFavoriteItem(itemId, ItemMode.ACTIVE))}
+ onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Favorite)}
+ onDialogOpen={this.handleCreationDialogOpen}
+ onItemClick={item => {
+ this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+ }}
+ onItemDoubleClick={item => {
+ this.props.dispatch<any>(setFavoriteItem(item.uuid, ItemMode.ACTIVE));
+ this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
+ }}
+ {...props} />
+
mainAppBarActions: MainAppBarActionProps = {
onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
@@ -239,7 +254,8 @@ export const Workbench = withStyles(styles)(
toggleSidePanelActive = (itemId: string) => {
this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
- this.props.dispatch(push("/"));
+ const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
+ this.props.dispatch(push(panelItem && panelItem.path ? panelItem.path : "/"));
}
handleCreationDialogOpen = (itemUuid: string) => {
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list