clusterConfig: ClusterConfigJSON;
}
+export const buildConfig = (clusterConfigJSON: ClusterConfigJSON): Config => {
+ const config = new Config();
+ config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
+ config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
+ config.uuidPrefix = clusterConfigJSON.ClusterID;
+ config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
+ config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
+ config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
+ config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
+ config.loginCluster = clusterConfigJSON.Login.LoginCluster;
+ config.clusterConfig = clusterConfigJSON;
+ mapRemoteHosts(clusterConfigJSON, config);
+ return config;
+};
+
export const fetchConfig = () => {
return Axios
.get<WorkbenchConfig>(WORKBENCH_CONFIG_URL + "?nocache=" + (new Date()).getTime())
throw new Error(`Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`);
}
return Axios.get<ClusterConfigJSON>(getClusterConfigURL(workbenchConfig.API_HOST)).then(response => {
- const config = new Config();
const clusterConfigJSON = response.data;
+ const config = buildConfig(clusterConfigJSON);
const warnLocalConfig = (varName: string) => console.warn(
`A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
}
config.vocabularyUrl = vocabularyUrl;
- config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
- config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
- config.uuidPrefix = clusterConfigJSON.ClusterID;
- config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
- config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
- config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
- config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
- config.loginCluster = clusterConfigJSON.Login.LoginCluster;
- config.clusterConfig = clusterConfigJSON;
- mapRemoteHosts(clusterConfigJSON, config);
-
return { config, apiHost: workbenchConfig.API_HOST };
});
});
};
export const ARVADOS_API_PATH = "arvados/v1";
-export const CLUSTER_CONFIG_URL = "arvados/v1/config";
-export const getClusterConfigURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${CLUSTER_CONFIG_URL}?nocache=${(new Date()).getTime()}`;
+export const CLUSTER_CONFIG_PATH = "arvados/v1/config";
+export const DISCOVERY_DOC_PATH = "discovery/v1/apis/arvados/v1/rest";
+export const getClusterConfigURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
const results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
+
+export function normalizeURLPath(url: string) {
+ const u = new URL(url);
+ u.pathname = u.pathname.replace(/\/\//, '/');
+ if (u.pathname[u.pathname.length - 1] === '/') {
+ u.pathname = u.pathname.substr(0, u.pathname.length - 1);
+ }
+ return u.toString();
+}
import { configureStore, RootStore } from '~/store/store';
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "~/views-components/api-token/api-token";
+import { AddSession } from "~/views-components/add-session/add-session";
import { initAuth } from "~/store/auth/auth-action";
import { createServices } from "~/services/services";
import { MuiThemeProvider } from '@material-ui/core/styles';
store.dispatch(loadFileViewersConfig);
const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={true} {...props} />;
+ const AddSessionComponent = (props: any) => <AddSession {...props} />;
const FedTokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={false} {...props} />;
const MainPanelComponent = (props: any) => <MainPanel {...props} />;
<Switch>
<Route path={Routes.TOKEN} component={TokenComponent} />
<Route path={Routes.FED_LOGIN} component={FedTokenComponent} />
+ <Route path={Routes.ADD_SESSION} component={AddSessionComponent} />
<Route path={Routes.ROOT} component={MainPanelComponent} />
</Switch>
</ConnectedRouter>
clusterId: string;
remoteHost: string;
baseUrl: string;
- username: string;
+ name: string;
email: string;
token: string;
+ uuid: string;
loggedIn: boolean;
status: SessionStatus;
active: boolean;
ROOT: '/',
TOKEN: '/token',
FED_LOGIN: '/fedtoken',
+ ADD_SESSION: '/add-session',
PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
} else if (config.remoteHostsConfig[cls]) {
let u: URL;
if (config.remoteHostsConfig[cls].workbench2Url) {
- u = new URL(config.remoteHostsConfig[cls].workbench2Url || "");
+ /* NOTE: wb2 presently doesn't support passing api_token
+ to arbitrary page to set credentials, only through
+ api-token route. So for navigation to work, user needs
+ to already be logged in. In the future we want to just
+ request the records and display in the current
+ workbench instance making this redirect unnecessary. */
+ 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;
matchPath(route, { path: Routes.TOKEN });
export const matchFedTokenRoute = (route: string) =>
- matchPath(route, {path: Routes.FED_LOGIN});
+ matchPath(route, { path: Routes.FED_LOGIN });
export const matchUsersRoute = (route: string) =>
matchPath(route, { path: Routes.USERS });
clusterId: cfg.uuidPrefix,
remoteHost: cfg.rootUrl,
baseUrl: cfg.baseUrl,
- username: getUserFullname(user),
+ name: getUserFullname(user),
email: user ? user.email : '',
token: this.getApiToken(),
loggedIn: true,
active: true,
+ uuid: user ? user.uuid : '',
status: SessionStatus.VALIDATED
} as Session;
const localSessions = this.getSessions().map(s => ({
clusterId,
remoteHost,
baseUrl: '',
- username: '',
+ name: '',
email: '',
token: '',
loggedIn: false,
active: false,
+ uuid: '',
status: SessionStatus.INVALIDATED
} as Session;
});
const cfg: AxiosRequestConfig = { params: CommonResourceService.mapKeys(_.snakeCase)(params) };
if (session) {
cfg.baseURL = session.baseUrl;
+ cfg.headers = { 'Authorization': 'Bearer ' + session.token };
}
const response = await CommonResourceService.defaultResponse(
import Axios from "axios";
import { getUserFullname, User } from "~/models/user";
import { authActions } from "~/store/auth/auth-action";
-import { Config, ClusterConfigJSON, CLUSTER_CONFIG_URL, ARVADOS_API_PATH } from "~/common/config";
+import {
+ Config, ClusterConfigJSON, CLUSTER_CONFIG_PATH, DISCOVERY_DOC_PATH,
+ buildConfig, mockClusterConfigJSON
+} from "~/common/config";
+import { normalizeURLPath } from "~/common/url";
import { Session, SessionStatus } from "~/models/session";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
import { AuthService, UserDetailsResponse } from "~/services/auth-service/auth-service";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import * as jsSHA from "jssha";
-const getRemoteHostBaseUrl = async (remoteHost: string): Promise<string | null> => {
+const getClusterConfig = async (origin: string): Promise<Config | null> => {
+ // Try the new public config endpoint
+ try {
+ const config = (await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_PATH}`)).data;
+ return buildConfig(config);
+ } catch { }
+
+ // Fall back to discovery document
+ try {
+ const config = (await Axios.get<any>(`${origin}/${DISCOVERY_DOC_PATH}`)).data;
+ return {
+ baseUrl: normalizeURLPath(config.baseUrl),
+ keepWebServiceUrl: config.keepWebServiceUrl,
+ remoteHosts: config.remoteHosts,
+ rootUrl: config.rootUrl,
+ uuidPrefix: config.uuidPrefix,
+ websocketUrl: config.websocketUrl,
+ workbenchUrl: config.workbenchUrl,
+ workbench2Url: config.workbench2Url,
+ loginCluster: "",
+ vocabularyUrl: "",
+ fileViewersConfigUrl: "",
+ clusterConfig: mockClusterConfigJSON({})
+ };
+ } catch { }
+
+ return null;
+};
+
+const getRemoteHostConfig = async (remoteHost: string): Promise<Config | null> => {
let url = remoteHost;
if (url.indexOf('://') < 0) {
url = 'https://' + url;
}
const origin = new URL(url).origin;
- let baseUrl: string | null = null;
+ // Maybe it is an API server URL, try fetching config and discovery doc
+ let r = await getClusterConfig(origin);
+ if (r !== null) {
+ return r;
+ }
+
+ // Maybe it is a Workbench2 URL, try getting config.json
try {
- const resp = await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_URL}`);
- baseUrl = `${resp.data.Services.Controller.ExternalURL}/${ARVADOS_API_PATH}`;
- } catch (err) {
- try {
- const resp = await Axios.get<any>(`${origin}/status.json`);
- baseUrl = resp.data.apiBaseURL;
- } catch (err) {
+ r = await getClusterConfig((await Axios.get<any>(`${origin}/config.json`)).data.API_HOST);
+ if (r !== null) {
+ return r;
}
- }
+ } catch { }
- if (baseUrl && baseUrl[baseUrl.length - 1] === '/') {
- baseUrl = baseUrl.substr(0, baseUrl.length - 1);
- }
+ // Maybe it is a Workbench1 URL, try getting status.json
+ try {
+ r = await getClusterConfig((await Axios.get<any>(`${origin}/status.json`)).data.apiBaseURL);
+ if (r !== null) {
+ return r;
+ }
+ } catch { }
- return baseUrl;
+ return null;
};
const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
return resp.data;
};
-const getTokenUuid = async (baseUrl: string, token: string): Promise<string> => {
- if (token.startsWith("v2/")) {
- const uuid = token.split("/")[1];
- return Promise.resolve(uuid);
- }
+const invalidV2Token = "Must be a v2 token";
- const resp = await Axios.get(`${baseUrl}api_client_authorizations`, {
- headers: {
- Authorization: `OAuth2 ${token}`
- },
- data: {
- filters: JSON.stringify([['api_token', '=', token]])
- }
- });
-
- return resp.data.items[0].uuid;
-};
-
-export const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
+export const getSaltedToken = (clusterId: string, token: string) => {
const shaObj = new jsSHA("SHA-1", "TEXT");
- let secret = token;
- if (token.startsWith("v2/")) {
- secret = token.split("/")[2];
+ const [ver, uuid, secret] = token.split("/");
+ if (ver !== "v2") {
+ throw new Error(invalidV2Token);
}
- shaObj.setHMACKey(secret, "TEXT");
- shaObj.update(clusterId);
- const hmac = shaObj.getHMAC("HEX");
- return `v2/${tokenUuid}/${hmac}`;
+ let salted = secret;
+ if (uuid.substr(0, 5) !== clusterId) {
+ shaObj.setHMACKey(secret, "TEXT");
+ shaObj.update(clusterId);
+ salted = shaObj.getHMAC("HEX");
+ }
+ return `v2/${uuid}/${salted}`;
};
-const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{ user: User, token: string }> => {
- const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
- const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
- const user = await getUserDetails(baseUrl, saltedToken);
+export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
+
+export const validateCluster = async (config: Config, useToken: string):
+ Promise<{ user: User; token: string }> => {
+
+ const saltedToken = getSaltedToken(config.uuidPrefix, useToken);
+ const user = await getUserDetails(config.baseUrl, saltedToken);
return {
user: {
firstName: user.first_name,
username: user.username,
prefs: user.prefs
},
- token: saltedToken
+ token: saltedToken,
};
};
-export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
-
-export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
- const baseUrl = await getRemoteHostBaseUrl(remoteHost);
- if (!baseUrl) {
- return Promise.reject(`Could not find base url for ${remoteHost}`);
- }
- const { user, token } = await clusterLogin(clusterId, baseUrl, activeSession);
- return { baseUrl, user, token };
-};
-
export const validateSession = (session: Session, activeSession: Session) =>
async (dispatch: Dispatch): Promise<Session> => {
dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
session.loggedIn = false;
- try {
- const { baseUrl, user, token } = await validateCluster(session.remoteHost, session.clusterId, activeSession);
+
+ const setupSession = (baseUrl: string, user: User, token: string) => {
session.baseUrl = baseUrl;
session.token = token;
session.email = user.email;
- session.username = getUserFullname(user);
+ session.uuid = user.uuid;
+ session.name = getUserFullname(user);
session.loggedIn = true;
- } catch {
- session.loggedIn = false;
- } finally {
- session.status = SessionStatus.VALIDATED;
- dispatch(authActions.UPDATE_SESSION(session));
+ };
+
+ let fail: Error | null = null;
+ const config = await getRemoteHostConfig(session.remoteHost);
+ if (config !== null) {
+ dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config }));
+ try {
+ const { user, token } = await validateCluster(config, session.token);
+ setupSession(config.baseUrl, user, token);
+ } catch (e) {
+ fail = new Error(`Getting current user for ${session.remoteHost}: ${e.message}`);
+ try {
+ const { user, token } = await validateCluster(config, activeSession.token);
+ setupSession(config.baseUrl, user, token);
+ fail = null;
+ } catch (e2) {
+ if (e.message === invalidV2Token) {
+ fail = new Error(`Getting current user for ${session.remoteHost}: ${e2.message}`);
+ }
+ }
+ }
+ } else {
+ fail = new Error(`Could not get config for ${session.remoteHost}`);
}
+ session.status = SessionStatus.VALIDATED;
+ dispatch(authActions.UPDATE_SESSION(session));
+
+ if (fail) {
+ throw fail;
+ }
+
return session;
};
dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
for (const session of sessions) {
if (session.status === SessionStatus.INVALIDATED) {
- await dispatch(validateSession(session, activeSession));
+ try {
+ /* Here we are dispatching a function, not an
+ action. This is legal (it calls the
+ function with a 'Dispatch' object as the
+ first parameter) but the typescript
+ annotations don't understand this case, so
+ we get an error from typescript unless
+ override it using Dispatch<any>. This
+ pattern is used in a bunch of different
+ places in Workbench2. */
+ await dispatch(validateSession(session, activeSession));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ kind: SnackbarKind.ERROR
+ }));
+ }
}
}
- services.authService.saveSessions(sessions);
+ services.authService.saveSessions(getState().auth.sessions);
dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
}
};
-export const addSession = (remoteHost: string) =>
+export const addSession = (remoteHost: string, token?: string, sendToLogin?: boolean) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const sessions = getState().auth.sessions;
const activeSession = getActiveSession(sessions);
- if (activeSession) {
- const clusterId = remoteHost.match(/^(\w+)\./)![1];
- if (sessions.find(s => s.clusterId === clusterId)) {
- return Promise.reject("Cluster already exists");
+ let useToken: string | null = null;
+ if (token) {
+ useToken = token;
+ } else if (activeSession) {
+ useToken = activeSession.token;
+ }
+
+ if (useToken) {
+ const config = await getRemoteHostConfig(remoteHost);
+ if (!config) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: `Could not get config for ${remoteHost}`,
+ kind: SnackbarKind.ERROR
+ }));
+ return;
}
+
try {
- const { baseUrl, user, token } = await validateCluster(remoteHost, clusterId, activeSession);
+ dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config }));
+ const { user, token } = await validateCluster(config, useToken);
const session = {
loggedIn: true,
status: SessionStatus.VALIDATED,
active: false,
email: user.email,
- username: getUserFullname(user),
+ name: getUserFullname(user),
+ uuid: user.uuid,
+ baseUrl: config.baseUrl,
+ clusterId: config.uuidPrefix,
remoteHost,
- baseUrl,
- clusterId,
token
};
- dispatch(authActions.ADD_SESSION(session));
+ if (sessions.find(s => s.clusterId === config.uuidPrefix)) {
+ await dispatch(authActions.UPDATE_SESSION(session));
+ } else {
+ await dispatch(authActions.ADD_SESSION(session));
+ }
services.authService.saveSessions(getState().auth.sessions);
return session;
- } catch (e) {
+ } catch {
+ if (sendToLogin) {
+ const rootUrl = new URL(config.baseUrl);
+ rootUrl.pathname = "";
+ window.location.href = `${rootUrl.toString()}/login?return_to=` + encodeURI(`${window.location.protocol}//${window.location.host}/add-session?baseURL=` + encodeURI(rootUrl.toString()));
+ return;
+ }
}
}
- return Promise.reject("Could not validate cluster");
+ return Promise.reject(new Error("Could not validate cluster"));
};
-export const toggleSession = (session: Session) =>
+
+export const removeSession = (clusterId: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- let s = { ...session };
+ await dispatch(authActions.REMOVE_SESSION(clusterId));
+ services.authService.saveSessions(getState().auth.sessions);
+ };
+
+export const toggleSession = (session: Session) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const s: Session = { ...session };
if (session.loggedIn) {
s.loggedIn = false;
+ dispatch(authActions.UPDATE_SESSION(s));
} else {
const sessions = getState().auth.sessions;
const activeSession = getActiveSession(sessions);
if (activeSession) {
- s = await dispatch<any>(validateSession(s, activeSession)) as Session;
+ try {
+ await dispatch(validateSession(s, activeSession));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ kind: SnackbarKind.ERROR
+ }));
+ s.loggedIn = false;
+ dispatch(authActions.UPDATE_SESSION(s));
+ }
}
}
- dispatch(authActions.UPDATE_SESSION(s));
services.authService.saveSessions(getState().auth.sessions);
};
export const initSessions = (authService: AuthService, config: Config, user: User) =>
(dispatch: Dispatch<any>) => {
const sessions = authService.buildSessions(config, user);
- authService.saveSessions(sessions);
dispatch(authActions.SET_SESSIONS(sessions));
dispatch(validateSessions());
};
"remoteHost": "https://zzzzz.arvadosapi.com",
"status": 2,
"token": "token",
- "username": "John Doe"
+ "name": "John Doe"
+ "uuid": "zzzzz-tpzed-abcefg",
}, {
"active": false,
"baseUrl": "",
"remoteHost": "xc59z.arvadosapi.com",
"status": 1,
"token": "",
- "username": ""
+ "name": "",
+ "uuid": "",
}],
user: {
email: "test@test.com",
import { SshKeyResource } from '~/models/ssh-key';
import { User, UserResource } from "~/models/user";
import { Session } from "~/models/session";
-import { getClusterConfigURL, Config, ClusterConfigJSON, mapRemoteHosts } from '~/common/config';
+import { Config } from '~/common/config';
import { initSessions } from "~/store/auth/auth-action-session";
import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
import { matchTokenRoute, matchFedTokenRoute } from '~/routes/routes';
-import Axios from "axios";
import { AxiosError } from "axios";
export const authActions = unionize({
const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const user = services.authService.getUser();
const token = services.authService.getApiToken();
- const homeCluster = services.authService.getHomeCluster();
+ let homeCluster = services.authService.getHomeCluster();
if (token) {
setAuthorizationHeader(services, token);
}
+ if (homeCluster && !config.remoteHosts[homeCluster]) {
+ homeCluster = undefined;
+ }
dispatch(authActions.CONFIG({ config }));
dispatch(authActions.SET_HOME_CLUSTER(config.loginCluster || homeCluster || config.uuidPrefix));
+ document.title = `Arvados Workbench (${config.uuidPrefix})`;
if (token && user) {
dispatch(authActions.INIT({ user, token }));
dispatch<any>(initSessions(services.authService, config, user));
if (err.response) {
// Bad token
if (err.response.status === 401) {
- logout()(dispatch, getState, services);
+ dispatch<any>(logout());
}
}
});
}
- Object.keys(config.remoteHosts).map((k) => {
- Axios.get<ClusterConfigJSON>(getClusterConfigURL(config.remoteHosts[k]))
- .then(response => {
- const remoteConfig = new Config();
- remoteConfig.uuidPrefix = response.data.ClusterID;
- remoteConfig.workbench2Url = response.data.Services.Workbench2.ExternalURL;
- remoteConfig.loginCluster = response.data.Login.LoginCluster;
- mapRemoteHosts(response.data, remoteConfig);
- dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config: remoteConfig }));
- });
- });
};
export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
return;
}
- try {
- const params = getParams(dataExplorer, searchValue);
-
- const responses = await Promise.all(sessions.map(session =>
- this.services.groupsService.contents('', params, session)
- ));
-
- const initial = {
- itemsAvailable: 0,
- items: [] as GroupContentsResource[],
- kind: '',
- offset: 0,
- limit: 10
- };
-
- const mergedResponse = responses.reduce((merged, current) => ({
- ...merged,
- itemsAvailable: merged.itemsAvailable + current.itemsAvailable,
- items: merged.items.concat(current.items)
- }), initial);
-
- api.dispatch(updateResources(mergedResponse.items));
-
- api.dispatch(criteriaChanged
- ? setItems(mergedResponse)
- : appendItems(mergedResponse));
-
- } catch {
- api.dispatch(couldNotFetchSearchResults());
+ const params = getParams(dataExplorer, searchValue);
+
+ const initial = {
+ itemsAvailable: 0,
+ items: [] as GroupContentsResource[],
+ kind: '',
+ offset: 0,
+ limit: 10
+ };
+
+ if (criteriaChanged) {
+ api.dispatch(setItems(initial));
}
+
+ sessions.map(session =>
+ this.services.groupsService.contents('', params, session)
+ .then((response) => {
+ api.dispatch(updateResources(response.items));
+ api.dispatch(appendItems(response));
+ }).catch(() => {
+ api.dispatch(couldNotFetchSearchResults(session.clusterId));
+ })
+ );
}
}
items: listResults.items.map(resource => resource.uuid),
});
-const couldNotFetchSearchResults = () =>
+const couldNotFetchSearchResults = (cluster: string) =>
snackbarActions.OPEN_SNACKBAR({
- message: `Could not fetch search results for some sessions.`,
+ message: `Could not fetch search results from ${cluster}.`,
kind: SnackbarKind.ERROR
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RouteProps } from "react-router";
+import * as React from "react";
+import { connect, DispatchProp } from "react-redux";
+import { getUrlParameter } from "~/common/url";
+import { navigateToSiteManager } from "~/store/navigation/navigation-action";
+import { addSession } from "~/store/auth/auth-action-session";
+
+export const AddSession = connect()(
+ class extends React.Component<RouteProps & DispatchProp<any>, {}> {
+ componentDidMount() {
+ const search = this.props.location ? this.props.location.search : "";
+ const apiToken = getUrlParameter(search, 'api_token');
+ const baseURL = getUrlParameter(search, 'baseURL');
+
+ this.props.dispatch(addSession(baseURL, apiToken));
+ this.props.dispatch(navigateToSiteManager);
+ }
+ render() {
+ return <div />;
+ }
+ }
+);
{loggedIn.length === 1 ?
<span>Searching local cluster <ResourceCluster uuid={props.localCluster} /></span>
: <span>Searching clusters: {loggedIn.map((ss) => <span key={ss.clusterId}>
- <a href={props.remoteHostsConfig[ss.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={ss.clusterId} /></a></span>)}</span>}
+ <a href={props.remoteHostsConfig[ss.clusterId] && props.remoteHostsConfig[ss.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={ss.clusterId} /></a>
+ </span>)}</span>}
{loggedIn.length === 1 && props.localCluster !== homeCluster ?
<span>To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></span>
: <span style={{ marginLeft: "2em" }}>Use <Link to={Routes.SITE_MANAGER} >Site Manager</Link> to manage which clusters will be searched.</span>}
CardContent,
CircularProgress,
Grid,
+ IconButton,
StyleRulesCallback,
Table,
TableBody,
import { SITE_MANAGER_REMOTE_HOST_VALIDATION } from "~/validators/validators";
import { Config } from '~/common/config';
import { ResourceCluster } from '~/views-components/data-explorer/renderers';
+import { TrashIcon } from "~/components/icon/icon";
type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' |
'remoteSiteInfo' | 'buttonAdd' | 'buttonLoggedIn' | 'buttonLoggedOut' |
export interface SiteManagerPanelRootActionProps {
toggleSession: (session: Session) => void;
+ removeSession: (session: Session) => void;
}
export interface SiteManagerPanelRootDataProps {
sessions: Session[];
remoteHostsConfig: { [key: string]: Config };
+ localClusterConfig: Config;
}
type SiteManagerPanelRootProps = SiteManagerPanelRootDataProps & SiteManagerPanelRootActionProps & WithStyles<CssRules> & InjectedFormProps;
const submitSession = (remoteHost: string) =>
(dispatch: Dispatch) => {
- dispatch<any>(addSession(remoteHost)).then(() => {
+ dispatch<any>(addSession(remoteHost, undefined, true)).then(() => {
dispatch(reset(SITE_MANAGER_FORM_NAME));
}).catch((e: any) => {
const errors = {
}
}),
withStyles(styles))
- (({ classes, sessions, handleSubmit, toggleSession, remoteHostsConfig }: SiteManagerPanelRootProps) =>
+ (({ classes, sessions, handleSubmit, toggleSession, removeSession, localClusterConfig, remoteHostsConfig }: SiteManagerPanelRootProps) =>
<Card className={classes.root}>
<CardContent>
<Grid container direction="row">
<TableRow className={classes.tableRow}>
<TableCell>Cluster ID</TableCell>
<TableCell>Host</TableCell>
- <TableCell>Username</TableCell>
<TableCell>Email</TableCell>
+ <TableCell>UUID</TableCell>
<TableCell>Status</TableCell>
+ <TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
<a href={remoteHostsConfig[session.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={session.clusterId} /></a>
: session.clusterId}</TableCell>
<TableCell>{session.remoteHost}</TableCell>
- <TableCell>{validating ? <CircularProgress size={20} /> : session.username}</TableCell>
<TableCell>{validating ? <CircularProgress size={20} /> : session.email}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20} /> : session.uuid}</TableCell>
<TableCell className={classes.statusCell}>
<Button fullWidth
disabled={validating || session.status === SessionStatus.INVALIDATED || session.active}
{validating ? "Validating" : (session.loggedIn ? "Logged in" : "Logged out")}
</Button>
</TableCell>
+ <TableCell>
+ {session.clusterId !== localClusterConfig.uuidPrefix &&
+ !localClusterConfig.clusterConfig.RemoteClusters[session.clusterId] &&
+ <IconButton onClick={() => removeSession(session)}>
+ <TrashIcon />
+ </IconButton>}
+ </TableCell>
</TableRow>;
})}
</TableBody>
SiteManagerPanelRootDataProps
} from "~/views/site-manager-panel/site-manager-panel-root";
import { Session } from "~/models/session";
-import { toggleSession } from "~/store/auth/auth-action-session";
+import { toggleSession, removeSession } from "~/store/auth/auth-action-session";
const mapStateToProps = (state: RootState): SiteManagerPanelRootDataProps => {
return {
sessions: state.auth.sessions,
- remoteHostsConfig: state.auth.remoteHostsConfig
+ remoteHostsConfig: state.auth.remoteHostsConfig,
+ localClusterConfig: state.auth.remoteHostsConfig[state.auth.localCluster]
};
};
const mapDispatchToProps = (dispatch: Dispatch): SiteManagerPanelRootActionProps => ({
toggleSession: (session: Session) => {
dispatch<any>(toggleSession(session));
- }
+ },
+ removeSession: (session: Session) => {
+ dispatch<any>(removeSession(session.clusterId));
+ },
});
export const SiteManagerPanel = connect(mapStateToProps, mapDispatchToProps)(SiteManagerPanelRoot);
if (!apiToken || !user || !user.uuid.startsWith(localCluster)) {
return <></>;
}
- const [, tokenUuid, token] = apiToken.split("/");
return <div id={"fedtoken-iframe-div"}>
{Object.keys(remoteHostsConfig)
.map((k) => {
return;
}
const fedtoken = (remoteHostsConfig[k].loginCluster === localCluster)
- ? apiToken : getSaltedToken(k, tokenUuid, token);
+ ? apiToken : getSaltedToken(k, apiToken);
return <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${fedtoken}`} style={{
height: 0,
width: 0,