.npmrc
src/lib/cwl-svg/*
tools/arvados_config.yml
+cypress/fixtures/files/5mb.bin
})
});
+ it('moves a collection to a different project', function () {
+ const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
+ const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
+ const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
+
+ cy.createCollection(adminUser.token, {
+ name: collName,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
+ }).as('testCollection');
+ cy.createGroup(adminUser.token, {
+ name: projName,
+ group_class: 'project',
+ owner_uuid: activeUser.user.uuid,
+ }).as('testProject');
+
+ cy.getAll('@testCollection', '@testProject')
+ .then(function ([testCollection, testProject]) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/collections/${testCollection.uuid}`);
+ cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
+ cy.get('[data-cy=collection-info-panel]')
+ .should('not.contain', projName)
+ .and('not.contain', testProject.uuid);
+ cy.get('[data-cy=collection-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Move to').click();
+ cy.get('[data-cy=form-dialog]')
+ .should('contain', 'Move to')
+ .within(() => {
+ cy.get('[data-cy=projects-tree-home-tree-picker]')
+ .find('i')
+ .click();
+ cy.get('[data-cy=projects-tree-home-tree-picker]')
+ .contains(projName)
+ .click();
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ cy.get('[data-cy=snackbar]')
+ .contains('Collection has been moved')
+ cy.get('[data-cy=collection-info-panel]')
+ .contains(projName).and('contain', testProject.uuid);
+ // Double check that the collection is in the project
+ cy.goToPath(`/projects/${testProject.uuid}`);
+ cy.get('[data-cy=project-panel]').should('contain', collName);
+ });
+ });
+
+ it('makes a copy of an existing collection', function() {
+ const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
+ const copyName = `Copy of: ${collName}`;
+
+ cy.createCollection(adminUser.token, {
+ name: collName,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
+ }).as('collection').then(function () {
+ cy.loginAs(activeUser)
+ cy.goToPath(`/collections/${this.collection.uuid}`);
+ cy.get('[data-cy=collection-files-panel]')
+ .should('contain', 'some-file');
+ cy.get('[data-cy=collection-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Make a copy').click();
+ cy.get('[data-cy=form-dialog]')
+ .should('contain', 'Make a copy')
+ .within(() => {
+ cy.get('[data-cy=projects-tree-home-tree-picker]')
+ .contains('Projects')
+ .click();
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+ cy.get('[data-cy=snackbar]')
+ .contains('Collection has been copied.')
+ cy.get('[data-cy=snackbar-goto-action]').click();
+ cy.get('[data-cy=project-panel]')
+ .contains(copyName).click();
+ cy.get('[data-cy=collection-files-panel]')
+ .should('contain', 'some-file');
+ });
+ });
+
it('uses the collection version browser to view a previous version', function () {
const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
.contains(adminUser.user.uuid);
});
});
+
+ describe('file upload', () => {
+ beforeEach(() => {
+ cy.createCollection(adminUser.token, {
+ name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ })
+ .as('testCollection1');
+ });
+
+ it('allows to cancel running upload', () => {
+ cy.getAll('@testCollection1')
+ .then(function([testCollection1]) {
+ cy.loginAs(activeUser);
+
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+ cy.get('[data-cy=upload-button]').click();
+
+ cy.fixture('files/5mb.bin', 'base64').then(content => {
+ cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+ cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+ cy.get('[data-cy=form-submit-btn]').click();
+
+ cy.get('button').contains('Cancel').click();
+
+ cy.get('[data-cy=form-submit-btn]').should('not.exist');
+ });
+ });
+ });
+
+ it('allows to cancel single file from the running upload', () => {
+ cy.getAll('@testCollection1')
+ .then(function([testCollection1]) {
+ cy.loginAs(activeUser);
+
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+ cy.get('[data-cy=upload-button]').click();
+
+ cy.fixture('files/5mb.bin', 'base64').then(content => {
+ cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+ cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+ cy.get('[data-cy=form-submit-btn]').click();
+
+ cy.get('button[aria-label=Remove]').eq(1).click();
+
+ cy.get('[data-cy=form-submit-btn]').should('not.exist');
+
+ cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
+ });
+ });
+ });
+
+ it('allows to cancel all files from the running upload', () => {
+ cy.getAll('@testCollection1')
+ .then(function([testCollection1]) {
+ cy.loginAs(activeUser);
+
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+ cy.get('[data-cy=upload-button]').click();
+
+ cy.fixture('files/5mb.bin', 'base64').then(content => {
+ cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+ cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+ cy.get('[data-cy=form-submit-btn]').click();
+
+ cy.get('button[aria-label=Remove]').should('exist');
+ cy.get('button[aria-label=Remove]').click({ multiple: true, force: true });
+
+ cy.get('[data-cy=form-submit-btn]').should('not.exist');
+ });
+ });
+ });
+ });
})
});
});
- it('can copy selected into the collection', () => {
+ // Disabled while addressing #18587
+ it.skip('can copy selected into the collection', () => {
cy.createCollection(adminUser.token, {
name: `Test source collection ${Math.floor(Math.random() * 999999)}`,
manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
cy.get('[data-cy=not-found-page]').should('not.exist');
});
});
+
+ it('shows details panel when clicking on the info icon', () => {
+ cy.createGroup(activeUser.token, {
+ name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('testRootProject').then(function(testRootProject) {
+ cy.loginAs(activeUser);
+
+ cy.get('[data-cy=side-panel-tree]').contains(testRootProject.name).click();
+
+ cy.get('[data-cy=additional-info-icon]').click();
+
+ cy.contains(testRootProject.uuid).should('exist');
+ });
+ });
+
+ it('clears search input when changing project', () => {
+ cy.createGroup(activeUser.token, {
+ name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('testProject1');
+
+ cy.getAll('@testProject1').then(function([testProject1]) {
+ cy.loginAs(activeUser);
+
+ cy.get('[data-cy=side-panel-tree]').contains(testProject1.name).click();
+
+ cy.get('[data-cy=search-input] input').type('test123');
+
+ cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+ cy.get('[data-cy=search-input] input').should('not.have.value', 'test123');
+ });
+ });
});
\ No newline at end of file
cy.addToFavorites(user.token, user.user.uuid, project.uuid);
}
});
-});
\ No newline at end of file
+});
+
+Cypress.Commands.add(
+ 'upload',
+ {
+ prevSubject: 'element',
+ },
+ (subject, file, fileName) => {
+ cy.window().then(window => {
+ const blob = b64toBlob(file, '', 512);
+ const testFile = new window.File([blob], fileName);
+
+ cy.wrap(subject).trigger('drop', {
+ dataTransfer: { files: [testFile] },
+ });
+ })
+ }
+)
+
+function b64toBlob(b64Data, contentType = '', sliceSize = 512) {
+ const byteCharacters = atob(b64Data)
+ const byteArrays = []
+
+ for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+ const slice = byteCharacters.slice(offset, offset + sliceSize);
+
+ const byteNumbers = new Array(slice.length);
+ for (let i = 0; i < slice.length; i++) {
+ byteNumbers[i] = slice.charCodeAt(i);
+ }
+
+ const byteArray = new Uint8Array(byteNumbers);
+
+ byteArrays.push(byteArray);
+ }
+
+ const blob = new Blob(byteArrays, { type: contentType });
+ return blob
+}
\ No newline at end of file
.keys(headers)
.forEach(key => r.setRequestHeader(key, headers[key]));
+ if (!(window as any).cancelTokens) {
+ Object.assign(window, { cancelTokens: {} });
+ }
+
+ (window as any).cancelTokens[config.url] = () => {
+ resolve(r);
+ r.abort();
+ }
+
if (config.onUploadProgress) {
r.upload.addEventListener('progress', config.onUploadProgress);
}
render() {
const { values, filler } = this.props;
return <Grid container spacing={8} className={this.props.classes.root}>
- {values.map(this.renderChip)}
+ {values && values.map(this.renderChip)}
{filler && <Grid item xs>{filler}</Grid>}
</Grid>;
}
wrapper: {
display: 'flex',
minHeight: '600px',
- marginBottom: '1rem',
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '0.875rem',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
</IconButton>
</Tooltip>
<div className={path.length > 1 ? classes.searchWrapper : classes.searchWrapperHidden}>
- <SearchInput label="Search" value={leftSearch} onSearch={setLeftSearch} />
+ <SearchInput selfClearProp={leftKey} label="Search" value={leftSearch} onSearch={setLeftSearch} />
</div>
<div className={classes.dataWrapper}>
{
data-parent-path={name}
className={classNames(classes.row, getActiveClass(name))}
key={id}>
- {getItemIcon(type, getActiveClass(name))}
+ {getItemIcon(type, getActiveClass(name))}
<div className={classes.rowName}>
{name}
</div>
</div>
<div className={classes.rightPanel}>
<div className={classes.searchWrapper}>
- <SearchInput label="Search" value={rightSearch} onSearch={setRightSearch} />
+ <SearchInput selfClearProp={rightKey} label="Search" value={rightSearch} onSearch={setRightSearch} />
</div>
{
isWritable &&
<Button
className={classes.uploadButton}
data-cy='upload-button'
- onClick={onUploadDataClick}
+ onClick={() => {
+ if (!collectionAutofetchEnabled) {
+ setCollectionAutofetchEnabled(true);
+ }
+ onUploadDataClick();
+ }}
variant='contained'
color='primary'
size='small'>
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { configure, shallow, mount } from "enzyme";
-import { WithStyles } from "@material-ui/core";
-import 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-files2';
-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
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { TreeItem, TreeItemStatus } from 'components/tree/tree';
-import { FileTreeData } from 'components/file-tree/file-tree-data';
-import { FileTree } from 'components/file-tree/file-tree';
-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>>;
- isWritable: boolean;
- 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;
- onCollapseToggle: (id: string, status: TreeItemStatus) => void;
- onFileClick: (id: string) => void;
- loadFilesFunc: () => void;
- currentItemUuid?: string;
-}
-
-export type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button' | 'centeredLabel' | 'cardHeaderContent' | 'cardHeaderContentTitle';
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
- root: {
- paddingBottom: theme.spacing.unit,
- height: '100%'
- },
- cardSubheader: {
- paddingTop: 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'
- },
- fileSizeHeader: {
- marginRight: '65px'
- },
- uploadIcon: {
- transform: 'rotate(180deg)'
- },
- button: {
- marginRight: -theme.spacing.unit,
- marginTop: '8px'
- },
- centeredLabel: {
- fontSize: '0.875rem',
- textAlign: 'center'
- },
-});
-
-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);
- }, [onSearchChange, 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}
- label='Search files'
- onSearch={setSearchValue} />
- </div>
- }
- 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>);
-};
-
-export const CollectionPanelFiles = withStyles(styles)(CollectionPanelFilesComponent);
import { ArvadosTheme } from "common/custom-theme";
import { createTree } from 'models/tree';
import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
-import { MoreOptionsIcon } from 'components/icon/icon';
+import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
import { PaperProps } from '@material-ui/core/Paper';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
-type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
+type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
paddingBottom: theme.spacing.unit * 2
},
toolbar: {
- paddingTop: theme.spacing.unit * 2
+ paddingTop: theme.spacing.unit,
+ paddingRight: theme.spacing.unit * 2,
},
toolbarUnderTitle: {
paddingTop: 0
overflow: 'auto'
},
root: {
- height: '100%'
+ height: '100%',
},
moreOptionsButton: {
padding: 0
paddingLeft: theme.spacing.unit * 3,
paddingTop: theme.spacing.unit * 3,
fontSize: '18px'
- }
+ },
+ dataTable: {
+ height: '100%',
+ overflow: 'auto',
+ },
+ container: {
+ height: '100%',
+ },
});
interface DataExplorerDataProps<T> {
extractKey?: (item: T) => React.Key;
}
-type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules>;
+type DataExplorerProps<T> = DataExplorerDataProps<T> &
+ DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
export const DataExplorer = withStyles(styles)(
class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+
componentDidMount() {
if (this.props.onSetColumns) {
this.props.onSetColumns(this.props.columns);
}
}
+
render() {
const {
columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
- paperKey, fetchMode, currentItemUuid, title
+ paperKey, fetchMode, currentItemUuid, title,
+ doHidePanel, doMaximizePanel, panelName, panelMaximized
} = this.props;
+
const dataCy = this.props["data-cy"];
return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={dataCy}>
- {title && <div className={classes.title}>{title}</div>}
- {(!hideColumnSelector || !hideSearchInput || !!actions) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
+ <Grid container direction="column" wrap="nowrap" className={classes.container}>
+ {title && <Grid item xs className={classes.title}>{title}</Grid>}
+ {(!hideColumnSelector || !hideSearchInput || !!actions) && <Grid item xs><Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
<Grid container justify="space-between" wrap="nowrap" alignItems="center">
{!hideSearchInput && <div className={classes.searchBox}>
{!hideSearchInput && <SearchInput
label={searchLabel}
value={searchValue}
+ selfClearProp={currentItemUuid}
onSearch={onSearch} />}
</div>}
{actions}
columns={columns}
onColumnToggle={onColumnToggle} />}
</Grid>
- </Toolbar>}
- <DataTable
+ { doMaximizePanel && !panelMaximized &&
+ <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
+ </Tooltip> }
+ { doHidePanel &&
+ <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+ </Tooltip> }
+ </Toolbar></Grid>}
+ <Grid item xs="auto" className={classes.dataTable}><DataTable
columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
items={items}
onRowClick={(_, item: T) => onRowClick(item)}
working={working}
defaultView={dataTableDefaultView}
currentItemUuid={currentItemUuid}
- currentRoute={paperKey} />
- <Toolbar className={classes.footer}>
+ currentRoute={paperKey} /></Grid>
+ <Grid item xs><Toolbar className={classes.footer}>
<Grid container justify="flex-end">
{fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
count={itemsAvailable}
onClick={this.loadMore}
>Load more</Button>}
</Grid>
- </Toolbar>
+ </Toolbar></Grid>
+ </Grid>
</Paper>;
}
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
root: {
- overflowX: 'auto',
- overflowY: 'auto',
- height: 'calc(100vh - 280px)',
+ width: '100%',
},
content: {
display: 'inline-block',
- width: '100%'
+ width: '100%',
},
tableBody: {
background: theme.palette.background.paper
if (!disabled) {
onDelete(file);
}
+
+ let interval = setInterval(() => {
+ const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
+
+ if (key) {
+ clearInterval(interval);
+ (window as any).cancelTokens[key]();
+ delete (window as any).cancelTokens[key];
+ }
+ }, 100);
+
}
render() {
const { classes, onDrop, disabled, files } = this.props;
inputs[0].focus();
}
}}
+ data-cy="drag-and-drop"
disabled={disabled}
inputProps={{
onFocus: () => {
dialogTitle: string;
formFields: React.ComponentType<InjectedFormProps<any> & WithDialogProps<any>>;
submitLabel?: string;
+ cancelCallback?: Function;
enableWhenPristine?: boolean;
+ doNotDisableCancel?: boolean;
}
type DialogProjectProps = DialogProjectDataProps & WithDialogProps<{}> & InjectedFormProps<any> & WithStyles<CssRules>;
<DialogActions className={props.classes.dialogActions}>
<Button
data-cy='form-cancel-btn'
- onClick={props.closeDialog}
+ onClick={() => {
+ props.closeDialog();
+
+ if (props.cancelCallback) {
+ props.cancelCallback();
+ props.reset();
+ props.initialize({});
+ }
+ }}
className={props.classes.button}
color="primary"
- disabled={props.submitting}>
+ disabled={props.doNotDisableCancel ? false : props.submitting}>
{props.cancelLabel || 'Cancel'}
</Button>
<Button
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import Warning from '@material-ui/icons/Warning';
+import Visibility from '@material-ui/icons/Visibility';
+import VisibilityOff from '@material-ui/icons/VisibilityOff';
import VpnKey from '@material-ui/icons/VpnKey';
import LinkOutlined from '@material-ui/icons/LinkOutlined';
import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
// Import FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPencilAlt, faSlash, faUsers } from '@fortawesome/free-solid-svg-icons';
+import { CropFreeSharp } from '@material-ui/icons';
library.add(
faPencilAlt,
faSlash,
export const HelpIcon: IconType = (props) => <Help {...props} />;
export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
+export const InfoIcon: IconType = (props) => <Info {...props} />;
export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
export const MailIcon: IconType = (props) => <Mail {...props} />;
+export const MaximizeIcon: IconType = (props) => <CropFreeSharp {...props} />;
export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
export const MoveToIcon: IconType = (props) => <Input {...props} />;
export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
export const TrashIcon: IconType = (props) => <Delete {...props} />;
export const UserPanelIcon: IconType = (props) => <Person {...props} />;
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
+export const VisibleIcon: IconType = (props) => <Visibility {...props} />;
+export const InvisibleIcon: IconType = (props) => <VisibilityOff {...props} />;
export const WorkflowIcon: IconType = (props) => <Code {...props} />;
export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
export const Link: IconType = (props) => <LinkOutlined {...props} />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { configure, mount } from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+import { MPVContainer } from './multi-panel-view';
+import { Button } from "@material-ui/core";
+
+configure({ adapter: new Adapter() });
+
+const PanelMock = ({panelName, panelMaximized, doHidePanel, doMaximizePanel, children, ...rest}) =>
+ <div {...rest}>{children}</div>;
+
+describe('<MPVContainer />', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ classes: {},
+ };
+ });
+
+ it('should show default panel buttons for every child', () => {
+ const childs = [
+ <PanelMock key={1}>This is one panel</PanelMock>,
+ <PanelMock key={2}>This is another panel</PanelMock>,
+ ];
+ const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+ expect(wrapper.find(Button).first().html()).toContain('Panel 1');
+ expect(wrapper.html()).toContain('This is one panel');
+ expect(wrapper.find(Button).last().html()).toContain('Panel 2');
+ expect(wrapper.html()).toContain('This is another panel');
+ });
+
+ it('should show panel when clicking on its button', () => {
+ const childs = [
+ <PanelMock key={1}>This is one panel</PanelMock>,
+ ];
+ props.panelStates = [
+ {name: 'Initially invisible Panel', visible: false},
+ ]
+
+ const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+
+ // Initial state: panel not visible
+ expect(wrapper.html()).not.toContain('This is one panel');
+ expect(wrapper.html()).toContain('All panels are hidden');
+
+ // Panel visible when clicking on its button
+ wrapper.find(Button).simulate('click');
+ expect(wrapper.html()).toContain('This is one panel');
+ expect(wrapper.html()).not.toContain('All panels are hidden');
+ });
+
+ it('should show custom panel buttons when config provided', () => {
+ const childs = [
+ <PanelMock key={1}>This is one panel</PanelMock>,
+ <PanelMock key={2}>This is another panel</PanelMock>,
+ ];
+ props.panelStates = [
+ {name: 'First Panel'},
+ ]
+ const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+ expect(wrapper.find(Button).first().html()).toContain('First Panel');
+ expect(wrapper.html()).toContain('This is one panel');
+ // Second panel received the default button naming and hidden status by default
+ expect(wrapper.find(Button).last().html()).toContain('Panel 2');
+ expect(wrapper.html()).not.toContain('This is another panel');
+ wrapper.find(Button).last().simulate('click');
+ expect(wrapper.html()).toContain('This is another panel');
+ });
+
+ it('should set panel hidden when requested', () => {
+ const childs = [
+ <PanelMock key={1}>This is one panel</PanelMock>,
+ ];
+ props.panelStates = [
+ {name: 'First Panel', visible: false},
+ ]
+ const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+ expect(wrapper.find(Button).html()).toContain('First Panel');
+ expect(wrapper.html()).not.toContain('This is one panel');
+ expect(wrapper.html()).toContain('All panels are hidden');
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { MutableRefObject, ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
+import {
+ Button,
+ Grid,
+ Paper,
+ StyleRulesCallback,
+ Tooltip,
+ withStyles,
+ WithStyles
+} from "@material-ui/core";
+import { GridProps } from '@material-ui/core/Grid';
+import { isArray } from 'lodash';
+import { DefaultView } from 'components/default-view/default-view';
+import { InfoIcon, InvisibleIcon, VisibleIcon } from 'components/icon/icon';
+import { ReactNodeArray } from 'prop-types';
+import classNames from 'classnames';
+
+type CssRules = 'button' | 'buttonIcon' | 'content';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ button: {
+ padding: '2px 5px',
+ marginRight: '5px',
+ },
+ buttonIcon: {
+ boxShadow: 'none',
+ padding: '2px 0px 2px 5px',
+ fontSize: '1rem'
+ },
+ content: {
+ overflow: 'auto',
+ },
+});
+
+interface MPVHideablePanelDataProps {
+ name: string;
+ visible: boolean;
+ maximized: boolean;
+ illuminated: boolean;
+ children: ReactNode;
+ panelRef?: MutableRefObject<any>;
+}
+
+interface MPVHideablePanelActionProps {
+ doHidePanel: () => void;
+ doMaximizePanel: () => void;
+}
+
+type MPVHideablePanelProps = MPVHideablePanelDataProps & MPVHideablePanelActionProps;
+
+const MPVHideablePanel = ({doHidePanel, doMaximizePanel, name, visible, maximized, illuminated, ...props}: MPVHideablePanelProps) =>
+ visible
+ ? <>
+ {React.cloneElement((props.children as ReactElement), { doHidePanel, doMaximizePanel, panelName: name, panelMaximized: maximized, panelIlluminated: illuminated, panelRef: props.panelRef })}
+ </>
+ : null;
+
+interface MPVPanelDataProps {
+ panelName?: string;
+ panelMaximized?: boolean;
+ panelIlluminated?: boolean;
+ panelRef?: MutableRefObject<any>;
+}
+
+interface MPVPanelActionProps {
+ doHidePanel?: () => void;
+ doMaximizePanel?: () => void;
+}
+
+// Props received by panel implementors
+export type MPVPanelProps = MPVPanelDataProps & MPVPanelActionProps;
+
+type MPVPanelContentProps = {children: ReactElement} & MPVPanelProps & GridProps;
+
+// Grid item compatible component for layout and MPV props passing
+export const MPVPanelContent = ({doHidePanel, doMaximizePanel, panelName, panelMaximized, panelIlluminated, panelRef, ...props}: MPVPanelContentProps) => {
+ useEffect(() => {
+ if (panelRef && panelRef.current) {
+ panelRef.current.scrollIntoView({behavior: 'smooth'});
+ }
+ }, [panelRef]);
+
+ return <Grid item {...props}>
+ <span ref={panelRef} /> {/* Element to scroll to when the panel is selected */}
+ <Paper style={{height: '100%'}} elevation={panelIlluminated ? 8 : 0}>
+ {React.cloneElement(props.children, { doHidePanel, doMaximizePanel, panelName, panelMaximized })}
+ </Paper>
+ </Grid>;
+}
+
+export interface MPVPanelState {
+ name: string;
+ visible?: boolean;
+}
+interface MPVContainerDataProps {
+ panelStates?: MPVPanelState[];
+}
+type MPVContainerProps = MPVContainerDataProps & GridProps;
+
+// Grid container compatible component that also handles panel toggling.
+const MPVContainerComponent = ({children, panelStates, classes, ...props}: MPVContainerProps & WithStyles<CssRules>) => {
+ if (children === undefined || children === null || children === {}) {
+ children = [];
+ } else if (!isArray(children)) {
+ children = [children];
+ }
+ const visibility = (children as ReactNodeArray).map((_, idx) =>
+ !!!panelStates || // if panelStates wasn't passed, default to all visible panels
+ (panelStates[idx] &&
+ (panelStates[idx].visible || panelStates[idx].visible === undefined)));
+ const [panelVisibility, setPanelVisibility] = useState<boolean[]>(visibility);
+ const [brightenedPanel, setBrightenedPanel] = useState<number>(-1);
+ const panelRef = useRef<any>(null);
+
+ let panels: JSX.Element[] = [];
+ let toggles: JSX.Element[] = [];
+
+ if (isArray(children)) {
+ for (let idx = 0; idx < children.length; idx++) {
+ const showFn = (idx: number) => () => {
+ setPanelVisibility([
+ ...panelVisibility.slice(0, idx),
+ true,
+ ...panelVisibility.slice(idx+1)
+ ]);
+ };
+ const hideFn = (idx: number) => () => {
+ setPanelVisibility([
+ ...panelVisibility.slice(0, idx),
+ false,
+ ...panelVisibility.slice(idx+1)
+ ])
+ };
+ const maximizeFn = (idx: number) => () => {
+ // Maximize X == hide all but X
+ setPanelVisibility([
+ ...panelVisibility.slice(0, idx).map(() => false),
+ true,
+ ...panelVisibility.slice(idx+1).map(() => false),
+ ])
+ };
+ const toggleIcon = panelVisibility[idx]
+ ? <VisibleIcon className={classNames(classes.buttonIcon)} />
+ : <InvisibleIcon className={classNames(classes.buttonIcon)}/>
+ const panelName = panelStates === undefined
+ ? `Panel ${idx+1}`
+ : (panelStates[idx] && panelStates[idx].name) || `Panel ${idx+1}`;
+ const toggleVariant = "outlined";
+ const toggleTooltip = panelVisibility[idx]
+ ? ''
+ :`Show ${panelName} panel`;
+ const panelIsMaximized = panelVisibility[idx] &&
+ panelVisibility.filter(e => e).length === 1;
+
+ toggles = [
+ ...toggles,
+ <Tooltip title={toggleTooltip} disableFocusListener>
+ <Button variant={toggleVariant} size="small" color="primary"
+ className={classNames(classes.button)}
+ onMouseEnter={() => setBrightenedPanel(idx)}
+ onMouseLeave={() => setBrightenedPanel(-1)}
+ onClick={showFn(idx)}>
+ {panelName}
+ {toggleIcon}
+ </Button>
+ </Tooltip>
+ ];
+
+ const aPanel =
+ <MPVHideablePanel key={idx} visible={panelVisibility[idx]} name={panelName}
+ panelRef={(idx === brightenedPanel) ? panelRef : undefined}
+ maximized={panelIsMaximized} illuminated={idx === brightenedPanel}
+ doHidePanel={hideFn(idx)} doMaximizePanel={maximizeFn(idx)}>
+ {children[idx]}
+ </MPVHideablePanel>;
+ panels = [...panels, aPanel];
+ };
+ };
+
+ return <Grid container {...props}>
+ <Grid container item direction="row">
+ { toggles.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>) }
+ </Grid>
+ <Grid container item {...props} xs className={classes.content}>
+ { panelVisibility.includes(true)
+ ? panels
+ : <Grid container item alignItems='center' justify='center'>
+ <DefaultView messages={["All panels are hidden.", "Click on the buttons above to show them."]} icon={InfoIcon} />
+ </Grid> }
+ </Grid>
+ </Grid>;
+};
+
+export const MPVContainer = withStyles(styles)(MPVContainerComponent);
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { DefaultViewDataProps, DefaultView } from 'components/default-view/default-view';
-
-type CssRules = 'classRoot' | 'classIcon' | 'classMessage';
-
-const styles: StyleRulesCallback<CssRules> = () => ({
- classRoot: {
- position: 'absolute',
- width: '80%',
- left: '50%',
- top: '50%',
- transform: 'translate(-50%, -50%)'
- },
- classMessage: {
- fontSize: '1.75rem',
- },
- classIcon: {
- fontSize: '6rem'
- }
-});
-
-type PanelDefaultViewProps = Pick<DefaultViewDataProps, 'icon' | 'messages'> & WithStyles<CssRules>;
-
-export const PanelDefaultView = withStyles(styles)(
- ({ classes, ...props }: PanelDefaultViewProps) =>
- <DefaultView {...classes} {...props} />);
describe("on submit", () => {
it("calls onSearch with initial value passed via props", () => {
- const searchInput = mount(<SearchInput value="initial value" onSearch={onSearch} />);
+ const searchInput = mount(<SearchInput selfClearProp="" value="initial value" onSearch={onSearch} />);
searchInput.find("form").simulate("submit");
expect(onSearch).toBeCalledWith("initial value");
});
it("calls onSearch with current value", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} />);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
searchInput.find("form").simulate("submit");
expect(onSearch).toBeCalledWith("current value");
});
it("calls onSearch with new value passed via props", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} />);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
searchInput.setProps({value: "new value"});
searchInput.find("form").simulate("submit");
});
it("cancels timeout set on input value change", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000} />);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={1000} />);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
searchInput.find("form").simulate("submit");
jest.runTimersToTime(1000);
describe("on input value change", () => {
it("calls onSearch after default timeout", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} />);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
expect(onSearch).not.toBeCalled();
jest.runTimersToTime(DEFAULT_SEARCH_DEBOUNCE);
});
it("calls onSearch after the time specified in props has passed", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={2000}/>);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={2000}/>);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
jest.runTimersToTime(1000);
expect(onSearch).not.toBeCalled();
});
it("calls onSearch only once after no change happened during the specified time", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={1000}/>);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
jest.runTimersToTime(500);
searchInput.find("input").simulate("change", { target: { value: "changed value" } });
});
it("calls onSearch again after the specified time has passed since previous call", () => {
- const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
+ const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={1000}/>);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
jest.runTimersToTime(500);
searchInput.find("input").simulate("change", { target: { value: "intermediate value" } });
});
+ describe("on input target change", () => {
+ it("clears the input value on selfClearProp change", () => {
+ const searchInput = mount(<SearchInput selfClearProp="abc" value="123" onSearch={onSearch} debounce={1000}/>);
+ searchInput.setProps({ selfClearProp: 'aaa' });
+ jest.runTimersToTime(1000);
+ expect(onSearch).toBeCalledWith("");
+ expect(onSearch).toHaveBeenCalledTimes(1);
+ });
+ });
+
});
interface SearchInputDataProps {
value: string;
label?: string;
+ selfClearProp: string;
}
interface SearchInputActionProps {
interface SearchInputState {
value: string;
label: string;
+ selfClearProp: string;
}
export const DEFAULT_SEARCH_DEBOUNCE = 1000;
class extends React.Component<SearchInputProps> {
state: SearchInputState = {
value: "",
- label: ""
+ label: "",
+ selfClearProp: ""
};
timeout: number;
<InputLabel>{this.state.label}</InputLabel>
<Input
type="text"
+ data-cy="search-input"
value={this.state.value}
onChange={this.handleChange}
endAdornment={
if (nextProps.value !== this.props.value) {
this.setState({ value: nextProps.value });
}
+ if (this.state.value !== '' && nextProps.selfClearProp && nextProps.selfClearProp !== this.state.selfClearProp) {
+ this.props.onSearch('');
+ this.setState({ selfClearProp: nextProps.selfClearProp });
+ }
}
componentWillUnmount() {
collectionService.update = jest.fn();
});
+ describe('get', () => {
+ it('should make a list request with uuid filtering', async () => {
+ serverApi.get = jest.fn(() => Promise.resolve(
+ { data: { items: [{}] } }
+ ));
+ const uuid = 'zzzzz-4zz18-0123456789abcde'
+ await collectionService.get(uuid);
+ expect(serverApi.get).toHaveBeenCalledWith(
+ '/collections', {
+ params: {
+ filters: `[["uuid","=","zzzzz-4zz18-0123456789abcde"]]`,
+ include_old_versions: true,
+ },
+ }
+ );
+ });
+
+ it('should be able to request specific fields', async () => {
+ serverApi.get = jest.fn(() => Promise.resolve(
+ { data: { items: [{}] } }
+ ));
+ const uuid = 'zzzzz-4zz18-0123456789abcde'
+ await collectionService.get(uuid, undefined, ['manifestText']);
+ expect(serverApi.get).toHaveBeenCalledWith(
+ '/collections', {
+ params: {
+ filters: `[["uuid","=","zzzzz-4zz18-0123456789abcde"]]`,
+ include_old_versions: true,
+ select: `["manifest_text"]`
+ },
+ }
+ );
+ });
+ });
+
describe('update', () => {
it('should call put selecting updated fields + others', async () => {
serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
import { TrashableResourceService } from "services/common-service/trashable-resource-service";
import { ApiActions } from "services/api/api-actions";
import { customEncodeURI } from "common/url";
+import { FilterBuilder } from "services/api/filter-builder";
+import { ListArguments } from "services/common-service/common-service";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
]);
}
+ async get(uuid: string, showErrors?: boolean, select?: string[]) {
+ super.validateUuid(uuid);
+ // We use a filtered list request to avoid getting the manifest text
+ const filters = new FilterBuilder().addEqual('uuid', uuid).getFilters();
+ const listArgs: ListArguments = {filters, includeOldVersions: true};
+ if (select) {
+ listArgs.select = select;
+ }
+ const lst = await super.list(listArgs, showErrors);
+ return lst.items[0];
+ }
+
create(data?: Partial<CollectionResource>) {
return super.create({ ...data, preserveVersion: true });
}
},
onUploadProgress: (e: ProgressEvent) => {
onProgress(fileId, e.loaded, e.total, Date.now());
- }
+ },
};
return this.webdavClient.upload(fileURL, [file], requestConfig);
}
}
}
- private validateUuid(uuid: string) {
+ protected validateUuid(uuid: string) {
if (uuid === "") {
throw new Error('UUID cannot be empty string');
}
);
}
- list(args: ListArguments = {}): Promise<ListResults<T>> {
- const { filters, order, ...other } = args;
+ list(args: ListArguments = {}, showErrors?: boolean): Promise<ListResults<T>> {
+ const { filters, select, ...other } = args;
const params = {
...CommonService.mapKeys(snakeCase)(other),
filters: filters ? `[${filters}]` : undefined,
- order: order ? order : undefined
+ select: select
+ ? `[${select.map(snakeCase).map(s => `"${s}"`).join(', ')}]`
+ : undefined
};
if (QueryString.stringify(params).length <= 1500) {
return CommonService.defaultResponse(
this.serverApi.get(`/${this.resourceType}`, { params }),
- this.actions
+ this.actions,
+ showErrors
);
} else {
// Using the POST special case to avoid URI length 414 errors.
_method: 'GET'
}
}),
- this.actions
+ this.actions,
+ showErrors
);
}
}
const collectionApiResponse = (apiResponse: CollectionResource) => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, portableDataHash, replicationDesired,
- replicationConfirmedAt, replicationConfirmed, manifestText, deleteAt, trashAt, isTrashed, storageClassesDesired,
+ replicationConfirmedAt, replicationConfirmed, deleteAt, trashAt, isTrashed, storageClassesDesired,
storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion, fileCount, fileSizeTotal } = apiResponse;
const response = `
"uuid": "${uuid}",
"replication_desired": ${stringify(replicationDesired)},
"replication_confirmed_at": ${stringify(replicationConfirmedAt)},
"replication_confirmed": ${stringify(replicationConfirmed)},
-"manifest_text": ${stringify(manifestText)},
"name": ${stringify(name)},
"description": ${stringify(description)},
"properties": ${stringifyObject(properties)},
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { getResource } from "store/resources/resources";
+import { CollectionResource } from "models/collection";
export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
export const copyCollection = (resource: CopyFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
+ let collection = getResource<CollectionResource>(resource.uuid)(getState().resources);
try {
- const collection = await services.collectionService.get(resource.uuid);
- const newCollection = await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
+ if (!collection) {
+ collection = await services.collectionService.get(resource.uuid);
+ }
+ const collManifestText = await services.collectionService.get(resource.uuid, undefined, ['manifestText']);
+ collection.manifestText = collManifestText.manifestText;
+ const {href, ...collectionRecord} = collection;
+ const newCollection = await services.collectionService.create({ ...collectionRecord, ownerUuid: resource.ownerUuid, name: resource.name });
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
return newCollection;
} catch (e) {
import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { getResource } from "store/resources/resources";
+import { CollectionResource } from "models/collection";
export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
export const moveCollection = (resource: MoveToFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
+ let cachedCollection = getResource<CollectionResource>(resource.uuid)(getState().resources);
try {
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_MOVE_FORM_NAME));
+ if (!cachedCollection) {
+ cachedCollection = await services.collectionService.get(resource.uuid);
+ }
const collection = await services.collectionService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
- return collection;
+ return {...cachedCollection, ...collection};
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
if (currentCollection) {
try {
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
- const collection = await services.collectionService.get(currentCollection.uuid);
+ const collectionManifestText = await services.collectionService.get(currentCollection.uuid, undefined, ['manifestText']);
const collectionCopy = {
name,
description,
ownerUuid: projectUuid,
uuid: undefined,
- manifestText: collection.manifestText,
+ manifestText: collectionManifestText.manifestText,
};
const newCollection = await services.collectionService.create(collectionCopy);
const copiedFiles = await services.collectionService.files(newCollection.uuid);
return !paths.find(path => path.indexOf(file.replace(newCollection.uuid, '')) > -1);
});
await services.collectionService.deleteFiles(
- '',
+ newCollection.uuid,
filesToDelete
);
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
const currentCollection = state.collectionPanel.item;
if (currentCollection && !currentCollection.manifestText) {
- const fetchedCurrentCollection = await services.collectionService.get(currentCollection.uuid);
+ const fetchedCurrentCollection = await services.collectionService.get(currentCollection.uuid, undefined, ['manifestText']);
currentCollection.manifestText = fetchedCurrentCollection.manifestText;
currentCollection.unsignedManifestText = fetchedCurrentCollection.unsignedManifestText;
}
});
const diffPathToRemove = difference(paths, pathsToRemove);
await services.collectionService.deleteFiles(selectedCollection.uuid, pathsToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
- const collectionWithDeletedFiles = await services.collectionService.get(collectionUuid);
+ const collectionWithDeletedFiles = await services.collectionService.get(collectionUuid, undefined, ['uuid', 'manifestText']);
await services.collectionService.update(collectionUuid, { manifestText: `${collectionWithDeletedFiles.manifestText}${(currentCollection.manifestText ? currentCollection.manifestText : currentCollection.unsignedManifestText) || ''}` });
await services.collectionService.deleteFiles(collectionWithDeletedFiles.uuid, diffPathToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
import { collectionPanelFilesAction } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
import { createTree } from 'models/tree';
import { loadCollectionPanel } from '../collection-panel/collection-panel-action';
+import * as WorkbenchActions from 'store/workbench/workbench-actions';
export const uploadCollectionFiles = (collectionUuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch(fileUploaderActions.START_UPLOAD());
const files = getState().fileUploader.map(file => file.file);
await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch));
+ dispatch(WorkbenchActions.loadCollection(collectionUuid));
dispatch(fileUploaderActions.CLEAR_UPLOAD());
};
import { resourcesActions } from "../resources/resources-actions";
import { navigateTo } from "../navigation/navigation-action";
import { dialogActions } from "../dialog/dialog-actions";
+import { getResource } from "store/resources/resources";
+import { CollectionResource } from "models/collection";
export const COLLECTION_RESTORE_VERSION_DIALOG = 'collectionRestoreVersionDialog';
export const restoreVersion = (resourceUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
try {
- // Request que entire record because stored old versions usually
- // don't include the manifest_text field.
- const oldVersion = await services.collectionService.get(resourceUuid);
+ // Request the manifest text because stored old versions usually
+ // don't include them.
+ let oldVersion = getResource<CollectionResource>(resourceUuid)(getState().resources);
+ if (!oldVersion) {
+ oldVersion = await services.collectionService.get(resourceUuid);
+ }
+ const oldVersionManifest = await services.collectionService.get(resourceUuid, undefined, ['manifestText']);
+ oldVersion.manifestText = oldVersionManifest.manifestText;
+
const { uuid, version, ...rest} = oldVersion;
const headVersion = await services.collectionService.update(
oldVersion.currentVersionUuid,
SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),
START_UPLOAD: ofType(),
DELETE_UPLOAD_FILE: ofType<UploadFile>(),
+ CANCEL_FILES_UPLOAD: ofType(),
});
export type FileUploaderAction = UnionOf<typeof fileUploaderActions>;
return updatedState;
},
+ CANCEL_FILES_UPLOAD: () => {
+ state.forEach((file) => {
+ let interval = setInterval(() => {
+ const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
+
+ if (key) {
+ clearInterval(interval);
+ (window as any).cancelTokens[key]();
+ delete (window as any).cancelTokens[key];
+ }
+ }, 100);
+ });
+
+ return [];
+ },
START_UPLOAD: () => {
const startTime = Date.now();
return state.map(f => ({ ...f, startTime, prevTime: startTime }));
-import { RootState } from '../store';
-import { matchProcessLogRoute } from 'routes/routes';
// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: AGPL-3.0
+import { RootState } from '../store';
+import { matchProcessLogRoute, matchProcessRoute } from 'routes/routes';
+
export interface ProcessLogsPanel {
filters: string[];
selectedFilter: string;
export const getProcessLogsPanelCurrentUuid = ({ router }: RootState) => {
const pathname = router.location ? router.location.pathname : '';
- const match = matchProcessLogRoute(pathname);
+ const match = matchProcessLogRoute(pathname) || matchProcessRoute(pathname);
return match ? match.params.id : undefined;
};
dispatch<any>(openCollectionPartialCopyDialog());
}
},
- {
- name: "Copy selected into the collection",
- execute: dispatch => {
- dispatch<any>(openCollectionPartialCopyToSelectedCollectionDialog());
- }
- }
+ // Disabled while addressing #18587
+ // {
+ // name: "Copy selected into the collection",
+ // execute: dispatch => {
+ // dispatch<any>(openCollectionPartialCopyToSelectedCollectionDialog());
+ // }
+ // }
]];
export const collectionFilesActionSet: ContextMenuActionSet = readOnlyCollectionFilesActionSet.concat([[
import React from 'react';
import { ProcessIcon } from 'components/icon/icon';
import { ProcessResource } from 'models/process';
-import { formatDate } from 'common/formatters';
-import { ResourceKind } from 'models/resource';
-import { resourceLabel } from 'common/labels';
import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "components/details-attribute/details-attribute";
-import { ResourceOwnerWithName } from '../data-explorer/renderers';
+import { ProcessDetailsAttributes } from 'views/process-panel/process-details-attributes';
export class ProcessDetails extends DetailsData<ProcessResource> {
}
getDetails() {
- return <div>
- <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
- <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} value={this.item.ownerUuid}
- uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
-
- <DetailsAttribute label='Status' value={this.item.state} />
- <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-
- <DetailsAttribute label='Started at' value={formatDate(this.item.createdAt)} />
- <DetailsAttribute label='Finished at' value={formatDate(this.item.expiresAt)} />
-
- <DetailsAttribute label='Outputs' value={this.item.outputPath} />
- <DetailsAttribute label='UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
- <DetailsAttribute label='Container UUID' value={this.item.containerUuid} />
-
- <DetailsAttribute label='Priority' value={this.item.priority} />
- <DetailsAttribute label='Runtime Constraints' value={JSON.stringify(this.item.runtimeConstraints)} />
-
- <DetailsAttribute label='Docker Image locator' linkToUuid={this.item.containerImage} value={this.item.containerImage} />
- </div>;
+ return <ProcessDetailsAttributes item={this.item} />;
}
}
import { require } from 'validators/require';
import { FileUploaderField } from 'views-components/file-uploader/file-uploader';
import { WarningCollection } from 'components/warning-collection/warning-collection';
+import { fileUploaderActions } from 'store/file-uploader/file-uploader-actions';
+import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
type DialogCollectionFilesUploadProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
-export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) =>
- <FormDialog
+export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) => {
+
+ return <FormDialog
dialogTitle='Upload data'
formFields={UploadCollectionFilesFields}
submitLabel='Upload data'
+ doNotDisableCancel
+ cancelCallback={() => {
+ const { submitting, dispatch } = (props as any);
+
+ if (submitting) {
+ dispatch(progressIndicatorActions.STOP_WORKING('uploadCollectionFilesDialog'));
+ dispatch(fileUploaderActions.CANCEL_FILES_UPLOAD());
+ dispatch(fileUploaderActions.CLEAR_UPLOAD());
+ }
+ }}
{...props}
/>;
+}
const UploadCollectionFilesFields = () => <>
<Field
onDrop(files);
}
},
- onDelete: file => dispatch(fileUploaderActions.DELETE_UPLOAD_FILE(file)),
+ onDelete: file => dispatch(fileUploaderActions.DELETE_UPLOAD_FILE(file))
});
export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload);
export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) =>
<div>
<Typography variant='caption'>{props.label}</Typography>
- <FileUploader disabled={props.meta.submitting} onDrop={props.input.onChange} />
+ <FileUploader disabled={false} onDrop={props.input.onChange} />
</div>;
buttonVisible: isButtonVisible(state),
projectUuid: state.detailsPanel.resourceUuid,
}), (dispatch) => ({
- onDetailsPanelToggle: toggleDetailsPanel,
+ onDetailsPanelToggle: () => dispatch<any>(toggleDetailsPanel()),
onRefreshButtonClick: (id) => {
dispatch<any>(loadSidePanelTreeProjects(id));
dispatch<any>(reloadProjectMatchingUuid([id]));
</Grid>
<Grid item>
{props.buttonVisible && <Tooltip title="Additional Info">
- <IconButton color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
+ <IconButton data-cy="additional-info-icon" color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
<DetailsIcon />
</IconButton>
</Tooltip>}
disableActivation
};
return <div>
- <HomeTreePicker pickerId={home} {...p} />
- <SharedTreePicker pickerId={shared} {...p} />
- <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
+ <div data-cy="projects-tree-home-tree-picker">
+ <HomeTreePicker pickerId={home} {...p} />
+ </div>
+ <div data-cy="projects-tree-shared-tree-picker">
+ <SharedTreePicker pickerId={shared} {...p} />
+ </div>
+ <div data-cy="projects-tree-public-favourites-tree-picker">
+ <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
+ </div>
<div data-cy="projects-tree-favourites-tree-picker">
- <FavoritesTreePicker pickerId={favorites} {...p} />
+ <FavoritesTreePicker pickerId={favorites} {...p} />
</div>
</div>;
};
onExited={props.onExited}
anchorOrigin={props.anchorOrigin}
autoHideDuration={props.autoHideDuration}>
- <SnackbarContent
+ <div data-cy="snackbar"><SnackbarContent
className={classNames(cssClass)}
aria-describedby="client-snackbar"
message={
</span>
}
action={actions(props)}
- />
+ /></div>
</MaterialSnackbar>
);
}
color="inherit"
className={classes.linkButton}
onClick={() => onClick(link)}>
- Go To
+ <span data-cy='snackbar-goto-action'>Go To</span>
</Button>
);
}
import { getProcess } from 'store/processes/process';
import { ResourcesState } from 'store/resources/resources';
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
button: {
marginLeft: theme.spacing.unit
},
+ root: {
+ width: '100%',
+ }
});
export enum AllProcessesPanelColumnNames {
}
render() {
- return <DataExplorer
+ return <div className={this.props.classes.root}><DataExplorer
id={ALL_PROCESSES_PANEL_ID}
onRowClick={this.handleRowClick}
onRowDoubleClick={this.handleRowDoubleClick}
onContextMenu={this.handleContextMenu}
contextMenuColumn={true}
- dataTableDefaultView={
- <DataTableDefaultView
- icon={ProcessIcon}
- messages={['Processes list empty.']}
- />
- } />;
+ dataTableDefaultView={ <DataTableDefaultView
+ icon={ProcessIcon}
+ messages={['Processes list empty.']}
+ /> } />
+ </div>
}
}
)
import React from 'react';
import {
- StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Tooltip, IconButton
+ StyleRulesCallback, WithStyles, withStyles
} from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
-import { HelpIcon, ShareMeIcon } from 'components/icon/icon';
+import { ShareMeIcon } from 'components/icon/icon';
import { createTree } from 'models/tree';
import { DataColumns } from 'components/data-table/data-table';
import { SortDirection } from 'components/data-table/data-column';
TokenLastUsedAt, TokenLastUsedByIpAddress, TokenScopes, TokenUserId
} from 'views-components/data-explorer/renderers';
-type CssRules = 'card' | 'cardContent' | 'helpIconGrid';
+type CssRules = 'root';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- card: {
+ root: {
width: '100%',
- overflow: 'auto'
- },
- cardContent: {
- padding: 0,
- '&:last-child': {
- paddingBottom: 0
- }
- },
- helpIconGrid: {
- textAlign: 'right'
}
});
onItemClick: (item: string) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
onItemDoubleClick: (item: string) => void;
- openHelpDialog: () => void;
}
export interface ApiClientAuthorizationPanelRootDataProps {
& ApiClientAuthorizationPanelRootDataProps & WithStyles<CssRules>;
export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
- ({ classes, onItemDoubleClick, onItemClick, onContextMenu, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
- <Card className={classes.card}>
- <CardContent className={classes.cardContent}>
- <Grid container direction="row" justify="flex-end">
- <Grid item xs={12} className={classes.helpIconGrid}>
- <Tooltip title="Api token - help">
- <IconButton onClick={openHelpDialog}>
- <HelpIcon />
- </IconButton>
- </Tooltip>
- </Grid>
- <Grid item xs={12}>
- <DataExplorer
- id={API_CLIENT_AUTHORIZATION_PANEL_ID}
- onRowClick={onItemClick}
- onRowDoubleClick={onItemDoubleClick}
- onContextMenu={onContextMenu}
- contextMenuColumn={true}
- hideColumnSelector
- hideSearchInput
- dataTableDefaultView={
- <DataTableDefaultView
- icon={ShareMeIcon}
- messages={[DEFAULT_MESSAGE]} />
- } />
- </Grid>
- </Grid>
- </CardContent>
- </Card>
+ ({ classes, onItemDoubleClick, onItemClick, onContextMenu }: ApiClientAuthorizationPanelRootProps) =>
+ <div className={classes.root}><DataExplorer
+ id={API_CLIENT_AUTHORIZATION_PANEL_ID}
+ onRowClick={onItemClick}
+ onRowDoubleClick={onItemDoubleClick}
+ onContextMenu={onContextMenu}
+ contextMenuColumn={true}
+ hideColumnSelector
+ hideSearchInput
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ShareMeIcon}
+ messages={[DEFAULT_MESSAGE]} />
+ } /></div>
);
\ No newline at end of file
ApiClientAuthorizationPanelRootActionProps
} from 'views/api-client-authorization-panel/api-client-authorization-panel-root';
import { openApiClientAuthorizationContextMenu } from 'store/context-menu/context-menu-actions';
-import { openApiClientAuthorizationsHelpDialog } from 'store/api-client-authorizations/api-client-authorizations-actions';
const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => {
return {
},
onItemClick: (resourceUuid: string) => { return; },
onItemDoubleClick: uuid => { return; },
- openHelpDialog: () => {
- dispatch<any>(openApiClientAuthorizationsHelpDialog());
- }
});
export const ApiClientAuthorizationPanel = connect(mapStateToProps, mapDispatchToProps)(ApiClientAuthorizationPanelRoot);
\ No newline at end of file
StyleRulesCallback,
WithStyles,
withStyles,
- Grid,
Button
} from '@material-ui/core';
import { CollectionIcon } from 'components/icon/icon';
import { RootState } from 'store/store';
import { CollectionResource } from 'models/collection';
-type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+type CssRules = 'backLink' | 'backIcon' | 'root' | 'content';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
backLink: {
backIcon: {
marginRight: theme.spacing.unit
},
- card: {
- width: '100%'
+ root: {
+ width: '100%',
},
- title: {
- color: theme.palette.grey["700"]
+ content: {
+ // reserve space for the content address bar
+ height: `calc(100% - ${theme.spacing.unit * 7}px)`,
},
- iconHeader: {
- fontSize: '1.875rem',
- color: theme.customs.colors.green700
- },
- link: {
- fontSize: '0.875rem',
- color: theme.palette.primary.main,
- textAlign: 'right',
- '&:hover': {
- cursor: 'pointer'
- }
- }
});
enum CollectionContentAddressPanelColumnNames {
connect(mapStateToProps, mapDispatchToProps)(
class extends React.Component<CollectionContentAddressPanelActionProps & CollectionContentAddressPanelDataProps & CollectionContentAddressDataProps & WithStyles<CssRules>> {
render() {
- return <Grid item xs={12}>
+ return <div className={this.props.classes.root}>
<Button
onClick={() => window.history.back()}
className={this.props.classes.backLink}>
<BackIcon className={this.props.classes.backIcon} />
Back
</Button>
- <DataExplorer
+ <div className={this.props.classes.content}><DataExplorer
id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
hideSearchInput
onRowClick={this.props.onItemClick}
<DataTableDefaultView
icon={CollectionIcon}
messages={['Collections with this content address not found.']} />
- } />;
- </Grid >;
+ } /></div>
+ </div>;
}
}
)
import React from 'react';
import {
- StyleRulesCallback, WithStyles, withStyles,
- IconButton, Grid, Tooltip, Typography, ExpansionPanel,
- ExpansionPanelSummary, ExpansionPanelDetails
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ IconButton,
+ Grid,
+ Tooltip,
+ Typography,
+ Card, CardHeader, CardContent,
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { ArvadosTheme } from 'common/custom-theme';
import { RootState } from 'store/store';
-import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon, CollectionOldVersionIcon } from 'components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, CollectionOldVersionIcon } from 'components/icon/icon';
import { DetailsAttribute } from 'components/details-attribute/details-attribute';
import { CollectionResource, getCollectionUrl } from 'models/collection';
import { CollectionPanelFiles } from 'views-components/collection-panel-files/collection-panel-files';
import { Link } from 'react-router-dom';
import { Link as ButtonLink } from '@material-ui/core';
import { ResourceOwnerWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
+import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
type CssRules = 'root'
| 'button'
+ | 'infoCard'
+ | 'propertiesCard'
| 'filesCard'
| 'iconHeader'
| 'tag'
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
- display: 'flex',
- flexFlow: 'column',
- height: 'calc(100vh - 130px)', // (100% viewport height) - (top bar + breadcrumbs)
+ width: '100%',
},
button: {
cursor: 'pointer'
},
+ infoCard: {
+ paddingLeft: theme.spacing.unit * 2,
+ paddingRight: theme.spacing.unit * 2,
+ paddingBottom: theme.spacing.unit * 2,
+ },
+ propertiesCard: {
+ padding: 0,
+ },
filesCard: {
- marginBottom: theme.spacing.unit * 2,
- flex: 1,
+ padding: 0,
},
iconHeader: {
fontSize: '1.875rem',
class extends React.Component<CollectionPanelProps> {
render() {
const { classes, item, dispatch, isWritable, isOldVersion, isLoadingFiles, tooManyFiles } = this.props;
+ const panelsData: MPVPanelState[] = [
+ {name: "Details"},
+ {name: "Properties"},
+ {name: "Files"},
+ ];
return item
- ? <div className={classes.root}>
- <ExpansionPanel data-cy='collection-info-panel' defaultExpanded>
- <ExpansionPanelSummary expandIcon={<ExpandIcon />}>
+ ? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
+ <MPVPanelContent xs="auto" data-cy='collection-info-panel'>
+ <Card className={classes.infoCard}>
<Grid container justify="space-between">
<Grid item xs={11}><span>
<IconButton onClick={this.openCollectionDetails}>
</Tooltip>
</Grid>
</Grid>
- </ExpansionPanelSummary>
- <ExpansionPanelDetails>
<Grid container justify="space-between">
<Grid item xs={12}>
<Typography variant="caption">
}
</Grid>
</Grid>
- </ExpansionPanelDetails>
- </ExpansionPanel>
-
- <ExpansionPanel data-cy='collection-properties-panel' defaultExpanded>
- <ExpansionPanelSummary expandIcon={<ExpandIcon />}>
- {"Properties"}
- </ExpansionPanelSummary>
- <ExpansionPanelDetails>
- <Grid container>
+ </Card>
+ </MPVPanelContent>
+ <MPVPanelContent xs="auto" data-cy='collection-properties-panel'>
+ <Card className={classes.propertiesCard}>
+ <CardHeader title="Properties" />
+ <CardContent><Grid container>
{isWritable && <Grid item xs={12}>
<CollectionTagForm />
</Grid>}
: <div className={classes.centeredLabel}>No properties set on this collection.</div>
}
</Grid>
- </Grid>
- </ExpansionPanelDetails>
- </ExpansionPanel>
- <div className={classes.filesCard}>
- <CollectionPanelFiles
- isWritable={isWritable}
- isLoading={isLoadingFiles}
- tooManyFiles={tooManyFiles}
- loadFilesFunc={() => {
- dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
- dispatch<any>(loadCollectionFiles(this.props.item.uuid));
- }
- } />
- </div>
- </div>
+ </Grid></CardContent>
+ </Card>
+ </MPVPanelContent>
+ <MPVPanelContent xs>
+ <Card className={classes.filesCard}>
+ <CollectionPanelFiles
+ isWritable={isWritable}
+ isLoading={isLoadingFiles}
+ tooManyFiles={tooManyFiles}
+ loadFilesFunc={() => {
+ dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
+ dispatch<any>(loadCollectionFiles(this.props.item.uuid));
+ }
+ } />
+ </Card>
+ </MPVPanelContent>
+ </MPVContainer>
: null;
}
import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
import { CollectionResource } from 'models/collection';
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
button: {
marginLeft: theme.spacing.unit
},
+ root: {
+ width: '100%',
+ },
});
export enum FavoritePanelColumnNames {
}
render() {
- return <DataExplorer
+ return <div className={this.props.classes.root}><DataExplorer
id={FAVORITE_PANEL_ID}
onRowClick={this.handleRowClick}
onRowDoubleClick={this.handleRowDoubleClick}
icon={FavoriteIcon}
messages={['Your favorites list is empty.']}
/>
- } />;
+ } /></div>;
}
}
)
import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog, getCurrentGroupDetailsPanelUuid } from 'store/group-details-panel/group-details-panel-actions';
import { openContextMenu } from 'store/context-menu/context-menu-actions';
import { ResourcesState, getResource } from 'store/resources/resources';
-import { Grid, Button, Tabs, Tab, Paper } from '@material-ui/core';
+import { Grid, Button, Tabs, Tab, Paper, WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core';
import { AddIcon } from 'components/icon/icon';
import { getUserUuid } from 'common/getuser';
import { GroupResource, isBuiltinGroup } from 'models/group';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = "root";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ }
+});
export enum GroupDetailsPanelMembersColumnNames {
FULL_NAME = "Name",
groupCanManage: boolean;
}
-export const GroupDetailsPanel = connect(
+export const GroupDetailsPanel = withStyles(styles)(connect(
mapStateToProps, mapDispatchToProps
)(
- class GroupDetailsPanel extends React.Component<GroupDetailsPanelProps> {
+ class GroupDetailsPanel extends React.Component<GroupDetailsPanelProps & WithStyles<CssRules>> {
state = {
value: 0,
};
render() {
const { value } = this.state;
return (
- <Paper>
+ <Paper className={this.props.classes.root}>
<Tabs value={value} onChange={this.handleChange} variant="fullWidth">
<Tab data-cy="group-details-members-tab" label="MEMBERS" />
<Tab data-cy="group-details-permissions-tab" label="PERMISSIONS" />
handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
this.setState({ value });
}
- });
+ }));
import React from 'react';
import { connect } from 'react-redux';
-import { Grid, Button, Typography } from "@material-ui/core";
+import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
import { DataExplorer } from "views-components/data-explorer/data-explorer";
import { DataColumns } from 'components/data-table/data-table';
import { SortDirection } from 'components/data-table/data-column';
import { openContextMenu } from 'store/context-menu/context-menu-actions';
import { ResourceKind } from 'models/resource';
import { LinkClass, LinkResource } from 'models/link';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = "root";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ }
+});
export enum GroupsPanelColumnNames {
GROUP = "Name",
resources: ResourcesState;
}
-export const GroupsPanel = connect(
+export const GroupsPanel = withStyles(styles)(connect(
mapStateToProps, mapDispatchToProps
)(
- class GroupsPanel extends React.Component<GroupsPanelProps> {
+ class GroupsPanel extends React.Component<GroupsPanelProps & WithStyles<CssRules>> {
render() {
return (
- <DataExplorer
+ <div className={this.props.classes.root}><DataExplorer
id={GROUPS_PANEL_ID}
data-cy="groups-panel-data-explorer"
onRowClick={noop}
<AddIcon /> New group
</Button>
</Grid>
- } />
+ } /></div>
);
}
});
}
}
- });
+ }));
const GroupMembersCount = connect(
import { ResourcesState } from 'store/resources/resources';
import { ShareMeIcon } from 'components/icon/icon';
import { createTree } from 'models/tree';
-import {
- ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail,
- ResourceLinkClass, ResourceLinkName }
+import {
+ ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail,
+ ResourceLinkClass, ResourceLinkName }
from 'views-components/data-explorer/renderers';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = "root";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ }
+});
export enum LinkPanelColumnNames {
NAME = "Name",
onItemDoubleClick: (item: string) => void;
}
-export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps;
+export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps & WithStyles<CssRules>;
-export const LinkPanelRoot = (props: LinkPanelRootProps) => {
- return <DataExplorer
+export const LinkPanelRoot = withStyles(styles)((props: LinkPanelRootProps) => {
+ return <div className={props.classes.root}><DataExplorer
id={LINK_PANEL_ID}
onRowClick={props.onItemClick}
onRowDoubleClick={props.onItemDoubleClick}
onContextMenu={props.onContextMenu}
- contextMenuColumn={true}
+ contextMenuColumn={true}
hideColumnSelector
hideSearchInput
dataTableDefaultView={
<DataTableDefaultView
icon={ShareMeIcon}
messages={['Your link list is empty.']} />
- }/>;
-};
\ No newline at end of file
+ }/></div>;
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { Grid } from "@material-ui/core";
+import { formatDate } from "common/formatters";
+import { resourceLabel } from "common/labels";
+import { DetailsAttribute } from "components/details-attribute/details-attribute";
+import { ProcessResource } from "models/process";
+import { ResourceKind } from "models/resource";
+import { ResourceOwnerWithName } from "views-components/data-explorer/renderers";
+
+type CssRules = 'label' | 'value';
+
+export const ProcessDetailsAttributes = (props: { item: ProcessResource, twoCol?: boolean, classes?: Record<CssRules, string> }) => {
+ const item = props.item;
+ const classes = props.classes || { label: '', value: '', button: '' };
+ const mdSize = props.twoCol ? 6 : 12;
+ return <Grid container>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Owner' linkToUuid={item.ownerUuid}
+ uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+ </Grid>
+ <Grid item xs={12} md={12}>
+ <DetailsAttribute label='Status' value={item.state} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Started at' value={formatDate(item.createdAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Finished at' value={formatDate(item.expiresAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Outputs' value={item.outputPath} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='UUID' linkToUuid={item.uuid} value={item.uuid} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Container UUID' value={item.containerUuid} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Priority' value={item.priority} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Runtime Constraints'
+ value={JSON.stringify(item.runtimeConstraints)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Docker Image locator'
+ linkToUuid={item.containerImage} value={item.containerImage} />
+ </Grid>
+ </Grid>;
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Card,
+ CardHeader,
+ IconButton,
+ CardContent,
+ Tooltip,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon } from 'components/icon/icon';
+import { Process } from 'store/processes/process';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { ProcessDetailsAttributes } from './process-details-attributes';
+
+type CssRules = 'card' | 'content' | 'title';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ height: '100%'
+ },
+ content: {
+ '&:last-child': {
+ paddingBottom: theme.spacing.unit * 2,
+ }
+ },
+ title: {
+ overflow: 'hidden',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+});
+
+export interface ProcessDetailsCardDataProps {
+ process: Process;
+}
+
+type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessDetailsCard = withStyles(styles)(
+ ({ classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
+ return <Card className={classes.card}>
+ <CardHeader
+ classes={{
+ content: classes.title,
+ }}
+ title='Details'
+ action={ doHidePanel &&
+ <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+ </Tooltip> } />
+ <CardContent className={classes.content}>
+ <ProcessDetailsAttributes item={process.containerRequest} twoCol />
+ </CardContent>
+ </Card>;
+ }
+);
+
CardHeader, IconButton, CardContent, Grid, Chip, Typography, Tooltip
} from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
-import { MoreOptionsIcon, ProcessIcon } from 'components/icon/icon';
+import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon';
import { DetailsAttribute } from 'components/details-attribute/details-attribute';
import { Process } from 'store/processes/process';
import { getProcessStatus, getProcessStatusColor } from 'store/processes/process';
import { formatDate } from 'common/formatters';
import classNames from 'classnames';
import { ContainerState } from 'models/container';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'link' | 'content' | 'title' | 'avatar' | 'cancelButton';
cancelProcess: (uuid: string) => void;
}
-type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles<CssRules, true>;
+type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles<CssRules, true> & MPVPanelProps;
export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
- ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess }: ProcessInformationCardProps) => {
+ ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess, doHidePanel, panelName }: ProcessInformationCardProps) => {
const { container } = process;
const startedAt = container ? formatDate(container.startedAt) : 'N/A';
const finishedAt = container ? formatDate(container.finishedAt) : 'N/A';
<MoreOptionsIcon />
</IconButton>
</Tooltip>
+ { doHidePanel &&
+ <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+ </Tooltip> }
</div>
}
title={
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { Grid } from '@material-ui/core';
+import { Grid, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { ProcessInformationCard } from './process-information-card';
import { DefaultView } from 'components/default-view/default-view';
import { ProcessIcon } from 'components/icon/icon';
import { Process } from 'store/processes/process';
import { SubprocessPanel } from 'views/subprocess-panel/subprocess-panel';
import { SubprocessFilterDataProps } from 'components/subprocess-filter/subprocess-filter';
+import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
+import { ArvadosTheme } from 'common/custom-theme';
+import { ProcessDetailsCard } from './process-details-card';
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ },
+});
export interface ProcessPanelRootDataProps {
process?: Process;
cancelProcess: (uuid: string) => void;
}
-export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
+export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
+
+const panelsData: MPVPanelState[] = [
+ {name: "Info"},
+ {name: "Details", visible: false},
+ {name: "Subprocesses"},
+];
-export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) =>
+export const ProcessPanelRoot = withStyles(styles)(({ process, ...props }: ProcessPanelRootProps) =>
process
- ? <Grid container spacing={16} alignItems="stretch">
- <Grid item sm={12} md={12}>
+ ? <MPVContainer className={props.classes.root} spacing={8} panelStates={panelsData} justify-content="flex-start" direction="column" wrap="nowrap">
+ <MPVPanelContent xs="auto">
<ProcessInformationCard
process={process}
onContextMenu={event => props.onContextMenu(event, process)}
openWorkflow={props.navigateToWorkflow}
cancelProcess={props.cancelProcess}
/>
- </Grid>
- <Grid item sm={12} md={12}>
+ </MPVPanelContent>
+ <MPVPanelContent xs="auto">
+ <ProcessDetailsCard process={process} />
+ </MPVPanelContent>
+ <MPVPanelContent xs>
<SubprocessPanel />
- </Grid>
- </Grid>
+ </MPVPanelContent>
+ </MPVContainer>
: <Grid container
alignItems='center'
justify='center'
<DefaultView
icon={ProcessIcon}
messages={['Process not found']} />
- </Grid>;
+ </Grid>);
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
- position: 'relative',
width: '100%',
- height: '100%'
},
button: {
marginLeft: theme.spacing.unit
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
- return <div className={classes.root}>
+ return <div data-cy='project-panel' className={classes.root}>
<DataExplorer
id={PROJECT_PANEL_ID}
onRowClick={this.handleRowClick}
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { CollectionResource } from 'models/collection';
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
button: {
marginLeft: theme.spacing.unit
},
+ root: {
+ width: '100%',
+ },
});
export enum PublicFavoritePanelColumnNames {
connect(mapStateToProps, mapDispatchToProps)(
class extends React.Component<FavoritePanelProps> {
render() {
- return <DataExplorer
+ return <div className={this.props.classes.root}><DataExplorer
id={PUBLIC_FAVORITE_PANEL_ID}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
<DataTableDefaultView
icon={PublicFavoriteIcon}
messages={['Public favorites list is empty.']} />
- } />;
+ } /></div>;
}
}
)
);
const required = (value: string[]) =>
- value.length > 0
+ value && value.length > 0
? undefined
: ERROR_MESSAGE;
);
const required = (value: string[]) =>
- value.length > 0
+ value && value.length > 0
? undefined
: ERROR_MESSAGE;
);
const required = (value: string[] = []) =>
- value.length > 0
+ value && value.length > 0
? undefined
: ERROR_MESSAGE;
<Grid container spacing={16}>
<Grid container item xs={6} className={classes.root}>
<Grid item xs={12} className={classes.searchGrid}>
- <SearchInput value='' onSearch={onSearch} />
+ <SearchInput selfClearProp={JSON.stringify(selectedWorkflow)} value='' onSearch={onSearch} />
</Grid>
<Grid item xs={12}>
<List className={classes.list}>
} from 'store/context-menu/context-menu-actions';
import { GroupContentsResource } from 'services/groups-service/groups-service';
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
button: {
marginLeft: theme.spacing.unit
},
+ root: {
+ width: '100%',
+ },
});
interface SharedWithMePanelDataProps {
}))(
class extends React.Component<SharedWithMePanelProps> {
render() {
- return <DataExplorer
+ return <div className={this.props.classes.root}><DataExplorer
id={SHARED_WITH_ME_PANEL_ID}
onRowClick={this.handleRowClick}
onRowDoubleClick={this.handleRowDoubleClick}
onContextMenu={this.handleContextMenu}
contextMenuColumn={false}
- dataTableDefaultView={<DataTableDefaultView icon={ShareMeIcon} />} />;
+ dataTableDefaultView={<DataTableDefaultView icon={ShareMeIcon} />} /></div>;
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
import { createTree } from 'models/tree';
import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
import { ResourcesState } from 'store/resources/resources';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
export enum SubprocessPanelColumnNames {
NAME = "Name",
'The current process may not have any or none matches current filtering.'
];
-export const SubprocessPanelRoot = (props: SubprocessPanelProps) => {
+export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps) => {
return <DataExplorer
id={SUBPROCESS_PANEL_ID}
onRowClick={props.onItemClick}
<DataTableDefaultView
icon={ProcessIcon}
messages={DEFAULT_VIEW_MESSAGES} />
- } />;
+ }
+ doHidePanel={props.doHidePanel}
+ doMaximizePanel={props.doMaximizePanel}
+ panelMaximized={props.panelMaximized}
+ panelName={props.panelName} />;
};
\ No newline at end of file
getTrashPanelTypeFilters
} from 'store/resource-type-filters/resource-type-filters';
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
button: {
marginLeft: theme.spacing.unit
},
+ root: {
+ width: '100%',
+ },
});
export enum TrashPanelColumnNames {
}))(
class extends React.Component<TrashPanelProps> {
render() {
- return <DataExplorer
+ return <div className={this.props.classes.root}><DataExplorer
id={TRASH_PANEL_ID}
onRowClick={this.handleRowClick}
onRowDoubleClick={this.handleRowDoubleClick}
<DataTableDefaultView
icon={TrashIcon}
messages={['Your trash list is empty.']}/>
- } />;
+ } /></div>;
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
import { USERS_PANEL_ID, openUserCreateDialog } from 'store/users/users-actions';
import { noop } from 'lodash';
-type UserPanelRules = "button";
+type UserPanelRules = "button" | 'root' | 'content';
const styles = withStyles<UserPanelRules>(theme => ({
button: {
textAlign: 'right',
alignSelf: 'center'
},
+ root: {
+ width: '100%',
+ },
+ content: {
+ // reserve space for the tab bar
+ height: `calc(100% - ${theme.spacing.unit * 7}px)`,
+ }
}));
export enum UserPanelColumnNames {
render() {
const { value } = this.state;
- return <Paper>
+ return <Paper className={this.props.classes.root}>
<Tabs value={value} onChange={this.handleChange} fullWidth>
<Tab label="USERS" />
<Tab label="ACTIVITY" disabled />
</Tabs>
{value === 0 &&
- <span>
+ <div className={this.props.classes.content}>
<DataExplorer
id={USERS_PANEL_ID}
onRowClick={noop}
icon={ShareMeIcon}
messages={['Your user list is empty.']} />
} />
- </span>}
+ </div>}
</Paper>;
}
minWidth: 0,
paddingLeft: theme.spacing.unit * 3,
paddingRight: theme.spacing.unit * 3,
+ // Reserve vertical space for app bar + MainContentBar
+ minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`,
+ display: 'flex',
}
});