favoritePanelMiddleware,
trashPanelMiddleware,
sharedWithMePanelMiddleware,
++ workflowPanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
import { loadSharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel-actions';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
+import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel';
+ import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+ import { getProgressIndicator } from '../progress-indicator/progress-indicator-reducer';
+
+ export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
+
+ export const isWorkbenchLoading = (state: RootState) => {
+ const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
+ return progress ? progress.working : false;
+ };
+
+ const handleFirstTimeLoad = (action: any) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState) => {
+ try {
+ await dispatch(action);
+ } finally {
+ if (isWorkbenchLoading(getState())) {
+ dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+ }
+ }
+ };
+
export const loadWorkbench = () =>
async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
- dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns}));
++ dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
dispatch<any>(initSidePanelTree());
if (router.location) {
const match = matchRootRoute(router.location.pathname);
}
};
- export const loadSharedWithMe = (dispatch: Dispatch) => {
- dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+ export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
dispatch<any>(loadSharedWithMePanel());
- dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
- };
+ await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+ await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
+ });
+
- export const loadWorkflow = (dispatch: Dispatch<any>) => {
++export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.WORKFLOWS));
- dispatch(loadWorkflowPanel());
++ await dispatch(loadWorkflowPanel());
+ dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
- };
++});
--- /dev/null
- import { DataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
- const response = await this.services.workflowService;
- api.dispatch(updateResources([]));
- api.dispatch(setItems({ kind: '', offset: 4, limit: 4, items: [], itemsAvailable: 4 }));
++import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { SortDirection } from '~/components/data-table/data-column';
+import { WorkflowPanelColumnNames } from '~/views/workflow-panel/workflow-panel';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { WorkflowResource } from '~/models/workflow';
+import { ListResults } from '~/services/common-service/common-resource-service';
+import { workflowPanelActions } from './workflow-panel-actions';
+
+export class WorkflowMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
++ const state = api.getState();
++ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ try {
++ const response = await this.services.workflowService.list({ order: getOrder(dataExplorer) });
++ api.dispatch(updateResources(response.items));
++ api.dispatch(setItems(response));
+ } catch {
+ api.dispatch(couldNotFetchWorkflows());
+ }
+ }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer),
+ filters: getFilters(dataExplorer),
+});
+
+export const getFilters = (dataExplorer: DataExplorer) => {
+ const filters = new FilterBuilder()
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters();
+ return `[${filters}]`;
+};
+
+export const getOrder = (dataExplorer: DataExplorer) => {
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const order = new OrderBuilder<WorkflowResource>();
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+ const columnName = sortColumn && sortColumn.name === WorkflowPanelColumnNames.NAME ? "name" : "modifiedAt";
+ return order
+ .addOrder(sortDirection, columnName)
+ .getOrder();
+ } else {
+ return order.getOrder();
+ }
+};
+
+export const setItems = (listResults: ListResults<WorkflowResource>) =>
+ workflowPanelActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
+
+const couldNotFetchWorkflows = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch workflows.',
+ kind: SnackbarKind.ERROR
+ });
interface Props {
id: string;
onRowClick: (item: any) => void;
-- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
++ onContextMenu?: (event: React.MouseEvent<HTMLElement>, item: any) => void;
onRowDoubleClick: (item: any) => void;
extractKey?: (item: any) => React.Key;
}
interface MainContentBarProps {
onDetailsPanelToggle: () => void;
+ buttonVisible: boolean;
}
- const isButtonVisible = ({ router }: RootState) => {
-export const MainContentBar = connect(undefined, {
- onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
-})((props: MainContentBarProps) =>
- <Toolbar>
- <Grid container>
- <Grid container item xs alignItems="center">
- <Breadcrumbs />
- </Grid>
- <Grid item>
- <Tooltip title="Additional Info">
- <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
- <DetailsIcon />
- </IconButton>
- </Tooltip>
++const isWorkflowPath = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
- const match = !matchWorkflowRoute(pathname);
++ const match = matchWorkflowRoute(pathname);
+ return !!match;
+};
+
+export const MainContentBar = connect((state: RootState) => ({
- buttonVisible: isButtonVisible(state)
++ buttonVisible: !isWorkflowPath(state)
+}), {
+ onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
+ })((props: MainContentBarProps) =>
+ <Toolbar>
+ <Grid container>
+ <Grid container item xs alignItems="center">
+ <Breadcrumbs />
+ </Grid>
+ <Grid item>
+ {props.buttonVisible ? <Tooltip title="Additional Info">
+ <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+ <DetailsIcon />
+ </IconButton>
+ </Tooltip> : null}
+ </Grid>
</Grid>
- </Grid>
- </Toolbar>);
+ </Toolbar>);
import { SharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel';
import SplitterLayout from 'react-splitter-layout';
import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
- import { isSystemWorking } from "~/store/progress-indicator/progress-indicator-reducer";
+import { WorkflowPanel } from '~/views/workflow-panel/workflow-panel';
- type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
+ type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
}
});
- interface WorkbenchDataProps {
- user?: User;
- currentToken?: string;
- working: boolean;
- }
-
- interface WorkbenchGeneralProps {
- authService: AuthService;
- buildInfo: string;
- }
-
- type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & DispatchProp<any> & WithStyles<CssRules>;
+ type WorkbenchPanelProps = WithStyles<CssRules>;
- interface WorkbenchState {
- searchText: string;
- }
-
- export const Workbench = withStyles(styles)(
- connect<WorkbenchDataProps>(
- (state: RootState) => ({
- user: state.auth.user,
- currentToken: state.auth.apiToken,
- working: isSystemWorking(state.progressIndicator)
- })
- )(
- class extends React.Component<WorkbenchProps, WorkbenchState> {
- state = {
- searchText: "",
- };
- render() {
- const { classes } = this.props;
- return <>
- <MainAppBar
- searchText={this.state.searchText}
- user={this.props.user}
- onSearch={this.onSearch}
- buildInfo={this.props.buildInfo}>
- {this.props.working ? <LinearProgress color="secondary" /> : null}
- </MainAppBar>
- <Grid container direction="column" className={classes.root}>
- {this.props.user &&
- <Grid container item xs alignItems="stretch" wrap="nowrap">
- <Grid container item className={classes.container}>
- <SplitterLayout customClassName={classes.splitter} percentage={true}
- primaryIndex={0} primaryMinSize={20} secondaryInitialSize={80} secondaryMinSize={40}>
- <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
- <SidePanel />
- </Grid>
- <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
- <Grid item>
- <MainContentBar />
- </Grid>
- <Grid item xs className={classes.content}>
- <Switch>
- <Route path={Routes.PROJECTS} component={ProjectPanel} />
- <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
- <Route path={Routes.FAVORITES} component={FavoritePanel} />
- <Route path={Routes.PROCESSES} component={ProcessPanel} />
- <Route path={Routes.TRASH} component={TrashPanel} />
- <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
- <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
- <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
- </Switch>
- </Grid>
- </Grid>
- </SplitterLayout>
- </Grid>
- <Grid item>
- <DetailsPanel />
- </Grid>
- </Grid>
- }
+ export const WorkbenchPanel =
+ withStyles(styles)(({ classes }: WorkbenchPanelProps) =>
+ <Grid container item xs className={classes.root}>
+ <Grid container item xs className={classes.container}>
+ <SplitterLayout customClassName={classes.splitter} percentage={true}
+ primaryIndex={0} primaryMinSize={20} secondaryInitialSize={80} secondaryMinSize={40}>
+ <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
+ <SidePanel />
</Grid>
- <ContextMenu />
- <CopyCollectionDialog />
- <CopyProcessDialog />
- <CreateCollectionDialog />
- <CreateProjectDialog />
- <CurrentTokenDialog />
- <FileRemoveDialog />
- <FileRemoveDialog />
- <FilesUploadCollectionDialog />
- <MoveCollectionDialog />
- <MoveProcessDialog />
- <MoveProjectDialog />
- <MultipleFilesRemoveDialog />
- <PartialCopyCollectionDialog />
- <ProcessCommandDialog />
- <RenameFileDialog />
- <Snackbar />
- <UpdateCollectionDialog />
- <UpdateProcessDialog />
- <UpdateProjectDialog />
- </>;
- }
-
- onSearch = (searchText: string) => {
- this.setState({ searchText });
- this.props.dispatch(push(`/search?q=${searchText}`));
- }
-
- toggleDetailsPanel = () => {
- this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
- }
-
- }
- )
- );
+ <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
+ <Grid item>
+ <MainContentBar />
+ </Grid>
+ <Grid item xs className={classes.content}>
+ <Switch>
+ <Route path={Routes.PROJECTS} component={ProjectPanel} />
+ <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+ <Route path={Routes.FAVORITES} component={FavoritePanel} />
+ <Route path={Routes.PROCESSES} component={ProcessPanel} />
+ <Route path={Routes.TRASH} component={TrashPanel} />
+ <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
+ <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
++ <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
+ </Switch>
+ </Grid>
+ </Grid>
+ </SplitterLayout>
+ </Grid>
+ <Grid item>
+ <DetailsPanel />
+ </Grid>
+ <ContextMenu />
+ <CopyCollectionDialog />
+ <CopyProcessDialog />
+ <CreateCollectionDialog />
+ <CreateProjectDialog />
+ <CurrentTokenDialog />
+ <FileRemoveDialog />
+ <FileRemoveDialog />
+ <FilesUploadCollectionDialog />
+ <MoveCollectionDialog />
+ <MoveProcessDialog />
+ <MoveProjectDialog />
+ <MultipleFilesRemoveDialog />
+ <PartialCopyCollectionDialog />
+ <ProcessCommandDialog />
+ <RenameFileDialog />
+ <Snackbar />
+ <UpdateCollectionDialog />
+ <UpdateProcessDialog />
+ <UpdateProjectDialog />
+ </Grid>
+ );
--- /dev/null
- import { StyleRulesCallback, WithStyles, withStyles, Card, CardHeader, Typography } from '@material-ui/core';
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
- <DataTableDefaultView
- icon={WorkflowIcon}
- messages={['Please select a workflow to see its description.']} />
++import { StyleRulesCallback, WithStyles, withStyles, Card, CardHeader, Typography, CardContent } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { WorkflowIcon } from '~/components/icon/icon';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+
+export type CssRules = 'card';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ height: '100%'
+ }
+});
+
+interface WorkflowDescriptionCardDataProps {
+}
+
+type WorkflowDescriptionCardProps = WorkflowDescriptionCardDataProps & WithStyles<CssRules>;
+
+export const WorkflowDescriptionCard = withStyles(styles)(
+ ({ classes }: WorkflowDescriptionCardProps) => {
+ return <Card className={classes.card}>
+ <CardHeader
+ title={<Typography noWrap variant="body2">
+ Workflow description:
+ </Typography>} />
++ <CardContent>
++ <DataTableDefaultView
++ icon={WorkflowIcon}
++ messages={['Please select a workflow to see its description.']} />
++ </CardContent>
+ </Card>;
+ });
--- /dev/null
- return <Grid container>
- <Grid item xs={6} style={{ paddingRight: '24px', display: 'grid' }}>
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from '~/store/store';
+import { WorkflowIcon } from '~/components/icon/icon';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { navigateTo } from "~/store/navigation/navigation-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { WORKFLOW_PANEL_ID } from '~/store/workflow-panel/workflow-panel-actions';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { GroupResource } from '~/models/group';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import {
+ ResourceLastModifiedDate,
+ ResourceName,
+} from "~/views-components/data-explorer/renderers";
+import { SortDirection } from '~/components/data-table/data-column';
+import { DataColumns } from '~/components/data-table/data-table';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { Grid } from '@material-ui/core';
+import { WorkflowDescriptionCard } from './workflow-description-card';
+
+export enum WorkflowPanelColumnNames {
+ NAME = "Name",
+ AUTHORISATION = "Authorisation",
+ LAST_MODIFIED = "Last modified",
+}
+
+interface WorkflowPanelDataProps {
+ resources: ResourcesState;
+}
+
+export enum ResourceStatus {
+ PUBLIC = 'public',
+ PRIVATE = 'private',
+ SHARED = 'shared'
+}
+
+const resourceStatus = (type: string) => {
+ switch (type) {
+ case ResourceStatus.PUBLIC:
+ return "Public";
+ case ResourceStatus.PRIVATE:
+ return "Private";
+ case ResourceStatus.SHARED:
+ return "Shared";
+ default:
+ return "Unknown";
+ }
+};
+
+export const workflowPanelColumns: DataColumns<string, DataTableFilterItem> = [
+ {
+ name: WorkflowPanelColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.ASC,
+ filters: [],
+ render: (uuid: string) => <ResourceName uuid={uuid} />
+ },
+ {
+ name: WorkflowPanelColumnNames.AUTHORISATION,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [
+ {
+ name: resourceStatus(ResourceStatus.PUBLIC),
+ selected: true,
+ },
+ {
+ name: resourceStatus(ResourceStatus.PRIVATE),
+ selected: true,
+ },
+ {
+ name: resourceStatus(ResourceStatus.SHARED),
+ selected: true,
+ }
+ ],
++ // do zmiany na ResourceAuthorisation
+ render: (uuid: string) => <ResourceName uuid={uuid} />,
+ },
+ {
+ name: WorkflowPanelColumnNames.LAST_MODIFIED,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: (uuid: string) => <ResourceLastModifiedDate uuid={uuid} />
+ }
+];
+
+type WorkflowPanelProps = WorkflowPanelDataProps & DispatchProp;
+
+export const WorkflowPanel = connect((state: RootState) => ({
+ resources: state.resources
+}))(
+ class extends React.Component<WorkflowPanelProps> {
+ render() {
++ return <Grid container spacing={16}>
++ <Grid item xs={6}>
+ <DataExplorer
+ id={WORKFLOW_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={false}
+ dataTableDefaultView={<DataTableDefaultView icon={WorkflowIcon} />} />
+ </Grid>
+ <Grid item xs={6}>
+ <WorkflowDescriptionCard />
+ </Grid>
+ </Grid>;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
+ if (resource) {
+ this.props.dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ isTrashed: resource.isTrashed,
+ kind: resource.kind,
+ menuKind: ContextMenuKind.PROJECT,
+ }));
+ }
+ }
+
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
+ }
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+ }
+);