[ARVADOS-WORKBENCH2] created: 2.1.0-249-g2f729687

Git user git at public.arvados.org
Fri Mar 12 15:52:07 UTC 2021


        at  2f72968731d9a1f54a23022fd3bb13c29a7049f0 (commit)


commit 2f72968731d9a1f54a23022fd3bb13c29a7049f0
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Mar 11 16:03:23 2021 -0500

    Can create new studies & show listing.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/common/plugintypes.ts b/src/common/plugintypes.ts
index 00cc1e36..2ce0bb12 100644
--- a/src/common/plugintypes.ts
+++ b/src/common/plugintypes.ts
@@ -3,16 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Dispatch } from 'redux';
+import { Dispatch, Middleware } from 'redux';
 import { RootStore, RootState } from '~/store/store';
 import { ResourcesState } from '~/store/resources/resources';
 import { Location } from 'history';
+import { ServiceRepository } from "~/services/services";
 
 export type ElementListReducer = (startingList: React.ReactElement[], itemClass?: string) => React.ReactElement[];
 export type CategoriesListReducer = (startingList: string[]) => string[];
 export type NavigateMatcher = (dispatch: Dispatch, getState: () => RootState, uuid: string) => boolean;
 export type LocationChangeMatcher = (store: RootStore, pathname: string) => boolean;
 export type EnableNew = (location: Location, currentItemId: string, currentUserUUID: string | undefined, resources: ResourcesState) => boolean;
+export type MiddlewareListReducer = (startingList: Middleware[], services: ServiceRepository) => Middleware[];
 
 export interface PluginConfig {
     // Customize the list of possible center panels by adding or removing Route components.
@@ -42,4 +44,6 @@ export interface PluginConfig {
     enableNewButtonMatchers: EnableNew[];
 
     newButtonMenuList: ElementListReducer[];
+
+    middlewares: MiddlewareListReducer[];
 }
diff --git a/src/plugins.tsx b/src/plugins.tsx
index a7d033dd..4fad335d 100644
--- a/src/plugins.tsx
+++ b/src/plugins.tsx
@@ -15,15 +15,17 @@ export const pluginConfig: PluginConfig = {
     appBarRight: undefined,
     accountMenuList: [],
     enableNewButtonMatchers: [],
-    newButtonMenuList: []
+    newButtonMenuList: [],
+    middlewares: []
 };
 
 // Starting here, import and register your Workbench 2 plugins. //
 
 // import { register as blankUIPluginRegister } from '~/plugins/blank/index';
-import { register as examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
+import { register as examplePluginRegister, routePath as trackerRoutePath } from '~/plugins/sample-tracker/index';
+// import { register as examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
 import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
 
 // blankUIPluginRegister(pluginConfig);
 examplePluginRegister(pluginConfig);
-rootRedirectRegister(pluginConfig, exampleRoutePath);
+rootRedirectRegister(pluginConfig, trackerRoutePath);
diff --git a/src/plugins/sample-tracker/index.tsx b/src/plugins/sample-tracker/index.tsx
new file mode 100644
index 00000000..69c90e0a
--- /dev/null
+++ b/src/plugins/sample-tracker/index.tsx
@@ -0,0 +1,75 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Plugin UI for laboratory sample tracking
+
+import { PluginConfig } from '~/common/plugintypes';
+import * as React from 'react';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { push } from "react-router-redux";
+import { Route, matchPath } from "react-router";
+import { RootStore } from '~/store/store';
+import { activateSidePanelTreeItem } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { setSidePanelBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { Location } from 'history';
+import { handleFirstTimeLoad } from '~/store/workbench/workbench-actions';
+import {
+    AddStudyMenuComponent, StudiesMainPanel, CreateStudyDialog,
+    studyPanelColumns, studyPanelActions, openStudiesPanel,
+    StudiesPanelMiddlewareService, STUDY_PANEL_ID
+} from './study';
+import { dataExplorerMiddleware } from "~/store/data-explorer/data-explorer-middleware";
+
+const categoryName = "Studies";
+export const routePath = "/sample_tracker_Studies";
+
+export const register = (pluginConfig: PluginConfig) => {
+
+    pluginConfig.centerPanelList.push((elms) => {
+        elms.push(<Route path={routePath} component={StudiesMainPanel} />);
+        return elms;
+    });
+
+    pluginConfig.newButtonMenuList.push((elms, menuItemClass) => {
+        elms.push(<AddStudyMenuComponent className={menuItemClass} />);
+        return elms;
+    });
+
+    pluginConfig.navigateToHandlers.push((dispatch: Dispatch, getState: () => RootState, uuid: string) => {
+        if (uuid === categoryName) {
+            dispatch(push(routePath));
+            return true;
+        }
+        return false;
+    });
+
+    pluginConfig.sidePanelCategories.push((cats: string[]): string[] => { cats.push(categoryName); return cats; });
+
+    pluginConfig.locationChangeHandlers.push((store: RootStore, pathname: string): boolean => {
+        if (matchPath(pathname, { path: routePath, exact: true })) {
+            store.dispatch(handleFirstTimeLoad(
+                (dispatch: Dispatch) => {
+                    dispatch(studyPanelActions.SET_COLUMNS({ columns: studyPanelColumns }));
+                    dispatch<any>(openStudiesPanel);
+                    dispatch<any>(activateSidePanelTreeItem(categoryName));
+                    dispatch<any>(setSidePanelBreadcrumbs(categoryName));
+                }));
+            return true;
+        }
+        return false;
+    });
+
+    pluginConfig.enableNewButtonMatchers.push((location: Location) => (!!matchPath(location.pathname, { path: routePath, exact: true })));
+
+    pluginConfig.dialogs.push(<CreateStudyDialog />);
+
+    pluginConfig.middlewares.push((elms, services) => {
+        elms.push(dataExplorerMiddleware(
+            new StudiesPanelMiddlewareService(services, STUDY_PANEL_ID)
+        ));
+        return elms;
+    });
+
+};
diff --git a/src/plugins/sample-tracker/study.tsx b/src/plugins/sample-tracker/study.tsx
new file mode 100644
index 00000000..326a2434
--- /dev/null
+++ b/src/plugins/sample-tracker/study.tsx
@@ -0,0 +1,188 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { ServiceRepository } from "~/services/services";
+import { compose, MiddlewareAPI, Dispatch } from "redux";
+import { Card, CardContent } from "@material-ui/core";
+import { reduxForm, initialize } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { RootState } from '~/store/store';
+import { DispatchProp, connect } from 'react-redux';
+import { MenuItem } from "@material-ui/core";
+import { createProject } from "~/store/workbench/workbench-actions";
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { DataColumns } from '~/components/data-table/data-table';
+import { createTree } from '~/models/tree';
+import { ResourceName } from '~/views-components/data-explorer/renderers';
+import { SortDirection } from '~/components/data-table/data-column';
+import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action";
+import {
+    DataExplorerMiddlewareService,
+    listResultsToDataExplorerItemsMeta,
+    dataExplorerToListParams
+} from '~/store/data-explorer/data-explorer-middleware-service';
+import { GroupResource } from "~/models/group";
+import { ListResults } from '~/services/common-service/common-service';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
+import { DataExplorer as DataExplorerState, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
+import { updateResources } from "~/store/resources/resources-actions";
+
+const STUDY_CREATE_FORM_NAME = "studyCreateFormName";
+export const STUDY_PANEL_ID = "studyPanel";
+export const studyPanelActions = bindDataExplorerActions(STUDY_PANEL_ID);
+export const sampleTrackerStudyType = "sample_tracker:study";
+
+export interface ProjectCreateFormDialogData {
+    ownerUuid: string;
+    name: string;
+    description: string;
+}
+
+type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
+
+const StudyAddFields = () => <span>
+    <ProjectNameField label="Study name" />
+    <ProjectDescriptionField />
+</span>;
+
+const DialogStudyCreate = (props: DialogProjectProps) =>
+    <FormDialog
+        dialogTitle='New study'
+        formFields={StudyAddFields}
+        submitLabel='Create a Study'
+        {...props}
+    />;
+
+export const CreateStudyDialog = compose(
+    withDialog(STUDY_CREATE_FORM_NAME),
+    reduxForm<ProjectCreateFormDialogData>({
+        form: STUDY_CREATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            data.properties = { type: sampleTrackerStudyType };
+            dispatch(createProject(data));
+        }
+    })
+)(DialogStudyCreate);
+
+
+interface TrackerProps {
+    className?: string;
+}
+
+const studiesMapStateToProps = (state: RootState) => ({});
+
+export const openStudyCreateDialog = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(initialize(STUDY_CREATE_FORM_NAME, {}));
+        dispatch(dialogActions.OPEN_DIALOG({ id: STUDY_CREATE_FORM_NAME, data: {} }));
+    };
+
+export const AddStudyMenuComponent = connect(studiesMapStateToProps)(
+    ({ dispatch, className }: TrackerProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => dispatch(openStudyCreateDialog())}>Add Study</MenuItem >
+);
+
+export enum StudyPanelColumnNames {
+    NAME = "Name"
+}
+
+export const studyPanelColumns: DataColumns<string> = [
+    {
+        name: StudyPanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceName uuid={uuid} />
+    }
+];
+
+export const openStudiesPanel = (dispatch: Dispatch) => {
+    // dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+    dispatch(studyPanelActions.REQUEST_ITEMS());
+};
+
+export const StudiesMainPanel = connect(studiesMapStateToProps)(
+    ({ }: TrackerProps) =>
+        <Card>
+            <CardContent>
+                <DataExplorer
+                    id={STUDY_PANEL_ID}
+                    onRowClick={(uuid: string) => { }}
+                    onRowDoubleClick={(uuid: string) => { }}
+                    onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+                    contextMenuColumn={true}
+                    dataTableDefaultView={
+                        <DataTableDefaultView />
+                    } />
+            </CardContent>
+        </Card>);
+
+export const setItems = (listResults: ListResults<GroupResource>) =>
+    studyPanelActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+export const getFilters = (dataExplorer: DataExplorerState) => {
+    //    const columns = dataExplorer.columns as DataColumns<string>;
+    //    const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE));
+    //    const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
+    //    const activeStatusFilter = Object.keys(statusColumnFilters).find(
+    //        filterName => statusColumnFilters[filterName].selected
+    //    );
+    const fb = new FilterBuilder();
+    fb.addEqual("properties.type", sampleTrackerStudyType);
+
+    const nameFilters = new FilterBuilder()
+        .addILike("name", dataExplorer.searchValue)
+        .getFilters();
+
+    return joinFilters(
+        fb.getFilters(),
+        nameFilters,
+    );
+};
+
+export const getParams = (dataExplorer: DataExplorerState) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    filters: getFilters(dataExplorer),
+});
+
+export class StudiesPanelMiddlewareService 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 {
+            api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+            const response = await this.services.groupsService.list(getParams(dataExplorer));
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch (e) {
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            api.dispatch(studyPanelActions.SET_ITEMS({
+                items: [],
+                itemsAvailable: 0,
+                page: 0,
+                rowsPerPage: dataExplorer.rowsPerPage
+            }));
+            // api.dispatch(couldNotFetchProjectContents());
+        }
+    }
+}
diff --git a/src/store/store.ts b/src/store/store.ts
index 517368aa..f236d029 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -70,6 +70,8 @@ import { SubprocessMiddlewareService } from '~/store/subprocess-panel/subprocess
 import { SUBPROCESS_PANEL_ID } from '~/store/subprocess-panel/subprocess-panel-actions';
 import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-panel-action';
 import { Config } from '~/common/config';
+import { pluginConfig } from '~/plugins';
+import { MiddlewareListReducer } from '~/common/plugintypes';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -142,7 +144,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         return next(action);
     };
 
-    const middlewares: Middleware[] = [
+    let middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
         authMiddleware(services),
@@ -164,6 +166,11 @@ export function configureStore(history: History, services: ServiceRepository, co
         subprocessMiddleware,
     ];
 
+    const reduceMiddlewaresFn: (a: Middleware[],
+        b: MiddlewareListReducer) => Middleware[] = (a, b) => b(a, services);
+
+    middlewares = pluginConfig.middlewares.reduce(reduceMiddlewaresFn, middlewares);
+
     const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
     return createStore(rootReducer, enhancer);
 }
diff --git a/src/views-components/form-fields/project-form-fields.tsx b/src/views-components/form-fields/project-form-fields.tsx
index dc1e1612..3f576ab1 100644
--- a/src/views-components/form-fields/project-form-fields.tsx
+++ b/src/views-components/form-fields/project-form-fields.tsx
@@ -11,6 +11,7 @@ import { RootState } from "~/store/store";
 
 interface ProjectNameFieldProps {
     validate: Validator[];
+    label?: string;
 }
 
 // Validation behavior depends on the value of ForwardSlashNameSubstitution.
@@ -32,7 +33,7 @@ export const ProjectNameField = connect(
             name='name'
             component={TextField}
             validate={props.validate}
-            label="Project Name"
+            label={props.label || "Project Name"}
             autoFocus={true} /></span>
     );
 

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list