Merge branch 'master' into #15165-running-a-process-from-workflow-section-doesnt... #15165-running-a-process-from-workflow-section-doesnt-contain-values-in-advanced
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 22 May 2019 10:11:56 +0000 (12:11 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 22 May 2019 10:11:56 +0000 (12:11 +0200)
refs #15165

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

36 files changed:
src/common/config.ts
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/components/details-attribute/details-attribute.tsx
src/index.tsx
src/models/resource.ts
src/routes/routes.ts
src/services/auth-service/auth-service.ts
src/store/auth/auth-action-session.ts
src/store/auth/auth-action.test.ts
src/store/auth/auth-action.ts
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/navigation/navigation-action.ts
src/store/process-logs-panel/process-logs-panel-actions.ts
src/store/process-panel/process-panel-actions.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/store/search-bar/search-bar-actions.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/details-panel/process-details.tsx
src/views-components/details-panel/project-details.tsx
src/views-components/main-app-bar/anonymous-menu.tsx
src/views-components/snackbar/snackbar.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/login-panel/login-panel.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/search-results-panel/search-results-panel.tsx
src/views/user-panel/user-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 5f01957753d0fc5252f5a31b3a63b83b221bd770..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' | 'title';
+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'
     },
@@ -55,9 +58,9 @@ interface DataExplorerDataProps<T> {
     paperProps?: PaperProps;
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
+    title?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
-    title?: string;
 }
 
 interface DataExplorerActionProps<T> {
@@ -93,8 +96,8 @@ export const DataExplorer = withStyles(styles)(
                 paperKey, fetchMode, currentItemUuid, title
             } = this.props;
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                {title ? <div className={classes.title}>Content Address: {title}</div> : null}
-                {(!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 e1309793f61c3d351a0b600c14331ee52febf7e8..841375882e2121a318af401b1e77364d4d71279d 100644 (file)
@@ -35,7 +35,7 @@ export interface DataTableDataProps<T> {
     currentRoute?: string;
 }
 
-type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell' | 'arrow' | 'arrowButton';
+type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell' | 'arrow' | 'arrowButton' | 'tableCellWorkflows';
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     root: {
@@ -56,6 +56,17 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     tableCell: {
         wordWrap: 'break-word'
     },
+    tableCellWorkflows: {
+        '&:nth-last-child(2)': {
+            padding: '0px',
+            maxWidth: '48px'
+        },
+        '&:last-child': {
+            padding: '0px',
+            paddingRight: '24px',
+            width: '48px'
+        }
+    },
     arrow: {
         margin: 0
     },
@@ -125,14 +136,14 @@ export const DataTable = withStyles(styles)(
             </TableCell>;
         }
 
-        ArrowIcon = ({className, ...props}: SvgIconProps) => (
+        ArrowIcon = ({ className, ...props }: SvgIconProps) => (
             <IconButton component='span' className={this.props.classes.arrowButton} tabIndex={-1}>
-                <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)}/>
+                <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)} />
             </IconButton>
         )
 
         renderBodyRow = (item: any, index: number) => {
-            const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid } = this.props;
+            const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
             return <TableRow
                 hover
                 key={extractKey ? extractKey(item) : index}
@@ -141,7 +152,7 @@ export const DataTable = withStyles(styles)(
                 onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}
                 selected={item === currentItemUuid}>
                 {this.mapVisibleColumns((column, index) => (
-                    <TableCell key={column.key || index} className={classes.tableCell}>
+                    <TableCell key={column.key || index} className={currentRoute === '/workflows' ? classes.tableCellWorkflows : classes.tableCell}>
                         {column.render(item)}
                     </TableCell>
                 ))}
index 3586d22de99aec68309fd10fadbc3542aa7b4412..8f4708588d786d1d268c0aa70fcd013571732d00 100644 (file)
@@ -3,13 +3,20 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
 import Typography from '@material-ui/core/Typography';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { Tooltip } from '@material-ui/core';
+import { CopyIcon } from '~/components/icon/icon';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
 import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
 import { Link } from 'react-router-dom';
+import { RootState } from "~/store/store";
+import { FederationConfig, getNavUrl } from "~/routes/routes";
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 
-type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link';
+type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link' | 'copyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     attribute: {
@@ -25,9 +32,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     value: {
         boxSizing: 'border-box',
         width: '60%',
-        display: 'flex',
-        alignItems: 'flex-start',
-        textTransform: 'capitalize'
+        alignItems: 'flex-start'
     },
     lowercaseValue: {
         textTransform: 'lowercase'
@@ -38,6 +43,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         textDecoration: 'none',
         overflowWrap: 'break-word',
         cursor: 'pointer'
+    },
+    copyIcon: {
+        marginLeft: theme.spacing.unit,
+        fontSize: '1.125rem',
+        color: theme.palette.grey["500"],
+        cursor: 'pointer'
     }
 });
 
@@ -50,25 +61,61 @@ interface DetailsAttributeDataProps {
     link?: string;
     children?: React.ReactNode;
     onValueClick?: () => void;
-    linkInsideCard?: string;
+    linkToUuid?: string;
 }
 
-type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
+type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules> & FederationConfig & DispatchProp;
 
-export const DetailsAttribute = withStyles(styles)(
-    ({ 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>}
-            {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>
-            }
+const mapStateToProps = ({ auth }: RootState): FederationConfig => ({
+    localCluster: auth.localCluster,
+    remoteHostsConfig: auth.remoteHostsConfig,
+    sessions: auth.sessions
+});
+
+export const DetailsAttribute = connect(mapStateToProps)(withStyles(styles)(
+    class extends React.Component<DetailsAttributeProps> {
+
+        onCopy = (message: string) => {
+            this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+                message,
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS
+            }));
+        }
 
-        </Typography>
-);
+        render() {
+            const { label, link, value, children, classes, classLabel,
+                classValue, lowercaseValue, onValueClick, linkToUuid,
+                localCluster, remoteHostsConfig, sessions } = this.props;
+            let valueNode: React.ReactNode;
+
+            if (linkToUuid) {
+                const linkUrl = getNavUrl(linkToUuid || "", { localCluster, remoteHostsConfig, sessions });
+                if (linkUrl[0] === '/') {
+                    valueNode = <Link to={linkUrl} className={classes.link}>{linkToUuid}</Link>;
+                } else {
+                    valueNode = <a href={linkUrl} className={classes.link} target='_blank'>{linkToUuid}</a>;
+                }
+            } else if (link) {
+                valueNode = <a href={link} className={classes.link} target='_blank'>{value}</a>;
+            } else {
+                valueNode = value;
+            }
+            return <Typography component="div" className={classes.attribute}>
+                <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
+                <Typography
+                    onClick={onValueClick}
+                    component="span"
+                    className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
+                    {valueNode}
+                    {children}
+                    {linkToUuid && <Tooltip title="Copy">
+                        <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
+                            <CopyIcon className={classes.copyIcon} />
+                        </CopyToClipboard>
+                    </Tooltip>}
+                </Typography>
+            </Typography>;
+        }
+    }
+));
index 9c7b39aae4d91dd25fad37633666bf3ccf4358f1..ee174b2c5325866878d6d7012552e64a1ae85478 100644 (file)
@@ -113,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 = () =>
@@ -123,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 31f3eb883f974b26c05b45cd28837fe4f493600c..239a67cc1251e88d6b6968c89a7d9b878ac9b214 100644 (file)
@@ -57,8 +57,9 @@ export enum ResourceObjectType {
     NODE = '7ekkf'
 }
 
-export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
+export const RESOURCE_UUID_PATTERN = '[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}';
 export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
+export const COLLECTION_PDH_REGEX = /[a-f0-9]{32}\+\d+/;
 
 export const isResourceUuid = (uuid: string) =>
     RESOURCE_UUID_REGEX.test(uuid);
@@ -102,6 +103,7 @@ export const extractUuidKind = (uuid: string = '') => {
         case ResourceObjectType.LINK:
             return ResourceKind.LINK;
         default:
-            return undefined;
+            const match = COLLECTION_PDH_REGEX.exec(uuid);
+            return match ? ResourceKind.COLLECTION : undefined;
     }
 };
index dd09d8ece55545546c482bf0b5eb5d5398ce51ae..37c7a816735a7763b8ef25fb13f731f268277883 100644 (file)
@@ -3,13 +3,22 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { matchPath } from 'react-router';
-import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
+import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind, COLLECTION_PDH_REGEX } from '~/models/resource';
 import { getProjectUrl } from '~/models/project';
 import { getCollectionUrl } from '~/models/collection';
+import { Config } from '~/common/config';
+import { Session } from "~/models/session";
+
+export interface FederationConfig {
+    localCluster: string;
+    remoteHostsConfig: { [key: string]: Config };
+    sessions: Session[];
+}
 
 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})`,
@@ -43,6 +52,8 @@ export const getResourceUrl = (uuid: string) => {
     switch (kind) {
         case ResourceKind.PROJECT:
             return getProjectUrl(uuid);
+        case ResourceKind.USER:
+            return getProjectUrl(uuid);
         case ResourceKind.COLLECTION:
             return getCollectionUrl(uuid);
         case ResourceKind.PROCESS:
@@ -52,6 +63,27 @@ export const getResourceUrl = (uuid: string) => {
     }
 };
 
+export const getNavUrl = (uuid: string, config: FederationConfig) => {
+    const path = getResourceUrl(uuid) || "";
+    const cls = uuid.substr(0, 5);
+    if (cls === config.localCluster || extractUuidKind(uuid) === ResourceKind.USER || COLLECTION_PDH_REGEX.exec(uuid)) {
+        return path;
+    } else if (config.remoteHostsConfig[cls]) {
+        let u: URL;
+        if (config.remoteHostsConfig[cls].workbench2Url) {
+            u = new URL(config.remoteHostsConfig[cls].workbench2Url || "");
+        } else {
+            u = new URL(config.remoteHostsConfig[cls].workbenchUrl);
+            u.search = "api_token=" + config.sessions.filter((s) => s.clusterId === cls)[0].token;
+        }
+        u.pathname = path;
+        return u.toString();
+    } else {
+        return "";
+    }
+};
+
+
 export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
 
 export const getProcessLogUrl = (uuid: string) => `/process-logs/${uuid}`;
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 11f29e45d304cc4bf866b25b2b900e1c702cc942..f28ae1790c9739f11eed385203fb04f4006c1cb3 100644 (file)
@@ -68,6 +68,7 @@ describe('auth-actions', () => {
                 zzzzz: "zzzzz.arvadosapi.com",
                 xc59z: "xc59z.arvadosapi.com"
             },
+           remoteHostsConfig: {},
             sessions: [{
                 "active": true,
                 "baseUrl": undefined,
index baf80595f300ad9fb7d181aa8ba5441303e71aab..c088418a61eaa4b3b4a4c8fdc7b7110fa6ebfa58 100644 (file)
@@ -10,8 +10,10 @@ import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
 import { User } 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 Axios from "axios";
+import { AxiosError } from "axios";
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
@@ -28,7 +30,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) {
@@ -47,17 +50,30 @@ function removeAuthorizationHeader(client: AxiosInstance) {
 export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     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) => {
@@ -66,8 +82,8 @@ export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: ()
     dispatch(authActions.SAVE_API_TOKEN(token));
 };
 
-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 38cf1581d3796dc5b89a4d7cfc897adc425271ce..14d92803cc1fc21750d662d723ded60c2d3221ad 100644 (file)
@@ -43,7 +43,8 @@ describe('auth-reducer', () => {
             sessions: [],
             homeCluster: "zzzzz",
             localCluster: "",
-            remoteHosts: {}
+            remoteHosts: {},
+            remoteHostsConfig: {}
         });
     });
 
@@ -59,6 +60,7 @@ describe('auth-reducer', () => {
             homeCluster: "",
             localCluster: "",
             remoteHosts: {},
+            remoteHostsConfig: {}
         });
     });
 
@@ -85,6 +87,7 @@ describe('auth-reducer', () => {
             homeCluster: "",
             localCluster: "",
             remoteHosts: {},
+            remoteHostsConfig: {},
             user: {
                 email: "test@test.com",
                 firstName: "John",
index 0335752678c56421336751dfd79eaef11316db94..e44c81e323297fdfcd8884a4efe08236d911a9f2 100644 (file)
@@ -7,6 +7,7 @@ import { User } 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) => {
@@ -41,6 +44,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 d7ad017878894ff0156333b9d3c290c0637276b3..f7eeae57c8f53226ae5aa39370cfc80998fbc678 100644 (file)
@@ -2,26 +2,22 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Dispatch, compose } from 'redux';
-import { push } from "react-router-redux";
+import { Dispatch, compose, AnyAction } from 'redux';
+import { push, RouterAction } from "react-router-redux";
 import { ResourceKind, extractUuidKind } from '~/models/resource';
 import { getCollectionUrl } from "~/models/collection";
 import { getProjectUrl } from "~/models/project";
 import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getProcessUrl, getProcessLogUrl, getGroupUrl } from '~/routes/routes';
+import { Routes, getProcessUrl, getProcessLogUrl, getGroupUrl, getNavUrl } from '~/routes/routes';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { GROUPS_PANEL_LABEL } from '~/store/breadcrumbs/breadcrumbs-actions';
 
 export const navigateTo = (uuid: string) =>
-    async (dispatch: Dispatch) => {
+    async (dispatch: Dispatch, getState: () => RootState) => {
         const kind = extractUuidKind(uuid);
-        if (kind === ResourceKind.PROJECT || kind === ResourceKind.USER) {
-            dispatch<any>(navigateToProject(uuid));
-        } else if (kind === ResourceKind.COLLECTION) {
-            dispatch<any>(navigateToCollection(uuid));
-        } else if (kind === ResourceKind.CONTAINER_REQUEST) {
-            dispatch<any>(navigateToProcess(uuid));
+        if (kind === ResourceKind.PROJECT || kind === ResourceKind.USER || kind === ResourceKind.COLLECTION || kind === ResourceKind.CONTAINER_REQUEST) {
+            dispatch<any>(pushOrGoto(getNavUrl(uuid, getState().auth)));
         } else if (kind === ResourceKind.VIRTUAL_MACHINE) {
             dispatch<any>(navigateToAdminVirtualMachines);
         }
@@ -50,18 +46,24 @@ export const navigateToPublicFavorites = push(Routes.PUBLIC_FAVORITES);
 
 export const navigateToWorkflows = push(Routes.WORKFLOWS);
 
-export const navigateToProject = compose(push, getProjectUrl);
-
-export const navigateToCollection = compose(push, getCollectionUrl);
+export const pushOrGoto = (url: string): AnyAction => {
+    if (url === "") {
+        return { type: "noop" };
+    } else if (url[0] === '/') {
+        return push(url);
+    } else {
+        window.location.href = url;
+        return { type: "noop" };
+    }
+};
 
-export const navigateToProcess = compose(push, getProcessUrl);
 
 export const navigateToProcessLogs = compose(push, getProcessLogUrl);
 
 export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     const rootProjectUuid = services.authService.getUuid();
     if (rootProjectUuid) {
-        dispatch(navigateToProject(rootProjectUuid));
+        dispatch<any>(navigateTo(rootProjectUuid));
     }
 };
 
@@ -77,11 +79,11 @@ export const navigateToAdminVirtualMachines = push(Routes.VIRTUAL_MACHINES_ADMIN
 
 export const navigateToRepositories = push(Routes.REPOSITORIES);
 
-export const navigateToSshKeysAdmin= push(Routes.SSH_KEYS_ADMIN);
+export const navigateToSshKeysAdmin = push(Routes.SSH_KEYS_ADMIN);
 
-export const navigateToSshKeysUser= push(Routes.SSH_KEYS_USER);
+export const navigateToSshKeysUser = push(Routes.SSH_KEYS_USER);
 
-export const navigateToSiteManager= push(Routes.SITE_MANAGER);
+export const navigateToSiteManager = push(Routes.SITE_MANAGER);
 
 export const navigateToMyAccount = push(Routes.MY_ACCOUNT);
 
index bf0dc93564ae37301434a6cc9c70403070e33dd7..fcb1b0ea970bc528dd0c254d08590e6fdba743d1 100644 (file)
@@ -16,7 +16,7 @@ import { ResourceEventMessage } from '~/websocket/resource-event-message';
 import { getProcess } from '~/store/processes/process';
 import { FilterBuilder } from "~/services/api/filter-builder";
 import { OrderBuilder } from "~/services/api/order-builder";
-import { navigateToCollection } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 
 export const processLogsPanelActions = unionize({
@@ -104,7 +104,7 @@ export const navigateToLogCollection = (uuid: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {
             await services.collectionService.get(uuid);
-            dispatch<any>(navigateToCollection(uuid));
+            dispatch<any>(navigateTo(uuid));
         } catch {
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'This collection does not exists!', hideDuration: 2000, kind: SnackbarKind.ERROR }));
         }
index 42a718bd69a9b4a4fde1f9217c36812d08a44829..da917f71f70f275aa25e96e3d4861b6b3b9011be 100644 (file)
@@ -8,7 +8,7 @@ import { Dispatch } from 'redux';
 import { ProcessStatus } from '~/store/processes/process';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from "~/services/services";
-import { navigateToCollection, navigateToWorkflows } from '~/store/navigation/navigation-action';
+import { navigateTo, navigateToWorkflows } from '~/store/navigation/navigation-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { SnackbarKind } from '../snackbar/snackbar-actions';
 import { showWorkflowDetails } from '~/store/workflow-panel/workflow-panel-actions';
@@ -32,7 +32,7 @@ export const navigateToOutput = (uuid: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {
             await services.collectionService.get(uuid);
-            dispatch<any>(navigateToCollection(uuid));
+            dispatch<any>(navigateTo(uuid));
         } catch {
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'This collection does not exists!', hideDuration: 2000, kind: SnackbarKind.ERROR }));
         }
index c39517fea46c000dc89bc0d48ff54ea5e1553b37..5b082b8c28eaffc19199f8f757fd5fa4f0b4a5eb 100644 (file)
@@ -13,7 +13,7 @@ import { RUN_PROCESS_INPUTS_FORM } from '~/views/run-process-panel/run-process-i
 import { WorkflowInputsData } from '~/models/workflow';
 import { createWorkflowMounts } from '~/models/process';
 import { ContainerRequestState } from '~/models/container-request';
-import { navigateToProcess } from '../navigation/navigation-action';
+import { navigateTo } from '../navigation/navigation-action';
 import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM, VCPUS_FIELD, RAM_FIELD, RUNTIME_FIELD, OUTPUT_FIELD, API_FIELD } from '~/views/run-process-panel/run-process-advanced-form';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
@@ -152,10 +152,10 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
             properties: {
                 workflowUuid: selectedWorkflow.uuid,
                 workflowName: selectedWorkflow.name
-            }       
+            }
         };
         const newProcess = await services.containerRequestService.create(newProcessData);
-        dispatch(navigateToProcess(newProcess.uuid));
+        dispatch(navigateTo(newProcess.uuid));
     }
 };
 
index 8734888bbcd329532848c89695a4788b69197f63..68efb4e6f2f6b0651a8aa409f65fd5f2b4d72a61 100644 (file)
@@ -10,7 +10,7 @@ import { RootState } from '~/store/store';
 import { initUserProject, treePickerActions } from '~/store/tree-picker/tree-picker-actions';
 import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from "~/services/api/filter-builder";
-import { ResourceKind, isResourceUuid, extractUuidKind } from '~/models/resource';
+import { ResourceKind, isResourceUuid, extractUuidKind, RESOURCE_UUID_REGEX, COLLECTION_PDH_REGEX } from '~/models/resource';
 import { SearchView } from '~/store/search-bar/search-bar-reducer';
 import { navigateTo, navigateToSearchResults } from '~/store/navigation/navigation-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
@@ -192,10 +192,14 @@ export const submitData = (event: React.FormEvent<HTMLFormElement>) =>
         dispatch<any>(saveRecentQuery(searchValue));
         dispatch<any>(loadRecentQueries());
         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
-        dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
-        dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
-        dispatch(searchResultsPanelActions.CLEAR());
-        dispatch(navigateToSearchResults);
+        if (RESOURCE_UUID_REGEX.exec(searchValue) || COLLECTION_PDH_REGEX.exec(searchValue)) {
+            dispatch<any>(navigateTo(searchValue));
+        } else {
+            dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
+            dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
+            dispatch(searchResultsPanelActions.CLEAR());
+            dispatch(navigateToSearchResults);
+        }
     };
 
 
@@ -327,7 +331,7 @@ export const queryToFilters = (query: string) => {
     const filter = new FilterBuilder();
     const resourceKind = data.type;
 
-    if(data.searchValue){
+    if (data.searchValue) {
         filter.addFullTextSearch(data.searchValue);
     }
 
index caf466f7ed89bb3937bae4e6a5882e77fd0aa592..1a567e9db08017ca227a3182848370b179fcae1b 100644 (file)
@@ -11,7 +11,7 @@ import { startSubmit, reset } from "redux-form";
 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 { navigateTo, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
 import { saveApiToken } from '~/store/auth/auth-action';
 
 export const USERS_PANEL_ID = 'usersPanel';
@@ -73,7 +73,7 @@ export const openUserCreateDialog = () =>
 
 export const openUserProjects = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch<any>(navigateToProject(uuid));
+        dispatch<any>(navigateTo(uuid));
     };
 
 
@@ -134,4 +134,4 @@ export const loadUsersData = () =>
 export const loadUsersPanel = () =>
     (dispatch: Dispatch) => {
         dispatch(userBindedActions.REQUEST_ITEMS());
-    };
\ No newline at end of file
+    };
index 2363b5795a8b9c2c0d0743524fb8e82f460f0caf..fb85c0e0c8392b8b6fb2742358b6246389f069b3 100644 (file)
@@ -32,7 +32,7 @@ import {
     setSidePanelBreadcrumbs,
     setTrashBreadcrumbs
 } from '~/store/breadcrumbs/breadcrumbs-actions';
-import { navigateToProject } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { ServiceRepository } from '~/services/services';
 import { getResource } from '~/store/resources/resources';
@@ -140,7 +140,7 @@ export const loadWorkbench = () =>
             if (router.location) {
                 const match = matchRootRoute(router.location.pathname);
                 if (match) {
-                    dispatch(navigateToProject(user.uuid));
+                    dispatch<any>(navigateTo(user.uuid));
                 }
             }
         } else {
index 43c55a92c9addbd8031fd537b5c59528b620e7ca..b78e7192dc0bd24f5c1ef14ed1ab6c0629aea564 100644 (file)
@@ -16,6 +16,7 @@ import { initSessions } from "~/store/auth/auth-action-session";
 interface ApiTokenProps {
     authService: AuthService;
     config: Config;
+    loadMainApp: boolean;
 }
 
 export const ApiToken = connect()(
@@ -23,15 +24,18 @@ 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(() => {
-                this.props.dispatch(navigateToRootProject);
+                if (loadMainApp) {
+                    this.props.dispatch(navigateToRootProject);
+                }
             });
         }
         render() {
-            return <div/>;
+            return <div />;
         }
     }
 );
index bf504f2b46b82e4efa475c1359cdf1a7c2c55f9e..cc311248cb1d9b0d3f208831604faf75fddd774c 100644 (file)
@@ -239,12 +239,12 @@ export const ResourceCluster = (props: { uuid: string }) => {
     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
     const ci = pos >= CLUSTER_ID_LENGTH ? (props.uuid.charCodeAt(0) + props.uuid.charCodeAt(1)) % clusterColors.length : 0;
     return <Typography>
-        <div style={{
+        <span style={{
             backgroundColor: clusterColors[ci][0],
             color: clusterColors[ci][1],
             padding: "2px 7px",
             borderRadius: 3
-        }}>{clusterId}</div>
+        }}>{clusterId}</span>
     </Typography>;
 };
 
index ec5bdabd833a097cac06b3eda5c30805ac5193be..a523d6fca8e1176c438779c9fee33493add23e55 100644 (file)
@@ -21,11 +21,11 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
         return <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.COLLECTION)} />
             <DetailsAttribute label='Size' value='---' />
-            <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
+            <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} lowercaseValue={true} />
             <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
-            <DetailsAttribute label='Collection UUID' linkInsideCard={this.item.uuid} value={this.item.uuid} />
-            <DetailsAttribute label='Content address' linkInsideCard={this.item.portableDataHash} value={this.item.portableDataHash} />
+            <DetailsAttribute label='Collection UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
+            <DetailsAttribute label='Content address' linkToUuid={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 9ce84867d736d5061d9461be923346c9dc3da604..f4aaa8436f753face5dd8bf97a5a80eaea85f134 100644 (file)
@@ -56,7 +56,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     tabContainer: {
         overflow: 'auto',
-        padding: theme.spacing.unit * 3,
+        padding: theme.spacing.unit * 1,
     },
 });
 
index e3c9823d018b62d1a0b247007cca32f158230c87..2fbdd31363afb88634f594f5fc53c9dd87b90acb 100644 (file)
@@ -13,7 +13,7 @@ import { DetailsAttribute } from "~/components/details-attribute/details-attribu
 
 export class ProcessDetails extends DetailsData<ProcessResource> {
 
-    getIcon(className?: string){
+    getIcon(className?: string) {
         return <ProcessIcon className={className} />;
     }
 
@@ -21,7 +21,7 @@ export class ProcessDetails extends DetailsData<ProcessResource> {
         return <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
             <DetailsAttribute label='Size' value='---' />
-            <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
+            <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} value={this.item.ownerUuid} />
 
             {/* Missing attr */}
             <DetailsAttribute label='Status' value={this.item.state} />
@@ -31,15 +31,14 @@ export class ProcessDetails extends DetailsData<ProcessResource> {
             <DetailsAttribute label='Started at' value={formatDate(this.item.createdAt)} />
             <DetailsAttribute label='Finished at' value={formatDate(this.item.expiresAt)} />
 
-            {/* Links but we dont have view */}
-            <DetailsAttribute label='Outputs' link={this.item.outputPath} value={this.item.outputPath} />
-            <DetailsAttribute label='UUID' link={this.item.uuid} value={this.item.uuid} />
-            <DetailsAttribute label='Container UUID' link={this.item.containerUuid || ''} value={this.item.containerUuid} />
+            <DetailsAttribute label='Outputs' value={this.item.outputPath} />
+            <DetailsAttribute label='UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
+            <DetailsAttribute label='Container UUID' value={this.item.containerUuid} />
 
             <DetailsAttribute label='Priority' value={this.item.priority} />
             <DetailsAttribute label='Runtime Constraints' value={JSON.stringify(this.item.runtimeConstraints)} />
-            {/* Link but we dont have view */}
-            <DetailsAttribute label='Docker Image locator' link={this.item.containerImage} value={this.item.containerImage} />
+
+            <DetailsAttribute label='Docker Image locator' linkToUuid={this.item.containerImage} value={this.item.containerImage} />
         </div>;
     }
 }
index 91c5e027ba61cb9a68deb9f4b8f214145d76561b..1ec1c60c04aae40b17ef2712486952c3fbdad8bb 100644 (file)
@@ -57,31 +57,32 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
     withStyles(styles)(
         ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
-                {/* Missing attr */}
-                <DetailsAttribute label='Size' value='---' />
-                <DetailsAttribute label='Owner' value={project.ownerUuid} lowercaseValue={true} />
-                <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
-                <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
-                {/* Missing attr */}
-                {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
-                <DetailsAttribute label='Description'>
-                    {project.description ?
-                        <RichTextEditorLink
-                            title={`Description of ${project.name}`}
-                            content={project.description}
-                            label='Show full description' />
-                        : '---'
-                    }
-                </DetailsAttribute>
-                <DetailsAttribute label='Properties'>
-                    <div onClick={onClick}>
-                        <RenameIcon className={classes.editIcon} />
-                    </div>
-                </DetailsAttribute>
-                {
-                    Object.keys(project.properties).map(k => {
-                        return <Chip key={k} className={classes.tag} label={`${k}: ${project.properties[k]}`} />;
-                    })
+            {/* Missing attr */}
+            <DetailsAttribute label='Size' value='---' />
+            <DetailsAttribute label='Owner' linkToUuid={project.ownerUuid} lowercaseValue={true} />
+            <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
+            <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
+            <DetailsAttribute label='Project UUID' linkToUuid={project.uuid} value={project.uuid} />
+            {/* Missing attr */}
+            {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
+            <DetailsAttribute label='Description'>
+                {project.description ?
+                    <RichTextEditorLink
+                        title={`Description of ${project.name}`}
+                        content={project.description}
+                        label='Show full description' />
+                    : '---'
                 }
+            </DetailsAttribute>
+            <DetailsAttribute label='Properties'>
+                <div onClick={onClick}>
+                    <RenameIcon className={classes.editIcon} />
+                </div>
+            </DetailsAttribute>
+            {
+                Object.keys(project.properties).map(k => {
+                    return <Chip key={k} className={classes.tag} label={`${k}: ${project.properties[k]}`} />;
+                })
+            }
         </div>
-));
\ No newline at end of file
+    ));
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 4250dcdfb3d7a7cdf35a41ed2240068daadd60b9..c5d0f4832799c20a174254ea66c76391bb59559b 100644 (file)
@@ -9,7 +9,7 @@ import { RootState } from "~/store/store";
 import { Button, IconButton, StyleRulesCallback, WithStyles, withStyles, SnackbarContent } from '@material-ui/core';
 import MaterialSnackbar, { SnackbarOrigin } from "@material-ui/core/Snackbar";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
-import { navigateToProject } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
 import WarningIcon from '@material-ui/icons/Warning';
 import CheckCircleIcon from '@material-ui/icons/CheckCircle';
 import ErrorIcon from '@material-ui/icons/Error';
@@ -56,7 +56,7 @@ const mapDispatchToProps = (dispatch: Dispatch): SnackbarEventProps => ({
         dispatch(snackbarActions.SHIFT_MESSAGES());
     },
     onClick: (uuid: string) => {
-        dispatch(navigateToProject(uuid));
+        dispatch<any>(navigateTo(uuid));
     }
 });
 
@@ -121,7 +121,7 @@ export const Snackbar = withStyles(styles)(connect(mapStateToProps, mapDispatchT
                     aria-describedby="client-snackbar"
                     message={
                         <span id="client-snackbar" className={classes.message}>
-                            <Icon className={classNames(classes.icon, classes.iconVariant)}/>
+                            <Icon className={classNames(classes.icon, classes.iconVariant)} />
                             {props.message}
                         </span>
                     }
@@ -150,7 +150,7 @@ const actions = (props: SnackbarProps) => {
                 size="small"
                 color="inherit"
                 className={classes.linkButton}
-                onClick={() => onClick(link) }>
+                onClick={() => onClick(link)}>
                 Go To
             </Button>
         );
index 89b23f7c2fe4b8f2874057e883866ca772b6e33c..b652b502886653bcf43695cf80eeaefd8d9e86bf 100644 (file)
@@ -141,7 +141,7 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
                         onRowDoubleClick={this.props.onItemDoubleClick}
                         onContextMenu={this.props.onContextMenu}
                         contextMenuColumn={true}
-                        title={this.props.match.params.id}
+                        title={`Content address: ${this.props.match.params.id}`}
                         dataTableDefaultView={
                             <DataTableDefaultView
                                 icon={CollectionIcon}
@@ -151,4 +151,4 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
             }
         }
     )
-);
\ No newline at end of file
+);
index 948a20a07b386fb52993d2e851586ebc5620280c..5d799f0b27c8fd040ac23c1f90acc5cfa8648d8e 100644 (file)
@@ -15,7 +15,6 @@ import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '~/components/icon/ico
 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';
 import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag, navigateToProcess } from '~/store/collection-panel/collection-panel-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
@@ -27,7 +26,7 @@ import { getResourceData } from "~/store/resources-data/resources-data";
 import { ResourceData } from "~/store/resources-data/resources-data-reducer";
 import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 
-type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value' | 'link';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     card: {
@@ -41,12 +40,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginRight: theme.spacing.unit,
         marginBottom: theme.spacing.unit
     },
-    copyIcon: {
-        marginLeft: theme.spacing.unit,
-        fontSize: '1.125rem',
-        color: theme.palette.grey["500"],
-        cursor: 'pointer'
-    },
     label: {
         fontSize: '0.875rem'
     },
@@ -105,22 +98,19 @@ export const CollectionPanel = withStyles(styles)(
                                 subheaderTypographyProps={this.titleProps} />
                             <CardContent>
                                 <Grid container direction="column">
-                                    <Grid item xs={6}>
+                                    <Grid item xs={10}>
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                                             label='Collection UUID'
-                                            value={item && item.uuid}>
-                                            <Tooltip title="Copy uuid">
-                                                <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy()}>
-                                                    <CopyIcon className={classes.copyIcon} />
-                                                </CopyToClipboard>
-                                            </Tooltip>
-                                        </DetailsAttribute>
+                                            linkToUuid={item && item.uuid} />
+                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                            label='Portable data hash'
+                                            linkToUuid={item && item.portableDataHash} />
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                                             label='Number of files' value={data && data.fileCount} />
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                                             label='Content size' value={data && formatFileSize(data.fileSize)} />
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Owner' value={item && item.ownerUuid} />
+                                            label='Owner' linkToUuid={item && item.ownerUuid} />
                                         {(item.properties.container_request || item.properties.containerRequest) &&
                                             <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
                                                 <DetailsAttribute classLabel={classes.link} label='Link to process' />
@@ -176,14 +166,6 @@ export const CollectionPanel = withStyles(styles)(
                 this.props.dispatch<any>(deleteCollectionTag(key));
             }
 
-            onCopy = () => {
-                this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: "Uuid has been copied",
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS
-                }));
-            }
-
             openCollectionDetails = () => {
                 const { item } = this.props;
                 if (item) {
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 368a0d6449171c5a7efdc592ae5ad365ad83514f..b82b174520ced7cf922d4fec423f3c0c0372051d 100644 (file)
@@ -8,7 +8,6 @@ import { DataColumns } from '~/components/data-table/data-table';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { ResourceKind } from '~/models/resource';
 import { ContainerRequestState } from '~/models/container-request';
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
 import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
 import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
 import {
@@ -21,6 +20,7 @@ import {
 } from '~/views-components/data-explorer/renderers';
 import { createTree } from '~/models/tree';
 import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+import { SearchResultsPanelProps } from "./search-results-panel";
 
 export enum SearchResultsPanelColumnNames {
     CLUSTER = "Cluster",
@@ -33,19 +33,6 @@ export enum SearchResultsPanelColumnNames {
     LAST_MODIFIED = "Last modified"
 }
 
-export interface SearchResultsPanelDataProps {
-    data: SearchBarAdvanceFormData;
-}
-
-export interface SearchResultsPanelActionProps {
-    onItemClick: (item: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
-    onDialogOpen: (ownerUuid: string) => void;
-    onItemDoubleClick: (item: string) => void;
-}
-
-export type SearchResultsPanelProps = SearchResultsPanelDataProps & SearchResultsPanelActionProps;
-
 export interface WorkflowPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
@@ -112,11 +99,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 />;
+        contextMenuColumn={false}
+        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..7de1abd3c501937ae9e7dcbf8aac97fb4b26d3ce 100644 (file)
@@ -5,26 +5,44 @@
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { navigateTo } from '~/store/navigation/navigation-action';
-import { SearchResultsPanelActionProps } from './search-results-panel-view';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { ResourceKind } from '~/models/resource';
+// import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+// 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';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { User } from "~/models/user";
+import { Config } from '~/common/config';
+import { Session } from "~/models/session";
+
+export interface SearchResultsPanelDataProps {
+    data: SearchBarAdvanceFormData;
+    user: User;
+    sessions: Session[];
+    remoteHostsConfig: { [key: string]: Config };
+    localCluster: string;
+}
+
+export interface SearchResultsPanelActionProps {
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+    onDialogOpen: (ownerUuid: string) => void;
+    onItemDoubleClick: (item: string) => void;
+}
+
+export type SearchResultsPanelProps = SearchResultsPanelDataProps & SearchResultsPanelActionProps;
+
+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) => {
-        const kind = resourceKindToContextMenuKind(resourceUuid);
-        if (kind) {
-            dispatch<any>(openContextMenu(event, {
-                name: '',
-                uuid: resourceUuid,
-                ownerUuid: '',
-                kind: ResourceKind.NONE,
-                menuKind: kind
-            }));
-        }
-        dispatch<any>(loadDetailsPanel(resourceUuid));
-    },
+    onContextMenu: (event, resourceUuid) => { return; },
     onDialogOpen: (ownerUuid: string) => { return; },
     onItemClick: (resourceUuid: string) => {
         dispatch<any>(loadDetailsPanel(resourceUuid));
@@ -34,4 +52,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 c42036edf17ea5c0fae129a72fef980b4fa1a8d7..6c2fdafe337cec8b2ed8c2f046bce252e0905603 100644 (file)
@@ -101,14 +101,6 @@ export const userPanelColumns: DataColumns<string> = [
         filters: createTree(),
         render: uuid => <ResourceIsAdmin uuid={uuid} />
     },
-    {
-        name: UserPanelColumnNames.REDIRECT_TO_USER,
-        selected: true,
-        configurable: false,
-        sortDirection: SortDirection.NONE,
-        filters: createTree(),
-        render: () => <Typography noWrap>(none)</Typography>
-    },
     {
         name: UserPanelColumnNames.USERNAME,
         selected: true,
diff --git a/src/views/workbench/fed-login.tsx b/src/views/workbench/fed-login.tsx
new file mode 100644 (file)
index 0000000..d09e689
--- /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;
+    localCluster: string;
+    remoteHostsConfig: { [key: string]: Config };
+}
+
+const mapStateToProps = ({ auth }: RootState) => ({
+    user: auth.user,
+    apiToken: auth.apiToken,
+    remoteHostsConfig: auth.remoteHostsConfig,
+    localCluster: auth.localCluster,
+});
+
+export const FedLogin = connect(mapStateToProps)(
+    class extends React.Component<FedLoginProps> {
+        render() {
+            const { apiToken, user, localCluster, remoteHostsConfig } = this.props;
+            if (!apiToken || !user || !user.uuid.startsWith(localCluster)) {
+                return <></>;
+            }
+            const [, tokenUuid, token] = apiToken.split("/");
+            return <div id={"fedtoken-iframe-div"}>
+                {Object.keys(remoteHostsConfig)
+                    .map((k) => k !== localCluster &&
+                        <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${getSaltedToken(k, tokenUuid, token)}`} style={{
+                            height: 0,
+                            width: 0,
+                            visibility: "hidden"
+                        }}
+                        />)}
+            </div>;
+        }
+    });
index e3668373ff754ce93bb1dbf978ad346461ff894d..20cbbdea0c9a0b262f976889d5e097243b777c6f 100644 (file)
@@ -92,6 +92,7 @@ import { GroupMemberAttributesDialog } from '~/views-components/groups-dialog/me
 import { AddGroupMembersDialog } from '~/views-components/dialog-forms/add-group-member-dialog';
 import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
 import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-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';
@@ -177,7 +178,7 @@ export const WorkbenchPanel =
                                 <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
                                 <Route path={Routes.LINKS} component={LinkPanel} />
                                 <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
-                                <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel}/>
+                                <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
                             </Switch>
                         </Grid>
                     </Grid>
@@ -242,5 +243,6 @@ export const WorkbenchPanel =
             <UserAttributesDialog />
             <UserManageDialog />
             <VirtualMachineAttributesDialog />
+            <FedLogin />
         </Grid>
     );