[ARVADOS-WORKBENCH2] created: 1.1.4-437-gf239b9e

Git user git at public.curoverse.com
Sun Jul 29 18:47:49 EDT 2018


        at  f239b9e88677d82a48b6b565ab2fd407d1171729 (commit)


commit f239b9e88677d82a48b6b565ab2fd407d1171729
Author: Daniel Kos <daniel.kos at contractors.roche.com>
Date:   Mon Jul 30 00:47:41 2018 +0200

    Introduce service repository
    
    Feature #13901
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos at contractors.roche.com>

diff --git a/src/index.tsx b/src/index.tsx
index 77d5763..4b2e335 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -13,7 +13,7 @@ import { configureStore } from "./store/store";
 import { ConnectedRouter } from "react-router-redux";
 import { ApiToken } from "./views-components/api-token/api-token";
 import { authActions } from "./store/auth/auth-action";
-import { authService } from "./services/services";
+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';
@@ -36,10 +36,13 @@ fetchConfig()
         setBaseUrl(config.API_HOST);
 
         const history = createBrowserHistory();
-        const store = configureStore(history);
+        const services = createServices();
+        const store = configureStore(history, services);
 
         store.dispatch(authActions.INIT());
-        store.dispatch<any>(getProjectList(authService.getUuid()));
+        store.dispatch<any>(getProjectList(services.authService.getUuid()));
+
+        const Token = (props: any) => <ApiToken authService={services.authService} {...props}/>;
 
         const App = () =>
             <MuiThemeProvider theme={CustomTheme}>
@@ -47,7 +50,7 @@ fetchConfig()
                     <ConnectedRouter history={history}>
                         <div>
                             <Route path="/" component={Workbench} />
-                            <Route path="/token" component={ApiToken} />
+                            <Route path="/token" component={Token} />
                         </div>
                     </ConnectedRouter>
                 </Provider>
diff --git a/src/services/services.ts b/src/services/services.ts
index a08ed3c..55ab836 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -9,8 +9,26 @@ import { ProjectService } from "./project-service/project-service";
 import { LinkService } from "./link-service/link-service";
 import { FavoriteService } from "./favorite-service/favorite-service";
 
-export const authService = new AuthService(authClient, apiClient);
-export const groupsService = new GroupsService(apiClient);
-export const projectService = new ProjectService(apiClient);
-export const linkService = new LinkService(apiClient);
-export const favoriteService = new FavoriteService(linkService, groupsService);
+export interface ServiceRepository {
+    authService: AuthService;
+    groupsService: GroupsService;
+    projectService: ProjectService;
+    linkService: LinkService;
+    favoriteService: FavoriteService;
+}
+
+export const createServices = (): ServiceRepository => {
+    const authService = new AuthService(authClient, apiClient);
+    const groupsService = new GroupsService(apiClient);
+    const projectService = new ProjectService(apiClient);
+    const linkService = new LinkService(apiClient);
+    const favoriteService = new FavoriteService(linkService, groupsService);
+
+    return {
+        authService,
+        groupsService,
+        projectService,
+        linkService,
+        favoriteService
+    };
+};
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index e9930a0..8b268cc 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -4,8 +4,9 @@
 
 import { ofType, default as unionize, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-import { authService } from "../../services/services";
 import { User } from "../../models/user";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
@@ -19,9 +20,9 @@ export const authActions = unionize({
     value: 'payload'
 });
 
-export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
+export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
     dispatch(authActions.USER_DETAILS_REQUEST());
-    return authService.getUserDetails().then(details => {
+    return services.authService.getUserDetails().then(details => {
         dispatch(authActions.USER_DETAILS_SUCCESS(details));
         return details;
     });
diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts
index 778b500..f686db5 100644
--- a/src/store/auth/auth-reducer.test.ts
+++ b/src/store/auth/auth-reducer.test.ts
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { authReducer } from "./auth-reducer";
-import { authActions } from "./auth-action";
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, authActions } from "./auth-action";
 import {
     API_TOKEN_KEY,
     USER_EMAIL_KEY,
@@ -14,15 +14,19 @@ import {
 } from "../../services/auth-service/auth-service";
 
 import 'jest-localstorage-mock';
+import { createServices } from "../../services/services";
 
 describe('auth-reducer', () => {
+    let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+
     beforeAll(() => {
         localStorage.clear();
+        reducer = authReducer(createServices());
     });
 
     it('should return default state on initialisation', () => {
         const initialState = undefined;
-        const state = authReducer(initialState, authActions.INIT());
+        const state = reducer(initialState, authActions.INIT());
         expect(state).toEqual({
             apiToken: undefined,
             user: undefined
@@ -39,7 +43,7 @@ describe('auth-reducer', () => {
         localStorage.setItem(USER_UUID_KEY, "uuid");
         localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
 
-        const state = authReducer(initialState, authActions.INIT());
+        const state = reducer(initialState, authActions.INIT());
         expect(state).toEqual({
             apiToken: "token",
             user: {
@@ -55,7 +59,7 @@ describe('auth-reducer', () => {
     it('should store token in local storage', () => {
         const initialState = undefined;
 
-        const state = authReducer(initialState, authActions.SAVE_API_TOKEN("token"));
+        const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
         expect(state).toEqual({
             apiToken: "token",
             user: undefined
@@ -75,7 +79,7 @@ describe('auth-reducer', () => {
             ownerUuid: "ownerUuid"
         };
 
-        const state = authReducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
+        const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
         expect(state).toEqual({
             apiToken: undefined,
             user: {
@@ -93,7 +97,7 @@ describe('auth-reducer', () => {
     it('should fire external url to login', () => {
         const initialState = undefined;
         window.location.assign = jest.fn();
-        authReducer(initialState, authActions.LOGIN());
+        reducer(initialState, authActions.LOGIN());
         expect(window.location.assign).toBeCalledWith(
             `/login?return_to=${window.location.protocol}//${window.location.host}/token`
         );
@@ -102,7 +106,7 @@ describe('auth-reducer', () => {
     it('should fire external url to logout', () => {
         const initialState = undefined;
         window.location.assign = jest.fn();
-        authReducer(initialState, authActions.LOGOUT());
+        reducer(initialState, authActions.LOGOUT());
         expect(window.location.assign).toBeCalledWith(
             `/logout?return_to=${location.protocol}//${location.host}`
         );
diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts
index 366385d..e3f968a 100644
--- a/src/store/auth/auth-reducer.ts
+++ b/src/store/auth/auth-reducer.ts
@@ -4,7 +4,7 @@
 
 import { authActions, AuthAction } from "./auth-action";
 import { User } from "../../models/user";
-import { authService } from "../../services/services";
+import { ServiceRepository } from "../../services/services";
 import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
 
 export interface AuthState {
@@ -12,34 +12,34 @@ export interface AuthState {
     apiToken?: string;
 }
 
-export const authReducer = (state: AuthState = {}, action: AuthAction) => {
+export const authReducer = (services: ServiceRepository) => (state: AuthState = {}, action: AuthAction) => {
     return authActions.match(action, {
         SAVE_API_TOKEN: (token: string) => {
-            authService.saveApiToken(token);
+            services.authService.saveApiToken(token);
             setServerApiAuthorizationHeader(token);
             return {...state, apiToken: token};
         },
         INIT: () => {
-            const user = authService.getUser();
-            const token = authService.getApiToken();
+            const user = services.authService.getUser();
+            const token = services.authService.getApiToken();
             if (token) {
                 setServerApiAuthorizationHeader(token);
             }
             return {user, apiToken: token};
         },
         LOGIN: () => {
-            authService.login();
+            services.authService.login();
             return state;
         },
         LOGOUT: () => {
-            authService.removeApiToken();
-            authService.removeUser();
+            services.authService.removeApiToken();
+            services.authService.removeUser();
             removeServerApiAuthorizationHeader();
-            authService.logout();
+            services.authService.logout();
             return {...state, apiToken: undefined};
         },
         USER_DETAILS_SUCCESS: (user: User) => {
-            authService.saveUser(user);
+            services.authService.saveUser(user);
             return {...state, user};
         },
         default: () => state
diff --git a/src/store/favorite-panel/favorite-panel-middleware.ts b/src/store/favorite-panel/favorite-panel-middleware.ts
index 548a117..62e93c6 100644
--- a/src/store/favorite-panel/favorite-panel-middleware.ts
+++ b/src/store/favorite-panel/favorite-panel-middleware.ts
@@ -4,7 +4,6 @@
 
 import { Middleware } from "redux";
 import { dataExplorerActions } from "../data-explorer/data-explorer-action";
-import { favoriteService } from "../../services/services";
 import { RootState } from "../store";
 import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
 import { FilterBuilder } from "../../common/api/filter-builder";
@@ -22,8 +21,9 @@ import { OrderBuilder } from "../../common/api/order-builder";
 import { SortDirection } from "../../components/data-table/data-column";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
 import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
+import { ServiceRepository } from "../../services/services";
 
-export const favoritePanelMiddleware: Middleware = store => next => {
+export const favoritePanelMiddleware = (services: ServiceRepository): Middleware => store => next => {
     next(dataExplorerActions.SET_COLUMNS({ id: FAVORITE_PANEL_ID, columns }));
 
     return action => {
@@ -62,7 +62,7 @@ export const favoritePanelMiddleware: Middleware = store => next => {
                 const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
                 const order = FavoriteOrderBuilder.create();
                 if (typeFilters.length > 0) {
-                    favoriteService
+                    services.favoriteService
                         .list(state.projects.currentItemId, {
                             limit: dataExplorer.rowsPerPage,
                             offset: dataExplorer.page * dataExplorer.rowsPerPage,
diff --git a/src/store/favorites/favorites-actions.ts b/src/store/favorites/favorites-actions.ts
index eb4f649..38229df 100644
--- a/src/store/favorites/favorites-actions.ts
+++ b/src/store/favorites/favorites-actions.ts
@@ -4,10 +4,10 @@
 
 import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-import { favoriteService } from "../../services/services";
 import { RootState } from "../store";
 import { checkFavorite } from "./favorites-reducer";
 import { snackbarActions } from "../snackbar/snackbar-actions";
+import { ServiceRepository } from "../../services/services";
 
 export const favoritesActions = unionize({
     TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -18,14 +18,14 @@ export const favoritesActions = unionize({
 export type FavoritesAction = UnionOf<typeof favoritesActions>;
 
 export const toggleFavorite = (resource: { uuid: string; name: string }) =>
-    (dispatch: Dispatch, getState: () => RootState): Promise<any> => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
         const promise: any = isFavorite
-            ? favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
-            : favoriteService.create({ userUuid, resource });
+            ? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
+            : services.favoriteService.create({ userUuid, resource });
 
         return promise
             .then(() => {
@@ -41,12 +41,12 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
     };
 
 export const checkPresenceInFavorites = (resourceUuids: string[]) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
-        favoriteService
+        services.favoriteService
             .checkPresenceInFavorites(userUuid, resourceUuids)
-            .then(results => {
+            .then((results: any) => {
                 dispatch(favoritesActions.UPDATE_FAVORITES(results));
             });
     };
diff --git a/src/store/project-panel/project-panel-middleware.ts b/src/store/project-panel/project-panel-middleware.ts
index b7ab03c..e984e06 100644
--- a/src/store/project-panel/project-panel-middleware.ts
+++ b/src/store/project-panel/project-panel-middleware.ts
@@ -5,7 +5,6 @@
 import { Middleware } from "redux";
 import { dataExplorerActions } from "../data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID, columns, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel";
-import { groupsService } from "../../services/services";
 import { RootState } from "../store";
 import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
 import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item";
@@ -16,8 +15,9 @@ import { OrderBuilder } from "../../common/api/order-builder";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
 import { SortDirection } from "../../components/data-table/data-column";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { ServiceRepository } from "../../services/services";
 
-export const projectPanelMiddleware: Middleware = store => next => {
+export const projectPanelMiddleware = (services: ServiceRepository): Middleware => store => next => {
     next(dataExplorerActions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
 
     return action => {
@@ -57,7 +57,7 @@ export const projectPanelMiddleware: Middleware = store => next => {
                 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) {
-                    groupsService
+                    services.groupsService
                         .contents(state.projects.currentItemId, {
                             limit: dataExplorer.rowsPerPage,
                             offset: dataExplorer.page * dataExplorer.rowsPerPage,
diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts
index cf38456..2f9963b 100644
--- a/src/store/project/project-action.ts
+++ b/src/store/project/project-action.ts
@@ -4,11 +4,11 @@
 import { default as unionize, ofType, UnionOf } from "unionize";
 
 import { ProjectResource } from "../../models/project";
-import { projectService } from "../../services/services";
 import { Dispatch } from "redux";
 import { FilterBuilder } from "../../common/api/filter-builder";
 import { RootState } from "../store";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { ServiceRepository } from "../../services/services";
 
 export const projectActions = unionize({
     OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
@@ -26,9 +26,9 @@ export const projectActions = unionize({
         value: 'payload'
     });
 
-export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState) => {
+export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
-    return projectService.list({
+    return services.projectService.list({
         filters: FilterBuilder
             .create<ProjectResource>()
             .addEqual("ownerUuid", parentUuid)
@@ -40,11 +40,11 @@ export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch,
 };
 
 export const createProject = (project: Partial<ProjectResource>) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { ownerUuid } = getState().projects.creator;
         const projectData = { ownerUuid, ...project };
         dispatch(projectActions.CREATE_PROJECT(projectData));
-        return projectService
+        return services.projectService
             .create(projectData)
             .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
     };
diff --git a/src/store/store.ts b/src/store/store.ts
index ae07744..cf07d6d 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -18,6 +18,7 @@ import { favoritePanelMiddleware } from "./favorite-panel/favorite-panel-middlew
 import { reducer as formReducer } from 'redux-form';
 import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
 import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
+import { ServiceRepository } from "../services/services";
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -36,26 +37,25 @@ export interface RootState {
     snackbar: SnackbarState;
 }
 
-const rootReducer = combineReducers({
-    auth: authReducer,
-    projects: projectsReducer,
-    router: routerReducer,
-    dataExplorer: dataExplorerReducer,
-    sidePanel: sidePanelReducer,
-    detailsPanel: detailsPanelReducer,
-    contextMenu: contextMenuReducer,
-    form: formReducer,
-    favorites: favoritesReducer,
-    snackbar: snackbarReducer,
-});
+export function configureStore(history: History, services: ServiceRepository) {
+    const rootReducer = combineReducers({
+        auth: authReducer(services),
+        projects: projectsReducer,
+        router: routerReducer,
+        dataExplorer: dataExplorerReducer,
+        sidePanel: sidePanelReducer,
+        detailsPanel: detailsPanelReducer,
+        contextMenu: contextMenuReducer,
+        form: formReducer,
+        favorites: favoritesReducer,
+        snackbar: snackbarReducer,
+    });
 
-
-export function configureStore(history: History) {
     const middlewares: Middleware[] = [
         routerMiddleware(history),
-        thunkMiddleware,
-        projectPanelMiddleware,
-        favoritePanelMiddleware
+        thunkMiddleware.withExtraArgument(services),
+        projectPanelMiddleware(services),
+        favoritePanelMiddleware(services)
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx
index 1d017cc..51e3ad1 100644
--- a/src/views-components/api-token/api-token.tsx
+++ b/src/views-components/api-token/api-token.tsx
@@ -6,11 +6,12 @@ import { Redirect, RouteProps } from "react-router";
 import * as React from "react";
 import { connect, DispatchProp } from "react-redux";
 import { authActions, getUserDetails } from "../../store/auth/auth-action";
-import { authService } from "../../services/services";
 import { getProjectList } from "../../store/project/project-action";
 import { getUrlParameter } from "../../common/url";
+import { AuthService } from "../../services/auth-service/auth-service";
 
 interface ApiTokenProps {
+    authService: AuthService;
 }
 
 export const ApiToken = connect()(
@@ -20,7 +21,7 @@ export const ApiToken = connect()(
             const apiToken = getUrlParameter(search, 'api_token');
             this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
             this.props.dispatch<any>(getUserDetails()).then(() => {
-                const rootUuid = authService.getRootUuid();
+                const rootUuid = this.props.authService.getRootUuid();
                 this.props.dispatch(getProjectList(rootUuid));
             });
         }
diff --git a/src/views/workbench/workbench.test.tsx b/src/views/workbench/workbench.test.tsx
index 538b8e7..48ea2de 100644
--- a/src/views/workbench/workbench.test.tsx
+++ b/src/views/workbench/workbench.test.tsx
@@ -11,12 +11,13 @@ import createBrowserHistory from "history/createBrowserHistory";
 import { ConnectedRouter } from "react-router-redux";
 import { MuiThemeProvider } from '@material-ui/core/styles';
 import { CustomTheme } from '../../common/custom-theme';
+import { createServices } from "../../services/services";
 
 const history = createBrowserHistory();
 
 it('renders without crashing', () => {
     const div = document.createElement('div');
-    const store = configureStore(createBrowserHistory());
+    const store = configureStore(createBrowserHistory(), createServices());
     ReactDOM.render(
         <MuiThemeProvider theme={CustomTheme}>
             <Provider store={store}>
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 3637528..b0fe4b3 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -24,7 +24,6 @@ import { ProjectPanel } from "../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 { authService } from '../../services/services';
 
 import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action";
 import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
@@ -36,6 +35,7 @@ import { FavoritePanel, FAVORITE_PANEL_ID } from "../favorite-panel/favorite-pan
 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 { AuthService } from "../../services/auth-service/auth-service";
 
 const drawerWidth = 240;
 const appBarHeight = 100;
@@ -86,10 +86,14 @@ interface WorkbenchDataProps {
     sidePanelItems: SidePanelItem[];
 }
 
+interface WorkbenchServiceProps {
+    authService: AuthService;
+}
+
 interface WorkbenchActionProps {
 }
 
-type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
 
 interface NavBreadcrumb extends Breadcrumb {
     itemId: string;
@@ -188,7 +192,7 @@ export const Workbench = withStyles(styles)(
                                     toggleActive={this.toggleSidePanelActive}
                                     sidePanelItems={this.props.sidePanelItems}
                                     onContextMenu={(event) => this.openContextMenu(event, {
-                                        uuid: authService.getUuid() || "",
+                                        uuid: this.props.authService.getUuid() || "",
                                         name: "",
                                         kind: ContextMenuKind.ROOT_PROJECT
                                     })}>

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list