[arvados-workbench2] updated: 2.4.1-10-g75ce74fa

git repository hosting git at public.arvados.org
Fri Aug 5 17:16:10 UTC 2022


Summary of changes:
 cypress/integration/process.spec.js                |  11 +-
 package.json                                       |   4 +-
 src/components/code-snippet/code-snippet.tsx       |  61 ++++++++--
 src/components/data-explorer/data-explorer.tsx     |  10 +-
 .../data-table-default-view.tsx                    |   5 +-
 .../data-table-filters-popover.tsx                 |  64 +++++-----
 .../data-table-filters/data-table-filters-tree.tsx |  17 ++-
 src/components/data-table/data-table.tsx           |  19 +--
 .../default-code-snippet/default-code-snippet.tsx  |   9 +-
 src/components/default-view/default-view.tsx       |   3 +-
 src/components/icon/icon.tsx                       |   4 +-
 src/components/tree/tree.tsx                       |   3 +
 src/models/log.ts                                  |   3 +-
 src/models/tree.ts                                 |  15 ++-
 .../all-processes-panel-middleware-service.ts      |  44 ++++++-
 src/store/data-explorer/data-explorer-reducer.ts   |   2 +-
 src/store/navigation/navigation-action.ts          |   2 +-
 .../process-logs-panel-actions.ts                  |  55 ++++++---
 .../process-logs-panel-reducer.ts                  |   5 +-
 src/store/processes/process-command-actions.ts     |  28 -----
 src/store/processes/processes-actions.ts           |  46 ++++++-
 .../project-panel-middleware-service.ts            |   3 +-
 .../resource-type-filters.test.ts                  |  36 +++---
 .../resource-type-filters/resource-type-filters.ts |  37 +++---
 .../subprocess-panel-middleware-service.ts         |   6 +-
 .../action-sets/process-resource-action-set.ts     |  20 +---
 .../process-command-dialog.tsx                     |  61 ----------
 .../token-dialog/token-dialog.test.tsx             |  34 +++++-
 .../all-processes-panel/all-processes-panel.tsx    |   7 +-
 .../api-client-authorization-panel-root.tsx        |  11 +-
 .../collection-content-address-panel.tsx           |  11 +-
 src/views/favorite-panel/favorite-panel.tsx        |  10 +-
 .../group-details-panel/group-details-panel.tsx    |  97 ++++++++-------
 src/views/link-panel/link-panel-root.tsx           |  11 +-
 src/views/process-panel/process-cmd-card.tsx       | 133 +++++++++++++++++++++
 src/views/process-panel/process-log-card.tsx       |  13 +-
 .../process-panel/process-log-code-snippet.tsx     |  36 ++++--
 src/views/process-panel/process-panel-root.tsx     |  11 +-
 src/views/process-panel/process-panel.tsx          |   2 +-
 src/views/project-panel/project-panel.tsx          |   9 +-
 .../public-favorites-panel.tsx                     |   9 +-
 .../shared-with-me-panel/shared-with-me-panel.tsx  |   9 +-
 .../subprocess-panel/subprocess-panel-root.tsx     |  10 +-
 src/views/trash-panel/trash-panel.tsx              |   9 +-
 src/views/user-panel/user-panel.tsx                |   8 +-
 .../user-profile-panel/user-profile-panel-root.tsx |   8 +-
 src/views/workbench/workbench.tsx                  |   2 -
 src/views/workflow-panel/workflow-panel-view.tsx   |   4 +-
 tools/run-integration-tests.sh                     |   1 +
 yarn.lock                                          |  46 ++-----
 50 files changed, 650 insertions(+), 414 deletions(-)
 delete mode 100644 src/store/processes/process-command-actions.ts
 delete mode 100644 src/views-components/process-command-dialog/process-command-dialog.tsx
 create mode 100644 src/views/process-panel/process-cmd-card.tsx

       via  75ce74fad69511374162b679072a88a42253a7ee (commit)
       via  5acb7d1c0f6f0d0576464caf427c3180c340a533 (commit)
       via  197d06165daf25f2091d4e6b844ac1d8cf7e85cd (commit)
       via  12af93ada5a3727f7b26f1717ef1e2b82be2a62e (commit)
       via  b86dab182e81575d4ec02cad04236eab00a69a2e (commit)
      from  41f1383f355af8340d789f4b364d6eb72de4497d (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 75ce74fad69511374162b679072a88a42253a7ee
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Aug 4 16:04:23 2022 -0400

    Merge branch '19231-fewer-rows-per-page' refs #19231
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/store/data-explorer/data-explorer-reducer.ts b/src/store/data-explorer/data-explorer-reducer.ts
index f67cccdc..1e5cd88f 100644
--- a/src/store/data-explorer/data-explorer-reducer.ts
+++ b/src/store/data-explorer/data-explorer-reducer.ts
@@ -32,7 +32,7 @@ export const initialDataExplorer: DataExplorer = {
     itemsAvailable: 0,
     page: 0,
     rowsPerPage: 50,
-    rowsPerPageOptions: [50, 100, 200, 500],
+    rowsPerPageOptions: [10, 20, 50, 100, 200, 500],
     searchValue: "",
     requestState: DataTableRequestState.IDLE
 };

commit 5acb7d1c0f6f0d0576464caf427c3180c340a533
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Aug 4 15:17:52 2022 -0400

    Merge branch '19142-avoid-loading-unneeded-mounts' into main. Closes #19142
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/all-processes-panel/all-processes-panel-middleware-service.ts b/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
index 88b64e62..5d5e77d6 100644
--- a/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
+++ b/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
@@ -37,7 +37,11 @@ export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareSe
             try {
                 api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const processItems = await this.services.containerRequestService.list(
-                    { ...getParams(dataExplorer) });
+                    {
+                        ...getParams(dataExplorer),
+                        // Omit mounts when viewing all process panel
+                        select: containerRequestFieldsNoMounts,
+                    });
 
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(processItems.items));
@@ -62,6 +66,44 @@ export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareSe
     }
 }
 
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+export const containerRequestFieldsNoMounts = [
+    "command",
+    "container_count_max",
+    "container_count",
+    "container_image",
+    "container_uuid",
+    "created_at",
+    "cwd",
+    "description",
+    "environment",
+    "etag",
+    "expires_at",
+    "filters",
+    "href",
+    "kind",
+    "log_uuid",
+    "modified_at",
+    "modified_by_client_uuid",
+    "modified_by_user_uuid",
+    "name",
+    "output_name",
+    "output_path",
+    "output_properties",
+    "output_storage_classes",
+    "output_ttl",
+    "output_uuid",
+    "owner_uuid",
+    "priority",
+    "properties",
+    "requesting_container_uuid",
+    "runtime_constraints",
+    "scheduling_parameters",
+    "state",
+    "use_existing",
+    "uuid",
+];
+
 const getParams = ( dataExplorer: DataExplorer ) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
diff --git a/src/store/processes/processes-actions.ts b/src/store/processes/processes-actions.ts
index 213e292b..c4d421ac 100644
--- a/src/store/processes/processes-actions.ts
+++ b/src/store/processes/processes-actions.ts
@@ -34,13 +34,55 @@ export const loadProcess = (containerRequestUuid: string) =>
         return { containerRequest };
     };
 
-export const loadContainers = (filters: string) =>
+export const loadContainers = (filters: string, loadMounts: boolean = true) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { items } = await services.containerService.list({ filters });
+        let args: any = { filters };
+        if (!loadMounts) {
+            args.select = containerFieldsNoMounts;
+        }
+        const { items } = await services.containerService.list(args);
         dispatch<any>(updateResources(items));
         return items;
     };
 
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+const containerFieldsNoMounts = [
+    "auth_uuid",
+    "command",
+    "container_image",
+    "created_at",
+    "cwd",
+    "environment",
+    "etag",
+    "exit_code",
+    "finished_at",
+    "gateway_address",
+    "href",
+    "interactive_session_started",
+    "kind",
+    "lock_count",
+    "locked_by_uuid",
+    "log",
+    "modified_at",
+    "modified_by_client_uuid",
+    "modified_by_user_uuid",
+    "output_path",
+    "output_properties",
+    "output_storage_classes",
+    "output",
+    "owner_uuid",
+    "priority",
+    "progress",
+    "runtime_auth_scopes",
+    "runtime_constraints",
+    "runtime_status",
+    "runtime_user_uuid",
+    "scheduling_parameters",
+    "started_at",
+    "state",
+    "uuid",
+]
+
 export const cancelRunningWorkflow = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts
index ccfa4fff..d8a5d82d 100644
--- a/src/store/project-panel/project-panel-middleware-service.ts
+++ b/src/store/project-panel/project-panel-middleware-service.ts
@@ -89,7 +89,8 @@ export const loadMissingProcessesInformation = (resources: GroupContentsResource
         }, []);
         if (containerUuids.length > 0) {
             await dispatch<any>(loadContainers(
-                new FilterBuilder().addIn('uuid', containerUuids).getFilters()
+                new FilterBuilder().addIn('uuid', containerUuids).getFilters(),
+                false
             ));
         }
     };
diff --git a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
index fb9b0e8b..dd303233 100644
--- a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
+++ b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
@@ -24,6 +24,7 @@ import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type
 import { ContainerRequestResource } from 'models/container-request';
 import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
 import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
+import { containerRequestFieldsNoMounts } from 'store/all-processes-panel/all-processes-panel-middleware-service';
 
 export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -40,7 +41,10 @@ export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
             const parentContainerRequest = await this.services.containerRequestService.get(parentContainerRequestUuid);
             const containerRequests = await this.services.containerRequestService.list(
-                { ...getParams(dataExplorer, parentContainerRequest) });
+                {
+                    ...getParams(dataExplorer, parentContainerRequest) ,
+                    select: containerRequestFieldsNoMounts
+                });
 
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
             api.dispatch(updateResources(containerRequests.items));
diff --git a/src/views-components/context-menu/action-sets/process-resource-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
index a7a75901..f17d74c9 100644
--- a/src/views-components/context-menu/action-sets/process-resource-action-set.ts
+++ b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
@@ -7,7 +7,7 @@ import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "store/favorites/favorites-actions";
 import {
     RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
-    RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon,
+    RemoveIcon, ReRunProcessIcon, OutputIcon,
     AdvancedIcon
 } from "components/icon/icon";
 import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
@@ -18,7 +18,6 @@ import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
 import { openRemoveProcessDialog, reRunProcess } from "store/processes/processes-actions";
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { openProcessInputDialog } from "store/processes/process-input-actions";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
@@ -52,13 +51,6 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
             }
         }
     },
-    {
-        icon: InputIcon,
-        name: "Inputs",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessInputDialog(resource.uuid));
-        }
-    },
     {
         icon: OutputIcon,
         name: "Outputs",

commit 197d06165daf25f2091d4e6b844ac1d8cf7e85cd
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Jul 28 10:58:42 2022 -0400

    Merge branch '16070-process-commandline-view-panel' into main. Closes #16070
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/package.json b/package.json
index a8b3ee81..9e663ca6 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
     "@types/react-virtualized-auto-sizer": "1.0.0",
     "@types/react-window": "1.8.2",
     "@types/redux-form": "7.4.12",
-    "@types/shell-quote": "1.6.0",
+    "@types/shell-escape": "^0.2.0",
     "axios": "^0.21.1",
     "babel-core": "6.26.3",
     "babel-runtime": "6.26.0",
@@ -71,7 +71,7 @@
     "redux-thunk": "2.3.0",
     "reselect": "4.0.0",
     "set-value": "2.0.1",
-    "shell-quote": "1.6.1",
+    "shell-escape": "^0.2.0",
     "sinon": "7.3",
     "tslint": "5.20.0",
     "tslint-etc": "1.6.0",
diff --git a/src/components/code-snippet/code-snippet.tsx b/src/components/code-snippet/code-snippet.tsx
index f0a2b213..83c378b8 100644
--- a/src/components/code-snippet/code-snippet.tsx
+++ b/src/components/code-snippet/code-snippet.tsx
@@ -3,9 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { StyleRulesCallback, WithStyles, Typography, withStyles } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, Typography, withStyles, Link } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 import classNames from 'classnames';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from 'store/store';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { Dispatch } from 'redux';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
 
 type CssRules = 'root' | 'space';
 
@@ -24,19 +29,57 @@ export interface CodeSnippetDataProps {
     lines: string[];
     className?: string;
     apiResponse?: boolean;
+    linked?: boolean;
+}
+
+interface CodeSnippetAuthProps {
+    auth: FederationConfig;
 }
 
 type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
 
-export const CodeSnippet = withStyles(styles)(
-    ({ classes, lines, className, apiResponse }: CodeSnippetProps) =>
+const mapStateToProps = (state: RootState): CodeSnippetAuthProps => ({
+    auth: state.auth,
+});
+
+export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+    ({ classes, lines, linked, className, apiResponse, dispatch, auth }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
         <Typography
         component="div"
         className={classNames(classes.root, className)}>
-            {
-                lines.map((line: string, index: number) => {
-                    return <Typography key={index} className={apiResponse ? classes.space : className} component="pre">{line}</Typography>;
-                })
-            }
+            <Typography className={apiResponse ? classes.space : className} component="pre">
+                {linked ?
+                    lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
+                    lines.join('\n')
+                }
+            </Typography>
         </Typography>
-    );
\ No newline at end of file
+    ));
+
+const renderLinks = (auth: FederationConfig, dispatch: Dispatch) => (text: string): JSX.Element => {
+    // Matches UUIDs & PDHs
+    const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
+    const links = text.match(REGEX);
+    if (!links) {
+        return <>{text}</>;
+    }
+    return <>
+        {text.split(REGEX).map((part, index) =>
+            <React.Fragment key={index}>
+                {part}
+                {links[index] &&
+                <Link onClick={() => {
+                    const url = getNavUrl(links[index], auth)
+                    if (url) {
+                        window.open(`${window.location.origin}${url}`, '_blank');
+                    } else {
+                        dispatch(navigationNotAvailable(links[index]));
+                    }
+                }}
+                    style={ {cursor: 'pointer'} }>
+                    {links[index]}
+                </Link>}
+            </React.Fragment>
+        )}
+    </>;
+  };
diff --git a/src/components/default-code-snippet/default-code-snippet.tsx b/src/components/default-code-snippet/default-code-snippet.tsx
index 7ba97db4..bdcfc10f 100644
--- a/src/components/default-code-snippet/default-code-snippet.tsx
+++ b/src/components/default-code-snippet/default-code-snippet.tsx
@@ -6,8 +6,9 @@ import React from 'react';
 import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
 import { CodeSnippet, CodeSnippetDataProps } from 'components/code-snippet/code-snippet';
 import grey from '@material-ui/core/colors/grey';
+import { themeOptions } from 'common/custom-theme';
 
-const theme = createMuiTheme({
+const theme = createMuiTheme(Object.assign({}, themeOptions, {
     overrides: {
         MuiTypography: {
             body1: {
@@ -22,9 +23,9 @@ const theme = createMuiTheme({
         fontFamily: 'monospace',
         useNextVariants: true,
     }
-});
+}));
 
-export const DefaultCodeSnippet = (props: CodeSnippetDataProps) => 
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
     <MuiThemeProvider theme={theme}>
         <CodeSnippet {...props} />
-    </MuiThemeProvider>;
\ No newline at end of file
+    </MuiThemeProvider>;
diff --git a/src/store/processes/process-command-actions.ts b/src/store/processes/process-command-actions.ts
deleted file mode 100644
index 9dec9b30..00000000
--- a/src/store/processes/process-command-actions.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { dialogActions } from 'store/dialog/dialog-actions';
-import { RootState } from 'store/store';
-import { Dispatch } from 'redux';
-import { getProcess } from 'store/processes/process';
-import { quote } from 'shell-quote';
-
-export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
-
-export interface ProcessCommandDialogData {
-    command: string;
-    processName: string;
-}
-
-export const openProcessCommandDialog = (processUuid: string) =>
-    (dispatch: Dispatch<any>, getState: () => RootState) => {
-        const process = getProcess(processUuid)(getState().resources);
-        if (process) {
-            const data: ProcessCommandDialogData = {
-                command: quote(process.containerRequest.command),
-                processName: process.containerRequest.name,
-            };
-            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
-        }
-    };
diff --git a/src/views-components/context-menu/action-sets/process-resource-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
index 55b2d31f..a7a75901 100644
--- a/src/views-components/context-menu/action-sets/process-resource-action-set.ts
+++ b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
@@ -7,7 +7,7 @@ import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "store/favorites/favorites-actions";
 import {
     RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
-    RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon, CommandIcon,
+    RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon,
     AdvancedIcon
 } from "components/icon/icon";
 import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
@@ -20,7 +20,6 @@ import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { openProcessInputDialog } from "store/processes/process-input-actions";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
-import { openProcessCommandDialog } from "store/processes/process-command-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
 import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
@@ -69,13 +68,6 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
             }
         }
     },
-    {
-        icon: CommandIcon,
-        name: "Command",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessCommandDialog(resource.uuid));
-        }
-    },
     {
         icon: DetailsIcon,
         name: "View details",
@@ -135,4 +127,4 @@ export const processResourceAdminActionSet: ContextMenuActionSet = [[
             });
         }
     },
-]];
\ No newline at end of file
+]];
diff --git a/src/views-components/process-command-dialog/process-command-dialog.tsx b/src/views-components/process-command-dialog/process-command-dialog.tsx
deleted file mode 100644
index 7695837e..00000000
--- a/src/views-components/process-command-dialog/process-command-dialog.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { Dialog, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles, Tooltip, IconButton, CardHeader } from '@material-ui/core';
-import { withDialog } from "store/dialog/with-dialog";
-import { PROCESS_COMMAND_DIALOG_NAME } from 'store/processes/process-command-actions';
-import { WithDialogProps } from 'store/dialog/with-dialog';
-import { ProcessCommandDialogData } from 'store/processes/process-command-actions';
-import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
-import { compose } from 'redux';
-import CopyToClipboard from "react-copy-to-clipboard";
-import { CopyIcon } from 'components/icon/icon';
-
-type CssRules = 'codeSnippet' | 'copyToClipboard';
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-    codeSnippet: {
-        marginLeft: theme.spacing.unit * 3,
-        marginRight: theme.spacing.unit * 3,
-    },
-    copyToClipboard: {
-        marginRight: theme.spacing.unit,
-    }
-});
-
-export const ProcessCommandDialog = compose(
-    withDialog(PROCESS_COMMAND_DIALOG_NAME),
-    withStyles(styles),
-)(
-    (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
-        <Dialog
-            open={props.open}
-            maxWidth="md"
-            onClose={props.closeDialog}
-            style={{ alignSelf: 'stretch' }}>
-            <CardHeader
-                title={`Command - ${props.data.processName}`}
-                action={
-                    <Tooltip title="Copy to clipboard">
-                        <CopyToClipboard text={props.data.command}>
-                            <IconButton className={props.classes.copyToClipboard}>
-                                <CopyIcon />
-                            </IconButton>
-                        </CopyToClipboard>
-                    </Tooltip>
-                } />
-            <DefaultCodeSnippet
-                className={props.classes.codeSnippet}
-                lines={[props.data.command]} />
-            <DialogActions>
-                <Button
-                    variant='text'
-                    color='primary'
-                    onClick={props.closeDialog}>
-                    Close
-                </Button>
-            </DialogActions>
-        </Dialog>
-);
\ No newline at end of file
diff --git a/src/views-components/token-dialog/token-dialog.test.tsx b/src/views-components/token-dialog/token-dialog.test.tsx
index d2ff77e3..400bb1e6 100644
--- a/src/views-components/token-dialog/token-dialog.test.tsx
+++ b/src/views-components/token-dialog/token-dialog.test.tsx
@@ -16,6 +16,8 @@ import { mount, configure } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
 import CopyToClipboard from 'react-copy-to-clipboard';
 import { TokenDialogComponent } from './token-dialog';
+import { combineReducers, createStore } from 'redux';
+import { Provider } from 'react-redux';
 
 configure({ adapter: new Adapter() });
 
@@ -24,6 +26,7 @@ jest.mock('toggle-selection', () => () => () => null);
 describe('<CurrentTokenDialog />', () => {
   let props;
   let wrapper;
+  let store;
 
   beforeEach(() => {
     props = {
@@ -33,11 +36,25 @@ describe('<CurrentTokenDialog />', () => {
       open: true,
       dispatch: jest.fn(),
     };
+
+    const initialAuthState = {
+      localCluster: "zzzzz",
+      remoteHostsConfig: {},
+      sessions: {},
+    };
+
+    store = createStore(combineReducers({
+      auth: (state: any = initialAuthState, action: any) => state,
+    }));
   });
 
   describe('Get API Token dialog', () => {
     beforeEach(() => {
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>
+      );
     });
 
     it('should include API host and token', () => {
@@ -51,7 +68,10 @@ describe('<CurrentTokenDialog />', () => {
 
       const someDate = '2140-01-01T00:00:00.000Z'
       props.tokenExpiration = new Date(someDate);
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
       expect(wrapper.html()).toContain(props.tokenExpiration.toLocaleString());
     });
 
@@ -60,14 +80,20 @@ describe('<CurrentTokenDialog />', () => {
       expect(wrapper.html()).not.toContain('GET NEW TOKEN');
 
       props.canCreateNewTokens = true;
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
       expect(wrapper.html()).toContain('GET NEW TOKEN');
     });
   });
 
   describe('copy to clipboard button', () => {
     beforeEach(() => {
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
     });
 
     it('should copy API TOKEN to the clipboard', () => {
diff --git a/src/views/process-panel/process-cmd-card.tsx b/src/views/process-panel/process-cmd-card.tsx
new file mode 100644
index 00000000..4143501e
--- /dev/null
+++ b/src/views/process-panel/process-cmd-card.tsx
@@ -0,0 +1,133 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Card,
+    CardHeader,
+    IconButton,
+    CardContent,
+    Tooltip,
+    Typography,
+    Grid,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, CommandIcon, CopyIcon } from 'components/icon/icon';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { DefaultCodeSnippet } from 'components/default-code-snippet/default-code-snippet';
+import { Process } from 'store/processes/process';
+import shellescape from 'shell-escape';
+import CopyToClipboard from 'react-copy-to-clipboard';
+
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'avatar' | 'iconHeader';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    card: {
+        height: '100%'
+    },
+    header: {
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
+    },
+    iconHeader: {
+        fontSize: '1.875rem',
+        color: theme.customs.colors.green700,
+    },
+    avatar: {
+        alignSelf: 'flex-start',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+    content: {
+        padding: theme.spacing.unit * 1.0,
+        paddingTop: theme.spacing.unit * 0.5,
+        '&:last-child': {
+            paddingBottom: theme.spacing.unit * 1,
+        }
+    },
+    title: {
+        overflow: 'hidden',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+});
+
+interface ProcessCmdCardDataProps {
+  process: Process;
+  onCopy: (text: string) => void;
+}
+
+type ProcessCmdCardProps = ProcessCmdCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessCmdCard = withStyles(styles)(
+  ({
+    process,
+    onCopy,
+    classes,
+    doHidePanel,
+  }: ProcessCmdCardProps) => {
+    const command = process.containerRequest.command.map((v) =>
+      shellescape([v]) // Escape each arg separately
+    );
+
+    let formattedCommand = [...command];
+    formattedCommand.forEach((item, i, arr) => {
+      // Indent lines after the first
+      const indent = i > 0 ? '  ' : '';
+      // Escape newlines on every non-last arg when there are multiple lines
+      const lineBreak = arr.length > 1 && i < arr.length - 1 ? ' \\' : '';
+      arr[i] = `${indent}${item}${lineBreak}`;
+    });
+
+    return (
+      <Card className={classes.card}>
+        <CardHeader
+          className={classes.header}
+          classes={{
+            content: classes.title,
+            avatar: classes.avatar,
+          }}
+          avatar={<CommandIcon className={classes.iconHeader} />}
+          title={
+            <Typography noWrap variant="h6" color="inherit">
+              Command
+            </Typography>
+          }
+          action={
+            <Grid container direction="row" alignItems="center">
+              <Grid item>
+                <Tooltip title="Copy to clipboard" disableFocusListener>
+                  <IconButton>
+                    <CopyToClipboard
+                      text={command.join(" ")}
+                      onCopy={() => onCopy("Command copied to clipboard")}
+                    >
+                      <CopyIcon />
+                    </CopyToClipboard>
+                  </IconButton>
+                </Tooltip>
+              </Grid>
+              <Grid item>
+                {doHidePanel && (
+                  <Tooltip
+                    title={`Close Command Panel`}
+                    disableFocusListener
+                  >
+                    <IconButton onClick={doHidePanel}>
+                      <CloseIcon />
+                    </IconButton>
+                  </Tooltip>
+                )}
+              </Grid>
+            </Grid>
+          }
+        />
+        <CardContent className={classes.content}>
+          <DefaultCodeSnippet lines={formattedCommand} linked />
+        </CardContent>
+      </Card>
+    );
+  }
+);
diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx
index 4f95d0d8..f8ff8430 100644
--- a/src/views/process-panel/process-panel-root.tsx
+++ b/src/views/process-panel/process-panel-root.tsx
@@ -15,6 +15,7 @@ import { ProcessDetailsCard } from './process-details-card';
 import { getProcessPanelLogs, ProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel';
 import { ProcessLogsCard } from './process-log-card';
 import { FilterOption } from 'views/process-panel/process-log-form';
+import { ProcessCmdCard } from './process-cmd-card';
 
 type CssRules = 'root';
 
@@ -37,13 +38,14 @@ export interface ProcessPanelRootActionProps {
     cancelProcess: (uuid: string) => void;
     onLogFilterChange: (filter: FilterOption) => void;
     navigateToLog: (uuid: string) => void;
-    onLogCopyToClipboard: (uuid: string) => void;
+    onCopyToClipboard: (uuid: string) => void;
 }
 
 export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
 
 const panelsData: MPVPanelState[] = [
     {name: "Details"},
+    {name: "Command"},
     {name: "Logs", visible: true},
     {name: "Subprocesses"},
 ];
@@ -59,9 +61,14 @@ export const ProcessPanelRoot = withStyles(styles)(
                     cancelProcess={props.cancelProcess}
                 />
             </MPVPanelContent>
+            <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
+                <ProcessCmdCard
+                    onCopy={props.onCopyToClipboard}
+                    process={process} />
+            </MPVPanelContent>
             <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
                 <ProcessLogsCard
-                    onCopy={props.onLogCopyToClipboard}
+                    onCopy={props.onCopyToClipboard}
                     process={process}
                     lines={getProcessPanelLogs(processLogsPanel)}
                     selectedFilter={{
diff --git a/src/views/process-panel/process-panel.tsx b/src/views/process-panel/process-panel.tsx
index de6b13b3..7afaa04d 100644
--- a/src/views/process-panel/process-panel.tsx
+++ b/src/views/process-panel/process-panel.tsx
@@ -36,7 +36,7 @@ const mapStateToProps = ({ router, resources, processPanel, processLogsPanel }:
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
-    onLogCopyToClipboard: (message: string) => {
+    onCopyToClipboard: (message: string) => {
         dispatch<any>(snackbarActions.OPEN_SNACKBAR({
             message,
             hideDuration: 2000,
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 28fae4cd..a6c49e34 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -33,7 +33,6 @@ import { MoveProjectDialog } from 'views-components/dialog-forms/move-project-di
 import { MoveCollectionDialog } from 'views-components/dialog-forms/move-collection-dialog';
 import { FilesUploadCollectionDialog } from 'views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyCollectionDialog } from 'views-components/dialog-forms/partial-copy-collection-dialog';
-import { ProcessCommandDialog } from 'views-components/process-command-dialog/process-command-dialog';
 import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
 import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
@@ -241,7 +240,6 @@ export const WorkbenchPanel =
             <PublicKeyDialog />
             <PartialCopyCollectionDialog />
             <PartialCopyToCollectionDialog />
-            <ProcessCommandDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />
diff --git a/tools/run-integration-tests.sh b/tools/run-integration-tests.sh
index 0a9a0fc4..367ccecd 100755
--- a/tools/run-integration-tests.sh
+++ b/tools/run-integration-tests.sh
@@ -114,6 +114,7 @@ coproc arvboot (~/go/bin/arvados-server boot \
     -type test \
     -config ${TMPDIR}/arvados.yml \
     -no-workbench1 \
+    -no-workbench2 \
     -own-temporary-database \
     -timeout 20m 2> ${ARVADOS_LOG})
 trap cleanup ERR EXIT
diff --git a/yarn.lock b/yarn.lock
index 13ea553a..6dfb5b18 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2817,10 +2817,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/shell-quote at npm:1.6.0":
-  version: 1.6.0
-  resolution: "@types/shell-quote at npm:1.6.0"
-  checksum: 5d9f4e35c8df32d9994f8ae2f1a1fe8a6b7ee96794f803e0904ceae7ad7255a214954e85cd75bd847fe77458d3746430522e87237438f223b7d72a23c4928c0e
+"@types/shell-escape at npm:^0.2.0":
+  version: 0.2.0
+  resolution: "@types/shell-escape at npm:0.2.0"
+  checksum: 020696ed313eeb02deb2abcc581e8b570be6f9ee662892339965b524bb4fbdc9a97b6520d914117740ec11147b0b1aa52358b8e03fa214c2da99743adb196853
   languageName: node
   linkType: hard
 
@@ -3600,13 +3600,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-filter at npm:~0.0.0":
-  version: 0.0.1
-  resolution: "array-filter at npm:0.0.1"
-  checksum: 0e9afdf5e248c45821c6fe1232071a13a3811e1902c2c2a39d12e4495e8b0b25739fd95bffbbf9884b9693629621f6077b4ae16207b8f23d17710fc2465cebbb
-  languageName: node
-  linkType: hard
-
 "array-find-index at npm:^1.0.1":
   version: 1.0.2
   resolution: "array-find-index at npm:1.0.2"
@@ -3648,20 +3641,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-map at npm:~0.0.0":
-  version: 0.0.0
-  resolution: "array-map at npm:0.0.0"
-  checksum: 30d73fdc99956c8bd70daea40db5a7d78c5c2c75a03c64fc77904885e79adf7d5a0595076534f4e58962d89435f0687182ac929e65634e3d19931698cbac8149
-  languageName: node
-  linkType: hard
-
-"array-reduce at npm:~0.0.0":
-  version: 0.0.0
-  resolution: "array-reduce at npm:0.0.0"
-  checksum: d6226325271f477e3dd65b4d40db8597735b8d08bebcca4972e52d3c173d6c697533664fa8865789ea2d076bdaf1989bab5bdfbb61598be92074a67f13057c3a
-  languageName: node
-  linkType: hard
-
 "array-union at npm:^1.0.1":
   version: 1.0.2
   resolution: "array-union at npm:1.0.2"
@@ -3769,7 +3748,7 @@ __metadata:
     "@types/redux-devtools": 3.0.44
     "@types/redux-form": 7.4.12
     "@types/redux-mock-store": 1.0.2
-    "@types/shell-quote": 1.6.0
+    "@types/shell-escape": ^0.2.0
     "@types/sinon": 7.5
     "@types/uuid": 3.4.4
     axios: ^0.21.1
@@ -3829,7 +3808,7 @@ __metadata:
     redux-thunk: 2.3.0
     reselect: 4.0.0
     set-value: 2.0.1
-    shell-quote: 1.6.1
+    shell-escape: ^0.2.0
     sinon: 7.3
     ts-mock-imports: 1.3.7
     tslint: 5.20.0
@@ -16502,15 +16481,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"shell-quote at npm:1.6.1":
-  version: 1.6.1
-  resolution: "shell-quote at npm:1.6.1"
-  dependencies:
-    array-filter: ~0.0.0
-    array-map: ~0.0.0
-    array-reduce: ~0.0.0
-    jsonify: ~0.0.0
-  checksum: 982a4fdf2d474f0dc40885de4222f100ba457d7c75d46b532bf23b01774b8617bc62522c6825cb1fa7dd4c54c18e9dcbae7df2ca8983101841b6f2e6a7cacd2f
+"shell-escape at npm:^0.2.0":
+  version: 0.2.0
+  resolution: "shell-escape at npm:0.2.0"
+  checksum: 0d87f1ae22ad22a74e148348ceaf64721e3024f83c90afcfb527318ce10ece654dd62e103dd89a242f2f4e4ce3cecdef63e3d148c40e5fabca8ba6c508f97d9f
   languageName: node
   linkType: hard
 

commit 12af93ada5a3727f7b26f1717ef1e2b82be2a62e
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon Jul 11 18:12:39 2022 -0300

    Merge branch '18975-log-improvements'. Closes #18975
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/process.spec.js b/cypress/integration/process.spec.js
index 48b936cf..a543ffde 100644
--- a/cypress/integration/process.spec.js
+++ b/cypress/integration/process.spec.js
@@ -168,8 +168,15 @@ describe('Process tests', function() {
             cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
                 cy.loginAs(activeUser);
                 cy.goToPath(`/processes/${containerRequest.uuid}`);
-                // Should should all logs
-                cy.get('[data-cy=process-logs-filter]').should('contain', 'All logs');
+                // Should show main logs by default
+                cy.get('[data-cy=process-logs-filter]').should('contain', 'Main logs');
+                cy.get('[data-cy=process-logs]')
+                    .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
+                    .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
+                    .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                // Select 'All logs'
+                cy.get('[data-cy=process-logs-filter]').click();
+                cy.get('body').contains('li', 'All logs').click();
                 cy.get('[data-cy=process-logs]')
                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
                     .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 19b4beea..db603597 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -77,6 +77,7 @@ import NotInterested from '@material-ui/icons/NotInterested';
 // Import FontAwesome icons
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
+import { FormatAlignLeft } from '@material-ui/icons';
 library.add(
     faPencilAlt,
     faSlash,
@@ -180,7 +181,8 @@ export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
 export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
 export const CanManageIcon: IconType = (props) => <Computer {...props} />;
 export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
-export const WordWrapIcon: IconType = (props) => <WrapText {...props} />;
+export const WordWrapOnIcon: IconType = (props) => <WrapText {...props} />;
+export const WordWrapOffIcon: IconType = (props) => <FormatAlignLeft {...props} />;
 export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
 export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
 export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
diff --git a/src/models/log.ts b/src/models/log.ts
index 20060f88..f5d351ac 100644
--- a/src/models/log.ts
+++ b/src/models/log.ts
@@ -17,12 +17,13 @@ export enum LogEventType {
     STDOUT = 'stdout',
     STDERR = 'stderr',
     CONTAINER = 'container',
+    KEEPSTORE = 'keepstore',
 }
 
 export interface LogResource extends Resource, ResourceWithProperties {
     kind: ResourceKind.LOG;
     objectUuid: string;
     eventAt: string;
-    eventType: string;
+    eventType: LogEventType;
     summary: string;
 }
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index c8811bf4..146530ca 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -14,7 +14,7 @@ import { pluginConfig } from 'plugins';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
 
-const navigationNotAvailable = (id: string) =>
+export const navigationNotAvailable = (id: string) =>
     snackbarActions.OPEN_SNACKBAR({
         message: `${id} not available`,
         hideDuration: 3000,
diff --git a/src/store/process-logs-panel/process-logs-panel-actions.ts b/src/store/process-logs-panel/process-logs-panel-actions.ts
index b0ddd7ee..d4f5ab59 100644
--- a/src/store/process-logs-panel/process-logs-panel-actions.ts
+++ b/src/store/process-logs-panel/process-logs-panel-actions.ts
@@ -45,21 +45,25 @@ export const addProcessLogsPanelItem = (message: ResourceEventMessage<{ text: st
     async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
         if (PROCESS_PANEL_LOG_EVENT_TYPES.indexOf(message.eventType) > -1) {
             const uuid = getProcessLogsPanelCurrentUuid(getState().router);
-            if (uuid) {
-                const process = getProcess(uuid)(getState().resources);
-                if (process) {
-                    const { containerRequest, container } = process;
-                    if (message.objectUuid === containerRequest.uuid
-                        || (container && message.objectUuid === container.uuid)) {
-                        dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
-                            logType: COMBINED_FILTER_TYPE,
-                            log: message.properties.text
-                        }));
-                        dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
-                            logType: message.eventType,
-                            log: message.properties.text
-                        }));
-                    }
+            if (!uuid) { return }
+            const process = getProcess(uuid)(getState().resources);
+            if (!process) { return }
+            const { containerRequest, container } = process;
+            if (message.objectUuid === containerRequest.uuid
+                || (container && message.objectUuid === container.uuid)) {
+                dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+                    logType: ALL_FILTER_TYPE,
+                    log: message.properties.text
+                }));
+                dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+                    logType: message.eventType,
+                    log: message.properties.text
+                }));
+                if (MAIN_EVENT_TYPES.indexOf(message.eventType) > -1) {
+                    dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+                        logType: MAIN_FILTER_TYPE,
+                        log: message.properties.text
+                    }));
                 }
             }
         }
@@ -84,6 +88,9 @@ const loadContainerLogs = async (containerUuid: string, logService: LogService)
 
 const createInitialLogPanelState = (logResources: LogResource[]) => {
     const allLogs = logsToLines(logResources);
+    const mainLogs = logsToLines(logResources.filter(
+        e => MAIN_EVENT_TYPES.indexOf(e.eventType) > -1
+    ));
     const groupedLogResources = groupBy(logResources, log => log.eventType);
     const groupedLogs = Object
         .keys(groupedLogResources)
@@ -91,8 +98,12 @@ const createInitialLogPanelState = (logResources: LogResource[]) => {
             ...grouped,
             [key]: logsToLines(groupedLogResources[key])
         }), {});
-    const filters = [COMBINED_FILTER_TYPE, ...Object.keys(groupedLogs)];
-    const logs = { [COMBINED_FILTER_TYPE]: allLogs, ...groupedLogs };
+    const filters = [MAIN_FILTER_TYPE, ALL_FILTER_TYPE, ...Object.keys(groupedLogs)];
+    const logs = {
+        [MAIN_FILTER_TYPE]: mainLogs,
+        [ALL_FILTER_TYPE]: allLogs,
+        ...groupedLogs
+    };
     return { filters, logs };
 };
 
@@ -111,7 +122,14 @@ export const navigateToLogCollection = (uuid: string) =>
 
 const MAX_AMOUNT_OF_LOGS = 10000;
 
-const COMBINED_FILTER_TYPE = 'All logs';
+const ALL_FILTER_TYPE = 'All logs';
+
+const MAIN_FILTER_TYPE = 'Main logs';
+const MAIN_EVENT_TYPES = [
+    LogEventType.CRUNCH_RUN,
+    LogEventType.STDERR,
+    LogEventType.STDOUT,
+];
 
 const PROCESS_PANEL_LOG_EVENT_TYPES = [
     LogEventType.ARV_MOUNT,
@@ -123,4 +141,5 @@ const PROCESS_PANEL_LOG_EVENT_TYPES = [
     LogEventType.STDERR,
     LogEventType.STDOUT,
     LogEventType.CONTAINER,
+    LogEventType.KEEPSTORE,
 ];
diff --git a/src/store/process-logs-panel/process-logs-panel-reducer.ts b/src/store/process-logs-panel/process-logs-panel-reducer.ts
index c7d694c0..c502f1b1 100644
--- a/src/store/process-logs-panel/process-logs-panel-reducer.ts
+++ b/src/store/process-logs-panel/process-logs-panel-reducer.ts
@@ -24,10 +24,13 @@ export const processLogsPanelReducer = (state = initialState, action: ProcessLog
             selectedFilter
         }),
         ADD_PROCESS_LOGS_PANEL_ITEM: ({ logType, log }) => {
+            const filters = state.filters.indexOf(logType) > -1
+                ? state.filters
+                : [...state.filters, logType];
             const currentLogs = state.logs[logType] || [];
             const logsOfType = [...currentLogs, log];
             const logs = { ...state.logs, [logType]: logsOfType };
-            return { ...state, logs };
+            return { ...state, logs, filters };
         },
         default: () => state,
     });
diff --git a/src/views/process-panel/process-log-card.tsx b/src/views/process-panel/process-log-card.tsx
index ac409ec1..936b31a5 100644
--- a/src/views/process-panel/process-log-card.tsx
+++ b/src/views/process-panel/process-log-card.tsx
@@ -24,7 +24,8 @@ import {
     MaximizeIcon,
     TextDecreaseIcon,
     TextIncreaseIcon,
-    WordWrapIcon
+    WordWrapOffIcon,
+    WordWrapOnIcon,
 } from 'components/icon/icon';
 import { Process } from 'store/processes/process';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
@@ -92,7 +93,7 @@ export const ProcessLogsCard = withStyles(styles)(
     ({ classes, process, filters, selectedFilter, lines,
         onLogFilterChange, navigateToLog, onCopy,
         doHidePanel, doMaximizePanel, panelMaximized, panelName }: ProcessLogsCardProps) => {
-        const [wordWrapToggle, setWordWrapToggle] = useState<boolean>(true);
+        const [wordWrap, setWordWrap] = useState<boolean>(true);
         const [fontSize, setFontSize] = useState<number>(3);
         const fontBaseSize = 10;
         const fontStepSize = 1;
@@ -130,9 +131,9 @@ export const ProcessLogsCard = withStyles(styles)(
                             </Tooltip>
                         </Grid>
                         <Grid item>
-                            <Tooltip title="Toggle word wrapping" disableFocusListener>
-                                <IconButton onClick={() => setWordWrapToggle(!wordWrapToggle)}>
-                                    <WordWrapIcon />
+                            <Tooltip title={`${wordWrap ? 'Disable' : 'Enable'} word wrapping`} disableFocusListener>
+                                <IconButton onClick={() => setWordWrap(!wordWrap)}>
+                                    {wordWrap ? <WordWrapOffIcon /> : <WordWrapOnIcon />}
                                 </IconButton>
                             </Tooltip>
                         </Grid>
@@ -166,7 +167,7 @@ export const ProcessLogsCard = withStyles(styles)(
                             spacing={24}
                             direction='column'>
                             <Grid className={classes.logViewer} item xs>
-                                <ProcessLogCodeSnippet fontSize={fontBaseSize+(fontStepSize*fontSize)} wordWrap={wordWrapToggle} lines={lines} />
+                                <ProcessLogCodeSnippet fontSize={fontBaseSize+(fontStepSize*fontSize)} wordWrap={wordWrap} lines={lines} />
                             </Grid>
                         </Grid>
                         : <DefaultView
diff --git a/src/views/process-panel/process-log-code-snippet.tsx b/src/views/process-panel/process-log-code-snippet.tsx
index 6ea628e6..2b7391c2 100644
--- a/src/views/process-panel/process-log-code-snippet.tsx
+++ b/src/views/process-panel/process-log-code-snippet.tsx
@@ -13,10 +13,12 @@ import {
 import grey from '@material-ui/core/colors/grey';
 import { ArvadosTheme } from 'common/custom-theme';
 import { Link, Typography } from '@material-ui/core';
-import { navigateTo } from 'store/navigation/navigation-action';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
 import { Dispatch } from 'redux';
 import { connect, DispatchProp } from 'react-redux';
 import classNames from 'classnames';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { RootState } from 'store/store';
 
 type CssRules = 'root' | 'wordWrap' | 'logText';
 
@@ -26,6 +28,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         overflow: 'auto',
         backgroundColor: '#000',
         height: `calc(100% - ${theme.spacing.unit * 4}px)`, // so that horizontal scollbar is visible
+        "& a": {
+            color: theme.palette.primary.main,
+        },
     },
     logText: {
         padding: theme.spacing.unit * 0.5,
@@ -55,7 +60,11 @@ interface ProcessLogCodeSnippetProps {
     wordWrap?: boolean;
 }
 
-const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) => {
+interface ProcessLogCodeSnippetAuthProps {
+    auth: FederationConfig;
+}
+
+const renderLinks = (fontSize: number, auth: FederationConfig, dispatch: Dispatch) => (text: string) => {
     // Matches UUIDs & PDHs
     const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
     const links = text.match(REGEX);
@@ -67,7 +76,14 @@ const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) =>
         <React.Fragment key={index}>
             {part}
             {links[index] &&
-            <Link onClick={() => dispatch<any>(navigateTo(links[index]))}
+            <Link onClick={() => {
+                const url = getNavUrl(links[index], auth)
+                if (url) {
+                    window.open(`${window.location.origin}${url}`, '_blank');
+                } else {
+                    dispatch(navigationNotAvailable(links[index]));
+                }
+            }}
                 style={ {cursor: 'pointer'} }>
                 {links[index]}
             </Link>}
@@ -76,9 +92,13 @@ const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) =>
     </Typography>;
 };
 
-export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
-    ({classes, lines, fontSize, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & DispatchProp) => {
-        const [followMode, setFollowMode] = useState<boolean>(false);
+const mapStateToProps = (state: RootState): ProcessLogCodeSnippetAuthProps => ({
+    auth: state.auth,
+});
+
+export const ProcessLogCodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+    ({classes, lines, fontSize, auth, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & ProcessLogCodeSnippetAuthProps & DispatchProp) => {
+        const [followMode, setFollowMode] = useState<boolean>(true);
         const scrollRef = useRef<HTMLDivElement>(null);
 
         useEffect(() => {
@@ -92,7 +112,7 @@ export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
             <div ref={scrollRef} className={classes.root}
                 onScroll={(e) => {
                     const elem = e.target as HTMLDivElement;
-                    if (elem.scrollTop + elem.clientHeight >= elem.scrollHeight) {
+                    if (elem.scrollTop + (elem.clientHeight*1.1) >= elem.scrollHeight) {
                         setFollowMode(true);
                     } else {
                         setFollowMode(false);
@@ -101,7 +121,7 @@ export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
                 { lines.map((line: string, index: number) =>
                 <Typography key={index} component="pre"
                     className={classNames(classes.logText, wordWrap ? classes.wordWrap : undefined)}>
-                    {renderLinks(fontSize, dispatch)(line)}
+                    {renderLinks(fontSize, auth, dispatch)(line)}
                 </Typography>
                 ) }
             </div>

commit b86dab182e81575d4ec02cad04236eab00a69a2e
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Jun 7 17:37:00 2022 -0400

    Merge branch '18984-project-type-filters-2' into main. Closes #18984
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index 0363d333..40617f73 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -11,7 +11,7 @@ import { SearchInput } from 'components/search-input/search-input';
 import { ArvadosTheme } from "common/custom-theme";
 import { createTree } from 'models/tree';
 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
-import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { CloseIcon, IconType, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { PaperProps } from '@material-ui/core/Paper';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 
@@ -64,7 +64,8 @@ interface DataExplorerDataProps<T> {
     rowsPerPageOptions: number[];
     page: number;
     contextMenuColumn: boolean;
-    dataTableDefaultView?: React.ReactNode;
+    defaultViewIcon?: IconType;
+    defaultViewMessages?: string[];
     working?: boolean;
     currentRefresh?: string;
     currentRoute?: string;
@@ -149,7 +150,7 @@ export const DataExplorer = withStyles(styles)(
                 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
-                dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
+                defaultViewIcon, defaultViewMessages, hideColumnSelector, actions, paperProps, hideSearchInput,
                 paperKey, fetchMode, currentItemUuid, title,
                 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
             } = this.props;
@@ -197,7 +198,8 @@ export const DataExplorer = withStyles(styles)(
                     onSortToggle={onSortToggle}
                     extractKey={extractKey}
                     working={this.state.showLoading}
-                    defaultView={dataTableDefaultView}
+                    defaultViewIcon={defaultViewIcon}
+                    defaultViewMessages={defaultViewMessages}
                     currentItemUuid={currentItemUuid}
                     currentRoute={paperKey} /></Grid>
                 <Grid item xs><Toolbar className={classes.footer}>
diff --git a/src/components/data-table-default-view/data-table-default-view.tsx b/src/components/data-table-default-view/data-table-default-view.tsx
index 2869ab82..b245c19b 100644
--- a/src/components/data-table-default-view/data-table-default-view.tsx
+++ b/src/components/data-table-default-view/data-table-default-view.tsx
@@ -16,12 +16,13 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginBottom: theme.spacing.unit * 4,
     },
 });
-type DataTableDefaultViewDataProps = Partial<Pick<DefaultViewDataProps, 'icon' | 'messages'>>;
+type DataTableDefaultViewDataProps = Partial<Pick<DefaultViewDataProps, 'icon' | 'messages' | 'filtersApplied'>>;
 type DataTableDefaultViewProps = DataTableDefaultViewDataProps & WithStyles<CssRules>;
 
 export const DataTableDefaultView = withStyles(styles)(
     ({ classes, ...props }: DataTableDefaultViewProps) => {
         const icon = props.icon || DetailsIcon;
-        const messages = props.messages || ['No items found'];
+        const filterWarning: string[] = props.filtersApplied ? ['Filters are applied to the data.'] : [];
+        const messages = filterWarning.concat(props.messages || ['No items found']);
         return <DefaultView {...classes} {...{ icon, messages }} />;
     });
diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx
index 3183157b..b5187866 100644
--- a/src/components/data-table-filters/data-table-filters-popover.tsx
+++ b/src/components/data-table-filters/data-table-filters-popover.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from "react";
+import React, { useEffect } from "react";
 import {
     WithStyles,
     withStyles,
@@ -23,6 +23,7 @@ import { DefaultTransformOrigin } from "components/popover/helpers";
 import { createTree } from 'models/tree';
 import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
 import { getNodeDescendants } from 'models/tree';
+import debounce from "lodash/debounce";
 
 export type CssRules = "root" | "icon" | "iconButton" | "active" | "checkbox";
 
@@ -127,7 +128,7 @@ export const DataTableFiltersPopover = withStyles(styles)(
                     open={!!this.state.anchorEl}
                     anchorOrigin={DefaultTransformOrigin}
                     transformOrigin={DefaultTransformOrigin}
-                    onClose={this.cancel}>
+                    onClose={this.close}>
                     <Card>
                         <CardContent>
                             <Typography variant="caption">
@@ -137,36 +138,21 @@ export const DataTableFiltersPopover = withStyles(styles)(
                         <DataTableFiltersTree
                             filters={this.state.filters}
                             mutuallyExclusive={this.props.mutuallyExclusive}
-                            onChange={filters => {
-                                this.setState({ filters });
-                                if (this.props.mutuallyExclusive) {
-                                    const { onChange } = this.props;
-                                    if (onChange) {
-                                        onChange(filters);
-                                    }
-                                    this.setState({ anchorEl: undefined });
-                                }
-                            }} />
+                            onChange={this.onChange} />
                         {this.props.mutuallyExclusive ||
                         <CardActions>
-                            <Button
-                                color="primary"
-                                variant='contained'
-                                size="small"
-                                onClick={this.submit}>
-                                Ok
-                            </Button>
                             <Button
                                 color="primary"
                                 variant="outlined"
                                 size="small"
-                                onClick={this.cancel}>
-                                Cancel
+                                onClick={this.close}>
+                                Close
                             </Button>
                         </CardActions >
                         }
                     </Card>
                 </Popover>
+                <this.MountHandler />
             </>;
         }
 
@@ -180,25 +166,43 @@ export const DataTableFiltersPopover = withStyles(styles)(
             this.setState({ anchorEl: this.icon.current || undefined });
         }
 
-        submit = () => {
+        onChange = (filters) => {
+            this.setState({ filters });
+            if (this.props.mutuallyExclusive) {
+                // Mutually exclusive filters apply immediately
+                const { onChange } = this.props;
+                if (onChange) {
+                    onChange(filters);
+                }
+                this.close();
+            } else {
+                // Non-mutually exclusive filters are debounced
+                this.submit();
+            }
+        }
+
+        submit = debounce (() => {
             const { onChange } = this.props;
             if (onChange) {
                 onChange(this.state.filters);
             }
-            this.setState({ anchorEl: undefined });
-        }
+        }, 1000);
+
+        MountHandler = () => {
+            useEffect(() => {
+                return () => {
+                    this.submit.cancel();
+                }
+            },[]);
+            return null;
+        };
 
-        cancel = () => {
+        close = () => {
             this.setState(prev => ({
                 ...prev,
-                filters: prev.prevFilters,
                 anchorEl: undefined
             }));
         }
 
-        setFilters = (filters: DataTableFilters) => {
-            this.setState({ filters });
-        }
-
     }
 );
diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx
index 6514078d..7b97865b 100644
--- a/src/components/data-table-filters/data-table-filters-tree.tsx
+++ b/src/components/data-table-filters/data-table-filters-tree.tsx
@@ -34,7 +34,7 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
             levelIndentation={hasSubfilters ? 20 : 0}
             itemRightPadding={20}
             items={filtersToTree(filters)}
-            render={renderItem}
+            render={this.props.mutuallyExclusive ? renderRadioItem : renderItem}
             showSelection
             useRadioButtons={this.props.mutuallyExclusive}
             disableRipple
@@ -76,13 +76,24 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
 }
 
 const renderItem = (item: TreeItem<DataTableFilterItem>) =>
-    <span>{item.data.name}</span>;
+    <span>
+        {item.data.name}
+        {item.initialState !== item.selected ? <>
+            *
+        </> : null}
+    </span>;
+
+const renderRadioItem = (item: TreeItem<DataTableFilterItem>) =>
+    <span>
+        {item.data.name}
+    </span>;
 
 const filterToTreeItem = (filters: DataTableFilters) =>
     (id: string): TreeItem<any> => {
         const node = getNode(id)(filters) || initTreeNode({ id: '', value: 'InvalidNode' });
         const items = getNodeChildrenIds(node.id)(filters)
             .map(filterToTreeItem(filters));
+        const isIndeterminate = !node.selected && items.some(i => i.selected || i.indeterminate);
 
         return {
             active: node.active,
@@ -91,6 +102,8 @@ const filterToTreeItem = (filters: DataTableFilters) =>
             items: items.length > 0 ? items : undefined,
             open: node.expanded,
             selected: node.selected,
+            initialState: node.initialState,
+            indeterminate: isIndeterminate,
             status: TreeItemStatus.LOADED,
         };
     };
diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx
index 14dfdaca..d942234d 100644
--- a/src/components/data-table/data-table.tsx
+++ b/src/components/data-table/data-table.tsx
@@ -9,8 +9,8 @@ import { DataColumn, SortDirection } from './data-column';
 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
 import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
 import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
-import { countNodes } from 'models/tree';
-import { PendingIcon } from 'components/icon/icon';
+import { countNodes, getTreeDirty } from 'models/tree';
+import { IconType, PendingIcon } from 'components/icon/icon';
 import { SvgIconProps } from '@material-ui/core/SvgIcon';
 import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
 
@@ -31,7 +31,8 @@ export interface DataTableDataProps<T> {
     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
     extractKey?: (item: T) => React.Key;
     working?: boolean;
-    defaultView?: React.ReactNode;
+    defaultViewIcon?: IconType;
+    defaultViewMessages?: string[];
     currentItemUuid?: string;
     currentRoute?: string;
 }
@@ -105,15 +106,17 @@ export const DataTable = withStyles(styles)(
                                 icon={PendingIcon}
                                 messages={['Loading data, please wait.']} />
                         </div> }
-                    {items.length === 0 && !working && this.renderNoItemsPlaceholder()}
+                    {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
                 </div>
             </div>;
         }
 
-        renderNoItemsPlaceholder = () => {
-            return this.props.defaultView
-                ? this.props.defaultView
-                : <DataTableDefaultView />;
+        renderNoItemsPlaceholder = (columns: DataColumns<T>) => {
+            const dirty = columns.some((column) => getTreeDirty('')(column.filters));
+            return <DataTableDefaultView
+                icon={this.props.defaultViewIcon}
+                messages={this.props.defaultViewMessages}
+                filtersApplied={dirty} />;
         }
 
         renderHeadCell = (column: DataColumn<T>, index: number) => {
diff --git a/src/components/default-view/default-view.tsx b/src/components/default-view/default-view.tsx
index 6e89db25..014b8cc4 100644
--- a/src/components/default-view/default-view.tsx
+++ b/src/components/default-view/default-view.tsx
@@ -27,6 +27,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export interface DefaultViewDataProps {
     classRoot?: string;
     messages: string[];
+    filtersApplied?: boolean;
     classMessage?: string;
     icon: IconType;
     classIcon?: string;
@@ -43,4 +44,4 @@ export const DefaultView = withStyles(styles)(
                     className={classnames([classes.message, classMessage])}>{msg}</Typography>;
             })}
         </Typography>
-);
\ No newline at end of file
+);
diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx
index 3ae884b6..fc9dbc74 100644
--- a/src/components/tree/tree.tsx
+++ b/src/components/tree/tree.tsx
@@ -97,6 +97,8 @@ export interface TreeItem<T> {
     open: boolean;
     active: boolean;
     selected?: boolean;
+    initialState?: boolean;
+    indeterminate?: boolean;
     flatTree?: boolean;
     status: TreeItemStatus;
     items?: Array<TreeItem<T>>;
@@ -292,6 +294,7 @@ export const Tree = withStyles(styles)(
                             {showSelection(it) && !useRadioButtons &&
                                 <Checkbox
                                     checked={it.selected}
+                                    indeterminate={!it.selected && it.indeterminate}
                                     className={classes.checkbox}
                                     color="primary"
                                     onClick={this.handleCheckboxChange(it)} />}
diff --git a/src/models/tree.ts b/src/models/tree.ts
index e9291388..996f98a4 100644
--- a/src/models/tree.ts
+++ b/src/models/tree.ts
@@ -14,6 +14,7 @@ export interface TreeNode<T = any> {
     parent: string;
     active: boolean;
     selected: boolean;
+    initialState?: boolean;
     expanded: boolean;
     status: TreeNodeStatus;
 }
@@ -197,6 +198,19 @@ export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { pare
     ...data,
 });
 
+export const getTreeDirty = (id: string) => <T>(tree: Tree<T>): boolean => {
+    const node = getNode(id)(tree);
+    const children = getNodeDescendants(id)(tree);
+    return (node
+            && node.initialState !== undefined
+            && node.selected !== node.initialState
+            )
+            || children.some(child =>
+                child.initialState !== undefined
+                && child.selected !== child.initialState
+            );
+}
+
 const toggleDescendantsSelection = (id: string) => <T>(tree: Tree<T>) => {
     const node = getNode(id)(tree);
     if (node) {
@@ -228,7 +242,6 @@ const toggleParentNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
     return tree;
 };
 
-
 const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
     ({ ...node, value: mapFn(node.value) });
 
diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts
index f001770e..5972f60c 100644
--- a/src/store/resource-type-filters/resource-type-filters.test.ts
+++ b/src/store/resource-type-filters/resource-type-filters.test.ts
@@ -4,7 +4,7 @@
 
 import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter, GroupTypeFilter, buildProcessStatusFilters, ProcessStatusFilter } from './resource-type-filters';
 import { ResourceKind } from 'models/resource';
-import { deselectNode } from 'models/tree';
+import { selectNode, deselectNode } from 'models/tree';
 import { pipe } from 'lodash/fp';
 import { FilterBuilder } from 'services/api/filter-builder';
 
@@ -31,21 +31,21 @@ describe("serializeResourceTypeFilters", () => {
         const filters = getInitialResourceTypeFilters();
         const serializedFilters = serializeResourceTypeFilters(filters);
         expect(serializedFilters)
-            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.COLLECTION}","${ResourceKind.WORKFLOW}"]]`);
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.COLLECTION}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
     });
 
     it("should serialize all but collection filters", () => {
         const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
         const serializedFilters = serializeResourceTypeFilters(filters);
         expect(serializedFilters)
-            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.WORKFLOW}"]]`);
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
     });
 
     it("should serialize output collections and projects", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROCESS),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
             deselectNode(CollectionTypeFilter.LOG_COLLECTION),
             deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
@@ -59,8 +59,8 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize intermediate collections and projects", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROCESS),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
             deselectNode(CollectionTypeFilter.LOG_COLLECTION),
             deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION),
@@ -75,8 +75,8 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
-            deselectNode(ObjectTypeFilter.PROCESS),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
         )();
 
@@ -91,7 +91,7 @@ describe("serializeResourceTypeFilters", () => {
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ProcessTypeFilter.CHILD_PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -104,8 +104,10 @@ describe("serializeResourceTypeFilters", () => {
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ProcessTypeFilter.MAIN_PROCESS),
+            deselectNode(ObjectTypeFilter.DEFINITION),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+
+            selectNode(ProcessTypeFilter.CHILD_PROCESS),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -116,9 +118,9 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize all project types", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -130,9 +132,9 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(GroupTypeFilter.PROJECT),
-            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -144,9 +146,9 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(GroupTypeFilter.FILTER_GROUP),
-            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts
index 64a391ca..361b52a6 100644
--- a/src/store/resource-type-filters/resource-type-filters.ts
+++ b/src/store/resource-type-filters/resource-type-filters.ts
@@ -25,9 +25,9 @@ export enum ProcessStatusFilter {
 
 export enum ObjectTypeFilter {
     PROJECT = 'Project',
-    PROCESS = 'Process',
-    COLLECTION = 'Data collection',
     WORKFLOW = 'Workflow',
+    COLLECTION = 'Data collection',
+    DEFINITION = 'Definition',
 }
 
 export enum GroupTypeFilter {
@@ -43,11 +43,11 @@ export enum CollectionTypeFilter {
 }
 
 export enum ProcessTypeFilter {
-    MAIN_PROCESS = 'Main',
-    CHILD_PROCESS = 'Child',
+    MAIN_PROCESS = 'Runs',
+    CHILD_PROCESS = 'Intermediate Steps',
 }
 
-const initFilter = (name: string, parent = '', isSelected?: boolean) =>
+const initFilter = (name: string, parent = '', isSelected?: boolean, isExpanded?: boolean) =>
     setNode<DataTableFilterItem>({
         id: name,
         value: { name },
@@ -55,16 +55,17 @@ const initFilter = (name: string, parent = '', isSelected?: boolean) =>
         children: [],
         active: false,
         selected: isSelected !== undefined ? isSelected : true,
-        expanded: false,
+        initialState: isSelected !== undefined ? isSelected : true,
+        expanded: isExpanded !== undefined ? isExpanded : false,
         status: TreeNodeStatus.LOADED,
     });
 
 export const getSimpleObjectTypeFilters = pipe(
     (): DataTableFilters => createTree<DataTableFilterItem>(),
     initFilter(ObjectTypeFilter.PROJECT),
-    initFilter(ObjectTypeFilter.PROCESS),
-    initFilter(ObjectTypeFilter.COLLECTION),
     initFilter(ObjectTypeFilter.WORKFLOW),
+    initFilter(ObjectTypeFilter.COLLECTION),
+    initFilter(ObjectTypeFilter.DEFINITION),
 );
 
 // Using pipe() with more than 7 arguments makes the return type be 'any',
@@ -72,23 +73,23 @@ export const getSimpleObjectTypeFilters = pipe(
 export const getInitialResourceTypeFilters = pipe(
     (): DataTableFilters => createTree<DataTableFilterItem>(),
     pipe(
-        initFilter(ObjectTypeFilter.PROJECT),
+        initFilter(ObjectTypeFilter.PROJECT, '', true, true),
         initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT),
         initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT),
     ),
     pipe(
-        initFilter(ObjectTypeFilter.PROCESS),
-        initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.PROCESS),
-        initFilter(ProcessTypeFilter.CHILD_PROCESS, ObjectTypeFilter.PROCESS)
+        initFilter(ObjectTypeFilter.WORKFLOW, '', false, true),
+        initFilter(ObjectTypeFilter.DEFINITION, ObjectTypeFilter.WORKFLOW),
+        initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.WORKFLOW),
+        initFilter(ProcessTypeFilter.CHILD_PROCESS, ObjectTypeFilter.WORKFLOW, false),
     ),
     pipe(
-        initFilter(ObjectTypeFilter.COLLECTION),
+        initFilter(ObjectTypeFilter.COLLECTION, '', true, true),
         initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
     ),
-    initFilter(ObjectTypeFilter.WORKFLOW)
 
 );
 
@@ -133,11 +134,11 @@ const objectTypeToResourceKind = (type: ObjectTypeFilter) => {
     switch (type) {
         case ObjectTypeFilter.PROJECT:
             return ResourceKind.PROJECT;
-        case ObjectTypeFilter.PROCESS:
+        case ObjectTypeFilter.WORKFLOW:
             return ResourceKind.PROCESS;
         case ObjectTypeFilter.COLLECTION:
             return ResourceKind.COLLECTION;
-        case ObjectTypeFilter.WORKFLOW:
+        case ObjectTypeFilter.DEFINITION:
             return ResourceKind.WORKFLOW;
     }
 };
@@ -155,7 +156,7 @@ const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof c
             ? set.add(ObjectTypeFilter.COLLECTION)
             : set,
         set => processFilters.length > 0
-            ? set.add(ObjectTypeFilter.PROCESS)
+            ? set.add(ObjectTypeFilter.WORKFLOW)
             : set,
         set => Array.from(set)
     )();
@@ -163,7 +164,7 @@ const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof c
     return {
         fb: typeFilters.length > 0
             ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind))
-            : fb,
+            : fb.addIsA('uuid', ResourceKind.NONE),
         selectedFilters,
     };
 };
diff --git a/src/views/all-processes-panel/all-processes-panel.tsx b/src/views/all-processes-panel/all-processes-panel.tsx
index b06b08e4..0e08a879 100644
--- a/src/views/all-processes-panel/all-processes-panel.tsx
+++ b/src/views/all-processes-panel/all-processes-panel.tsx
@@ -27,7 +27,6 @@ import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { ContainerRequestState } from "models/container-request";
 import { RootState } from 'store/store';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from 'store/resource-type-filters/resource-type-filters';
 import { getProcess } from 'store/processes/process';
@@ -151,10 +150,8 @@ export const AllProcessesPanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
-                    dataTableDefaultView={ <DataTableDefaultView
-                        icon={ProcessIcon}
-                        messages={['Processes list empty.']}
-                        /> } />
+                    defaultViewIcon={ProcessIcon}
+                    defaultViewMessages={['Processes list empty.']} />
                 </div>
             }
         }
diff --git a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
index 8f87cb26..ddca138c 100644
--- a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
+++ b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
@@ -11,7 +11,6 @@ import { ShareMeIcon } from 'components/icon/icon';
 import { createTree } from 'models/tree';
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
-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';
@@ -141,9 +140,7 @@ export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
             contextMenuColumn={true}
             hideColumnSelector
             hideSearchInput
-            dataTableDefaultView={
-                <DataTableDefaultView
-                    icon={ShareMeIcon}
-                    messages={[DEFAULT_MESSAGE]} />
-            } /></div>
-);
\ No newline at end of file
+            defaultViewIcon={ShareMeIcon}
+            defaultViewMessages={[DEFAULT_MESSAGE]} />
+        </div>
+);
diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx
index f1278049..8e8266cc 100644
--- a/src/views/collection-content-address-panel/collection-content-address-panel.tsx
+++ b/src/views/collection-content-address-panel/collection-content-address-panel.tsx
@@ -12,7 +12,6 @@ import {
 import { CollectionIcon } from 'components/icon/icon';
 import { ArvadosTheme } from 'common/custom-theme';
 import { BackIcon } from 'components/icon/icon';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from 'store/collections-content-address-panel/collections-content-address-panel-actions';
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { Dispatch } from 'redux';
@@ -165,12 +164,10 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
                         onContextMenu={this.props.onContextMenu(this.props.resources)}
                         contextMenuColumn={true}
                         title={`Content address: ${this.props.match.params.id}`}
-                        dataTableDefaultView={
-                            <DataTableDefaultView
-                                icon={CollectionIcon}
-                                messages={['Collections with this content address not found.']} />
-                        } /></div>
-                    </div>;
+                        defaultViewIcon={CollectionIcon}
+                        defaultViewMessages={['Collections with this content address not found.']} />
+                    </div>
+                </div>;
             }
         }
     )
diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx
index e520a59c..cb02f1ad 100644
--- a/src/views/favorite-panel/favorite-panel.tsx
+++ b/src/views/favorite-panel/favorite-panel.tsx
@@ -31,7 +31,6 @@ import { navigateTo } from 'store/navigation/navigation-action';
 import { ContainerRequestState } from "models/container-request";
 import { FavoritesState } from 'store/favorites/favorites-reducer';
 import { RootState } from 'store/store';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getSimpleObjectTypeFilters } from 'store/resource-type-filters/resource-type-filters';
 import { getResource, ResourcesState } from 'store/resources/resources';
@@ -185,12 +184,9 @@ export const FavoritePanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={FavoriteIcon}
-                            messages={['Your favorites list is empty.']}
-                            />
-                    } /></div>;
+                    defaultViewIcon={FavoriteIcon}
+                    defaultViewMessages={['Your favorites list is empty.']} />
+                </div>;
             }
         }
     )
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index 9cee3cbc..311bc86e 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -15,16 +15,20 @@ import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, ope
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { Grid, Button, Tabs, Tab, Paper, WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core';
-import { AddIcon } from 'components/icon/icon';
+import { AddIcon, UserPanelIcon, KeyIcon } from 'components/icon/icon';
 import { getUserUuid } from 'common/getuser';
 import { GroupResource, isBuiltinGroup } from 'models/group';
 import { ArvadosTheme } from 'common/custom-theme';
 
-type CssRules = "root";
+type CssRules = "root" | "content";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         width: '100%',
+    },
+    content: {
+        // reserve space for the tab bar
+        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
     }
 });
 
@@ -44,6 +48,9 @@ export enum GroupDetailsPanelPermissionsColumnNames {
     REMOVE = "Remove",
 }
 
+const MEMBERS_DEFAULT_MESSAGE = 'Members list is empty.';
+const PERMISSIONS_DEFAULT_MESSAGE = 'Permissions list is empty.';
+
 export const groupDetailsMembersPanelColumns: DataColumns<string> = [
     {
         name: GroupDetailsPanelMembersColumnNames.FULL_NAME,
@@ -165,46 +172,52 @@ export const GroupDetailsPanel = withStyles(styles)(connect(
                       <Tab data-cy="group-details-members-tab" label="MEMBERS" />
                       <Tab data-cy="group-details-permissions-tab" label="PERMISSIONS" />
                   </Tabs>
-                  {value === 0 &&
-                      <DataExplorer
-                          id={GROUP_DETAILS_MEMBERS_PANEL_ID}
-                          data-cy="group-members-data-explorer"
-                          onRowClick={noop}
-                          onRowDoubleClick={noop}
-                          onContextMenu={noop}
-                          contextMenuColumn={false}
-                          hideColumnSelector
-                          hideSearchInput
-                          actions={
-                                this.props.groupCanManage &&
-                                <Grid container justify='flex-end'>
-                                    <Button
-                                      data-cy="group-member-add"
-                                      variant="contained"
-                                      color="primary"
-                                      onClick={this.props.onAddUser}>
-                                      <AddIcon /> Add user
-                                    </Button>
-                                </Grid>
-                          }
-                          paperProps={{
-                              elevation: 0,
-                          }} />
-                  }
-                  {value === 1 &&
-                      <DataExplorer
-                          id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
-                          data-cy="group-permissions-data-explorer"
-                          onRowClick={noop}
-                          onRowDoubleClick={noop}
-                          onContextMenu={noop}
-                          contextMenuColumn={false}
-                          hideColumnSelector
-                          hideSearchInput
-                          paperProps={{
-                              elevation: 0,
-                          }} />
-                  }
+                  <div className={this.props.classes.content}>
+                    {value === 0 &&
+                        <DataExplorer
+                            id={GROUP_DETAILS_MEMBERS_PANEL_ID}
+                            data-cy="group-members-data-explorer"
+                            onRowClick={noop}
+                            onRowDoubleClick={noop}
+                            onContextMenu={noop}
+                            contextMenuColumn={false}
+                            defaultViewIcon={UserPanelIcon}
+                            defaultViewMessages={[MEMBERS_DEFAULT_MESSAGE]}
+                            hideColumnSelector
+                            hideSearchInput
+                            actions={
+                                    this.props.groupCanManage &&
+                                    <Grid container justify='flex-end'>
+                                        <Button
+                                        data-cy="group-member-add"
+                                        variant="contained"
+                                        color="primary"
+                                        onClick={this.props.onAddUser}>
+                                        <AddIcon /> Add user
+                                        </Button>
+                                    </Grid>
+                            }
+                            paperProps={{
+                                elevation: 0,
+                            }} />
+                    }
+                    {value === 1 &&
+                        <DataExplorer
+                            id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
+                            data-cy="group-permissions-data-explorer"
+                            onRowClick={noop}
+                            onRowDoubleClick={noop}
+                            onContextMenu={noop}
+                            contextMenuColumn={false}
+                            defaultViewIcon={KeyIcon}
+                            defaultViewMessages={[PERMISSIONS_DEFAULT_MESSAGE]}
+                            hideColumnSelector
+                            hideSearchInput
+                            paperProps={{
+                                elevation: 0,
+                            }} />
+                    }
+                  </div>
                 </Paper>
             );
         }
diff --git a/src/views/link-panel/link-panel-root.tsx b/src/views/link-panel/link-panel-root.tsx
index b32208cd..c24d4637 100644
--- a/src/views/link-panel/link-panel-root.tsx
+++ b/src/views/link-panel/link-panel-root.tsx
@@ -7,7 +7,6 @@ import { LINK_PANEL_ID } from 'store/link-panel/link-panel-actions';
 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
 import { SortDirection } from 'components/data-table/data-column';
 import { DataColumns } from 'components/data-table/data-table';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { ResourcesState } from 'store/resources/resources';
 import { ShareMeIcon } from 'components/icon/icon';
 import { createTree } from 'models/tree';
@@ -94,9 +93,7 @@ export const LinkPanelRoot = withStyles(styles)((props: LinkPanelRootProps) => {
         contextMenuColumn={true}
         hideColumnSelector
         hideSearchInput
-        dataTableDefaultView={
-            <DataTableDefaultView
-                icon={ShareMeIcon}
-                messages={['Your link list is empty.']} />
-        }/></div>;
-});
\ No newline at end of file
+        defaultViewIcon={ShareMeIcon}
+        defaultViewMessages={['Your link list is empty.']} />
+    </div>;
+});
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index a5594d8e..ccb40d53 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -36,7 +36,6 @@ import {
 import { navigateTo } from 'store/navigation/navigation-action';
 import { getProperty } from 'store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { ArvadosTheme } from "common/custom-theme";
 import { createTree } from 'models/tree';
 import {
@@ -155,11 +154,9 @@ export const ProjectPanel = withStyles(styles)(
                         onRowDoubleClick={this.handleRowDoubleClick}
                         onContextMenu={this.handleContextMenu}
                         contextMenuColumn={true}
-                        dataTableDefaultView={
-                            <DataTableDefaultView
-                                icon={ProjectIcon}
-                                messages={DEFAULT_VIEW_MESSAGES} />
-                        } />
+                        defaultViewIcon={ProjectIcon}
+                        defaultViewMessages={DEFAULT_VIEW_MESSAGES}
+                    />
                 </div>;
             }
 
diff --git a/src/views/public-favorites-panel/public-favorites-panel.tsx b/src/views/public-favorites-panel/public-favorites-panel.tsx
index 9b1e9102..8eb2a87c 100644
--- a/src/views/public-favorites-panel/public-favorites-panel.tsx
+++ b/src/views/public-favorites-panel/public-favorites-panel.tsx
@@ -30,7 +30,6 @@ import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { ContainerRequestState } from "models/container-request";
 import { RootState } from 'store/store';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getSimpleObjectTypeFilters } from 'store/resource-type-filters/resource-type-filters';
 import { PUBLIC_FAVORITE_PANEL_ID } from 'store/public-favorites-panel/public-favorites-action';
@@ -169,11 +168,9 @@ export const PublicFavoritePanel = withStyles(styles)(
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu(this.props.resources)}
                     contextMenuColumn={true}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={PublicFavoriteIcon}
-                            messages={['Public favorites list is empty.']} />
-                    } /></div>;
+                    defaultViewIcon={PublicFavoriteIcon}
+                    defaultViewMessages={['Public favorites list is empty.']} />
+                </div>;
             }
         }
     )
diff --git a/src/views/shared-with-me-panel/shared-with-me-panel.tsx b/src/views/shared-with-me-panel/shared-with-me-panel.tsx
index 7ba9077c..e6cfccd2 100644
--- a/src/views/shared-with-me-panel/shared-with-me-panel.tsx
+++ b/src/views/shared-with-me-panel/shared-with-me-panel.tsx
@@ -12,7 +12,6 @@ import { ShareMeIcon } from 'components/icon/icon';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { navigateTo } from "store/navigation/navigation-action";
 import { loadDetailsPanel } from "store/details-panel/details-panel-action";
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { SHARED_WITH_ME_PANEL_ID } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
 import {
     openContextMenu,
@@ -55,11 +54,9 @@ export const SharedWithMePanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={false}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={ShareMeIcon}
-                            messages={['No shared items']} />
-                    } /></div>;
+                    defaultViewIcon={ShareMeIcon}
+                    defaultViewMessages={['No shared items']} />
+                </div>;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
diff --git a/src/views/subprocess-panel/subprocess-panel-root.tsx b/src/views/subprocess-panel/subprocess-panel-root.tsx
index 41a8f66b..d4ccae9c 100644
--- a/src/views/subprocess-panel/subprocess-panel-root.tsx
+++ b/src/views/subprocess-panel/subprocess-panel-root.tsx
@@ -13,7 +13,6 @@ import { ResourceCreatedAtDate, ProcessStatus, ContainerRunTime } from 'views-co
 import { ProcessIcon } from 'components/icon/icon';
 import { ResourceName } from 'views-components/data-explorer/renderers';
 import { SUBPROCESS_PANEL_ID } from 'store/subprocess-panel/subprocess-panel-actions';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
 import { ResourcesState } from 'store/resources/resources';
@@ -88,13 +87,10 @@ export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps)
         onRowDoubleClick={props.onItemDoubleClick}
         onContextMenu={(event, item) => props.onContextMenu(event, item, props.resources)}
         contextMenuColumn={true}
-        dataTableDefaultView={
-            <DataTableDefaultView
-                icon={ProcessIcon}
-                messages={DEFAULT_VIEW_MESSAGES} />
-        }
+        defaultViewIcon={ProcessIcon}
+        defaultViewMessages={DEFAULT_VIEW_MESSAGES}
         doHidePanel={props.doHidePanel}
         doMaximizePanel={props.doMaximizePanel}
         panelMaximized={props.panelMaximized}
         panelName={props.panelName} />;
-};
\ No newline at end of file
+};
diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx
index d303c2f7..67326829 100644
--- a/src/views/trash-panel/trash-panel.tsx
+++ b/src/views/trash-panel/trash-panel.tsx
@@ -30,7 +30,6 @@ import { loadDetailsPanel } from "store/details-panel/details-panel-action";
 import { toggleTrashed } from "store/trash/trash-actions";
 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 {
     getTrashPanelTypeFilters
@@ -155,11 +154,9 @@ export const TrashPanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={false}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={TrashIcon}
-                            messages={['Your trash list is empty.']}/>
-                    } /></div>;
+                    defaultViewIcon={TrashIcon}
+                    defaultViewMessages={['Your trash list is empty.']} />
+                </div>;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 589353cd..f2491dc2 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -20,7 +20,6 @@ import {
     UserResourceAccountStatus,
 } from "views-components/data-explorer/renderers";
 import { navigateToUserProfile } from "store/navigation/navigation-action";
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { compose, Dispatch } from 'redux';
 import { UserResource } from 'models/user';
@@ -148,11 +147,8 @@ export const UserPanel = compose(
                         paperProps={{
                             elevation: 0,
                         }}
-                        dataTableDefaultView={
-                            <DataTableDefaultView
-                                icon={ShareMeIcon}
-                                messages={['Your user list is empty.']} />
-                        } />
+                        defaultViewIcon={ShareMeIcon}
+                        defaultViewMessages={['Your user list is empty.']} />
                 </Paper>;
             }
 
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 1c8b1da7..53c0799f 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -24,7 +24,6 @@ import {
     IconButton,
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
@@ -327,11 +326,8 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     paperProps={{
                                         elevation: 0,
                                     }}
-                                    dataTableDefaultView={
-                                        <DataTableDefaultView
-                                            icon={GroupsIcon}
-                                            messages={['Group list is empty.']} />
-                                    } />
+                                    defaultViewIcon={GroupsIcon}
+                                    defaultViewMessages={['Group list is empty.']} />
                         </div>}
                 </Paper >;
             }
diff --git a/src/views/workflow-panel/workflow-panel-view.tsx b/src/views/workflow-panel/workflow-panel-view.tsx
index ca84a0fc..44e14fd3 100644
--- a/src/views/workflow-panel/workflow-panel-view.tsx
+++ b/src/views/workflow-panel/workflow-panel-view.tsx
@@ -5,7 +5,6 @@
 import React from 'react';
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { WorkflowIcon } from 'components/icon/icon';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { WORKFLOW_PANEL_ID } from 'store/workflow-panel/workflow-panel-actions';
 import {
     ResourceLastModifiedDate,
@@ -131,7 +130,8 @@ export const WorkflowPanelView = (props: WorkflowPanelProps) => {
                 onRowDoubleClick={props.handleRowDoubleClick}
                 contextMenuColumn={false}
                 onContextMenu={e => e}
-                dataTableDefaultView={<DataTableDefaultView icon={WorkflowIcon} />} />
+                defaultViewIcon={WorkflowIcon}
+                defaultViewMessages={['Workflow list is empty.']} />
         </Grid>
         <Grid item xs={6}>
             <Paper style={{ height: '100%' }}>

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list