Add example and utility plugins.
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>
MAINTAINER=Arvados Package Maintainers <packaging@arvados.org>
# DEST_DIR will have the build package copied.
-DEST_DIR=/var/www/arvados-workbench2/workbench2/
+DEST_DIR=/var/www/$(APP_NAME)/workbench2/
# Debian package file
DEB_FILE=$(APP_NAME)_$(VERSION)-$(ITERATION)_amd64.deb
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Dispatch } from 'redux';
+import { RootStore, RootState } from '~/store/store';
+
+export type RouteListReducer = (startingList: React.ReactElement[]) => 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 interface PluginConfig {
+ // Customize the list of possible center panels by adding or removing Route components.
+ centerPanelList: RouteListReducer[];
+
+ // Customize the list of side panel categories
+ sidePanelCategories: CategoriesListReducer[];
+
+ // Add to the list of possible dialogs by adding dialog components.
+ dialogs: React.ReactElement[];
+
+ // Add navigation actions for identifiers
+ navigateToHandlers: NavigateMatcher[];
+
+ // Add handlers for navigation actions
+ locationChangeHandlers: LocationChangeMatcher[];
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PluginConfig } from '~/common/plugintypes';
+
+export const pluginConfig: PluginConfig = {
+ centerPanelList: [],
+ sidePanelCategories: [],
+ dialogs: [],
+ navigateToHandlers: [],
+ locationChangeHandlers: []
+};
+
+// 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 rootRedirectRegister } from '~/plugins/root-redirect/index';
+
+blankUIPluginRegister(pluginConfig);
+examplePluginRegister(pluginConfig);
+rootRedirectRegister(pluginConfig, exampleRoutePath);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Example plugin.
+
+import { PluginConfig } from '~/common/plugintypes';
+
+export const register = (pluginConfig: PluginConfig) => {
+
+ pluginConfig.centerPanelList.push((elms) => []);
+
+ pluginConfig.sidePanelCategories.push((cats: string[]): string[] => []);
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Example plugin.
+
+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 { Typography } from "@material-ui/core";
+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';
+
+const categoryName = "Plugin Example";
+export const routePath = "/examplePlugin";
+
+const ExamplePluginMainPanel = (props: {}) => {
+ return <Typography>
+ This is a example main panel plugin.
+ </Typography>;
+};
+
+export const register = (pluginConfig: PluginConfig) => {
+
+ pluginConfig.centerPanelList.push((elms) => {
+ elms.push(<Route path={routePath} component={ExamplePluginMainPanel} />);
+ 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(activateSidePanelTreeItem(categoryName));
+ store.dispatch<any>(setSidePanelBreadcrumbs(categoryName));
+ return true;
+ }
+ return false;
+ });
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PluginConfig } from '~/common/plugintypes';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { push } from "react-router-redux";
+
+export const register = (pluginConfig: PluginConfig, redirect: string) => {
+
+ pluginConfig.navigateToHandlers.push((dispatch: Dispatch, getState: () => RootState, uuid: string) => {
+ if (uuid === SidePanelTreeCategory.PROJECTS) {
+ dispatch(push(redirect));
+ return true;
+ }
+ return false;
+ });
+};
import { dialogActions } from '~/store/dialog/dialog-actions';
import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
import { searchBarActions } from '~/store/search-bar/search-bar-actions';
+import { pluginConfig } from '~/plugins';
export const addRouteChangeHandlers = (history: History, store: RootStore) => {
const handler = handleLocationChange(store);
store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
store.dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
+ for (const locChangeFn of pluginConfig.locationChangeHandlers) {
+ if (locChangeFn(store, pathname)) {
+ return;
+ }
+ }
+
if (projectMatch) {
store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
} else if (collectionMatch) {
} else if (allProcessesMatch) {
store.dispatch(WorkbenchActions.loadAllProcesses());
}
-};
\ No newline at end of file
+};
export const setTrashBreadcrumbs = (uuid: string) =>
setCategoryBreadcrumbs(uuid, SidePanelTreeCategory.TRASH);
-export const setCategoryBreadcrumbs = (uuid: string, category: SidePanelTreeCategory) =>
+export const setCategoryBreadcrumbs = (uuid: string, category: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const ancestors = await services.ancestorsService.ancestors(uuid, '');
dispatch(updateResources(ancestors));
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { GROUPS_PANEL_LABEL } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { pluginConfig } from '~/plugins';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+
+const navigationNotAvailable = (id: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `${id} not available`,
+ hideDuration: 3000,
+ kind: SnackbarKind.ERROR
+ });
export const navigateTo = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState) => {
+
+ for (const navToFn of pluginConfig.navigateToHandlers) {
+ if (navToFn(dispatch, getState, uuid)) {
+ return;
+ }
+ }
+
const kind = extractUuidKind(uuid);
switch (kind) {
case ResourceKind.PROJECT:
}
switch (uuid) {
+ case SidePanelTreeCategory.PROJECTS:
+ const usr = getState().auth.user;
+ if (usr) {
+ dispatch<any>(pushOrGoto(getNavUrl(usr.uuid, getState().auth)));
+ }
+ return;
case SidePanelTreeCategory.FAVORITES:
dispatch<any>(navigateToFavorites);
return;
dispatch(navigateToAllProcesses);
return;
}
+
+ dispatch(navigationNotAvailable(uuid));
};
+
export const navigateToNotFound = push(Routes.NO_MATCH);
export const navigateToRoot = push(Routes.ROOT);
export const navigateToProcessLogs = compose(push, getProcessLogUrl);
export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const usr = getState().auth.user;
- if (usr) {
- dispatch<any>(navigateTo(usr.uuid));
- }
+ navigateTo(SidePanelTreeCategory.PROJECTS)(dispatch, getState);
};
export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME);
import { ResourceKind } from '~/models/resource';
import { GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
import { GroupClass } from '~/models/group';
+import { CategoriesListReducer } from '~/common/plugintypes';
+import { pluginConfig } from '~/plugins';
export enum SidePanelTreeCategory {
PROJECTS = 'Projects',
return [];
};
-const SIDE_PANEL_CATEGORIES: string[] = [
+let SIDE_PANEL_CATEGORIES: string[] = [
SidePanelTreeCategory.PROJECTS,
SidePanelTreeCategory.SHARED_WITH_ME,
SidePanelTreeCategory.PUBLIC_FAVORITES,
SidePanelTreeCategory.FAVORITES,
SidePanelTreeCategory.WORKFLOWS,
SidePanelTreeCategory.ALL_PROCESSES,
- SidePanelTreeCategory.TRASH,
- "Blibber blubber"
+ SidePanelTreeCategory.TRASH
];
+const reduceCatsFn: (a: string[],
+ b: CategoriesListReducer) => string[] = (a, b) => b(a);
+
+SIDE_PANEL_CATEGORIES = pluginConfig.sidePanelCategories.reduce(reduceCatsFn, SIDE_PANEL_CATEGORIES);
+
export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
+
export const initSidePanelTree = () =>
(dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
const rootProjectUuid = getUserUuid(getState());
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from 'redux';
-import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe, navigateToWorkflows, navigateToPublicFavorites, navigateToAllProcesses } from '~/store/navigation/navigation-action';
-import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions';
+import { navigateTo } from '~/store/navigation/navigation-action';
export const navigateFromSidePanel = (id: string) =>
(dispatch: Dispatch) => {
- if (isSidePanelTreeCategory(id)) {
- dispatch<any>(getSidePanelTreeCategoryAction(id));
- } else {
- dispatch<any>(navigateTo(id));
- }
+ dispatch<any>(navigateTo(id));
};
-
-const getSidePanelTreeCategoryAction = (id: string) => {
- switch (id) {
- case SidePanelTreeCategory.FAVORITES:
- return navigateToFavorites;
- case SidePanelTreeCategory.PUBLIC_FAVORITES:
- return navigateToPublicFavorites;
- case SidePanelTreeCategory.TRASH:
- return navigateToTrash;
- case SidePanelTreeCategory.SHARED_WITH_ME:
- return navigateToSharedWithMe;
- case SidePanelTreeCategory.WORKFLOWS:
- return navigateToWorkflows;
- case SidePanelTreeCategory.ALL_PROCESSES:
- return navigateToAllProcesses;
- default:
- return sidePanelTreeCategoryNotAvailable(id);
- }
-};
-
-const sidePanelTreeCategoryNotAvailable = (id: string) =>
- snackbarActions.OPEN_SNACKBAR({
- message: `${id} not available`,
- hideDuration: 3000,
- kind: SnackbarKind.ERROR
- });
setSidePanelBreadcrumbs,
setTrashBreadcrumbs
} from '~/store/breadcrumbs/breadcrumbs-actions';
-import { navigateTo } from '~/store/navigation/navigation-action';
+import { navigateTo, navigateToRootProject } from '~/store/navigation/navigation-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { ServiceRepository } from '~/services/services';
import { getResource } from '~/store/resources/resources';
if (router.location) {
const match = matchRootRoute(router.location.pathname);
if (match) {
- dispatch<any>(navigateTo(user.uuid));
+ dispatch<any>(navigateToRootProject);
}
}
} else {
import { AutoLogout } from '~/views-components/auto-logout/auto-logout';
import { RestoreCollectionVersionDialog } from '~/views-components/collections-dialog/restore-version-dialog';
import { WebDavS3InfoDialog } from '~/views-components/webdav-s3-dialog/webdav-s3-dialog';
-import { pluginCenterPanelRoutes, RouteReducer, pluginDialogs } from '~/plugins';
+import { pluginConfig } from '~/plugins';
+import { RouteListReducer } from '~/common/plugintypes';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
</>;
const reduceRoutesFn: (a: React.ReactElement[],
- b: RouteReducer) => React.ReactElement[] = (a, b) => b(a);
+ b: RouteListReducer) => React.ReactElement[] = (a, b) => b(a);
-routes = React.createElement(React.Fragment, null, pluginCenterPanelRoutes.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
+routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
export const WorkbenchPanel =
withStyles(styles)((props: WorkbenchPanelProps) =>
<VirtualMachineAttributesDialog />
<FedLogin />
<WebDavS3InfoDialog />
- {pluginDialogs}
+ {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
</Grid>
);