workflow-view-without-working-services
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Thu, 20 Sep 2018 14:39:53 +0000 (16:39 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Thu, 20 Sep 2018 14:39:53 +0000 (16:39 +0200)
Feature #13857

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

18 files changed:
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/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.tsx [new file with mode: 0644]

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 c68c5398754e13c6a439af6176cf53a36def01d7..d332e0fca600dc03c6c3f9c6ebc944ac0edb7cbf 100644 (file)
@@ -24,8 +24,10 @@ export const navigateTo = (uuid: string) =>
         }
         if (uuid === SidePanelTreeCategory.FAVORITES) {
             dispatch<any>(navigateToFavorites);
-        } else if(uuid === SidePanelTreeCategory.SHARED_WITH_ME){
+        } else if (uuid === SidePanelTreeCategory.SHARED_WITH_ME) {
             dispatch(navigateToSharedWithMe);
+        } else if (uuid === SidePanelTreeCategory.WORKFLOWS) {
+            dispatch(navigateToWorkflows);
         }
     };
 
@@ -33,6 +35,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 23c5ea2217972d07765cc720b0e3253f4ac50b2a..073de22c4ea4b9fa30aae1b15fc2311dfc706fe0 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';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
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..97be93d868927822df3129b8e60a681de2abb5dc 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),
index c79dc48f9f58e8c0bf81b659dcb3b00493001702..9124c660ddf4c4c69c459dc2e7ac4039459d9d01 100644 (file)
@@ -38,8 +38,9 @@ import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-a
 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';
 
 export const loadWorkbench = () =>
     async (dispatch: Dispatch, getState: () => RootState) => {
@@ -52,6 +53,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);
@@ -276,3 +278,9 @@ export const loadSharedWithMe = (dispatch: Dispatch) => {
     dispatch<any>(loadSharedWithMePanel());
     dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
 };
+
+export const loadWorkflow = (dispatch: Dispatch<any>) => {
+    dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.WORKFLOWS));
+    dispatch(loadWorkflowPanel());
+    dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
+};
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..2ca5337
--- /dev/null
@@ -0,0 +1,75 @@
+// 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 } 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>) {
+        try {
+            const response = await this.services.workflowService;
+            api.dispatch(updateResources([]));
+            api.dispatch(setItems({ kind: '', offset: 4, limit: 4, items: [], itemsAvailable: 4 }));
+        } 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..733fab1
--- /dev/null
@@ -0,0 +1,16 @@
+// 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';
+
+export const WORKFLOW_PANEL_ID = "workflowPanel";
+export const workflowPanelActions = bindDataExplorerActions(WORKFLOW_PANEL_ID);
+
+export const loadWorkflowPanel = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(workflowPanelActions.REQUEST_ITEMS());
+    };
\ No newline at end of file
index b9cc63c30475edc89797a07de66963806deea154..b9475f5010f595ffcfba8cae5836d20106183e00 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { Grid, Typography, withStyles } 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 } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
 import { connect } from 'react-redux';
@@ -48,6 +48,8 @@ export const renderIcon = (item: { kind: string }) => {
             return <CollectionIcon />;
         case ResourceKind.PROCESS:
             return <ProcessIcon />;
+        case ResourceKind.WORKFLOW:
+            return <WorkflowIcon />;
         default:
             return <DefaultIcon />;
     }
index 071b986ab57352050ef411e5cd6db7ecc3bd61ea..41442bba0d395e0f02f43d4ae6b3adb3bab00cbb 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 isButtonVisible = ({ router }: RootState) => {
+    const pathname = router.location ? router.location.pathname : '';
+    const match = !matchWorkflowRoute(pathname);
+    return !!match;
+};
+
+export const MainContentBar = connect((state: RootState) => ({
+    buttonVisible: isButtonVisible(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 ad1a266881993b8f5e38eb568ac0f8f2c175ef3d..54336575742ea83c51a6eb7a1b4a35d2a5f82427 100644 (file)
@@ -46,6 +46,7 @@ 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';
 
@@ -125,8 +126,8 @@ export const Workbench = withStyles(styles)(
                         {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}>
+                                    <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>
@@ -143,6 +144,7 @@ export const Workbench = withStyles(styles)(
                                                     <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..7aa26e6
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, Card, CardHeader, Typography } 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>} />
+            <DataTableDefaultView
+                icon={WorkflowIcon}
+                messages={['Please select a workflow to see its description.']} />
+        </Card>;
+    });
\ 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..ef0b5fb
--- /dev/null
@@ -0,0 +1,142 @@
+// 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,
+            }
+        ],
+        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>
+                <Grid item xs={6} style={{ paddingRight: '24px', display: 'grid' }}>
+                    <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));
+        }
+    }
+);
\ No newline at end of file