version: string;
websocketUrl: string;
workbenchUrl: string;
+ workbench2Url?: string;
vocabularyUrl: string;
fileViewersConfigUrl: string;
}
});
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()}`;
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: {
toolbar: {
paddingTop: theme.spacing.unit * 2
},
+ toolbarUnderTitle: {
+ paddingTop: 0
+ },
footer: {
overflow: 'auto'
},
paperProps?: PaperProps;
actions?: React.ReactNode;
hideSearchInput?: boolean;
+ title?: React.ReactNode;
paperKey?: string;
currentItemUuid: string;
- title?: string;
}
interface DataExplorerActionProps<T> {
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
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: {
tableCell: {
wordWrap: 'break-word'
},
+ tableCellWorkflows: {
+ '&:nth-last-child(2)': {
+ padding: '0px',
+ maxWidth: '48px'
+ },
+ '&:last-child': {
+ padding: '0px',
+ paddingRight: '24px',
+ width: '48px'
+ }
+ },
arrow: {
margin: 0
},
</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}
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>
))}
// 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: {
value: {
boxSizing: 'border-box',
width: '60%',
- display: 'flex',
- alignItems: 'flex-start',
- textTransform: 'capitalize'
+ alignItems: 'flex-start'
},
lowercaseValue: {
textTransform: 'lowercase'
textDecoration: 'none',
overflowWrap: 'break-word',
cursor: 'pointer'
+ },
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ fontSize: '1.125rem',
+ color: theme.palette.grey["500"],
+ cursor: 'pointer'
}
});
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>;
+ }
+ }
+));
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 = () =>
<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>
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);
case ResourceObjectType.LINK:
return ResourceKind.LINK;
default:
- return undefined;
+ const match = COLLECTION_PDH_REGEX.exec(uuid);
+ return match ? ResourceKind.COLLECTION : undefined;
}
};
// 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})`,
switch (kind) {
case ResourceKind.PROJECT:
return getProjectUrl(uuid);
+ case ResourceKind.USER:
+ return getProjectUrl(uuid);
case ResourceKind.COLLECTION:
return getCollectionUrl(uuid);
case ResourceKind.PROCESS:
}
};
+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}`;
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;
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
+ localStorage.setItem(HOME_CLUSTER, token.split('/')[1].substr(0, 5));
}
public removeApiToken() {
return localStorage.getItem(API_TOKEN_KEY) || undefined;
}
+ public getHomeCluster() {
+ return localStorage.getItem(HOME_CLUSTER) || undefined;
+ }
+
public getUuid() {
return localStorage.getItem(USER_UUID_KEY) || undefined;
}
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() {
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/")) {
zzzzz: "zzzzz.arvadosapi.com",
xc59z: "xc59z.arvadosapi.com"
},
+ remoteHostsConfig: {},
sessions: [{
"active": true,
"baseUrl": undefined,
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>(),
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) {
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) => {
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());
};
sessions: [],
homeCluster: "zzzzz",
localCluster: "",
- remoteHosts: {}
+ remoteHosts: {},
+ remoteHostsConfig: {}
});
});
homeCluster: "",
localCluster: "",
remoteHosts: {},
+ remoteHostsConfig: {}
});
});
homeCluster: "",
localCluster: "",
remoteHosts: {},
+ remoteHostsConfig: {},
user: {
email: "test@test.com",
firstName: "John",
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;
localCluster: string;
homeCluster: string;
remoteHosts: { [key: string]: string };
+ remoteHostsConfig: { [key: string]: Config };
}
const initialState: AuthState = {
sessions: [],
localCluster: "",
homeCluster: "",
- remoteHosts: {}
+ remoteHosts: {},
+ remoteHostsConfig: {}
};
export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
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) };
},
//
// 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);
}
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));
}
};
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);
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({
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 }));
}
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';
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 }));
}
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';
properties: {
workflowUuid: selectedWorkflow.uuid,
workflowName: selectedWorkflow.name
- }
+ }
};
const newProcess = await services.containerRequestService.create(newProcessData);
- dispatch(navigateToProcess(newProcess.uuid));
+ dispatch(navigateTo(newProcess.uuid));
}
};
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';
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);
+ }
};
const filter = new FilterBuilder();
const resourceKind = data.type;
- if(data.searchValue){
+ if (data.searchValue) {
filter.addFullTextSearch(data.searchValue);
}
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';
export const openUserProjects = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(navigateToProject(uuid));
+ dispatch<any>(navigateTo(uuid));
};
export const loadUsersPanel = () =>
(dispatch: Dispatch) => {
dispatch(userBindedActions.REQUEST_ITEMS());
- };
\ No newline at end of file
+ };
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';
if (router.location) {
const match = matchRootRoute(router.location.pathname);
if (match) {
- dispatch(navigateToProject(user.uuid));
+ dispatch<any>(navigateTo(user.uuid));
}
}
} else {
interface ApiTokenProps {
authService: AuthService;
config: Config;
+ loadMainApp: boolean;
}
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 />;
}
}
);
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>;
};
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)} />
},
tabContainer: {
overflow: 'auto',
- padding: theme.spacing.unit * 3,
+ padding: theme.spacing.unit * 1,
},
});
export class ProcessDetails extends DetailsData<ProcessResource> {
- getIcon(className?: string){
+ getIcon(className?: string) {
return <ProcessIcon className={className} />;
}
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} />
<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>;
}
}
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
+ ));
({ dispatch }: DispatchProp<any>) =>
<Button
color="inherit"
- onClick={() => dispatch(login("", ""))}>
+ onClick={() => dispatch(login("", "", {}))}>
Sign in
</Button>);
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';
dispatch(snackbarActions.SHIFT_MESSAGES());
},
onClick: (uuid: string) => {
- dispatch(navigateToProject(uuid));
+ dispatch<any>(navigateTo(uuid));
}
});
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>
}
size="small"
color="inherit"
className={classes.linkButton}
- onClick={() => onClick(link) }>
+ onClick={() => onClick(link)}>
Go To
</Button>
);
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}
}
}
)
-);
\ No newline at end of file
+);
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';
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: {
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'
},
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' />
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) {
<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> with user from {homeCluster}</span>}
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 {
} 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",
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;
}
];
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>
+ }
+ />;
};
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));
}
});
-export const SearchResultsPanel = connect(null, mapDispatchToProps)(SearchResultsPanelView);
\ No newline at end of file
+export const SearchResultsPanel = connect(mapStateToProps, mapDispatchToProps)(SearchResultsPanelView);
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,
--- /dev/null
+// 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>;
+ }
+ });
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';
<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>
<UserAttributesDialog />
<UserManageDialog />
<VirtualMachineAttributesDialog />
+ <FedLogin />
</Grid>
);