Merge branch '13857-workflow-view'
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 28 Sep 2018 07:29:37 +0000 (09:29 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 28 Sep 2018 07:29:37 +0000 (09:29 +0200)
refs #13857

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

22 files changed:
src/components/data-explorer/data-explorer.tsx
src/index.tsx
src/models/resource.ts
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/services.ts
src/services/workflow-service/workflow-service.ts [new file with mode: 0644]
src/store/navigation/navigation-action.ts
src/store/shared-with-me-panel/shared-with-me-middleware-service.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/side-panel/side-panel-action.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/store/workflow-panel/workflow-middleware-service.ts [new file with mode: 0644]
src/store/workflow-panel/workflow-panel-actions.ts [new file with mode: 0644]
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views/workbench/workbench.tsx
src/views/workflow-panel/workflow-description-card.tsx [new file with mode: 0644]
src/views/workflow-panel/workflow-panel-view.tsx [new file with mode: 0644]
src/views/workflow-panel/workflow-panel.tsx [new file with mode: 0644]

index 59f4dbebb47832ff4ec5a827c44eb1a434db61ac..d7abde7bddd2f81d3a668f4587f5063bab010d60 100644 (file)
@@ -12,7 +12,7 @@ import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
 import { SearchInput } from '../search-input/search-input';
 import { ArvadosTheme } from "~/common/custom-theme";
 
-type CssRules = 'searchBox' | "toolbar";
+type CssRules = 'searchBox' | "toolbar" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     searchBox: {
@@ -21,6 +21,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
         paddingTop: theme.spacing.unit * 2
     },
+    root: {
+        height: '100%'
+    }
 });
 
 interface DataExplorerDataProps<T> {
@@ -66,7 +69,7 @@ export const DataExplorer = withStyles(styles)(
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView
             } = this.props;
-            return <Paper>
+            return <Paper className={classes.root}>
                 <Toolbar className={classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         <div className={classes.searchBox}>
index 52852847355b79bce73b8307481166176991470e..d0154b663e0a9150419edbf437ac1b1eb729405d 100644 (file)
@@ -38,6 +38,7 @@ import { addRouteChangeHandlers } from './routes/route-change-handlers';
 import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
 import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { setUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
 import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
@@ -78,6 +79,7 @@ fetchConfig()
         store.subscribe(initListener(history, store, services, config));
         store.dispatch(initAuth());
         store.dispatch(setCurrentTokenDialogApiHost(apiHost));
+        store.dispatch(setUuidPrefix(config.uuidPrefix));
 
         const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
         const MainPanelComponent = (props: any) => <MainPanel buildInfo={buildInfo} {...props} />;
index 698bcf73188ec21c375a349d3f2605953bbd2074..7e40c738f8076b1aa10652411c4e97a2d13c09a2 100644 (file)
@@ -40,6 +40,7 @@ export enum ResourceObjectType {
     GROUP = 'j7d0g',
     LOG = '57u5n',
     USER = 'tpzed',
+    WORKFLOW = '7fd4e'
 }
 
 export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
@@ -70,6 +71,8 @@ export const extractUuidKind = (uuid: string = '') => {
             return ResourceKind.CONTAINER;
         case ResourceObjectType.LOG:
             return ResourceKind.LOG;
+        case ResourceObjectType.WORKFLOW:
+            return ResourceKind.WORKFLOW;
         default:
             return undefined;
     }
index 33e0bef753c6f5535a18a02e94b7e8e3012052ab..97613147cc7087a491f6ba5682008141ff81ce96 100644 (file)
@@ -4,11 +4,10 @@
 
 import { History, Location } from 'history';
 import { RootStore } from '~/store/store';
-import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute } from './routes';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchWorkflowRoute } from './routes';
 import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog } from '~/store/workbench/workbench-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
-import { navigateToSharedWithMe } from '../store/navigation/navigation-action';
-import { loadSharedWithMe } from '../store/workbench/workbench-actions';
+import { loadSharedWithMe, loadWorkflow } from '~/store/workbench/workbench-actions';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -25,6 +24,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const processMatch = matchProcessRoute(pathname);
     const processLogMatch = matchProcessLogRoute(pathname);
     const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
+    const workflowMatch = matchWorkflowRoute(pathname);
 
     if (projectMatch) {
         store.dispatch(loadProject(projectMatch.params.id));
@@ -42,5 +42,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(navigateToRootProject);
     } else if (sharedWithMeMatch) {
         store.dispatch(loadSharedWithMe);
+    } else if (workflowMatch) {
+        store.dispatch(loadWorkflow);
     }
 };
index fb28bd05bee5ff360c17cedd01bf2487d7cf4f22..34b15e1132f18cd35c6ec24b04c299ae16cf8925 100644 (file)
@@ -17,6 +17,7 @@ export const Routes = {
     TRASH: '/trash',
     PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
     SHARED_WITH_ME: '/shared-with-me',
+    WORKFLOWS: '/workflows'
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -64,3 +65,6 @@ export const matchProcessLogRoute = (route: string) =>
 
 export const matchSharedWithMeRoute = (route: string) =>
     matchPath(route, { path: Routes.SHARED_WITH_ME });
+
+export const matchWorkflowRoute = (route: string) =>
+    matchPath<ResourceRouteParams>(route, { path: Routes.WORKFLOWS });
index 9c764b0910a2b2338347951b56f427cfcfe9cfa3..738b69de28584a5afdf0416eb428e20cf21e8c61 100644 (file)
@@ -21,6 +21,7 @@ import { ContainerRequestService } from './container-request-service/container-r
 import { ContainerService } from './container-service/container-service';
 import { LogService } from './log-service/log-service';
 import { ApiActions } from "~/services/api/api-actions";
+import { WorkflowService } from './workflow-service/workflow-service';
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
@@ -39,6 +40,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
     const logService = new LogService(apiClient, actions);
     const projectService = new ProjectService(apiClient, actions);
     const userService = new UserService(apiClient, actions);
+    const workflowService = new WorkflowService(apiClient, actions);
 
     const ancestorsService = new AncestorService(groupsService, userService);
     const authService = new AuthService(apiClient, config.rootUrl, actions);
@@ -64,6 +66,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
         tagService,
         userService,
         webdavClient,
+        workflowService
     };
 };
 
diff --git a/src/services/workflow-service/workflow-service.ts b/src/services/workflow-service/workflow-service.ts
new file mode 100644 (file)
index 0000000..60f898f
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { WorkflowResource } from '~/models/workflow';
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
+
+export class WorkflowService extends CommonResourceService<WorkflowResource> {
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "workflows", actions);
+    }
+}
index 943f38ce98b4eb633b6d369c2bcc2313d85d932f..c8a554c7651ea5d5f955ab11bb91051ee96626b2 100644 (file)
@@ -26,6 +26,8 @@ export const navigateTo = (uuid: string) =>
             dispatch<any>(navigateToFavorites);
         } else if (uuid === SidePanelTreeCategory.SHARED_WITH_ME) {
             dispatch(navigateToSharedWithMe);
+        } else if (uuid === SidePanelTreeCategory.WORKFLOWS) {
+            dispatch(navigateToWorkflows);
         } else if (uuid === SidePanelTreeCategory.TRASH) {
             dispatch(navigateToTrash);
         }
@@ -35,6 +37,8 @@ export const navigateToFavorites = push(Routes.FAVORITES);
 
 export const navigateToTrash = push(Routes.TRASH);
 
+export const navigateToWorkflows = push(Routes.WORKFLOWS);
+
 export const navigateToProject = compose(push, getProjectUrl);
 
 export const navigateToCollection = compose(push, getCollectionUrl);
index 1ebb13ecb05053cfb06336f9a5c5bf9ca65dd687..c26a7a5aa7eb7cb846393bd5a67af79f2b016eb1 100644 (file)
@@ -37,7 +37,6 @@ export class SharedWithMeMiddlewareService extends DataExplorerMiddlewareService
         } catch (e) {
             api.dispatch(couldNotFetchSharedItems());
         }
-
     }
 }
 
index 3fd2d68af6930946c933657a8d59aec8dfdcddc6..22a83dda8c9f71b130e5b8251b3e8d8c321bb714 100644 (file)
@@ -13,7 +13,6 @@ 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';
-import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
 import { OrderBuilder } from '../../services/api/order-builder';
 
 export enum SidePanelTreeCategory {
index 2a5fdd0c2d17c1aae6c3455034fd303ddb523a36..fd08ee1319d5b1550bd278591df4997d7d7f247a 100644 (file)
@@ -4,7 +4,7 @@
 
 import { Dispatch } from 'redux';
 import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe } from '../navigation/navigation-action';
+import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe, navigateToWorkflows } from '../navigation/navigation-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 
 export const navigateFromSidePanel = (id: string) =>
@@ -24,6 +24,8 @@ const getSidePanelTreeCategoryAction = (id: string) => {
             return navigateToTrash;
         case SidePanelTreeCategory.SHARED_WITH_ME:
             return navigateToSharedWithMe;
+        case SidePanelTreeCategory.WORKFLOWS:
+            return navigateToWorkflows;
         default:
             return sidePanelTreeCategoryNotAvailable(id);
     }
index 012b747425b72e714472a5b2a0cfe89d03dc2546..16d0d055e30d9c100f8985e071ceee1e63768dd8 100644 (file)
@@ -35,6 +35,8 @@ import { processPanelReducer } from '~/store/process-panel/process-panel-reducer
 import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
 import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-with-me-middleware-service';
 import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer';
+import { WorkflowMiddlewareService } from './workflow-panel/workflow-middleware-service';
+import { WORKFLOW_PANEL_ID } from './workflow-panel/workflow-panel-actions';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -60,6 +62,9 @@ export function configureStore(history: History, services: ServiceRepository): R
     const sharedWithMePanelMiddleware = dataExplorerMiddleware(
         new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID)
     );
+    const workflowPanelMiddleware = dataExplorerMiddleware(
+        new WorkflowMiddlewareService(services, WORKFLOW_PANEL_ID)
+    );
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
@@ -68,6 +73,7 @@ export function configureStore(history: History, services: ServiceRepository): R
         favoritePanelMiddleware,
         trashPanelMiddleware,
         sharedWithMePanelMiddleware,
+        workflowPanelMiddleware
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
index 94b4b4f5bb7602f811b257edc08ab48289522618..8f034ec0383b4f61cb4e8b5b98ebe688e9da0e4f 100644 (file)
@@ -38,6 +38,8 @@ import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
 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-view';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
 import { getProgressIndicator } from '../progress-indicator/progress-indicator-reducer';
 import { ResourceKind, extractUuidKind } from '~/models/resource';
@@ -76,6 +78,7 @@ export const loadWorkbench = () =>
                 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<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
@@ -347,6 +350,11 @@ export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) =
     await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
 });
 
+export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+    dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.WORKFLOWS));
+    await dispatch(loadWorkflowPanel());
+    dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
+});
 const finishLoadingProject = (project: GroupContentsResource | string) =>
     async (dispatch: Dispatch<any>) => {
         const uuid = typeof project === 'string' ? project : project.uuid;
diff --git a/src/store/workflow-panel/workflow-middleware-service.ts b/src/store/workflow-panel/workflow-middleware-service.ts
new file mode 100644 (file)
index 0000000..1002979
--- /dev/null
@@ -0,0 +1,77 @@
+// 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';
+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-view';
+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(getParams(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
+    });
\ No newline at end of file
diff --git a/src/store/workflow-panel/workflow-panel-actions.ts b/src/store/workflow-panel/workflow-panel-actions.ts
new file mode 100644 (file)
index 0000000..aa79347
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { propertiesActions } from '~/store/properties/properties-actions';
+
+export const WORKFLOW_PANEL_ID = "workflowPanel";
+const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
+
+export const workflowPanelActions = bindDataExplorerActions(WORKFLOW_PANEL_ID);
+
+export const loadWorkflowPanel = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(workflowPanelActions.REQUEST_ITEMS());
+    };
+
+export const setUuidPrefix = (uuidPrefix: string) =>
+    propertiesActions.SET_PROPERTY({ key: UUID_PREFIX_PROPERTY_NAME, value: uuidPrefix });
+
+export const getUuidPrefix = (state: RootState) =>{
+    return state.properties.uuidPrefix;
+};
\ No newline at end of file
index 74c3e64aaacf1139d5a90cff0c5ada69f40b14b2..17f2c77b0a44fdd3b235b1625c9d0ddf39ba430f 100644 (file)
@@ -15,7 +15,7 @@ import { DataColumns } from "~/components/data-table/data-table";
 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;
 }
index b9cc63c30475edc89797a07de66963806deea154..12e1be7805c61c6f6674e282af0456cd78ce6670 100644 (file)
@@ -3,10 +3,10 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Grid, Typography, withStyles } from '@material-ui/core';
+import { Grid, Typography, withStyles, Tooltip, IconButton } from '@material-ui/core';
 import { FavoriteStar } from '../favorite-star/favorite-star';
 import { ResourceKind, TrashableResource } from '~/models/resource';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
 import { connect } from 'react-redux';
@@ -16,6 +16,9 @@ import { GroupContentsResource } from '~/services/groups-service/groups-service'
 import { getProcess, Process, getProcessStatus, getProcessStatusColor } from '~/store/processes/process';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { compose } from 'redux';
+import { WorkflowResource } from '~/models/workflow';
+import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
+import { getUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
 
 export const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -48,6 +51,8 @@ export const renderIcon = (item: { kind: string }) => {
             return <CollectionIcon />;
         case ResourceKind.PROCESS:
             return <ProcessIcon />;
+        case ResourceKind.WORKFLOW:
+            return <WorkflowIcon />;
         default:
             return <DefaultIcon />;
     }
@@ -57,6 +62,68 @@ export const renderDate = (date?: string) => {
     return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
 };
 
+export const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ownerUuid: string }) =>
+    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
+        <Grid item>
+            {renderIcon(item)}
+        </Grid>
+        <Grid item>
+            <Typography color="primary" style={{ width: '100px' }}>
+                {item.name}
+            </Typography>
+        </Grid>
+    </Grid>;
+
+export const RosurceWorkflowName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
+        return resource || { name: '', uuid: '', kind: '', ownerUuid: '' };
+    })(renderWorkflowName);
+
+const getPublicUuid = (uuidPrefix: string) => {
+    return `${uuidPrefix}-tpzed-anonymouspublic`;
+};
+
+// do share onClick
+export const resourceShare = (uuidPrefix: string, ownerUuid?: string) => {
+    return <Tooltip title="Share">
+        <IconButton onClick={() => undefined}>
+            {ownerUuid === getPublicUuid(uuidPrefix) ? <ShareIcon /> : null}
+        </IconButton>
+    </Tooltip>;
+};
+
+export const ResourceShare = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
+        const uuidPrefix = getUuidPrefix(state);
+        return {
+            ownerUuid: resource ? resource.ownerUuid : '',
+            uuidPrefix
+        };
+    })((props: { ownerUuid?: string, uuidPrefix: string }) => resourceShare(props.uuidPrefix, props.ownerUuid));
+
+export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
+    if (ownerUuid === getPublicUuid(uuidPrefix)) {
+        return renderStatus(ResourceStatus.PUBLIC);
+    } else {
+        return renderStatus(ResourceStatus.PRIVATE);
+    }
+};
+
+const renderStatus = (status: string) =>
+    <Typography noWrap style={{ width: '60px' }}>{status}</Typography>;
+
+export const ResourceWorkflowStatus = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
+        const uuidPrefix = getUuidPrefix(state);
+        return {
+            ownerUuid: resource ? resource.ownerUuid : '',
+            uuidPrefix
+        };
+    })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
+
 export const ResourceLastModifiedDate = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
index 071b986ab57352050ef411e5cd6db7ecc3bd61ea..6fb419e36710aa187e0f28a79b46fba82ed5f0d7 100644 (file)
@@ -8,25 +8,36 @@ import { DetailsIcon } from "~/components/icon/icon";
 import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
 import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
 import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { matchWorkflowRoute } from '~/routes/routes';
 
 interface MainContentBarProps {
     onDetailsPanelToggle: () => void;
+    buttonVisible: boolean;
 }
 
-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);
+    return !!match;
+};
+
+export const MainContentBar = connect((state: RootState) => ({
+    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>);
index b0d14f09072bf0be2c139bb7eee28525588c9bfd..776850ceb8950ffe89b2fa7f1e7fc58414dce685 100644 (file)
@@ -39,6 +39,7 @@ import { Grid } from '@material-ui/core';
 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 { WorkflowPanel } from '~/views/workflow-panel/workflow-panel';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -94,6 +95,7 @@ export const WorkbenchPanel =
                                 <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>
diff --git a/src/views/workflow-panel/workflow-description-card.tsx b/src/views/workflow-panel/workflow-description-card.tsx
new file mode 100644 (file)
index 0000000..60e17b6
--- /dev/null
@@ -0,0 +1,58 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, CardContent, Tab, Tabs, Paper } 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';
+import { WorkflowResource } from '~/models/workflow';
+
+export type CssRules = 'root' | 'tab';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        height: '100%',
+    },
+    tab: {
+        minWidth: '50%'
+    }
+});
+
+interface WorkflowDetailsCardDataProps {
+    workflow?: WorkflowResource;
+}
+
+type WorkflowDetailsCardProps = WorkflowDetailsCardDataProps & WithStyles<CssRules>;
+
+export const WorkflowDetailsCard = withStyles(styles)(
+    class extends React.Component<WorkflowDetailsCardProps> {
+        state = {
+            value: 0,
+        };
+
+        handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+            this.setState({ value });
+        }
+
+        render() {
+            const { classes } = this.props;
+            const { value } = this.state;
+            return <Paper className={classes.root}>
+                <Tabs value={value} onChange={this.handleChange} centered={true}>
+                    <Tab className={classes.tab} label="Description" />
+                    <Tab className={classes.tab} label="Inputs" />
+                </Tabs>
+                {value === 0 && <CardContent>
+                    Description
+                    <DataTableDefaultView
+                        icon={WorkflowIcon}
+                        messages={['Please select a workflow to see its description.']} />
+                </CardContent>}
+                {value === 1 && <CardContent>
+                    Inputs
+                </CardContent>}
+            </Paper>;
+        }
+    });
\ No newline at end of file
diff --git a/src/views/workflow-panel/workflow-panel-view.tsx b/src/views/workflow-panel/workflow-panel-view.tsx
new file mode 100644 (file)
index 0000000..8a29cb7
--- /dev/null
@@ -0,0 +1,120 @@
+// 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 { WorkflowIcon } from '~/components/icon/icon';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { WORKFLOW_PANEL_ID } from '~/store/workflow-panel/workflow-panel-actions';
+import {
+    ResourceLastModifiedDate,
+    RosurceWorkflowName,
+    ResourceWorkflowStatus,
+    ResourceShare
+} 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 { WorkflowDetailsCard } from './workflow-description-card';
+
+export enum WorkflowPanelColumnNames {
+    NAME = "Name",
+    AUTHORISATION = "Authorisation",
+    LAST_MODIFIED = "Last modified",
+    SHARE = 'Share'
+}
+
+export interface WorkflowPanelFilter extends DataTableFilterItem {
+    type: ResourceStatus;
+}
+
+interface WorkflowPanelDataProps {
+    handleRowDoubleClick: any;
+    handleRowClick: any;
+}
+
+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, WorkflowPanelFilter> = [
+    {
+        name: WorkflowPanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.ASC,
+        filters: [],
+        render: (uuid: string) => <RosurceWorkflowName uuid={uuid} />
+    },
+    {
+        name: WorkflowPanelColumnNames.AUTHORISATION,
+        selected: true,
+        configurable: true,
+        filters: [
+            {
+                name: resourceStatus(ResourceStatus.PUBLIC),
+                selected: true,
+                type: ResourceStatus.PUBLIC
+            },
+            {
+                name: resourceStatus(ResourceStatus.PRIVATE),
+                selected: true,
+                type: ResourceStatus.PRIVATE
+            },
+            {
+                name: resourceStatus(ResourceStatus.SHARED),
+                selected: true,
+                type: ResourceStatus.SHARED
+            }
+        ],
+        render: (uuid: string) => <ResourceWorkflowStatus uuid={uuid} />,
+    },
+    {
+        name: WorkflowPanelColumnNames.LAST_MODIFIED,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
+        render: (uuid: string) => <ResourceLastModifiedDate uuid={uuid} />
+    },
+    {
+        name: '',
+        selected: true,
+        configurable: false,
+        filters: [],
+        render: (uuid: string) => <ResourceShare uuid={uuid} />
+    }
+];
+
+export const WorkflowPanelView = ({...props}) => {
+    return <Grid container spacing={16}>
+        <Grid item xs={6}>
+            <DataExplorer
+                id={WORKFLOW_PANEL_ID}
+                onRowClick={props.handleRowClick}
+                onRowDoubleClick={props.handleRowDoubleClick}
+                contextMenuColumn={false}
+                dataTableDefaultView={<DataTableDefaultView icon={WorkflowIcon} />} />
+        </Grid>
+        <Grid item xs={6}>
+            <WorkflowDetailsCard />
+        </Grid>
+    </Grid>;
+};
\ No newline at end of file
diff --git a/src/views/workflow-panel/workflow-panel.tsx b/src/views/workflow-panel/workflow-panel.tsx
new file mode 100644 (file)
index 0000000..279097d
--- /dev/null
@@ -0,0 +1,24 @@
+// 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 { navigateTo } from '~/store/navigation/navigation-action';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { WorkflowPanelView } from '~/views/workflow-panel/workflow-panel-view';
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+
+    handleRowDoubleClick: (uuid: string) => {
+        dispatch<any>(navigateTo(uuid));
+    },
+    
+    handleRowClick: (uuid: string) => {
+        dispatch(loadDetailsPanel(uuid));
+    }
+});
+
+export const WorkflowPanel= connect(undefined, mapDispatchToProps)(
+    (props) => <WorkflowPanelView {...props}/>);
\ No newline at end of file