Merge branch 'master' into 15088-merge-account
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Wed, 15 May 2019 15:04:57 +0000 (11:04 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Wed, 15 May 2019 15:04:57 +0000 (11:04 -0400)
46 files changed:
src/common/config.ts
src/components/data-explorer/data-explorer.tsx
src/components/details-attribute/details-attribute.tsx
src/index.tsx
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/auth-service/auth-service.ts
src/store/auth/auth-action-session.ts
src/store/auth/auth-action.ts
src/store/auth/auth-reducer.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collection-panel/collection-panel-reducer.ts
src/store/collections-content-address-panel/collections-content-address-middleware-service.ts [new file with mode: 0644]
src/store/collections-content-address-panel/collections-content-address-panel-actions.ts [new file with mode: 0644]
src/store/context-menu/context-menu-actions.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/navigation/navigation-action.ts
src/store/owner-name/owner-name-actions.ts [new file with mode: 0644]
src/store/owner-name/owner-name-reducer.ts [new file with mode: 0644]
src/store/projects/project-move-actions.ts
src/store/projects/project-update-actions.ts
src/store/public-favorites-panel/public-favorites-middleware-service.ts
src/store/public-favorites/public-favorites-actions.ts
src/store/store.ts
src/store/trash/trash-actions.ts
src/store/users/user-panel-middleware-service.ts
src/store/users/users-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/api-token/api-token.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/collection-details.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/favorite-star/favorite-star.tsx
src/views-components/main-app-bar/anonymous-menu.tsx
src/views-components/projects-tree-picker/projects-tree-picker.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx [new file with mode: 0644]
src/views/favorite-panel/favorite-panel.tsx
src/views/login-panel/login-panel.tsx
src/views/process-log-panel/process-log-main-card.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/search-results-panel/search-results-panel.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
src/views/workbench/fed-login.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

index 3961d5aa2496fec7fbba912a96738f1bc15b8b5d..71b7774c5fa8d765818b7ce611f6e4bfa1805c11 100644 (file)
@@ -51,6 +51,7 @@ export interface Config {
     version: string;
     websocketUrl: string;
     workbenchUrl: string;
+    workbench2Url?: string;
     vocabularyUrl: string;
     fileViewersConfigUrl: string;
 }
@@ -136,4 +137,4 @@ const getDefaultConfig = (): ConfigJSON => ({
 });
 
 export const DISCOVERY_URL = 'discovery/v1/apis/arvados/v1/rest';
-const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${DISCOVERY_URL}`;
+export const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${DISCOVERY_URL}?nocache=${(new Date()).getTime()}`;
index 7c1f9045b4c0f61211c2678e121a8f8694fda8a4..7107bd70823526226e45aa16687bdbcb5609b38a 100644 (file)
@@ -14,7 +14,7 @@ import { DataTableFilters } from '~/components/data-table-filters/data-table-fil
 import { MoreOptionsIcon } from '~/components/icon/icon';
 import { PaperProps } from '@material-ui/core/Paper';
 
-type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton';
+type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     searchBox: {
@@ -23,6 +23,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
         paddingTop: theme.spacing.unit * 2
     },
+    toolbarUnderTitle: {
+        paddingTop: 0
+    },
     footer: {
         overflow: 'auto'
     },
@@ -31,6 +34,11 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     moreOptionsButton: {
         padding: 0
+    },
+    title: {
+        paddingLeft: theme.spacing.unit * 3,
+        paddingTop: theme.spacing.unit * 3,
+        fontSize: '18px'
     }
 });
 
@@ -50,6 +58,7 @@ interface DataExplorerDataProps<T> {
     paperProps?: PaperProps;
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
+    title?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
 }
@@ -84,10 +93,11 @@ export const DataExplorer = withStyles(styles)(
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
-                paperKey, fetchMode, currentItemUuid
+                paperKey, fetchMode, currentItemUuid, title
             } = this.props;
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={classes.toolbar}>
+                {title && <div className={classes.title}>{title}</div>}
+                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         <div className={classes.searchBox}>
                             {!hideSearchInput && <SearchInput
index d255d14b1b7538f9bcce620ed705c827d8caef8e..3586d22de99aec68309fd10fadbc3542aa7b4412 100644 (file)
@@ -7,6 +7,7 @@ import Typography from '@material-ui/core/Typography';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
+import { Link } from 'react-router-dom';
 
 type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link';
 
@@ -49,23 +50,25 @@ interface DetailsAttributeDataProps {
     link?: string;
     children?: React.ReactNode;
     onValueClick?: () => void;
+    linkInsideCard?: string;
 }
 
 type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
 
 export const DetailsAttribute = withStyles(styles)(
-    ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue, onValueClick }: DetailsAttributeProps) =>
+    ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue, onValueClick, linkInsideCard }: DetailsAttributeProps) =>
         <Typography component="div" className={classes.attribute}>
             <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
-            { link
-                ? <a href={link} className={classes.link} target='_blank'>{value}</a>
-                : <Typography
-                    onClick={onValueClick}
-                    component="span"
-                    className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}
-                >
-                    {value}
-                    {children}
-                </Typography> }
+            {link && <a href={link} className={classes.link} target='_blank'>{value}</a>}
+            {linkInsideCard && <Link to={`/collections/${linkInsideCard}`} className={classes.link}>{value}</Link>}
+            {!link && !linkInsideCard && <Typography
+                onClick={onValueClick}
+                component="span"
+                className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
+                {value}
+                {children}
+            </Typography>
+            }
+
         </Typography>
 );
index 9f9b27ca912809f4730435382214a8d5ced02eeb..ee174b2c5325866878d6d7012552e64a1ae85478 100644 (file)
@@ -14,7 +14,6 @@ import { configureStore, RootStore } from '~/store/store';
 import { ConnectedRouter } from "react-router-redux";
 import { ApiToken } from "~/views-components/api-token/api-token";
 import { initAuth } from "~/store/auth/auth-action";
-import { configActions } from "~/store/config/config-action";
 import { createServices } from "~/services/services";
 import { MuiThemeProvider } from '@material-ui/core/styles';
 import { CustomTheme } from '~/common/custom-theme';
@@ -40,7 +39,6 @@ import { addRouteChangeHandlers } from './routes/route-change-handlers';
 import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
 import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
-import { setUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
 import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
 import { ContainerRequestState } from '~/models/container-request';
 import { MountKind } from '~/models/mount-types';
@@ -115,7 +113,8 @@ fetchConfig()
         store.dispatch(loadVocabulary);
         store.dispatch(loadFileViewersConfig);
 
-        const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} {...props} />;
+        const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={true} {...props} />;
+        const FedTokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={false} {...props} />;
         const MainPanelComponent = (props: any) => <MainPanel {...props} />;
 
         const App = () =>
@@ -125,6 +124,7 @@ fetchConfig()
                         <ConnectedRouter history={history}>
                             <Switch>
                                 <Route path={Routes.TOKEN} component={TokenComponent} />
+                                <Route path={Routes.FED_LOGIN} component={FedTokenComponent} />
                                 <Route path={Routes.ROOT} component={MainPanelComponent} />
                             </Switch>
                         </ConnectedRouter>
index e0a51550f745eeb2fd9ab125165a3ad8885b6aad..b43e84bb72147cc240a81a12cffbd223d17b32f0 100644 (file)
@@ -23,7 +23,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const projectMatch = Routes.matchProjectRoute(pathname);
     const collectionMatch = Routes.matchCollectionRoute(pathname);
     const favoriteMatch = Routes.matchFavoritesRoute(pathname);
-    const publicFavoritesMatch = Routes.matchPublicFavorites(pathname);
+    const publicFavoritesMatch = Routes.matchPublicFavoritesRoute(pathname);
     const trashMatch = Routes.matchTrashRoute(pathname);
     const processMatch = Routes.matchProcessRoute(pathname);
     const processLogMatch = Routes.matchProcessLogRoute(pathname);
@@ -46,6 +46,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const groupsMatch = Routes.matchGroupsRoute(pathname);
     const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
     const linksMatch = Routes.matchLinksRoute(pathname);
+    const collectionsContentAddressMatch = Routes.matchCollectionsContentAddressRoute(pathname);
 
     store.dispatch(dialogActions.CLOSE_ALL_DIALOGS());
     store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
@@ -105,5 +106,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(WorkbenchActions.loadGroupDetailsPanel(groupDetailsMatch.params.id));
     } else if (linksMatch) {
         store.dispatch(WorkbenchActions.loadLinks);
+    } else if (collectionsContentAddressMatch) {
+        store.dispatch(WorkbenchActions.loadCollectionContentAddress);
     }
 };
\ No newline at end of file
index ba7e2a45aff16946ad61b8a2316eba6e84dd632e..76f5c32dc192f9205674f5e52e6c88a1979cdd9a 100644 (file)
@@ -10,6 +10,7 @@ import { getCollectionUrl } from '~/models/collection';
 export const Routes = {
     ROOT: '/',
     TOKEN: '/token',
+    FED_LOGIN: '/fedtoken',
     PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
     COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
     PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
@@ -35,7 +36,8 @@ export const Routes = {
     GROUPS: '/groups',
     GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`,
     LINKS: '/links',
-    PUBLIC_FAVORITES: '/public-favorites'
+    PUBLIC_FAVORITES: '/public-favorites',
+    COLLECTIONS_CONTENT_ADDRESS: '/collections/:id',
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -143,5 +145,8 @@ export const matchGroupDetailsRoute = (route: string) =>
 export const matchLinksRoute = (route: string) =>
     matchPath(route, { path: Routes.LINKS });
 
-export const matchPublicFavorites = (route: string) =>
+export const matchPublicFavoritesRoute = (route: string) =>
     matchPath(route, { path: Routes.PUBLIC_FAVORITES });
+
+export const matchCollectionsContentAddressRoute = (route: string) =>
+    matchPath(route, { path: Routes.COLLECTIONS_CONTENT_ADDRESS });
index eae219dd0ad2a547883b94047a961d10048f5019..a80d89ba146f374f329ff62f179217997fd34e6f 100644 (file)
@@ -20,6 +20,7 @@ export const USER_IS_ADMIN = 'isAdmin';
 export const USER_IS_ACTIVE = 'isActive';
 export const USER_USERNAME = 'username';
 export const USER_PREFS = 'prefs';
+export const HOME_CLUSTER = 'homeCluster';
 
 export interface UserDetailsResponse {
     email: string;
@@ -42,6 +43,7 @@ export class AuthService {
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
+        localStorage.setItem(HOME_CLUSTER, token.split('/')[1].substr(0, 5));
     }
 
     public removeApiToken() {
@@ -52,6 +54,10 @@ export class AuthService {
         return localStorage.getItem(API_TOKEN_KEY) || undefined;
     }
 
+    public getHomeCluster() {
+        return localStorage.getItem(HOME_CLUSTER) || undefined;
+    }
+
     public getUuid() {
         return localStorage.getItem(USER_UUID_KEY) || undefined;
     }
@@ -108,9 +114,10 @@ export class AuthService {
         localStorage.removeItem(USER_PREFS);
     }
 
-    public login(uuidPrefix: string, homeCluster: string) {
+    public login(uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) {
         const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
-        window.location.assign(`https://${homeCluster}/login?remote=${uuidPrefix}&return_to=${currentUrl}`);
+        const homeClusterHost = remoteHosts[homeCluster];
+        window.location.assign(`https://${homeClusterHost}/login?${uuidPrefix !== homeCluster ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
     }
 
     public logout() {
index 5bb192b8816e6c4fc7bff110424ff7ad83617d02..b889e9cf39d7d301a0c5e8e4b8e70859e7d7c3ff 100644 (file)
@@ -68,7 +68,7 @@ const getTokenUuid = async (baseUrl: string, token: string): Promise<string> =>
     return resp.data.items[0].uuid;
 };
 
-const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
+export const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
     const shaObj = new jsSHA("SHA-1", "TEXT");
     let secret = token;
     if (token.startsWith("v2/")) {
index 7ee8399286c381287fb60b236f15ed87ea0df90b..6ca7140339f86542632e01c7616b6cec0e92073a 100644 (file)
@@ -10,10 +10,12 @@ import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
 import { User, UserResource } from "~/models/user";
 import { Session } from "~/models/session";
-import { Config } from '~/common/config';
+import { getDiscoveryURL, Config } from '~/common/config';
 import { initSessions } from "~/store/auth/auth-action-session";
 import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
 import { matchTokenRoute } from '~/routes/routes';
+import Axios from "axios";
+import { AxiosError } from "axios";
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
@@ -31,7 +33,8 @@ export const authActions = unionize({
     SET_SESSIONS: ofType<Session[]>(),
     ADD_SESSION: ofType<Session>(),
     REMOVE_SESSION: ofType<string>(),
-    UPDATE_SESSION: ofType<Session>()
+    UPDATE_SESSION: ofType<Session>(),
+    REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
 });
 
 function setAuthorizationHeader(services: ServiceRepository, token: string) {
@@ -57,17 +60,30 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () =>
 
     const user = services.authService.getUser();
     const token = services.authService.getApiToken();
+    const homeCluster = services.authService.getHomeCluster();
     if (token) {
         setAuthorizationHeader(services, token);
     }
     dispatch(authActions.CONFIG({ config }));
+    dispatch(authActions.SET_HOME_CLUSTER(homeCluster || config.uuidPrefix));
     if (token && user) {
         dispatch(authActions.INIT({ user, token }));
         dispatch<any>(initSessions(services.authService, config, user));
         dispatch<any>(getUserDetails()).then((user: User) => {
             dispatch(authActions.INIT({ user, token }));
+        }).catch((err: AxiosError) => {
+            if (err.response) {
+                // Bad token
+                if (err.response.status === 401) {
+                    logout()(dispatch, getState, services);
+                }
+            }
         });
     }
+    Object.keys(config.remoteHosts).map((k) => {
+        Axios.get<Config>(getDiscoveryURL(config.remoteHosts[k]))
+            .then(response => dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config: response.data })));
+    });
 };
 
 export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -81,8 +97,8 @@ export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: (
     dispatch(authActions.SAVE_USER(user));
 };
 
-export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    services.authService.login(uuidPrefix, homeCluster);
+export const login = (uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.login(uuidPrefix, homeCluster, remoteHosts);
     dispatch(authActions.LOGIN());
 };
 
index c87fc5ddb15320eb89357022870c7a649b87ab0f..cded9f0e71816636ce1876d51c83d3976abb589c 100644 (file)
@@ -7,6 +7,7 @@ import { User, UserResource } from "~/models/user";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
 import { Session } from "~/models/session";
+import { Config } from '~/common/config';
 
 export interface AuthState {
     user?: User;
@@ -16,6 +17,7 @@ export interface AuthState {
     localCluster: string;
     homeCluster: string;
     remoteHosts: { [key: string]: string };
+    remoteHostsConfig: { [key: string]: Config };
 }
 
 const initialState: AuthState = {
@@ -25,7 +27,8 @@ const initialState: AuthState = {
     sessions: [],
     localCluster: "",
     homeCluster: "",
-    remoteHosts: {}
+    remoteHosts: {},
+    remoteHostsConfig: {}
 };
 
 export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
@@ -44,6 +47,12 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
                 homeCluster: config.uuidPrefix
             };
         },
+        REMOTE_CLUSTER_CONFIG: ({ config }) => {
+            return {
+                ...state,
+                remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config },
+            };
+        },
         INIT: ({ user, token }) => {
             return { ...state, user, apiToken: token, homeCluster: user.uuid.substr(0, 5) };
         },
index cd4264710cb8feb93e264a3cfb90c78fd9cc8bce..b1dd8389611611940f2c270889dbaf5663bb3a25 100644 (file)
@@ -7,7 +7,7 @@ import { loadCollectionFiles } from "./collection-panel-files/collection-panel-f
 import { CollectionResource } from '~/models/collection';
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "~/models/tree";
-import { RootState } from "../store";
+import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { TagProperty } from "~/models/tag";
 import { snackbarActions } from "../snackbar/snackbar-actions";
@@ -15,6 +15,7 @@ import { resourcesActions } from "~/store/resources/resources-actions";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { navigateTo } from '~/store/navigation/navigation-action';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const collectionPanelActions = unionize({
     SET_COLLECTION: ofType<CollectionResource>(),
@@ -31,6 +32,7 @@ export const loadCollectionPanel = (uuid: string) =>
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
         const collection = await services.collectionService.get(uuid);
+        dispatch(loadDetailsPanel(collection.uuid));
         dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection }));
         dispatch(resourcesActions.SET_RESOURCES([collection]));
         dispatch<any>(loadCollectionFiles(collection.uuid));
index b75de94a7e890ac74871b496b565bf270c3d861e..534d70d480e2fcf263db814a1e46b539619ababd 100644 (file)
@@ -77,8 +77,8 @@ export const openFileRemoveDialog = (filePath: string) =>
                 ? 'Are you sure you want to remove this directory?'
                 : 'Are you sure you want to remove this file?';
             const info = isDirectory
-                ? 'Removing files will change content adress.'
-                : 'Removing a file will change content adress.';
+                ? 'Removing files will change content address.'
+                : 'Removing a file will change content address.';
 
             dispatch(dialogActions.OPEN_DIALOG({
                 id: FILE_REMOVE_DIALOG,
@@ -101,7 +101,7 @@ export const openMultipleFilesRemoveDialog = () =>
         data: {
             title: 'Removing files',
             text: 'Are you sure you want to remove selected files?',
-            info: 'Removing files will change content adress.',
+            info: 'Removing files will change content address.',
             confirmButtonLabel: 'Remove'
         }
     });
index 55829cb5d094b8e4b9dbddd313dffa256f78eb6f..f09b019873e98e09b082a638a3f12a5b0eea93b2 100644 (file)
@@ -17,5 +17,5 @@ export const collectionPanelReducer = (state: CollectionPanelState = initialStat
     collectionPanelActions.match(action, {
         default: () => state,
         SET_COLLECTION: (item) => ({ ...state, item }),
-        LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
+        LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item })
     });
diff --git a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
new file mode 100644 (file)
index 0000000..642e7b8
--- /dev/null
@@ -0,0 +1,137 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { resourcesActions } from '~/store/resources/resources-actions';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { SortDirection } from '~/components/data-table/data-column';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { FavoritePanelColumnNames } from '~/views/favorite-panel/favorite-panel';
+import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { collectionsContentAddressActions } from './collections-content-address-panel-actions';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { updateFavorites } from '~/store/favorites/favorites-actions';
+import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
+import { setBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { ResourceKind, extractUuidKind } from '~/models/resource';
+import { ownerNameActions } from '~/store/owner-name/owner-name-actions';
+
+export class CollectionsWithSameContentAddressMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+        if (!dataExplorer) {
+            api.dispatch(collectionPanelDataExplorerIsNotSet());
+        } else {
+            const sortColumn = getSortColumn(dataExplorer);
+
+            const contentOrder = new OrderBuilder<GroupContentsResource>();
+
+            if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+                const direction = sortColumn.sortDirection === SortDirection.ASC
+                    ? OrderDirection.ASC
+                    : OrderDirection.DESC;
+
+                contentOrder
+                    .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION);
+            }
+            try {
+                api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+                const userUuid = api.getState().auth.user!.uuid;
+                const pathname = api.getState().router.location!.pathname;
+                const contentAddress = pathname.split('/')[2];
+                const response = await this.services.collectionService.list({
+                    limit: dataExplorer.rowsPerPage,
+                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    filters: new FilterBuilder()
+                        .addEqual('portableDataHash', contentAddress)
+                        .addILike("name", dataExplorer.searchValue)
+                        .getFilters()
+                });
+                const userUuids = response.items.map(it => {
+                    if (extractUuidKind(it.ownerUuid) === ResourceKind.USER) {
+                        return it.ownerUuid;
+                    } else {
+                        return '';
+                    }
+                }
+                );
+                const groupUuids = response.items.map(it => {
+                    if (extractUuidKind(it.ownerUuid) === ResourceKind.GROUP) {
+                        return it.ownerUuid;
+                    } else {
+                        return '';
+                    }
+                });
+                const responseUsers = await this.services.userService.list({
+                    limit: dataExplorer.rowsPerPage,
+                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    filters: new FilterBuilder()
+                        .addIn('uuid', userUuids)
+                        .getFilters()
+                });
+                const responseGroups = await this.services.groupsService.list({
+                    limit: dataExplorer.rowsPerPage,
+                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    filters: new FilterBuilder()
+                        .addIn('uuid', groupUuids)
+                        .getFilters()
+                });
+                responseUsers.items.map(it=>{
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid}));
+                });
+                responseGroups.items.map(it=>{
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: `Project: ${it.name}`, uuid: it.uuid}));
+                });
+                api.dispatch<any>(setBreadcrumbs([{ label: 'Projects', uuid: userUuid }]));
+                api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+                api.dispatch<any>(updatePublicFavorites(response.items.map(item => item.uuid)));
+                if (response.itemsAvailable === 1) {
+                    api.dispatch<any>(navigateTo(response.items[0].uuid));
+                    api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                } else {
+                    api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                    api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+                    api.dispatch(collectionsContentAddressActions.SET_ITEMS({
+                        items: response.items.map((resource: any) => resource.uuid),
+                        itemsAvailable: response.itemsAvailable,
+                        page: Math.floor(response.offset / response.limit),
+                        rowsPerPage: response.limit
+                    }));
+                }
+            } catch (e) {
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                api.dispatch(collectionsContentAddressActions.SET_ITEMS({
+                    items: [],
+                    itemsAvailable: 0,
+                    page: 0,
+                    rowsPerPage: dataExplorer.rowsPerPage
+                }));
+                api.dispatch(couldNotFetchCollections());
+            }
+        }
+    }
+}
+
+const collectionPanelDataExplorerIsNotSet = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Collection panel is not ready.',
+        kind: SnackbarKind.ERROR
+    });
+
+const couldNotFetchCollections = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch collection with this content address.',
+        kind: SnackbarKind.ERROR
+    });
\ No newline at end of file
diff --git a/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts b/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts
new file mode 100644 (file)
index 0000000..11f1a8c
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+
+export const COLLECTIONS_CONTENT_ADDRESS_PANEL_ID = 'collectionsContentAddressPanel';
+
+export const collectionsContentAddressActions = bindDataExplorerActions(COLLECTIONS_CONTENT_ADDRESS_PANEL_ID);
+
+export const loadCollectionsContentAddressPanel = () =>
+    (dispatch: Dispatch) => {
+        dispatch(collectionsContentAddressActions.REQUEST_ITEMS());
+    };
index b21ca0a7688ae324f09edc6f30e294b86665cb9c..431d15e8495c87e311678c66da64e6f6ec23524b 100644 (file)
@@ -152,12 +152,13 @@ export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>,
 export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const res = getResource<ProjectResource>(projectUuid)(getState().resources);
+        const isAdmin = getState().auth.user!.isAdmin;
         if (res) {
             dispatch<any>(openContextMenu(event, {
                 name: res.name,
                 uuid: res.uuid,
                 kind: res.kind,
-                menuKind: ContextMenuKind.PROJECT,
+                menuKind: !isAdmin ? ContextMenuKind.PROJECT : ContextMenuKind.PROJECT_ADMIN,
                 ownerUuid: res.ownerUuid,
                 isTrashed: res.isTrashed
             }));
index e518482b503905d20a455120528fa13681700a64..868d7b05753d0f0b7d9dc0dc006a1f1dd70a639a 100644 (file)
@@ -56,14 +56,14 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
             }
             try {
                 api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
-                const resp1 = await this.services.linkService.list({
+                const responseLinks = await this.services.linkService.list({
                     filters: new FilterBuilder()
                         .addEqual("linkClass", 'star')
                         .addEqual('tailUuid', this.services.authService.getUuid()!)
                         .addEqual('tailKind', ResourceKind.USER)
                         .getFilters()
                 }).then(results => results);
-                const uuids = resp1.items.map(it => it.headUuid);
+                const uuids = responseLinks.items.map(it => it.headUuid);
                 const groupItems: any = await this.services.groupsService.list({
                     filters: new FilterBuilder()
                         .addIn("uuid", uuids)
@@ -94,7 +94,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                     response.itemsAvailable++;
                     response.items.push(it);
                 });
-
+                
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
index 3bec160992a92ae4342e3789321e57cf7ab2471c..6d393f03ce8538a44670bfe3f65ca7f73e9d71d9 100644 (file)
@@ -100,3 +100,5 @@ export const navigateToGroups = push(Routes.GROUPS);
 export const navigateToGroupDetails = compose(push, getGroupUrl);
 
 export const navigateToLinks = push(Routes.LINKS);
+
+export const navigateToCollectionsContentAddress = push(Routes.COLLECTIONS_CONTENT_ADDRESS);
diff --git a/src/store/owner-name/owner-name-actions.ts b/src/store/owner-name/owner-name-actions.ts
new file mode 100644 (file)
index 0000000..6c2784a
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+
+export const ownerNameActions = unionize({
+    SET_OWNER_NAME: ofType<OwnerNameState>()
+});
+
+interface OwnerNameState {
+    name: string;
+    uuid: string;
+}
+
+export type OwnerNameAction = UnionOf<typeof ownerNameActions>;
diff --git a/src/store/owner-name/owner-name-reducer.ts b/src/store/owner-name/owner-name-reducer.ts
new file mode 100644 (file)
index 0000000..58df209
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ownerNameActions, OwnerNameAction } from './owner-name-actions';
+
+export const ownerNameReducer = (state = [], action: OwnerNameAction) =>
+    ownerNameActions.match(action, {
+        SET_OWNER_NAME: data => [...state, { uuid: data.uuid, name: data.name }],
+        default: () => state,
+    });
\ No newline at end of file
index 365e07aab61cd662e427d5bb6e05c1998ca693ee..8876be0f252afedd7e1dd0b752a6b4eba8981cf8 100644 (file)
@@ -11,6 +11,8 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
 
 export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
 
@@ -24,11 +26,14 @@ export const openMoveProjectDialog = (resource: { name: string, uuid: string })
 
 export const moveProject = (resource: MoveToFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUuid = getState().auth.user!.uuid;
         dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
         try {
             const project = await services.projectService.get(resource.uuid);
             const newProject = await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
+            dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
+            await dispatch<any>(loadSidePanelTreeProjects(userUuid));
             return newProject;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
index 321b85548610298005299b3f64b5911d11437113..b92069762a0e300e62044f0f289510fae68911ae 100644 (file)
@@ -11,6 +11,7 @@ import { ServiceRepository } from "~/services/services";
 import { ProjectResource } from '~/models/project';
 import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 import { getResource } from '~/store/resources/resources';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -33,6 +34,7 @@ export const updateProject = (project: Partial<ProjectResource>) =>
         dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
         try {
             const updatedProject = await services.projectService.update(uuid, project);
+            dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
             return updatedProject;
         } catch (e) {
index c7bbf8dd1e957bdc651627ffcb677e73664c7aa6..be7f5285953df1fcf0e2feb36b01792a95cd3174 100644 (file)
@@ -20,7 +20,6 @@ import { serializeSimpleObjectTypeFilters } from '../resource-type-filters/resou
 import { LinkResource, LinkClass } from '~/models/link';
 import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
-import { loadMissingProcessesInformation } from '~/store/project-panel/project-panel-middleware-service';
 import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
 
 export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareService {
@@ -56,7 +55,7 @@ export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareServ
                 api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const uuidPrefix = api.getState().config.uuidPrefix;
                 const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
-                const response = await this.services.linkService.list({
+                const responseLinks = await this.services.linkService.list({
                     limit: dataExplorer.rowsPerPage,
                     offset: dataExplorer.page * dataExplorer.rowsPerPage,
                     filters: new FilterBuilder()
@@ -66,15 +65,46 @@ export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareServ
                         .addIsA("headUuid", typeFilters)
                         .getFilters()
                 });
+                const uuids = responseLinks.items.map(it => it.headUuid);
+                const groupItems: any = await this.services.groupsService.list({
+                    filters: new FilterBuilder()
+                        .addIn("uuid", uuids)
+                        .addILike("name", dataExplorer.searchValue)
+                        .addIsA("uuid", typeFilters)
+                        .getFilters()
+                });
+                const collectionItems: any = await this.services.collectionService.list({
+                    filters: new FilterBuilder()
+                        .addIn("uuid", uuids)
+                        .addILike("name", dataExplorer.searchValue)
+                        .addIsA("uuid", typeFilters)
+                        .getFilters()
+                });
+                const processItems: any = await this.services.containerRequestService.list({
+                    filters: new FilterBuilder()
+                        .addIn("uuid", uuids)
+                        .addILike("name", dataExplorer.searchValue)
+                        .addIsA("uuid", typeFilters)
+                        .getFilters()
+                });
+                const response = groupItems;
+                collectionItems.items.map((it: any) => {
+                    response.itemsAvailable++;
+                    response.items.push(it);
+                });
+                processItems.items.map((it: any) => {
+                    response.itemsAvailable++;
+                    response.items.push(it);
+                });
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 api.dispatch(publicFavoritePanelActions.SET_ITEMS({
-                    items: response.items.map(resource => resource.uuid),
+                    items: response.items.map((resource: any) => resource.uuid),
                     itemsAvailable: response.itemsAvailable,
                     page: Math.floor(response.offset / response.limit),
                     rowsPerPage: response.limit
                 }));
-                api.dispatch<any>(updatePublicFavorites(response.items.map(item => item.headUuid)));
+                api.dispatch<any>(updatePublicFavorites(response.items.map((item: any) => item.uuid)));
             } catch (e) {
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(publicFavoritePanelActions.SET_ITEMS({
index b5aa9fcef70d202bd0ddb2244577b2de86020ebb..50b9070baf0987ab6ec64c4624cf301dee96d02d 100644 (file)
@@ -9,8 +9,6 @@ import { checkPublicFavorite } from "./public-favorites-reducer";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { ServiceRepository } from "~/services/services";
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { getResource } from '~/store/resources/resources';
-import { LinkResource } from "~/models/link";
 
 export const publicFavoritesActions = unionize({
     TOGGLE_PUBLIC_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -69,12 +67,6 @@ export const updatePublicFavorites = (resourceUuids: string[]) =>
             });
     };
 
-export const getHeadUuid = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const resource = getResource<LinkResource>(uuid)(getState().resources);
-        return resource!.headUuid;
-    };
-
 export const getIsAdmin = () =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const resource = getState().auth.user!.isAdmin;
index 6a37572bc553254eb9c6e1446f6f2d204c658223..8a2ca2400cb1cbd3f1229cce96294291e91c8d66 100644 (file)
@@ -63,6 +63,9 @@ import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel
 import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
 import { publicFavoritesReducer } from '~/store/public-favorites/public-favorites-reducer';
 import { linkAccountPanelReducer } from './link-account-panel/link-account-panel-reducer';
+import { CollectionsWithSameContentAddressMiddlewareService } from '~/store/collections-content-address-panel/collections-content-address-middleware-service';
+import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+import { ownerNameReducer } from '~/store/owner-name/owner-name-reducer';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -116,6 +119,10 @@ export function configureStore(history: History, services: ServiceRepository): R
     const publicFavoritesMiddleware = dataExplorerMiddleware(
         new PublicFavoritesMiddlewareService(services, PUBLIC_FAVORITE_PANEL_ID)
     );
+    const collectionsContentAddress = dataExplorerMiddleware(
+        new CollectionsWithSameContentAddressMiddlewareService(services, COLLECTIONS_CONTENT_ADDRESS_PANEL_ID)
+    );
+
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
@@ -131,7 +138,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         linkPanelMiddleware,
         computeNodeMiddleware,
         apiClientAuthorizationMiddlewareService,
-        publicFavoritesMiddleware
+        publicFavoritesMiddleware,
+        collectionsContentAddress
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
@@ -147,6 +155,7 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     detailsPanel: detailsPanelReducer,
     dialog: dialogReducer,
     favorites: favoritesReducer,
+    ownerName: ownerNameReducer,
     publicFavorites: publicFavoritesReducer,
     form: formReducer,
     processLogsPanel: processLogsPanelReducer,
index 693a0ece7e9197a5ff494583cee4660ebd7f2a8d..b810b1e490a8aa61fc9a6abf792dbd92cc285954 100644 (file)
@@ -29,6 +29,7 @@ export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed:
             } else {
                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO }));
                 await services.groupsService.trash(uuid);
+                dispatch(projectPanelActions.REQUEST_ITEMS());
                 dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: "Added to trash",
index bc4bb130ae39797160eb3200c53adad51b30e101..cffcce4d65d9d3f362a80651aecbfeacaaaa1e3e 100644 (file)
@@ -27,24 +27,43 @@ export class UserMiddlewareService extends DataExplorerMiddlewareService {
         const state = api.getState();
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         try {
-            const response = await this.services.userService.list(getParams(dataExplorer));
-            api.dispatch(updateResources(response.items));
-            api.dispatch(setItems(response));
+            const responseFirstName = await this.services.userService.list(getParamsFirstName(dataExplorer));
+            if (responseFirstName.itemsAvailable) {
+                api.dispatch(updateResources(responseFirstName.items));
+                api.dispatch(setItems(responseFirstName));
+            } else {
+                const responseLastName = await this.services.userService.list(getParamsLastName(dataExplorer));
+                api.dispatch(updateResources(responseLastName.items));
+                api.dispatch(setItems(responseLastName));
+            }
         } catch {
             api.dispatch(couldNotFetchUsers());
         }
     }
 }
 
-export const getParams = (dataExplorer: DataExplorer) => ({
+const getParamsFirstName = (dataExplorer: DataExplorer) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
-    filters: getFilters(dataExplorer)
+    filters: getFiltersFirstName(dataExplorer)
 });
 
-export const getFilters = (dataExplorer: DataExplorer) => {
+const getParamsLastName = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer),
+    filters: getFiltersLastName(dataExplorer)
+});
+
+const getFiltersFirstName = (dataExplorer: DataExplorer) => {
+    const filters = new FilterBuilder()
+        .addILike("firstName", dataExplorer.searchValue)
+        .getFilters();
+    return filters;
+};
+
+const getFiltersLastName = (dataExplorer: DataExplorer) => {
     const filters = new FilterBuilder()
-        .addILike("username", dataExplorer.searchValue)
+        .addILike("lastName", dataExplorer.searchValue)
         .getFilters();
     return filters;
 };
index 066aa80b40267225d12eaf2fe10c4169aad14ddd..caf466f7ed89bb3937bae4e6a5882e77fd0aa592 100644 (file)
@@ -12,7 +12,7 @@ import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions
 import { UserResource } from "~/models/user";
 import { getResource } from '~/store/resources/resources';
 import { navigateToProject, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
-import { saveApiToken, getUserDetails } from '~/store/auth/auth-action';
+import { saveApiToken } from '~/store/auth/auth-action';
 
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
index 0cffcaceaeaaef03af2c5b1e942146bd81ba832b..27ac76f335198e75bc37bcbc30df330d39226cbd 100644 (file)
@@ -69,8 +69,7 @@ import { FilterBuilder } from '~/services/api/filter-builder';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import { MatchCases, ofType, unionize, UnionOf } from '~/common/unionize';
 import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
-import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
+import { collectionPanelActions, loadCollectionPanel } from "~/store/collection-panel/collection-panel-action";
 import { CollectionResource } from "~/models/collection";
 import {
     loadSearchResultsPanel,
@@ -96,6 +95,8 @@ import { DataTableFetchMode } from "~/components/data-table/data-table";
 import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
 import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
 import { USER_LINK_ACCOUNT_KEY } from '~/services/link-account-service/link-account-service';
+import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+import { collectionContentAddressPanelColumns } from '~/views/collection-content-address-panel/collection-content-address-panel';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -135,6 +136,7 @@ export const loadWorkbench = () =>
             dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
             dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+            dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
 
             if (sessionStorage.getItem(USER_LINK_ACCOUNT_KEY)) {
                 dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
@@ -160,6 +162,11 @@ export const loadFavorites = () =>
             dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
         });
 
+export const loadCollectionContentAddress = handleFirstTimeLoad(
+    async (dispatch: Dispatch<any>) => {
+        await dispatch(loadCollectionsContentAddressPanel());
+    });
+
 export const loadTrash = () =>
     handleFirstTimeLoad(
         (dispatch: Dispatch) => {
@@ -263,21 +270,21 @@ export const loadCollection = (uuid: string) =>
                         dispatch(updateResources([collection]));
                         await dispatch(activateSidePanelTreeItem(collection.ownerUuid));
                         dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
-                        dispatch(loadCollectionFiles(collection.uuid));
+                        dispatch(loadCollectionPanel(collection.uuid));
                     },
                     SHARED: collection => {
                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
                         dispatch(updateResources([collection]));
                         dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
                         dispatch(activateSidePanelTreeItem(collection.ownerUuid));
-                        dispatch(loadCollectionFiles(collection.uuid));
+                        dispatch(loadCollectionPanel(collection.uuid));
                     },
                     TRASHED: collection => {
                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
                         dispatch(updateResources([collection]));
                         dispatch(setTrashBreadcrumbs(''));
                         dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
-                        dispatch(loadCollectionFiles(collection.uuid));
+                        dispatch(loadCollectionPanel(collection.uuid));
                     },
 
                 });
index b0fd03134c613fff4a3be001fd88cd8c02d5e73c..2f8d87e1ede0d1b95fe688738583605136254904 100644 (file)
@@ -17,6 +17,7 @@ import { getAccountLinkData } from "~/store/link-account-panel/link-account-pane
 interface ApiTokenProps {
     authService: AuthService;
     config: Config;
+    loadMainApp: boolean;
 }
 
 export const ApiToken = connect()(
@@ -24,20 +25,23 @@ export const ApiToken = connect()(
         componentDidMount() {
             const search = this.props.location ? this.props.location.search : "";
             const apiToken = getUrlParameter(search, 'api_token');
+            const loadMainApp = this.props.loadMainApp;
             this.props.dispatch(saveApiToken(apiToken));
             this.props.dispatch<any>(getUserDetails()).then((user: User) => {
                 this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
             }).finally(() => {
+                if (loadMainApp) {
                 if (this.props.dispatch(getAccountLinkData())) {
                     this.props.dispatch(navigateToLinkAccount);
                 }
                 else {
                     this.props.dispatch(navigateToRootProject);
                 }
+                }
             });
         }
         render() {
-            return <div/>;
+            return <div />;
         }
     }
 );
index 04dbfa7c03ae05f40cf5a42003728d2ce91cddbd..bf504f2b46b82e4efa475c1359cdf1a7c2c55f9e 100644 (file)
@@ -3,8 +3,8 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Button } from '@material-ui/core';
-import { FavoriteStar } from '../favorite-star/favorite-star';
+import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
+import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { ResourceKind, TrashableResource } from '~/models/resource';
 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
@@ -27,19 +27,20 @@ import { LinkResource } from '~/models/link';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { withResourceData } from '~/views-components/data-explorer/with-resources';
 
-const renderName = (item: { name: string; uuid: string, kind: string }) =>
+const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item.kind)}
         </Grid>
         <Grid item>
-            <Typography color="primary" style={{ width: 'auto' }}>
+            <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
                 {item.name}
             </Typography>
         </Grid>
         <Grid item>
             <Typography variant="caption">
                 <FavoriteStar resourceUuid={item.uuid} />
+                <PublicFavoriteStar resourceUuid={item.uuid} />
             </Typography>
         </Grid>
     </Grid>;
@@ -48,7 +49,7 @@ export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return resource || { name: '', uuid: '', kind: '' };
-    })(renderName);
+    })((resource: { name: string; uuid: string, kind: string } & DispatchProp<any>) => renderName(resource.dispatch, resource));
 
 const renderIcon = (kind: string) => {
     switch (kind) {
@@ -326,35 +327,6 @@ export const ResourceLinkUuid = connect(
         return resource || { uuid: '' };
     })(renderUuid);
 
-const renderLinkNameAndIcon = (item: { name: string; headUuid: string, headKind: string }) =>
-    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
-        <Grid item>
-            {renderIcon(item.headKind)}
-        </Grid>
-        <Grid item>
-            <Typography color="primary" style={{ width: 'auto' }}>
-                {item.name}
-            </Typography>
-        </Grid>
-        <Grid item>
-            <Typography variant="caption">
-                <FavoriteStar resourceUuid={item.headUuid} />
-            </Typography>
-        </Grid>
-    </Grid>;
-
-export const ResourceLinkNameAndIcon = connect(
-    (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<LinkResource>(props.uuid)(state.resources);
-        return resource || { name: '', headUuid: '', headKind: '' };
-    })(renderLinkNameAndIcon);
-
-export const ResourceLinkType = connect(
-    (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<LinkResource>(props.uuid)(state.resources);
-        return { type: resource ? resource.headKind : '' };
-    })((props: { type: string }) => renderType(props.type));
-
 // Process Resources
 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
     return (
@@ -429,7 +401,7 @@ export const ResourceFileSize = connect(
     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 
 const renderOwner = (owner: string) =>
-    <Typography noWrap color="primary" >
+    <Typography noWrap>
         {owner}
     </Typography>;
 
@@ -439,6 +411,14 @@ export const ResourceOwner = connect(
         return { owner: resource ? resource.ownerUuid : '' };
     })((props: { owner: string }) => renderOwner(props.owner));
 
+export const ResourceOwnerName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+        const ownerNameState = state.ownerName;
+        const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
+        return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
+    })((props: { owner: string }) => renderOwner(props.owner));
+
 const renderType = (type: string) =>
     <Typography noWrap>
         {resourceLabel(type)}
index 98fa388640757d4bd5a09d9cfbdd9d806a617b47..ec5bdabd833a097cac06b3eda5c30805ac5193be 100644 (file)
@@ -24,9 +24,8 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
             <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
             <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
-            {/* Links but we dont have view */}
-            <DetailsAttribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
-            <DetailsAttribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
+            <DetailsAttribute label='Collection UUID' linkInsideCard={this.item.uuid} value={this.item.uuid} />
+            <DetailsAttribute label='Content address' linkInsideCard={this.item.portableDataHash} value={this.item.portableDataHash} />
             {/* Missing attrs */}
             <DetailsAttribute label='Number of files' value={this.data && this.data.fileCount} />
             <DetailsAttribute label='Content size' value={formatFileSize(this.data && this.data.fileSize)} />
index 2a30ae4783d75d426d29bc82574cd19a086058b3..9ce84867d736d5061d9461be923346c9dc3da604 100644 (file)
@@ -85,7 +85,7 @@ const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPan
     const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
     return {
         isOpened: detailsPanel.isOpened,
-        item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData)
+        item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData),
     };
 };
 
index cf744886306ffc2d20ec1fb638434e748c6a2333..0598e5f5226e104555f3d143335015f47b2514d3 100644 (file)
@@ -6,7 +6,7 @@ import * as React from "react";
 import { FavoriteIcon, PublicFavoriteIcon } from "~/components/icon/icon";
 import { connect } from "react-redux";
 import { RootState } from "~/store/store";
-import { withStyles, StyleRulesCallback, WithStyles } from "@material-ui/core";
+import { withStyles, StyleRulesCallback, WithStyles, Tooltip } from "@material-ui/core";
 
 type CssRules = "icon";
 
@@ -23,11 +23,18 @@ const mapStateToProps = (state: RootState, props: { resourceUuid: string; classN
 });
 
 export const FavoriteStar = connect(mapStateToProps)(
-    withStyles(styles)((props: { isFavoriteVisible: boolean; isPublicFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
+    withStyles(styles)((props: { isFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
+        if (props.isFavoriteVisible) {
+            return <Tooltip enterDelay={500} title="Favorite"><FavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
+        } else {
+            return null;
+        }
+    }));
+
+export const PublicFavoriteStar = connect(mapStateToProps)(
+    withStyles(styles)((props: { isPublicFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
         if (props.isPublicFavoriteVisible) {
-            return <PublicFavoriteIcon className={props.className || props.classes.icon} />;
-        } else if (props.isFavoriteVisible) {
-            return <FavoriteIcon className={props.className || props.classes.icon} />;
+            return <Tooltip enterDelay={500} title="Public Favorite"><PublicFavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
         } else {
             return null;
         }
index 5ffc42158acb1c9f5fbc04cd0d1f6aedaa7cfdad..15329a43e9119850a8fce69b759319c7f5f69015 100644 (file)
@@ -11,6 +11,6 @@ export const AnonymousMenu = connect()(
     ({ dispatch }: DispatchProp<any>) =>
         <Button
             color="inherit"
-            onClick={() => dispatch(login("", ""))}>
+            onClick={() => dispatch(login("", "", {}))}>
             Sign in
         </Button>);
index 653ae604e524574a30009c47c6cb944003d347e9..f4969f5cf4e0de16fc3a804c4740f7fb2ac0bfd8 100644 (file)
@@ -32,8 +32,8 @@ export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerPro
     return <div>
         <HomeTreePicker pickerId={home} {...p} />
         <SharedTreePicker pickerId={shared} {...p} />
-        <FavoritesTreePicker pickerId={favorites} {...p} />
         <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
+        <FavoritesTreePicker pickerId={favorites} {...p} />  
     </div>;
 };
 
diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx
new file mode 100644 (file)
index 0000000..b652b50
--- /dev/null
@@ -0,0 +1,154 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, Grid, Button } from '@material-ui/core';
+import { CollectionIcon } from '~/components/icon/icon';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { BackIcon } from '~/components/icon/icon';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { Dispatch } from 'redux';
+import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
+import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { ResourceKind } from '~/models/resource';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { connect } from 'react-redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { createTree } from '~/models/tree';
+import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate } from '~/views-components/data-explorer/renderers';
+
+type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    backLink: {
+        fontSize: '14px',
+        fontWeight: 600,
+        display: 'flex',
+        alignItems: 'center',
+        padding: theme.spacing.unit,
+        marginBottom: theme.spacing.unit,
+        color: theme.palette.grey["700"],
+    },
+    backIcon: {
+        marginRight: theme.spacing.unit
+    },
+    card: {
+        width: '100%'
+    },
+    title: {
+        color: theme.palette.grey["700"]
+    },
+    iconHeader: {
+        fontSize: '1.875rem',
+        color: theme.customs.colors.green700
+    },
+    link: {
+        fontSize: '0.875rem',
+        color: theme.palette.primary.main,
+        textAlign: 'right',
+        '&:hover': {
+            cursor: 'pointer'
+        }
+    }
+});
+
+enum CollectionContentAddressPanelColumnNames {
+    COLLECTION_WITH_THIS_ADDRESS = "Collection with this address",
+    LOCATION = "Location",
+    LAST_MODIFIED = "Last modified"
+}
+
+export const collectionContentAddressPanelColumns: DataColumns<string> = [
+    {
+        name: CollectionContentAddressPanelColumnNames.COLLECTION_WITH_THIS_ADDRESS,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceName uuid={uuid} />
+    },
+    {
+        name: CollectionContentAddressPanelColumnNames.LOCATION,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceOwnerName uuid={uuid} />
+    },
+    {
+        name: CollectionContentAddressPanelColumnNames.LAST_MODIFIED,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.DESC,
+        filters: createTree(),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />
+    }
+];
+
+export interface CollectionContentAddressPanelActionProps {
+    onContextMenu: (event: React.MouseEvent<any>, uuid: string) => void;
+    onItemClick: (item: string) => void;
+    onItemDoubleClick: (item: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressPanelActionProps => ({
+    onContextMenu: (event, resourceUuid) => {
+        const isAdmin = dispatch<any>(getIsAdmin());
+        const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+        if (kind) {
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: resourceUuid,
+                ownerUuid: '',
+                kind: ResourceKind.NONE,
+                menuKind: kind
+            }));
+        }
+        dispatch<any>(loadDetailsPanel(resourceUuid));
+    },
+    onItemClick: (uuid: string) => {
+        dispatch<any>(loadDetailsPanel(uuid));
+    },
+    onItemDoubleClick: uuid => {
+        dispatch<any>(navigateTo(uuid));
+    }
+});
+
+interface CollectionContentAddressDataProps {
+    match: {
+        params: { id: string }
+    };
+}
+
+export const CollectionsContentAddressPanel = withStyles(styles)(
+    connect(null, mapDispatchToProps)(
+        class extends React.Component<CollectionContentAddressPanelActionProps & CollectionContentAddressDataProps & WithStyles<CssRules>> {
+            render() {
+                return <Grid item xs={12}>
+                    <Button
+                        onClick={() => history.back()}
+                        className={this.props.classes.backLink}>
+                        <BackIcon className={this.props.classes.backIcon} />
+                        Back
+                    </Button>
+                    <DataExplorer
+                        id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
+                        onRowClick={this.props.onItemClick}
+                        onRowDoubleClick={this.props.onItemDoubleClick}
+                        onContextMenu={this.props.onContextMenu}
+                        contextMenuColumn={true}
+                        title={`Content address: ${this.props.match.params.id}`}
+                        dataTableDefaultView={
+                            <DataTableDefaultView
+                                icon={CollectionIcon}
+                                messages={['Collections with this content address not found.']} />
+                        } />;
+                    </Grid >;
+            }
+        }
+    )
+);
index 5c19c30ba793fad59f3f837d633f162292985720..e7234009d9e3baa15442a13ffbbb4bd2d917f55b 100644 (file)
@@ -22,7 +22,6 @@ import {
     ResourceType
 } from '~/views-components/data-explorer/renderers';
 import { FavoriteIcon } from '~/components/icon/icon';
-import { Dispatch } from 'redux';
 import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
@@ -107,53 +106,54 @@ export const favoritePanelColumns: DataColumns<string> = [
 
 interface FavoritePanelDataProps {
     favorites: FavoritesState;
+    isAdmin: boolean;
 }
 
 interface FavoritePanelActionProps {
     onItemClick: (item: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
     onDialogOpen: (ownerUuid: string) => void;
     onItemDoubleClick: (item: string) => void;
 }
-const mapStateToProps = ({ favorites }: RootState): FavoritePanelDataProps => ({
-    favorites
-});
-
-const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
-    onContextMenu: (event, resourceUuid) => {
-        const kind = resourceKindToContextMenuKind(resourceUuid);
-        if (kind) {
-            dispatch<any>(openContextMenu(event, {
-                name: '',
-                uuid: resourceUuid,
-                ownerUuid: '',
-                kind: ResourceKind.NONE,
-                menuKind: kind
-            }));
-        }
-        dispatch<any>(loadDetailsPanel(resourceUuid));
-    },
-    onDialogOpen: (ownerUuid: string) => { return; },
-    onItemClick: (resourceUuid: string) => {
-        dispatch<any>(loadDetailsPanel(resourceUuid));
-    },
-    onItemDoubleClick: uuid => {
-        dispatch<any>(navigateTo(uuid));
-    }
+const mapStateToProps = (state : RootState): FavoritePanelDataProps => ({
+    favorites: state.favorites,
+    isAdmin: state.auth.user!.isAdmin
 });
 
 type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
     & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 export const FavoritePanel = withStyles(styles)(
-    connect(mapStateToProps, mapDispatchToProps)(
+    connect(mapStateToProps)(
         class extends React.Component<FavoritePanelProps> {
+
+            handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
+                if (menuKind) {
+                    this.props.dispatch<any>(openContextMenu(event, {
+                        name: '',
+                        uuid: resourceUuid,
+                        ownerUuid: '',
+                        kind: ResourceKind.NONE,
+                        menuKind
+                    }));
+                }
+                this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
+            }
+
+            handleRowDoubleClick = (uuid: string) => {
+                this.props.dispatch<any>(navigateTo(uuid));
+            }
+
+            handleRowClick = (uuid: string) => {
+                this.props.dispatch(loadDetailsPanel(uuid));
+            }
+
             render() {
                 return <DataExplorer
                     id={FAVORITE_PANEL_ID}
-                    onRowClick={this.props.onItemClick}
-                    onRowDoubleClick={this.props.onItemDoubleClick}
-                    onContextMenu={this.props.onContextMenu}
+                    onRowClick={this.handleRowClick}
+                    onRowDoubleClick={this.handleRowDoubleClick}
+                    onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
                     dataTableDefaultView={
                         <DataTableDefaultView
index b9f3194ab0b2aa12fc375a7ac5f966dc6630db1a..41a17bf9682c1b7fb86d5436097ac24673b19693 100644 (file)
@@ -93,7 +93,7 @@ export const LoginPanel = withStyles(styles)(
 
                 <Typography component="div" align="right">
                     <Button variant="contained" color="primary" style={{ margin: "1em" }} className={classes.button}
-                        onClick={() => dispatch(login(uuidPrefix, remoteHosts[homeCluster]))}>
+                        onClick={() => dispatch(login(uuidPrefix, homeCluster, remoteHosts))}>
                         Log in to {uuidPrefix}
                         {uuidPrefix !== homeCluster &&
                             <span>&nbsp;with user from {homeCluster}</span>}
index 6b2521c083e70dc01e9372f884189b1cce3cd59d..ec6a912a71f8b6dae38296539458020a651ed5cf 100644 (file)
@@ -21,7 +21,7 @@ type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'lin
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     backLink: {
-        fontSize: '1rem',
+        fontSize: '14px',
         fontWeight: 600,
         display: 'flex',
         alignItems: 'center',
@@ -72,7 +72,7 @@ export const ProcessLogMainCard = withStyles(styles)(
     ({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu, navigateToLogCollection }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
         <Grid item xs={12}>
             <Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
-                <BackIcon className={classes.backIcon} /> Back
+                <BackIcon className={classes.backIcon} /> BACK
             </Link>
             <Card className={classes.card}>
                 <CardHeader
index 4a01c6f4b0d9a46b35cc9b04b776a4ab84552ade..ab423a6e1b6c10d3106586e8e25cedf1a7235487 100644 (file)
@@ -16,8 +16,8 @@ import {
     ProcessStatus,
     ResourceFileSize,
     ResourceLastModifiedDate,
-    ResourceLinkNameAndIcon,
-    ResourceLinkType
+    ResourceType,
+    ResourceName
 } from '~/views-components/data-explorer/renderers';
 import { PublicFavoriteIcon } from '~/components/icon/icon';
 import { Dispatch } from 'redux';
@@ -31,7 +31,7 @@ import { createTree } from '~/models/tree';
 import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
 import { PublicFavoritesState } from '~/store/public-favorites/public-favorites-reducer';
-import { getHeadUuid, getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
+import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
 
 type CssRules = "toolbar" | "button";
 
@@ -45,7 +45,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
-export enum FavoritePanelColumnNames {
+export enum PublicFavoritePanelColumnNames {
     NAME = "Name",
     STATUS = "Status",
     TYPE = "Type",
@@ -60,12 +60,12 @@ export interface FavoritePanelFilter extends DataTableFilterItem {
 
 export const publicFavoritePanelColumns: DataColumns<string> = [
     {
-        name: FavoritePanelColumnNames.NAME,
+        name: PublicFavoritePanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ResourceLinkNameAndIcon uuid={uuid} />
+        render: uuid => <ResourceName uuid={uuid} />
     },
     {
         name: "Status",
@@ -75,21 +75,21 @@ export const publicFavoritePanelColumns: DataColumns<string> = [
         render: uuid => <ProcessStatus uuid={uuid} />
     },
     {
-        name: FavoritePanelColumnNames.TYPE,
+        name: PublicFavoritePanelColumnNames.TYPE,
         selected: true,
         configurable: true,
         filters: getSimpleObjectTypeFilters(),
-        render: uuid => <ResourceLinkType uuid={uuid} />
+        render: uuid => <ResourceType uuid={uuid} />
     },
     {
-        name: FavoritePanelColumnNames.FILE_SIZE,
+        name: PublicFavoritePanelColumnNames.FILE_SIZE,
         selected: true,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
     {
-        name: FavoritePanelColumnNames.LAST_MODIFIED,
+        name: PublicFavoritePanelColumnNames.LAST_MODIFIED,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.DESC,
@@ -115,24 +115,24 @@ const mapStateToProps = ({ publicFavorites }: RootState): PublicFavoritePanelDat
 const mapDispatchToProps = (dispatch: Dispatch): PublicFavoritePanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
         const isAdmin = dispatch<any>(getIsAdmin());
-        const kind = resourceKindToContextMenuKind(dispatch<any>(getHeadUuid(resourceUuid)), isAdmin);
+        const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
-                uuid: dispatch<any>(getHeadUuid(resourceUuid)),
+                uuid: resourceUuid,
                 ownerUuid: '',
                 kind: ResourceKind.NONE,
                 menuKind: kind
             }));
         }
-        dispatch<any>(loadDetailsPanel(dispatch<any>(getHeadUuid(resourceUuid))));
+        dispatch<any>(loadDetailsPanel(resourceUuid));
     },
     onDialogOpen: (ownerUuid: string) => { return; },
     onItemClick: (uuid: string) => {
-        dispatch<any>(loadDetailsPanel(dispatch<any>(getHeadUuid(uuid))));
+        dispatch<any>(loadDetailsPanel(uuid));
     },
     onItemDoubleClick: uuid => {
-        dispatch<any>(navigateTo(dispatch<any>(getHeadUuid(uuid))));
+        dispatch<any>(navigateTo(uuid));
     }
 });
 
index 368a0d6449171c5a7efdc592ae5ad365ad83514f..72a9b282824888e1142c5b85edea3b586ef36890 100644 (file)
@@ -21,6 +21,9 @@ import {
 } from '~/views-components/data-explorer/renderers';
 import { createTree } from '~/models/tree';
 import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+import { User } from "~/models/user";
+import { Config } from '~/common/config';
+import { Session } from "~/models/session";
 
 export enum SearchResultsPanelColumnNames {
     CLUSTER = "Cluster",
@@ -35,6 +38,10 @@ export enum SearchResultsPanelColumnNames {
 
 export interface SearchResultsPanelDataProps {
     data: SearchBarAdvanceFormData;
+    user: User;
+    sessions: Session[];
+    remoteHostsConfig: { [key: string]: Config };
+    localCluster: string;
 }
 
 export interface SearchResultsPanelActionProps {
@@ -112,11 +119,18 @@ export const searchResultsPanelColumns: DataColumns<string> = [
 ];
 
 export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
+    const homeCluster = props.user.uuid.substr(0, 5);
     return <DataExplorer
         id={SEARCH_RESULTS_PANEL_ID}
         onRowClick={props.onItemClick}
         onRowDoubleClick={props.onItemDoubleClick}
         onContextMenu={props.onContextMenu}
         contextMenuColumn={true}
-        hideSearchInput />;
+        hideSearchInput
+        title={
+            props.localCluster === homeCluster ?
+                <div>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</div> :
+                <div>Searching local cluster {props.localCluster} only.  To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></div>
+        }
+    />;
 };
index 65999a5cbb30aea18b1c7ff66b9227b0cab5e7c0..bde7207a8a8b19f156ba6b87b57d6092d8193060 100644 (file)
@@ -10,6 +10,16 @@ import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-
 import { ResourceKind } from '~/models/resource';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { SearchResultsPanelView } from '~/views/search-results-panel/search-results-panel-view';
+import { RootState } from '~/store/store';
+
+const mapStateToProps = (rootState: RootState) => {
+    return {
+        user: rootState.auth.user,
+        sessions: rootState.auth.sessions,
+        remoteHostsConfig: rootState.auth.remoteHostsConfig,
+        localCluster: rootState.auth.localCluster,
+    };
+};
 
 const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
@@ -34,4 +44,4 @@ const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps =
     }
 });
 
-export const SearchResultsPanel = connect(null, mapDispatchToProps)(SearchResultsPanelView);
\ No newline at end of file
+export const SearchResultsPanel = connect(mapStateToProps, mapDispatchToProps)(SearchResultsPanelView);
index 582aa9c7416f50917c94aa32afc00be5ee96fa42..7fd00ba166d7a29159df8299392a216f4f23d815 100644 (file)
@@ -31,13 +31,15 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 interface SharedWithMePanelDataProps {
     resources: ResourcesState;
+    isAdmin: boolean;
 }
 
 type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithStyles<CssRules>;
 
 export const SharedWithMePanel = withStyles(styles)(
     connect((state: RootState) => ({
-        resources: state.resources
+        resources: state.resources,
+        isAdmin: state.auth.user!.isAdmin
     }))(
         class extends React.Component<SharedWithMePanelProps> {
             render() {
@@ -51,7 +53,7 @@ export const SharedWithMePanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const menuKind = resourceKindToContextMenuKind(resourceUuid);
+                const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
                 const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
diff --git a/src/views/workbench/fed-login.tsx b/src/views/workbench/fed-login.tsx
new file mode 100644 (file)
index 0000000..399b419
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { AuthState } from '~/store/auth/auth-reducer';
+import { User } from "~/models/user";
+import { getSaltedToken } from '~/store/auth/auth-action-session';
+import { Config } from '~/common/config';
+
+export interface FedLoginProps {
+    user?: User;
+    apiToken?: string;
+    homeCluster: string;
+    remoteHostsConfig: { [key: string]: Config };
+}
+
+const mapStateToProps = ({ auth }: RootState) => ({
+    user: auth.user,
+    apiToken: auth.apiToken,
+    remoteHostsConfig: auth.remoteHostsConfig,
+    homeCluster: auth.homeCluster,
+});
+
+export const FedLogin = connect(mapStateToProps)(
+    class extends React.Component<FedLoginProps> {
+        render() {
+            const { apiToken, user, homeCluster, remoteHostsConfig } = this.props;
+            if (!apiToken || !user || !user.uuid.startsWith(homeCluster)) {
+                return <></>;
+            }
+            const [, tokenUuid, token] = apiToken.split("/");
+            return <div id={"fedtoken-iframe-div"}>
+                {Object.keys(remoteHostsConfig)
+                    .map((k) => k !== homeCluster &&
+                        <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${getSaltedToken(k, tokenUuid, token)}`} style={{
+                            height: 0,
+                            width: 0,
+                            visibility: "hidden"
+                        }}
+                        />)}
+            </div>;
+        }
+    });
index e852150c2721001476ee0eb07399a0505bd02770..d015d4ec363255c5982ce3a8bb12af2393d1b2ec 100644 (file)
@@ -94,6 +94,8 @@ import { AddGroupMembersDialog } from '~/views-components/dialog-forms/add-group
 import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
 import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
 import { LinkAccountPanel } from '~/views/link-account-panel/link-account-panel';
+import { FedLogin } from './fed-login';
+import { CollectionsContentAddressPanel } from '~/views/collection-content-address-panel/collection-content-address-panel';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -184,6 +186,7 @@ export const WorkbenchPanel =
                                 <Route path={Routes.LINKS} component={LinkPanel} />
                                 <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
                                 <Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
+                                <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
                             </Switch>
                         </Grid>
                     </Grid>
@@ -248,5 +251,6 @@ export const WorkbenchPanel =
             <UserAttributesDialog />
             <UserManageDialog />
             <VirtualMachineAttributesDialog />
+            <FedLogin />
         </Grid>
     );