From 6245c45e8475639e6e41988c0565f94f3a4d4657 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 6 Jul 2022 10:04:03 +0100 Subject: [PATCH 1/6] Auto detect fragment in URL and jump to element --- src/helpPage/helpPage.component.tsx | 9 +++-- src/hooks/useAnchor.ts | 51 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useAnchor.ts diff --git a/src/helpPage/helpPage.component.tsx b/src/helpPage/helpPage.component.tsx index 3a87d7d2..86b1f798 100644 --- a/src/helpPage/helpPage.component.tsx +++ b/src/helpPage/helpPage.component.tsx @@ -1,9 +1,9 @@ import React from 'react'; import Typography from '@material-ui/core/Typography'; import { - Theme, - StyleRules, createStyles, + StyleRules, + Theme, WithStyles, withStyles, } from '@material-ui/core'; @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { AppStrings } from '../state/scigateway.types'; import { StateType } from '../state/state.types'; import { UKRITheme } from '../theming'; +import useAnchor from '../hooks/useAnchor'; const styles = (theme: Theme): StyleRules => createStyles({ @@ -117,6 +118,10 @@ const HelpPage = (props: CombinedHelpPageProps): React.ReactElement => { el.insertAdjacentHTML('afterbegin', topOfPageIcon); }); + console.log('render'); + + useAnchor(); + return (
( + (state) => state.scigateway.siteLoading + ); + + useEffect(() => { + // need to make sure the website is not loading, + // if the website is not done loading and the hook runs prematurely, + // document.getElementById will return null because the page is not rendered fully. + // once the website is done loading, the page should be fully rendered, and the target element should be available. + if (hash && !isSiteLoading) { + const elemId = hash.replace('#', ''); + console.log( + 'document.getElementById(elemId)', + document.getElementById(elemId) + ); + // find the element with the ID specified by the fragment + // scroll to the element if found + const elem = document.getElementById(elemId); + if (elem) { + // note: there is a problem where when this hook is run + // even though the element that should be scrolled to is found + // it is not actually mounted to the actual DOM yet + // (I suspect it is a synchronization issue between the vDOM and the browser DOM, + // where the element is in the vDOM but not in the browser DOM) + // setTimeout is a hacky solution to it. we wait for a tiny amount of time (100ms) + // to wait for the element to be properly mounted, then we call scrollIntoView. + // I tried to find better solutions online, but a lot of people point to setTimeout as the solution. + setTimeout(() => { + console.log('scrolled into view'); + elem.scrollIntoView(true); + }, 100); + } + } + }, [hash, isSiteLoading]); +} + +export default useAnchor; From 88997e2e63f305871dafbcace99baff0bcc51609 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 6 Jul 2022 12:23:49 +0100 Subject: [PATCH 2/6] Add explanation for usage of setTimeout --- src/hooks/useAnchor.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/hooks/useAnchor.ts b/src/hooks/useAnchor.ts index 4c84a8e7..e83a2ce2 100644 --- a/src/hooks/useAnchor.ts +++ b/src/hooks/useAnchor.ts @@ -23,24 +23,24 @@ function useAnchor(): void { // once the website is done loading, the page should be fully rendered, and the target element should be available. if (hash && !isSiteLoading) { const elemId = hash.replace('#', ''); - console.log( - 'document.getElementById(elemId)', - document.getElementById(elemId) - ); // find the element with the ID specified by the fragment // scroll to the element if found const elem = document.getElementById(elemId); if (elem) { - // note: there is a problem where when this hook is run - // even though the element that should be scrolled to is found - // it is not actually mounted to the actual DOM yet - // (I suspect it is a synchronization issue between the vDOM and the browser DOM, - // where the element is in the vDOM but not in the browser DOM) - // setTimeout is a hacky solution to it. we wait for a tiny amount of time (100ms) - // to wait for the element to be properly mounted, then we call scrollIntoView. - // I tried to find better solutions online, but a lot of people point to setTimeout as the solution. + // TODO: Remove usage of setTimeout after upgrade to React 18. + // + // useEffect does not work well with Suspense in React <18. + // When any child of Suspense are suspended, React will add 'display: none' to hide all the children of Suspense + // and show the fallback component. This means unsuspended children are still mounted despite being hidden + // which also means useEffect is still called on them. at that point, the tree/DOM is "suspended" and unstable + // so accessing the DOM will result in unexpected results. + // + // unfortunately, there is no good way to tell when the tree is no longer suspended and when the DOM is stable, + // so the only hacky way is to set a short delay before accessing the DOM, + // hoping that the tree will finish suspending and the DOM will be stable after the delay. + // + // related issue: https://github.com/facebook/react/issues/14536 setTimeout(() => { - console.log('scrolled into view'); elem.scrollIntoView(true); }, 100); } From cc6443b2493c38020fe6a2e011e30b587bcef3c0 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 6 Jul 2022 15:33:24 +0100 Subject: [PATCH 3/6] Add unit tests --- src/helpPage/helpPage.component.test.tsx | 9 +- src/helpPage/helpPage.component.tsx | 2 - src/hooks/useAnchor.test.tsx | 127 +++++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/hooks/useAnchor.test.tsx diff --git a/src/helpPage/helpPage.component.test.tsx b/src/helpPage/helpPage.component.test.tsx index 13a809db..27aca17a 100644 --- a/src/helpPage/helpPage.component.test.tsx +++ b/src/helpPage/helpPage.component.test.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { createMount } from '@material-ui/core/test-utils'; import { shallow } from 'enzyme'; import { - HelpPageWithStyles, CombinedHelpPageProps, - TableOfContents, HelpPageWithoutStyles, + HelpPageWithStyles, + TableOfContents, } from './helpPage.component'; import { MuiThemeProvider } from '@material-ui/core'; import { buildTheme } from '../theming'; @@ -18,6 +18,11 @@ const dummyClasses = { toc: 'toc-class', }; +jest.mock('../hooks/useAnchor', () => ({ + __esModule: true, + default: jest.fn(), +})); + describe('Help page component', () => { let mount; let props: CombinedHelpPageProps; diff --git a/src/helpPage/helpPage.component.tsx b/src/helpPage/helpPage.component.tsx index 86b1f798..ecd93667 100644 --- a/src/helpPage/helpPage.component.tsx +++ b/src/helpPage/helpPage.component.tsx @@ -118,8 +118,6 @@ const HelpPage = (props: CombinedHelpPageProps): React.ReactElement => { el.insertAdjacentHTML('afterbegin', topOfPageIcon); }); - console.log('render'); - useAnchor(); return ( diff --git a/src/hooks/useAnchor.test.tsx b/src/hooks/useAnchor.test.tsx new file mode 100644 index 00000000..4178b88b --- /dev/null +++ b/src/hooks/useAnchor.test.tsx @@ -0,0 +1,127 @@ +/** + * A mock location that useLocation will return + */ +import type { MockStoreCreator } from 'redux-mock-store'; +import configureStore from 'redux-mock-store'; +import type { DeepPartial } from 'redux'; +import React from 'react'; +import { createMount } from '@material-ui/core/test-utils'; +import { StateType } from '../state/state.types'; +import useAnchor from './useAnchor'; +import { Provider } from 'react-redux'; +import { useLocation } from 'react-router'; +import { createLocation } from 'history'; + +function TestComponent(): JSX.Element { + useAnchor(); + return <>; +} + +const MOCK_REACT_ROUTER_LOCATION: Partial = { + hash: '#fragment', +}; + +// mock implementation of useLocation to return the mock URL +jest.mock('react-router', () => ({ + __esModule: true, + ...jest.requireActual('react-router'), + useLocation: jest.fn(), +})); + +describe('useAnchor', () => { + let mount: ReturnType; + let createMockStore: MockStoreCreator>; + + beforeEach(() => { + // use fake timers bc useAnchor uses setTimeout under the hood + jest.useFakeTimers(); + // for some reason scrollIntoView is undefined in JSDOM + // we need to create a stub for it + Element.prototype.scrollIntoView = jest.fn(); + (useLocation as jest.Mock).mockReturnValue(MOCK_REACT_ROUTER_LOCATION); + mount = createMount(); + createMockStore = configureStore(); + }); + + afterEach(() => { + mount.cleanUp(); + jest.clearAllMocks(); + jest.useRealTimers(); + }); + + it('should scroll the element into view if the fragment in URL matches an element', () => { + const mockStore = createMockStore({ + scigateway: { + siteLoading: false, + }, + router: { location: createLocation('/') }, + }); + mount( + + +
+ + ); + const element = document.getElementById('fragment'); + if (!element) { + throw new Error('Unexpected condition occurred.'); + } + + jest.runAllTimers(); + + const spy = jest.spyOn(element, 'scrollIntoView'); + // fragment matches an element, should be scrolled into view + expect(spy).toBeCalledTimes(1); + }); + + it('should do nothing if the fragment in URL does not match any element', () => { + const mockStore = createMockStore({ + scigateway: { + siteLoading: false, + }, + router: { location: createLocation('/') }, + }); + mount( + + +
+ + ); + const element = document.getElementById('abc'); + if (!element) { + throw new Error('Unexpected condition occurred.'); + } + + jest.runAllTimers(); + + const spy = jest.spyOn(element, 'scrollIntoView'); + // fragment is #fragment but div id is abc + // should NOT be scrolled into view + expect(spy).not.toBeCalled(); + }); + + it('should do nothing even when fragment matches an element when website is loading', function () { + const mockStore = createMockStore({ + scigateway: { + siteLoading: true, + }, + router: { location: createLocation('/') }, + }); + mount( + + +
+ + ); + const element = document.getElementById('fragment'); + if (!element) { + throw new Error('Unexpected condition occurred.'); + } + + jest.runAllTimers(); + + const spy = jest.spyOn(element, 'scrollIntoView'); + // fragment matches an element but website still loading + expect(spy).not.toBeCalled(); + }); +}); From 09ddafe4dfe7c787d97c56896829cff7abe66a77 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Fri, 8 Jul 2022 15:25:36 +0100 Subject: [PATCH 4/6] Use enzyme mount instead of mui createMount --- src/hooks/useAnchor.test.tsx | 85 +++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/src/hooks/useAnchor.test.tsx b/src/hooks/useAnchor.test.tsx index 4178b88b..64303044 100644 --- a/src/hooks/useAnchor.test.tsx +++ b/src/hooks/useAnchor.test.tsx @@ -1,22 +1,25 @@ -/** - * A mock location that useLocation will return - */ import type { MockStoreCreator } from 'redux-mock-store'; import configureStore from 'redux-mock-store'; import type { DeepPartial } from 'redux'; -import React from 'react'; -import { createMount } from '@material-ui/core/test-utils'; -import { StateType } from '../state/state.types'; -import useAnchor from './useAnchor'; +import * as React from 'react'; +import { mount } from 'enzyme'; import { Provider } from 'react-redux'; import { useLocation } from 'react-router'; import { createLocation } from 'history'; +import useAnchor from './useAnchor'; +import { StateType } from '../state/state.types'; +/** + * A simple React component that uses useAnchor for testing purposes. + */ function TestComponent(): JSX.Element { useAnchor(); return <>; } +/** + * A mock value for what useLocation from react-router would return + */ const MOCK_REACT_ROUTER_LOCATION: Partial = { hash: '#fragment', }; @@ -29,22 +32,16 @@ jest.mock('react-router', () => ({ })); describe('useAnchor', () => { - let mount: ReturnType; let createMockStore: MockStoreCreator>; beforeEach(() => { // use fake timers bc useAnchor uses setTimeout under the hood jest.useFakeTimers(); - // for some reason scrollIntoView is undefined in JSDOM - // we need to create a stub for it - Element.prototype.scrollIntoView = jest.fn(); (useLocation as jest.Mock).mockReturnValue(MOCK_REACT_ROUTER_LOCATION); - mount = createMount(); createMockStore = configureStore(); }); afterEach(() => { - mount.cleanUp(); jest.clearAllMocks(); jest.useRealTimers(); }); @@ -56,22 +53,26 @@ describe('useAnchor', () => { }, router: { location: createLocation('/') }, }); + + const mockScrollIntoView = jest.fn(); + // pretend an element is found that matches the fragment + // the weird type cast is to get around TypeScript error saying + // the object is missing a bunch of other properties + // we obviously don't care about them so there's no point in stubbing them. + jest.spyOn(document, 'getElementById').mockReturnValueOnce({ + scrollIntoView: mockScrollIntoView, + } as unknown as HTMLDivElement); + mount( -
); - const element = document.getElementById('fragment'); - if (!element) { - throw new Error('Unexpected condition occurred.'); - } jest.runAllTimers(); - const spy = jest.spyOn(element, 'scrollIntoView'); // fragment matches an element, should be scrolled into view - expect(spy).toBeCalledTimes(1); + expect(mockScrollIntoView).toBeCalledTimes(1); }); it('should do nothing if the fragment in URL does not match any element', () => { @@ -81,23 +82,31 @@ describe('useAnchor', () => { }, router: { location: createLocation('/') }, }); + + const mockScrollIntoView = jest.fn(); + // pretend no element with #fragment is found + // and pretend there is other elements with IDs != fragment + jest.spyOn(document, 'getElementById').mockImplementation((id) => + id === 'fragment' + ? null + : ({ + scrollIntoView: mockScrollIntoView, + } as unknown as HTMLDivElement) + ); + // another element with ID "other", which is obv != fragment + const otherElem = document.getElementById('other'); + mount( -
); - const element = document.getElementById('abc'); - if (!element) { - throw new Error('Unexpected condition occurred.'); - } jest.runAllTimers(); - const spy = jest.spyOn(element, 'scrollIntoView'); - // fragment is #fragment but div id is abc - // should NOT be scrolled into view - expect(spy).not.toBeCalled(); + // fragment doesn't match any element, useAnchor should not randomly + // jump to other elements + expect(otherElem.scrollIntoView).not.toBeCalled(); }); it('should do nothing even when fragment matches an element when website is loading', function () { @@ -107,21 +116,25 @@ describe('useAnchor', () => { }, router: { location: createLocation('/') }, }); + + const mockScrollIntoView = jest.fn(); + // pretend an element is found that matches the fragment + // the weird type cast is to get around TypeScript error saying + // the object is missing a bunch of other properties + // we obviously don't care about them so there's no point in stubbing them. + jest.spyOn(document, 'getElementById').mockReturnValueOnce({ + scrollIntoView: mockScrollIntoView, + } as unknown as HTMLDivElement); + mount( -
); - const element = document.getElementById('fragment'); - if (!element) { - throw new Error('Unexpected condition occurred.'); - } jest.runAllTimers(); - const spy = jest.spyOn(element, 'scrollIntoView'); // fragment matches an element but website still loading - expect(spy).not.toBeCalled(); + expect(mockScrollIntoView).not.toBeCalled(); }); }); From fbd0e6c56292383b80b8b279ff97a1f5ec8218fe Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 19 Jul 2022 11:53:58 +0100 Subject: [PATCH 5/6] Remove usage of Suspense --- src/App.test.tsx | 17 +++++++++++++---- src/App.tsx | 24 +++++++++++++++--------- src/hooks/useAnchor.ts | 22 +++------------------- src/index.tsx | 5 ++++- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index ff23aeaa..8fb3e0ee 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,13 +1,14 @@ -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import axios from 'axios'; import React from 'react'; import ReactDOM from 'react-dom'; import { act } from 'react-dom/test-utils'; import { Provider } from 'react-redux'; import * as singleSpa from 'single-spa'; -import App from './App'; +import App, { AppSansHoc } from './App'; import { flushPromises } from './setupTests'; import { loadAuthProvider } from './state/actions/scigateway.actions'; +import { Preloader } from './preloader/preloader.component'; describe('App', () => { beforeEach(() => { @@ -17,12 +18,20 @@ describe('App', () => { afterEach(() => { jest.useRealTimers(); }); + it('renders without crashing', () => { const div = document.createElement('div'); - ReactDOM.render(, div); + ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); + it('should show preloader when react-i18next is not ready', () => { + const wrapper = shallow( + + ); + expect(wrapper.find(Preloader).exists()).toBe(true); + }); + it('loadMaintenanceState dispatched when maintenance changes', async () => { // this test only works with old jest fake timers // when they remove legacy timers refactor this test to use real timers @@ -36,7 +45,7 @@ describe('App', () => { }) ); - const wrapper = mount(); + const wrapper = mount(); const realStore = wrapper.find(Provider).prop('store'); // Set provider to icat as that supports maintenance states realStore.dispatch( diff --git a/src/App.tsx b/src/App.tsx index 62e0d1d0..2c459478 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,8 +11,8 @@ import { loadMaintenanceState, } from './state/actions/scigateway.actions'; import ScigatewayMiddleware, { - listenToPlugins, autoLoginMiddleware, + listenToPlugins, } from './state/middleware/scigateway.middleware'; import AppReducer from './state/reducers/App.reducer'; import { StateType } from './state/state.types'; @@ -21,6 +21,7 @@ import { ConnectedThemeProvider } from './theming'; import ReduxToastr from 'react-redux-toastr'; import PageContainer from './pageContainer.component'; import { Preloader } from './preloader/preloader.component'; +import { WithTranslation, withTranslation } from 'react-i18next'; const history = createBrowserHistory(); @@ -68,7 +69,7 @@ const toastrConfig = (): React.ReactElement => ( /> ); -class App extends React.Component { +class App extends React.Component { public componentDidMount(): void { // Check for changes in maintenance state. Ensures that state changes are // loaded when a user does not reload the site for longer than an hour. @@ -94,12 +95,14 @@ class App extends React.Component { - } - > - {toastrConfig()} - - + {this.props.tReady ? ( + <> + {toastrConfig()} + + + ) : ( + + )} @@ -108,4 +111,7 @@ class App extends React.Component { } } -export default App; +// export app with no hoc for testing +export { App as AppSansHoc }; + +export default withTranslation()(App); diff --git a/src/hooks/useAnchor.ts b/src/hooks/useAnchor.ts index e83a2ce2..2f99fa05 100644 --- a/src/hooks/useAnchor.ts +++ b/src/hooks/useAnchor.ts @@ -25,25 +25,9 @@ function useAnchor(): void { const elemId = hash.replace('#', ''); // find the element with the ID specified by the fragment // scroll to the element if found - const elem = document.getElementById(elemId); - if (elem) { - // TODO: Remove usage of setTimeout after upgrade to React 18. - // - // useEffect does not work well with Suspense in React <18. - // When any child of Suspense are suspended, React will add 'display: none' to hide all the children of Suspense - // and show the fallback component. This means unsuspended children are still mounted despite being hidden - // which also means useEffect is still called on them. at that point, the tree/DOM is "suspended" and unstable - // so accessing the DOM will result in unexpected results. - // - // unfortunately, there is no good way to tell when the tree is no longer suspended and when the DOM is stable, - // so the only hacky way is to set a short delay before accessing the DOM, - // hoping that the tree will finish suspending and the DOM will be stable after the delay. - // - // related issue: https://github.com/facebook/react/issues/14536 - setTimeout(() => { - elem.scrollIntoView(true); - }, 100); - } + setTimeout(() => { + document.getElementById(elemId)?.scrollIntoView(true); + }, 0); } }, [hash, isSiteLoading]); } diff --git a/src/index.tsx b/src/index.tsx index b0fa5cad..a165bb45 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,4 +7,7 @@ import ReactDOM from 'react-dom'; import App from './App'; import 'typeface-roboto'; -ReactDOM.render(, document.getElementById('scigateway')); +ReactDOM.render( + , + document.getElementById('scigateway') +); From 1e3261b1b794aa99bdc42e85977108cc6c94043f Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 19 Jul 2022 12:03:00 +0100 Subject: [PATCH 6/6] Specify img dimension in help page HTML --- public/res/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/res/default.json b/public/res/default.json index 6645df69..af3738f7 100644 --- a/public/res/default.json +++ b/public/res/default.json @@ -75,7 +75,7 @@ "title": "Help", "table-of-contents": "Table of contents", "top-of-page-icon": "", - "contents": "

Logging in

When you first access DataGateway, you will be presented with a login page.

You can login by entering your username and password provided by the Diamond User Office. Alternatively, to view only public data, select Anonymous from the Authentication Type drop down menu and click login.

Once you've successfully logged in, you will see a page with three tabs.

My Data

The My Data tab gives you quick access to all your visits and is presented in a grid.

Meta Data
If you click on a row (make sure it is not a hyperlink), tabs will appear on the bottom half of the page. Each tab gives you more information about the visit.

Browse

The Browse tab allows you to browse your data and any public data.

The grid works the same as in the My Data tab. You can sort and filter as well as drill drown the data hierarchy using the hyperlinks on each row. You can use the breadcrumb to go up the hierarchy. The breadcrumb is a useful reference to tell you where you are in the data hierarchy.

If the first column of the grid is a tick box, you can click the box to add the item to your cart. Please note if you select a dataset, all datafiles belonging to that dataset will be selected. It is not possible for example to unselect one datafile if the parent visit is already selected. Similar to My Data, clicking on a row (not hyperlink) will display its meta data information in the panel at the bottom of the page.The search tabs contains an interface the allows you search across all the visits, datasets and datafiles.

The Cart

On the very first column of each row on a grid (at the dataset or datafile level), you will see tick boxes.

Clicking this box will add or remove the selected visit to your cart. Multiple items can be added using the 'Shift' key.

You can use the Cart items counter on the top right corner to check if an item was added or removed.

Clicking on the above will open up the cart window:

Here you can remove already added items or request download of the items in the cart by clicking the Download Cart button. When the button is clicked, you will be presented with a window with several options.

The Download Name is a name that identifies your particular download. It is used as the filename when your download becomes ready.

The Access Method selection option determines whether you would like to download your files or whether you want them restored to another location.

You can optionally enter your email address. When your download is available, an email will be sent notifying you that your download is available.
Clicking the OK button on the window will submit your download request to be processed. You should see a Cart successfully submitted notification popup on the right corner. The request will be added inside the downloads window.

Download

Clicking on the above will open up the downloads window:

The Downloads window is a grid that list all your download requests.

The Status column gives you the current status of your download. Possible values are Restoring from Tape and Available. The Download button is disabled when the status is restoring. Once available, you can click the download button to download the file via the browser if Https transport type was used for that request.

Please note that download pausing and resuming is not supported. Multi-part download managers are also not supported.

For downloads using Globus as the transport type, when the status is complete, you can login to Globus Online and initiate file transfer to your endpoint." + "contents": "

Logging in

When you first access DataGateway, you will be presented with a login page.

You can login by entering your username and password provided by the Diamond User Office. Alternatively, to view only public data, select Anonymous from the Authentication Type drop down menu and click login.

Once you've successfully logged in, you will see a page with three tabs.

My Data

The My Data tab gives you quick access to all your visits and is presented in a grid.

Meta Data
If you click on a row (make sure it is not a hyperlink), tabs will appear on the bottom half of the page. Each tab gives you more information about the visit.

Browse

The Browse tab allows you to browse your data and any public data.

The grid works the same as in the My Data tab. You can sort and filter as well as drill drown the data hierarchy using the hyperlinks on each row. You can use the breadcrumb to go up the hierarchy. The breadcrumb is a useful reference to tell you where you are in the data hierarchy.

If the first column of the grid is a tick box, you can click the box to add the item to your cart. Please note if you select a dataset, all datafiles belonging to that dataset will be selected. It is not possible for example to unselect one datafile if the parent visit is already selected. Similar to My Data, clicking on a row (not hyperlink) will display its meta data information in the panel at the bottom of the page.The search tabs contains an interface the allows you search across all the visits, datasets and datafiles.

The Cart

On the very first column of each row on a grid (at the dataset or datafile level), you will see tick boxes.

Clicking this box will add or remove the selected visit to your cart. Multiple items can be added using the 'Shift' key.

You can use the Cart items counter on the top right corner to check if an item was added or removed.

Clicking on the above will open up the cart window:

Here you can remove already added items or request download of the items in the cart by clicking the Download Cart button. When the button is clicked, you will be presented with a window with several options.

The Download Name is a name that identifies your particular download. It is used as the filename when your download becomes ready.

The Access Method selection option determines whether you would like to download your files or whether you want them restored to another location.

You can optionally enter your email address. When your download is available, an email will be sent notifying you that your download is available.
Clicking the OK button on the window will submit your download request to be processed. You should see a Cart successfully submitted notification popup on the right corner. The request will be added inside the downloads window.

Download

Clicking on the above will open up the downloads window:

The Downloads window is a grid that list all your download requests.

The Status column gives you the current status of your download. Possible values are Restoring from Tape and Available. The Download button is disabled when the status is restoring. Once available, you can click the download button to download the file via the browser if Https transport type was used for that request.

Please note that download pausing and resuming is not supported. Multi-part download managers are also not supported.

For downloads using Globus as the transport type, when the status is complete, you can login to Globus Online and initiate file transfer to your endpoint." }, "admin": { "title": "Admin",