Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>
test: unit-tests integration-tests
-build: test
+build: yarn-install
VERSION=$(VERSION) yarn build
$(DEB_FILE): build
make integration-tests-in-docker
</pre>
+### Run tests interactively in container
+
+<pre>
+$ xhost +local:root
+$ ARVADOS_DIR=/path/to/arvados
+$ docker run -ti -v$PWD:$PWD -v$ARVADOS_DIR:/usr/src/arvados -w$PWD --env="DISPLAY" --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" workbench2-build /bin/bash
+(inside container)
+# yarn run cypress install
+# tools/run-integration-tests.sh -i -a /usr/src/arvados
+</pre>
+
### Production build
<pre>
yarn install
cy.get('[data-cy=collection-files-panel-options-btn]')
.click()
cy.get('[data-cy=context-menu]')
- .should('contain', 'Download selected')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
+ // .should('contain', 'Download selected')
+ .should(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
.type('{esc}'); // Collapse the options menu
// File item 'more options' button
cy.get('[data-cy=file-item-options-btn')
})
})
})
-})
\ No newline at end of file
+})
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Collection panel tests', function() {
+ let activeUser;
+ let adminUser;
+
+ before(function() {
+ // Only set up common users once. These aren't set up as aliases because
+ // aliases are cleaned up after every test. Also it doesn't make sense
+ // to set the same users on beforeEach() over and over again, so we
+ // separate a little from Cypress' 'Best Practices' here.
+ cy.getUser('admin', 'Admin', 'User', true, true)
+ .as('adminUser').then(function() {
+ adminUser = this.adminUser;
+ }
+ );
+ cy.getUser('collectionuser1', 'Collection', 'User', false, true)
+ .as('activeUser').then(function() {
+ activeUser = this.activeUser;
+ }
+ );
+ })
+
+ beforeEach(function() {
+ cy.clearCookies()
+ cy.clearLocalStorage()
+ })
+
+ it('checks that Public favorites does not appear under shared with me', function() {
+ cy.loginAs(adminUser);
+ cy.contains('Shared with me').click();
+ cy.get('main').contains('Public favorites').should('not.exist');
+ })
+
+ it('creates and removes a public favorite', function() {
+ cy.loginAs(adminUser);
+ cy.createGroup(adminUser.token, {
+ name: `my-favorite-project`,
+ group_class: 'project',
+ }).as('myFavoriteProject').then(function() {
+ cy.contains('Refresh').click();
+ cy.get('main').contains('my-favorite-project').rightclick();
+ cy.contains('Add to public favorites').click();
+ cy.contains('Public Favorites').click();
+ cy.get('main').contains('my-favorite-project').rightclick();
+ cy.contains('Remove from public favorites').click();
+ cy.get('main').contains('my-favorite-project').should('not.exist');
+ cy.trashGroup(adminUser.token, this.myFavoriteProject.uuid);
+ });
+ })
+})
}
)
+Cypress.Commands.add(
+ "trashGroup", (token, uuid) => {
+ return cy.deleteResource(token, 'groups', uuid);
+ }
+)
+
Cypress.Commands.add(
"createCollection", (token, data) => {
return cy.createResource(token, 'collections', {
}
)
+Cypress.Commands.add(
+ "deleteResource", (token, suffix, uuid) => {
+ return cy.doRequest('DELETE', '/arvados/v1/'+suffix+'/'+uuid)
+ .its('body').as('resource')
+ .then(function() {
+ return this.resource;
+ })
+ }
+)
+
Cypress.Commands.add(
"loginAs", (user) => {
cy.visit(`/token/?api_token=${user.token}`);
cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)');
cy.get('div#root').should('not.contain', 'Your account is inactive');
}
-)
\ No newline at end of file
+)
apiRevision: number;
}
-export const buildConfig = (clusterConfigJSON: ClusterConfigJSON): Config => {
+export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
+ const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
const config = new Config();
config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
}
});
- return {...config, Services: svcs};
+ return { ...config, Services: svcs };
};
export const fetchConfig = () => {
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(async response => {
- const clusterConfigJSON = removeTrailingSlashes(response.data);
- const apiRevision = await getApiRevision(clusterConfigJSON.Services.Controller.ExternalURL);
- const config = { ...buildConfig(clusterConfigJSON), apiRevision };
+ const apiRevision = await getApiRevision(response.data.Services.Controller.ExternalURL.replace(/\/+$/, ''));
+ const config = { ...buildConfig(response.data), apiRevision };
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}`);
fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
}
else {
- fileViewerConfigUrl = clusterConfigJSON.Workbench.FileViewersConfigURL || "/file-viewers-example.json";
+ fileViewerConfigUrl = config.clusterConfig.Workbench.FileViewersConfigURL || "/file-viewers-example.json";
}
config.fileViewersConfigUrl = fileViewerConfigUrl;
vocabularyUrl = workbenchConfig.VOCABULARY_URL;
}
else {
- vocabularyUrl = clusterConfigJSON.Workbench.VocabularyURL || "/vocabulary-example.json";
+ vocabularyUrl = config.clusterConfig.Workbench.VocabularyURL || "/vocabulary-example.json";
}
config.vocabularyUrl = vocabularyUrl;
expect(request).toBeInstanceOf(XMLHttpRequest);
});
- it('COPY - adds baseURL to Destination header', async () => {
+ it('COPY - adds baseURL with trailing slash to Destination header', async () => {
const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
const webdav = new WebDAV(undefined, createRequest);
webdav.defaults.baseURL = 'base/';
expect(request).toBeInstanceOf(XMLHttpRequest);
});
+ it('COPY - adds baseURL without trailing slash to Destination header', async () => {
+ const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ webdav.defaults.baseURL = 'base';
+ const promise = webdav.copy('foo', 'foo-copy');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('COPY', 'base/foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'base/foo-copy');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
it('MOVE', async () => {
const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
const webdav = new WebDAV(undefined, createRequest);
- const promise = webdav.move('foo', 'foo-copy');
+ const promise = webdav.move('foo', 'foo-moved');
load();
const request = await promise;
expect(open).toHaveBeenCalledWith('MOVE', 'foo');
- expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'foo-copy');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'foo-moved');
expect(request).toBeInstanceOf(XMLHttpRequest);
});
- it('MOVE - adds baseURL to Destination header', async () => {
+ it('MOVE - adds baseURL with trailing slash to Destination header', async () => {
const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
const webdav = new WebDAV(undefined, createRequest);
webdav.defaults.baseURL = 'base/';
expect(request).toBeInstanceOf(XMLHttpRequest);
});
+ it('MOVE - adds baseURL without trailing slash to Destination header', async () => {
+ const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ webdav.defaults.baseURL = 'base';
+ const promise = webdav.move('foo', 'foo-moved');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('MOVE', 'base/foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'base/foo-moved');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
it('DELETE', async () => {
const { open, load, createRequest } = mockCreateRequest();
const webdav = new WebDAV(undefined, createRequest);
this.request({
...config, url,
method: 'COPY',
- headers: { ...config.headers, Destination: this.defaults.baseURL + destination }
+ headers: {
+ ...config.headers,
+ Destination: this.defaults.baseURL
+ ? this.defaults.baseURL.replace(/\/+$/, '') + '/' + destination.replace(/^\/+/, '')
+ : destination
+ }
})
move = (url: string, destination: string, config: WebDAVRequestConfig = {}) =>
this.request({
...config, url,
method: 'MOVE',
- headers: { ...config.headers, Destination: this.defaults.baseURL + destination }
+ headers: {
+ ...config.headers,
+ Destination: this.defaults.baseURL
+ ? this.defaults.baseURL.replace(/\/+$/, '') + '/' + destination.replace(/^\/+/, '')
+ : destination
+ }
})
delete = (url: string, config: WebDAVRequestConfig = {}) =>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { configure, shallow, mount } from "enzyme";
+import { WithStyles } from "@material-ui/core";
+import * as Adapter from "enzyme-adapter-react-16";
+import { TreeItem, TreeItemStatus } from '../tree/tree';
+import { FileTreeData } from '../file-tree/file-tree-data';
+import { CollectionFileType } from "../../models/collection-file";
+import { CollectionPanelFilesComponent, CollectionPanelFilesProps, CssRules } from './collection-panel-files';
+import { SearchInput } from '../search-input/search-input';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('~/components/file-tree/file-tree', () => ({
+ FileTree: () => 'FileTree',
+}));
+
+describe('<CollectionPanelFiles />', () => {
+ let props: CollectionPanelFilesProps & WithStyles<CssRules>;
+
+ beforeEach(() => {
+ props = {
+ classes: {} as Record<CssRules, string>,
+ items: [],
+ isWritable: true,
+ isLoading: false,
+ tooManyFiles: false,
+ onUploadDataClick: jest.fn(),
+ onSearchChange: jest.fn(),
+ onItemMenuOpen: jest.fn(),
+ onOptionsMenuOpen: jest.fn(),
+ onSelectionToggle: jest.fn(),
+ onCollapseToggle: jest.fn(),
+ onFileClick: jest.fn(),
+ loadFilesFunc: jest.fn(),
+ currentItemUuid: '',
+ };
+ });
+
+ it('renders properly', () => {
+ // when
+ const wrapper = shallow(<CollectionPanelFilesComponent {...props} />);
+
+ // then
+ expect(wrapper).not.toBeUndefined();
+ });
+
+ it('filters out files', () => {
+ // given
+ const searchPhrase = 'test';
+ const items: Array<TreeItem<FileTreeData>> = [
+ {
+ data: {
+ url: '',
+ type: CollectionFileType.DIRECTORY,
+ name: 'test',
+ },
+ id: '1',
+ open: true,
+ active: true,
+ status: TreeItemStatus.LOADED,
+ },
+ {
+ data: {
+ url: '',
+ type: CollectionFileType.FILE,
+ name: 'test123',
+ },
+ id: '2',
+ open: true,
+ active: true,
+ status: TreeItemStatus.LOADED,
+ },
+ {
+ data: {
+ url: '',
+ type: CollectionFileType.FILE,
+ name: 'another-file',
+ },
+ id: '3',
+ open: true,
+ active: true,
+ status: TreeItemStatus.LOADED,
+ }
+ ];
+
+ // setup
+ props.items = items;
+ const wrapper = mount(<CollectionPanelFilesComponent {...props} />);
+ wrapper.find(SearchInput).simulate('change', { target: { value: searchPhrase } });
+
+ // when
+ setTimeout(() => { // we have to use set timeout because of the debounce
+ expect(wrapper.find('FileTree').prop('items'))
+ .toEqual([
+ {
+ data: { url: '', type: 'directory', name: 'test' },
+ id: '1',
+ open: true,
+ active: true,
+ status: 'loaded'
+ },
+ {
+ data: { url: '', type: 'file', name: 'test123' },
+ id: '2',
+ open: true,
+ active: true,
+ status: 'loaded'
+ }
+ ]);
+ }, 0);
+ });
+});
\ No newline at end of file
import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, Card, Button, Tooltip, CircularProgress } from '@material-ui/core';
import { CustomizeTableIcon } from '~/components/icon/icon';
import { DownloadIcon } from '~/components/icon/icon';
+import { SearchInput } from '../search-input/search-input';
export interface CollectionPanelFilesProps {
items: Array<TreeItem<FileTreeData>>;
isLoading: boolean;
tooManyFiles: boolean;
onUploadDataClick: () => void;
+ onSearchChange: (searchValue: string) => void;
onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>, isWritable: boolean) => void;
onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => void;
onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
currentItemUuid?: string;
}
-type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button' | 'centeredLabel';
+export type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button' | 'centeredLabel' | 'cardHeaderContent' | 'cardHeaderContentTitle';
const styles: StyleRulesCallback<CssRules> = theme => ({
root: {
},
cardSubheader: {
paddingTop: 0,
- paddingBottom: 0
+ paddingBottom: 0,
+ minHeight: 8 * theme.spacing.unit,
+ },
+ cardHeaderContent: {
+ display: 'flex',
+ paddingRight: 2 * theme.spacing.unit,
+ justifyContent: 'space-between',
+ },
+ cardHeaderContentTitle: {
+ paddingLeft: theme.spacing.unit,
+ paddingTop: 2 * theme.spacing.unit,
+ paddingRight: 2 * theme.spacing.unit,
},
nameHeader: {
marginLeft: '75px'
},
button: {
marginRight: -theme.spacing.unit,
- marginTop: '0px'
+ marginTop: '8px'
},
centeredLabel: {
fontSize: '0.875rem',
},
});
-export const CollectionPanelFiles =
- withStyles(styles)(
- ({ onItemMenuOpen, onOptionsMenuOpen, onUploadDataClick, classes,
- isWritable, isLoading, tooManyFiles, loadFilesFunc, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
- <Card data-cy='collection-files-panel' className={classes.root}>
- <CardHeader
- title="Files"
- className={classes.cardSubheader}
- classes={{ action: classes.button }}
- action={<>
- {isWritable &&
- <Button
- data-cy='upload-button'
- onClick={onUploadDataClick}
- variant='contained'
- color='primary'
- size='small'>
- <DownloadIcon className={classes.uploadIcon} />
- Upload data
- </Button>}
- {!tooManyFiles &&
- <Tooltip title="More options" disableFocusListener>
- <IconButton
- data-cy='collection-files-panel-options-btn'
- onClick={(ev) => onOptionsMenuOpen(ev, isWritable)}>
- <CustomizeTableIcon />
- </IconButton>
- </Tooltip>}
- </>
- } />
- { tooManyFiles
- ? <div className={classes.centeredLabel}>
- File listing may take some time, please click to browse: <Button onClick={loadFilesFunc}><DownloadIcon/>Show files</Button>
+export const CollectionPanelFilesComponent = ({ onItemMenuOpen, onSearchChange, onOptionsMenuOpen, onUploadDataClick, classes,
+ isWritable, isLoading, tooManyFiles, loadFilesFunc, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) => {
+ const { useState, useEffect } = React;
+ const [searchValue, setSearchValue] = useState('');
+
+ useEffect(() => {
+ onSearchChange(searchValue);
+ }, [searchValue]);
+
+ return (<Card data-cy='collection-files-panel' className={classes.root}>
+ <CardHeader
+ title={
+ <div className={classes.cardHeaderContent}>
+ <span className={classes.cardHeaderContentTitle}>Files</span>
+ <SearchInput
+ value={searchValue}
+ onSearch={setSearchValue} />
</div>
- : <>
- <Grid container justify="space-between">
- <Typography variant="caption" className={classes.nameHeader}>
- Name
- </Typography>
- <Typography variant="caption" className={classes.fileSizeHeader}>
- File size
- </Typography>
- </Grid>
- { isLoading
+ }
+ className={classes.cardSubheader}
+ classes={{ action: classes.button }}
+ action={<>
+ {isWritable &&
+ <Button
+ data-cy='upload-button'
+ onClick={onUploadDataClick}
+ variant='contained'
+ color='primary'
+ size='small'>
+ <DownloadIcon className={classes.uploadIcon} />
+ Upload data
+ </Button>}
+ {!tooManyFiles &&
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton
+ data-cy='collection-files-panel-options-btn'
+ onClick={(ev) => onOptionsMenuOpen(ev, isWritable)}>
+ <CustomizeTableIcon />
+ </IconButton>
+ </Tooltip>}
+ </>
+ } />
+ {tooManyFiles
+ ? <div className={classes.centeredLabel}>
+ File listing may take some time, please click to browse: <Button onClick={loadFilesFunc}><DownloadIcon />Show files</Button>
+ </div>
+ : <>
+ <Grid container justify="space-between">
+ <Typography variant="caption" className={classes.nameHeader}>
+ Name
+ </Typography>
+ <Typography variant="caption" className={classes.fileSizeHeader}>
+ File size
+ </Typography>
+ </Grid>
+ {isLoading
? <div className={classes.centeredLabel}><CircularProgress /></div>
- : <div style={{height: 'calc(100% - 60px)'}}><FileTree onMenuOpen={(ev, item) => onItemMenuOpen(ev, item, isWritable)} {...treeProps} /></div> }
- </>
- }
- </Card>);
+ : <div style={{ height: 'calc(100% - 60px)' }}>
+ <FileTree
+ onMenuOpen={(ev, item) => onItemMenuOpen(ev, item, isWritable)}
+ {...treeProps}
+ items={treeProps.items} /></div>}
+ </>
+ }
+ </Card>);
+};
+
+export const CollectionPanelFiles = withStyles(styles)(CollectionPanelFilesComponent);
render() {
return <form onSubmit={this.handleSubmit}>
<FormControl>
- <InputLabel>Search</InputLabel>
+ <InputLabel>Search files</InputLabel>
<Input
type="text"
value={this.state.value}
onChange={this.handleChange}
endAdornment={
<InputAdornment position="end">
- <Tooltip title='Search'>
+ <Tooltip title='Search files'>
<IconButton
onClick={this.handleSubmit}>
<SearchIcon />
loggedIn: boolean;
status: SessionStatus;
active: boolean;
+ userIsActive: boolean;
apiRevision: number;
}
export const mapTreeValues = <T, R>(mapFn: (value: T) => R) => (tree: Tree<T>): Tree<R> =>
getNodeDescendantsIds('')(tree)
.map(id => getNode(id)(tree))
+ .filter(node => !!node)
.map(mapNodeValue(mapFn))
.reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
return this.getStorage().getItem(HOME_CLUSTER) || undefined;
}
+ public getApiClient() {
+ return this.apiClient;
+ }
+
public removeUser() {
this.getStorage().removeItem(USER_EMAIL_KEY);
this.getStorage().removeItem(USER_FIRST_NAME_KEY);
window.location.assign(`${this.baseUrl || ""}/logout?return_to=${currentUrl}`);
}
- public getUserDetails = (): Promise<User> => {
+ public getUserDetails = (showErrors?: boolean): Promise<User> => {
const reqId = uuid();
this.actions.progressFn(reqId, true);
return this.apiClient
})
.catch(e => {
this.actions.progressFn(reqId, false);
- this.actions.errorFn(reqId, e);
+ this.actions.errorFn(reqId, e, showErrors);
throw e;
});
}
clusterId: cfg.uuidPrefix,
remoteHost: cfg.rootUrl,
baseUrl: cfg.baseUrl,
- name: user ? getUserDisplayName(user): '',
+ name: user ? getUserDisplayName(user) : '',
email: user ? user.email : '',
+ userIsActive: user ? user.isActive : false,
token: this.getApiToken(),
loggedIn: true,
active: true,
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { RootState } from "~/store/store";
import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
-import Axios from "axios";
+import Axios, { AxiosInstance } from "axios";
import { User, getUserDisplayName } from "~/models/user";
import { authActions } from "~/store/auth/auth-action";
import {
import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import * as jsSHA from "jssha";
-const getClusterConfig = async (origin: string): Promise<Config | null> => {
+const getClusterConfig = async (origin: string, apiClient: AxiosInstance): Promise<Config | null> => {
let configFromDD: Config | undefined;
try {
- const dd = (await Axios.get<any>(`${origin}/${DISCOVERY_DOC_PATH}`)).data;
+ const dd = (await apiClient.get<any>(`${origin}/${DISCOVERY_DOC_PATH}`)).data;
configFromDD = {
baseUrl: normalizeURLPath(dd.baseUrl),
keepWebServiceUrl: dd.keepWebServiceUrl,
};
} catch { }
- // Try the new public config endpoint
+ // Try public config endpoint
try {
- const config = (await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_PATH}`)).data;
- return {...buildConfig(config), apiRevision: configFromDD ? configFromDD.apiRevision : 0};
+ const config = (await apiClient.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_PATH}`)).data;
+ return { ...buildConfig(config), apiRevision: configFromDD ? configFromDD.apiRevision : 0 };
} catch { }
// Fall back to discovery document
return null;
};
-const getRemoteHostConfig = async (remoteHost: string): Promise<Config | null> => {
+const getRemoteHostConfig = async (remoteHost: string, useApiClient?: AxiosInstance): Promise<Config | null> => {
+ const apiClient = useApiClient || Axios.create({ headers: {} });
+
let url = remoteHost;
if (url.indexOf('://') < 0) {
url = 'https://' + url;
const origin = new URL(url).origin;
// Maybe it is an API server URL, try fetching config and discovery doc
- let r = await getClusterConfig(origin);
+ let r = await getClusterConfig(origin, apiClient);
if (r !== null) {
return r;
}
// Maybe it is a Workbench2 URL, try getting config.json
try {
- r = await getClusterConfig((await Axios.get<any>(`${origin}/config.json`)).data.API_HOST);
+ r = await getClusterConfig((await apiClient.get<any>(`${origin}/config.json`)).data.API_HOST, apiClient);
if (r !== null) {
return r;
}
// Maybe it is a Workbench1 URL, try getting status.json
try {
- r = await getClusterConfig((await Axios.get<any>(`${origin}/status.json`)).data.apiBaseURL);
+ r = await getClusterConfig((await apiClient.get<any>(`${origin}/status.json`)).data.apiBaseURL, apiClient);
if (r !== null) {
return r;
}
const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
setAuthorizationHeader(svc, saltedToken);
- const user = await svc.authService.getUserDetails();
+ const user = await svc.authService.getUserDetails(false);
return {
user,
token: saltedToken,
};
};
-export const validateSession = (session: Session, activeSession: Session) =>
+export const validateSession = (session: Session, activeSession: Session, useApiClient?: AxiosInstance) =>
async (dispatch: Dispatch): Promise<Session> => {
dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
session.loggedIn = false;
session.baseUrl = baseUrl;
session.token = token;
session.email = user.email;
+ session.userIsActive = user.isActive;
session.uuid = user.uuid;
session.name = getUserDisplayName(user);
session.loggedIn = true;
};
let fail: Error | null = null;
- const config = await getRemoteHostConfig(session.remoteHost);
+ const config = await getRemoteHostConfig(session.remoteHost, useApiClient);
if (config !== null) {
dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config }));
try {
return session;
};
-export const validateSessions = () =>
+export const validateSessions = (useApiClient?: AxiosInstance) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const sessions = getState().auth.sessions;
const activeSession = getActiveSession(sessions);
override it using Dispatch<any>. This
pattern is used in a bunch of different
places in Workbench2. */
- await dispatch(validateSession(session, activeSession));
+ await dispatch(validateSession(session, activeSession, useApiClient));
} catch (e) {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- kind: SnackbarKind.ERROR
- }));
+ // Don't do anything here. User may get
+ // spammed with multiple messages that are not
+ // helpful. They can see the individual
+ // errors by going to site manager and trying
+ // to toggle the session.
}
}
}
status: SessionStatus.VALIDATED,
active: false,
email: user.email,
+ userIsActive: user.isActive,
name: getUserDisplayName(user),
uuid: user.uuid,
baseUrl: config.baseUrl,
(dispatch: Dispatch<any>) => {
const sessions = authService.buildSessions(config, user);
dispatch(authActions.SET_SESSIONS(sessions));
- dispatch(validateSessions());
+ dispatch(validateSessions(authService.getApiClient()));
};
export const loadSiteManagerPanel = () =>
import { ServiceRepository, createServices } from "~/services/services";
import { configureStore, RootStore } from "../store";
import { createBrowserHistory } from "history";
-import { mockConfig } from '~/common/config';
+import { mockConfig, DISCOVERY_DOC_PATH, } from '~/common/config';
import { ApiActions } from "~/services/api/api-actions";
import { ACCOUNT_LINK_STATUS_KEY } from '~/services/link-account-service/link-account-service';
import Axios from "axios";
prefs: {}
});
+ axiosMock
+ .onGet("https://xc59z.arvadosapi.com/discovery/v1/apis/arvados/v1/rest")
+ .reply(200, {
+ baseUrl: "https://xc59z.arvadosapi.com/arvados/v1",
+ keepWebServiceUrl: "",
+ remoteHosts: {},
+ rootUrl: "https://xc59z.arvadosapi.com",
+ uuidPrefix: "xc59z",
+ websocketUrl: "",
+ workbenchUrl: "",
+ workbench2Url: "",
+ revision: 12345678
+ });
+
importMocks.push(ImportMock.mockFunction(servicesModule, 'createServices', services));
// Only test the case when a link account operation is not being cancelled
"rootUrl": "https://zzzzz.arvadosapi.com",
"uuidPrefix": "zzzzz",
},
+ "xc59z": mockConfig({
+ apiRevision: 12345678,
+ baseUrl: "https://xc59z.arvadosapi.com/arvados/v1",
+ rootUrl: "https://xc59z.arvadosapi.com",
+ uuidPrefix: "xc59z"
+ })
},
remoteHosts: {
zzzzz: "zzzzz.arvadosapi.com",
"name": "John Doe",
"apiRevision": 12345678,
"uuid": "zzzzz-tpzed-abcefg",
+ "userIsActive": true
}, {
"active": false,
"baseUrl": "",
// TODO: Add remaining action tests
/*
- it('should fire external url to login', () => {
- const initialState = undefined;
- window.location.assign = jest.fn();
- reducer(initialState, authActions.LOGIN());
- expect(window.location.assign).toBeCalledWith(
- `/login?return_to=${window.location.protocol}//${window.location.host}/token`
- );
- });
-
- it('should fire external url to logout', () => {
- const initialState = undefined;
- window.location.assign = jest.fn();
- reducer(initialState, authActions.LOGOUT());
- expect(window.location.assign).toBeCalledWith(
- `/logout?return_to=${location.protocol}//${location.host}`
- );
- });
- */
+ it('should fire external url to login', () => {
+ const initialState = undefined;
+ window.location.assign = jest.fn();
+ reducer(initialState, authActions.LOGIN());
+ expect(window.location.assign).toBeCalledWith(
+ `/login?return_to=${window.location.protocol}//${window.location.host}/token`
+ );
+ });
+
+ it('should fire external url to logout', () => {
+ const initialState = undefined;
+ window.location.assign = jest.fn();
+ reducer(initialState, authActions.LOGOUT());
+ expect(window.location.assign).toBeCalledWith(
+ `/logout?return_to=${location.protocol}//${location.host}`
+ );
+ });
+ */
});
TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+ ON_SEARCH_CHANGE: ofType<string>(),
});
export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
import { createTree, mapTreeValues, getNode, setNode, getNodeAncestorsIds, getNodeDescendantsIds, setNodeValueWith, mapTree } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
+let fetchedFiles: any = {};
+
export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
// Low-level tree handling setNode() func does in-place data modifications
// for performance reasons, so we pass a copy of 'state' to avoid side effects.
return collectionPanelFilesAction.match(action, {
- SET_COLLECTION_FILES: files =>
- mergeCollectionPanelFilesStates({...state}, mapTree(mapCollectionFileToCollectionPanelFile)(files)),
+ SET_COLLECTION_FILES: files => {
+ fetchedFiles = files;
+ return mergeCollectionPanelFilesStates({ ...state }, mapTree(mapCollectionFileToCollectionPanelFile)(files));
+ },
TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
- toggleCollapse(data.id)({...state}),
+ toggleCollapse(data.id)({ ...state }),
- TOGGLE_COLLECTION_FILE_SELECTION: data => [{...state}]
+ TOGGLE_COLLECTION_FILE_SELECTION: data => [{ ...state }]
.map(toggleSelected(data.id))
.map(toggleAncestors(data.id))
.map(toggleDescendants(data.id))[0],
+ ON_SEARCH_CHANGE: (searchValue) => {
+ const fileIds: string[] = [];
+ const directoryIds: string[] = [];
+ const filteredFiles = Object.keys(fetchedFiles)
+ .filter((key: string) => {
+ const node = fetchedFiles[key];
+
+ if (node.value === undefined) {
+ return false;
+ }
+
+ const { id, value: { type, name } } = node;
+
+ if (type === CollectionFileType.DIRECTORY) {
+ directoryIds.push(id);
+ return true;
+ }
+
+ const includeFile = name.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
+
+ if (includeFile) {
+ fileIds.push(id);
+ }
+
+ return includeFile;
+ })
+ .reduce((prev, next) => {
+ const node = JSON.parse(JSON.stringify(fetchedFiles[next]));
+ const { value: { type }, children } = node;
+
+ node.children = node.children.filter((key: string) => {
+ const isFile = directoryIds.indexOf(key) === -1;
+ return isFile ?
+ fileIds.indexOf(key) > -1 :
+ !!fileIds.find(id => id.indexOf(key) > -1);
+ });
+
+ if (type === CollectionFileType.FILE || children.length > 0) {
+ prev[next] = node;
+ }
+
+ return prev;
+ }, {});
+
+ return mapTreeValues((v: CollectionPanelDirectory | CollectionPanelFile) => {
+ if (v.type === CollectionFileType.DIRECTORY) {
+ return ({
+ ...v,
+ collapsed: searchValue.length === 0,
+ });
+ }
+
+ return ({ ...v });
+ })({ ...filteredFiles });
+ },
+
SELECT_ALL_COLLECTION_FILES: () =>
- mapTreeValues(v => ({ ...v, selected: true }))({...state}),
+ mapTreeValues(v => ({ ...v, selected: true }))({ ...state }),
UNSELECT_ALL_COLLECTION_FILES: () =>
- mapTreeValues(v => ({ ...v, selected: false }))({...state}),
+ mapTreeValues(v => ({ ...v, selected: false }))({ ...state }),
default: () => state
}) as CollectionPanelFilesState;
export const filterCollectionFilesBySelection = (tree: CollectionPanelFilesState, selected: boolean) => {
const allFiles = getNodeDescendants('')(tree).map(node => node.value);
-
const selectedDirectories = allFiles.filter(file => file.selected === selected && file.type === CollectionFileType.DIRECTORY);
const selectedFiles = allFiles.filter(file => file.selected === selected && !selectedDirectories.some(dir => dir.id === file.path));
return [...selectedDirectories, ...selectedFiles];
manifestText: collection.manifestText,
};
const newCollection = await services.collectionService.create(collectionCopy);
- const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, false).map(file => file.id);
- await services.collectionService.deleteFiles(newCollection.uuid, paths);
+ const copiedFiles = await services.collectionService.files(newCollection.uuid);
+ const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true).map(file => file.id);
+ const filesToDelete = copiedFiles.map(({ id }) => id).filter(file => {
+ return !paths.find(path => path.indexOf(file.replace(newCollection.uuid, '')) > -1);
+ });
+ await services.collectionService.deleteFiles(
+ '',
+ filesToDelete
+ );
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({
message: 'New collection created.',
import { getDataExplorerColumnFilters } from '~/store/data-explorer/data-explorer-middleware-service';
import { serializeSimpleObjectTypeFilters } from '../resource-type-filters/resource-type-filters';
import { ResourceKind } from "~/models/resource";
+import { LinkClass } from "~/models/link";
export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const responseLinks = await this.services.linkService.list({
filters: new FilterBuilder()
- .addEqual("link_class", 'star')
+ .addEqual("link_class", LinkClass.STAR)
.addEqual('tail_uuid', getUserUuid(api.getState()))
.addEqual('tail_kind', ResourceKind.USER)
.getFilters()
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const uuidPrefix = api.getState().auth.config.uuidPrefix;
- const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
+ const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
const responseLinks = await this.services.linkService.list({
limit: dataExplorer.rowsPerPage,
offset: dataExplorer.page * dataExplorer.rowsPerPage,
filters: new FilterBuilder()
.addEqual('link_class', LinkClass.STAR)
- .addILike("name", dataExplorer.searchValue)
- .addEqual('owner_uuid', uuid)
+ .addEqual('owner_uuid', publicProjectUuid)
.addIsA("head_uuid", typeFilters)
.getFilters()
});
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
dispatch(progressIndicatorActions.START_WORKING("togglePublicFavorite"));
const uuidPrefix = getState().auth.config.uuidPrefix;
- const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
+ const uuid = `${uuidPrefix}-j7d0g-publicfavorites`;
dispatch(publicFavoritesActions.TOGGLE_PUBLIC_FAVORITE({ resourceUuid: resource.uuid }));
const isPublicFavorite = checkPublicFavorite(resource.uuid, getState().publicFavorites);
dispatch(snackbarActions.OPEN_SNACKBAR({
export const updatePublicFavorites = (resourceUuids: string[]) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const uuidPrefix = getState().auth.config.uuidPrefix;
- const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
+ const uuid = `${uuidPrefix}-j7d0g-publicfavorites`;
dispatch(publicFavoritesActions.CHECK_PRESENCE_IN_PUBLIC_FAVORITES(resourceUuids));
services.favoriteService
.checkPresenceInFavorites(uuid, resourceUuids)
import { updateFavorites } from '~/store/favorites/favorites-actions';
import { updateResources } from '~/store/resources/resources-actions';
import { loadMissingProcessesInformation, getFilters } from '~/store/project-panel/project-panel-middleware-service';
-import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { sharedWithMePanelActions } from './shared-with-me-panel-actions';
import { ListResults } from '~/services/common-service/common-service';
import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
import { ProjectPanelColumnNames } from '~/views/project-panel/project-panel';
import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
+import { FilterBuilder } from '~/services/api/filter-builder';
export class SharedWithMeMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
.contents('', {
...getParams(dataExplorer),
excludeHomeProject: true,
+ filters: new FilterBuilder().addDistinct('uuid', `${state.auth.config.uuidPrefix}-j7d0g-publicfavorites`).getFilters()
});
api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
api.dispatch<any>(updatePublicFavorites(response.items.map(item => item.uuid)));
dispatch(resourcesActions.SET_RESOURCES(items));
};
-const loadSharedRoot = async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
+const loadSharedRoot = async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.SHARED_WITH_ME, pickerId: SIDE_PANEL_TREE }));
const params = {
filters: `[${new FilterBuilder()
.addIsA('uuid', ResourceKind.PROJECT)
.addEqual('group_class', GroupClass.PROJECT)
+ .addDistinct('uuid', getState().auth.config.uuidPrefix + '-j7d0g-publicfavorites')
.getFilters()}]`,
order: new OrderBuilder<ProjectResource>()
.addAsc('name', GroupContentsResourcePrefix.PROJECT)
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const { pickerId, includeCollections = false, includeFiles = false } = params;
const uuidPrefix = getState().auth.config.uuidPrefix;
- const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
- if (uuid) {
+ const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
- const filters = pipe(
- (fb: FilterBuilder) => includeCollections
- ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
- : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
- fb => fb
- .addEqual('link_class', LinkClass.STAR)
- .addEqual('owner_uuid', uuid)
- .getFilters(),
- )(new FilterBuilder());
+ const filters = pipe(
+ (fb: FilterBuilder) => includeCollections
+ ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
+ : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
+ fb => fb
+ .addEqual('link_class', LinkClass.STAR)
+ .addEqual('owner_uuid', publicProjectUuid)
+ .getFilters(),
+ )(new FilterBuilder());
- const { items } = await services.linkService.list({ filters });
+ const { items } = await services.linkService.list({ filters });
- dispatch<any>(receiveTreePickerData<LinkResource>({
- id: 'Public Favorites',
- pickerId,
- data: items,
- extractNodeData: item => ({
- id: item.headUuid,
- value: item,
- status: item.headKind === ResourceKind.PROJECT
+ dispatch<any>(receiveTreePickerData<LinkResource>({
+ id: 'Public Favorites',
+ pickerId,
+ data: items,
+ extractNodeData: item => ({
+ id: item.headUuid,
+ value: item,
+ status: item.headKind === ResourceKind.PROJECT
+ ? TreeNodeStatus.INITIAL
+ : includeFiles
? TreeNodeStatus.INITIAL
- : includeFiles
- ? TreeNodeStatus.INITIAL
- : TreeNodeStatus.LOADED
- }),
- }));
- }
+ : TreeNodeStatus.LOADED
+ }),
+ }));
};
export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
};
};
-const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onSearchChange' | 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
onUploadDataClick: () => {
dispatch<any>(openUploadCollectionFilesDialog());
},
}
));
},
+ onSearchChange: (searchValue: string) => {
+ dispatch(collectionPanelFilesAction.ON_SEARCH_CHANGE(searchValue));
+ },
onOptionsMenuOpen: (event, isWritable) => {
dispatch<any>(openCollectionFilesContextMenu(event, isWritable));
},
import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
import { openCollectionPartialCopyDialog, openCollectionPartialCopyToSelectedCollectionDialog } from '~/store/collections/collection-partial-copy-actions';
-import { DownloadCollectionFileAction } from "~/views-components/context-menu/actions/download-collection-file-action";
+// import { DownloadCollectionFileAction } from "~/views-components/context-menu/actions/download-collection-file-action";
export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[
{
dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES());
}
},
- {
- component: DownloadCollectionFileAction,
- execute: () => { return; }
- },
+ // { // Disabled for now as we need to create backend version of this feature which will be less buggy
+ // component: DownloadCollectionFileAction,
+ // execute: () => { return; }
+ // },
{
name: "Create a new collection with selected",
execute: dispatch => {
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { RemoveIcon } from "~/components/icon/icon";
+import { RemoveIcon, RenameIcon } from "~/components/icon/icon";
import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
-import { openFileRemoveDialog } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
+import { openFileRemoveDialog, openRenameFileDialog } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
import { CollectionFileViewerAction } from '~/views-components/context-menu/actions/collection-file-viewer-action';
import { CollectionCopyToClipboardAction } from "../actions/collection-copy-to-clipboard-action";
]];
export const collectionFilesItemActionSet: ContextMenuActionSet = readOnlyCollectionFilesItemActionSet.concat([[
- // FIXME: This isn't working. Maybe something related to WebDAV?
- // {
- // name: "Rename",
- // icon: RenameIcon,
- // execute: (dispatch, resource) => {
- // dispatch<any>(openRenameFileDialog({ name: resource.name, id: resource.uuid }));
- // }
- // },
+ {
+ name: "Rename",
+ icon: RenameIcon,
+ execute: (dispatch, resource) => {
+ dispatch<any>(openRenameFileDialog({ name: resource.name, id: resource.uuid }));
+ }
+ },
{
name: "Remove",
icon: RemoveIcon,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import axios from 'axios';
+import { configure, shallow } from "enzyme";
+import * as Adapter from 'enzyme-adapter-react-16';
+import { ListItem } from '@material-ui/core';
+import * as JSZip from 'jszip';
+import { DownloadAction } from './download-action';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('axios');
+
+jest.mock('file-saver', () => ({
+ saveAs: jest.fn(),
+}));
+
+const mock = {
+ file: jest.fn(),
+ generateAsync: jest.fn().mockImplementation(() => Promise.resolve('test')),
+};
+
+jest.mock('jszip', () => jest.fn().mockImplementation(() => mock));
+
+describe('<DownloadAction />', () => {
+ let props;
+ let zip;
+
+ beforeEach(() => {
+ props = {};
+ zip = new JSZip();
+ (axios as any).get.mockImplementationOnce(() => Promise.resolve({ data: '1234' }));
+ });
+
+ it('should return null if missing href or kind of file in props', () => {
+ // when
+ const wrapper = shallow(<DownloadAction {...props} />);
+
+ // then
+ expect(wrapper.html()).toBeNull();
+ });
+
+ it('should return a element', () => {
+ // setup
+ props.href = '#';
+
+ // when
+ const wrapper = shallow(<DownloadAction {...props} />);
+
+ // then
+ expect(wrapper.html()).not.toBeNull();
+ });
+
+ it('should handle download', () => {
+ // setup
+ props = {
+ href: ['file1'],
+ kind: 'files',
+ download: [],
+ currentCollectionUuid: '123412-123123'
+ };
+ const wrapper = shallow(<DownloadAction {...props} />);
+
+ // when
+ wrapper.find(ListItem).simulate('click');
+
+ // then
+ expect(axios.get).toHaveBeenCalledWith(props.href[0]);
+ });
+});
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import * as React from "react";
-import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
-import { DownloadIcon } from "../../../components/icon/icon";
-import * as JSZip from "jszip";
+import * as React from 'react';
+import { ListItemIcon, ListItemText, ListItem } from '@material-ui/core';
+import { DownloadIcon } from '../../../components/icon/icon';
+import * as JSZip from 'jszip';
import * as FileSaver from 'file-saver';
import axios from 'axios';
const downloadProps = props.download ? { download: props.download } : {};
const createZip = (fileUrls: string[], download: string[]) => {
- const zip = new JSZip();
let id = 1;
- fileUrls.map((href: string) => {
- axios.get(href).then(response => response).then(({ data }: any) => {
- const splittedByDot = href.split('.');
- if (splittedByDot[splittedByDot.length - 1] !== 'json') {
- if (fileUrls.length === id) {
- zip.file(download[id - 1], data);
- zip.generateAsync({ type: 'blob' }).then((content) => {
- FileSaver.saveAs(content, `download-${props.currentCollectionUuid}.zip`);
- });
+ const zip = new JSZip();
+ const filteredFileUrls = fileUrls
+ .filter((href: string) => {
+ const letter = href.split('').pop();
+ return letter !== '/';
+ });
+
+ filteredFileUrls
+ .map((href: string) => {
+ axios.get(href).then(response => response).then(({ data }: any) => {
+ const splittedByDot = href.split('.');
+ if (splittedByDot[splittedByDot.length - 1] !== 'json') {
+ if (filteredFileUrls.length === id) {
+ zip.file(download[id - 1], data);
+ zip.generateAsync({ type: 'blob' }).then((content) => {
+ FileSaver.saveAs(content, `download-${props.currentCollectionUuid}.zip`);
+ });
+ } else {
+ zip.file(download[id - 1], data);
+ zip.generateAsync({ type: 'blob' });
+ }
} else {
- zip.file(download[id - 1], data);
+ zip.file(download[id - 1], JSON.stringify(data));
zip.generateAsync({ type: 'blob' });
}
- } else {
- zip.file(download[id - 1], JSON.stringify(data));
- zip.generateAsync({ type: 'blob' });
- }
- id++;
+ id++;
+ });
});
- });
};
return props.href || props.kind === 'files'
connect((state: RootState) => ({
buttonVisible: isButtonVisible(state)
}), {
- onDetailsPanelToggle: toggleDetailsPanel,
- })(
- withStyles(styles)(
- (props: MainContentBarProps & WithStyles<CssRules> & any) =>
- <Toolbar>
- <Grid container alignItems="center">
- <Grid item xs>
- <Breadcrumbs />
+ onDetailsPanelToggle: toggleDetailsPanel,
+ })(
+ withStyles(styles)(
+ (props: MainContentBarProps & WithStyles<CssRules> & any) =>
+ <Toolbar>
+ <Grid container>
+ <Grid container item xs alignItems="center">
+ <Breadcrumbs />
+ </Grid>
+ <Grid item>
+ <RefreshButton />
+ </Grid>
+ <Grid item>
+ {props.buttonVisible && <Tooltip title="Additional Info">
+ <IconButton color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
+ <DetailsIcon />
+ </IconButton>
+ </Tooltip>}
+ </Grid>
</Grid>
- <Grid item>
- <RefreshButton />
- </Grid>
- <Grid item>
- {props.buttonVisible && <Tooltip title="Additional Info">
- <IconButton color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
- <DetailsIcon />
- </IconButton>
- </Tooltip>}
- </Grid>
- </Grid>
- </Toolbar>
- )
- );
+ </Toolbar>
+ )
+ );
export const SearchResultsPanelView = withStyles(styles, { withTheme: true })(
(props: SearchResultsPanelProps & WithStyles<CssRules, true>) => {
const homeCluster = props.user.uuid.substr(0, 5);
- const loggedIn = props.sessions.filter((ss) => ss.loggedIn);
+ const loggedIn = props.sessions.filter((ss) => ss.loggedIn && ss.userIsActive);
return <DataExplorer
id={SEARCH_RESULTS_PANEL_ID}
onRowClick={props.onItemClick}
disabled={validating || session.status === SessionStatus.INVALIDATED || session.active}
className={session.loggedIn ? classes.buttonLoggedIn : classes.buttonLoggedOut}
onClick={() => toggleSession(session)}>
- {validating ? "Validating" : (session.loggedIn ? "Logged in" : "Logged out")}
+ {validating ? "Validating"
+ : (session.loggedIn ?
+ (session.userIsActive ? "Logged in" : "Inactive")
+ : "Logged out")}
</Button>
</TableCell>
<TableCell>
cleanup() {
set -x
+ set +e +o pipefail
kill ${arvboot_PID} ${consume_stdout_PID} ${wb2_PID} ${consume_wb2_stdout_PID}
wait ${arvboot_PID} ${consume_stdout_PID} ${wb2_PID} ${consume_wb2_stdout_PID} || true
if [ "${CLEANUP_ARVADOS_DIR}" -eq "1" ]; then