import { ProjectService } from "./project-service/project-service";
import { LinkService } from "./link-service/link-service";
import { FavoriteService } from "./favorite-service/favorite-service";
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "../common/api/common-resource-service";
+import { CollectionResource } from "../models/collection";
+import { Resource } from "../models/resource";
+ import { CollectionService } from "./collection-service/collection-service";
+import Axios from "axios";
-export const authService = new AuthService(authClient, apiClient);
-export const groupsService = new GroupsService(apiClient);
-export const projectService = new ProjectService(apiClient);
-export const collectionService = new CollectionService(apiClient);
-export const linkService = new LinkService(apiClient);
-export const favoriteService = new FavoriteService(linkService, groupsService);
+export interface ServiceRepository {
+ apiClient: AxiosInstance;
+
+ authService: AuthService;
+ groupsService: GroupsService;
+ projectService: ProjectService;
+ linkService: LinkService;
+ favoriteService: FavoriteService;
+ collectionService: CommonResourceService<Resource>;
+}
+
+export const createServices = (baseUrl: string): ServiceRepository => {
+ const apiClient = Axios.create();
+ apiClient.defaults.baseURL = `${baseUrl}/arvados/v1`;
+
+ const authService = new AuthService(apiClient, baseUrl);
+ const groupsService = new GroupsService(apiClient);
+ const projectService = new ProjectService(apiClient);
+ const linkService = new LinkService(apiClient);
+ const favoriteService = new FavoriteService(linkService, groupsService);
- const collectionService = new CommonResourceService<CollectionResource>(apiClient, "collections");
++ const collectionService = new CollectionService(apiClient);
+
+ return {
+ apiClient,
+ authService,
+ groupsService,
+ projectService,
+ linkService,
+ favoriteService,
+ collectionService
+ };
+};
--- /dev/null
-import { apiClient } from "../../common/api/server-api";
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+
+ import { unionize, ofType, UnionOf } from "unionize";
+ import { CommonResourceService } from "../../common/api/common-resource-service";
- (dispatch: Dispatch) => {
+ import { Dispatch } from "redux";
+ import { ResourceKind } from "../../models/resource";
+ import { CollectionResource } from "../../models/collection";
++import { RootState } from "../store";
++import { ServiceRepository } from "../../services/services";
+
+ export const collectionPanelActions = unionize({
+ LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
+ LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
+ }, { tag: 'type', value: 'payload' });
+
+ export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
+
+ export const loadCollection = (uuid: string, kind: ResourceKind) =>
- return new CommonResourceService(apiClient, "collections")
++ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
++ return new CommonResourceService(services.apiClient, "collections")
+ .get(uuid)
+ .then(item => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: item as CollectionResource }));
+ });
+ };
+
+
+
--- /dev/null
-import { collectionService } from '../../../services/services';
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+
+ import { default as unionize, ofType, UnionOf } from "unionize";
+ import { Dispatch } from "redux";
+
+ import { RootState } from "../../store";
- (dispatch: Dispatch, getState: () => RootState) => {
+ import { CollectionResource } from '../../../models/collection';
++import { ServiceRepository } from "../../../services/services";
+
+ export const collectionCreateActions = unionize({
+ OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
+ CLOSE_COLLECTION_CREATOR: ofType<{}>(),
+ CREATE_COLLECTION: ofType<{}>(),
+ CREATE_COLLECTION_SUCCESS: ofType<{}>(),
+ }, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+ export const createCollection = (collection: Partial<CollectionResource>) =>
- return collectionService
++ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { ownerUuid } = getState().collectionCreation.creator;
+ const collectiontData = { ownerUuid, ...collection };
+ dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
-export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
++ return services.collectionService
+ .create(collectiontData)
+ .then(collection => dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection)));
+ };
+
++export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
--- /dev/null
-import { favoriteService, authService } from "../../services/services";
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+
+ import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+ import { FavoritePanelFilter, FavoritePanelColumnNames } from "../../views/favorite-panel/favorite-panel";
+ import { RootState } from "../store";
+ import { DataColumns } from "../../components/data-table/data-table";
+ import { FavoritePanelItem, resourceToDataItem } from "../../views/favorite-panel/favorite-panel-item";
+ import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
- constructor(id: string) {
++import { ServiceRepository } from "../../services/services";
+ import { SortDirection } from "../../components/data-table/data-column";
+ import { FilterBuilder } from "../../common/api/filter-builder";
+ import { LinkResource } from "../../models/link";
+ import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+ import { favoritePanelActions } from "./favorite-panel-action";
+ import { Dispatch, MiddlewareAPI } from "redux";
+
+ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
- favoriteService
- .list(authService.getUuid()!, {
++ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const dataExplorer = api.getState().dataExplorer[this.getId()];
+ const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
+ const sortColumn = dataExplorer.columns.find(
+ ({ sortDirection }) => sortDirection !== undefined && sortDirection !== "none"
+ );
+ const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const order = FavoriteOrderBuilder.create();
+ if (typeFilters.length > 0) {
++ this.services.favoriteService
++ .list(this.services.authService.getUuid()!, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ order: sortColumn!.name === FavoritePanelColumnNames.NAME
+ ? sortColumn!.sortDirection === SortDirection.ASC
+ ? order.addDesc("name")
+ : order.addAsc("name")
+ : order,
+ filters: FilterBuilder
+ .create<LinkResource>()
+ .addIsA("headUuid", typeFilters.map(filter => filter.type))
+ .addILike("name", dataExplorer.searchValue)
+ })
+ .then(response => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ });
+ } else {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ }
+ }
+ }
+
+ const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
+ const column = columns.find(c => c.name === columnName);
+ return column && column.filters ? column.filters.filter(f => f.selected) : [];
+ };
--- /dev/null
-import { groupsService } from "../../services/services";
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+
+ import { DataExplorerMiddlewareService } 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";
- constructor(id: string) {
++import { ServiceRepository } from "../../services/services";
+ import { ProjectPanelItem, resourceToDataItem } from "../../views/project-panel/project-panel-item";
+ import { SortDirection } from "../../components/data-table/data-column";
+ import { OrderBuilder } from "../../common/api/order-builder";
+ import { FilterBuilder } from "../../common/api/filter-builder";
+ import { ProcessResource } from "../../models/process";
+ import { GroupContentsResourcePrefix, GroupContentsResource } from "../../services/groups-service/groups-service";
+ import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+ import { projectPanelActions } from "./project-panel-action";
+ import { Dispatch, MiddlewareAPI } from "redux";
+
+ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
- groupsService
++ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const state = api.getState();
+ const dataExplorer = state.dataExplorer[this.getId()];
+ const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
+ const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+ const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+ const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
+ if (typeFilters.length > 0) {
++ this.services.groupsService
+ .contents(state.projects.currentItemId, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ order: sortColumn
+ ? sortColumn.name === ProjectPanelColumnNames.NAME
+ ? getOrder("name", sortDirection)
+ : getOrder("createdAt", sortDirection)
+ : OrderBuilder.create(),
+ filters: FilterBuilder
+ .create()
+ .concat(FilterBuilder
+ .create()
+ .addIsA("uuid", typeFilters.map(f => f.type)))
+ .concat(FilterBuilder
+ .create<ProcessResource>(GroupContentsResourcePrefix.PROCESS)
+ .addIn("state", statusFilters.map(f => f.type)))
+ .concat(getSearchFilter(dataExplorer.searchValue))
+ })
+ .then(response => {
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ });
+ } else {
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ }
+ }
+ }
+
+ const getColumnFilters = (columns: DataColumns<ProjectPanelItem, ProjectPanelFilter>, columnName: string) => {
+ const column = columns.find(c => c.name === columnName);
+ return column && column.filters ? column.filters.filter(f => f.selected) : [];
+ };
+
+ const getOrder = (attribute: "name" | "createdAt", direction: SortDirection) =>
+ [
+ OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
+ OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
+ OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)
+ ].reduce((acc, b) =>
+ acc.concat(direction === SortDirection.ASC
+ ? b.addAsc(attribute)
+ : b.addDesc(attribute)), OrderBuilder.create());
+
+ const getSearchFilter = (searchValue: string) =>
+ searchValue
+ ? [
+ FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
+ FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
+ FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)]
+ .reduce((acc, b) =>
+ acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
+ : FilterBuilder.create();
import { reducer as formReducer } from 'redux-form';
import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
+ import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
+ import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
+ import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
+ import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
+ import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
+ import { CollectionCreatorState, collectionCreationReducer } from './collections/creator/collection-creator-reducer';
+ import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { ServiceRepository } from "../services/services";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
snackbar: SnackbarState;
}
-const rootReducer = combineReducers({
- auth: authReducer,
- projects: projectsReducer,
- collectionCreation: collectionCreationReducer,
- router: routerReducer,
- dataExplorer: dataExplorerReducer,
- sidePanel: sidePanelReducer,
- collectionPanel: collectionPanelReducer,
- detailsPanel: detailsPanelReducer,
- contextMenu: contextMenuReducer,
- form: formReducer,
- favorites: favoritesReducer,
- snackbar: snackbarReducer,
-});
+export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
+
+export function configureStore(history: History, services: ServiceRepository): RootStore {
- const rootReducer = combineReducers({
- auth: authReducer(services),
- projects: projectsReducer,
- router: routerReducer,
- dataExplorer: dataExplorerReducer,
- sidePanel: sidePanelReducer,
- detailsPanel: detailsPanelReducer,
- contextMenu: contextMenuReducer,
- form: formReducer,
- favorites: favoritesReducer,
- snackbar: snackbarReducer,
- });
++ const rootReducer = combineReducers({
++ auth: authReducer(services),
++ projects: projectsReducer,
++ collectionCreation: collectionCreationReducer,
++ router: routerReducer,
++ dataExplorer: dataExplorerReducer,
++ sidePanel: sidePanelReducer,
++ collectionPanel: collectionPanelReducer,
++ detailsPanel: detailsPanelReducer,
++ contextMenu: contextMenuReducer,
++ form: formReducer,
++ favorites: favoritesReducer,
++ snackbar: snackbarReducer,
++ });
+
-export function configureStore(history: History) {
+ const projectPanelMiddleware = dataExplorerMiddleware(
- new ProjectPanelMiddlewareService(PROJECT_PANEL_ID)
++ new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
+ );
+ const favoritePanelMiddleware = dataExplorerMiddleware(
- new FavoritePanelMiddlewareService(FAVORITE_PANEL_ID)
++ new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
- thunkMiddleware,
+ thunkMiddleware.withExtraArgument(services),
- projectPanelMiddleware(services),
- favoritePanelMiddleware(services)
+ projectPanelMiddleware,
+ favoritePanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog';
import { dataExplorerActions } from '../../store/data-explorer/data-explorer-action';
import { Snackbar } from '../../views-components/snackbar/snackbar';
+ import { favoritePanelActions } from '../../store/favorite-panel/favorite-panel-action';
+ import { CreateCollectionDialog } from '../../views-components/create-collection-dialog/create-collection-dialog';
+ import { CollectionPanel } from '../collection-panel/collection-panel';
+ import { loadCollection } from '../../store/collection-panel/collection-panel-action';
+ import { getCollectionUrl } from '../../models/collection';
+import { AuthService } from "../../services/auth-service/auth-service";
-const drawerWidth = 240;
-const appBarHeight = 100;
+const DRAWER_WITDH = 240;
+const APP_BAR_HEIGHT = 100;
type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
);
}
+ renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
+ onItemRouteChange={(collectionId) => this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION))}
+ onContextMenu={(event, item) => {
+ this.openContextMenu(event, {
+ uuid: item.uuid,
+ name: item.name,
+ kind: ContextMenuKind.COLLECTION
+ });
+ }}
+ {...props} />
+
renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
- onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
+ onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
onContextMenu={(event, item) => {
+
const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
this.openContextMenu(event, {
uuid: item.uuid,
kind
});
}}
- onDialogOpen={this.handleCreationDialogOpen}
+ onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
+ onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
onItemClick={item => {
- this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
}}
onItemDoubleClick={item => {
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
+ switch (item.kind) {
+ case ResourceKind.COLLECTION:
- this.props.dispatch<any>(loadCollection(item.uuid, item.kind as ResourceKind));
++ this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(push(getCollectionUrl(item.uuid)));
+ default:
- this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
++ this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
++ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
+ }
+
}}
{...props} />
renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
- onItemRouteChange={() => this.props.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }))}
- onItemRouteChange={() => this.props.dispatch<any>(favoritePanelActions.REQUEST_ITEMS())}
++ onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
onContextMenu={(event, item) => {
const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
this.openContextMenu(event, {
kind,
});
}}
- onDialogOpen={this.handleCreationDialogOpen}
+ onDialogOpen={this.handleProjectCreationDialogOpen}
onItemClick={item => {
- this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
}}
onItemDoubleClick={item => {
- this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ switch (item.kind) {
+ case ResourceKind.COLLECTION:
- this.props.dispatch<any>(loadCollection(item.uuid, item.kind as ResourceKind));
++ this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(push(getCollectionUrl(item.uuid)));
+ default:
- this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.PROJECT));
- this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
++ this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
++ this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
++ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ }
+
}}
{...props} />