[ARVADOS-WORKBENCH2] updated: 2.1.0-254-gc257e8b3

Git user git at public.arvados.org
Mon Mar 15 21:42:24 UTC 2021


Summary of changes:
 src/models/link.ts                                 |   5 +-
 src/plugins/sample-tracker/index.tsx               |  14 +-
 src/plugins/sample-tracker/sampleList.tsx          | 334 +++++++++++++++++++++
 .../project-properties-dialog.tsx                  |  70 ++---
 4 files changed, 384 insertions(+), 39 deletions(-)
 create mode 100644 src/plugins/sample-tracker/sampleList.tsx

       via  c257e8b30811ef9d7be4d52b0af13c566c8f5756 (commit)
      from  6c9150296ea85b9bba0fe151b552a687748b8141 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit c257e8b30811ef9d7be4d52b0af13c566c8f5756
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Mon Mar 15 17:42:09 2021 -0400

    WIP dialog for adding samples
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/models/link.ts b/src/models/link.ts
index 785d531c..1c82fe58 100644
--- a/src/models/link.ts
+++ b/src/models/link.ts
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { TagProperty } from "~/models/tag";
 import { Resource, ResourceKind } from '~/models/resource';
 
 export interface LinkResource extends Resource {
@@ -12,7 +11,7 @@ export interface LinkResource extends Resource {
     tailKind: string;
     linkClass: string;
     name: string;
-    properties: TagProperty;
+    properties: any;
     kind: ResourceKind.LINK;
 }
 
@@ -21,4 +20,4 @@ export enum LinkClass {
     TAG = 'tag',
     PERMISSION = 'permission',
     PRESET = 'preset',
-}
\ No newline at end of file
+}
diff --git a/src/plugins/sample-tracker/index.tsx b/src/plugins/sample-tracker/index.tsx
index bd6faa97..fabe85ca 100644
--- a/src/plugins/sample-tracker/index.tsx
+++ b/src/plugins/sample-tracker/index.tsx
@@ -26,6 +26,9 @@ import {
     PatientListPanelMiddlewareService,
     patientListPanelColumns, patientListPanelActions, patientRoutePath, patientBaseRoutePath
 } from './patientList';
+import {
+    AddSampleMenuComponent, CreateSampleDialog
+} from './sampleList';
 import {
     openStudyPanel, StudyMainPanel
 } from './study';
@@ -50,6 +53,7 @@ export const register = (pluginConfig: PluginConfig) => {
     pluginConfig.newButtonMenuList.push((elms, menuItemClass) => {
         elms.push(<AddStudyMenuComponent className={menuItemClass} />);
         elms.push(<AddPatientMenuComponent className={menuItemClass} />);
+        elms.push(<AddSampleMenuComponent className={menuItemClass} />);
         return elms;
     });
 
@@ -106,7 +110,13 @@ export const register = (pluginConfig: PluginConfig) => {
                     // dispatch<any>(activateSidePanelTreeItem(categoryName));
                     const patientrsc = getResource<GroupResource>(pathname)(store.getState().resources);
                     if (patientrsc) {
-                        dispatch<any>(setBreadcrumbs([{ label: categoryName, uuid: categoryName }, { label: patientrsc.name, uuid: pathname }]));
+                        const studyid = studyListRoutePath + "/" + patientrsc.ownerUuid;
+                        const studyrsc = getResource<GroupResource>(studyid)(store.getState().resources);
+                        if (studyrsc) {
+                            dispatch<any>(setBreadcrumbs([{ label: categoryName, uuid: categoryName },
+                            { label: studyrsc.name, uuid: studyid },
+                            { label: patientrsc.name, uuid: pathname }]));
+                        }
                     }
                 }));
             return true;
@@ -116,9 +126,11 @@ export const register = (pluginConfig: PluginConfig) => {
     });
 
     pluginConfig.enableNewButtonMatchers.push((location: Location) => (!!matchPath(location.pathname, { path: studyListRoutePath, exact: false })));
+    pluginConfig.enableNewButtonMatchers.push((location: Location) => (!!matchPath(location.pathname, { path: patientBaseRoutePath, exact: false })));
 
     pluginConfig.dialogs.push(<CreateStudyDialog />);
     pluginConfig.dialogs.push(<CreatePatientDialog />);
+    pluginConfig.dialogs.push(<CreateSampleDialog />);
 
     pluginConfig.middlewares.push((elms, services) => {
         elms.push(dataExplorerMiddleware(
diff --git a/src/plugins/sample-tracker/sampleList.tsx b/src/plugins/sample-tracker/sampleList.tsx
new file mode 100644
index 00000000..af05bd6a
--- /dev/null
+++ b/src/plugins/sample-tracker/sampleList.tsx
@@ -0,0 +1,334 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { ServiceRepository } from "~/services/services";
+import { compose, MiddlewareAPI, Dispatch } from "redux";
+import { reduxForm, initialize, WrappedFieldProps, InjectedFormProps, Field, reset, startSubmit } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { RootState } from '~/store/store';
+import { DispatchProp, connect } from 'react-redux';
+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 { TextField } from "~/components/text-field/text-field";
+import { FormControl, InputLabel } from '@material-ui/core';
+import { withStyles, WithStyles } from '@material-ui/core/styles';
+
+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";
+import {
+    patientRoutePath, patientBaseRoutePath
+} from './patientList';
+import { matchPath } from "react-router";
+import { getProperty } from '~/store/properties/properties';
+import { MenuItem, Select } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { getResource } from "~/store/resources/resources";
+
+const SAMPLE_CREATE_FORM_NAME = "sampleCreateFormName";
+export const SAMPLE_LIST_PANEL_ID = "sampleListPanel";
+export const sampleListPanelActions = bindDataExplorerActions(SAMPLE_LIST_PANEL_ID);
+export const sampleTrackerSampleType = "sample_tracker:sample";
+export const PATIENT_PANEL_CURRENT_UUID = "PatientPanelCurrentUUID";
+export const SAMPLE_PANEL_CURRENT_UUID = "SamplePanelCurrentUUID";
+export const sampleBaseRoutePath = "/SampleTracker/Sample";
+export const sampleRoutePath = sampleBaseRoutePath + "/:uuid";
+
+export interface SampleCreateFormDialogData {
+    patientUuid: string;
+    collectionType: string;
+    sampleType: string;
+    collectedAt: string;
+    timePoint: number;
+    flowStartedAt: string;
+    flowCompletedAt: string;
+}
+
+type DialogSampleProps = WithDialogProps<{}> & InjectedFormProps<SampleCreateFormDialogData>;
+
+type CssRules = 'selectWidth';
+
+const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
+    selectWidth: {
+        width: theme.spacing.unit * 20,
+    }
+}));
+
+enum CollectionType {
+    PERIPHERAL_BLOOD = 'peripheral_blood',
+    BONE_MARROW = 'bone_marrow'
+}
+
+enum SampleType {
+    TUMOR = 'tumor',
+    NORMAL = 'normal'
+}
+
+export const CollectionTypeSelect = styles(
+    ({ classes, input }: WrappedFieldProps & WithStyles<CssRules>) =>
+        <FormControl className={classes.selectWidth}>
+            <Select
+                {...input}>
+                <MenuItem value={CollectionType.PERIPHERAL_BLOOD}>
+                    Peripheral Blood
+		</MenuItem>
+                <MenuItem value={CollectionType.BONE_MARROW}>
+                    Bone Marrow
+		</MenuItem>
+            </Select>
+        </FormControl>);
+
+export const SampleTypeSelect = styles(
+    ({ classes, input }: WrappedFieldProps & WithStyles<CssRules>) =>
+        <FormControl className={classes.selectWidth}>
+            <Select
+                {...input}>
+                <MenuItem value={SampleType.TUMOR}>
+                    Tumor
+		</MenuItem>
+                <MenuItem value={SampleType.NORMAL}>
+                    Normal
+		</MenuItem>
+            </Select>
+        </FormControl>);
+
+const SampleAddFields = () => <span>
+
+    <InputLabel>Patient time point</InputLabel>
+    <Field
+        name='timePoint'
+        component={TextField}
+        type="number" />
+
+    <InputLabel>Collection date</InputLabel>
+    <Field
+        name='collectedAt'
+        component={TextField}
+        type="date" />
+
+    <InputLabel>Collection type</InputLabel>
+    <div>
+        <Field
+            name='collectionType'
+            component={CollectionTypeSelect} />
+    </div>
+
+    <InputLabel>Sample type</InputLabel>
+    <div>
+        <Field
+            name='sampleType'
+            component={SampleTypeSelect} />
+    </div>
+    <InputLabel>Flow started at</InputLabel>
+    <Field
+        name='flowStartedAt'
+        component={TextField}
+        type="date"
+    />
+
+    <InputLabel>Flow ended at</InputLabel>
+    <Field
+        name='flowEndedAt'
+        component={TextField}
+        type="date"
+    />
+</span>;
+
+const DialogSampleCreate = (props: DialogSampleProps) =>
+    <FormDialog
+        dialogTitle='Add sample'
+        formFields={SampleAddFields}
+        submitLabel='Add a sample'
+        {...props}
+    />;
+
+const makeSampleId = (data: SampleCreateFormDialogData, state: RootState): string => {
+    const rsc = getResource<GroupResource>(patientBaseRoutePath + "/" + data.patientUuid)(state.resources);
+    let id = rsc!.name;
+    if (data.collectionType === CollectionType.PERIPHERAL_BLOOD) {
+        id = id + "_PB";
+    }
+    if (data.collectionType === CollectionType.BONE_MARROW) {
+        id = id + "_BM";
+    }
+    if (data.timePoint < 10) {
+        id = id + "_0" + data.timePoint;
+    } else {
+        id = id + "_" + data.timePoint;
+    }
+    return id;
+};
+
+const createSample = (data: SampleCreateFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+        dispatch(startSubmit(SAMPLE_CREATE_FORM_NAME));
+        const sampleId = makeSampleId(data, getState());
+        await services.linkService.create({
+            ownerUuid: data.patientUuid,
+            name: sampleId,
+            linkClass: sampleTrackerSampleType,
+            properties: {
+                "sample_tracker:collection_type": data.collectionType,
+                "sample_tracker:sample_type": data.sampleType,
+                "sample_tracker:collected_at": data.collectedAt,
+                "sample_tracker:time_point": data.timePoint,
+                "sample_tracker:flow_started_at": data.flowStartedAt,
+                "sample_tracker:flow_completed_at": data.flowCompletedAt,
+            },
+        });
+        dispatch(dialogActions.CLOSE_DIALOG({ id: SAMPLE_CREATE_FORM_NAME }));
+        dispatch(reset(SAMPLE_CREATE_FORM_NAME));
+    };
+
+export const CreateSampleDialog = compose(
+    withDialog(SAMPLE_CREATE_FORM_NAME),
+    reduxForm<SampleCreateFormDialogData>({
+        form: SAMPLE_CREATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(createSample(data));
+        }
+    })
+)(DialogSampleCreate);
+
+export interface MenuItemProps {
+    className?: string;
+    patientUuid?: string;
+}
+
+export interface PatientPathId {
+    uuid: string;
+}
+
+export const samplesMapStateToProps = (state: RootState) => {
+    const props: MenuItemProps = {};
+    const patientid = matchPath<PatientPathId>(state.router.location!.pathname, { path: patientRoutePath, exact: true });
+    if (patientid) {
+        props.patientUuid = patientid.params.uuid;
+    }
+    return props;
+};
+
+const openSampleCreateDialog = (patientUuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(initialize(SAMPLE_CREATE_FORM_NAME, { patientUuid }));
+        dispatch(dialogActions.OPEN_DIALOG({ id: SAMPLE_CREATE_FORM_NAME, data: {} }));
+    };
+
+export const AddSampleMenuComponent = connect<{}, {}, MenuItemProps>(samplesMapStateToProps)(
+    ({ patientUuid, dispatch, className }: MenuItemProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => dispatch(openSampleCreateDialog(patientUuid!))} disabled={!patientUuid}>Add Sample</MenuItem >
+);
+
+
+enum SamplePanelColumnNames {
+    NAME = "Name"
+}
+
+export const sampleListPanelColumns: DataColumns<string> = [
+    {
+        name: SamplePanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceName uuid={uuid} />
+    }
+];
+
+export interface TrackerProps {
+    className?: string;
+}
+
+export const SampleListPanel = connect(samplesMapStateToProps)(
+    ({ }: TrackerProps) =>
+        <DataExplorer
+            id={SAMPLE_LIST_PANEL_ID}
+            onRowClick={(uuid: string) => { }}
+            onRowDoubleClick={(uuid: string) => { }}
+            onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+            contextMenuColumn={true}
+            dataTableDefaultView={
+                <DataTableDefaultView />
+            } />);
+
+const setItems = (listResults: ListResults<GroupResource>) =>
+    sampleListPanelActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const getFilters = (dataExplorer: DataExplorerState, patientUuid: string) => {
+    const fb = new FilterBuilder();
+    fb.addEqual("owner_uuid", patientUuid);
+    fb.addEqual("properties.type", sampleTrackerSampleType);
+
+    const nameFilters = new FilterBuilder()
+        .addILike("name", dataExplorer.searchValue)
+        .getFilters();
+
+    return joinFilters(
+        fb.getFilters(),
+        nameFilters,
+    );
+};
+
+const getParams = (dataExplorer: DataExplorerState, patientUuid: string) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    filters: getFilters(dataExplorer, patientUuid),
+});
+
+export class SampleListPanelMiddlewareService 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());
+
+        const patientUuid = getProperty<string>(PATIENT_PANEL_CURRENT_UUID)(state.properties);
+
+        if (!patientUuid) {
+            return;
+        }
+
+        try {
+            api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+            const response = await this.services.groupsService.list(getParams(dataExplorer, patientUuid));
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            for (const i of response.items) {
+                i.uuid = sampleBaseRoutePath + "/" + i.uuid;
+            }
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch (e) {
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            api.dispatch(sampleListPanelActions.SET_ITEMS({
+                items: [],
+                itemsAvailable: 0,
+                page: 0,
+                rowsPerPage: dataExplorer.rowsPerPage
+            }));
+            // api.dispatch(couldNotFetchProjectContents());
+        }
+    }
+}
diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
index e1874d95..c2982b3d 100644
--- a/src/views-components/project-properties-dialog/project-properties-dialog.tsx
+++ b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
@@ -40,42 +40,42 @@ const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionPr
     handleDelete: (key: string, value: string) => () => dispatch<any>(deleteProjectProperty(key, value)),
 });
 
-type ProjectPropertiesDialogProps =  ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
+type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
 
 export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)(
     withStyles(styles)(
-    withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
-        ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
-            <Dialog open={open}
-                onClose={closeDialog}
-                fullWidth
-                maxWidth='sm'>
-                <DialogTitle>Properties</DialogTitle>
-                <DialogContent>
-                    <ProjectPropertiesForm />
-                    {project && project.properties &&
-                        Object.keys(project.properties).map(k =>
-                            Array.isArray(project.properties[k])
-                            ? project.properties[k].map((v: string) =>
-                                getPropertyChip(
-                                    k, v,
-                                    handleDelete(k, v),
-                                    classes.tag))
-                            : getPropertyChip(
-                                k, project.properties[k],
-                                handleDelete(k, project.properties[k]),
-                                classes.tag)
-                        )
-                    }
-                </DialogContent>
-                <DialogActions>
-                    <Button
-                        variant='text'
-                        color='primary'
-                        onClick={closeDialog}>
-                        Close
+        withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
+            ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
+                <Dialog open={open}
+                    onClose={closeDialog}
+                    fullWidth
+                    maxWidth='sm'>
+                    <DialogTitle>Properties</DialogTitle>
+                    <DialogContent>
+                        <ProjectPropertiesForm />
+                        {project && project.properties &&
+                            Object.keys(project.properties).map(k =>
+                                Array.isArray(project.properties[k])
+                                    ? project.properties[k].map((v: string) =>
+                                        getPropertyChip(
+                                            k, v,
+                                            handleDelete(k, v),
+                                            classes.tag))
+                                    : getPropertyChip(
+                                        k, project.properties[k],
+                                        handleDelete(k, project.properties[k]),
+                                        classes.tag)
+                            )
+                        }
+                    </DialogContent>
+                    <DialogActions>
+                        <Button
+                            variant='text'
+                            color='primary'
+                            onClick={closeDialog}>
+                            Close
                     </Button>
-                </DialogActions>
-            </Dialog>
-    )
-));
\ No newline at end of file
+                    </DialogActions>
+                </Dialog>
+        )
+    ));

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list