[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