Merge branch 'master' into 14604-ui-improvements
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 4 Jan 2019 10:42:17 +0000 (11:42 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 4 Jan 2019 10:42:17 +0000 (11:42 +0100)
refs #14604

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

116 files changed:
src/common/custom-theme.ts
src/components/collection-panel-files/collection-panel-files.tsx
src/components/confirmation-dialog/confirmation-dialog.tsx
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table-filters/data-table-filters-popover.tsx
src/components/data-table-filters/data-table-filters.tsx
src/components/data-table/data-table.tsx
src/components/default-code-snippet/default-code-snippet.tsx
src/components/default-view/default-view.tsx
src/components/file-tree/file-tree-item.tsx
src/components/file-tree/file-tree.tsx
src/components/file-upload-dialog/file-upload-dialog.tsx
src/components/file-upload/file-upload.tsx
src/components/form-field/form-field.tsx [new file with mode: 0644]
src/components/list-item-text-icon/list-item-text-icon.tsx
src/components/rename-dialog/rename-dialog.tsx
src/components/switch-field/switch-field.tsx [new file with mode: 0644]
src/components/text-field/text-field.tsx
src/models/api-client-authorization.ts
src/models/details.ts
src/models/scheduling-parameters.ts
src/models/search-bar.ts
src/routes/route-change-handlers.ts
src/services/api/order-builder.test.ts
src/services/auth-service/auth-service.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/common-service.ts
src/services/groups-service/groups-service.ts
src/store/advanced-tab/advanced-tab.ts
src/store/api-client-authorizations/api-client-authorizations-actions.ts
src/store/api-client-authorizations/api-client-authorizations-middleware-service.ts [new file with mode: 0644]
src/store/api-client-authorizations/api-client-authorizations-reducer.ts [deleted file]
src/store/auth/auth-action-session.ts
src/store/context-menu/context-menu-actions.ts
src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/data-explorer/data-explorer-middleware.test.ts
src/store/data-explorer/data-explorer-middleware.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/details-panel/details-panel-action.ts
src/store/details-panel/details-panel-reducer.ts
src/store/resource-type-filters/resource-type-filters.ts
src/store/resources/resources-actions.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/store/search-bar/search-bar-actions.test.ts
src/store/search-bar/search-bar-actions.ts
src/store/search-results-panel/search-results-middleware-service.ts
src/store/store.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/store/users/users-actions.ts
src/store/workbench/workbench-actions.ts
src/validators/min.tsx [new file with mode: 0644]
src/validators/optional.tsx [new file with mode: 0644]
src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
src/views-components/api-client-authorizations-dialog/attributes-dialog.tsx
src/views-components/api-client-authorizations-dialog/help-dialog.tsx
src/views-components/collection-panel-files/collection-panel-files.ts
src/views-components/compute-nodes-dialog/attributes-dialog.tsx
src/views-components/context-menu/action-sets/user-action-set.ts
src/views-components/current-token-dialog/current-token-dialog.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/details-panel/file-details.tsx [new file with mode: 0644]
src/views-components/dialog-create/dialog-user-create.tsx
src/views-components/dialog-forms/setup-shell-account-dialog.tsx [new file with mode: 0644]
src/views-components/form-fields/search-bar-form-fields.tsx
src/views-components/form-fields/user-form-fields.tsx
src/views-components/groups-dialog/attributes-dialog.tsx
src/views-components/groups-dialog/member-attributes-dialog.tsx
src/views-components/keep-services-dialog/attributes-dialog.tsx
src/views-components/links-dialog/attributes-dialog.tsx
src/views-components/main-app-bar/help-menu.tsx
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/process-command-dialog/process-command-dialog.tsx
src/views-components/process-input-dialog/process-input-dialog.tsx
src/views-components/project-properties-dialog/project-properties-dialog.tsx
src/views-components/remove-dialog/remove-dialog.tsx
src/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog.tsx
src/views-components/repository-attributes-dialog/repository-attributes-dialog.tsx
src/views-components/rich-text-editor-dialog/rich-text-editor-dialog.tsx
src/views-components/sharing-dialog/sharing-dialog-content.tsx
src/views-components/sharing-dialog/sharing-management-form-component.tsx
src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
src/views-components/ssh-keys-dialog/attributes-dialog.tsx
src/views-components/ssh-keys-dialog/public-key-dialog.tsx
src/views-components/user-dialog/attributes-dialog.tsx
src/views-components/user-dialog/manage-dialog.tsx [new file with mode: 0644]
src/views-components/virtual-machines-dialog/attributes-dialog.tsx
src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
src/views/api-client-authorization-panel/api-client-authorization-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/compute-node-panel/compute-node-panel-root.tsx
src/views/login-panel/login-panel.tsx
src/views/my-account-panel/my-account-panel-root.tsx
src/views/process-log-panel/process-log-code-snippet.tsx
src/views/process-log-panel/process-log-main-card.tsx
src/views/process-panel/process-information-card.tsx
src/views/process-panel/process-subprocesses-card.tsx
src/views/process-panel/subprocesses-card.tsx
src/views/repositories-panel/repositories-panel.tsx
src/views/run-process-panel/inputs/directory-array-input.tsx
src/views/run-process-panel/inputs/file-array-input.tsx
src/views/run-process-panel/inputs/int-input.tsx
src/views/run-process-panel/run-process-advanced-form.tsx
src/views/run-process-panel/run-process-second-step.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/site-manager-panel/site-manager-panel-root.tsx
src/views/ssh-key-panel/ssh-key-panel-root.tsx
src/views/trash-panel/trash-panel.tsx
src/views/user-panel/user-panel.tsx
src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
src/views/workbench/workbench.tsx
src/views/workflow-panel/workflow-description-card.tsx
src/views/workflow-panel/workflow-panel-view.tsx

index f7559eed08050876519c215745d45e447d9cb0b0..0faf781d6d27edbcb8b09db7496902927ffb8feb 100644 (file)
@@ -39,6 +39,9 @@ const grey900 = grey["900"];
 const rocheBlue = '#06C';
 
 export const themeOptions: ArvadosThemeOptions = {
+    typography: {
+        useNextVariants: true,
+    },
     customs: {
         colors: {
             green700: green["700"],
@@ -148,7 +151,7 @@ export const themeOptions: ArvadosThemeOptions = {
             dark: teal.A400,
             contrastText: '#fff'
         }
-    }
+    },
 };
 
 export const CustomTheme = createMuiTheme(themeOptions);
\ No newline at end of file
index 9a534e4bcba8a1fcf24dd09f1d0b32fc53fdf1fa..581c3a7691dee32a4c9545533742699a52e585a6 100644 (file)
@@ -17,6 +17,7 @@ export interface CollectionPanelFilesProps {
     onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
     onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
     onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+    onFileClick: (id: string) => void;
 }
 
 type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button';
@@ -53,7 +54,7 @@ export const CollectionPanelFiles =
                     classes={{ action: classes.button }}
                     action={
                         <Button onClick={onUploadDataClick}
-                            variant='raised'
+                            variant='contained'
                             color='primary'
                             size='small'>
                             <DownloadIcon className={classes.uploadIcon} />
index 079576fd101800f6f04cf24280bdc3e4ca45af05..257eadf3f8ff75bf7fb8152d4bb9a7d79d98e3ed 100644 (file)
@@ -31,7 +31,7 @@ export const ConfirmationDialog = (props: ConfirmationDialogProps & WithDialogPr
         </DialogContent>
         <DialogActions style={{ margin: '0px 24px 24px' }}>
             <Button
-                variant='flat'
+                variant='text'
                 color='primary'
                 onClick={props.closeDialog}>
                 {props.data.cancelButtonLabel || 'Cancel'}
index d74b5319d789b743bfa6878bd6e359839cdbe16d..1d8738abb1e29bcb3207efa755a6f3e1f1bb4fc8 100644 (file)
@@ -8,11 +8,10 @@ import * as Adapter from 'enzyme-adapter-react-16';
 
 import { DataExplorer } from "./data-explorer";
 import { ColumnSelector } from "../column-selector/column-selector";
-import { DataTable } from "../data-table/data-table";
+import { DataTable, DataTableFetchMode } from "../data-table/data-table";
 import { SearchInput } from "../search-input/search-input";
 import { TablePagination } from "@material-ui/core";
 import { ProjectIcon } from '../icon/icon';
-import { DefaultView } from '../default-view/default-view';
 import { SortDirection } from '../data-table/data-column';
 
 configure({ adapter: new Adapter() });
@@ -36,7 +35,7 @@ describe("<DataExplorer />", () => {
     it("communicates with <ColumnSelector/>", () => {
         const onColumnToggle = jest.fn();
         const onSetColumns = jest.fn();
-        const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [] }];
+        const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: {} }];
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
             columns={columns}
@@ -53,7 +52,7 @@ describe("<DataExplorer />", () => {
         const onSortToggle = jest.fn();
         const onRowClick = jest.fn();
         const onSetColumns = jest.fn();
-        const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [] }];
+        const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: {} }];
         const items = [{ name: "item 1" }];
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
@@ -95,6 +94,7 @@ describe("<DataExplorer />", () => {
 });
 
 const mockDataExplorerProps = () => ({
+    fetchMode: DataTableFetchMode.PAGINATED,
     columns: [],
     items: [],
     itemsAvailable: 0,
@@ -114,6 +114,7 @@ const mockDataExplorerProps = () => ({
     onContextMenu: jest.fn(),
     defaultIcon: ProjectIcon,
     onSetColumns: jest.fn(),
+    onLoadMore: jest.fn(),
     defaultMessages: ['testing'],
     contextMenuColumn: true
 });
index b6ca215d56463ec7f7ba3742ae06f5105ffa69ad..878f47ff769b026741c7203b78c77bdb9317e156 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from '@material-ui/core';
 import { ColumnSelector } from "~/components/column-selector/column-selector";
-import { DataTable, DataColumns } from "~/components/data-table/data-table";
+import { DataTable, DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
 import { DataColumn, SortDirection } from "~/components/data-table/data-column";
 import { SearchInput } from '~/components/search-input/search-input';
 import { ArvadosTheme } from "~/common/custom-theme";
@@ -35,6 +35,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 interface DataExplorerDataProps<T> {
+    fetchMode: DataTableFetchMode;
     items: T[];
     itemsAvailable: number;
     columns: DataColumns<T>;
@@ -63,6 +64,7 @@ interface DataExplorerActionProps<T> {
     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
     onChangePage: (page: number) => void;
     onChangeRowsPerPage: (rowsPerPage: number) => void;
+    onLoadMore: (page: number) => void;
     extractKey?: (item: T) => React.Key;
 }
 
@@ -81,10 +83,10 @@ export const DataExplorer = withStyles(styles)(
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
-                paperKey
+                paperKey, fetchMode
             } = this.props;
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                <Toolbar className={classes.toolbar}>
+                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         {!hideSearchInput && <div className={classes.searchBox}>
                             <SearchInput
@@ -96,7 +98,7 @@ export const DataExplorer = withStyles(styles)(
                             columns={columns}
                             onColumnToggle={onColumnToggle} />}
                     </Grid>
-                </Toolbar>
+                </Toolbar>}
                 <DataTable
                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
                     items={items}
@@ -110,14 +112,18 @@ export const DataExplorer = withStyles(styles)(
                     defaultView={dataTableDefaultView} />
                 <Toolbar className={classes.footer}>
                     <Grid container justify="flex-end">
-                        <TablePagination
+                        {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
                             count={itemsAvailable}
                             rowsPerPage={rowsPerPage}
                             rowsPerPageOptions={rowsPerPageOptions}
                             page={this.props.page}
                             onChangePage={this.changePage}
                             onChangeRowsPerPage={this.changeRowsPerPage}
-                            component="div" />
+                            component="div" /> : <Button
+                                variant="text"
+                                size="medium"
+                                onClick={this.loadMore}
+                                >Load more</Button>}
                     </Grid>
                 </Toolbar>
             </Paper>;
@@ -131,6 +137,10 @@ export const DataExplorer = withStyles(styles)(
             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
         }
 
+        loadMore = () => {
+            this.props.onLoadMore(this.props.page + 1);
+        }
+
         renderContextMenuTrigger = (item: T) =>
             <Grid container justify="center">
                 <Tooltip title="More options" disableFocusListener>
@@ -144,7 +154,6 @@ export const DataExplorer = withStyles(styles)(
             name: "Actions",
             selected: true,
             configurable: false,
-            sortDirection: SortDirection.NONE,
             filters: createTree(),
             key: "context-actions",
             render: this.renderContextMenuTrigger
index b79d36b47a9236e11503bd4e995daac18a703ca2..4015126a554ac8b5c80b0a5a09732c9f3567e8c2 100644 (file)
@@ -113,7 +113,7 @@ export const DataTableFiltersPopover = withStyles(styles)(
                         <CardActions>
                             <Button
                                 color="primary"
-                                variant="raised"
+                                variant='contained'
                                 size="small"
                                 onClick={this.submit}>
                                 Ok
index 7033d369e7dcdb0f1daa0e9168fac1427719db3c..a57f29aac44805b3abb24aad5128447b635b32b3 100644 (file)
@@ -151,7 +151,7 @@ export const DataTableFilters = withStyles(styles)(
                         <CardActions>
                             <Button
                                 color="primary"
-                                variant="raised"
+                                variant='contained'
                                 size="small"
                                 onClick={this.submit}>
                                 Ok
index d9157a6a5d0e14851f141309c8a89f96af818c50..8298861b2473a34ab13a0b3c85099b9e1af4b4e4 100644 (file)
@@ -12,6 +12,11 @@ import { countNodes } from '~/models/tree';
 
 export type DataColumns<T> = Array<DataColumn<T>>;
 
+export enum DataTableFetchMode {
+    PAGINATED,
+    INFINITE
+}
+
 export interface DataTableDataProps<T> {
     items: T[];
     columns: DataColumns<T>;
index e8b89f321ca809302bcbdab1f09f4c13d09f6d38..c81865dbd30ad8563c175498d220cc82e77a2dc7 100644 (file)
@@ -19,7 +19,8 @@ const theme = createMuiTheme({
         }
     },
     typography: {
-        fontFamily: 'monospace'
+        fontFamily: 'monospace',
+        useNextVariants: true,
     }
 });
 
index 3bc3e5290dddb0baa6b79b0893fce0fcdeebaf2b..036fe1e4be179575309dd47b382d668b751b6721 100644 (file)
@@ -39,7 +39,7 @@ export const DefaultView = withStyles(styles)(
         <Typography className={classnames([classes.root, classRoot])} component="div">
             <Icon className={classnames([classes.icon, classIcon])} />
             {messages.map((msg: string, index: number) => {
-                return <Typography key={index} variant="body1" 
+                return <Typography key={index}  
                     className={classnames([classes.message, classMessage])}>{msg}</Typography>;
             })}
         </Typography>
index 0e8c92e2da6c2343f589696fa999d7e7f9fce49a..0ece937775d2527fc1881a198314f224be6840fa 100644 (file)
@@ -46,7 +46,7 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
             return <>
                 <div className={classes.root}>
                     <ListItemTextIcon
-                        icon={getIcon(item)}
+                        icon={getIcon(item.data.type)}
                         name={item.data.name} />
                     <div className={classes.spacer} />
                     <Typography
@@ -60,7 +60,6 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
                         </IconButton>
                     </Tooltip>
                 </div >
-                <FileThumbnail file={item.data} />
             </>;
         }
 
@@ -69,8 +68,8 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
         }
     });
 
-const getIcon = (item: TreeItem<FileTreeData>) => {
-    switch (item.data.type) {
+export const getIcon = (type: string) => {
+    switch (type) {
         case 'directory':
             return ProjectIcon;
         case 'file':
index 0a96254c58607c2ccc477a1128c624fb75f76cc9..4f581a6cf860c3ad5178d9f74d90390d41a7713a 100644 (file)
@@ -12,6 +12,7 @@ export interface FileTreeProps {
     onMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
     onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
     onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+    onFileClick: (id: string) => void;
 }
 
 export class FileTree extends React.Component<FileTreeProps> {
@@ -36,7 +37,9 @@ export class FileTree extends React.Component<FileTreeProps> {
         this.props.onCollapseToggle(id, status);
     }
 
-    handleToggleActive = () => { return; };
+    handleToggleActive = (_: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
+        this.props.onFileClick(item.id);
+    }
 
     handleSelectionChange = (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
         event.stopPropagation();
index 7215b6d12ad7ee92380b827d7724334354c3898e..b178cdae6edd0d5af1759a1fe65311b80f047665 100644 (file)
@@ -32,7 +32,7 @@ export const FilesUploadDialog = (props: FilesUploadDialogProps & WithDialogProp
         </DialogContent>
         <DialogActions>
             <Button
-                variant='flat'
+                variant='text'
                 color='primary'
                 disabled={props.uploading}
                 onClick={props.closeDialog}>
index 41054df43f9da6c6d76fb446a118dc15781606bd..64656e4865a920435883d1e7fa146a99890d1c90 100644 (file)
@@ -136,7 +136,7 @@ export const FileUpload = withStyles(styles)(
                     {files.length === 0 &&
                         <Grid container justify="center" alignItems="center" className={classes.container}>
                             <Grid item component={"span"}>
-                                <Typography variant={"subheading"}>
+                                <Typography variant='subtitle1'>
                                     <CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
                             </Typography>
                             </Grid>
diff --git a/src/components/form-field/form-field.tsx b/src/components/form-field/form-field.tsx
new file mode 100644 (file)
index 0000000..32362ac
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps, WrappedFieldInputProps } from 'redux-form';
+import { FormGroup, FormLabel, FormHelperText } from '@material-ui/core';
+
+interface FormFieldCustomProps {
+    children: <P>(props: WrappedFieldInputProps) => React.ReactElement<P>;
+    label?: string;
+    helperText?: string;
+    required?: boolean;
+}
+
+export type FormFieldProps = FormFieldCustomProps & WrappedFieldProps;
+
+export const FormField = ({ children, ...props }: FormFieldProps & WrappedFieldProps) => {
+    return (
+        <FormGroup>
+
+            <FormLabel
+                focused={props.meta.active}
+                required={props.required}
+                error={props.meta.touched && !!props.meta.error}>
+                {props.label}
+            </FormLabel>
+
+            { children(props.input) }
+
+            <FormHelperText error={props.meta.touched && !!props.meta.error}>
+                {
+                    props.meta.touched && props.meta.error
+                        ? props.meta.error
+                        : props.helperText
+                }
+            </FormHelperText>
+
+        </FormGroup>
+    );
+};
index 29768c050f797974bd4e33815ce5d41d6ae6dc2c..3afc2cf8773293a18566c468253aaade648c3a4a 100644 (file)
@@ -51,7 +51,7 @@ export const ListItemTextIcon = withStyles(styles)(
                         <Icon style={{ fontSize: `${iconSize}rem` }} />
                     </ListItemIcon>
                     <ListItemText primary={
-                        <Typography variant='body1' className={classnames(classes.listItemText, {
+                        <Typography  className={classnames(classes.listItemText, {
                                 [classes.active]: isActive
                             })}>
                             {name}
index e025764848de03adae00bc14227764acff00c58b..75c25b789cb19a9f8d10d546aaa75595fdb87f45 100644 (file)
@@ -23,7 +23,7 @@ export const RenameDialog = (props: WithDialogProps<string> & InjectedFormProps<
             </DialogContent>
             <DialogActions>
                 <Button
-                    variant='flat'
+                    variant='text'
                     color='primary'
                     disabled={props.submitting}
                     onClick={props.closeDialog}>
diff --git a/src/components/switch-field/switch-field.tsx b/src/components/switch-field/switch-field.tsx
new file mode 100644 (file)
index 0000000..ac7b140
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FormFieldProps, FormField } from '~/components/form-field/form-field';
+import { Switch } from '@material-ui/core';
+import { SwitchProps } from '@material-ui/core/Switch';
+
+export const SwitchField = ({ switchProps, ...props }: FormFieldProps & { switchProps: SwitchProps }) =>
+    <FormField {...props}>
+        {input => <Switch {...switchProps} checked={input.value} onChange={input.onChange} />}
+    </FormField>;
+
index 93c4080f0fead0c7330f0a65a6824bdb628da8e1..4788e18c51df5824608fdfc8e7793f07a1d2288a 100644 (file)
@@ -26,10 +26,11 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 type TextFieldProps = WrappedFieldProps & WithStyles<CssRules>;
 
 export const TextField = withStyles(styles)((props: TextFieldProps & {
-    label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, disabled?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string
+    label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, disabled?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string,
+    helperText?: string, type?: string,
 }) =>
     <MaterialTextField
-        helperText={props.meta.touched && props.meta.error}
+        helperText={(props.meta.touched && props.meta.error) || props.helperText}
         className={props.classes.textField}
         label={props.label}
         disabled={props.disabled || props.meta.submitting}
@@ -42,6 +43,7 @@ export const TextField = withStyles(styles)((props: TextFieldProps & {
         children={props.children}
         margin={props.margin}
         placeholder={props.placeholder}
+        type={props.type}
         {...props.input}
     />);
 
index aff50be6d3aed62945ab5d02622c02ac243a39d4..01a92017d54d9ae9b8323d101f549359208323d5 100644 (file)
@@ -2,7 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-export interface ApiClientAuthorization {
+import { Resource } from '~/models/resource';
+
+export interface ApiClientAuthorization extends Resource {
     uuid: string;
     apiToken: string;
     apiClientId: number;
index 42eb5c9c604acf6d14082cf2745000b719cecaa0..f53007086749c55cb4070d7fd187548a8c76058c 100644 (file)
@@ -6,5 +6,6 @@ import { ProjectResource } from "./project";
 import { CollectionResource } from "./collection";
 import { ProcessResource } from "./process";
 import { EmptyResource } from "./empty";
+import { CollectionFile, CollectionDirectory } from '~/models/collection-file';
 
-export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory;
index 62f7224c3b3dee3abd6078c84f04c3103eb3bf2e..50ce4156a13fe689cacd02efb95762a6c5f4afdb 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 export interface SchedulingParameters {
-    partitions: string[];
-    preemptible: boolean;
-    maxRunTime: number;
+    partitions?: string[];
+    preemptible?: boolean;
+    maxRunTime?: number;
 }
index 798f9c8f27bd84d1aeb6fbb06aaa244771ff840b..effaeed4c0e676882e6bccc9f445e9eb732d9483 100644 (file)
@@ -6,7 +6,7 @@ import { ResourceKind } from '~/models/resource';
 
 export type SearchBarAdvanceFormData = {
     type?: ResourceKind;
-    cluster?: ClusterObjectType;
+    cluster?: string;
     projectUuid?: string;
     inTrash: boolean;
     dateFrom: string;
@@ -21,9 +21,3 @@ export interface PropertyValue {
     key: string;
     value: string;
 }
-
-export enum ClusterObjectType {
-    INDIANAPOLIS = "indianapolis",
-    KAISERAUGST = "kaiseraugst",
-    PENZBERG = "penzberg"
-}
index bb88f4a1aff5c33173d9526a9d74753f25a53abf..141ae20b54e57214ec4c9fe8f40362f30c110c8c 100644 (file)
@@ -18,6 +18,7 @@ export const addRouteChangeHandlers = (history: History, store: RootStore) => {
 };
 
 const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
+
     const rootMatch = Routes.matchRootRoute(pathname);
     const projectMatch = Routes.matchProjectRoute(pathname);
     const collectionMatch = Routes.matchCollectionRoute(pathname);
@@ -99,4 +100,4 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     } else if (linksMatch) {
         store.dispatch(WorkbenchActions.loadLinks);
     }
-};
+};
\ No newline at end of file
index f56e0634357e2ad4ff3a8b27eba6fedec8ee493c..496b74a2563545c926c15bfdad24d4a3d9e5765f 100644 (file)
@@ -8,8 +8,8 @@ describe("OrderBuilder", () => {
     it("should build correct order query", () => {
         const order = new OrderBuilder()
             .addAsc("kind")
-            .addDesc("modifiedAt")
+            .addDesc("createdAt")
             .getOrder();
-        expect(order).toEqual("kind asc,modified_at desc");
+        expect(order).toEqual("kind asc,created_at desc");
     });
 });
index 8601e2084def92f70cc82794ae19ad3b24353c5c..6fe27c8d4d819a24556ca38983404c17091fa3b2 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getUserFullname, User, UserPrefs } from "~/models/user";
+import { getUserFullname, User, UserPrefs, UserResource } from '~/models/user';
 import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
 import * as uuid from "uuid/v4";
@@ -77,7 +77,7 @@ export class AuthService {
             : undefined;
     }
 
-    public saveUser(user: User) {
+    public saveUser(user: User | UserResource) {
         localStorage.setItem(USER_EMAIL_KEY, user.email);
         localStorage.setItem(USER_FIRST_NAME_KEY, user.firstName);
         localStorage.setItem(USER_LAST_NAME_KEY, user.lastName);
index 471c32fa21020e8b9eb1db9ae248ad3c6aa30f8f..17c287d22ebaf6fad2d60b774d0fc400f29af5b7 100644 (file)
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as _ from "lodash";
 import { AxiosInstance } from "axios";
 import { Resource } from "src/models/resource";
 import { ApiActions } from "~/services/api/api-actions";
@@ -18,11 +17,9 @@ export enum CommonResourceServiceError {
 }
 
 export class CommonResourceService<T extends Resource> extends CommonService<T> {
-
-    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+   constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
         super(serverApi, resourceType, actions);
     }
-    
 }
 
 export const getCommonResourceServiceError = (errorResponse: any) => {
index b301a727551135ef4d6bff23f11d90446b13b57b..07ff398a4abdd7d4409bb67dd660e02f304af359 100644 (file)
@@ -23,6 +23,7 @@ export interface ListArguments {
 }
 
 export interface ListResults<T> {
+    clusterId?: string;
     kind: string;
     offset: number;
     limit: number;
@@ -128,4 +129,4 @@ export class CommonService<T> {
             this.actions
         );
     }
-}
\ No newline at end of file
+}
index d8b33f601f94c553e79e8be078f0dc2c9c4ee62f..a676557a78afd4e44c3b5c1ae771077b63541f49 100644 (file)
@@ -5,7 +5,7 @@
 import * as _ from "lodash";
 import { CommonResourceService } from '~/services/common-service/common-resource-service';
 import { ListResults, ListArguments } from '~/services/common-service/common-service';
-import { AxiosInstance } from "axios";
+import { AxiosInstance, AxiosRequestConfig } from "axios";
 import { CollectionResource } from "~/models/collection";
 import { ProjectResource } from "~/models/project";
 import { ProcessResource } from "~/models/process";
@@ -13,6 +13,7 @@ import { ResourceKind } from '~/models/resource';
 import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
 import { ApiActions } from "~/services/api/api-actions";
 import { GroupResource } from "~/models/group";
+import { Session } from "~/models/session";
 
 export interface ContentsArguments {
     limit?: number;
@@ -39,7 +40,7 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
         super(serverApi, "groups", actions);
     }
 
-    async contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
+    async contents(uuid: string, args: ContentsArguments = {}, session?: Session): Promise<ListResults<GroupContentsResource>> {
         const { filters, order, ...other } = args;
         const params = {
             ...other,
@@ -48,17 +49,18 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
         };
 
         const pathUrl = uuid ? `${uuid}/contents` : 'contents';
+
+        const cfg: AxiosRequestConfig = { params: CommonResourceService.mapKeys(_.snakeCase)(params) };
+        if (session) {
+            cfg.baseURL = session.baseUrl;
+        }
+
         const response = await CommonResourceService.defaultResponse(
-                this.serverApi
-                    .get(this.resourceType + pathUrl, {
-                        params: CommonResourceService.mapKeys(_.snakeCase)(params)
-                    }),
-                this.actions, 
-                false
-            );
+            this.serverApi.get(this.resourceType + pathUrl, cfg), this.actions, false
+        );
 
         const { items, ...res } = response;
-        const mappedItems = items.map((item: GroupContentsResource) => {
+        const mappedItems = (items || []).map((item: GroupContentsResource) => {
             const mappedItem = TrashableResourceService.mapKeys(_.camelCase)(item);
             if (item.kind === ResourceKind.COLLECTION || item.kind === ResourceKind.PROJECT) {
                 const { properties } = item;
@@ -68,7 +70,7 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
             }
         });
         const mappedResponse = { ...TrashableResourceService.mapKeys(_.camelCase)(res) };
-        return { ...mappedResponse, items: mappedItems };
+        return { ...mappedResponse, items: mappedItems, clusterId: session && session.clusterId };
     }
 
     shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
index 0cb1c74038503c5c1f80fc58c36cb07d9f0803e5..921b1cd7d33784d88d19b9b1c092741595f2f904 100644 (file)
@@ -257,7 +257,8 @@ export const openAdvancedTabDialog = (uuid: string) =>
                 dispatch<any>(initAdvancedTabDialog(advanceDataComputeNode));
                 break;
             case ResourceKind.API_CLIENT_AUTHORIZATION:
-                const dataApiClientAuthorization = getState().apiClientAuthorizations.find(item => item.uuid === uuid);
+                const apiClientAuthorizationResources = getState().resources;
+                const dataApiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(apiClientAuthorizationResources);
                 const advanceDataApiClientAuthorization = advancedTabData({
                     uuid,
                     metadata: '',
index 8ed8a3890e27e1405b90c1dc820685d610c7cc70..e4f9e9f7e52224bf5a2166caffb199ae920c1d8d 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { RootState } from '~/store/store';
 import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
 import { ServiceRepository } from "~/services/services";
@@ -11,28 +10,26 @@ import { dialogActions } from '~/store/dialog/dialog-actions';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
 import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { getResource } from '~/store/resources/resources';
 
-export const apiClientAuthorizationsActions = unionize({
-    SET_API_CLIENT_AUTHORIZATIONS: ofType<ApiClientAuthorization[]>(),
-    REMOVE_API_CLIENT_AUTHORIZATION: ofType<string>()
-});
 
-export type ApiClientAuthorizationsActions = UnionOf<typeof apiClientAuthorizationsActions>;
+export const API_CLIENT_AUTHORIZATION_PANEL_ID = 'apiClientAuthorizationPanelId';
+export const apiClientAuthorizationsActions = bindDataExplorerActions(API_CLIENT_AUTHORIZATION_PANEL_ID);
 
 export const API_CLIENT_AUTHORIZATION_REMOVE_DIALOG = 'apiClientAuthorizationRemoveDialog';
 export const API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG = 'apiClientAuthorizationAttributesDialog';
 export const API_CLIENT_AUTHORIZATION_HELP_DIALOG = 'apiClientAuthorizationHelpDialog';
 
+
 export const loadApiClientAuthorizationsPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const user = getState().auth.user;
         if (user && user.isAdmin) {
             try {
                 dispatch(setBreadcrumbs([{ label: 'Api client authorizations' }]));
-                const response = await services.apiClientAuthorizationService.list();
-                dispatch(apiClientAuthorizationsActions.SET_API_CLIENT_AUTHORIZATIONS(response.items));
+                dispatch(apiClientAuthorizationsActions.REQUEST_ITEMS());
             } catch (e) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 }));
                 return;
             }
         } else {
@@ -43,7 +40,8 @@ export const loadApiClientAuthorizationsPanel = () =>
 
 export const openApiClientAuthorizationAttributesDialog = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const apiClientAuthorization = getState().apiClientAuthorizations.find(node => node.uuid === uuid);
+        const { resources } = getState();
+        const apiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(resources);
         dispatch(dialogActions.OPEN_DIALOG({ id: API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG, data: { apiClientAuthorization } }));
     };
 
@@ -65,7 +63,7 @@ export const removeApiClientAuthorization = (uuid: string) =>
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
         try {
             await services.apiClientAuthorizationService.delete(uuid);
-            dispatch(apiClientAuthorizationsActions.REMOVE_API_CLIENT_AUTHORIZATION(uuid));
+            dispatch(apiClientAuthorizationsActions.REQUEST_ITEMS());
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Api client authorization has been successfully removed.', hideDuration: 2000 }));
         } catch (e) {
             return;
diff --git a/src/store/api-client-authorizations/api-client-authorizations-middleware-service.ts b/src/store/api-client-authorizations/api-client-authorizations-middleware-service.ts
new file mode 100644 (file)
index 0000000..99e2a95
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-service';
+import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { ApiClientAuthorizationPanelColumnNames } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
+import { SortDirection } from '~/components/data-table/data-column';
+
+export class ApiClientAuthorizationMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        try {
+            const response = await this.services.apiClientAuthorizationService.list(getParams(dataExplorer));
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch {
+            api.dispatch(couldNotFetchLinks());
+        }
+    }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = getSortColumn(dataExplorer);
+    const order = new OrderBuilder<ApiClientAuthorization>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+
+        const columnName = sortColumn && sortColumn.name === ApiClientAuthorizationPanelColumnNames.UUID ? "uuid" : "updatedAt";
+        return order
+            .addOrder(sortDirection, columnName)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<ApiClientAuthorization>) =>
+    apiClientAuthorizationsActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchLinks = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch api client authorizations.',
+        kind: SnackbarKind.ERROR
+    });
diff --git a/src/store/api-client-authorizations/api-client-authorizations-reducer.ts b/src/store/api-client-authorizations/api-client-authorizations-reducer.ts
deleted file mode 100644 (file)
index 7084dea..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { 
-    apiClientAuthorizationsActions, 
-    ApiClientAuthorizationsActions 
-} from '~/store/api-client-authorizations/api-client-authorizations-actions';
-import { ApiClientAuthorization } from '~/models/api-client-authorization';
-
-export type ApiClientAuthorizationsState = ApiClientAuthorization[];
-
-const initialState: ApiClientAuthorizationsState = [];
-
-export const apiClientAuthorizationsReducer = 
-    (state: ApiClientAuthorizationsState = initialState, action: ApiClientAuthorizationsActions): ApiClientAuthorizationsState =>
-        apiClientAuthorizationsActions.match(action, {
-            SET_API_CLIENT_AUTHORIZATIONS: apiClientAuthorizations => apiClientAuthorizations,
-            REMOVE_API_CLIENT_AUTHORIZATION: (uuid: string) => 
-                state.filter((apiClientAuthorization) => apiClientAuthorization.uuid !== uuid),
-            default: () => state
-        });
\ No newline at end of file
index e5e2e575cc71f113fe696f9d6ea06eda0bf50d9e..b32e205016a1824a8c6ed3554637ffac68b2b726 100644 (file)
@@ -56,7 +56,7 @@ const getTokenUuid = async (baseUrl: string, token: string): Promise<string> =>
         return Promise.resolve(uuid);
     }
 
-    const resp = await Axios.get(`${baseUrl}/api_client_authorizations`, {
+    const resp = await Axios.get(`${baseUrl}api_client_authorizations`, {
         headers: {
             Authorization: `OAuth2 ${token}`
         },
@@ -80,7 +80,7 @@ const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) =>
     return `v2/${tokenUuid}/${hmac}`;
 };
 
-const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{user: User, token: string}> => {
+const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{ user: User, token: string }> => {
     const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
     const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
     const user = await getUserDetails(baseUrl, saltedToken);
@@ -99,7 +99,7 @@ const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: S
     };
 };
 
-const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
+export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
 
 export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
     const baseUrl = await getRemoteHostBaseUrl(remoteHost);
@@ -207,7 +207,7 @@ export const initSessions = (authService: AuthService, config: Config, user: Use
 export const loadSiteManagerPanel = () =>
     async (dispatch: Dispatch<any>) => {
         try {
-            dispatch(setBreadcrumbs([{ label: 'Site Manager'}]));
+            dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
             dispatch(validateSessions());
         } catch (e) {
             return;
index c43d5685655c8667bc272246bedf6597b41ca7c9..ca89f3eb3fe87cf9b220299ca764ea652c535cba 100644 (file)
@@ -123,12 +123,12 @@ export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>,
     };
 
 export const openApiClientAuthorizationContextMenu =
-    (event: React.MouseEvent<HTMLElement>, apiClientAuthorization: ApiClientAuthorization) =>
+    (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
         (dispatch: Dispatch) => {
             dispatch<any>(openContextMenu(event, {
                 name: '',
-                uuid: apiClientAuthorization.uuid,
-                ownerUuid: apiClientAuthorization.ownerUuid,
+                uuid: resourceUuid,
+                ownerUuid: '',
                 kind: ResourceKind.API_CLIENT_AUTHORIZATION,
                 menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION
             }));
index 7797ae6cefc27759d3f994b0d7f14f9225b5f193..546ec8f3679512e24f00c1563cf4ad67d6f9a5fc 100644 (file)
@@ -3,15 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { DataColumns } from "~/components/data-table/data-table";
+import { DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
 import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
 
 export const dataExplorerActions = unionize({
+    CLEAR: ofType<{ id: string }>(),
     RESET_PAGINATION: ofType<{ id: string }>(),
-    REQUEST_ITEMS: ofType<{ id: string }>(),
+    REQUEST_ITEMS: ofType<{ id: string, criteriaChanged?: boolean }>(),
+    SET_FETCH_MODE: ofType<({ id: string, fetchMode: DataTableFetchMode })>(),
     SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
     SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilters }>(),
     SET_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
+    APPEND_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
     SET_PAGE: ofType<{ id: string, page: number }>(),
     SET_ROWS_PER_PAGE: ofType<{ id: string, rowsPerPage: number }>(),
     TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
@@ -22,16 +25,22 @@ export const dataExplorerActions = unionize({
 export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
 
 export const bindDataExplorerActions = (id: string) => ({
+    CLEAR: () =>
+        dataExplorerActions.CLEAR({ id }),
     RESET_PAGINATION: () =>
         dataExplorerActions.RESET_PAGINATION({ id }),
-    REQUEST_ITEMS: () =>
-        dataExplorerActions.REQUEST_ITEMS({ id }),
+    REQUEST_ITEMS: (criteriaChanged?: boolean) =>
+        dataExplorerActions.REQUEST_ITEMS({ id, criteriaChanged }),
+    SET_FETCH_MODE: (payload: { fetchMode: DataTableFetchMode }) =>
+        dataExplorerActions.SET_FETCH_MODE({ ...payload, id }),
     SET_COLUMNS: (payload: { columns: DataColumns<any> }) =>
         dataExplorerActions.SET_COLUMNS({ ...payload, id }),
     SET_FILTERS: (payload: { columnName: string, filters: DataTableFilters }) =>
         dataExplorerActions.SET_FILTERS({ ...payload, id }),
     SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
         dataExplorerActions.SET_ITEMS({ ...payload, id }),
+    APPEND_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
+        dataExplorerActions.APPEND_ITEMS({ ...payload, id }),
     SET_PAGE: (payload: { page: number }) =>
         dataExplorerActions.SET_PAGE({ ...payload, id }),
     SET_ROWS_PER_PAGE: (payload: { rowsPerPage: number }) =>
index 82ba5b4b8e517e9ad6541f228e53213e9ab3f81b..57fd0b59e34f759f7a2b0573613a78f9b7847a20 100644 (file)
@@ -25,7 +25,7 @@ export abstract class DataExplorerMiddlewareService {
         return getDataExplorerColumnFilters(columns, columnName);
     }
 
-    abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
+    abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean): void;
 }
 
 export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnName: string): DataTableFilters => {
index 00931bf8e3c95cd5d98404ac74a14b136253536a..5d729ce2d600290cb2f9d05acb268db861ad9eba 100644 (file)
@@ -38,7 +38,7 @@ describe("DataExplorerMiddleware", () => {
         middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
         middleware(dataExplorerActions.SET_PAGE({ id: "ServiceId", page: 0 }));
         middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
-        expect(api.dispatch).toHaveBeenCalledWith(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId" }));
+        expect(api.dispatch).toHaveBeenCalledWith(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId", criteriaChanged: false }));
         expect(api.dispatch).toHaveBeenCalledTimes(1);
     });
 
index f90f9a6ce39d1ceddff638959544d1ec24c1d06c..e377f3410fef8ba215747fcd5dbb41f16931e10f 100644 (file)
@@ -20,24 +20,24 @@ export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService):
             };
         dataExplorerActions.match(action, {
             SET_PAGE: handleAction(() => {
-                api.dispatch(actions.REQUEST_ITEMS());
+                api.dispatch(actions.REQUEST_ITEMS(false));
             }),
             SET_ROWS_PER_PAGE: handleAction(() => {
-                api.dispatch(actions.REQUEST_ITEMS());
+                api.dispatch(actions.REQUEST_ITEMS(true));
             }),
             SET_FILTERS: handleAction(() => {
                 api.dispatch(actions.RESET_PAGINATION());
-                api.dispatch(actions.REQUEST_ITEMS());
+                api.dispatch(actions.REQUEST_ITEMS(true));
             }),
             TOGGLE_SORT: handleAction(() => {
-                api.dispatch(actions.REQUEST_ITEMS());
+                api.dispatch(actions.REQUEST_ITEMS(true));
             }),
             SET_EXPLORER_SEARCH_VALUE: handleAction(() => {
                 api.dispatch(actions.RESET_PAGINATION());
-                api.dispatch(actions.REQUEST_ITEMS());
+                api.dispatch(actions.REQUEST_ITEMS(true));
             }),
-            REQUEST_ITEMS: handleAction(() => {
-                service.requestItems(api);
+            REQUEST_ITEMS: handleAction(({ criteriaChanged }) => {
+                service.requestItems(api, criteriaChanged);
             }),
             default: () => next(action)
         });
index 613bf278edd81d67ad73cc84402ab991aa8353a7..0fa329054435609745630311ffc95201716e229b 100644 (file)
@@ -2,12 +2,18 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { DataColumn, toggleSortDirection, resetSortDirection, SortDirection } from "~/components/data-table/data-column";
-import { dataExplorerActions, DataExplorerAction } from "./data-explorer-action";
-import { DataColumns } from "~/components/data-table/data-table";
+import {
+    DataColumn,
+    resetSortDirection,
+    SortDirection,
+    toggleSortDirection
+} from "~/components/data-table/data-column";
+import { DataExplorerAction, dataExplorerActions } from "./data-explorer-action";
+import { DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
 import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree";
 
 export interface DataExplorer {
+    fetchMode: DataTableFetchMode;
     columns: DataColumns<any>;
     items: any[];
     itemsAvailable: number;
@@ -19,6 +25,7 @@ export interface DataExplorer {
 }
 
 export const initialDataExplorer: DataExplorer = {
+    fetchMode: DataTableFetchMode.PAGINATED,
     columns: [],
     items: [],
     itemsAvailable: 0,
@@ -32,9 +39,15 @@ export type DataExplorerState = Record<string, DataExplorer>;
 
 export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
     dataExplorerActions.match(action, {
+        CLEAR: ({ id }) =>
+            update(state, id, explorer => ({ ...explorer, page: 0, itemsAvailable: 0, items: [] })),
+
         RESET_PAGINATION: ({ id }) =>
             update(state, id, explorer => ({ ...explorer, page: 0 })),
 
+        SET_FETCH_MODE: ({ id, fetchMode }) =>
+            update(state, id, explorer => ({ ...explorer, fetchMode })),
+
         SET_COLUMNS: ({ id, columns }) =>
             update(state, id, setColumns(columns)),
 
@@ -44,6 +57,15 @@ export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataE
         SET_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
             update(state, id, explorer => ({ ...explorer, items, itemsAvailable, page, rowsPerPage })),
 
+        APPEND_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
+            update(state, id, explorer => ({
+                ...explorer,
+                items: state[id].items.concat(items),
+                itemsAvailable: state[id].itemsAvailable + itemsAvailable,
+                page,
+                rowsPerPage
+            })),
+
         SET_PAGE: ({ id, page }) =>
             update(state, id, explorer => ({ ...explorer, page })),
 
index e13c84f61f45ac63514ca5f528a15d0f27974076..52ea0e785ebb1c15b32ff404fdf19dcb3aaa9f76 100644 (file)
@@ -18,6 +18,7 @@ export const SLIDE_TIMEOUT = 500;
 
 export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
+    OPEN_DETAILS_PANEL: ofType<string>(),
     LOAD_DETAILS_PANEL: ofType<string>()
 });
 
@@ -28,6 +29,8 @@ export const PROJECT_PROPERTIES_DIALOG_NAME = 'projectPropertiesDialogName';
 
 export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
 
+export const openDetailsPanel = (uuid: string) => detailsPanelActions.OPEN_DETAILS_PANEL(uuid);
+
 export const openProjectPropertiesDialog = () =>
     (dispatch: Dispatch) => {
         dispatch<any>(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } }));
index 091b2fa2cf1adcfa55f19454fdae2f37d9fef7d9..38c0edd506d148c8442a98ad20e49bdfe8494d4d 100644 (file)
@@ -18,5 +18,6 @@ export const detailsPanelReducer = (state: DetailsPanelState = initialState, act
     detailsPanelActions.match(action, {
         default: () => state,
         LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
-        TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
+        OPEN_DETAILS_PANEL: resourceUuid => ({ resourceUuid, isOpened: true }),
+        TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened }),
     });
index 78777be10941a0e34d8b46a186fed8a8f6bfbd4f..a6abf44b7142a70e2552f945462fcafb3eb223b7 100644 (file)
@@ -52,6 +52,14 @@ export const getInitialResourceTypeFilters = pipe(
     initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
 );
 
+export const getTrashPanelTypeFilters = pipe(
+    (): DataTableFilters => createTree<DataTableFilterItem>(),
+    initFilter(ObjectTypeFilter.PROJECT),
+    initFilter(ObjectTypeFilter.COLLECTION),
+    initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
+    initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
+    initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
+);
 
 const createFiltersBuilder = (filters: DataTableFilters) =>
     ({ fb: new FilterBuilder(), selectedFilters: getSelectedNodes(filters) });
index 0453236a3f797e97e39c2c2027312b5124e68796..1de2feff8b9f8d42111090f132f7dfba4a624a0c 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { Resource, extractUuidKind } from '~/models/resource';
+import { extractUuidKind, Resource } from '~/models/resource';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
index 0cbd9cd6823abd18e284e95c9ecc1177b2febb69..f7649860e478b55a825ad180a84fa99b0c9b98fd 100644 (file)
@@ -14,7 +14,7 @@ import { WorkflowInputsData } from '~/models/workflow';
 import { createWorkflowMounts } from '~/models/process';
 import { ContainerRequestState } from '~/models/container-request';
 import { navigateToProcess } from '../navigation/navigation-action';
-import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from '~/views/run-process-panel/run-process-advanced-form';
+import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM, VCPUS_FIELD, RAM_FIELD, RUNTIME_FIELD, OUTPUT_FIELD, API_FIELD } from '~/views/run-process-panel/run-process-advanced-form';
 import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
@@ -79,10 +79,12 @@ export const setWorkflow = (workflow: WorkflowResource, isWorkflowChanged = true
             dispatch(runProcessPanelActions.SET_STEP_CHANGED(false));
             dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
             dispatch<any>(loadPresets(workflow.uuid));
+            dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, DEFAULT_ADVANCED_FORM_VALUES));
         }
         if (!isWorkflowChanged) {
             dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
             dispatch<any>(loadPresets(workflow.uuid));
+            dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, DEFAULT_ADVANCED_FORM_VALUES));
         }
     };
 
@@ -115,7 +117,7 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
     const state = getState();
     const basicForm = getFormValues(RUN_PROCESS_BASIC_FORM)(state) as RunProcessBasicFormData;
     const inputsForm = getFormValues(RUN_PROCESS_INPUTS_FORM)(state) as WorkflowInputsData;
-    const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData;
+    const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || DEFAULT_ADVANCED_FORM_VALUES;
     const userUuid = getState().auth.user!.uuid;
     const router = getState();
     const properties = getState().properties;
@@ -129,28 +131,36 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
             mounts: createWorkflowMounts(selectedWorkflow, normalizeInputKeys(inputsForm)),
             runtimeConstraints: {
                 API: true,
-                vcpus: 1,
-                ram: 1073741824,
+                vcpus: advancedForm[VCPUS_FIELD],
+                ram: advancedForm[RAM_FIELD],
+                api: advancedForm[API_FIELD],
+            },
+            schedulingParameters: {
+                maxRunTime: advancedForm[RUNTIME_FIELD]
             },
             containerImage: 'arvados/jobs',
             cwd: '/var/spool/cwl',
             command: [
                 'arvados-cwl-runner',
-                '--local',
                 '--api=containers',
-                `--project-uuid=${processOwnerUuid}`,
                 '/var/lib/cwl/workflow.json#main',
                 '/var/lib/cwl/cwl.input.json'
             ],
             outputPath: '/var/spool/cwl',
             priority: 1,
-            outputName: advancedForm && advancedForm.output ? advancedForm.output : undefined,
+            outputName: advancedForm[OUTPUT_FIELD] ? advancedForm[OUTPUT_FIELD] : undefined,
         };
         const newProcess = await services.containerRequestService.create(newProcessData);
         dispatch(navigateToProcess(newProcess.uuid));
     }
 };
 
+export const DEFAULT_ADVANCED_FORM_VALUES: Partial<RunProcessAdvancedFormData> = {
+    [VCPUS_FIELD]: 1,
+    [RAM_FIELD]: 1073741824,
+    [API_FIELD]: true,
+};
+
 const normalizeInputKeys = (inputs: WorkflowInputsData): WorkflowInputsData =>
     Object.keys(inputs).reduce((normalizedInputs, key) => ({
         ...normalizedInputs,
index aa6e4759e0f6d7d4f015624c08c2945a4bf9a307..ea290b4d7def5fd3bc35941f39725c1bd2341ec2 100644 (file)
@@ -4,7 +4,6 @@
 
 import { getAdvancedDataFromQuery, getQueryFromAdvancedData, parseSearchQuery } from "~/store/search-bar/search-bar-actions";
 import { ResourceKind } from "~/models/resource";
-import { ClusterObjectType } from "~/models/search-bar";
 
 describe('search-bar-actions', () => {
     describe('parseSearchQuery', () => {
@@ -95,11 +94,11 @@ describe('search-bar-actions', () => {
         });
 
         it('should correctly build advanced data record from query #2', () => {
-            const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:filesize:101mb is:trashed type:arvados#collection cluster:indianapolis');
+            const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:filesize:101mb is:trashed type:arvados#collection cluster:c97qx');
             expect(r).toEqual({
                 searchValue: 'document pdf',
                 type: ResourceKind.COLLECTION,
-                cluster: ClusterObjectType.INDIANAPOLIS,
+                cluster: 'c97qx',
                 projectUuid: undefined,
                 inTrash: true,
                 dateFrom: '2017-08-01',
@@ -119,7 +118,7 @@ describe('search-bar-actions', () => {
             const q = getQueryFromAdvancedData({
                 searchValue: 'document pdf',
                 type: ResourceKind.COLLECTION,
-                cluster: ClusterObjectType.INDIANAPOLIS,
+                cluster: 'c97qx',
                 projectUuid: undefined,
                 inTrash: true,
                 dateFrom: '2017-08-01',
@@ -131,7 +130,7 @@ describe('search-bar-actions', () => {
                 saveQuery: false,
                 queryName: ''
             });
-            expect(q).toBe('document pdf type:arvados#collection cluster:indianapolis is:trashed from:2017-08-01 has:filesize:101mb');
+            expect(q).toBe('document pdf type:arvados#collection cluster:c97qx is:trashed from:2017-08-01 has:filesize:101mb');
         });
     });
 });
index 199ec3f95788c9f131e373862e52c07ddb9703bd..c81cba04266ad6252f7bd308abbb7f70bc090402 100644 (file)
@@ -15,11 +15,14 @@ import { GroupClass } from '~/models/group';
 import { SearchView } from '~/store/search-bar/search-bar-reducer';
 import { navigateTo, navigateToSearchResults } from '~/store/navigation/navigation-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { ClusterObjectType, PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar';
+import { PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar';
 import { debounce } from 'debounce';
 import * as _ from "lodash";
 import { getModifiedKeysValues } from "~/common/objects";
 import { activateSearchBarProject } from "~/store/search-bar/search-bar-tree-actions";
+import { Session } from "~/models/session";
+import { searchResultsPanelActions } from "~/store/search-results-panel/search-results-panel-actions";
+import { ListResults } from "~/services/common-service/common-service";
 
 export const searchBarActions = unionize({
     SET_CURRENT_VIEW: ofType<string>(),
@@ -189,6 +192,7 @@ export const submitData = (event: React.FormEvent<HTMLFormElement>) =>
         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
         dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
+        dispatch(searchResultsPanelActions.CLEAR());
         dispatch(navigateToSearchResults);
     };
 
@@ -205,12 +209,19 @@ const searchGroups = (searchValue: string, limit: number) =>
         const currentView = getState().searchBar.currentView;
 
         if (searchValue || currentView === SearchView.ADVANCED) {
-            const filters = getFilters('name', searchValue);
-            const { items } = await services.groupsService.contents('', {
-                filters,
-                limit,
-                recursive: true
-            });
+            const sq = parseSearchQuery(searchValue);
+            const clusterId = getSearchQueryFirstProp(sq, 'cluster');
+            const sessions = getSearchSessions(clusterId, getState().auth.sessions);
+            const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session => {
+                const filters = getFilters('name', searchValue, sq);
+                return services.groupsService.contents('', {
+                    filters,
+                    limit,
+                    recursive: true
+                }, session);
+            }));
+
+            const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
             dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
         }
     };
@@ -288,7 +299,7 @@ export const getQueryFromAdvancedData = (data: SearchBarAdvanceFormData, prevDat
     return value;
 };
 
-export interface ParseSearchQuery {
+export class ParseSearchQuery {
     hasKeywords: boolean;
     values: string[];
     properties: {
@@ -351,9 +362,9 @@ export const parseSearchQuery: (query: string) => ParseSearchQuery = (searchValu
     return { hasKeywords: keywordsCnt > 0, values, properties };
 };
 
-const getFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0];
-const getPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value);
-const getProperties = (sq: ParseSearchQuery): PropertyValue[] => {
+export const getSearchQueryFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0];
+export const getSearchQueryPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value);
+export const getSearchQueryProperties = (sq: ParseSearchQuery): PropertyValue[] => {
     if (sq.properties.has) {
         return sq.properties.has.map((value: string) => {
             const v = value.split(':');
@@ -371,23 +382,26 @@ export const getAdvancedDataFromQuery = (query: string): SearchBarAdvanceFormDat
 
     return {
         searchValue: sq.values.join(' '),
-        type: getFirstProp(sq, 'type') as ResourceKind,
-        cluster: getFirstProp(sq, 'cluster') as ClusterObjectType,
-        projectUuid: getFirstProp(sq, 'project'),
-        inTrash: getPropValue(sq, 'is', 'trashed') !== undefined,
-        dateFrom: getFirstProp(sq, 'from'),
-        dateTo: getFirstProp(sq, 'to'),
-        properties: getProperties(sq),
+        type: getSearchQueryFirstProp(sq, 'type') as ResourceKind,
+        cluster: getSearchQueryFirstProp(sq, 'cluster'),
+        projectUuid: getSearchQueryFirstProp(sq, 'project'),
+        inTrash: getSearchQueryPropValue(sq, 'is', 'trashed') !== undefined,
+        dateFrom: getSearchQueryFirstProp(sq, 'from'),
+        dateTo: getSearchQueryFirstProp(sq, 'to'),
+        properties: getSearchQueryProperties(sq),
         saveQuery: false,
         queryName: ''
     };
 };
 
-export const getFilters = (filterName: string, searchValue: string): string => {
+export const getSearchSessions = (clusterId: string | undefined, sessions: Session[]): Session[] => {
+    return sessions.filter(s => s.loggedIn && (!clusterId || s.clusterId === clusterId));
+};
+
+export const getFilters = (filterName: string, searchValue: string, sq: ParseSearchQuery): string => {
     const filter = new FilterBuilder();
-    const sq = parseSearchQuery(searchValue);
 
-    const resourceKind = getFirstProp(sq, 'type') as ResourceKind;
+    const resourceKind = getSearchQueryFirstProp(sq, 'type') as ResourceKind;
 
     let prefix = '';
     switch (resourceKind) {
@@ -402,11 +416,16 @@ export const getFilters = (filterName: string, searchValue: string): string => {
             break;
     }
 
+    const isTrashed = getSearchQueryPropValue(sq, 'is', 'trashed');
+
     if (!sq.hasKeywords) {
         filter
             .addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION)
-            .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS)
             .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT);
+
+        if (isTrashed) {
+            filter.addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS);
+        }
     } else {
         if (prefix) {
             sq.values.forEach(v =>
@@ -416,33 +435,38 @@ export const getFilters = (filterName: string, searchValue: string): string => {
             sq.values.forEach(v => {
                 filter
                     .addILike(filterName, v, GroupContentsResourcePrefix.COLLECTION)
-                    .addILike(filterName, v, GroupContentsResourcePrefix.PROCESS)
                     .addILike(filterName, v, GroupContentsResourcePrefix.PROJECT);
+
+                if (isTrashed) {
+                    filter.addILike(filterName, v, GroupContentsResourcePrefix.PROCESS);
+                }
             });
         }
 
-        if (getPropValue(sq, 'is', 'trashed')) {
+        if (isTrashed) {
             filter.addEqual("is_trashed", true);
         }
 
-        const projectUuid = getFirstProp(sq, 'project');
+        const projectUuid = getSearchQueryFirstProp(sq, 'project');
         if (projectUuid) {
             filter.addEqual('uuid', projectUuid, prefix);
         }
 
-        const dateFrom = getFirstProp(sq, 'from');
+        const dateFrom = getSearchQueryFirstProp(sq, 'from');
         if (dateFrom) {
             filter.addGte('modified_at', buildDateFilter(dateFrom));
         }
 
-        const dateTo = getFirstProp(sq, 'to');
+        const dateTo = getSearchQueryFirstProp(sq, 'to');
         if (dateTo) {
             filter.addLte('modified_at', buildDateFilter(dateTo));
         }
 
-        const props = getProperties(sq);
+        const props = getSearchQueryProperties(sq);
         props.forEach(p => {
-            // filter.addILike(`properties.${p.key}`, p.value);
+            if (p.value) {
+                filter.addILike(`properties.${p.key}`, p.value);
+            }
             filter.addExists(p.key);
         });
     }
index d8b4d7e76f7e6c6616f6b69b1d7779fd9cbd2238..de3252221ef8bd6ec61280735c6e8f118092870e 100644 (file)
@@ -15,7 +15,12 @@ import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { ListResults } from '~/services/common-service/common-service';
 import { searchResultsPanelActions } from '~/store/search-results-panel/search-results-panel-actions';
-import { getFilters } from '~/store/search-bar/search-bar-actions';
+import {
+    getFilters,
+    getSearchQueryFirstProp,
+    getSearchSessions, ParseSearchQuery,
+    parseSearchQuery
+} from '~/store/search-bar/search-bar-actions';
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
 
 export class SearchResultsMiddlewareService extends DataExplorerMiddlewareService {
@@ -23,24 +28,53 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic
         super(id);
     }
 
-    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean) {
         const state = api.getState();
         const userUuid = state.auth.user!.uuid;
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         const searchValue = state.searchBar.searchValue;
+        const sq = parseSearchQuery(searchValue);
+        const clusterId = getSearchQueryFirstProp(sq, 'cluster');
+        const sessions = getSearchSessions(clusterId, state.auth.sessions);
+
+        if (searchValue.trim() === '') {
+            return;
+        }
+
         try {
-            const response = await this.services.groupsService.contents('', getParams(dataExplorer, searchValue));
-            api.dispatch(updateResources(response.items));
-            api.dispatch(setItems(response));
+            const params = getParams(dataExplorer, searchValue, sq);
+            const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session =>
+                this.services.groupsService.contents('', params, session)
+            ));
+
+            const items = lists
+                .reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
+
+            const itemsAvailable = lists
+                .reduce((itemsAvailable, list) => itemsAvailable + list.itemsAvailable, 0);
+
+            const list: ListResults<GroupContentsResource> = {
+                ...params,
+                kind: '',
+                items,
+                itemsAvailable
+            };
+
+            api.dispatch(updateResources(list.items));
+            api.dispatch(criteriaChanged
+                ? setItems(list)
+                : appendItems(list)
+            );
+
         } catch {
-            api.dispatch(couldNotFetchWorkflows());
+            api.dispatch(couldNotFetchSearchResults());
         }
     }
 }
 
-export const getParams = (dataExplorer: DataExplorer, searchValue: string) => ({
+export const getParams = (dataExplorer: DataExplorer, searchValue: string, sq: ParseSearchQuery) => ({
     ...dataExplorerToListParams(dataExplorer),
-    filters: getFilters('name', searchValue),
+    filters: getFilters('name', searchValue, sq),
     order: getOrder(dataExplorer)
 });
 
@@ -69,8 +103,14 @@ export const setItems = (listResults: ListResults<GroupContentsResource>) =>
         items: listResults.items.map(resource => resource.uuid),
     });
 
-const couldNotFetchWorkflows = () =>
+export const appendItems = (listResults: ListResults<GroupContentsResource>) =>
+    searchResultsPanelActions.APPEND_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchSearchResults = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch workflows.',
+        message: `Could not fetch search results for some sessions.`,
         kind: SnackbarKind.ERROR
     });
index 14a6ba11d7651930b878919fa3f3f5d2bb54205a..f842b0c6efadc59f43b812bea337ee32663ee996 100644 (file)
@@ -48,7 +48,6 @@ import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
 import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
 import { UserMiddlewareService } from '~/store/users/user-panel-middleware-service';
 import { USERS_PANEL_ID } from '~/store/users/users-actions';
-import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer';
 import { GroupsPanelMiddlewareService } from '~/store/groups-panel/groups-panel-middleware-service';
 import { GROUPS_PANEL_ID } from '~/store/groups-panel/groups-panel-actions';
 import { GroupDetailsPanelMiddlewareService } from '~/store/group-details-panel/group-details-panel-middleware-service';
@@ -57,10 +56,12 @@ import { LINK_PANEL_ID } from '~/store/link-panel/link-panel-actions';
 import { LinkMiddlewareService } from '~/store/link-panel/link-panel-middleware-service';
 import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
 import { ComputeNodeMiddlewareService } from '~/store/compute-nodes/compute-nodes-middleware-service';
+import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { ApiClientAuthorizationMiddlewareService } from '~/store/api-client-authorizations/api-client-authorizations-middleware-service';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
-        window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
+        window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true, traceLimit: 25})) ||
     compose;
 
 export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
@@ -97,13 +98,15 @@ export function configureStore(history: History, services: ServiceRepository): R
     const groupDetailsPanelMiddleware = dataExplorerMiddleware(
         new GroupDetailsPanelMiddlewareService(services, GROUP_DETAILS_PANEL_ID)
     );
-
     const linkPanelMiddleware = dataExplorerMiddleware(
         new LinkMiddlewareService(services, LINK_PANEL_ID)
     );
     const computeNodeMiddleware = dataExplorerMiddleware(
         new ComputeNodeMiddlewareService(services, COMPUTE_NODE_PANEL_ID)
     );
+    const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware(
+        new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID)
+    );
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
@@ -118,6 +121,7 @@ export function configureStore(history: History, services: ServiceRepository): R
         groupDetailsPanelMiddleware,
         linkPanelMiddleware,
         computeNodeMiddleware,
+        apiClientAuthorizationMiddlewareService
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
@@ -148,6 +152,5 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     searchBar: searchBarReducer,
     virtualMachines: virtualMachinesReducer,
     repositories: repositoriesReducer,
-    keepServices: keepServicesReducer,
-    apiClientAuthorizations: apiClientAuthorizationsReducer
+    keepServices: keepServicesReducer
 });
index f52421a1d6581581cdf97f9613d848d6b3f74e15..3708f073b9033b60922348b180f14425dd35fc6f 100644 (file)
@@ -41,7 +41,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
 
         const otherFilters = new FilterBuilder()
             .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
-            .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+            // .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
             .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
             .addEqual("is_trashed", true)
             .getFilters();
index 585a3663bcdf5fabf7b34fe295f0d30981f08fd0..066aa80b40267225d12eaf2fe10c4169aad14ddd 100644 (file)
@@ -12,14 +12,16 @@ import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions
 import { UserResource } from "~/models/user";
 import { getResource } from '~/store/resources/resources';
 import { navigateToProject, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
+import { saveApiToken, getUserDetails } from '~/store/auth/auth-action';
 
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
 export const USER_CREATE_FORM_NAME = 'userCreateFormName';
+export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
+export const SETUP_SHELL_ACCOUNT_DIALOG = 'setupShellAccountDialog';
 
 export interface UserCreateFormDialogData {
     email: string;
-    identityUrl: string;
     virtualMachineName: string;
     groupVirtualMachine: string;
 }
@@ -31,6 +33,35 @@ export const openUserAttributes = (uuid: string) =>
         dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
     };
 
+export const openUserManagement = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { resources } = getState();
+        const data = getResource<UserResource>(uuid)(resources);
+        dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data }));
+    };
+
+export const openSetupShellAccount = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { resources } = getState();
+        const user = getResource<UserResource>(uuid)(resources);
+        const virtualMachines = await services.virtualMachineService.list();
+        dispatch(dialogActions.CLOSE_DIALOG({ id: USER_MANAGEMENT_DIALOG }));
+        dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: { user, ...virtualMachines } }));
+    };
+
+export const loginAs = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { resources } = getState();
+        const data = getResource<UserResource>(uuid)(resources);
+        if (data) {
+            services.authService.saveUser(data);
+        }
+        const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
+        dispatch<any>(saveApiToken(`v2/${client.uuid}/${client.apiToken}`));
+        location.reload();
+        dispatch<any>(navigateToRootProject);
+    };
+
 export const openUserCreateDialog = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const userUuid = await services.authService.getUuid();
index 46598bde085a5a282e702dcf0ba410a551fbe441..2bac55af40e998c7e4a41bce9861d902b731877a 100644 (file)
@@ -5,21 +5,37 @@
 import { Dispatch } from 'redux';
 import { RootState } from "~/store/store";
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
-import { openProjectPanel, projectPanelActions, setIsProjectPanelTrashed } from '~/store/project-panel/project-panel-action';
-import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { favoritePanelActions, loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
+import {
+    getProjectPanelCurrentUuid,
+    openProjectPanel,
+    projectPanelActions,
+    setIsProjectPanelTrashed
+} from '~/store/project-panel/project-panel-action';
+import {
+    activateSidePanelTreeItem,
+    initSidePanelTree,
+    loadSidePanelTreeProjects,
+    SidePanelTreeCategory
+} from '~/store/side-panel-tree/side-panel-tree-actions';
 import { loadResource, updateResources } from '~/store/resources/resources-actions';
-import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
 import { projectPanelColumns } from '~/views/project-panel/project-panel';
 import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
 import { matchRootRoute } from '~/routes/routes';
-import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs, setBreadcrumbs, setGroupDetailsBreadcrumbs, setGroupsBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import {
+    setBreadcrumbs,
+    setGroupDetailsBreadcrumbs,
+    setGroupsBreadcrumbs,
+    setProcessBreadcrumbs,
+    setSharedWithMeBreadcrumbs,
+    setSidePanelBreadcrumbs,
+    setTrashBreadcrumbs
+} from '~/store/breadcrumbs/breadcrumbs-actions';
 import { navigateToProject } from '~/store/navigation/navigation-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { ServiceRepository } from '~/services/services';
 import { getResource } from '~/store/resources/resources';
-import { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action';
 import * as projectCreateActions from '~/store/projects/project-create-actions';
 import * as projectMoveActions from '~/store/projects/project-move-actions';
 import * as projectUpdateActions from '~/store/projects/project-update-actions';
@@ -35,8 +51,10 @@ import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
 import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { initProcessLogsPanel } from '~/store/process-logs-panel/process-logs-panel-actions';
 import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
-import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
-import { loadSharedWithMePanel } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import {
+    loadSharedWithMePanel,
+    sharedWithMePanelActions
+} from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
 import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
@@ -45,31 +63,35 @@ import { loadSiteManagerPanel } from '~/store/auth/auth-action-session';
 import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
 import { getProgressIndicator } from '~/store/progress-indicator/progress-indicator-reducer';
-import { ResourceKind, extractUuidKind } from '~/models/resource';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
 import { FilterBuilder } from '~/services/api/filter-builder';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { unionize, ofType, UnionOf, MatchCases } from '~/common/unionize';
+import { MatchCases, ofType, unionize, UnionOf } from '~/common/unionize';
 import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
 import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
 import { CollectionResource } from "~/models/collection";
-import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
+import {
+    loadSearchResultsPanel,
+    searchResultsPanelActions
+} from '~/store/search-results-panel/search-results-panel-actions';
 import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
 import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
 import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
 import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
 import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
-import { loadLinkPanel, linkPanelActions } from '~/store/link-panel/link-panel-actions';
-import { loadComputeNodesPanel, computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+import { linkPanelActions, loadLinkPanel } from '~/store/link-panel/link-panel-actions';
+import { computeNodesActions, loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
 import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
 import { userPanelColumns } from '~/views/user-panel/user-panel';
 import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
-import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { apiClientAuthorizationPanelColumns } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
 import * as groupPanelActions from '~/store/groups-panel/groups-panel-actions';
 import { groupsPanelColumns } from '~/views/groups-panel/groups-panel';
 import * as groupDetailsPanelActions from '~/store/group-details-panel/group-details-panel-actions';
 import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-details-panel';
+import { DataTableFetchMode } from "~/components/data-table/data-table";
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -102,12 +124,15 @@ export const loadWorkbench = () =>
                 dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
                 dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                 dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
+                dispatch(searchResultsPanelActions.SET_FETCH_MODE({ fetchMode: DataTableFetchMode.INFINITE }));
                 dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
                 dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
                 dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
                 dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({columns: groupDetailsPanelColumns}));
                 dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
                 dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
+                dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
diff --git a/src/validators/min.tsx b/src/validators/min.tsx
new file mode 100644 (file)
index 0000000..e326a70
--- /dev/null
@@ -0,0 +1,12 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { isNumber } from 'lodash';
+
+export const ERROR_MESSAGE = (minValue: number) => `Minimum value is ${minValue}`;
+
+export const min =
+    (minValue: number, errorMessage = ERROR_MESSAGE) =>
+        (value: any) =>
+            isNumber(value) && value >= minValue ? undefined : errorMessage(minValue);
diff --git a/src/validators/optional.tsx b/src/validators/optional.tsx
new file mode 100644 (file)
index 0000000..da3a825
--- /dev/null
@@ -0,0 +1,7 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const optional = (validator: (value: any) => string | undefined) =>
+    (value: any) =>
+        value === undefined || value === null || value === ''  ? undefined : validator(value);
\ No newline at end of file
index 8bce416d2ac8b9eb24ca3fd3246ae14f403b0347..7433340943c34756869f53f16f7633f4c7ebcc5e 100644 (file)
@@ -87,7 +87,7 @@ export const AdvancedTabDialog = compose(
                     {value === 4 && dialogContent(curlHeader, curlExample, classes)}
                 </DialogContent>
                 <DialogActions>
-                    <Button variant='flat' color='primary' onClick={closeDialog}>
+                    <Button variant='text' color='primary' onClick={closeDialog}>
                         Close
                     </Button>
                 </DialogActions>
index e7defd64187259fe47a85524bf0133170a8beca3..57ad97bbf4950b06f64c94ffd7caa4892ee30dfe 100644 (file)
@@ -68,7 +68,7 @@ export const AttributesApiClientAuthorizationDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index d2802fadb42357c962e34a012274cf71cabfb066..7dc62552f9111ea3bf706cf7371d7f016d3bf114 100644 (file)
@@ -47,7 +47,7 @@ export const HelpApiClientAuthorizationDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
index d912ac1302c9b066e059c9a31dddc1e0602e4a5e..7836e1f794d2436e7f49a5742b9a4071f3920fc1 100644 (file)
@@ -23,6 +23,7 @@ import { CollectionFileType, createCollectionDirectory } from "~/models/collecti
 import { openContextMenu, openCollectionFilesContextMenu } from '~/store/context-menu/context-menu-actions';
 import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
 import { ResourceKind } from "~/models/resource";
+import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 const memoizedMapStateToProps = () => {
     let prevState: CollectionPanelFilesState;
@@ -40,7 +41,7 @@ const memoizedMapStateToProps = () => {
     };
 };
 
-const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
     onUploadDataClick: () => {
         dispatch<any>(openUploadCollectionFilesDialog());
     },
@@ -56,6 +57,9 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps,
     onOptionsMenuOpen: (event) => {
         dispatch<any>(openCollectionFilesContextMenu(event));
     },
+    onFileClick: (id) => {
+        dispatch(openDetailsPanel(id));
+    },
 });
 
 
index 3959909c29963e24f9af7652a17e83c3ccdbb285..41ca63950f977cab61e9499dcb17fe6e648b7b3f 100644 (file)
@@ -51,7 +51,7 @@ export const AttributesComputeNodeDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index 7b0884e668b02ef9f3c5f30bf4c2b2eab3607873..d2b97d15525755f6d66260ff6b78bdf7f9d3400e 100644 (file)
@@ -5,7 +5,7 @@
 import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
 import { AdvancedIcon, ProjectIcon, AttributesIcon, UserPanelIcon } from "~/components/icon/icon";
 import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects } from "~/store/users/users-actions";
+import { openUserAttributes, openUserProjects, openUserManagement } from "~/store/users/users-actions";
 
 export const userActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -25,11 +25,10 @@ export const userActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
     }
-},
-{
+}, {
     name: "Manage",
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
-        dispatch<any>(openAdvancedTabDialog(uuid));
+        dispatch<any>(openUserManagement(uuid));
     }
 }]];
index 934be54d37f15336d5e6e5b5a89a66002bdf4b34..bc0071aff9397055706648aa582ffc7b24e991f3 100644 (file)
@@ -47,7 +47,7 @@ export const CurrentTokenDialog =
                 maxWidth='md'>
                 <DialogTitle>Current Token</DialogTitle>
                 <DialogContent>
-                    <Typography variant='body1' paragraph={true}>
+                    <Typography  paragraph={true}>
                         The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
                                 <Typography component='p'>
                             For more information see
@@ -56,11 +56,11 @@ export const CurrentTokenDialog =
                                     </a>
                         </Typography>
                     </Typography>
-                    <Typography variant='body1' paragraph={true}>
+                    <Typography  paragraph={true}>
                         Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
                             </Typography>
                     <DefaultCodeSnippet lines={[getSnippet(data)]} />
-                    <Typography variant='body1'>
+                    <Typography >
                         Arvados
                                 <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
                         do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
index 8cddf3ba1a5eea67880519a292a46d5146c58e5f..8c95355f6265990773ef20a58965dd1050063676 100644 (file)
@@ -57,6 +57,10 @@ const mapDispatchToProps = () => {
             dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
         },
 
+        onLoadMore: (page: number) => {
+            dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+        },
+
         onRowClick,
 
         onRowDoubleClick,
index bb3f4e10ed1ab5b2b7fe5c305befa9bf920735c5..be0fc793a3cb8716d879f1e8400c5acfacdd3e78 100644 (file)
@@ -81,7 +81,7 @@ const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ow
         </Grid>
     </Grid>;
 
-export const RosurceWorkflowName = connect(
+export const ResourceWorkflowName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
         return resource || { name: '', uuid: '', kind: '', ownerUuid: '' };
@@ -191,33 +191,75 @@ export const ResourceUsername = connect(
         return resource || { username: '' };
     })(renderUsername);
 
-// Compute Node Resources
-const renderNodeDate = (date: string) =>
+// Common methods
+const renderCommonData = (data: string) =>
+    <Typography noWrap>{data}</Typography>;
+
+const renderCommonDate = (date: string) =>
     <Typography noWrap>{formatDate(date)}</Typography>;
 
-const renderNodeData = (data: string) => {
-    return <Typography noWrap>{data}</Typography>;
-};
+export const CommonUuid = withResourceData('uuid', renderCommonData);
+
+// Api Client Authorizations
+export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
+
+export const TokenApiToken = withResourceData('apiToken', renderCommonData);
+
+export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
+
+export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
+
+export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
+
+export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
 
+export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
+
+export const TokenScopes = withResourceData('scopes', renderCommonData);
+
+export const TokenUserId = withResourceData('userId', renderCommonData);
+
+// Compute Node Resources
 const renderNodeInfo = (data: string) => {
     return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
 };
 
-export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
+const clusterColors = [
+    ['#f44336', '#fff'],
+    ['#2196f3', '#fff'],
+    ['#009688', '#fff'],
+    ['#cddc39', '#fff'],
+    ['#ff9800', '#fff']
+];
+
+export const ResourceCluster = (props: { uuid: string }) => {
+    const CLUSTER_ID_LENGTH = 5;
+    const pos = props.uuid.indexOf('-');
+    const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
+    const ci = pos >= CLUSTER_ID_LENGTH ? (props.uuid.charCodeAt(0) + props.uuid.charCodeAt(1)) % clusterColors.length : 0;
+    return <Typography>
+        <div style={{
+            backgroundColor: clusterColors[ci][0],
+            color: clusterColors[ci][1],
+            padding: "2px 7px",
+            borderRadius: 3
+        }}>{clusterId}</div>
+    </Typography>;
+};
 
-export const ComputeNodeUuid = withResourceData('uuid', renderNodeData);
+export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
 
-export const ComputeNodeDomain = withResourceData('domain', renderNodeData);
+export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
 
-export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderNodeDate);
+export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
 
-export const ComputeNodeHostname = withResourceData('hostname', renderNodeData);
+export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
 
-export const ComputeNodeIpAddress = withResourceData('ipAddress', renderNodeData);
+export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
 
-export const ComputeNodeJobUuid = withResourceData('jobUuid', renderNodeData);
+export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
 
-export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderNodeDate);
+export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
 
 // Links Resources
 const renderLinkName = (item: { name: string }) =>
index fe434b6c731aef10539945b662cb36bec2c8b9dd..2a30ae4783d75d426d29bc82574cd19a086058b3 100644 (file)
@@ -24,6 +24,8 @@ import { getResource } from '~/store/resources/resources';
 import { ResourceData } from "~/store/resources-data/resources-data-reducer";
 import { getResourceData } from "~/store/resources-data/resources-data";
 import { toggleDetailsPanel, SLIDE_TIMEOUT } from '~/store/details-panel/details-panel-action';
+import { FileDetails } from '~/views-components/details-panel/file-details';
+import { getNode } from '~/models/tree';
 
 type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
@@ -58,26 +60,32 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
-const getItem = (resource: DetailsResource, resourceData?: ResourceData): DetailsData => {
-    const res = resource || { kind: undefined, name: 'Projects' };
-    switch (res.kind) {
-        case ResourceKind.PROJECT:
-            return new ProjectDetails(res);
-        case ResourceKind.COLLECTION:
-            return new CollectionDetails(res, resourceData);
-        case ResourceKind.PROCESS:
-            return new ProcessDetails(res);
-        default:
-            return new EmptyDetails(res as EmptyResource);
+const EMPTY_RESOURCE: EmptyResource = { kind: undefined, name: 'Projects' };
+
+const getItem = (res: DetailsResource, resourceData?: ResourceData): DetailsData => {
+    if ('kind' in res) {
+        switch (res.kind) {
+            case ResourceKind.PROJECT:
+                return new ProjectDetails(res);
+            case ResourceKind.COLLECTION:
+                return new CollectionDetails(res, resourceData);
+            case ResourceKind.PROCESS:
+                return new ProcessDetails(res);
+            default:
+                return new EmptyDetails(res);
+        }
+    } else {
+        return new FileDetails(res);
     }
 };
 
-const mapStateToProps = ({ detailsPanel, resources, resourcesData }: RootState) => {
-    const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPanelFiles }: RootState) => {
+    const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
+    const file = getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
     const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
     return {
         isOpened: detailsPanel.isOpened,
-        item: getItem(resource, resourceData)
+        item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData)
     };
 };
 
@@ -144,7 +152,7 @@ export const DetailsPanel = withStyles(styles)(
                         </Grid>
                         <Grid item xs={8}>
                             <Tooltip title={item.getTitle()}>
-                                <Typography variant="title" noWrap>
+                                <Typography variant='h6' noWrap>
                                     {item.getTitle()}
                                 </Typography>
                             </Tooltip>
diff --git a/src/views-components/details-panel/file-details.tsx b/src/views-components/details-panel/file-details.tsx
new file mode 100644 (file)
index 0000000..db7c9cf
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DetailsData } from "./details-data";
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
+import { getIcon } from '~/components/file-tree/file-tree-item';
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
+import { formatFileSize } from '~/common/formatters';
+import { FileThumbnail } from '~/components/file-tree/file-thumbnail';
+import isImage from 'is-image';
+
+export class FileDetails extends DetailsData<CollectionFile | CollectionDirectory> {
+
+    getIcon(className?: string) {
+        const Icon = getIcon(this.item.type);
+        return <Icon className={className} />;
+    }
+
+    getDetails() {
+        const { item } = this;
+        return item.type === CollectionFileType.FILE
+            ? <>
+                <DetailsAttribute label='Size' value={formatFileSize(item.size)} />
+                {
+                    isImage(item.url) && <>
+                        <DetailsAttribute label='Preview' />
+                        <FileThumbnail file={item} />
+                    </>
+                }
+            </>
+            : <div />;
+    }
+}
index 14365af7d6c69f1d8dddc91fc486b6d807969326..06db587307a3119e06ae38d8cdb7c6211b18dd69 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { InjectedFormProps } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { UserEmailField, UserIdentityUrlField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
+import { UserEmailField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
 
 export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
 
@@ -20,7 +20,6 @@ export const UserRepositoryCreate = (props: DialogUserProps) =>
 
 const UserAddFields = (props: DialogUserProps) => <span>
     <UserEmailField />
-    <UserIdentityUrlField />
     <UserVirtualMachineField data={props.data}/>
     <UserGroupsVirtualMachineField />
 </span>;
diff --git a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
new file mode 100644 (file)
index 0000000..c53f53c
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import * as React from 'react';
+import { compose } from "redux";
+import { reduxForm, InjectedFormProps, Field } from 'redux-form';
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { TextField } from '~/components/text-field/text-field';
+import { VirtualMachinesResource } from '~/models/virtual-machines';
+import { USER_LENGTH_VALIDATION } from '~/validators/validators';
+import { InputLabel } from '@material-ui/core';
+import { NativeSelectField } from '~/components/select-field/select-field';
+import { SETUP_SHELL_ACCOUNT_DIALOG, createUser } from '~/store/users/users-actions';
+import { UserResource } from '~/models/user';
+
+interface SetupShellAccountFormDialogData {
+    email: string;
+    virtualMachineName: string;
+    groupVirtualMachine: string;
+}
+
+export const SetupShellAccountDialog = compose(
+    withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
+    reduxForm<SetupShellAccountFormDialogData>({
+        form: SETUP_SHELL_ACCOUNT_DIALOG,
+        onSubmit: (data, dispatch) => {
+            dispatch(createUser(data));
+        }
+    })
+)(
+    (props: SetupShellAccountDialogComponentProps) =>
+        <FormDialog
+            dialogTitle='Setup shell account'
+            formFields={SetupShellAccountFormFields}
+            submitLabel='Submit'
+            {...props}
+        />
+);
+
+interface UserProps {
+    data: {
+        user: UserResource;
+    };
+}
+
+interface VirtualMachinesProps {
+    data: {
+        items: VirtualMachinesResource[];
+    };
+}
+interface DataProps {
+    user: UserResource;
+    items: VirtualMachinesResource[];
+}
+
+const UserEmailField = ({ data }: UserProps) =>
+    <span>
+        <Field
+            name='email'
+            component={TextField}
+            disabled
+            label={data.user.email} /></span>;
+
+const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
+    <div style={{ marginBottom: '21px' }}>
+        <InputLabel>Virtual Machine</InputLabel>
+        <Field
+            name='virtualMachine'
+            component={NativeSelectField}
+            validate={USER_LENGTH_VALIDATION}
+            items={getVirtualMachinesList(data.items)} />
+    </div>;
+
+const UserGroupsVirtualMachineField = () =>
+    <Field
+        name='groups'
+        component={TextField}
+        validate={USER_LENGTH_VALIDATION}
+        label="Groups for virtual machine (comma separated list)" />;
+
+const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
+    virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
+
+type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
+
+const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
+    <>
+        <UserEmailField data={props.data as DataProps} />
+        <UserVirtualMachineField data={props.data as DataProps} />
+        <UserGroupsVirtualMachineField />
+    </>;
+
+
+
index 85abbe19f3f266769bb70b9f1da0e03d847399f1..8de48ea744a8494ae2575a5ee51358a0c591902f 100644 (file)
@@ -8,7 +8,6 @@ import { TextField, DateTextField } from "~/components/text-field/text-field";
 import { CheckboxField } from '~/components/checkbox-field/checkbox-field';
 import { NativeSelectField } from '~/components/select-field/select-field';
 import { ResourceKind } from '~/models/resource';
-import { ClusterObjectType } from '~/models/search-bar';
 import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
 import { SEARCH_BAR_ADVANCE_FORM_PICKER_ID } from '~/store/search-bar/search-bar-actions';
 import { SearchBarAdvancedPropertiesView } from '~/views-components/search-bar/search-bar-advanced-properties-view';
@@ -18,6 +17,8 @@ import { PropertyKeyInput } from '~/views-components/resource-properties-form/pr
 import { PropertyValueInput, PropertyValueFieldProps } from '~/views-components/resource-properties-form/property-value-field';
 import { VocabularyProp, connectVocabulary } from '~/views-components/resource-properties-form/property-field-common';
 import { compose } from 'redux';
+import { connect } from "react-redux";
+import { RootState } from "~/store/store";
 
 export const SearchBarTypeField = () =>
     <Field
@@ -30,16 +31,25 @@ export const SearchBarTypeField = () =>
             { key: ResourceKind.PROCESS, value: 'Process' }
         ]} />;
 
-export const SearchBarClusterField = () =>
-    <Field
+
+interface SearchBarClusterFieldProps {
+    clusters: { key: string, value: string }[];
+}
+
+export const SearchBarClusterField = connect(
+    (state: RootState) => ({
+        clusters: [{key: '', value: 'Any'}].concat(
+            state.auth.sessions
+                .filter(s => s.loggedIn)
+                .map(s => ({
+                    key: s.clusterId,
+                    value: s.clusterId
+                })))
+    }))((props: SearchBarClusterFieldProps) => <Field
         name='cluster'
         component={NativeSelectField}
-        items={[
-            { key: '', value: 'Any' },
-            { key: ClusterObjectType.INDIANAPOLIS, value: 'Indianapolis' },
-            { key: ClusterObjectType.KAISERAUGST, value: 'Kaiseraugst' },
-            { key: ClusterObjectType.PENZBERG, value: 'Penzberg' }
-        ]} />;
+        items={props.clusters}/>
+    );
 
 export const SearchBarProjectField = () =>
     <Field
index 856344492f72f6fe361a2e0c6858f9f0c347a29b..11d7d80280c699d5bd55c88781db65459d905ab6 100644 (file)
@@ -18,13 +18,6 @@ export const UserEmailField = () =>
         autoFocus={true}
         label="Email" />;
 
-export const UserIdentityUrlField = () =>
-    <Field
-        name='identityUrl'
-        component={TextField}
-        validate={USER_LENGTH_VALIDATION}
-        label="Identity URL Prefix" />;
-
 export const UserVirtualMachineField = ({ data }: any) =>
     <div style={{ marginBottom: '21px' }}>
         <InputLabel>Virtual Machine</InputLabel>
index f6ab8c13a4360b71237f8eff220e05057318a356..c95392a7a10cf706263b2254803e6604a2fc8a7d 100644 (file)
@@ -45,13 +45,13 @@ export const GroupAttributesDialog = compose(
                 maxWidth="sm">
                 <DialogTitle>Attributes</DialogTitle>
                 <DialogContent>
-                    <Typography variant="body2" className={props.classes.spacing}>
+                    <Typography variant='body1' className={props.classes.spacing}>
                         {props.data && attributes(props.data, props.classes)}
                     </Typography>
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
index 7aa8653dc6932fbac0b7d524e91b14a2f6c3ef14..9299e492626dc8a0156991bd6f87896e6a503928 100644 (file)
@@ -45,13 +45,13 @@ export const GroupMemberAttributesDialog = compose(
                 maxWidth="sm">
                 <DialogTitle>Attributes</DialogTitle>
                 <DialogContent>
-                    <Typography variant="body2" className={props.classes.spacing}>
+                    <Typography variant='body1' className={props.classes.spacing}>
                         {props.data && attributes(props.data, props.classes)}
                     </Typography>
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
index 113d191b4735ad9985e5fad65f3eea39582587ae..fb83a380a799ffe53dd18fce2debae29585626a3 100644 (file)
@@ -63,7 +63,7 @@ export const AttributesKeepServiceDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index 8226c62144299e6c72de6cf293f0a365a56f1c62..75f1010b3729cd55e62c9b7f56c6cbccd56d910c 100644 (file)
@@ -63,7 +63,7 @@ export const AttributesLinkDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index 94da69e7c62311d94d4e324462cd7a73d3e02ac5..859c907a6fd88553118038df3c7b763ea0b70873 100644 (file)
@@ -78,7 +78,7 @@ export const HelpMenu = compose(
                         <MenuItem key={link.title}>
                             <a href={link.link} target="_blank" className={classes.link}>
                                 <ImportContactsIcon className={classes.icon} />
-                                <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
+                                <Typography  className={classes.linkTitle}>{link.title}</Typography>
                             </a>
                         </MenuItem>
                     )
index 8a7e9f2000f0af954275b3ac6717ff6479550ce1..ff3bee61973f016bc63af7c9792beec1d3ff484a 100644 (file)
@@ -41,7 +41,7 @@ export const MainAppBar = withStyles(styles)(
             <Toolbar className={props.classes.toolbar}>
                 <Grid container justify="space-between">
                     <Grid container item xs={3} direction="column" justify="center">
-                        <Typography variant="title" color="inherit" noWrap>
+                        <Typography variant='h6' color="inherit" noWrap>
                             <Link to={Routes.ROOT} className={props.classes.link}>
                                 arvados workbench
                             </Link>
index c95a209a9d57be6de7652a44d0e3726aa5af5e71..f6f02a5bfba0cb1e95f049524b977157827f2a46 100644 (file)
@@ -51,7 +51,7 @@ export const ProcessCommandDialog = compose(
                 lines={[props.data.command]} />
             <DialogActions>
                 <Button
-                    variant='flat'
+                    variant='text'
                     color='primary'
                     onClick={props.closeDialog}>
                     Close
index bbbd97f535f7cab080e670d4b970e0461bb01f23..4ba7fd708cb53620f25fd70c62fe1101c5c80925 100644 (file)
@@ -22,7 +22,7 @@ export const ProcessInputDialog = withDialog(PROCESS_INPUT_DIALOG_NAME)(
             </DialogContent>
             <DialogActions>
                 <Button
-                    variant='flat'
+                    variant='text'
                     color='primary'
                     onClick={props.closeDialog}>
                     Close
index d165f9810a0389757188d052d97bd697f1ce04bc..caedd4e6593703970ed47ee047635d924130e496 100644 (file)
@@ -63,7 +63,7 @@ export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToPro
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index 1bf76e49eaa7fafc0bcebf3165ceb82947adf6e8..b9472fa5240ca529c5dfc30fa9eba6fd0c1965d3 100644 (file)
@@ -18,12 +18,12 @@ export const RemoveDialog = withDialog(REMOVE_DIALOG)(
             </DialogContent>
             <DialogActions>
                 <Button
-                    variant='flat'
+                    variant='text'
                     color='primary'
                     onClick={props.closeDialog}>
                     Cancel
                 </Button>
-                <Button variant='raised' color='primary'>
+                <Button variant='contained' color='primary'>
                     Remove
                 </Button>
             </DialogActions>
index 1a00e9770a26f806277f802f419a71443e11da21..6899018cd66785ed81a531623ad5fbf0a97ed585 100644 (file)
@@ -52,7 +52,7 @@ export const RepositoriesSampleGitDialog = compose(
                     <DefaultCodeSnippet
                         className={props.classes.codeSnippet}
                         lines={[snippetText(props.data.uuidPrefix)]} />
-                    <Typography variant="body2" className={props.classes.spacing}>
+                    <Typography variant='body1' className={props.classes.spacing}>
                         See also:
                         <div><a href="https://doc.arvados.org/user/getting_started/ssh-access-unix.html" className={props.classes.link} target="_blank">SSH access</a></div>
                         <div><a href="https://doc.arvados.org/user/tutorials/tutorial-firstscript.html" className={props.classes.link} target="_blank">Writing a Crunch Script</a></div>
@@ -60,7 +60,7 @@ export const RepositoriesSampleGitDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
index 94a0e6ca7fc392f23c2c03b08554e0d079e6e1c9..c9c72026f99d38361ccf90edaffaa20d5239f422 100644 (file)
@@ -45,13 +45,13 @@ export const RepositoryAttributesDialog = compose(
                 maxWidth="sm">
                 <DialogTitle>Attributes</DialogTitle>
                 <DialogContent>
-                    <Typography variant="body2" className={props.classes.spacing}>
+                    <Typography variant='body1' className={props.classes.spacing}>
                         {props.data.repositoryData && attributes(props.data.repositoryData, props.classes)}
                     </Typography>
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
index 86422bafde731b5a36a288bb040934cf6a3fc84e..c8fd6cdcec0193e6332735cd0ffe0eb2349198f5 100644 (file)
@@ -28,7 +28,7 @@ export const RichTextEditorDialog = withDialog(RICH_TEXT_EDITOR_DIALOG_NAME)(
             </DialogContent>
             <DialogActions>
                 <Button
-                    variant='flat'
+                    variant='text'
                     color='primary'
                     onClick={props.closeDialog}>
                     Close
index 7347c664787b19d7c036d905ace99165aff2b25b..2c9bb01d38bfaedd113e1322c961bd80a4f904b3 100644 (file)
@@ -14,7 +14,7 @@ export const SharingDialogContent = (props: { advancedViewOpen: boolean }) =>
         {props.advancedViewOpen &&
             <>
                 <Grid item>
-                    <Typography variant='subheading'>
+                    <Typography variant='subtitle1'>
                         Who can access
                     </Typography>
                     <SharingPublicAccessForm />
index 5e374042b32438cd0ed5045097dd8ca61b1bf66c..e4096ea597cfa8e651fe9e39645aeb8763f44088 100644 (file)
@@ -40,7 +40,7 @@ const PermissionManagementRow = withStyles(permissionManagementRowStyles)(
             <Divider />
             <Grid container alignItems='center' spacing={8} wrap='nowrap' className={classes.root}>
                 <Grid item xs={8}>
-                    <Typography noWrap variant='subheading'>{fields.get(index).email}</Typography>
+                    <Typography noWrap variant='subtitle1'>{fields.get(index).email}</Typography>
                 </Grid>
                 <Grid item xs={4} container wrap='nowrap'>
                     <Field
index 7f3c4b7b4f833bc1b045615c125b115a51774f03..03e9413e8379abcd22771eeeceef6bfe04f242d3 100644 (file)
@@ -22,7 +22,7 @@ const SharingPublicAccessForm = withStyles(sharingPublicAccessStyles)(
             <Divider />
             <Grid container alignItems='center' spacing={8} className={classes.root}>
                 <Grid item xs={8}>
-                    <Typography variant='subheading'>
+                    <Typography variant='subtitle1'>
                         {renderVisibilityInfo(visibility)}
                     </Typography>
                 </Grid>
index 0c164dbd26061e26c81aed12342ab97a1f2120bf..f3b3b11fa46b5e924f15f8b3731ac7e7725011d7 100644 (file)
@@ -59,7 +59,7 @@ export const AttributesSshKeyDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index 11ec159501a41f85928e97c6bfafde2993b6df3d..40ca57133409205e7412855b30389e5a36ccb378 100644 (file)
@@ -45,7 +45,7 @@ export const PublicKeyDialog = compose(
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={closeDialog}>
                         Close
index 66a488156d41a0a53d94adf70726c4283db3772a..11594f5e1d187d39a27072a6e37b42d28cca3a10 100644 (file)
@@ -45,13 +45,13 @@ export const UserAttributesDialog = compose(
                 maxWidth="sm">
                 <DialogTitle>Attributes</DialogTitle>
                 <DialogContent>
-                    <Typography variant="body2" className={props.classes.spacing}>
+                    <Typography variant='body1' className={props.classes.spacing}>
                         {props.data && attributes(props.data, props.classes)}
                     </Typography>
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
diff --git a/src/views-components/user-dialog/manage-dialog.tsx b/src/views-components/user-dialog/manage-dialog.tsx
new file mode 100644 (file)
index 0000000..05e4a3f
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { compose, Dispatch } from "redux";
+import { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "~/store/users/users-actions";
+import { connect } from "react-redux";
+
+type CssRules = 'spacing';
+
+const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
+    spacing: {
+        paddingBottom: theme.spacing.unit * 2,
+        paddingTop: theme.spacing.unit * 2,
+    }
+}));
+
+interface UserManageDataProps {
+    data: any;
+}
+
+interface UserManageActionProps {
+    openSetupShellAccount: (uuid: string) => void;
+    loginAs: (uuid: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
+    loginAs: (uuid: string) => dispatch<any>(loginAs(uuid))
+});
+
+type UserManageProps = UserManageDataProps & UserManageActionProps & WithStyles<CssRules>;
+
+export const UserManageDialog = compose(
+    connect(null, mapDispatchToProps),
+    withDialog(USER_MANAGEMENT_DIALOG),
+    styles)(
+        (props: WithDialogProps<UserManageProps> & UserManageProps) =>
+            <Dialog open={props.open}
+                onClose={props.closeDialog}
+                fullWidth
+                maxWidth="md">
+                {props.data &&
+                    <span>
+                        <DialogTitle>{`Manage - ${props.data.firstName} ${props.data.lastName}`}</DialogTitle>
+                        <DialogContent>
+                            <Typography variant='body1' className={props.classes.spacing}>
+                                As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
+                    </Typography>
+                            <Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.uuid)}>
+                                {`LOG IN AS ${props.data.firstName} ${props.data.lastName}`}
+                            </Button>
+                            <Typography variant='body1' className={props.classes.spacing}>
+                                As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
+                    </Typography>
+                            <Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
+                                {`SETUP SHELL ACCOUNT FOR ${props.data.firstName} ${props.data.lastName}`}
+                            </Button>
+                        </DialogContent></span>}
+
+                <DialogActions>
+                    <Button
+                        variant='text'
+                        color='primary'
+                        onClick={props.closeDialog}>
+                        Close
+                </Button>
+                </DialogActions>
+            </Dialog>
+    );
index 05c1b0a07fbc17479fe2d6e9d645b79bc6693782..33588dc020b05912c9c5049b35e865f068d7d8f2 100644 (file)
@@ -45,13 +45,13 @@ export const VirtualMachineAttributesDialog = compose(
                 maxWidth="sm">
                 <DialogTitle>Attributes</DialogTitle>
                 <DialogContent>
-                    <Typography variant="body2" className={props.classes.spacing}>
+                    <Typography variant='body1' className={props.classes.spacing}>
                         {props.data.virtualMachineData && attributes(props.data.virtualMachineData, props.classes)}
                     </Typography>
                 </DialogContent>
                 <DialogActions>
                     <Button
-                        variant='flat'
+                        variant='text'
                         color='primary'
                         onClick={props.closeDialog}>
                         Close
index 52921b30186f6dc412f9b9b1331e30e357b0d128..4a8cb9d5b81a5fd836463f57ab21e7256d10721c 100644 (file)
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { 
-    StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, 
-    Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton
+import {
+    StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Tooltip, IconButton
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { MoreOptionsIcon, HelpIcon } from '~/components/icon/icon';
-import { ApiClientAuthorization } from '~/models/api-client-authorization';
-import { formatDate } from '~/common/formatters';
+import { HelpIcon, 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';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '../../store/api-client-authorizations/api-client-authorizations-actions';
+import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
+import { ResourcesState } from '~/store/resources/resources';
+import {
+    CommonUuid, TokenApiClientId, TokenApiToken, TokenCreatedByIpAddress, TokenDefaultOwnerUuid, TokenExpiresAt,
+    TokenLastUsedAt, TokenLastUsedByIpAddress, TokenScopes, TokenUserId
+} from '~/views-components/data-explorer/renderers';
 
-type CssRules = 'root' | 'tableRow' | 'helpIconGrid' | 'tableGrid';
+type CssRules = 'card' | 'cardContent' | 'helpIconGrid';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
+    card: {
         width: '100%',
         overflow: 'auto'
     },
+    cardContent: {
+        padding: 0,
+        '&:last-child': {
+            paddingBottom: 0
+        }
+    },
     helpIconGrid: {
         textAlign: 'right'
+    }
+});
+
+
+export enum ApiClientAuthorizationPanelColumnNames {
+    UUID = 'UUID',
+    API_CLIENT_ID = 'API Client ID',
+    API_TOKEN = 'API Token',
+    CREATED_BY_IP_ADDRESS = 'Created by IP address',
+    DEFAULT_OWNER_UUID = 'Default owner',
+    EXPIRES_AT = 'Expires at',
+    LAST_USED_AT = 'Last used at',
+    LAST_USED_BY_IP_ADDRESS = 'Last used by IP address',
+    SCOPES = 'Scopes',
+    USER_ID = 'User ID'
+}
+
+export const apiClientAuthorizationPanelColumns: DataColumns<string> = [
+    {
+        name: ApiClientAuthorizationPanelColumnNames.UUID,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <CommonUuid uuid={uuid} />
     },
-    tableGrid: {
-        marginTop: theme.spacing.unit
+    {
+        name: ApiClientAuthorizationPanelColumnNames.API_CLIENT_ID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenApiClientId uuid={uuid} />
     },
-    tableRow: {
-        '& td, th': {
-            whiteSpace: 'nowrap'
-        }
+    {
+        name: ApiClientAuthorizationPanelColumnNames.API_TOKEN,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenApiToken uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.CREATED_BY_IP_ADDRESS,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenCreatedByIpAddress uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.DEFAULT_OWNER_UUID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenDefaultOwnerUuid uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.EXPIRES_AT,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenExpiresAt uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.LAST_USED_AT,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenLastUsedAt uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.LAST_USED_BY_IP_ADDRESS,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenLastUsedByIpAddress uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.SCOPES,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenScopes uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.USER_ID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenUserId uuid={uuid} />
     }
-});
+];
+
+const DEFAULT_MESSAGE = 'Your api client authorization list is empty.';
 
 export interface ApiClientAuthorizationPanelRootActionProps {
-    openRowOptions: (event: React.MouseEvent<HTMLElement>, keepService: ApiClientAuthorization) => void;
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+    onItemDoubleClick: (item: string) => void;
     openHelpDialog: () => void;
 }
 
 export interface ApiClientAuthorizationPanelRootDataProps {
-    apiClientAuthorizations: ApiClientAuthorization[];
-    hasApiClientAuthorizations: boolean;
+    resources: ResourcesState;
 }
 
-type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps 
+type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps
     & ApiClientAuthorizationPanelRootDataProps & WithStyles<CssRules>;
 
 export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
-    ({ classes, hasApiClientAuthorizations, apiClientAuthorizations, openRowOptions, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
-        <Card className={classes.root}>
-            <CardContent>
-                {hasApiClientAuthorizations && <Grid container direction="row" justify="flex-end">
+    ({ 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}>
@@ -58,47 +155,21 @@ export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
                         </Tooltip>
                     </Grid>
                     <Grid item xs={12}>
-                        <Table>
-                            <TableHead>
-                                <TableRow className={classes.tableRow}>
-                                    <TableCell>UUID</TableCell>
-                                    <TableCell>API Client ID</TableCell>
-                                    <TableCell>API Token</TableCell>
-                                    <TableCell>Created by IP address</TableCell>
-                                    <TableCell>Default owner</TableCell>
-                                    <TableCell>Expires at</TableCell>
-                                    <TableCell>Last used at</TableCell>
-                                    <TableCell>Last used by IP address</TableCell>
-                                    <TableCell>Scopes</TableCell>
-                                    <TableCell>User ID</TableCell>
-                                    <TableCell />
-                                </TableRow>
-                            </TableHead>
-                            <TableBody>
-                                {apiClientAuthorizations.map((apiClientAuthorizatio, index) =>
-                                    <TableRow key={index} className={classes.tableRow}>
-                                        <TableCell>{apiClientAuthorizatio.uuid}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.apiClientId}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.apiToken}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.createdByIpAddress || '(none)'}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.defaultOwnerUuid || '(none)'}</TableCell>
-                                        <TableCell>{formatDate(apiClientAuthorizatio.expiresAt) || '(none)'}</TableCell>
-                                        <TableCell>{formatDate(apiClientAuthorizatio.lastUsedAt) || '(none)'}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.lastUsedByIpAddress || '(none)'}</TableCell>
-                                        <TableCell>{JSON.stringify(apiClientAuthorizatio.scopes)}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.userId}</TableCell>
-                                        <TableCell>
-                                            <Tooltip title="More options" disableFocusListener>
-                                                <IconButton onClick={event => openRowOptions(event, apiClientAuthorizatio)}>
-                                                    <MoreOptionsIcon />
-                                                </IconButton>
-                                            </Tooltip>
-                                        </TableCell>
-                                    </TableRow>)}
-                            </TableBody>
-                        </Table>
+                        <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>}
+                </Grid>
             </CardContent>
         </Card>
 );
\ No newline at end of file
index 75b79abff6270fa2eb9aff5695495f1e6f3a9f3f..b16e507646f0c8c22238efc8ccc773aba3d2254c 100644 (file)
@@ -15,15 +15,16 @@ import { openApiClientAuthorizationsHelpDialog } from '~/store/api-client-author
 
 const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => {
     return {
-        apiClientAuthorizations: state.apiClientAuthorizations,
-        hasApiClientAuthorizations: state.apiClientAuthorizations.length > 0
+        resources: state.resources
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ApiClientAuthorizationPanelRootActionProps => ({
-    openRowOptions: (event, apiClientAuthorization) => {
+    onContextMenu: (event, apiClientAuthorization) => {
         dispatch<any>(openApiClientAuthorizationContextMenu(event, apiClientAuthorization));
     },
+    onItemClick: (resourceUuid: string) => { return; },
+    onItemDoubleClick: uuid => { return; },
     openHelpDialog: () => {
         dispatch<any>(openApiClientAuthorizationsHelpDialog());
     }
index 4124344d3f3311129d1320541352221a11416df1..3557afe574cb8c064488f146f650e5c737e49dce 100644 (file)
@@ -25,6 +25,7 @@ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { formatFileSize } from "~/common/formatters";
 import { getResourceData } from "~/store/resources-data/resources-data";
 import { ResourceData } from "~/store/resources-data/resources-data-reducer";
+import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value' | 'link';
 
@@ -77,13 +78,18 @@ export const CollectionPanel = withStyles(styles)(
         return { item, data };
     })(
         class extends React.Component<CollectionPanelProps> {
+
             render() {
                 const { classes, item, data, dispatch } = this.props;
                 return item
                     ? <>
                         <Card className={classes.card}>
                             <CardHeader
-                                avatar={<CollectionIcon className={classes.iconHeader} />}
+                                avatar={
+                                    <IconButton onClick={this.openCollectionDetails}>
+                                        <CollectionIcon className={classes.iconHeader} />
+                                    </IconButton>
+                                }
                                 action={
                                     <Tooltip title="More options" disableFocusListener>
                                         <IconButton
@@ -94,7 +100,9 @@ export const CollectionPanel = withStyles(styles)(
                                     </Tooltip>
                                 }
                                 title={item && item.name}
-                                subheader={item && item.description} />
+                                titleTypographyProps={this.titleProps}
+                                subheader={item && item.description}
+                                subheaderTypographyProps={this.titleProps} />
                             <CardContent>
                                 <Grid container direction="column">
                                     <Grid item xs={6}>
@@ -113,7 +121,7 @@ export const CollectionPanel = withStyles(styles)(
                                             label='Content size' value={data && formatFileSize(data.fileSize)} />
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                                             label='Owner' value={item && item.ownerUuid} />
-                                        <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request  || item.properties.containerRequest))}>
+                                        <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
                                             <DetailsAttribute classLabel={classes.link} label='Link to process' />
                                         </span>
                                     </Grid>
@@ -173,6 +181,18 @@ export const CollectionPanel = withStyles(styles)(
                     kind: SnackbarKind.SUCCESS
                 }));
             }
+
+            openCollectionDetails = () => {
+                const { item } = this.props;
+                if (item) {
+                    this.props.dispatch(openDetailsPanel(item.uuid));
+                }
+            }
+
+            titleProps = {
+                onClick: this.openCollectionDetails
+            };
+
         }
     )
 );
index feaadb5e5b86a92232354759506c185c3a334cca..6770e61a00faaa4b809415935abf3639e7da417a 100644 (file)
@@ -10,9 +10,9 @@ import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actio
 import { DataColumns } from '~/components/data-table/data-table';
 import { SortDirection } from '~/components/data-table/data-column';
 import { createTree } from '~/models/tree';
-import { 
-    ComputeNodeUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
-    ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress
+import {
+    ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
+    ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress, CommonUuid
 } from '~/views-components/data-explorer/renderers';
 import { ResourcesState } from '~/store/resources/resources';
 
@@ -41,7 +41,7 @@ export const computeNodePanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ComputeNodeUuid uuid={uuid} />
+        render: uuid => <CommonUuid uuid={uuid} />
     },
     {
         name: ComputeNodePanelColumnNames.DOMAIN,
@@ -115,4 +115,4 @@ export const ComputeNodePanelRoot = (props: ComputeNodePanelRootProps) => {
                 icon={ShareMeIcon}
                 messages={[DEFAULT_MESSAGE]} />
         } />;
-};
\ No newline at end of file
+};
index 6a9210acad79f58288f2fc9448903af750c79f10..151311d0c3ef6575d34a235b0e52c57e443dcfff 100644 (file)
@@ -54,20 +54,20 @@ export const LoginPanel = withStyles(styles)(connect()(
     ({ classes, dispatch }: LoginPanelProps) =>
     <Grid container direction="column" item xs alignItems="center" justify="center" className={classes.root}>
         <Grid item className={classes.container}>
-            <Typography variant="title" align="center" className={classes.title}>
+            <Typography variant='h6' align="center" className={classes.title}>
                 Welcome to the Arvados Workbench
             </Typography>
-            <Typography variant="body1" className={classes.content}>
+            <Typography  className={classes.content}>
                 The "Log in" button below will show you a Google sign-in page.
                 After you assure Google that you want to log in here with your Google account, you will be redirected back here to Arvados Workbench.
             </Typography>
-            <Typography variant="body1" className={classes.content}>
+            <Typography  className={classes.content}>
                 If you have never used Arvados Workbench before, logging in for the first time will automatically create a new account.
             </Typography>
-            <Typography variant="body2" className={classNames(classes.content, classes.content__bolder)}>
+            <Typography variant='body1' className={classNames(classes.content, classes.content__bolder)}>
                 IMPORTANT: Please keep in mind to store exploratory data only but not any information used for clinical decision making.
             </Typography>
-            <Typography variant="body1" className={classes.content}>
+            <Typography  className={classes.content}>
                 Arvados Workbench uses your name and email address only for identification, and does not retrieve any other personal information from Google.
             </Typography>
             <Typography component="div" align="right">
index a0b29fb4b41f6764285c08378a8ee1649d3efd20..819a16a410801d2ae6bff4638bcf32c71f5bb34d 100644 (file)
@@ -69,7 +69,7 @@ export const MyAccountPanelRoot = withStyles(styles)(
     ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting }: MyAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-                <Typography variant="title" className={classes.title}>User profile</Typography>
+                <Typography variant='h6' className={classes.title}>User profile</Typography>
                 <form onSubmit={handleSubmit}>
                     <Grid container direction="row" spacing={24}>
                         <Grid item xs={6}>
index d02fc02ce1431c74a2aefdaf4137991d0552ba61..a7f6c4761adcb8422774be19a319583680f4fa8a 100644 (file)
@@ -27,7 +27,8 @@ const theme = createMuiTheme({
         }
     },
     typography: {
-        fontFamily: 'monospace'
+        fontFamily: 'monospace',
+        useNextVariants: true,
     }
 });
 
index 42df2d92d7dc33933500cb8c7e54daf7d5a166a2..9b75d112b737e6ccc86aa5fa2ad85d137fb8c4e5 100644 (file)
@@ -80,7 +80,7 @@ export const ProcessLogMainCard = withStyles(styles)(
                         </Tooltip>}
                     title={
                         <Tooltip title={process.containerRequest.name} placement="bottom-start">
-                            <Typography noWrap variant="title" className={classes.title}>
+                            <Typography noWrap variant='h6' className={classes.title}>
                                 {process.containerRequest.name}
                             </Typography>
                         </Tooltip>}
index 52f13987d9fdf214853ec58e292d413dc6a228c7..1f42db6b396d7a3ea945e9a902a51b92cc128c26 100644 (file)
@@ -103,14 +103,14 @@ export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
                 }
                 title={
                     <Tooltip title={process.containerRequest.name} placement="bottom-start">
-                        <Typography noWrap variant="title" color='inherit'>
+                        <Typography noWrap variant='h6' color='inherit'>
                             {process.containerRequest.name}
                         </Typography>
                     </Tooltip>
                 }
                 subheader={
                     <Tooltip title={getDescription(process)} placement="bottom-start">
-                        <Typography noWrap variant="body2" color='inherit'>
+                        <Typography noWrap variant='body1' color='inherit'>
                             {getDescription(process)}
                         </Typography>
                     </Tooltip>}/>
index 7a1901f13f797644cfd1842a97b3b37b2b6ffe63..7b455b6722f338f5b21d7b59fe9fb3e1989b0c87 100644 (file)
@@ -83,7 +83,7 @@ export const ProcessSubprocessesCard = withStyles(styles, { withTheme: true })(
                 classes={{ content: classes.title, action: classes.action }}
                 action={
                     <div className={classes.rightSideHeader}>
-                        <Typography noWrap variant="body2" className={classes.status}>
+                        <Typography noWrap variant='body1' className={classes.status}>
                             {getProcessStatus(subprocess)}
                         </Typography>
                         <Tooltip title="More options" disableFocusListener>
@@ -91,21 +91,23 @@ export const ProcessSubprocessesCard = withStyles(styles, { withTheme: true })(
                                 className={classes.options}
                                 aria-label="More options"
                                 onClick={onContextMenu}>
-                                <MoreOptionsIcon className={classes.moreOptions}/>
+                                <MoreOptionsIcon className={classes.moreOptions} />
                             </IconButton>
                         </Tooltip>
                     </div>
                 }
                 title={
                     <Tooltip title={subprocess.containerRequest.name}>
-                        <Typography noWrap variant="body2" className={classes.titleHeader}>
+                        <Typography noWrap variant='body1' className={classes.titleHeader}>
                             {subprocess.containerRequest.name}
                         </Typography>
                     </Tooltip>
                 } />
             <CardContent className={classes.content}>
                 <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                    label="Runtime" value={formatTime(getProcessRuntime(subprocess))} />
+                    label="Runtime" value={subprocess.container && subprocess.container.startedAt && subprocess.container.finishedAt
+                        ? formatTime(getProcessRuntime(subprocess)) :
+                        '(none)'} />
             </CardContent>
         </Card>;
     });
index 9cff1e981630ed6a72e8a2868bb8c70c413b22cd..0314a543f0e0302c57a45239438809e61575fea3 100644 (file)
@@ -40,7 +40,7 @@ export const SubprocessesCard = withStyles(styles)(
             <CardHeader
                 className={classes.title}
                 title={
-                    <Typography noWrap variant="title" color='inherit'>
+                    <Typography noWrap variant='h6' color='inherit'>
                         Subprocess and filters
                 </Typography>} />
             <CardContent>
index 62e91b5f1de5e4923abecd46b4494eff377f85ca..f6e02af4dffeadc5b540bf76b3cd077494c93fb6 100644 (file)
@@ -101,7 +101,7 @@ export const RepositoriesPanel = compose(
                         <CardContent>
                             <Grid container direction="row">
                                 <Grid item xs={8}>
-                                    <Typography variant="body2">
+                                    <Typography variant='body1'>
                                         When you are using an Arvados virtual machine, you should clone the https:// URLs. This will authenticate automatically using your API token. <br />
                                         In order to clone git repositories using SSH, <Link to={Routes.SSH_KEYS_USER} className={classes.link}>add an SSH key to your account</Link> and clone the git@ URLs.
                                     </Typography>
index 6da3210371490e968a041e9bc90adc59b5f2f8e4..a7d83daef57f82b9cbbd8bf5f70ca1088ae78f9e 100644 (file)
@@ -280,7 +280,7 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                     </div>
                     <Divider />
                     <div className={classes.chips}>
-                        <Typography variant='subheading'>Selected collections ({this.state.directories.length}):</Typography>
+                        <Typography variant='subtitle1'>Selected collections ({this.state.directories.length}):</Typography>
                         <Chips
                             orderable
                             deletable
index bb85825d232b111d4d59c34fc14468f78677fd92..c134548c734dee8270d4e36d4ba9793a5e065d98 100644 (file)
@@ -263,7 +263,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                     </div>
                     <Divider />
                     <div className={classes.chips}>
-                        <Typography variant='subheading'>Selected files ({this.state.files.length}):</Typography>
+                        <Typography variant='subtitle1'>Selected files ({this.state.files.length}):</Typography>
                         <Chips
                             orderable
                             deletable
index 32ebeb75c27bc8ee6be8ed186b5051a5dcff0ab0..3273f35458c5e0aedd214ec838f60e7d4189592f 100644 (file)
@@ -22,9 +22,9 @@ export const IntInput = ({ input }: IntInputProps) =>
         format={format}
         validate={getValidation(input)} />;
 
-const parse = (value: any) => parseInt(value, 10);
+export const parse = (value: any) => value === '' ? '' : parseInt(value, 10);
 
-const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
+export const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
 
 const getValidation = memoize(
     (input: IntCommandInputParameter) => ([
index 19beab6be3328ee13a1c70500669557f5999de4e..30ff494c1ebbae138811432817fc298244ab5a73 100644 (file)
@@ -8,17 +8,33 @@ import { reduxForm, Field } from 'redux-form';
 import { Grid } from '@material-ui/core';
 import { TextField } from '~/components/text-field/text-field';
 import { ExpandIcon } from '~/components/icon/icon';
+import * as IntInput from './inputs/int-input';
+import { require } from '~/validators/require';
+import { min } from '~/validators/min';
+import { optional } from '~/validators/optional';
+import { SwitchField } from '~/components/switch-field/switch-field';
 
 export const RUN_PROCESS_ADVANCED_FORM = 'runProcessAdvancedForm';
 
+export const OUTPUT_FIELD = 'output';
+export const RUNTIME_FIELD = 'runtime';
+export const RAM_FIELD = 'ram';
+export const VCPUS_FIELD = 'vcpus';
+export const KEEP_CACHE_RAM_FIELD = 'keepCacheRam';
+export const API_FIELD = 'api';
+
 export interface RunProcessAdvancedFormData {
-    output: string;
-    runtime: string;
+    [OUTPUT_FIELD]?: string;
+    [RUNTIME_FIELD]?: number;
+    [RAM_FIELD]: number;
+    [VCPUS_FIELD]: number;
+    [KEEP_CACHE_RAM_FIELD]?: number;
+    [API_FIELD]?: boolean;
 }
 
 export const RunProcessAdvancedForm =
     reduxForm<RunProcessAdvancedFormData>({
-        form: RUN_PROCESS_ADVANCED_FORM
+        form: RUN_PROCESS_ADVANCED_FORM,
     })(() =>
         <form>
             <ExpansionPanel elevation={0}>
@@ -29,17 +45,72 @@ export const RunProcessAdvancedForm =
                     <Grid container spacing={32}>
                         <Grid item xs={12} md={6}>
                             <Field
-                                name='output'
+                                name={OUTPUT_FIELD}
                                 component={TextField}
                                 label="Output name" />
                         </Grid>
                         <Grid item xs={12} md={6}>
                             <Field
-                                name='runtime'
+                                name={RUNTIME_FIELD}
+                                component={TextField}
+                                helperText="Maximum running time (in seconds) that this container will be allowed to run before being cancelled."
+                                label="Runtime limit"
+                                parse={IntInput.parse}
+                                format={IntInput.format}
+                                type='number'
+                                validate={runtimeValidation} />
+                        </Grid>
+                        <Grid item xs={12} md={6}>
+                            <Field
+                                name={RAM_FIELD}
+                                component={TextField}
+                                label="RAM"
+                                helperText="Number of ram bytes to be used to run this process."
+                                parse={IntInput.parse}
+                                format={IntInput.format}
+                                type='number'
+                                required
+                                validate={ramValidation} />
+                        </Grid>
+                        <Grid item xs={12} md={6}>
+                            <Field
+                                name={VCPUS_FIELD}
                                 component={TextField}
-                                label="Runtime limit (hh)" />
+                                label="VCPUs"
+                                helperText="Number of cores to be used to run this process."
+                                parse={IntInput.parse}
+                                format={IntInput.format}
+                                type='number'
+                                required
+                                validate={vcpusValidation} />
+                        </Grid>
+                        <Grid item xs={12} md={6}>
+                            <Field
+                                name={KEEP_CACHE_RAM_FIELD}
+                                component={TextField}
+                                label="Keep cache RAM"
+                                helperText="Number of keep cache bytes to be used to run this process."
+                                parse={IntInput.parse}
+                                format={IntInput.format}
+                                type='number'
+                                validate={keepCacheRamValdation} />
+                        </Grid>
+                        <Grid item xs={12} md={6}>
+                            <Field
+                                name={API_FIELD}
+                                component={SwitchField}
+                                switchProps={{
+                                    color: 'primary'
+                                }}
+                                label='API'
+                                helperText='When set, ARVADOS_API_HOST and ARVADOS_API_TOKEN will be set, and process will have networking enabled to access the Arvados API server.' />
                         </Grid>
                     </Grid>
                 </ExpansionPanelDetails>
             </ExpansionPanel>
         </form >);
+
+const ramValidation = [min(0)];
+const vcpusValidation = [min(1)];
+const keepCacheRamValdation = [optional(min(0))];
+const runtimeValidation = [optional(min(1))];
index a7e4a87f172661f1359b3e0aa6b5f751aafca102..8e855ab395a95666e11d1f708315465bf5aff098 100644 (file)
@@ -11,7 +11,7 @@ import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
 import { isValid } from 'redux-form';
 import { RUN_PROCESS_INPUTS_FORM } from './run-process-inputs-form';
-import { RunProcessAdvancedForm } from './run-process-advanced-form';
+import { RunProcessAdvancedForm, RUN_PROCESS_ADVANCED_FORM } from './run-process-advanced-form';
 import { createSelector, createStructuredSelector } from 'reselect';
 import { WorkflowPresetSelect } from '~/views/run-process-panel/workflow-preset-select';
 import { selectPreset } from '~/store/run-process-panel/run-process-panel-actions';
@@ -43,7 +43,7 @@ const inputsSelector = (state: RootState) =>
     state.runProcessPanel.inputs;
 
 const validSelector = (state: RootState) =>
-    isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state);
+    isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state) && isValid(RUN_PROCESS_ADVANCED_FORM)(state);
 
 const mapStateToProps = createStructuredSelector({
     inputs: inputsSelector,
index 7bfc2bfef6caa70aa952f654a8d15423dd234d8a..6827a00288c4327c7b30f05c94b5ffea38362521 100644 (file)
@@ -12,7 +12,7 @@ import { SearchBarAdvanceFormData } from '~/models/search-bar';
 import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
 import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
 import {
-    ProcessStatus,
+    ProcessStatus, ResourceCluster,
     ResourceFileSize,
     ResourceLastModifiedDate,
     ResourceName,
@@ -23,6 +23,7 @@ import { createTree } from '~/models/tree';
 import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 
 export enum SearchResultsPanelColumnNames {
+    CLUSTER = "Cluster",
     NAME = "Name",
     PROJECT = "Project",
     STATUS = "Status",
@@ -50,6 +51,13 @@ export interface WorkflowPanelFilter extends DataTableFilterItem {
 }
 
 export const searchResultsPanelColumns: DataColumns<string> = [
+    {
+        name: SearchResultsPanelColumnNames.CLUSTER,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid: string) => <ResourceCluster uuid={uuid} />
+    },
     {
         name: SearchResultsPanelColumnNames.NAME,
         selected: true,
@@ -110,4 +118,4 @@ export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
         onRowDoubleClick={props.onItemDoubleClick}
         onContextMenu={props.onContextMenu}
         contextMenuColumn={true} />;
-};
\ No newline at end of file
+};
index 2b6d3c97a46dd94b3e91ec43f229fc21172c326e..684e35b4d31680a630c846d506cb05fb6d38241f 100644 (file)
@@ -119,7 +119,7 @@ export const SiteManagerPanelRoot = compose(
             <CardContent>
                 <Grid container direction="row">
                     <Grid item xs={12}>
-                        <Typography variant='body1' paragraph={true} >
+                        <Typography  paragraph={true} >
                             You can log in to multiple Arvados sites here, then use the multi-site search page to search collections and projects on all sites at once.
                         </Typography>
                     </Grid>
@@ -157,7 +157,7 @@ export const SiteManagerPanelRoot = compose(
                 <form onSubmit={handleSubmit}>
                     <Grid container direction="row">
                         <Grid item xs={12}>
-                            <Typography variant='body1' paragraph={true} className={classes.remoteSiteInfo}>
+                            <Typography  paragraph={true} className={classes.remoteSiteInfo}>
                                 To add a remote Arvados site, paste the remote site's host here (see "ARVADOS_API_HOST" on the "current token" page).
                             </Typography>
                         </Grid>
index 2cdad07dd14fb774ca9260d87b1c4a2285c40e52..fcef5b3d67e5105dc5fba162dd70725fd366cac4 100644 (file)
@@ -55,14 +55,14 @@ export const SshKeyPanelRoot = withStyles(styles)(
             <CardContent>
                 <Grid container direction="row">
                     <Grid item xs={8}>
-                        { !hasKeys && <Typography variant='body1' paragraph={true} >
+                        { !hasKeys && <Typography  paragraph={true} >
                             You have not yet set up an SSH public key for use with Arvados.
                             <a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html'
                                 target='blank' className={classes.link}>
                                 Learn more.
                             </a>
                         </Typography>}
-                        { !hasKeys && <Typography variant='body1' paragraph={true}>
+                        { !hasKeys && <Typography  paragraph={true}>
                             When you have an SSH key you would like to use, add it using button below.
                         </Typography> }
                     </Grid>
index bcc661144fe64f0f0aee9faf0499a7d2c3ff06b4..7a319ef70286ac08d8ba1b5282fc603680b6c73c 100644 (file)
@@ -32,7 +32,10 @@ import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
 import { Dispatch } from "redux";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { createTree } from '~/models/tree';
-import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+import {
+    getInitialResourceTypeFilters,
+    getTrashPanelTypeFilters
+} from '~/store/resource-type-filters/resource-type-filters';
 
 type CssRules = "toolbar" | "button";
 
@@ -93,7 +96,7 @@ export const trashPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: getInitialResourceTypeFilters(),
+        filters: getTrashPanelTypeFilters(),
         render: uuid => <ResourceType uuid={uuid} />,
     },
     {
index b152896ff0d98a5d6592260133a06ff91e35fed8..c42036edf17ea5c0fae129a72fef980b4fa1a8d7 100644 (file)
@@ -28,6 +28,7 @@ import { compose, Dispatch } from 'redux';
 import { UserResource } from '~/models/user';
 import { ShareMeIcon, AddIcon } from '~/components/icon/icon';
 import { USERS_PANEL_ID, openUserCreateDialog } from '~/store/users/users-actions';
+import { noop } from 'lodash';
 
 type UserPanelRules = "button";
 
@@ -165,8 +166,8 @@ export const UserPanel = compose(
                         <span>
                             <DataExplorer
                                 id={USERS_PANEL_ID}
-                                onRowClick={this.handleRowClick}
-                                onRowDoubleClick={this.handleRowDoubleClick}
+                                onRowClick={noop}
+                                onRowDoubleClick={noop}
                                 onContextMenu={this.handleContextMenu}
                                 contextMenuColumn={true}
                                 hideColumnSelector
@@ -205,13 +206,5 @@ export const UserPanel = compose(
                     });
                 }
             }
-
-            handleRowDoubleClick = (uuid: string) => {
-                this.props.handleRowDoubleClick(uuid);
-            }
-
-            handleRowClick = () => {
-                return;
-            }
         }
     );
index 5cb4565ee3af3c4f24909e24bfe884bfe2809a5c..291041b3b984cbf4ddf3951b99c1931043f5335c 100644 (file)
@@ -110,7 +110,7 @@ const CardContentWithoutVirtualMachines = (props: VirtualMachineProps) =>
         <Card>
             <CardContent className={props.classes.cardWithoutMachines}>
                 <Grid item xs={6}>
-                    <Typography variant="body2">
+                    <Typography variant='body1'>
                         You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell.
                     </Typography>
                 </Grid>
@@ -149,7 +149,7 @@ const virtualMachineSendRequest = (props: VirtualMachineProps) =>
             SEND REQUEST FOR SHELL ACCESS
         </Button>
         {props.requestedDate &&
-            <Typography variant="body1">
+            <Typography >
                 A request for shell access was sent on {props.requestedDate}
             </Typography>}
     </span>;
@@ -188,7 +188,7 @@ const CardSSHSection = (props: VirtualMachineProps) =>
     <Grid item xs={12}>
         <Card>
             <CardContent>
-                <Typography variant="body2">
+                <Typography variant='body1'>
                     In order to access virtual machines using SSH, <Link to={Routes.SSH_KEYS_USER} className={props.classes.link}>add an SSH key to your account</Link> and add a section like this to your SSH configuration file ( ~/.ssh/config):
                 </Typography>
                 <DefaultCodeSnippet
index 90b2dad0197215578d8020b1da153d7d38c9e88e..cca374d6738e9645f5183871252a20d66311a979 100644 (file)
@@ -80,6 +80,8 @@ import { UserPanel } from '~/views/user-panel/user-panel';
 import { UserAttributesDialog } from '~/views-components/user-dialog/attributes-dialog';
 import { CreateUserDialog } from '~/views-components/dialog-forms/create-user-dialog';
 import { HelpApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/help-dialog';
+import { UserManageDialog } from '~/views-components/user-dialog/manage-dialog';
+import { SetupShellAccountDialog } from '~/views-components/dialog-forms/setup-shell-account-dialog';
 import { GroupsPanel } from '~/views/groups-panel/groups-panel';
 import { CreateGroupDialog } from '~/views-components/dialog-forms/create-group-dialog';
 import { RemoveGroupDialog } from '~/views-components/groups-dialog/remove-dialog';
@@ -225,12 +227,14 @@ export const WorkbenchPanel =
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />
             <RichTextEditorDialog />
+            <SetupShellAccountDialog />
             <SharingDialog />
             <Snackbar />
             <UpdateCollectionDialog />
             <UpdateProcessDialog />
             <UpdateProjectDialog />
             <UserAttributesDialog />
+            <UserManageDialog />
             <VirtualMachineAttributesDialog />
         </Grid>
     );
index 936c3485746b001b3fa4d3a52e8f493e209677e8..2294ab5e5373bb841bebac6752cf80c1a0d3da44 100644 (file)
@@ -72,9 +72,6 @@ export const WorkflowDetailsCard = withStyles(styles)(
 
         render() {
             const { classes, workflow } = this.props;
-            if (workflow) {
-                console.log(workflow.definition);
-            }
             const { value } = this.state;
             return <div className={classes.root}>
                 <Tabs value={value} onChange={this.handleChange} centered={true}>
index da8a0c4bd303d5de5a66c86d6b4fb1adb8a6861c..d8fa100e73d9d8f0061279c11407e6f2ead0a6fa 100644 (file)
@@ -9,7 +9,7 @@ import { DataTableDefaultView } from '~/components/data-table-default-view/data-
 import { WORKFLOW_PANEL_ID } from '~/store/workflow-panel/workflow-panel-actions';
 import {
     ResourceLastModifiedDate,
-    RosurceWorkflowName,
+    ResourceWorkflowName,
     ResourceWorkflowStatus,
     ResourceShare,
     ResourceRunProcess
@@ -70,7 +70,7 @@ export const workflowPanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.ASC,
         filters: createTree(),
-        render: (uuid: string) => <RosurceWorkflowName uuid={uuid} />
+        render: (uuid: string) => <ResourceWorkflowName uuid={uuid} />
     },
     {
         name: WorkflowPanelColumnNames.AUTHORISATION,