.put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
}
+
}
export const getCommonResourceServiceError = (errorResponse: any) => {
import { Provider } from "react-redux";
import { Workbench } from './views/workbench/workbench';
import './index.css';
-import { Route } from "react-router";
+import { Route } from 'react-router';
import createBrowserHistory from "history/createBrowserHistory";
-import { configureStore } from "./store/store";
+import { History } from "history";
+import { configureStore, RootStore } from './store/store';
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "./views-components/api-token/api-token";
import { initAuth } from "./store/auth/auth-action";
import { createServices } from "./services/services";
-import { getProjectList } from "./store/project/project-action";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from './common/custom-theme';
import { fetchConfig } from './common/config';
import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
+import { addRouteChangeHandlers } from './routes/routes';
+import { loadWorkbench } from './store/navigation/navigation-action';
const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
fetchConfig()
- .then(config => {
+ .then(async (config) => {
const history = createBrowserHistory();
const services = createServices(config);
const store = configureStore(history, services);
+ store.subscribe(initListener(history, store));
+
store.dispatch(initAuth());
- store.dispatch(getProjectList(services.authService.getUuid()));
- const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
- const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
+ const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
+ const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props} />;
const App = () =>
<MuiThemeProvider theme={CustomTheme}>
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
- <Route path="/" component={WorkbenchComponent} />
<Route path="/token" component={TokenComponent} />
+ <Route path="/" component={WorkbenchComponent} />
</div>
</ConnectedRouter>
</Provider>
<App />,
document.getElementById('root') as HTMLElement
);
+
+
});
+const initListener = (history: History, store: RootStore) => {
+ let initialized = false;
+ return async () => {
+ const { router, auth } = store.getState();
+ if (router.location && auth.user && !initialized) {
+ initialized = true;
+ await store.dispatch(loadWorkbench());
+ addRouteChangeHandlers(history, store);
+ }
+ };
+};
+
COLLECTION = '4zz18'
}
+export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
+export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
+
+export const isResourceUuid = (uuid: string) =>
+ RESOURCE_UUID_REGEX.test(uuid);
+
export const extractUuidObjectType = (uuid: string) => {
- const match = /(.{5})-(.{5})-(.{15})/.exec(uuid);
+ const match = RESOURCE_UUID_REGEX.exec(uuid);
return match
- ? match[2]
+ ? match[0].split('-')[1]
: undefined;
};
export const extractUuidKind = (uuid: string = '') => {
const objectType = extractUuidObjectType(uuid);
- switch(objectType){
+ switch (objectType) {
case ResourceObjectType.USER:
return ResourceKind.USER;
case ResourceObjectType.GROUP:
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { History, Location } from 'history';
+import { RootStore } from '../store/store';
+import { matchPath } from 'react-router';
+import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
+import { getProjectUrl } from '../models/project';
+import { getCollectionUrl } from '~/models/collection';
+import { loadProject, loadFavorites, loadCollection } from '../store/navigation/navigation-action';
+
+export const Routes = {
+ ROOT: '/',
+ PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
+ COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
+ FAVORITES: '/favorites',
+};
+
+export const getResourceUrl = (uuid: string) => {
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ return getProjectUrl(uuid);
+ case ResourceKind.COLLECTION:
+ return getCollectionUrl(uuid);
+ default:
+ return undefined;
+ }
+};
+
+export const addRouteChangeHandlers = (history: History, store: RootStore) => {
+ const handler = handleLocationChange(store);
+ handler(history.location);
+ history.listen(handler);
+};
+
+export const matchRootRoute = (route: string) =>
+ matchPath(route, { path: Routes.ROOT, exact: true });
+
+export const matchFavoritesRoute = (route: string) =>
+ matchPath(route, { path: Routes.FAVORITES });
+
+export interface ProjectRouteParams {
+ id: string;
+}
+
+export const matchProjectRoute = (route: string) =>
+ matchPath<ProjectRouteParams>(route, { path: Routes.PROJECTS });
+
+export interface CollectionRouteParams {
+ id: string;
+}
+
+export const matchCollectionRoute = (route: string) =>
+ matchPath<ProjectRouteParams>(route, { path: Routes.COLLECTIONS });
+
+
+const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
+ const projectMatch = matchProjectRoute(pathname);
+ const collectionMatch = matchCollectionRoute(pathname);
+ const favoriteMatch = matchFavoritesRoute(pathname);
+ if (projectMatch) {
+ store.dispatch(loadProject(projectMatch.params.id));
+ } else if (collectionMatch) {
+ store.dispatch(loadCollection(collectionMatch.params.id));
+ } else if (favoriteMatch) {
+ store.dispatch(loadFavorites());
+ }
+};
import { RootState } from "../store";
import { ServiceRepository } from "~/services/services";
import { AxiosInstance } from "axios";
+import { initSidePanelTree } from '../side-panel-tree/side-panel-tree-actions';
+import { updateResources } from '../resources/resources-actions';
export const authActions = unionize({
SAVE_API_TOKEN: ofType<string>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
}, {
- tag: 'type',
- value: 'payload'
-});
+ tag: 'type',
+ value: 'payload'
+ });
function setAuthorizationHeader(services: ServiceRepository, token: string) {
services.apiClient.defaults.headers.common = {
delete client.defaults.headers.common.Authorization;
}
-export const initAuth = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const user = services.authService.getUser();
- const token = services.authService.getApiToken();
- if (token) {
- setAuthorizationHeader(services, token);
- }
- if (token && user) {
- dispatch(authActions.INIT({ user, token }));
- }
-};
+export const initAuth = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const user = services.authService.getUser();
+ const token = services.authService.getApiToken();
+ if (token) {
+ setAuthorizationHeader(services, token);
+ }
+ if (token && user) {
+ dispatch(authActions.INIT({ user, token }));
+ }
+ };
export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
services.authService.saveApiToken(token);
export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
-export const loadCollection = (uuid: string) =>
+export const loadCollectionPanel = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
dispatch(resourcesActions.SET_RESOURCES([collection]));
dispatch<any>(loadCollectionFiles(collection.uuid));
dispatch<any>(loadCollectionTags(collection.uuid));
+ return collection;
};
export const loadCollectionTags = (uuid: string) =>
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataExplorer } from './data-explorer-reducer';
+import { ListArguments, ListResults } from '~/common/api/common-resource-service';
export abstract class DataExplorerMiddlewareService {
protected readonly id: string;
abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
}
+
+export const getDataExplorerColumnFilters = <T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] => {
+ const column = columns.find(c => c.name === columnName);
+ return column ? column.filters.filter(f => f.selected) : [];
+};
+
+export const dataExplorerToListParams = <R>(dataExplorer: DataExplorer) => ({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+});
+
+export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
+ itemsAvailable,
+ page: Math.floor(offset / limit),
+ rowsPerPage: limit
+});
\ No newline at end of file
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { updateFavorites } from "../favorites/favorites-actions";
import { favoritePanelActions } from "./favorite-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
import { LinkResource } from "~/models/link";
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
import { resourcesActions } from "~/store/resources/resources-actions";
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
}
requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
- const dataExplorer = api.getState().dataExplorer[this.getId()];
- const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
- const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
- const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+ if (!dataExplorer) {
+ api.dispatch(favoritesPanelDataExplorerIsNotSet());
+ } else {
- const linkOrder = new OrderBuilder<LinkResource>();
- const contentOrder = new OrderBuilder<GroupContentsResource>();
+ const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
- if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
- const direction = sortColumn.sortDirection === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+ const linkOrder = new OrderBuilder<LinkResource>();
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
- linkOrder.addOrder(direction, "name");
- contentOrder
- .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
- .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
- .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
- }
+ if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+ const direction = sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ linkOrder.addOrder(direction, "name");
+ contentOrder
+ .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
+ }
- this.services.favoriteService
- .list(this.services.authService.getUuid()!, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- linkOrder: linkOrder.getOrder(),
- contentOrder: contentOrder.getOrder(),
- filters: new FilterBuilder()
- .addIsA("headUuid", typeFilters.map(filter => filter.type))
- .addILike("name", dataExplorer.searchValue)
- .getFilters()
- })
- .then(response => {
- api.dispatch(resourcesActions.SET_RESOURCES(response.items));
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: response.items.map(resource => resource.uuid),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- })
- .catch(() => {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- });
+ this.services.favoriteService
+ .list(this.services.authService.getUuid()!, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ linkOrder: linkOrder.getOrder(),
+ contentOrder: contentOrder.getOrder(),
+ filters: new FilterBuilder()
+ .addIsA("headUuid", typeFilters.map(filter => filter.type))
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters()
+ })
+ .then(response => {
+ api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: response.items.map(resource => resource.uuid),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ })
+ .catch(() => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ });
+ }
}
}
+
+const favoritesPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Favorites panel is not ready.'
+ });
});
};
-export const checkPresenceInFavorites = (resourceUuids: string[]) =>
+export const updateFavorites = (resourceUuids: string[]) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
dispatch(favoritesActions.UPDATE_FAVORITES(results));
});
};
-
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from "redux";
-import { getProjectList, projectActions } from "../project/project-action";
+import { Dispatch, compose } from 'redux';
import { push } from "react-router-redux";
-import { TreeItemStatus } from "~/components/tree/tree";
-import { findTreeItem } from "../project/project-reducer";
import { RootState } from "../store";
import { ResourceKind, Resource } from '~/models/resource';
-import { projectPanelActions } from "../project-panel/project-panel-action";
import { getCollectionUrl } from "~/models/collection";
-import { getProjectUrl, ProjectResource } from "~/models/project";
-import { ProjectService } from "~/services/project-service/project-service";
-import { ServiceRepository } from "~/services/services";
-import { sidePanelActions } from "../side-panel/side-panel-action";
-import { SidePanelId } from "../side-panel/side-panel-reducer";
-import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
+import { getProjectUrl } from "~/models/project";
import { getResource } from '~/store/resources/resources';
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { loadCollection } from '~/store/collection-panel/collection-panel-action';
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
+import { loadCollectionPanel } from '~/store/collection-panel/collection-panel-action';
import { snackbarActions } from '../snackbar/snackbar-actions';
import { resourceLabel } from "~/common/labels";
+import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action';
+import { openProjectPanel, projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
+import { Routes } from '~/routes/routes';
+import { loadResource } from '../resources/resources-actions';
+import { ServiceRepository } from '~/services/services';
+import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
+import { projectPanelColumns } from '~/views/project-panel/project-panel';
+import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
+import { matchRootRoute } from '~/routes/routes';
-export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
- switch (resourceKind) {
- case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
- case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
+export const navigateToResource = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const resource = getResource(uuid)(getState().resources);
+ if (resource) {
+ dispatch<any>(getResourceNavigationAction(resource));
+ }
+ };
+
+const getResourceNavigationAction = (resource: Resource) => {
+ switch (resource.kind) {
+ case ResourceKind.COLLECTION:
+ return navigateToCollection(resource.uuid);
+ case ResourceKind.PROJECT:
+ case ResourceKind.USER:
+ return navigateToProject(resource.uuid);
default:
- return '';
+ return cannotNavigateToResource(resource);
}
};
-export enum ItemMode {
- BOTH,
- OPEN,
- ACTIVE
-}
-
-export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { projects, router } = getState();
- const treeItem = findTreeItem(projects.items, itemId);
-
- if (treeItem) {
- const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
-
- if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- if (router.location && !router.location.pathname.includes(resourceUrl)) {
- dispatch(push(resourceUrl));
+export const loadWorkbench = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { auth, router } = getState();
+ const { user } = auth;
+ if (user) {
+ const userResource = await dispatch<any>(loadResource(user.uuid));
+ if (userResource) {
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+ dispatch<any>(initSidePanelTree());
+ if (router.location) {
+ const match = matchRootRoute(router.location.pathname);
+ if (match) {
+ dispatch(navigateToProject(userResource.uuid));
+ }
}
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+ } else {
+ dispatch(userIsNotAuthenticated);
}
-
- const promise = treeItem.status === TreeItemStatus.LOADED
- ? Promise.resolve()
- : dispatch<any>(getProjectList(itemId));
-
- promise
- .then(() => dispatch<any>(() => {
- if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
- }
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }));
} else {
- const uuid = services.authService.getUuid();
- if (itemId === uuid) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }
+ dispatch(userIsNotAuthenticated);
}
};
-export const restoreBranch = (itemId: string) =>
+export const navigateToFavorites = push(Routes.FAVORITES);
+
+export const loadFavorites = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const ancestors = await loadProjectAncestors(itemId, services.projectService);
- const uuids = ancestors.map(ancestor => ancestor.uuid);
- await loadBranch(uuids, dispatch);
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
- uuids.forEach(uuid => {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
- });
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+ dispatch<any>(loadFavoritePanel());
};
-export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
- if (getUuidObjectType(uuid) === ObjectTypes.USER) {
- return [];
- } else {
- const currentProject = await projectService.get(uuid);
- const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
- return [...ancestors, currentProject];
- }
-};
-const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
- const [uuid, ...rest] = uuids;
- if (uuid) {
- await dispatch<any>(getProjectList(uuid));
- return loadBranch(rest, dispatch);
- }
-};
-
-export const navigateToResource = (uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- const resource = getResource(uuid)(getState().resources);
- resource
- ? dispatch<any>(getResourceNavigationAction(resource))
- : dispatch<any>(resourceIsNotLoaded(uuid));
- };
+export const navigateToProject = compose(push, getProjectUrl);
-const getResourceNavigationAction = (resource: Resource) => {
- switch (resource.kind) {
- case ResourceKind.COLLECTION:
- return navigateToCollection(resource);
- case ResourceKind.PROJECT:
- return navigateToProject(resource);
- default:
- return cannotNavigateToResource(resource);
- }
-};
-
-export const navigateToProject = ({ uuid }: Resource) =>
+export const loadProject = (uuid: string) =>
(dispatch: Dispatch) => {
- dispatch<any>(setProjectItem(uuid, ItemMode.BOTH));
+ dispatch<any>(activateSidePanelTreeItem(uuid));
+ dispatch<any>(openProjectPanel(uuid));
dispatch(loadDetailsPanel(uuid));
};
-export const navigateToCollection = ({ uuid }: Resource) =>
- (dispatch: Dispatch) => {
- dispatch<any>(loadCollection(uuid));
- dispatch(push(getCollectionUrl(uuid)));
+export const navigateToCollection = compose(push, getCollectionUrl);
+
+export const loadCollection = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(loadCollectionPanel(uuid));
+ dispatch<any>(activateSidePanelTreeItem(collection.ownerUuid));
+ dispatch(loadDetailsPanel(uuid));
};
export const cannotNavigateToResource = ({ kind, uuid }: Resource) =>
snackbarActions.OPEN_SNACKBAR({
- message: `${resourceLabel(kind)} identified by ${uuid} cannot be opened.`,
- hideDuration: 3000
+ message: `${resourceLabel(kind)} identified by ${uuid} cannot be opened.`
});
-
export const resourceIsNotLoaded = (uuid: string) =>
snackbarActions.OPEN_SNACKBAR({
- message: `Resource identified by ${uuid} is not loaded.`,
- hideDuration: 3000
+ message: `Resource identified by ${uuid} is not loaded.`
});
+
+export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
+ message: 'User is not authenticated'
+});
+
+export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not load user'
+});
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
-
+import { propertiesActions } from "~/store/properties/properties-actions";
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { RootState } from '~/store/store';
+import { getProperty } from "~/store/properties/properties";
export const PROJECT_PANEL_ID = "projectPanel";
+export const PROJECT_PANEL_CURRENT_UUID = "projectPanelCurrentUuid";
export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
+
+export const openProjectPanel = (projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ };
+
+export const getProjectPanelCurrentUuid = (state: RootState) => getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { DataExplorerMiddlewareService, getDataExplorerColumnFilters, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '../data-explorer/data-explorer-middleware-service';
import { ProjectPanelColumnNames, ProjectPanelFilter } from "~/views/project-panel/project-panel";
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
import { SortDirection } from "~/components/data-table/data-column";
import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { projectPanelActions } from "./project-panel-action";
+import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
+import { updateFavorites } from "../favorites/favorites-actions";
+import { projectPanelActions, PROJECT_PANEL_CURRENT_UUID } from './project-panel-action';
import { Dispatch, MiddlewareAPI } from "redux";
import { ProjectResource } from "~/models/project";
-import { resourcesActions } from "~/store/resources/resources-actions";
+import { updateResources } from "~/store/resources/resources-actions";
+import { getProperty } from "~/store/properties/properties";
+import { snackbarActions } from '../snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
+import { ListResults } from '~/common/api/common-resource-service';
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
super(id);
}
- requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
const state = api.getState();
- const dataExplorer = state.dataExplorer[this.getId()];
- const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
- const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
- const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
- const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ const projectUuid = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+ if (!projectUuid) {
+ api.dispatch(projectPanelCurrentUuidIsNotSet());
+ } else if (!dataExplorer) {
+ api.dispatch(projectPanelDataExplorerIsNotSet());
+ } else {
+ try {
+ const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ api.dispatch(updateResources(response.items));
+ api.dispatch(setItems(response));
+ } catch (e) {
+ api.dispatch(couldNotFetchProjectContents());
+ }
+ }
+ }
+}
- const order = new OrderBuilder<ProjectResource>();
+const setItems = (listResults: ListResults<GroupContentsResource>) =>
+ projectPanelActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
- if (sortColumn) {
- const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer),
+ filters: getFilters(dataExplorer),
+});
- const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
- order
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
- }
+const getFilters = (dataExplorer: DataExplorer) => {
+ const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
+ const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+ const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+ return new FilterBuilder()
+ .addIsA("uuid", typeFilters.map(f => f.type))
+ .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .getFilters();
+};
+
+const getOrder = (dataExplorer: DataExplorer) => {
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const order = new OrderBuilder<ProjectResource>();
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
- this.services.groupsService
- .contents(state.projects.currentItemId, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: order.getOrder(),
- filters: new FilterBuilder()
- .addIsA("uuid", typeFilters.map(f => f.type))
- .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
- .getFilters()
- })
- .then(response => {
- api.dispatch(resourcesActions.SET_RESOURCES(response.items));
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: response.items.map(resource => resource.uuid),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- })
- .catch(() => {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- });
+ const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+ return order
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT)
+ .getOrder();
+ } else {
+ return order.getOrder();
}
-}
+};
+
+const projectPanelCurrentUuidIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Project panel is not opened.'
+ });
+
+const couldNotFetchProjectContents = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch project contents.'
+ });
+
+const projectPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Project panel is not ready.'
+ });
import { Dispatch } from "redux";
import { FilterBuilder } from "~/common/api/filter-builder";
import { RootState } from "../store";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { updateFavorites } from "../favorites/favorites-actions";
import { ServiceRepository } from "~/services/services";
import { projectPanelActions } from "~/store/project-panel/project-panel-action";
import { resourcesActions } from "~/store/resources/resources-actions";
.getFilters()
}).then(({ items: projects }) => {
dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
- dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
+ dispatch<any>(updateFavorites(projects.map(project => project.uuid)));
return projects;
});
};
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { Resource } from '~/models/resource';
+import { Resource, extractUuidKind } from '~/models/resource';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { getResourceService } from '~/services/services';
export const resourcesActions = unionize({
SET_RESOURCES: ofType<Resource[]>(),
DELETE_RESOURCES: ofType<string[]>()
});
-export type ResourcesAction = UnionOf<typeof resourcesActions>;
\ No newline at end of file
+export type ResourcesAction = UnionOf<typeof resourcesActions>;
+
+export const updateResources = (resources: Resource[]) => resourcesActions.SET_RESOURCES(resources);
+
+export const loadResource = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const kind = extractUuidKind(uuid);
+ const service = getResourceService(kind)(services);
+ if (service) {
+ const resource = await service.get(uuid);
+ dispatch<any>(updateResources([resource]));
+ return resource;
+ }
+ return undefined;
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
+import { createTreePickerNode } from '~/store/tree-picker/tree-picker';
+import { RootState } from '../store';
+import { ServiceRepository } from '~/services/services';
+import { FilterBuilder } from '~/common/api/filter-builder';
+import { resourcesActions } from '../resources/resources-actions';
+import { getNodeValue } from '../../models/tree';
+import { getTreePicker } from '../tree-picker/tree-picker';
+import { TreeItemStatus } from "~/components/tree/tree";
+
+export enum SidePanelTreeCategory {
+ PROJECTS = 'Projects',
+ SHARED_WITH_ME = 'Shared with me',
+ WORKFLOWS = 'Workflows',
+ RECENT_OPEN = 'Recent open',
+ FAVORITES = 'Favorites',
+ TRASH = 'Trash'
+}
+
+export const SIDE_PANEL_TREE = 'sidePanelTree';
+
+const SIDE_PANEL_CATEGORIES = [
+ SidePanelTreeCategory.SHARED_WITH_ME,
+ SidePanelTreeCategory.WORKFLOWS,
+ SidePanelTreeCategory.RECENT_OPEN,
+ SidePanelTreeCategory.FAVORITES,
+ SidePanelTreeCategory.TRASH,
+];
+
+export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
+
+export const initSidePanelTree = () =>
+ (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
+ const rootProjectUuid = authService.getUuid() || '';
+ const nodes = SIDE_PANEL_CATEGORIES.map(nodeId => createTreePickerNode({ nodeId, value: nodeId }));
+ const projectsNode = createTreePickerNode({ nodeId: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: '',
+ pickerId: SIDE_PANEL_TREE,
+ nodes: [projectsNode, ...nodes]
+ }));
+ SIDE_PANEL_CATEGORIES.forEach(category => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: category,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: []
+ }));
+ });
+ };
+
+export const loadSidePanelTreeProjects = (projectUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: projectUuid, pickerId: SIDE_PANEL_TREE }));
+ const params = {
+ filters: new FilterBuilder()
+ .addEqual('ownerUuid', projectUuid)
+ .getFilters()
+ };
+ const { items } = await services.projectService.list(params);
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: projectUuid,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: items.map(item => createTreePickerNode({ nodeId: item.uuid, value: item })),
+ }));
+ dispatch(resourcesActions.SET_RESOURCES(items));
+ };
+
+export const activateSidePanelTreeItem = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const node = getSidePanelTreeNode(nodeId)(getState());
+ if (node && !node.selected) {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ }
+ if (!isSidePanelTreeCategory(nodeId)) {
+ dispatch<any>(activateSidePanelTreeProject(nodeId));
+ }
+ };
+
+export const activateSidePanelTreeProject = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const node = getSidePanelTreeNode(nodeId)(getState());
+ if (node && node.status !== TreeItemStatus.LOADED) {
+ await dispatch<any>(loadSidePanelTreeProjects(nodeId));
+ if (node.collapsed) {
+ dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
+ }
+ } else if (node === undefined) {
+ dispatch<any>(activateSidePanelTreeBranch(nodeId));
+ }
+ };
+
+export const activateSidePanelTreeBranch = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const ancestors = await services.ancestorsService.ancestors(nodeId, services.authService.getUuid() || '');
+ for (const ancestor of ancestors) {
+ await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
+ }
+ for (const ancestor of ancestors) {
+ dispatch<any>(toggleSidePanelTreeItemCollapse(ancestor.uuid));
+ }
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ };
+
+const getSidePanelTreeNode = (nodeId: string) => (state: RootState) => {
+ const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(state.treePicker);
+ return sidePanelTree
+ ? getNodeValue(nodeId)(sidePanelTree)
+ : undefined;
+};
+
+export const toggleSidePanelTreeItemCollapse = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ };
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
-import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+import { Dispatch } from 'redux';
+import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { navigateToFavorites, navigateToResource } from '../navigation/navigation-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-export const sidePanelActions = unionize({
- TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
-}, {
- tag: 'type',
- value: 'payload'
-});
+export const navigateFromSidePanel = (id: string) =>
+ (dispatch: Dispatch) => {
+ if (isSidePanelTreeCategory(id)) {
+ dispatch<any>(getSidePanelTreeCategoryAction(id));
+ } else {
+ dispatch<any>(navigateToResource(id));
+ }
+ };
-export type SidePanelAction = UnionOf<typeof sidePanelActions>;
+const getSidePanelTreeCategoryAction = (id: string) => {
+ switch (id) {
+ case SidePanelTreeCategory.FAVORITES:
+ return navigateToFavorites;
+ default:
+ return sidePanelTreeCategoryNotAvailable(id);
+ }
+};
+
+const sidePanelTreeCategoryNotAvailable = (id: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `${id} not available`,
+ hideDuration: 3000,
+ });
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelReducer } from "./side-panel-reducer";
-import { sidePanelActions } from "./side-panel-action";
-import { ProjectsIcon } from "~/components/icon/icon";
-
-describe('side-panel-reducer', () => {
- it('should open side-panel item', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- url: "/projects",
- icon: ProjectsIcon,
- open: false
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: true,
- url: "/projects"
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
- expect(state).toEqual(project);
- });
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelActions, SidePanelAction } from './side-panel-action';
-import { SidePanelItem } from '~/components/side-panel/side-panel';
-import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "~/components/icon/icon";
-import { Dispatch } from "redux";
-import { push } from "react-router-redux";
-import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
-import { projectPanelActions } from "../project-panel/project-panel-action";
-import { projectActions } from "../project/project-action";
-import { getProjectUrl } from "../../models/project";
-import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
-import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
-
-export type SidePanelState = SidePanelItem[];
-
-export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
- return sidePanelActions.match(action, {
- TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
- state.map(it => ({...it, open: itemId === it.id && it.open === false})),
- default: () => state
- });
-};
-
-export enum SidePanelId {
- PROJECTS = "Projects",
- SHARED_WITH_ME = "SharedWithMe",
- WORKFLOWS = "Workflows",
- RECENT_OPEN = "RecentOpen",
- FAVORITES = "Favourites",
- TRASH = "Trash"
-}
-
-export const sidePanelItems = [
- {
- id: SidePanelId.PROJECTS,
- name: "Projects",
- url: "/projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- margin: true,
- openAble: true,
- activeAction: (dispatch: Dispatch, uuid: string) => {
- dispatch(push(getProjectUrl(uuid)));
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
- dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }
- },
- {
- id: SidePanelId.SHARED_WITH_ME,
- name: "Shared with me",
- url: "/shared",
- icon: ShareMeIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/shared"));
- }
- },
- {
- id: SidePanelId.WORKFLOWS,
- name: "Workflows",
- url: "/workflows",
- icon: WorkflowIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/workflows"));
- }
- },
- {
- id: SidePanelId.RECENT_OPEN,
- name: "Recent open",
- url: "/recent",
- icon: RecentIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/recent"));
- }
- },
- {
- id: SidePanelId.FAVORITES,
- name: "Favorites",
- url: "/favorites",
- icon: FavoriteIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/favorites"));
- dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
- dispatch(favoritePanelActions.RESET_PAGINATION());
- dispatch(favoritePanelActions.REQUEST_ITEMS());
- }
- },
- {
- id: SidePanelId.TRASH,
- name: "Trash",
- url: "/trash",
- icon: TrashIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/trash"));
- }
- }
-];
import { Tree } from "~/models/tree";
import { TreeItemStatus } from "~/components/tree/tree";
+import { RootState } from '~/store/store';
export type TreePicker = { [key: string]: Tree<TreePickerNode> };
collapsed: true,
status: TreeItemStatus.INITIAL
});
+
+export const getTreePicker = (id: string) => (state: TreePicker): Tree<TreePickerNode> | undefined => state[id];
\ No newline at end of file
import { getProjectList } from "~/store/project/project-action";
import { getUrlParameter } from "~/common/url";
import { AuthService } from "~/services/auth-service/auth-service";
+import { loadWorkbench } from '../../store/navigation/navigation-action';
interface ApiTokenProps {
authService: AuthService;
this.props.dispatch(saveApiToken(apiToken));
this.props.dispatch<any>(getUserDetails()).then(() => {
const rootUuid = this.props.authService.getRootUuid();
- this.props.dispatch(getProjectList(rootUuid));
+ this.props.dispatch(loadWorkbench());
});
}
render() {
interface Props {
id: string;
- columns: DataColumns<any>;
onRowClick: (item: any) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
onRowDoubleClick: (item: any) => void;
extractKey?: (item: any) => React.Key;
}
-const mapStateToProps = (state: RootState, { id, columns }: Props) => {
- const s = getDataExplorer(state.dataExplorer, id);
- if (s.columns.length === 0) {
- s.columns = columns;
- }
- return s;
+const mapStateToProps = (state: RootState, { id }: Props) => {
+ return getDataExplorer(state.dataExplorer, id);
};
const mapDispatchToProps = () => {
- return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+ return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
onSetColumns: (columns: DataColumns<any>) => {
dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
},
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
-import { connect } from "react-redux";
-import { ProjectTree } from '~/views-components/project-tree/project-tree';
-import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { RootState } from '~/store/store';
-import { TreeItem } from '~/components/tree/tree';
-import { ProjectResource } from '~/models/project';
-import { sidePanelActions } from '../../store/side-panel/side-panel-action';
-import { Dispatch } from 'redux';
-import { projectActions } from '~/store/project/project-action';
-import { navigateToResource } from '../../store/navigation/navigation-action';
-import { openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-
-
-const DRAWER_WITDH = 240;
-
-type CssRules = 'drawerPaper' | 'toolbar';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- drawerPaper: {
- position: 'relative',
- width: DRAWER_WITDH,
- display: 'flex',
- flexDirection: 'column',
- },
- toolbar: theme.mixins.toolbar
-});
-
-interface NavigationPanelDataProps {
- projects: Array<TreeItem<ProjectResource>>;
- sidePanelItems: SidePanelItem[];
-}
-
-interface NavigationPanelActionProps {
- toggleSidePanelOpen: (panelItemId: string) => void;
- toggleSidePanelActive: (panelItemId: string) => void;
- toggleProjectOpen: (projectUuid: string) => void;
- toggleProjectActive: (projectUuid: string) => void;
- openRootContextMenu: (event: React.MouseEvent<any>) => void;
- openProjectContextMenu: (event: React.MouseEvent<any>, item: TreeItem<ProjectResource>) => void;
-}
-
-type NavigationPanelProps = NavigationPanelDataProps & NavigationPanelActionProps & WithStyles<CssRules>;
-
-const mapStateToProps = (state: RootState): NavigationPanelDataProps => ({
- projects: state.projects.items,
- sidePanelItems: state.sidePanel
-});
-
-const mapDispatchToProps = (dispatch: Dispatch): NavigationPanelActionProps => ({
- toggleSidePanelOpen: panelItemId => {
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(panelItemId));
- },
- toggleSidePanelActive: panelItemId => {
- dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(panelItemId));
-
- // const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
- // if (panelItem && panelItem.activeAction) {
- // panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
- // }
- },
- toggleProjectOpen: projectUuid => {
- dispatch<any>(navigateToResource(projectUuid));
- },
- toggleProjectActive: projectUuid => {
- dispatch<any>(navigateToResource(projectUuid));
- },
- openRootContextMenu: event => {
- dispatch<any>(openContextMenu(event, {
- uuid: "",
- name: "",
- kind: ContextMenuKind.ROOT_PROJECT
- }));
- },
- openProjectContextMenu: (event, item) => {
- dispatch<any>(openContextMenu(event, {
- uuid: item.data.uuid,
- name: item.data.name,
- kind: ContextMenuKind.PROJECT
- }));
- }
-});
-
-export const NavigationPanel = withStyles(styles)(
- connect(mapStateToProps, mapDispatchToProps)(
- ({ classes, sidePanelItems, projects, ...actions }: NavigationPanelProps) => <Drawer
- variant="permanent"
- classes={{ paper: classes.drawerPaper }}>
- <div className={classes.toolbar} />
- <SidePanel
- toggleOpen={actions.toggleSidePanelOpen}
- toggleActive={actions.toggleSidePanelOpen}
- sidePanelItems={sidePanelItems}
- onContextMenu={actions.openRootContextMenu}>
- <ProjectTree
- projects={projects}
- toggleOpen={actions.toggleProjectOpen}
- onContextMenu={actions.openProjectContextMenu}
- toggleActive={actions.toggleProjectActive} />
- </SidePanel>
- </Drawer>
- )
-);
--- /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 { connect } from "react-redux";
+import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
+import { TreeItem } from "~/components/tree/tree";
+import { ProjectResource } from "~/models/project";
+import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon';
+import { RecentIcon, WorkflowIcon } from '~/components/icon/icon';
+import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+
+export interface SidePanelTreeProps {
+ onItemActivation: (id: string) => void;
+}
+
+type SidePanelTreeActionProps = Pick<TreePickerProps, 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({
+ toggleItemActive: (nodeId) => {
+ dispatch<any>(activateSidePanelTreeItem(nodeId));
+ props.onItemActivation(nodeId);
+ },
+ toggleItemOpen: (nodeId) => {
+ dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
+ }
+});
+
+export const SidePanelTree = connect(undefined, mapDispatchToProps)(
+ (props: SidePanelTreeActionProps) =>
+ <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
+
+const renderSidePanelItem = (item: TreeItem<ProjectResource>) =>
+ <ListItemTextIcon
+ icon={getProjectPickerIcon(item)}
+ name={typeof item.data === 'string' ? item.data : item.data.name}
+ isActive={item.active}
+ hasMargin={true} />;
+
+const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
+ typeof item.data === 'string'
+ ? getSidePanelIcon(item.data)
+ : ProjectIcon;
+
+const getSidePanelIcon = (category: string) => {
+ switch (category) {
+ case SidePanelTreeCategory.FAVORITES:
+ return FavoriteIcon;
+ case SidePanelTreeCategory.PROJECTS:
+ return ProjectsIcon;
+ case SidePanelTreeCategory.RECENT_OPEN:
+ return RecentIcon;
+ case SidePanelTreeCategory.SHARED_WITH_ME:
+ return ShareMeIcon;
+ case SidePanelTreeCategory.TRASH:
+ return TrashIcon;
+ case SidePanelTreeCategory.WORKFLOWS:
+ return WorkflowIcon;
+ default:
+ return ProjectIcon;
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import Drawer from '@material-ui/core/Drawer';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
+import { compose, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+
+const DRAWER_WITDH = 240;
+
+type CssRules = 'drawerPaper' | 'toolbar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ drawerPaper: {
+ position: 'relative',
+ width: DRAWER_WITDH,
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: 58,
+ overflow: 'auto',
+ },
+ toolbar: theme.mixins.toolbar
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
+ onItemActivation: id => {
+ dispatch<any>(navigateFromSidePanel(id));
+ }
+});
+
+export const SidePanel = compose(
+ withStyles(styles),
+ connect(undefined, mapDispatchToProps)
+)(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
+ <Drawer
+ variant="permanent"
+ classes={{ paper: classes.drawerPaper }}>
+ <div className={classes.toolbar} />
+ <SidePanelTree {...props} />
+ </Drawer>);
import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { getResource } from '~/store/resources/resources';
-import { loadCollection } from '../../store/collection-panel/collection-panel-action';
import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
}));
}
- componentDidMount() {
- const { match, item } = this.props;
- if (!item && match.params.id) {
- this.props.dispatch<any>(loadCollection(match.params.id));
- }
- }
-
}
)
);
type: ResourceKind | ContainerRequestState;
}
-export const columns: DataColumns<string, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
{
name: FavoritePanelColumnNames.NAME,
selected: true,
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
onDialogOpen: (ownerUuid: string) => void;
onItemDoubleClick: (item: string) => void;
- onMount: () => void;
}
const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
},
onItemDoubleClick: uuid => {
dispatch<any>(navigateToResource(uuid));
- },
- onMount: () => {
- dispatch(loadFavoritePanel());
- },
+ }
});
type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
render() {
return <DataExplorer
id={FAVORITE_PANEL_ID}
- columns={columns}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
defaultIcon={FavoriteIcon}
defaultMessages={['Your favorites list is empty.']} />;
}
-
- componentDidMount() {
- this.props.onMount();
- }
}
)
);
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
-import { restoreBranch, setProjectItem, ItemMode } from '~/store/navigation/navigation-action';
import { ProjectIcon } from '~/components/icon/icon';
import { ResourceName } from '~/views-components/data-explorer/renderers';
import { ResourcesState, getResource } from '~/store/resources/resources';
import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
import { navigateToResource } from '~/store/navigation/navigation-action';
+import { getProperty } from '~/store/properties/properties';
+import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
type CssRules = 'root' | "toolbar" | "button";
type: ResourceKind | ContainerRequestState;
}
-export const columns: DataColumns<string, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
{
name: ProjectPanelColumnNames.NAME,
selected: true,
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const ProjectPanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId, resources: state.resources }))(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ resources: state.resources
+ }))(
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
</div>
<DataExplorer
id={PROJECT_PANEL_ID}
- columns={columns}
onRowClick={this.handleRowClick}
onRowDoubleClick={this.handleRowDoubleClick}
onContextMenu={this.handleContextMenu}
this.props.dispatch(loadDetailsPanel(uuid));
}
- async componentDidMount() {
- if (this.props.match.params.id && this.props.currentItemId === '') {
- await this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
- this.props.dispatch<any>(setProjectItem(this.props.match.params.id, ItemMode.BOTH));
- }
- }
}
)
);
import { push } from 'react-router-redux';
import { TreeItem } from "~/components/tree/tree";
import { getTreePath } from '~/store/project/project-reducer';
-import { ItemMode, setProjectItem } from "~/store/navigation/navigation-action";
import { ProjectPanel } from "~/views/project-panel/project-panel";
import { DetailsPanel } from '~/views-components/details-panel/details-panel';
import { ArvadosTheme } from '~/common/custom-theme';
import { CreateProjectDialog } from "~/views-components/create-project-dialog/create-project-dialog";
import { detailsPanelActions, loadDetailsPanel } from "~/store/details-panel/details-panel-action";
import { openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ProjectResource } from '~/models/project';
+import { ProjectResource, getProjectUrl } from '~/models/project';
import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
import { FavoritePanel } from "../favorite-panel/favorite-panel";
import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog';
import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog';
import { ProjectCopyDialog } from '~/views-components/project-copy-dialog/project-copy-dialog';
-import { CollectionPartialCopyDialog } from '../../views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
+import { CollectionPartialCopyDialog } from '~/views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
import { MoveProjectDialog } from '~/views-components/move-project-dialog/move-project-dialog';
import { MoveCollectionDialog } from '~/views-components/move-collection-dialog/move-collection-dialog';
-import { NavigationPanel } from '~/views-components/navigation-panel/navigation-panel';
+import { SidePanel } from '~/views-components/side-panel/side-panel';
+import { Routes } from '~/routes/routes';
+import { navigateToResource } from '../../store/navigation/navigation-action';
const APP_BAR_HEIGHT = 100;
status: item.status
}));
+ const rootProjectUuid = this.props.authService.getUuid();
+
const { classes, user } = this.props;
return (
<div className={classes.root}>
buildInfo={this.props.buildInfo}
{...this.mainAppBarActions} />
</div>
- {user && <NavigationPanel />}
+ {user && <SidePanel />}
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
- <Route path="/projects/:id" component={ProjectPanel} />
- <Route path="/favorites" component={FavoritePanel} />
- <Route path="/collections/:id" component={CollectionPanel} />
+ <Route path={Routes.PROJECTS} component={ProjectPanel} />
+ <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+ <Route path={Routes.FAVORITES} component={FavoritePanel} />
</Switch>
</div>
{user && <DetailsPanel />}
mainAppBarActions: MainAppBarActionProps = {
onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch(loadDetailsPanel(itemId));
+ this.props.dispatch(navigateToResource(itemId));
},
onSearch: searchText => {
this.setState({ searchText });