[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