"@material-ui/core": "1.4.0",
"@material-ui/icons": "1.1.0",
"@types/lodash": "4.14.112",
+ "@types/react-copy-to-clipboard": "4.2.5",
"@types/redux-form": "7.4.1",
"axios": "0.18.0",
"classnames": "2.2.6",
"lodash": "4.17.10",
"react": "16.4.1",
+ "react-copy-to-clipboard": "5.0.1",
"react-dom": "16.4.1",
"react-redux": "5.0.7",
"react-router": "4.3.1",
}));
}
- update(uuid: string) {
- throw new Error("Not implemented");
+ update(uuid: string, data: any) {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .put<T>(this.resourceType + uuid, data));
+
}
}
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import Axios, { AxiosInstance } from "axios";
-
-export const API_HOST = process.env.REACT_APP_ARVADOS_API_HOST;
-
-export const authClient: AxiosInstance = Axios.create();
-export const apiClient: AxiosInstance = Axios.create();
-
-export function setServerApiAuthorizationHeader(token: string) {
- [authClient, apiClient].forEach(client => {
- client.defaults.headers.common = {
- Authorization: `OAuth2 ${token}`
- };
- });
-}
-
-export function removeServerApiAuthorizationHeader() {
- [authClient, apiClient].forEach(client => {
- delete client.defaults.headers.common.Authorization;
- });
-}
-
-export const setBaseUrl = (url: string) => {
- authClient.defaults.baseURL = url;
- apiClient.defaults.baseURL = url + "/arvados/v1";
-};
root: {
padding: '8px 16px'
}
+ },
+ MuiInput: {
+ underline: {
+ '&:after': {
+ borderBottomColor: purple800
+ },
+ '&:hover:not($disabled):not($focused):not($error):before': {
+ borderBottom: '1px solid inherit'
+ }
+ }
+ },
+ MuiFormLabel: {
+ focused: {
+ "&$focused:not($error)": {
+ color: purple800
+ }
+ }
}
},
mixins: {
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 { 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 { setBaseUrl } from './common/api/server-api';
import { addMenuActionSet, ContextMenuKind } from "./views-components/context-menu/context-menu";
import { rootProjectActionSet } from "./views-components/context-menu/action-sets/root-project-action-set";
import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
fetchConfig()
.then(config => {
-
- setBaseUrl(config.API_HOST);
-
const history = createBrowserHistory();
- const store = configureStore(history);
+ const services = createServices(config.API_HOST);
+ const store = configureStore(history, services);
+
+ store.dispatch(initAuth());
+ store.dispatch(getProjectList(services.authService.getUuid()));
- store.dispatch(authActions.INIT());
- store.dispatch<any>(getProjectList(authService.getUuid()));
+ const Token = (props: any) => <ApiToken authService={services.authService} {...props}/>;
const App = () =>
<MuiThemeProvider theme={CustomTheme}>
<ConnectedRouter history={history}>
<div>
<Route path="/" component={Workbench} />
- <Route path="/token" component={ApiToken} />
+ <Route path="/token" component={Token} />
</div>
</ConnectedRouter>
</Provider>
//
// SPDX-License-Identifier: AGPL-3.0
-import { API_HOST } from "../../common/api/server-api";
import { User } from "../../models/user";
import { AxiosInstance } from "../../../node_modules/axios";
export class AuthService {
constructor(
- protected authClient: AxiosInstance,
- protected apiClient: AxiosInstance) { }
+ protected apiClient: AxiosInstance,
+ protected baseUrl: string) { }
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
public login() {
const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
- window.location.assign(`${this.authClient.defaults.baseURL || ""}/login?return_to=${currentUrl}`);
+ window.location.assign(`${this.baseUrl || ""}/login?return_to=${currentUrl}`);
}
public logout() {
const currentUrl = `${window.location.protocol}//${window.location.host}`;
- window.location.assign(`${this.authClient.defaults.baseURL || ""}/logout?return_to=${currentUrl}`);
+ window.location.assign(`${this.baseUrl || ""}/logout?return_to=${currentUrl}`);
}
public getUserDetails = (): Promise<User> => {
import { AuthService } from "./auth-service/auth-service";
import { GroupsService } from "./groups-service/groups-service";
-import { authClient, apiClient } from "../common/api/server-api";
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 { CollectionService } from "./collection-service/collection-service";
+import Axios from "axios";
import { CollectionFilesService } from "./collection-files-service/collection-files-service";
-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 collectionFilesService = new CollectionFilesService(collectionService);
-export const linkService = new LinkService(apiClient);
-export const favoriteService = new FavoriteService(linkService, groupsService);
\ No newline at end of file
+export interface ServiceRepository {
+ apiClient: AxiosInstance;
+
+ authService: AuthService;
+ groupsService: GroupsService;
+ projectService: ProjectService;
+ linkService: LinkService;
+ favoriteService: FavoriteService;
+ collectionService: CollectionService;
+ collectionFilesService: CollectionFilesService;
+}
+
+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 CollectionService(apiClient);
+ const collectionFilesService = new CollectionFilesService(collectionService);
+
+ return {
+ apiClient,
+ authService,
+ groupsService,
+ projectService,
+ linkService,
+ favoriteService,
+ collectionService,
+ collectionFilesService
+ };
+};
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";
+import { AxiosInstance } from "axios";
export const authActions = unionize({
SAVE_API_TOKEN: ofType<string>(),
LOGIN: {},
LOGOUT: {},
- INIT: {},
+ INIT: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
}, {
value: 'payload'
});
-export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
+function setAuthorizationHeader(client: AxiosInstance, token: string) {
+ client.defaults.headers.common = {
+ Authorization: `OAuth2 ${token}`
+ };
+}
+
+function removeAuthorizationHeader(client: AxiosInstance) {
+ 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.apiClient, token);
+ }
+ if (token && user) {
+ dispatch(authActions.INIT({ user, token }));
+ }
+};
+
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.saveApiToken(token);
+ setAuthorizationHeader(services.apiClient, token);
+ dispatch(authActions.SAVE_API_TOKEN(token));
+};
+
+export const login = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.login();
+ dispatch(authActions.LOGIN());
+};
+
+export const logout = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.removeApiToken();
+ services.authService.removeUser();
+ removeAuthorizationHeader(services.apiClient);
+ services.authService.logout();
+ dispatch(authActions.LOGOUT());
+};
+
+export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
dispatch(authActions.USER_DETAILS_REQUEST());
- return authService.getUserDetails().then(details => {
- dispatch(authActions.USER_DETAILS_SUCCESS(details));
- return details;
+ return services.authService.getUserDetails().then(user => {
+ services.authService.saveUser(user);
+ dispatch(authActions.USER_DETAILS_SUCCESS(user));
+ return user;
});
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, initAuth } from "./auth-action";
+import {
+ API_TOKEN_KEY,
+ USER_EMAIL_KEY,
+ USER_FIRST_NAME_KEY,
+ USER_LAST_NAME_KEY,
+ USER_OWNER_UUID_KEY,
+ USER_UUID_KEY
+} from "../../services/auth-service/auth-service";
+
+import 'jest-localstorage-mock';
+import { createServices } from "../../services/services";
+import { configureStore, RootStore } from "../store";
+import createBrowserHistory from "history/createBrowserHistory";
+
+describe('auth-actions', () => {
+ let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+ let store: RootStore;
+
+ beforeEach(() => {
+ store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
+ localStorage.clear();
+ reducer = authReducer(createServices("/arvados/v1"));
+ });
+
+ it('should initialise state with user and api token from local storage', () => {
+
+ localStorage.setItem(API_TOKEN_KEY, "token");
+ localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
+ localStorage.setItem(USER_FIRST_NAME_KEY, "John");
+ localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
+ localStorage.setItem(USER_UUID_KEY, "uuid");
+ localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
+
+ store.dispatch(initAuth());
+
+ expect(store.getState().auth).toEqual({
+ apiToken: "token",
+ user: {
+ email: "test@test.com",
+ firstName: "John",
+ lastName: "Doe",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid"
+ }
+ });
+ });
+
+ // TODO: Add remaining action tests
+ /*
+ it('should fire external url to login', () => {
+ const initialState = undefined;
+ window.location.assign = jest.fn();
+ reducer(initialState, authActions.LOGIN());
+ expect(window.location.assign).toBeCalledWith(
+ `/login?return_to=${window.location.protocol}//${window.location.host}/token`
+ );
+ });
+
+ it('should fire external url to logout', () => {
+ const initialState = undefined;
+ window.location.assign = jest.fn();
+ reducer(initialState, authActions.LOGOUT());
+ expect(window.location.assign).toBeCalledWith(
+ `/logout?return_to=${location.protocol}//${location.host}`
+ );
+ });
+ */
+});
//
// SPDX-License-Identifier: AGPL-3.0
-import { authReducer } from "./auth-reducer";
-import { authActions } from "./auth-action";
-import {
- API_TOKEN_KEY,
- USER_EMAIL_KEY,
- USER_FIRST_NAME_KEY,
- USER_LAST_NAME_KEY,
- USER_OWNER_UUID_KEY,
- USER_UUID_KEY
-} from "../../services/auth-service/auth-service";
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, authActions } from "./auth-action";
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("/arvados/v1"));
});
- it('should return default state on initialisation', () => {
- const initialState = undefined;
- const state = authReducer(initialState, authActions.INIT());
- expect(state).toEqual({
- apiToken: undefined,
- user: undefined
- });
- });
-
- it('should read user and api token from local storage on init if they are there', () => {
+ it('should correctly initialise state', () => {
const initialState = undefined;
-
- localStorage.setItem(API_TOKEN_KEY, "token");
- localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
- localStorage.setItem(USER_FIRST_NAME_KEY, "John");
- localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
- localStorage.setItem(USER_UUID_KEY, "uuid");
- localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
-
- const state = authReducer(initialState, authActions.INIT());
+ const user = {
+ email: "test@test.com",
+ firstName: "John",
+ lastName: "Doe",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid"
+ };
+ const state = reducer(initialState, authActions.INIT({user, token: "token"}));
expect(state).toEqual({
apiToken: "token",
- user: {
- email: "test@test.com",
- firstName: "John",
- lastName: "Doe",
- uuid: "uuid",
- ownerUuid: "ownerUuid"
- }
+ user
});
});
- it('should store token in local storage', () => {
+ it('should save api token', () => {
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
});
-
- expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
});
it('should set user details on success fetch', () => {
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: {
ownerUuid: "ownerUuid",
}
});
-
- expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
- });
-
- it('should fire external url to login', () => {
- const initialState = undefined;
- window.location.assign = jest.fn();
- authReducer(initialState, authActions.LOGIN());
- expect(window.location.assign).toBeCalledWith(
- `/login?return_to=${window.location.protocol}//${window.location.host}/token`
- );
- });
-
- it('should fire external url to logout', () => {
- const initialState = undefined;
- window.location.assign = jest.fn();
- authReducer(initialState, authActions.LOGOUT());
- expect(window.location.assign).toBeCalledWith(
- `/logout?return_to=${location.protocol}//${location.host}`
- );
});
});
import { authActions, AuthAction } from "./auth-action";
import { User } from "../../models/user";
-import { authService } from "../../services/services";
-import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
+import { ServiceRepository } from "../../services/services";
export interface AuthState {
user?: User;
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);
- setServerApiAuthorizationHeader(token);
return {...state, apiToken: token};
},
- INIT: () => {
- const user = authService.getUser();
- const token = authService.getApiToken();
- if (token) {
- setServerApiAuthorizationHeader(token);
- }
- return {user, apiToken: token};
+ INIT: ({ user, token }) => {
+ return { user, apiToken: token };
},
LOGIN: () => {
- authService.login();
return state;
},
LOGOUT: () => {
- authService.removeApiToken();
- authService.removeUser();
- removeServerApiAuthorizationHeader();
- authService.logout();
return {...state, apiToken: undefined};
},
USER_DETAILS_SUCCESS: (user: User) => {
- authService.saveUser(user);
return {...state, user};
},
default: () => state
import { Dispatch } from "redux";
import { ResourceKind } from "../../models/resource";
import { CollectionResource } from "../../models/collection";
-import { collectionService, collectionFilesService } from "../../services/services";
import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
import { createTree } from "../../models/tree";
-import { mapManifestToCollectionFilesTree } from "../../services/collection-files-service/collection-manifest-mapper";
-import { parseKeepManifestText } from "../../services/collection-files-service/collection-manifest-parser";
+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 }>(),
+ LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>()
}, { tag: 'type', value: 'payload' });
export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
export const loadCollection = (uuid: string, kind: ResourceKind) =>
- (dispatch: Dispatch) => {
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
- return collectionService
+ return services.collectionService
.get(uuid)
.then(item => {
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
- return collectionFilesService.getFiles(item.uuid);
+ return services.collectionFilesService.getFiles(item.uuid);
})
.then(files => {
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files }));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { combineReducers } from 'redux';
+import * as creator from "./creator/collection-creator-reducer";
+import * as updator from "./updator/collection-updator-reducer";
+
+export type CollectionsState = {
+ creator: creator.CollectionCreatorState;
+ updator: updator.CollectionUpdatorState;
+};
+
+export const collectionsReducer = combineReducers({
+ creator: creator.collectionCreationReducer,
+ updator: updator.collectionCreationReducer
+});
\ No newline at end of file
import { Dispatch } from "redux";
import { RootState } from "../../store";
-import { collectionService } from '../../../services/services';
import { CollectionResource } from '../../../models/collection';
+import { ServiceRepository } from "../../../services/services";
export const collectionCreateActions = unionize({
OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
});
export const createCollection = (collection: Partial<CollectionResource>) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- const { ownerUuid } = getState().collectionCreation.creator;
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { ownerUuid } = getState().collections.creator;
const collectiontData = { ownerUuid, ...collection };
dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
- return collectionService
+ return services.collectionService
.create(collectiontData)
.then(collection => dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection)));
};
-export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
\ No newline at end of file
+export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
describe('collection-reducer', () => {
it('should open collection creator dialog', () => {
- const initialState = {
- creator: { opened: false, ownerUuid: "" }
- };
- const collection = {
- creator: { opened: true, ownerUuid: "" },
- };
-
- const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState.creator));
+ const initialState = { opened: false, ownerUuid: "" };
+ const collection = { opened: true, ownerUuid: "" };
+
+ const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
expect(state).toEqual(collection);
});
it('should close collection creator dialog', () => {
- const initialState = {
- creator: { opened: true, ownerUuid: "" }
- };
- const collection = {
- creator: { opened: false, ownerUuid: "" },
- };
+ const initialState = { opened: true, ownerUuid: "" };
+ const collection = { opened: false, ownerUuid: "" };
const state = collectionCreationReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
expect(state).toEqual(collection);
});
it('should reset collection creator dialog props', () => {
- const initialState = {
- creator: { opened: true, ownerUuid: "test" }
- };
- const collection = {
- creator: { opened: false, ownerUuid: "" },
- };
+ const initialState = { opened: true, ownerUuid: "test" };
+ const collection = { opened: false, ownerUuid: "" };
const state = collectionCreationReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
expect(state).toEqual(collection);
import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action';
-export type CollectionCreatorState = {
- creator: CollectionCreator
-};
+export type CollectionCreatorState = CollectionCreator;
interface CollectionCreator {
opened: boolean;
const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreator>) => ({
...state,
- creator: {
- ...state.creator,
- ...creator
- }
+ ...creator
});
const initialState: CollectionCreatorState = {
- creator: {
- opened: false,
- ownerUuid: ""
- }
+ opened: false,
+ ownerUuid: ''
};
export const collectionCreationReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
--- /dev/null
+// 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";
+import { ServiceRepository } from "../../../services/services";
+import { CollectionResource } from '../../../models/collection';
+import { initialize } from 'redux-form';
+import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
+
+export const collectionUpdatorActions = unionize({
+ OPEN_COLLECTION_UPDATOR: ofType<{ uuid: string }>(),
+ CLOSE_COLLECTION_UPDATOR: ofType<{}>(),
+ UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+
+export const COLLECTION_FORM_NAME = 'collectionEditDialog';
+
+export const openUpdator = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(collectionUpdatorActions.OPEN_COLLECTION_UPDATOR({ uuid }));
+ const item = getState().collectionPanel.item;
+ if(item) {
+ dispatch(initialize(COLLECTION_FORM_NAME, { name: item.name, description: item.description }));
+ }
+ };
+
+export const updateCollection = (collection: Partial<CollectionResource>) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { uuid } = getState().collections.updator;
+ return services.collectionService
+ .update(uuid, collection)
+ .then(collection => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
+ dispatch(collectionUpdatorActions.UPDATE_COLLECTION_SUCCESS());
+ }
+ );
+ };
+
+export type CollectionUpdatorAction = UnionOf<typeof collectionUpdatorActions>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionUpdatorActions, CollectionUpdatorAction } from './collection-updator-action';
+
+export type CollectionUpdatorState = CollectionUpdator;
+
+interface CollectionUpdator {
+ opened: boolean;
+ uuid: string;
+}
+
+const updateCollection = (state: CollectionUpdatorState, updator?: Partial<CollectionUpdator>) => ({
+ ...state,
+ ...updator
+});
+
+const initialState: CollectionUpdatorState = {
+ opened: false,
+ uuid: ''
+};
+
+export const collectionCreationReducer = (state: CollectionUpdatorState = initialState, action: CollectionUpdatorAction) => {
+ return collectionUpdatorActions.match(action, {
+ OPEN_COLLECTION_UPDATOR: ({ uuid }) => updateCollection(state, { uuid, opened: true }),
+ CLOSE_COLLECTION_UPDATOR: () => updateCollection(state, { opened: false }),
+ UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, uuid: "" }),
+ default: () => state
+ });
+};
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "unionize";
-import { CommonResourceService } from "../../common/api/common-resource-service";
import { Dispatch } from "redux";
-import { apiClient } from "../../common/api/server-api";
import { Resource, ResourceKind } from "../../models/resource";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
export const loadDetails = (uuid: string, kind: ResourceKind) =>
- (dispatch: Dispatch) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
- getService(kind)
- .get(uuid)
- .then(project => {
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
- });
+ const item = await getService(services, kind).get(uuid);
+ dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
};
-const getService = (kind: ResourceKind) => {
+const getService = (services: ServiceRepository, kind: ResourceKind) => {
switch (kind) {
case ResourceKind.PROJECT:
- return new CommonResourceService(apiClient, "groups");
+ return services.projectService;
case ResourceKind.COLLECTION:
- return new CommonResourceService(apiClient, "collections");
+ return services.collectionService;
default:
- return new CommonResourceService(apiClient, "");
+ return services.projectService;
}
};
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";
-import { favoriteService, authService } from "../../services/services";
+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 { Dispatch, MiddlewareAPI } from "redux";
export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
- constructor(id: string) {
+ constructor(private services: ServiceRepository, id: string) {
super(id);
}
const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
const order = FavoriteOrderBuilder.create();
if (typeFilters.length > 0) {
- favoriteService
- .list(authService.getUuid()!, {
+ this.services.favoriteService
+ .list(this.services.authService.getUuid()!, {
limit: dataExplorer.rowsPerPage,
offset: dataExplorer.page * dataExplorer.rowsPerPage,
order: sortColumn!.name === FavoritePanelColumnNames.NAME
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 }>(),
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(() => {
};
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));
});
};
import { ProjectPanelColumnNames, ProjectPanelFilter } from "../../views/project-panel/project-panel";
import { RootState } from "../store";
import { DataColumns } from "../../components/data-table/data-table";
-import { groupsService } from "../../services/services";
+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 { Dispatch, MiddlewareAPI } from "redux";
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
- constructor(id: string) {
+ constructor(private services: ServiceRepository, id: string) {
super(id);
}
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
+ this.services.groupsService
.contents(state.projects.currentItemId, {
limit: dataExplorer.rowsPerPage,
offset: dataExplorer.page * dataExplorer.rowsPerPage,
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 }>(),
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)
};
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)));
};
//
// SPDX-License-Identifier: AGPL-3.0
-import { createStore, applyMiddleware, compose, Middleware, combineReducers } from 'redux';
+import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
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 { DialogState, dialogReducer } from './dialog/dialog-reducer';
+import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
+import { ServiceRepository } from "../services/services";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
export interface RootState {
auth: AuthState;
projects: ProjectState;
- collectionCreation: CollectionCreatorState;
+ collections: CollectionsState;
router: RouterState;
dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
dialog: DialogState;
}
-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,
- collectionPanelFiles: collectionPanelFilesReducer,
- dialog: dialogReducer
-});
+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,
+ collections: collectionsReducer,
+ router: routerReducer,
+ dataExplorer: dataExplorerReducer,
+ sidePanel: sidePanelReducer,
+ collectionPanel: collectionPanelReducer,
+ detailsPanel: detailsPanelReducer,
+ contextMenu: contextMenuReducer,
+ form: formReducer,
+ favorites: favoritesReducer,
+ snackbar: snackbarReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ dialog: dialogReducer
+ });
-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,
favoritePanelMiddleware
];
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 { getUserDetails, saveApiToken } from "../../store/auth/auth-action";
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()(
componentDidMount() {
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
- this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+ this.props.dispatch(saveApiToken(apiToken));
this.props.dispatch<any>(getUserDetails()).then(() => {
- const rootUuid = authService.getRootUuid();
+ const rootUuid = this.props.authService.getRootUuid();
this.props.dispatch(getProjectList(rootUuid));
});
}
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "../../../components/icon/icon";
+import { openUpdator } from "../../../store/collections/updator/collection-updator-action";
import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
export const collectionActionSet: ContextMenuActionSet = [[
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(openUpdator(resource.uuid));
}
},
{
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
});
}
},
import { snackbarActions } from "../../store/snackbar/snackbar-actions";
const mapStateToProps = (state: RootState) => ({
- open: state.collectionCreation.creator.opened
+ open: state.collections.creator.opened
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
(dispatch: Dispatch) => {
return dispatch<any>(createCollection(data)).then(() => {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Created a new collection",
+ message: "Collection has been successfully created.",
hideDuration: 2000
}));
dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
const styles: StyleRulesCallback<CssRules> = theme => ({
button: {
formContainer: {
display: "flex",
flexDirection: "column",
- marginTop: "20px",
- },
- dialogTitle: {
- paddingBottom: "0"
},
textField: {
- marginTop: "32px",
- },
- dialog: {
- minWidth: "600px",
- minHeight: "320px"
+ marginBottom: theme.spacing.unit * 3
},
createProgress: {
position: "absolute",
right: "110px"
},
dialogActions: {
- marginBottom: "24px"
+ marginBottom: theme.spacing.unit * 3
}
});
interface DialogCollectionCreateProps {
<Dialog
open={open}
onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
disableBackdropClick={true}
disableEscapeKeyDown={true}>
- <div className={classes.dialog}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
- <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a collection</DialogTitle>
- <DialogContent className={classes.formContainer}>
- <Field name="name"
- component={this.renderTextField}
- floatinglabeltext="Collection Name"
- validate={COLLECTION_NAME_VALIDATION}
- className={classes.textField}
- label="Collection Name"/>
- <Field name="description"
- component={this.renderTextField}
- floatinglabeltext="Description - optional"
- validate={COLLECTION_DESCRIPTION_VALIDATION}
- className={classes.textField}
- label="Description - optional"/>
- </DialogContent>
- <DialogActions className={classes.dialogActions}>
- <Button onClick={handleClose} className={classes.button} color="primary"
- disabled={submitting}>CANCEL</Button>
- <Button type="submit"
- className={classes.lastButton}
- color="primary"
- disabled={invalid || submitting || pristine}
- variant="contained">
- CREATE A COLLECTION
- </Button>
- {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
- </DialogActions>
- </form>
- </div>
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
+ <DialogContent className={classes.formContainer}>
+ <Field name="name"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Collection Name"
+ validate={COLLECTION_NAME_VALIDATION}
+ className={classes.textField}
+ label="Collection Name"/>
+ <Field name="description"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Description - optional"
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ className={classes.textField}
+ label="Description - optional"/>
+ </DialogContent>
+ <DialogActions className={classes.dialogActions}>
+ <Button onClick={handleClose} className={classes.button} color="primary"
+ disabled={submitting}>CANCEL</Button>
+ <Button type="submit"
+ className={classes.lastButton}
+ color="primary"
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ CREATE A COLLECTION
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
+ </DialogActions>
+ </form>
</Dialog>
);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
+
+type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ content: {
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ actions: {
+ margin: 0,
+ padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px
+ ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
+ },
+ textField: {
+ marginBottom: theme.spacing.unit * 3
+ },
+ buttonWrapper: {
+ position: 'relative'
+ },
+ saveButton: {
+ boxShadow: 'none'
+ },
+ circularProgress: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ margin: 'auto'
+ }
+});
+
+interface DialogCollectionDataProps {
+ open: boolean;
+ handleSubmit: any;
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface DialogCollectionAction {
+ handleClose: () => void;
+ onSubmit: (data: { name: string, description: string }) => void;
+}
+
+type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
+
+interface TextFieldProps {
+ label: string;
+ floatinglabeltext: string;
+ className?: string;
+ input?: string;
+ meta?: any;
+}
+
+export const DialogCollectionUpdate = compose(
+ reduxForm({ form: COLLECTION_FORM_NAME }),
+ withStyles(styles))(
+
+ class DialogCollectionUpdate extends React.Component<DialogCollectionProps> {
+
+ render() {
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
+ return (
+ <Dialog open={open}
+ onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}>
+
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <DialogTitle>Edit Collection</DialogTitle>
+ <DialogContent className={classes.content}>
+ <Field name="name"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Collection Name"
+ validate={COLLECTION_NAME_VALIDATION}
+ className={classes.textField}
+ label="Collection Name" />
+ <Field name="description"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Description - optional"
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ className={classes.textField}
+ label="Description - optional" />
+ </DialogContent>
+ <DialogActions className={classes.actions}>
+ <Button onClick={handleClose} color="primary"
+ disabled={submitting}>CANCEL</Button>
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ SAVE
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </DialogActions>
+ </form>
+ </Dialog>
+ );
+ }
+
+ renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+ <TextField
+ helperText={touched && error}
+ label={label}
+ className={this.props.classes.textField}
+ error={touched && !!error}
+ autoComplete='off'
+ {...input}
+ {...custom}
+ />
+ )
+ }
+ );
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+import { RootState } from "../../store/store";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { collectionUpdatorActions, updateCollection } from "../../store/collections/updator/collection-updator-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { DialogCollectionUpdate } from "../dialog-update/dialog-collection-update";
+
+const mapStateToProps = (state: RootState) => ({
+ open: state.collections.updator.opened
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ handleClose: () => {
+ dispatch(collectionUpdatorActions.CLOSE_COLLECTION_UPDATOR());
+ },
+ onSubmit: (data: { name: string, description: string }) => {
+ return dispatch<any>(editCollection(data))
+ .catch((e: any) => {
+ if(e.errors) {
+ throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
+ }
+ });
+ }
+});
+
+const editCollection = (data: { name: string, description: string }) =>
+ (dispatch: Dispatch) => {
+ return dispatch<any>(updateCollection(data)).then(() => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ });
+ };
+
+export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
\ No newline at end of file
import * as React from 'react';
import {
StyleRulesCallback, WithStyles, withStyles, Card,
- CardHeader, IconButton, CardContent, Grid
+ CardHeader, IconButton, CardContent, Grid, Chip
} from '@material-ui/core';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { ArvadosTheme } from '../../common/custom-theme';
import { RootState } from '../../store/store';
-import { MoreOptionsIcon, CollectionIcon } from '../../components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '../../components/icon/icon';
import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
import { CollectionResource } from '../../models/collection';
import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
-type CssRules = 'card' | 'iconHeader';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
iconHeader: {
fontSize: '1.875rem',
color: theme.customs.colors.yellow700
+ },
+ tag: {
+ marginRight: theme.spacing.unit
+ },
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ fontSize: '1.125rem',
+ cursor: 'pointer'
}
});
type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
export const CollectionPanel = withStyles(styles)(
connect((state: RootState) => ({ item: state.collectionPanel.item }))(
class extends React.Component<CollectionPanelProps> {
<MoreOptionsIcon />
</IconButton>
}
- title={item && item.name } />
+ title={item && item.name }
+ subheader={item && item.description} />
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
- <DetailsAttribute label='Collection UUID' value={item && item.uuid} />
- <DetailsAttribute label='Content size' value='54 MB' />
+ <DetailsAttribute label='Collection UUID' value={item && item.uuid}>
+ <CopyToClipboard text={item && item.uuid}>
+ <CopyIcon className={classes.copyIcon} />
+ </CopyToClipboard>
+ </DetailsAttribute>
+ <DetailsAttribute label='Content size' value='54 MB' />
<DetailsAttribute label='Owner' value={item && item.ownerUuid} />
</Grid>
</Grid>
<CardContent>
<Grid container direction="column">
<Grid item xs={4}>
- Tags
+ <Chip label="Tag 1" className={classes.tag}/>
+ <Chip label="Tag 2" className={classes.tag}/>
+ <Chip label="Tag 3" className={classes.tag}/>
</Grid>
</Grid>
</CardContent>
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("/arvados/v1"));
ReactDOM.render(
<MuiThemeProvider theme={CustomTheme}>
<Provider store={store}>
import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
import { Route, Switch, RouteComponentProps } from "react-router";
-import { authActions } from "../../store/auth/auth-action";
+import { login, logout } from "../../store/auth/auth-action";
import { User } from "../../models/user";
import { RootState } from "../../store/store";
import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
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";
import { getCollectionUrl } from '../../models/collection';
import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
+import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
+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';
},
drawerPaper: {
position: 'relative',
- width: drawerWidth,
+ width: DRAWER_WITDH,
display: 'flex',
flexDirection: 'column',
},
display: "flex",
flexGrow: 1,
minWidth: 0,
- paddingTop: appBarHeight
+ paddingTop: APP_BAR_HEIGHT
},
content: {
padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
sidePanelItems: SidePanelItem[];
}
+interface WorkbenchServiceProps {
+ authService: AuthService;
+}
+
interface WorkbenchActionProps {
}
-type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
interface NavBreadcrumb extends Breadcrumb {
itemId: string;
},
{
label: "Logout",
- action: () => this.props.dispatch(authActions.LOGOUT())
+ action: () => this.props.dispatch(logout())
},
{
label: "My account",
anonymousMenu: [
{
label: "Sign in",
- action: () => this.props.dispatch(authActions.LOGIN())
+ action: () => this.props.dispatch(login())
}
]
}
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
})}>
<ProjectTree
projects={this.props.projects}
- toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
+ toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
onContextMenu={(event, item) => this.openContextMenu(event, {
uuid: item.data.uuid,
name: item.data.name,
kind: ContextMenuKind.PROJECT
})}
toggleActive={itemId => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.PROJECT));
- this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
+ this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
}} />
</SidePanel>
</Drawer>}
<CreateCollectionDialog />
<RemoveDialog />
<RenameDialog />
+ <UpdateCollectionDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
open={this.state.isCurrentTokenDialogOpen}
{...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;
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 => {
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));
+ default:
+ 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<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, {
}}
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 => {
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));
}
}}
mainAppBarActions: MainAppBarActionProps = {
onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.PROJECT));
+ this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
+ this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
},
onSearch: searchText => {
this.setState({ searchText });
version "10.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
+"@types/react-copy-to-clipboard@4.2.5":
+ version "4.2.5"
+ resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+ dependencies:
+ "@types/react" "*"
+
"@types/react-dom@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+copy-to-clipboard@^3:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+ dependencies:
+ toggle-selection "^1.0.3"
+
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-copy-to-clipboard@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
+ dependencies:
+ copy-to-clipboard "^3"
+ prop-types "^15.5.8"
+
react-dev-utils@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
regex-not "^1.0.2"
safe-regex "^1.1.0"
+toggle-selection@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
toposort@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"